diff --git a/.buildkite/ftr_configs.yml b/.buildkite/ftr_configs.yml index b792825bd9dcf..ac9ccb04c68e2 100644 --- a/.buildkite/ftr_configs.yml +++ b/.buildkite/ftr_configs.yml @@ -192,6 +192,7 @@ enabled: - x-pack/test/api_integration/apis/asset_manager/config_when_disabled.ts - x-pack/test/api_integration/apis/asset_manager/config_when_enabled.ts - x-pack/test/api_integration/apis/cases/config.ts + - x-pack/test/api_integration/apis/content_management/config.ts - x-pack/test/api_integration/apis/cloud_security_posture/config.ts - x-pack/test/api_integration/apis/console/config.ts - x-pack/test/api_integration/apis/es/config.ts diff --git a/.buildkite/pipeline-resource-definitions/kibana-apis-capacity-testing-daily.yml b/.buildkite/pipeline-resource-definitions/kibana-apis-capacity-testing-daily.yml new file mode 100644 index 0000000000000..c52e6203485f4 --- /dev/null +++ b/.buildkite/pipeline-resource-definitions/kibana-apis-capacity-testing-daily.yml @@ -0,0 +1,49 @@ +# yaml-language-server: $schema=https://gist.githubusercontent.com/elasticmachine/988b80dae436cafea07d9a4a460a011d/raw/rre.schema.json +apiVersion: backstage.io/v1alpha1 +kind: Resource +metadata: + name: bk-kibana-apis-capacity-testing + description: Runs capacity tests for Kibana apis + links: + - url: 'https://buildkite.com/elastic/kibana-apis-capacity-testing' + title: Pipeline link +spec: + type: buildkite-pipeline + owner: 'group:kibana-operations' + system: buildkite + implementation: + apiVersion: buildkite.elastic.dev/v1 + kind: Pipeline + metadata: + name: kibana / apis-capacity-testing + description: Runs capacity tests for Kibana apis + spec: + env: + SLACK_NOTIFICATIONS_CHANNEL: '#kibana-performance-alerts' + BAZEL_CACHE_MODE: none + ELASTIC_SLACK_NOTIFICATIONS_ENABLED: 'true' + allow_rebuilds: true + branch_configuration: main + default_branch: main + repository: elastic/kibana + pipeline_file: .buildkite/pipelines/scalability/api_capacity_testing_daily.yml + skip_intermediate_builds: false + provider_settings: + trigger_mode: none + build_branches: true + prefix_pull_request_fork_branch_names: true + skip_pull_request_builds_for_existing_commits: true + teams: + everyone: + access_level: BUILD_AND_READ + kibana-operations: + access_level: MANAGE_BUILD_AND_READ + appex-qa: + access_level: MANAGE_BUILD_AND_READ + kibana-tech-leads: + access_level: MANAGE_BUILD_AND_READ + schedules: + Capacity every 3h testing: + cronline: 0 1/3 * * * Europe/Berlin + message: Capacity every 3h testing + branch: main diff --git a/.buildkite/pipeline-resource-definitions/kibana-performance-daily.yml b/.buildkite/pipeline-resource-definitions/kibana-performance-daily.yml new file mode 100644 index 0000000000000..9ed561c9cfdbe --- /dev/null +++ b/.buildkite/pipeline-resource-definitions/kibana-performance-daily.yml @@ -0,0 +1,50 @@ +# yaml-language-server: $schema=https://gist.githubusercontent.com/elasticmachine/988b80dae436cafea07d9a4a460a011d/raw/rre.schema.json +apiVersion: backstage.io/v1alpha1 +kind: Resource +metadata: + name: bk-kibana-single-user-performance + description: Runs single user performance tests for kibana + links: + - url: 'https://buildkite.com/elastic/kibana-single-user-performance' + title: Pipeline link +spec: + type: buildkite-pipeline + owner: 'group:kibana-operations' + system: buildkite + implementation: + apiVersion: buildkite.elastic.dev/v1 + kind: Pipeline + metadata: + name: kibana / single-user-performance + description: Runs single user performance tests for kibana + spec: + env: + SLACK_NOTIFICATIONS_CHANNEL: '#kibana-performance-alerts' + BAZEL_CACHE_MODE: none + ELASTIC_SLACK_NOTIFICATIONS_ENABLED: 'true' + allow_rebuilds: true + branch_configuration: main + default_branch: main + repository: elastic/kibana + pipeline_file: .buildkite/pipelines/performance/daily.yml + skip_intermediate_builds: false + provider_settings: + trigger_mode: none + build_branches: true + prefix_pull_request_fork_branch_names: true + publish_commit_status: false + skip_pull_request_builds_for_existing_commits: true + teams: + everyone: + access_level: BUILD_AND_READ + kibana-operations: + access_level: MANAGE_BUILD_AND_READ + appex-qa: + access_level: MANAGE_BUILD_AND_READ + kibana-tech-leads: + access_level: MANAGE_BUILD_AND_READ + schedules: + Single user daily test: + cronline: 0 */3 * * * Europe/Berlin + message: Single user daily test + branch: main diff --git a/.buildkite/pipeline-resource-definitions/kibana-performance-data-set-extraction-daily.yml b/.buildkite/pipeline-resource-definitions/kibana-performance-data-set-extraction-daily.yml new file mode 100644 index 0000000000000..aa38564fd963b --- /dev/null +++ b/.buildkite/pipeline-resource-definitions/kibana-performance-data-set-extraction-daily.yml @@ -0,0 +1,49 @@ +# yaml-language-server: $schema=https://gist.githubusercontent.com/elasticmachine/988b80dae436cafea07d9a4a460a011d/raw/rre.schema.json +apiVersion: backstage.io/v1alpha1 +kind: Resource +metadata: + name: bk-kibana-performance-data-set-extraction + description: Runs single user performance tests and extract APM traces + links: + - url: 'https://buildkite.com/elastic/kibana-performance-data-set-extraction' + title: Pipeline link +spec: + type: buildkite-pipeline + owner: 'group:kibana-operations' + system: buildkite + implementation: + apiVersion: buildkite.elastic.dev/v1 + kind: Pipeline + metadata: + name: kibana / performance-data-set-extraction + description: Runs single user performance tests and extract APM traces + spec: + env: + SLACK_NOTIFICATIONS_CHANNEL: '#kibana-performance-alerts' + BAZEL_CACHE_MODE: none + ELASTIC_SLACK_NOTIFICATIONS_ENABLED: 'true' + allow_rebuilds: true + branch_configuration: main + default_branch: main + repository: elastic/kibana + pipeline_file: .buildkite/pipelines/performance/data_set_extraction_daily.yml + skip_intermediate_builds: false + provider_settings: + trigger_mode: none + build_branches: true + prefix_pull_request_fork_branch_names: true + skip_pull_request_builds_for_existing_commits: true + teams: + everyone: + access_level: BUILD_AND_READ + kibana-operations: + access_level: MANAGE_BUILD_AND_READ + appex-qa: + access_level: MANAGE_BUILD_AND_READ + kibana-tech-leads: + access_level: MANAGE_BUILD_AND_READ + schedules: + Extract APM traces: + cronline: 0 3/8 * * * Europe/Berlin + message: Extract APM traces + branch: main diff --git a/.buildkite/pipeline-resource-definitions/locations.yml b/.buildkite/pipeline-resource-definitions/locations.yml index 2cc1439713e48..7e1d136589fc1 100644 --- a/.buildkite/pipeline-resource-definitions/locations.yml +++ b/.buildkite/pipeline-resource-definitions/locations.yml @@ -8,6 +8,7 @@ spec: type: url targets: - https://github.com/elastic/kibana/blob/main/.buildkite/pipeline-resource-definitions/kibana-api-docs.yml + - https://github.com/elastic/kibana/blob/main/.buildkite/pipeline-resource-definitions/kibana-apis-capacity-testing-daily.yml - https://github.com/elastic/kibana/blob/main/.buildkite/pipeline-resource-definitions/kibana-coverage-daily.yml - https://github.com/elastic/kibana/blob/main/.buildkite/pipeline-resource-definitions/kibana-es-serverless-snapshots.yml - https://github.com/elastic/kibana/blob/main/.buildkite/pipeline-resource-definitions/kibana-es-snapshots.yml @@ -17,9 +18,12 @@ spec: - https://github.com/elastic/kibana/blob/main/.buildkite/pipeline-resource-definitions/kibana-fleet-packages-daily.yml - https://github.com/elastic/kibana/blob/main/.buildkite/pipeline-resource-definitions/kibana-migration-staging.yml - https://github.com/elastic/kibana/blob/main/.buildkite/pipeline-resource-definitions/kibana-on-merge-unsupported-ftrs.yml + - https://github.com/elastic/kibana/blob/main/.buildkite/pipeline-resource-definitions/kibana-performance-daily.yml + - https://github.com/elastic/kibana/blob/main/.buildkite/pipeline-resource-definitions/kibana-performance-data-set-extraction-daily.yml - https://github.com/elastic/kibana/blob/main/.buildkite/pipeline-resource-definitions/kibana-purge-cloud-deployments.yml - https://github.com/elastic/kibana/blob/main/.buildkite/pipeline-resource-definitions/kibana-serverless-release.yml - https://github.com/elastic/kibana/blob/main/.buildkite/pipeline-resource-definitions/kibana-serverless-security-solution-quality-gate-api-integration.yml + - https://github.com/elastic/kibana/blob/main/.buildkite/pipeline-resource-definitions/scalability_testing-daily.yml - https://github.com/elastic/kibana/blob/main/.buildkite/pipeline-resource-definitions/security-solution-ess/security-solution-ess.yml - https://github.com/elastic/kibana/blob/main/.buildkite/pipeline-resource-definitions/security-solution-quality-gate/kibana-serverless-security-solution-quality-gate-defend-workflows.yml - https://github.com/elastic/kibana/blob/main/.buildkite/pipeline-resource-definitions/security-solution-quality-gate/kibana-serverless-security-solution-quality-gate-detection-engine.yml diff --git a/.buildkite/pipeline-resource-definitions/scalability_testing-daily.yml b/.buildkite/pipeline-resource-definitions/scalability_testing-daily.yml new file mode 100644 index 0000000000000..06f2f2dd6634b --- /dev/null +++ b/.buildkite/pipeline-resource-definitions/scalability_testing-daily.yml @@ -0,0 +1,49 @@ +# yaml-language-server: $schema=https://gist.githubusercontent.com/elasticmachine/988b80dae436cafea07d9a4a460a011d/raw/rre.schema.json +apiVersion: backstage.io/v1alpha1 +kind: Resource +metadata: + name: bk-kibana-scalability-benchmarking + description: Runs scalability tests for Kibana server + links: + - url: 'https://buildkite.com/elastic/kibana-scalability-benchmarking' + title: Pipeline link +spec: + type: buildkite-pipeline + owner: 'group:kibana-operations' + system: buildkite + implementation: + apiVersion: buildkite.elastic.dev/v1 + kind: Pipeline + metadata: + name: kibana / scalability-benchmarking + description: Runs scalability tests for Kibana server + spec: + env: + SLACK_NOTIFICATIONS_CHANNEL: '#kibana-performance-alerts' + BAZEL_CACHE_MODE: none + ELASTIC_SLACK_NOTIFICATIONS_ENABLED: 'true' + allow_rebuilds: true + branch_configuration: main + default_branch: main + repository: elastic/kibana + pipeline_file: .buildkite/pipelines/scalability/daily.yml + skip_intermediate_builds: false + provider_settings: + trigger_mode: none + build_branches: true + prefix_pull_request_fork_branch_names: true + skip_pull_request_builds_for_existing_commits: true + teams: + everyone: + access_level: BUILD_AND_READ + kibana-operations: + access_level: MANAGE_BUILD_AND_READ + appex-qa: + access_level: MANAGE_BUILD_AND_READ + kibana-tech-leads: + access_level: MANAGE_BUILD_AND_READ + schedules: + Scalability daily benchmarking: + cronline: 0 6 * * * Europe/Berlin + message: Scalability daily benchmarking + branch: main diff --git a/.buildkite/pipelines/performance/daily.yml b/.buildkite/pipelines/performance/daily.yml index a58deb281d2c5..a9a350729e5c6 100644 --- a/.buildkite/pipelines/performance/daily.yml +++ b/.buildkite/pipelines/performance/daily.yml @@ -2,14 +2,20 @@ steps: - label: '👨‍🔧 Pre-Build' command: .buildkite/scripts/lifecycle/pre_build.sh agents: - queue: kibana-default + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-qa + provider: gcp + machineType: n2-standard-2 - wait - label: '🧑‍🏭 Build Kibana Distribution and Plugins' command: .buildkite/scripts/steps/build_kibana.sh agents: - queue: c2-16 + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-qa + provider: gcp + machineType: c2-standard-16 key: build if: "build.env('KIBANA_BUILD_ID') == null || build.env('KIBANA_BUILD_ID') == ''" @@ -26,7 +32,12 @@ steps: - label: '📈 Report performance metrics to ci-stats' command: .buildkite/scripts/steps/functional/report_performance_metrics.sh agents: - queue: n2-2 + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-qa + provider: gcp + localSsds: 1 + localSsdInterface: nvme + machineType: n2-standard-2 depends_on: tests - wait: ~ @@ -35,4 +46,7 @@ steps: - label: '🦸 Post-Build' command: .buildkite/scripts/lifecycle/post_build.sh agents: - queue: kibana-default + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-qa + provider: gcp + machineType: n2-standard-2 diff --git a/.buildkite/pipelines/performance/data_set_extraction_daily.yml b/.buildkite/pipelines/performance/data_set_extraction_daily.yml index 39ebad2757f59..34a5c680988dd 100644 --- a/.buildkite/pipelines/performance/data_set_extraction_daily.yml +++ b/.buildkite/pipelines/performance/data_set_extraction_daily.yml @@ -2,7 +2,10 @@ steps: - label: ':male-mechanic::skin-tone-2: Pre-Build' command: .buildkite/scripts/lifecycle/pre_build.sh agents: - queue: kibana-default + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-qa + provider: gcp + machineType: n2-standard-2 timeout_in_minutes: 10 - wait @@ -10,14 +13,21 @@ steps: - label: ':building_construction: Build Kibana Distribution and Plugins' command: .buildkite/scripts/steps/build_kibana.sh agents: - queue: c2-16 + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-qa + provider: gcp + machineType: c2-standard-16 key: build if: "build.env('KIBANA_BUILD_ID') == null || build.env('KIBANA_BUILD_ID') == ''" - label: ':kibana: Performance Tests with Playwright config' command: .buildkite/scripts/steps/functional/performance_playwright.sh agents: - queue: n2-2-spot + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-qa + provider: gcp + machineType: n2-standard-2 + preemptible: true depends_on: build key: tests timeout_in_minutes: 90 @@ -31,7 +41,12 @@ steps: - label: ':ship: Single user journeys dataset extraction for scalability benchmarking' command: .buildkite/scripts/steps/functional/scalability_dataset_extraction.sh agents: - queue: n2-2 + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-qa + provider: gcp + localSsds: 1 + localSsdInterface: nvme + machineType: n2-standard-2 depends_on: tests - wait: ~ @@ -40,4 +55,7 @@ steps: - label: ':male_superhero::skin-tone-2: Post-Build' command: .buildkite/scripts/lifecycle/post_build.sh agents: - queue: kibana-default + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-qa + provider: gcp + machineType: n2-standard-2 diff --git a/.buildkite/pipelines/pull_request/base.yml b/.buildkite/pipelines/pull_request/base.yml index 5a4d7afcaa128..e36e3ab5f59dd 100644 --- a/.buildkite/pipelines/pull_request/base.yml +++ b/.buildkite/pipelines/pull_request/base.yml @@ -70,6 +70,16 @@ steps: - exit_status: '-1' limit: 3 + - command: .buildkite/scripts/steps/check_types.sh + label: 'Check Types' + agents: + queue: n2-4-spot + timeout_in_minutes: 60 + retry: + automatic: + - exit_status: '-1' + limit: 3 + - command: .buildkite/scripts/steps/lint_with_types.sh label: 'Linting (with types)' agents: diff --git a/.buildkite/pipelines/scalability/api_capacity_testing_daily.yml b/.buildkite/pipelines/scalability/api_capacity_testing_daily.yml index 35fba7584bd4f..cc20be1f025c0 100644 --- a/.buildkite/pipelines/scalability/api_capacity_testing_daily.yml +++ b/.buildkite/pipelines/scalability/api_capacity_testing_daily.yml @@ -2,14 +2,21 @@ steps: - label: 'Pre-Build' command: .buildkite/scripts/lifecycle/pre_build.sh agents: - queue: kibana-default + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-qa + provider: gcp + machineType: n2-standard-2 - wait - label: 'Build Kibana Distribution and Plugins' command: .buildkite/scripts/steps/build_kibana.sh agents: - queue: n2-16-spot + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-qa + provider: gcp + machineType: n2-standard-16 + preemptible: true key: build if: "build.env('KIBANA_BUILD_ID') == null || build.env('KIBANA_BUILD_ID') == ''" timeout_in_minutes: 60 @@ -35,4 +42,7 @@ steps: - label: 'Post-Build' command: .buildkite/scripts/lifecycle/post_build.sh agents: - queue: kibana-default + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-qa + provider: gcp + machineType: n2-standard-2 diff --git a/.buildkite/pipelines/scalability/daily.yml b/.buildkite/pipelines/scalability/daily.yml index d88c6c00e7383..8499212347c56 100644 --- a/.buildkite/pipelines/scalability/daily.yml +++ b/.buildkite/pipelines/scalability/daily.yml @@ -2,7 +2,10 @@ steps: - label: ':male-mechanic::skin-tone-2: Pre-Build' command: .buildkite/scripts/lifecycle/pre_build.sh agents: - queue: kibana-default + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-qa + provider: gcp + machineType: n2-standard-2 timeout_in_minutes: 10 - wait @@ -23,5 +26,8 @@ steps: - label: ':male_superhero::skin-tone-2: Post-Build' command: .buildkite/scripts/lifecycle/post_build.sh agents: - queue: kibana-default + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-qa + provider: gcp + machineType: n2-standard-2 timeout_in_minutes: 10 diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index e85da3abf6ec2..73712f259aa02 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -51,7 +51,8 @@ packages/kbn-apm-synthtrace-client @elastic/obs-ux-infra_services-team @elastic/ packages/kbn-apm-utils @elastic/obs-ux-infra_services-team test/plugin_functional/plugins/app_link_test @elastic/kibana-core x-pack/test/usage_collection/plugins/application_usage_test @elastic/kibana-core -x-pack/plugins/asset_manager @elastic/obs-knowledge-team +x-pack/plugins/observability_solution/asset_manager @elastic/obs-knowledge-team +x-pack/plugins/observability_solution/assets_data_access @elastic/obs-knowledge-team x-pack/test/security_api_integration/plugins/audit_log @elastic/kibana-security packages/kbn-axe-config @elastic/kibana-qa packages/kbn-babel-preset @elastic/kibana-operations @@ -555,7 +556,7 @@ x-pack/examples/third_party_maps_source_example @elastic/kibana-gis src/plugins/maps_ems @elastic/kibana-gis x-pack/plugins/maps @elastic/kibana-gis x-pack/packages/maps/vector_tile_utils @elastic/kibana-gis -x-pack/plugins/metrics_data_access @elastic/obs-knowledge-team @elastic/obs-ux-infra_services-team +x-pack/plugins/observability_solution/metrics_data_access @elastic/obs-knowledge-team @elastic/obs-ux-infra_services-team x-pack/packages/ml/agg_utils @elastic/ml-ui x-pack/packages/ml/anomaly_utils @elastic/ml-ui x-pack/packages/ml/cancellable_search @elastic/ml-ui diff --git a/config/serverless.es.yml b/config/serverless.es.yml index 1f7f200c71a13..c7d5044ac2c2c 100644 --- a/config/serverless.es.yml +++ b/config/serverless.es.yml @@ -46,4 +46,4 @@ xpack.ml.compatibleModuleType: 'search' data_visualizer.resultLinks.fileBeat.enabled: false # Search Playground -xpack.searchPlayground.ui.enabled: false +xpack.searchPlayground.ui.enabled: true diff --git a/config/serverless.yml b/config/serverless.yml index 7e18b184468ef..3324fa53727b4 100644 --- a/config/serverless.yml +++ b/config/serverless.yml @@ -48,6 +48,10 @@ xpack.index_management.enableIndexStats: false xpack.index_management.editableIndexSettings: limited # Disable Storage size column in the Data streams table from Index Management UI xpack.index_management.enableDataStreamsStorageColumn: false +# Disable _source field in the Mappings editor's advanced options form from Index Management UI +xpack.index_management.enableMappingsSourceFieldSection: false +# Disable toggle for enabling data retention in DSL form from Index Management UI +xpack.index_management.enableTogglingDataRetention: false # Keep deeplinks visible so that they are shown in the sidenav dev_tools.deeplinks.navLinkStatus: visible @@ -136,7 +140,7 @@ xpack.actions.queued.max: 10000 # Disables ESQL in advanced settings (hides it from the UI) uiSettings: overrides: - discover:enableESQL: false + enableESQL: true # Task Manager xpack.task_manager.allow_reading_invalid_state: false diff --git a/dev_docs/tutorials/configuring_cross_cluster_search.mdx b/dev_docs/tutorials/configuring_cross_cluster_search.mdx new file mode 100644 index 0000000000000..eed00cf0becf3 --- /dev/null +++ b/dev_docs/tutorials/configuring_cross_cluster_search.mdx @@ -0,0 +1,40 @@ +--- +id: kibDevTutorialCcsSetup +slug: /kibana-dev-docs/tutorials/cross-cluster-search +title: Local cross cluster search setup +description: Local cross cluster search setup +date: 2024-04-29 +tags: ['kibana', 'onboarding', 'dev' ] +--- + +### Local CCS (cross cluster search) instructions +* Spin up a "remote" instance of Elasticsearch (this needs to start before the "local" instance for some odd reason): +``` +yarn es snapshot -E http.port=9500 -E transport.port=9600 -E path.data=../remote +``` + +* Spin up the "local" instance of Elasticsearch: +``` +yarn es snapshot +``` + +* Load data into both the "remote" and "local" clusters: +``` +node scripts/makelogs.js -c 100000 -d 100/10 --url elastic:changeme@localhost:9500 +node scripts/makelogs.js -c 100000 -d 100/10 --url elastic:changeme@localhost:9200 +``` + +* Spin up Kibana: +``` +yarn start +``` + +Once it is running, do the following: +* Open http://localhost:5601/app/management/data/remote_clusters +* Click "Add a remote cluster" +* Choose a name, put "localhost:9600" for "Seed nodes", and save (check "Yes, I have setup trust") +* Make sure the connection status is "Connected" +* Open http://localhost:5601/app/management/kibana/dataViews +* Click "Create data view" +* For "Index pattern", put "logstash-\*,\*:logstash-\*" (the asterisk indicates all remote clusters) and save +* Go to Discover and select your data view, and it should be querying using CCS \ No newline at end of file diff --git a/docs/CHANGELOG.asciidoc b/docs/CHANGELOG.asciidoc index 36cda12ee15da..1e2097639cd30 100644 --- a/docs/CHANGELOG.asciidoc +++ b/docs/CHANGELOG.asciidoc @@ -10,6 +10,7 @@ Review important information about the {kib} 8.x releases. +* <> * <> * <> * <> @@ -63,6 +64,36 @@ Review important information about the {kib} 8.x releases. * <> -- + +[[release-notes-8.13.3]] +== {kib} 8.13.3 + +The 8.13.3 release includes the following bug fixes. + +[float] +[[fixes-v8.13.3]] +=== Bug Fixes + +Alerting:: +* Manage loading fields at initialization ({kibana-pull}180412[#180412]). +Elastic Security:: +For the Elastic Security 8.13.3 release information, refer to {security-guide}/release-notes.html[_Elastic Security Solution Release Notes_]. +Fleet:: +* Fixes managed agent policy preconfiguration update ({kibana-pull}181624[#181624]). +* Use lowercase dataset in template names ({kibana-pull}180887[#180887]). +* Fixes KQL/kuery for getting Fleet Server agent count ({kibana-pull}180650[#180650]). +Lens & Visualizations:: +* Fixes table sorting on time picker interval change in *Lens* ({kibana-pull}182173[#182173]). +* Fixes controls on fields with custom label ({kibana-pull}180615[#180615]). +Machine Learning:: +* Fixes deep link for Index data visualizer & ES|QL data visualizer ({kibana-pull}180389[#180389]). +Observability:: +* Make anomalyDetectorTypes optional ({kibana-pull}180717[#180717]). +SharedUX:: +* Revert change to shared UX markdown component for dashboard vis ({kibana-pull}180906[#180906]). +Sharing:: +* Default to saved object description when panel description is not provided ({kibana-pull}181177[#181177]). + [[release-notes-8.13.2]] == {kib} 8.13.2 diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index 3bbc594a21c5a..465c57ed09b38 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -462,10 +462,14 @@ The plugin exposes the static DefaultEditorController class to consume. |WARNING: Missing README. -|{kib-repo}blob/{branch}/x-pack/plugins/asset_manager/README.md[assetManager] +|{kib-repo}blob/{branch}/x-pack/plugins/observability_solution/asset_manager/README.md[assetManager] |This plugin provides access to observed asset data, such as information about hosts, pods, containers, services, and more. +|{kib-repo}blob/{branch}/x-pack/plugins/observability_solution/assets_data_access[assetsDataAccess] +|WARNING: Missing README. + + |{kib-repo}blob/{branch}/x-pack/plugins/banners/README.md[banners] |Allow to add a header banner that will be displayed on every page of the Kibana application @@ -659,7 +663,7 @@ using the CURL scripts in the scripts folder. |Visualize geo data from Elasticsearch or 3rd party geo-services. -|{kib-repo}blob/{branch}/x-pack/plugins/metrics_data_access/README.md[metricsDataAccess] +|{kib-repo}blob/{branch}/x-pack/plugins/observability_solution/metrics_data_access/README.md[metricsDataAccess] |Exposes utilities to access metrics data. diff --git a/docs/discover/try-esql.asciidoc b/docs/discover/try-esql.asciidoc index 6c827240c1832..c95733fd42e6b 100644 --- a/docs/discover/try-esql.asciidoc +++ b/docs/discover/try-esql.asciidoc @@ -11,7 +11,7 @@ In this tutorial we'll use the {kib} sample web logs in Discover and Lens to exp [[prerequisite]] === Prerequisite -To be able to select **Try {esql}** from the Data views menu the `discover:enableESQL` setting must be enabled from **Stack Management > Advanced Settings**. It is enabled by default. +To be able to select **Try {esql}** from the Data views menu the `enableESQL` setting must be enabled from **Stack Management > Advanced Settings**. It is enabled by default. [float] [[tutorial-try-esql]] diff --git a/docs/management/advanced-options.asciidoc b/docs/management/advanced-options.asciidoc index 9e400f01c88dd..3501b440d13ba 100644 --- a/docs/management/advanced-options.asciidoc +++ b/docs/management/advanced-options.asciidoc @@ -209,6 +209,9 @@ The default selection in the time filter. The maximum height that a cell occupies in a table. Set to 0 to disable truncation. +[[enableESQL]]`enableESQL`:: +This setting enables ES|QL in Kibana. + [float] [[presentation-labs]] ==== Presentation Labs @@ -290,9 +293,6 @@ in the current data view is used. The columns that appear by default on the *Discover* page. The default is `_source`. -[[discover:enableESQL]]`discover:enableESQL`:: -experimental[] Allows ES|QL queries for search. - [[discover-max-doc-fields-displayed]]`discover:maxDocFieldsDisplayed`:: Specifies the maximum number of fields to show in the document column of the *Discover* table. diff --git a/docs/setup/settings.asciidoc b/docs/setup/settings.asciidoc index 9d5cedff34c84..a1f0a4ebed8a4 100644 --- a/docs/setup/settings.asciidoc +++ b/docs/setup/settings.asciidoc @@ -14,8 +14,11 @@ KBN_PATH_CONF=/home/kibana/config ./bin/kibana -- The default host and port settings configure {kib} to run on `localhost:5601`. To change this behavior and allow remote users to connect, you'll need to update your `kibana.yml` file. You can also enable SSL and set a -variety of other options. Finally, environment variables can be injected into -configuration using `${MY_ENV_VAR}` syntax. +variety of other options. + +Environment variables can be injected into configuration using `${MY_ENV_VAR}` syntax. By default, configuration validation +will fail if an environment variable used in the config file is not present when Kibana starts. This behavior can be changed by using a default value +for the environment variable, using the `${MY_ENV_VAR:defaultValue}` syntax. `console.ui.enabled`:: Toggling this causes the server to regenerate assets on the next startup, diff --git a/docs/user/alerting/images/rule-types-index-threshold-select.png b/docs/user/alerting/images/rule-types-index-threshold-select.png deleted file mode 100644 index 98866694e6e36..0000000000000 Binary files a/docs/user/alerting/images/rule-types-index-threshold-select.png and /dev/null differ diff --git a/docs/user/alerting/rule-types/es-query.asciidoc b/docs/user/alerting/rule-types/es-query.asciidoc index 99c0e6f965306..17e50ceb40b66 100644 --- a/docs/user/alerting/rule-types/es-query.asciidoc +++ b/docs/user/alerting/rule-types/es-query.asciidoc @@ -12,7 +12,8 @@ The {es} query rule type runs a user-configured query, compares the number of matches to a configured threshold, and schedules actions to run when the threshold condition is met. -In *{stack-manage-app}* > *{rules-ui}*, click *Create rule*, fill in the name and optional tags, then select *{es} query*. +In *{stack-manage-app}* > *{rules-ui}*, click *Create rule*. +Select the *{es} query* rule type then fill in the name and optional tags. An {es} query rule can be defined using {es} Query Domain Specific Language (DSL), {es} Query Language (ES|QL), {kib} Query Language (KQL), or Lucene. [float] diff --git a/docs/user/alerting/rule-types/geo-rule-types.asciidoc b/docs/user/alerting/rule-types/geo-rule-types.asciidoc index af26780a3a6aa..96851919e4b58 100644 --- a/docs/user/alerting/rule-types/geo-rule-types.asciidoc +++ b/docs/user/alerting/rule-types/geo-rule-types.asciidoc @@ -10,7 +10,8 @@ The tracking containment rule alerts when an entity is contained or no longer contained within a boundary. -In *{stack-manage-app}* > *{rules-ui}*, click *Create rule*, fill in the name and optional tags, then select *Tracking containment*. +In *{stack-manage-app}* > *{rules-ui}*, click *Create rule*. +Select the *Tracking containment* rule type then fill in the name and optional tags. [float] === Define the conditions diff --git a/docs/user/alerting/rule-types/index-threshold.asciidoc b/docs/user/alerting/rule-types/index-threshold.asciidoc index a5f7c79e1be74..2e40c6c3bbcda 100644 --- a/docs/user/alerting/rule-types/index-threshold.asciidoc +++ b/docs/user/alerting/rule-types/index-threshold.asciidoc @@ -10,7 +10,8 @@ The index threshold rule type runs an {es} query. It aggregates field values from documents, compares them to threshold values, and schedules actions to run when the thresholds are met. -In *{stack-manage-app}* > *{rules-ui}*, click *Create rule*, fill in the name and optional tags, then select *Index threshold*. +In *{stack-manage-app}* > *{rules-ui}*, click *Create rule*. +Select the *Index threshold* rule type then fill in the name and optional tags. [float] === Define the conditions @@ -107,15 +108,11 @@ You can also specify <>. In this example, you will use the {kib} <> to set up and tune the conditions on an index threshold rule. For this example, you want to detect when any of the top four sites serve more than 420,000 bytes over a 24 hour period. -. Open the main menu, then click *{stack-manage-app} > {rules-ui}*. +. Go to *{stack-manage-app} > {rules-ui}* and click *Create rule*. -. Create a new rule. +. Select the **Index threshold** rule type. -.. Provide a rule name and select the **Index threshold** rule type. -+ -[role="screenshot"] -image::user/alerting/images/rule-types-index-threshold-select.png[Choosing an index threshold rule type] -// NOTE: This is an autogenerated screenshot. Do not edit it directly. +.. Provide a rule name. .. Select an index. Click *Index*, and set *Indices to query* to `kibana_sample_data_logs`. Set the *Time field* to `@timestamp`. + diff --git a/examples/bfetch_explorer/public/components/page/index.tsx b/examples/bfetch_explorer/public/components/page/index.tsx index 6e282f528362b..07b7bf7a83209 100644 --- a/examples/bfetch_explorer/public/components/page/index.tsx +++ b/examples/bfetch_explorer/public/components/page/index.tsx @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import * as React from 'react'; +import React, { FC, PropsWithChildren } from 'react'; import { EuiPageTemplate, EuiPageSection, EuiPageHeader } from '@elastic/eui'; export interface PageProps { @@ -14,7 +14,11 @@ export interface PageProps { sidebar?: React.ReactNode; } -export const Page: React.FC = ({ title = 'Untitled', sidebar, children }) => { +export const Page: FC> = ({ + title = 'Untitled', + sidebar, + children, +}) => { return ( {sidebar} diff --git a/examples/bfetch_explorer/public/containers/app/pages/page_count_until/index.tsx b/examples/bfetch_explorer/public/containers/app/pages/page_count_until/index.tsx index b0ed74a570bb4..14112dc4ffcc9 100644 --- a/examples/bfetch_explorer/public/containers/app/pages/page_count_until/index.tsx +++ b/examples/bfetch_explorer/public/containers/app/pages/page_count_until/index.tsx @@ -13,10 +13,7 @@ import { Page } from '../../../../components/page'; import { useDeps } from '../../../../hooks/use_deps'; import { Sidebar } from '../../sidebar'; -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface Props {} - -export const PageCountUntil: React.FC = () => { +export const PageCountUntil = () => { const { plugins } = useDeps(); return ( diff --git a/examples/bfetch_explorer/public/containers/app/pages/page_double_integers/index.tsx b/examples/bfetch_explorer/public/containers/app/pages/page_double_integers/index.tsx index 0af8218708cbb..5a5ba21c638fc 100644 --- a/examples/bfetch_explorer/public/containers/app/pages/page_double_integers/index.tsx +++ b/examples/bfetch_explorer/public/containers/app/pages/page_double_integers/index.tsx @@ -13,10 +13,7 @@ import { Page } from '../../../../components/page'; import { useDeps } from '../../../../hooks/use_deps'; import { Sidebar } from '../../sidebar'; -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface Props {} - -export const PageDoubleIntegers: React.FC = () => { +export const PageDoubleIntegers = () => { const { explorer } = useDeps(); return ( diff --git a/examples/content_management_examples/public/examples/msearch/msearch_app.tsx b/examples/content_management_examples/public/examples/msearch/msearch_app.tsx index 2bb4ab1bf6fb9..290d2e3d7ca07 100644 --- a/examples/content_management_examples/public/examples/msearch/msearch_app.tsx +++ b/examples/content_management_examples/public/examples/msearch/msearch_app.tsx @@ -10,7 +10,6 @@ import React from 'react'; import { ContentClientProvider, type ContentClient } from '@kbn/content-management-plugin/public'; import { TableListViewKibanaProvider } from '@kbn/content-management-table-list-view-table'; import type { CoreStart } from '@kbn/core/public'; -import { toMountPoint } from '@kbn/kibana-react-plugin/public'; import { FormattedRelative, I18nProvider } from '@kbn/i18n-react'; import { SavedObjectTaggingOssPluginStart } from '@kbn/saved-objects-tagging-oss-plugin/public'; import { MSearchTable } from './msearch_table'; @@ -25,7 +24,6 @@ export const MSearchApp = (props: { diff --git a/examples/content_management_examples/tsconfig.json b/examples/content_management_examples/tsconfig.json index 938648544d067..c94193dc151cc 100644 --- a/examples/content_management_examples/tsconfig.json +++ b/examples/content_management_examples/tsconfig.json @@ -21,7 +21,6 @@ "@kbn/shared-ux-link-redirect-app", "@kbn/content-management-table-list-view", "@kbn/content-management-table-list-view-table", - "@kbn/kibana-react-plugin", "@kbn/i18n-react", "@kbn/saved-objects-tagging-oss-plugin", "@kbn/core-saved-objects-api-browser", diff --git a/examples/discover_customization_examples/public/plugin.tsx b/examples/discover_customization_examples/public/plugin.tsx index f4cc4a8bc1a1e..8afe18cbe9c92 100644 --- a/examples/discover_customization_examples/public/plugin.tsx +++ b/examples/discover_customization_examples/public/plugin.tsx @@ -433,7 +433,7 @@ export class DiscoverCustomizationExamplesPlugin implements Plugin { customizations.set({ id: 'flyout', - size: '60%', + size: 650, title: 'Example custom flyout', actions: { getActionItems: () => diff --git a/examples/embeddable_examples/public/app/render_examples.tsx b/examples/embeddable_examples/public/app/render_examples.tsx index c3e9b0a79a55b..f956a71711c7c 100644 --- a/examples/embeddable_examples/public/app/render_examples.tsx +++ b/examples/embeddable_examples/public/app/render_examples.tsx @@ -24,7 +24,7 @@ import { TimeRange } from '@kbn/es-query'; import { useBatchedOptionalPublishingSubjects } from '@kbn/presentation-publishing'; import { SearchEmbeddableRenderer } from '../react_embeddables/search/search_embeddable_renderer'; import { SEARCH_EMBEDDABLE_ID } from '../react_embeddables/search/constants'; -import type { Api, State } from '../react_embeddables/search/types'; +import type { SearchApi, SearchSerializedState } from '../react_embeddables/search/types'; export const RenderExamples = () => { const initialState = useMemo(() => { @@ -48,7 +48,7 @@ export const RenderExamples = () => { // only run onMount }, []); - const [api, setApi] = useState(null); + const [api, setApi] = useState(null); const [hidePanelChrome, setHidePanelChrome] = useState(false); const [dataLoading, timeRange] = useBatchedOptionalPublishingSubjects( api?.dataLoading, @@ -104,7 +104,7 @@ export const RenderExamples = () => { - + key={hidePanelChrome ? 'hideChrome' : 'showChrome'} type={SEARCH_EMBEDDABLE_ID} state={initialState} diff --git a/examples/embeddable_examples/public/react_embeddables/data_table/create_data_table_action.ts b/examples/embeddable_examples/public/react_embeddables/data_table/create_data_table_action.ts index 3d0c99289b121..9971535e148dd 100644 --- a/examples/embeddable_examples/public/react_embeddables/data_table/create_data_table_action.ts +++ b/examples/embeddable_examples/public/react_embeddables/data_table/create_data_table_action.ts @@ -10,7 +10,7 @@ import { i18n } from '@kbn/i18n'; import { apiIsPresentationContainer } from '@kbn/presentation-containers'; import { EmbeddableApiContext } from '@kbn/presentation-publishing'; import { IncompatibleActionError, UiActionsStart } from '@kbn/ui-actions-plugin/public'; -import { addPanelGrouping } from '../add_panel_grouping'; +import { embeddableExamplesGrouping } from '../embeddable_examples_grouping'; import { ADD_DATA_TABLE_ACTION_ID, DATA_TABLE_ID } from './constants'; // ----------------------------------------------------------------------------- @@ -20,7 +20,7 @@ import { ADD_DATA_TABLE_ACTION_ID, DATA_TABLE_ID } from './constants'; export const registerCreateDataTableAction = (uiActions: UiActionsStart) => { uiActions.registerAction({ id: ADD_DATA_TABLE_ACTION_ID, - grouping: [addPanelGrouping], + grouping: [embeddableExamplesGrouping], getIconType: () => 'tableDensityNormal', isCompatible: async ({ embeddable }) => { return apiIsPresentationContainer(embeddable); diff --git a/examples/embeddable_examples/public/react_embeddables/add_panel_grouping.ts b/examples/embeddable_examples/public/react_embeddables/embeddable_examples_grouping.ts similarity index 84% rename from examples/embeddable_examples/public/react_embeddables/add_panel_grouping.ts rename to examples/embeddable_examples/public/react_embeddables/embeddable_examples_grouping.ts index 2c043569d5cfb..fa2ecd03b5d25 100644 --- a/examples/embeddable_examples/public/react_embeddables/add_panel_grouping.ts +++ b/examples/embeddable_examples/public/react_embeddables/embeddable_examples_grouping.ts @@ -6,7 +6,8 @@ * Side Public License, v 1. */ -export const addPanelGrouping = { +export const embeddableExamplesGrouping = { id: 'embeddableExamples', + getIconType: () => 'documentation', getDisplayName: () => 'Embeddable examples', }; diff --git a/examples/embeddable_examples/public/react_embeddables/eui_markdown/create_eui_markdown_action.tsx b/examples/embeddable_examples/public/react_embeddables/eui_markdown/create_eui_markdown_action.tsx index 21306a4037262..81c23a4d960b8 100644 --- a/examples/embeddable_examples/public/react_embeddables/eui_markdown/create_eui_markdown_action.tsx +++ b/examples/embeddable_examples/public/react_embeddables/eui_markdown/create_eui_markdown_action.tsx @@ -10,8 +10,9 @@ import { i18n } from '@kbn/i18n'; import { apiCanAddNewPanel } from '@kbn/presentation-containers'; import { EmbeddableApiContext } from '@kbn/presentation-publishing'; import { IncompatibleActionError, UiActionsStart } from '@kbn/ui-actions-plugin/public'; -import { addPanelGrouping } from '../add_panel_grouping'; +import { embeddableExamplesGrouping } from '../embeddable_examples_grouping'; import { ADD_EUI_MARKDOWN_ACTION_ID, EUI_MARKDOWN_ID } from './constants'; +import { MarkdownEditorSerializedState } from './types'; // ----------------------------------------------------------------------------- // Create and register an action which allows this embeddable to be created from @@ -20,14 +21,14 @@ import { ADD_EUI_MARKDOWN_ACTION_ID, EUI_MARKDOWN_ID } from './constants'; export const registerCreateEuiMarkdownAction = (uiActions: UiActionsStart) => { uiActions.registerAction({ id: ADD_EUI_MARKDOWN_ACTION_ID, - grouping: [addPanelGrouping], + grouping: [embeddableExamplesGrouping], getIconType: () => 'editorCodeBlock', isCompatible: async ({ embeddable }) => { return apiCanAddNewPanel(embeddable); }, execute: async ({ embeddable }) => { if (!apiCanAddNewPanel(embeddable)) throw new IncompatibleActionError(); - embeddable.addNewPanel( + embeddable.addNewPanel( { panelType: EUI_MARKDOWN_ID, initialState: { content: '# hello world!' }, diff --git a/examples/embeddable_examples/public/react_embeddables/eui_markdown/eui_markdown_react_embeddable.tsx b/examples/embeddable_examples/public/react_embeddables/eui_markdown/eui_markdown_react_embeddable.tsx index 3a79952e16223..64cdf1cb06e08 100644 --- a/examples/embeddable_examples/public/react_embeddables/eui_markdown/eui_markdown_react_embeddable.tsx +++ b/examples/embeddable_examples/public/react_embeddables/eui_markdown/eui_markdown_react_embeddable.tsx @@ -26,12 +26,7 @@ export const markdownEmbeddableFactory: ReactEmbeddableFactory< MarkdownEditorApi > = { type: EUI_MARKDOWN_ID, - deserializeState: (state) => { - /** - * Here we can run clientside migrations and inject references. - */ - return state.rawState as MarkdownEditorSerializedState; - }, + deserializeState: (state) => state.rawState, /** * The buildEmbeddable function is async so you can async import the component or load a saved * object here. The loading will be handed gracefully by the Presentation Container. diff --git a/examples/embeddable_examples/public/react_embeddables/field_list/create_field_list_action.tsx b/examples/embeddable_examples/public/react_embeddables/field_list/create_field_list_action.tsx index 8e02aa6e385e0..e05868e7737d1 100644 --- a/examples/embeddable_examples/public/react_embeddables/field_list/create_field_list_action.tsx +++ b/examples/embeddable_examples/public/react_embeddables/field_list/create_field_list_action.tsx @@ -11,20 +11,21 @@ import { apiCanAddNewPanel } from '@kbn/presentation-containers'; import { EmbeddableApiContext } from '@kbn/presentation-publishing'; import { IncompatibleActionError } from '@kbn/ui-actions-plugin/public'; import { UiActionsPublicStart } from '@kbn/ui-actions-plugin/public/plugin'; +import { embeddableExamplesGrouping } from '../embeddable_examples_grouping'; import { ADD_FIELD_LIST_ACTION_ID, FIELD_LIST_ID } from './constants'; -import { addPanelGrouping } from '../add_panel_grouping'; +import { FieldListSerializedStateState } from './types'; export const registerCreateFieldListAction = (uiActions: UiActionsPublicStart) => { uiActions.registerAction({ id: ADD_FIELD_LIST_ACTION_ID, - grouping: [addPanelGrouping], + grouping: [embeddableExamplesGrouping], getIconType: () => 'indexOpen', isCompatible: async ({ embeddable }) => { return apiCanAddNewPanel(embeddable); }, execute: async ({ embeddable }) => { if (!apiCanAddNewPanel(embeddable)) throw new IncompatibleActionError(); - embeddable.addNewPanel({ + embeddable.addNewPanel({ panelType: FIELD_LIST_ID, }); }, diff --git a/examples/embeddable_examples/public/react_embeddables/field_list/field_list_react_embeddable.tsx b/examples/embeddable_examples/public/react_embeddables/field_list/field_list_react_embeddable.tsx index c7067cba6ce8c..0c7d3d127efb9 100644 --- a/examples/embeddable_examples/public/react_embeddables/field_list/field_list_react_embeddable.tsx +++ b/examples/embeddable_examples/public/react_embeddables/field_list/field_list_react_embeddable.tsx @@ -66,7 +66,7 @@ export const getFieldListFactory = ( > = { type: FIELD_LIST_ID, deserializeState: (state) => { - const serializedState = cloneDeep(state.rawState) as FieldListSerializedStateState; + const serializedState = cloneDeep(state.rawState); // inject the reference const dataViewIdRef = state.references?.find( (ref) => ref.name === FIELD_LIST_DATA_VIEW_REF_NAME diff --git a/examples/embeddable_examples/public/react_embeddables/search/register_add_search_panel_action.tsx b/examples/embeddable_examples/public/react_embeddables/search/register_add_search_panel_action.tsx index 391a9e3c370d4..18bd40fe69dae 100644 --- a/examples/embeddable_examples/public/react_embeddables/search/register_add_search_panel_action.tsx +++ b/examples/embeddable_examples/public/react_embeddables/search/register_add_search_panel_action.tsx @@ -9,13 +9,14 @@ import { apiCanAddNewPanel } from '@kbn/presentation-containers'; import { EmbeddableApiContext } from '@kbn/presentation-publishing'; import { IncompatibleActionError, UiActionsStart } from '@kbn/ui-actions-plugin/public'; -import { addPanelGrouping } from '../add_panel_grouping'; +import { embeddableExamplesGrouping } from '../embeddable_examples_grouping'; import { ADD_SEARCH_ACTION_ID, SEARCH_EMBEDDABLE_ID } from './constants'; +import { SearchSerializedState } from './types'; export const registerAddSearchPanelAction = (uiActions: UiActionsStart) => { uiActions.registerAction({ id: ADD_SEARCH_ACTION_ID, - grouping: [addPanelGrouping], + grouping: [embeddableExamplesGrouping], getDisplayName: () => 'Search example', getIconType: () => 'search', isCompatible: async ({ embeddable }) => { @@ -23,10 +24,9 @@ export const registerAddSearchPanelAction = (uiActions: UiActionsStart) => { }, execute: async ({ embeddable }) => { if (!apiCanAddNewPanel(embeddable)) throw new IncompatibleActionError(); - embeddable.addNewPanel( + embeddable.addNewPanel( { panelType: SEARCH_EMBEDDABLE_ID, - initialState: {}, }, true ); diff --git a/examples/embeddable_examples/public/react_embeddables/search/search_embeddable_renderer.tsx b/examples/embeddable_examples/public/react_embeddables/search/search_embeddable_renderer.tsx index 0fa6e785b72c1..39fa75234e3f4 100644 --- a/examples/embeddable_examples/public/react_embeddables/search/search_embeddable_renderer.tsx +++ b/examples/embeddable_examples/public/react_embeddables/search/search_embeddable_renderer.tsx @@ -10,7 +10,7 @@ import React, { useEffect, useMemo } from 'react'; import { BehaviorSubject } from 'rxjs'; import { TimeRange } from '@kbn/es-query'; import { ReactEmbeddableRenderer } from '@kbn/embeddable-plugin/public'; -import type { Api, State } from './types'; +import type { SearchApi, SearchSerializedState } from './types'; import { SEARCH_EMBEDDABLE_ID } from './constants'; interface Props { @@ -42,7 +42,7 @@ export function SearchEmbeddableRenderer(props: Props) { return (
- + type={SEARCH_EMBEDDABLE_ID} state={initialState} parentApi={parentApi} diff --git a/examples/embeddable_examples/public/react_embeddables/search/search_react_embeddable.tsx b/examples/embeddable_examples/public/react_embeddables/search/search_react_embeddable.tsx index e6fd182319631..2b789a17da4ae 100644 --- a/examples/embeddable_examples/public/react_embeddables/search/search_react_embeddable.tsx +++ b/examples/embeddable_examples/public/react_embeddables/search/search_react_embeddable.tsx @@ -6,26 +6,27 @@ * Side Public License, v 1. */ -import { EuiCallOut } from '@elastic/eui'; +import { EuiBadge, EuiStat } from '@elastic/eui'; +import { css } from '@emotion/react'; import { DataView } from '@kbn/data-views-plugin/common'; import { ReactEmbeddableFactory } from '@kbn/embeddable-plugin/public'; +import { i18n } from '@kbn/i18n'; import { - initializeTimeRange, fetch$, + initializeTimeRange, useBatchedPublishingSubjects, } from '@kbn/presentation-publishing'; +import { euiThemeVars } from '@kbn/ui-theme'; import React, { useEffect } from 'react'; import { BehaviorSubject, switchMap, tap } from 'rxjs'; import { SEARCH_EMBEDDABLE_ID } from './constants'; import { getCount } from './get_count'; -import { Api, Services, State } from './types'; +import { SearchApi, Services, SearchSerializedState } from './types'; export const getSearchEmbeddableFactory = (services: Services) => { - const factory: ReactEmbeddableFactory = { + const factory: ReactEmbeddableFactory = { type: SEARCH_EMBEDDABLE_ID, - deserializeState: (state) => { - return state.rawState as State; - }, + deserializeState: (state) => state.rawState, buildEmbeddable: async (state, buildApi, uuid, parentApi) => { const timeRange = initializeTimeRange(state); const defaultDataView = await services.dataViews.getDefaultDataView(); @@ -33,10 +34,22 @@ export const getSearchEmbeddableFactory = (services: Services) => { defaultDataView ? [defaultDataView] : undefined ); const dataLoading$ = new BehaviorSubject(false); + const blockingError$ = new BehaviorSubject(undefined); + + if (!defaultDataView) { + blockingError$.next( + new Error( + i18n.translate('embeddableExamples.search.noDataViewError', { + defaultMessage: 'Please install a data view to view this example', + }) + ) + ); + } const api = buildApi( { ...timeRange.api, + blockingError: blockingError$, dataViews: dataViews$, dataLoading: dataLoading$, serializeState: () => { @@ -53,7 +66,6 @@ export const getSearchEmbeddableFactory = (services: Services) => { } ); - const error$ = new BehaviorSubject(undefined); const count$ = new BehaviorSubject(0); let prevRequestAbortController: AbortController | undefined; const fetchSubscription = fetch$(api) @@ -64,7 +76,7 @@ export const getSearchEmbeddableFactory = (services: Services) => { } }), switchMap(async (fetchContext) => { - error$.next(undefined); + blockingError$.next(undefined); if (!defaultDataView) { return; } @@ -103,14 +115,14 @@ export const getSearchEmbeddableFactory = (services: Services) => { count$.next(next.count); } if (next && next.hasOwnProperty('error')) { - error$.next(next.error); + blockingError$.next(next.error); } }); return { api, Component: () => { - const [count, error] = useBatchedPublishingSubjects(count$, error$); + const [count, error] = useBatchedPublishingSubjects(count$, blockingError$); useEffect(() => { return () => { @@ -118,26 +130,37 @@ export const getSearchEmbeddableFactory = (services: Services) => { }; }, []); - if (!defaultDataView) { - return ( - -

Please install a sample data set to run example.

-
- ); - } - - if (error) { - return ( - -

{error.message}

-
- ); - } + // in error case we can return null because the panel will handle rendering the blocking error. + if (error || !defaultDataView) return null; return ( -

- Found {count} from {defaultDataView.name} -

+
+ + + {i18n.translate('embeddableExamples.search.dataViewName', { + defaultMessage: '{dataViewName}', + values: { dataViewName: defaultDataView.name }, + })} + + + } + titleSize="l" + > + {i18n.translate('embeddableExamples.search.result', { + defaultMessage: '{count, plural, one {document} other {documents}} found', + values: { count }, + })} + +
); }, }; diff --git a/examples/embeddable_examples/public/react_embeddables/search/types.ts b/examples/embeddable_examples/public/react_embeddables/search/types.ts index dffe119eee7a0..835b2380d46a7 100644 --- a/examples/embeddable_examples/public/react_embeddables/search/types.ts +++ b/examples/embeddable_examples/public/react_embeddables/search/types.ts @@ -9,23 +9,18 @@ import { DataPublicPluginStart } from '@kbn/data-plugin/public'; import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; import type { DefaultEmbeddableApi } from '@kbn/embeddable-plugin/public'; -import { TimeRange } from '@kbn/es-query'; import { HasParentApi, PublishesDataLoading, PublishesDataViews, PublishesUnifiedSearch, PublishesWritableUnifiedSearch, + SerializedTimeRange, } from '@kbn/presentation-publishing'; -export interface State { - /* - * Time range only applied to this embeddable, overrides parentApi.timeRange$ - */ - timeRange: TimeRange | undefined; -} +export type SearchSerializedState = SerializedTimeRange; -export type Api = DefaultEmbeddableApi & +export type SearchApi = DefaultEmbeddableApi & PublishesDataViews & PublishesDataLoading & Pick & diff --git a/examples/response_stream/public/components/page.tsx b/examples/response_stream/public/components/page.tsx index 0b902558ee307..e3138fafa71ab 100644 --- a/examples/response_stream/public/components/page.tsx +++ b/examples/response_stream/public/components/page.tsx @@ -6,15 +6,14 @@ * Side Public License, v 1. */ -import * as React from 'react'; - +import React, { FC, PropsWithChildren } from 'react'; import { EuiPageTemplate, EuiTitle } from '@elastic/eui'; export interface PageProps { title?: React.ReactNode; } -export const Page: React.FC = ({ title = 'Untitled', children }) => { +export const Page: FC> = ({ title = 'Untitled', children }) => { return ( <> diff --git a/examples/response_stream/server/plugin.ts b/examples/response_stream/server/plugin.ts index 9620a58ae517b..6bb0c55003059 100644 --- a/examples/response_stream/server/plugin.ts +++ b/examples/response_stream/server/plugin.ts @@ -27,7 +27,7 @@ export class ResponseStreamPlugin implements Plugin { public setup(core: CoreSetup, plugins: ResponseStreamSetupPlugins) { const router = core.http.createRouter(); - core.getStartServices().then(([_, depsStart]) => { + void core.getStartServices().then(([_, depsStart]) => { defineReducerStreamRoute(router, this.logger); defineSimpleStringStreamRoute(router, this.logger); }); diff --git a/examples/response_stream/server/routes/reducer_stream.ts b/examples/response_stream/server/routes/reducer_stream.ts index 5e03cd0732e74..02606b8c44756 100644 --- a/examples/response_stream/server/routes/reducer_stream.ts +++ b/examples/response_stream/server/routes/reducer_stream.ts @@ -85,44 +85,45 @@ export const defineReducerStreamRoute = (router: IRouter, logger: Logger) => { let progress = 0; async function pushStreamUpdate() { - setTimeout(() => { - try { - progress++; - - if (progress > 100 || shouldStop) { - end(); - return; - } - - push(updateProgressAction(progress)); - - const randomEntity = entities[Math.floor(Math.random() * entities.length)]; - const randomAction = actions[Math.floor(Math.random() * actions.length)]; - - if (randomAction === 'add') { - const randomCommits = Math.floor(Math.random() * 100); - push(addToEntityAction(randomEntity, randomCommits)); - } else if (randomAction === 'delete') { - push(deleteEntityAction(randomEntity)); - } else if (randomAction === 'throw-error') { - // Throw an error. It should not crash Kibana! - // It should be caught and logged to the Kibana server console. - throw new Error('There was a (simulated) server side error!'); - } else if (randomAction === 'emit-error') { - // Emit an error as a stream action. - push(errorAction('(Simulated) error pushed to the stream')); - return; - } - - pushStreamUpdate(); - } catch (e) { - logger.error(e); + await new Promise((resolve) => + setTimeout(resolve, Math.floor(Math.random() * maxTimeoutMs)) + ); + try { + progress++; + + if (progress > 100 || shouldStop) { + end(); + return; } - }, Math.floor(Math.random() * maxTimeoutMs)); + + push(updateProgressAction(progress)); + + const randomEntity = entities[Math.floor(Math.random() * entities.length)]; + const randomAction = actions[Math.floor(Math.random() * actions.length)]; + + if (randomAction === 'add') { + const randomCommits = Math.floor(Math.random() * 100); + push(addToEntityAction(randomEntity, randomCommits)); + } else if (randomAction === 'delete') { + push(deleteEntityAction(randomEntity)); + } else if (randomAction === 'throw-error') { + // Throw an error. It should not crash Kibana! + // It should be caught and logged to the Kibana server console. + throw new Error('There was a (simulated) server side error!'); + } else if (randomAction === 'emit-error') { + // Emit an error as a stream action. + push(errorAction('(Simulated) error pushed to the stream')); + return; + } + + void pushStreamUpdate(); + } catch (e) { + logger.error(e); + } } // do not call this using `await` so it will run asynchronously while we return the stream already. - pushStreamUpdate(); + void pushStreamUpdate(); return response.ok(responseWithHeaders); } diff --git a/examples/response_stream/server/routes/single_string_stream.ts b/examples/response_stream/server/routes/single_string_stream.ts index 26c26f0fc6b66..d9cb65686b71e 100644 --- a/examples/response_stream/server/routes/single_string_stream.ts +++ b/examples/response_stream/server/routes/single_string_stream.ts @@ -67,7 +67,7 @@ export const defineSimpleStringStreamRoute = (router: IRouter, logger: Logger) = await timeout(Math.floor(Math.random() * maxTimeoutMs)); if (!shouldStop) { - pushStreamUpdate(); + void pushStreamUpdate(); } } else { end(); @@ -78,7 +78,7 @@ export const defineSimpleStringStreamRoute = (router: IRouter, logger: Logger) = } // do not call this using `await` so it will run asynchronously while we return the stream already. - pushStreamUpdate(); + void pushStreamUpdate(); return response.ok(responseWithHeaders); } diff --git a/examples/search_examples/public/common/example_page.tsx b/examples/search_examples/public/common/example_page.tsx index 7be20aeef1d8d..ca52ebe1ae761 100644 --- a/examples/search_examples/public/common/example_page.tsx +++ b/examples/search_examples/public/common/example_page.tsx @@ -49,11 +49,11 @@ interface Props { basePath: IBasePath; } -export const SearchExamplePage: React.FC = ({ +export const SearchExamplePage: React.FC> = ({ children, exampleLinks, basePath, -}: PropsWithChildren) => { +}) => { return ( diff --git a/examples/search_examples/server/plugin.ts b/examples/search_examples/server/plugin.ts index 57a8b055f9b44..27680a3287aba 100644 --- a/examples/search_examples/server/plugin.ts +++ b/examples/search_examples/server/plugin.ts @@ -48,7 +48,7 @@ export class SearchExamplesPlugin this.logger.debug('search_examples: Setup'); const router = core.http.createRouter(); - core.getStartServices().then(([_, depsStart]) => { + void core.getStartServices().then(([_, depsStart]) => { const myStrategy = mySearchStrategyProvider(depsStart.data); const fibonacciStrategy = fibonacciStrategyProvider(); deps.data.search.registerSearchStrategy('myStrategy', myStrategy); diff --git a/examples/state_containers_examples/public/common/example_page.tsx b/examples/state_containers_examples/public/common/example_page.tsx index b60fb87248d9f..1a18dfe79f68a 100644 --- a/examples/state_containers_examples/public/common/example_page.tsx +++ b/examples/state_containers_examples/public/common/example_page.tsx @@ -46,11 +46,11 @@ interface Props { exampleLinks: ExampleLink[]; } -export const StateContainersExamplesPage: React.FC = ({ +export const StateContainersExamplesPage: React.FC> = ({ navigateToApp, children, exampleLinks, -}: PropsWithChildren) => { +}) => { return ( diff --git a/examples/user_profile_examples/public/panel_with_code_block.tsx b/examples/user_profile_examples/public/panel_with_code_block.tsx index 17854fb3ad36d..366fca931e2bf 100644 --- a/examples/user_profile_examples/public/panel_with_code_block.tsx +++ b/examples/user_profile_examples/public/panel_with_code_block.tsx @@ -5,7 +5,7 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -import React from 'react'; +import React, { FC, PropsWithChildren } from 'react'; import { EuiTitle, EuiSpacer, EuiSplitPanel, EuiCodeBlock } from '@elastic/eui'; export interface PanelWithCodeBlockProps { @@ -13,7 +13,7 @@ export interface PanelWithCodeBlockProps { code: string; } -export const PanelWithCodeBlock: React.FunctionComponent = ({ +export const PanelWithCodeBlock: FC> = ({ title, code, children, diff --git a/fleet_packages.json b/fleet_packages.json index 4de7895192d4e..317c4f1d60043 100644 --- a/fleet_packages.json +++ b/fleet_packages.json @@ -56,6 +56,6 @@ }, { "name": "security_detection_engine", - "version": "8.13.4" + "version": "8.13.5" } ] \ No newline at end of file diff --git a/nav-kibana-dev.docnav.json b/nav-kibana-dev.docnav.json index f7f412e576365..cfe16536856d0 100644 --- a/nav-kibana-dev.docnav.json +++ b/nav-kibana-dev.docnav.json @@ -166,6 +166,9 @@ }, { "id": "kibDevTutorialAdvancedSettings" + }, + { + "id": "kibDevTutorialCcsSetup" } ] }, diff --git a/package.json b/package.json index dc0e1b763c80d..af8f586026759 100644 --- a/package.json +++ b/package.json @@ -102,7 +102,7 @@ "@dnd-kit/utilities": "^2.0.0", "@elastic/apm-rum": "^5.16.0", "@elastic/apm-rum-react": "^2.0.2", - "@elastic/charts": "64.0.2", + "@elastic/charts": "64.1.0", "@elastic/datemath": "5.0.3", "@elastic/ecs": "^8.11.1", "@elastic/elasticsearch": "^8.13.0", @@ -173,7 +173,8 @@ "@kbn/apm-utils": "link:packages/kbn-apm-utils", "@kbn/app-link-test-plugin": "link:test/plugin_functional/plugins/app_link_test", "@kbn/application-usage-test-plugin": "link:x-pack/test/usage_collection/plugins/application_usage_test", - "@kbn/assetManager-plugin": "link:x-pack/plugins/asset_manager", + "@kbn/assetManager-plugin": "link:x-pack/plugins/observability_solution/asset_manager", + "@kbn/assets-data-access-plugin": "link:x-pack/plugins/observability_solution/assets_data_access", "@kbn/audit-log-plugin": "link:x-pack/test/security_api_integration/plugins/audit_log", "@kbn/banners-plugin": "link:x-pack/plugins/banners", "@kbn/bfetch-error": "link:packages/kbn-bfetch-error", @@ -577,7 +578,7 @@ "@kbn/maps-ems-plugin": "link:src/plugins/maps_ems", "@kbn/maps-plugin": "link:x-pack/plugins/maps", "@kbn/maps-vector-tile-utils": "link:x-pack/packages/maps/vector_tile_utils", - "@kbn/metrics-data-access-plugin": "link:x-pack/plugins/metrics_data_access", + "@kbn/metrics-data-access-plugin": "link:x-pack/plugins/observability_solution/metrics_data_access", "@kbn/ml-agg-utils": "link:x-pack/packages/ml/agg_utils", "@kbn/ml-anomaly-utils": "link:x-pack/packages/ml/anomaly_utils", "@kbn/ml-cancellable-search": "link:x-pack/packages/ml/cancellable_search", @@ -989,7 +990,7 @@ "deepmerge": "^4.2.2", "del": "^6.1.0", "diff": "^5.1.0", - "elastic-apm-node": "^4.5.2", + "elastic-apm-node": "^4.5.3", "email-addresses": "^5.0.0", "eventsource-parser": "^1.1.1", "execa": "^5.1.1", @@ -1052,7 +1053,7 @@ "lz-string": "^1.4.4", "mapbox-gl-draw-rectangle-mode": "1.0.4", "maplibre-gl": "3.1.0", - "markdown-it": "^12.3.2", + "markdown-it": "^14.1.0", "mdast-util-to-hast": "10.2.0", "memoize-one": "^6.0.0", "mime": "^2.4.4", diff --git a/packages/cloud/deployment_details/services.tsx b/packages/cloud/deployment_details/services.tsx index f25575cf8b01d..2b8ac4d1e65a4 100644 --- a/packages/cloud/deployment_details/services.tsx +++ b/packages/cloud/deployment_details/services.tsx @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import React, { FC, useContext } from 'react'; +import React, { FC, PropsWithChildren, useContext } from 'react'; export interface DeploymentDetailsContextValue { cloudId?: string; @@ -22,7 +22,7 @@ const DeploymentDetailsContext = React.createContext = ({ +export const DeploymentDetailsProvider: FC> = ({ children, ...services }) => { @@ -75,10 +75,9 @@ export interface DeploymentDetailsKibanaDependencies { /** * Kibana-specific Provider that maps to known dependency types. */ -export const DeploymentDetailsKibanaProvider: FC = ({ - children, - ...services -}) => { +export const DeploymentDetailsKibanaProvider: FC< + PropsWithChildren +> = ({ children, ...services }) => { const { core: { application: { navigateToUrl }, diff --git a/packages/content-management/content_editor/src/services.tsx b/packages/content-management/content_editor/src/services.tsx index 6666d3f3b0967..6edf58e45c846 100644 --- a/packages/content-management/content_editor/src/services.tsx +++ b/packages/content-management/content_editor/src/services.tsx @@ -6,12 +6,16 @@ * Side Public License, v 1. */ -import React, { useContext, useCallback, useMemo } from 'react'; -import type { FC, ReactNode } from 'react'; -import type { Observable } from 'rxjs'; +import type { FC, PropsWithChildren, ReactNode } from 'react'; +import React, { useCallback, useContext, useMemo } from 'react'; + import type { EuiComboBoxProps } from '@elastic/eui'; +import type { AnalyticsServiceStart } from '@kbn/core-analytics-browser'; +import type { I18nStart } from '@kbn/core-i18n-browser'; import type { MountPoint, OverlayRef } from '@kbn/core-mount-utils-browser'; import type { OverlayFlyoutOpenOptions } from '@kbn/core-overlays-browser'; +import type { ThemeServiceStart } from '@kbn/core-theme-browser'; +import { toMountPoint } from '@kbn/react-kibana-mount'; type NotifyFn = (title: JSX.Element, text?: string) => void; @@ -45,16 +49,28 @@ const ContentEditorContext = React.createContext(null); /** * Abstract external service Provider. */ -export const ContentEditorProvider: FC = ({ children, ...services }) => { +export const ContentEditorProvider: FC> = ({ + children, + ...services +}) => { return {children}; }; +/** + * Specific services for mounting React + */ +interface ContentEditorStartServices { + analytics: Pick; + i18n: I18nStart; + theme: Pick; +} + /** * Kibana-specific service types. */ export interface ContentEditorKibanaDependencies { /** CoreStart contract */ - core: { + core: ContentEditorStartServices & { overlays: { openFlyout(mount: MountPoint, options?: OverlayFlyoutOpenOptions): OverlayRef; }; @@ -63,21 +79,7 @@ export interface ContentEditorKibanaDependencies { addDanger: (notifyArgs: { title: MountPoint; text?: string }) => void; }; }; - theme: { - theme$: Observable; - }; }; - /** - * Handler from the '@kbn/kibana-react-plugin/public' Plugin - * - * ``` - * import { toMountPoint } from '@kbn/kibana-react-plugin/public'; - * ``` - */ - toMountPoint: ( - node: React.ReactNode, - options?: { theme$: Observable<{ readonly darkMode: boolean }> } - ) => MountPoint; /** * The public API from the savedObjectsTaggingOss plugin. * It is returned by calling `getTaggingApi()` from the SavedObjectTaggingOssPluginStart @@ -109,13 +111,12 @@ export interface ContentEditorKibanaDependencies { /** * Kibana-specific Provider that maps to known dependency types. */ -export const ContentEditorKibanaProvider: FC = ({ - children, - ...services -}) => { - const { core, toMountPoint, savedObjectsTagging } = services; - const { openFlyout: coreOpenFlyout } = core.overlays; - const { theme$ } = core.theme; +export const ContentEditorKibanaProvider: FC< + PropsWithChildren +> = ({ children, ...services }) => { + const { core, savedObjectsTagging } = services; + const { overlays, notifications, ...startServices } = core; + const { openFlyout: coreOpenFlyout } = overlays; const TagList = useMemo(() => { const Comp: Services['TagList'] = ({ references }) => { @@ -131,16 +132,16 @@ export const ContentEditorKibanaProvider: FC = const openFlyout = useCallback( (node: ReactNode, options: OverlayFlyoutOpenOptions) => { - return coreOpenFlyout(toMountPoint(node, { theme$ }), options); + return coreOpenFlyout(toMountPoint(node, startServices), options); }, - [coreOpenFlyout, toMountPoint, theme$] + [coreOpenFlyout, startServices] ); return ( { - core.notifications.toasts.addDanger({ title: toMountPoint(title), text }); + notifications.toasts.addDanger({ title: toMountPoint(title, startServices), text }); }} TagList={TagList} TagSelector={savedObjectsTagging?.ui.components.SavedObjectSaveModalTagSelector} diff --git a/packages/content-management/content_editor/tsconfig.json b/packages/content-management/content_editor/tsconfig.json index c5ee5594be9ff..8a7a1c08ba74a 100644 --- a/packages/content-management/content_editor/tsconfig.json +++ b/packages/content-management/content_editor/tsconfig.json @@ -15,16 +15,20 @@ }, "include": [ "**/*.ts", - "**/*.tsx", + "**/*.tsx" ], "kbn_references": [ "@kbn/i18n", "@kbn/i18n-react", "@kbn/core-mount-utils-browser", "@kbn/core-overlays-browser", + "@kbn/core-analytics-browser", + "@kbn/core-i18n-browser", + "@kbn/core-theme-browser", "@kbn/test-jest-helpers", + "@kbn/react-kibana-mount" ], "exclude": [ - "target/**/*", + "target/**/*" ] } diff --git a/packages/content-management/table_list_view/src/table_list_view.tsx b/packages/content-management/table_list_view/src/table_list_view.tsx index 1fd8cc8cc70c2..a5150c959e4f5 100644 --- a/packages/content-management/table_list_view/src/table_list_view.tsx +++ b/packages/content-management/table_list_view/src/table_list_view.tsx @@ -37,6 +37,7 @@ export type TableListViewProps & { title: string; description?: string; @@ -73,6 +74,7 @@ export const TableListView = ({ titleColumnName, additionalRightSideActions, withoutPageTemplateWrapper, + createdByEnabled, }: TableListViewProps) => { const PageTemplate = withoutPageTemplateWrapper ? (React.Fragment as unknown as typeof KibanaPageTemplate) @@ -121,6 +123,7 @@ export const TableListView = ({ withoutPageTemplateWrapper={withoutPageTemplateWrapper} onFetchSuccess={onFetchSuccess} setPageDataTestSubject={setPageDataTestSubject} + createdByEnabled={createdByEnabled} /> diff --git a/packages/content-management/table_list_view_common/index.ts b/packages/content-management/table_list_view_common/index.ts index 4ee102fb1e9a8..fea9e6a918673 100644 --- a/packages/content-management/table_list_view_common/index.ts +++ b/packages/content-management/table_list_view_common/index.ts @@ -11,6 +11,7 @@ import type { SavedObjectsReference } from '@kbn/content-management-content-edit export interface UserContentCommonSchema { id: string; updatedAt: string; + createdBy?: string; managed?: boolean; references: SavedObjectsReference[]; type: string; diff --git a/packages/content-management/table_list_view_table/src/__jest__/created_by.test.tsx b/packages/content-management/table_list_view_table/src/__jest__/created_by.test.tsx new file mode 100644 index 0000000000000..f4a28d0c333cf --- /dev/null +++ b/packages/content-management/table_list_view_table/src/__jest__/created_by.test.tsx @@ -0,0 +1,202 @@ +/* + * Copyright 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 { MemoryRouter } from 'react-router-dom'; +import { render, screen, within, waitForElementToBeRemoved } from '@testing-library/react'; +import { I18nProvider } from '@kbn/i18n-react'; +import { WithServices } from './tests.helpers'; +import { TableListViewTable, type TableListViewTableProps } from '../table_list_view_table'; +import type { UserContentCommonSchema } from '@kbn/content-management-table-list-view-common'; +import userEvent from '@testing-library/user-event'; + +const hits: UserContentCommonSchema[] = [ + { + id: 'item-1', + type: 'dashboard', + updatedAt: '2020-01-01T00:00:00Z', + attributes: { + title: 'Item 1', + }, + references: [], + }, + { + id: 'item-2', + type: 'dashboard', + updatedAt: '2020-01-01T00:00:00Z', + attributes: { + title: 'Item 2', + }, + createdBy: 'u_1', + references: [], + }, + { + id: 'item-3', + type: 'dashboard', + updatedAt: '2020-01-01T00:00:00Z', + attributes: { + title: 'Item 3', + }, + createdBy: 'u_2', + references: [], + }, +]; + +describe('created_by', () => { + const requiredProps: TableListViewTableProps = { + entityName: 'test', + entityNamePlural: 'tests', + listingLimit: 500, + initialFilter: '', + initialPageSize: 20, + findItems: jest.fn().mockResolvedValue({ total: 0, hits }), + getDetailViewLink: () => 'http://elastic.co', + urlStateEnabled: false, + onFetchSuccess: () => {}, + tableCaption: 'my caption', + setPageDataTestSubject: () => {}, + }; + + const mockBulkGetUserProfiles = jest.fn((uids) => + Promise.resolve( + [ + { + uid: 'u_1', + enabled: true, + user: { + username: 'user1', + }, + data: {}, + }, + { + uid: 'u_2', + enabled: true, + user: { + username: 'user2', + }, + data: {}, + }, + ].filter((user) => uids.includes(user.uid)) + ) + ); + + const TableListViewWithServices = WithServices(TableListViewTable, { + bulkGetUserProfiles: mockBulkGetUserProfiles, + }); + const TableListView = (overrides: Partial) => ( + + + + + + ); + + test("shouldn't render created by filter when createdBy is disabled", async () => { + render(); + + // wait until first render + expect(await screen.findByTestId('itemsInMemTable')).toBeVisible(); + + expect(() => screen.getByTestId('userFilterPopoverButton')).toThrow(); + }); + + test('should be able to filter by creators', async () => { + render(); + + // wait until first render + expect(await screen.findByTestId('itemsInMemTable')).toBeVisible(); + + // 3 items in the list + expect(screen.getAllByTestId(/userContentListingTitleLink/)).toHaveLength(3); + + userEvent.click(screen.getByTestId('userFilterPopoverButton')); + + const userSelectablePopover = screen.getByTestId('userSelectableList'); + const popover = within(userSelectablePopover); + expect(await popover.findAllByTestId(/userProfileSelectableOption/)).toHaveLength(3); + + userEvent.click(popover.getByTestId('userProfileSelectableOption-user1')); + + // 2 item in the list + expect(screen.getAllByTestId(/userContentListingTitleLink/)).toHaveLength(1); + + userEvent.click(popover.getByTestId('userProfileSelectableOption-user2')); + + // 2 item in the list + expect(screen.getAllByTestId(/userContentListingTitleLink/)).toHaveLength(2); + }); + + test('should be able to filter by "no creators"', async () => { + render(); + + // wait until first render + expect(await screen.findByTestId('itemsInMemTable')).toBeVisible(); + + // 3 items in the list + expect(screen.getAllByTestId(/userContentListingTitleLink/)).toHaveLength(3); + + userEvent.click(screen.getByTestId('userFilterPopoverButton')); + + const userSelectablePopover = screen.getByTestId('userSelectableList'); + const popover = within(userSelectablePopover); + userEvent.click(await popover.findByTestId('userProfileSelectableOption-null')); + + // just 1 item in the list + expect(screen.getAllByTestId(/userContentListingTitleLink/)).toHaveLength(1); + }); + + test('"no creators" options shouldn\'t appear if all objects have creators', async () => { + render( + ({ + hits: [hits[1], hits[2]], + total: 2, + })} + /> + ); + + // wait until first render + expect(await screen.findByTestId('itemsInMemTable')).toBeVisible(); + // 2 items in the list + expect(screen.getAllByTestId(/userContentListingTitleLink/)).toHaveLength(2); + + userEvent.click(screen.getByTestId('userFilterPopoverButton')); + + const userSelectablePopover = screen.getByTestId('userSelectableList'); + const popover = within(userSelectablePopover); + expect(await popover.findAllByTestId(/userProfileSelectableOption/)).toHaveLength(2); + expect(() => popover.getByTestId('userProfileSelectableOption-null')).toThrow(); + }); + + test('empty message in case no objects have creators', async () => { + render( + ({ + hits: [hits[0]], + total: 1, + })} + /> + ); + + // wait until first render + expect(await screen.findByTestId('itemsInMemTable')).toBeVisible(); + // 1 item in the list + expect(screen.getAllByTestId(/userContentListingTitleLink/)).toHaveLength(1); + + userEvent.click(screen.getByTestId('userFilterPopoverButton')); + + const userSelectablePopover = screen.getByTestId('userSelectableList'); + const popover = within(userSelectablePopover); + await waitForElementToBeRemoved(() => popover.getByRole('progressbar')); + expect(popover.getAllByTestId('userFilterEmptyMessage')[1]).toBeVisible(); + }); +}); diff --git a/packages/content-management/table_list_view_table/src/__jest__/tests.helpers.tsx b/packages/content-management/table_list_view_table/src/__jest__/tests.helpers.tsx index 5fb8b605d9202..baca088ac8985 100644 --- a/packages/content-management/table_list_view_table/src/__jest__/tests.helpers.tsx +++ b/packages/content-management/table_list_view_table/src/__jest__/tests.helpers.tsx @@ -25,6 +25,7 @@ export const getMockServices = (overrides?: Partial) => { itemHasTags: () => true, getTagManagementUrl: () => '', getTagIdsFromReferences: () => [], + bulkGetUserProfiles: jest.fn(() => Promise.resolve([])), ...overrides, }; diff --git a/packages/content-management/table_list_view_table/src/actions.ts b/packages/content-management/table_list_view_table/src/actions.ts index 4ae4686779746..8f3e2efe9f86b 100644 --- a/packages/content-management/table_list_view_table/src/actions.ts +++ b/packages/content-management/table_list_view_table/src/actions.ts @@ -57,6 +57,7 @@ export interface OnTableChangeAction { pageIndex: number; pageSize: number; }; + filter?: Partial['tableFilter']>; }; } diff --git a/packages/content-management/table_list_view_table/src/components/table.tsx b/packages/content-management/table_list_view_table/src/components/table.tsx index a459bc26ede50..475b4557740ac 100644 --- a/packages/content-management/table_list_view_table/src/components/table.tsx +++ b/packages/content-management/table_list_view_table/src/components/table.tsx @@ -35,10 +35,15 @@ import { TagFilterPanel } from './tag_filter_panel'; import { useTagFilterPanel } from './use_tag_filter_panel'; import type { Params as UseTagFilterPanelParams } from './use_tag_filter_panel'; import type { SortColumnField } from './table_sort_select'; +import { + UserFilterPanel, + UserFilterContextProvider, + NULL_USER as USER_FILTER_NULL_USER, +} from './user_filter_panel'; type State = Pick< TableListViewState, - 'items' | 'selectedIds' | 'searchQuery' | 'tableSort' | 'pagination' + 'items' | 'selectedIds' | 'searchQuery' | 'tableSort' | 'pagination' | 'tableFilter' >; type TagManagementProps = Pick< @@ -59,8 +64,10 @@ interface Props extends State, TagManageme renderCreateButton: () => React.ReactElement | undefined; onSortChange: (column: SortColumnField, direction: Direction) => void; onTableChange: (criteria: CriteriaWithPagination) => void; + onFilterChange: (filter: Partial['tableFilter']>) => void; onTableSearchChange: (arg: { query: Query | null; queryText: string }) => void; clearTagSelection: () => void; + createdByEnabled: boolean; } export function Table({ @@ -72,6 +79,7 @@ export function Table({ pagination, tableColumns, tableSort, + tableFilter, hasUpdatedAtMetadata, entityName, entityNamePlural, @@ -83,9 +91,11 @@ export function Table({ onTableChange, onTableSearchChange, onSortChange, + onFilterChange, addOrRemoveExcludeTagFilter, addOrRemoveIncludeTagFilter, clearTagSelection, + createdByEnabled, }: Props) { const { getTagList } = useServices(); @@ -200,9 +210,20 @@ export function Table({ clearTagSelection, ]); + const userFilterPanel = useMemo(() => { + return createdByEnabled + ? { + type: 'custom_component', + component: UserFilterPanel, + } + : null; + }, [createdByEnabled]); + const searchFilters = useMemo(() => { - return [tableSortSelectFilter, tagFilterPanel]; - }, [tableSortSelectFilter, tagFilterPanel]); + return [tableSortSelectFilter, tagFilterPanel, userFilterPanel].filter( + (f: SearchFilterConfig | null): f is SearchFilterConfig => Boolean(f) + ); + }, [tableSortSelectFilter, tagFilterPanel, userFilterPanel]); const search = useMemo((): Search => { return { @@ -226,22 +247,57 @@ export function Table({ /> ); + const visibleItems = React.useMemo(() => { + if (tableFilter?.createdBy?.length > 0) { + return items.filter((item) => { + if (item.createdBy) return tableFilter.createdBy.includes(item.createdBy); + else return tableFilter.createdBy.includes(USER_FILTER_NULL_USER); + }); + } + + return items; + }, [items, tableFilter]); + + const { allUsers, showNoUserOption } = useMemo(() => { + if (!createdByEnabled) return { allUsers: [], showNoUserOption: false }; + + let _showNoUserOption = false; + const users = new Set(); + items.forEach((item) => { + if (item.createdBy) users.add(item.createdBy); + else { + _showNoUserOption = true; + } + }); + return { allUsers: Array.from(users), showNoUserOption: _showNoUserOption }; + }, [createdByEnabled, items]); + return ( - - itemId="id" - items={items} - columns={tableColumns} - pagination={pagination} - loading={isFetchingItems} - message={noItemsMessage} - selection={selection} - search={search} - executeQueryOptions={{ enabled: false }} - sorting={tableSort ? { sort: tableSort as PropertySort } : undefined} - onChange={onTableChange} - data-test-subj="itemsInMemTable" - rowHeader="attributes.title" - tableCaption={tableCaption} - /> + { + onFilterChange({ createdBy: selectedUsers }); + }} + selectedUsers={tableFilter.createdBy} + showNoUserOption={showNoUserOption} + > + + itemId="id" + items={visibleItems} + columns={tableColumns} + pagination={pagination} + loading={isFetchingItems} + message={noItemsMessage} + selection={selection} + search={search} + executeQueryOptions={{ enabled: false }} + sorting={tableSort ? { sort: tableSort as PropertySort } : undefined} + onChange={onTableChange} + data-test-subj="itemsInMemTable" + rowHeader="attributes.title" + tableCaption={tableCaption} + /> + ); } diff --git a/packages/content-management/table_list_view_table/src/components/user_filter_panel.tsx b/packages/content-management/table_list_view_table/src/components/user_filter_panel.tsx new file mode 100644 index 0000000000000..82a2838ab44e3 --- /dev/null +++ b/packages/content-management/table_list_view_table/src/components/user_filter_panel.tsx @@ -0,0 +1,187 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { FC } from 'react'; +import React from 'react'; +import { EuiFilterButton, EuiIconTip, useEuiTheme } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { UserProfile, UserProfilesPopover } from '@kbn/user-profile-components'; +import { useQuery, QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { i18n } from '@kbn/i18n'; +import { useServices } from '../services'; + +interface Context { + enabled: boolean; + onSelectedUsersChange: (users: string[]) => void; + selectedUsers: string[]; + allUsers: string[]; + showNoUserOption: boolean; +} + +const UserFilterContext = React.createContext(null); +const queryClient = new QueryClient({ + defaultOptions: { queries: { retry: false, staleTime: 30 * 60 * 1000 } }, +}); + +export const UserFilterContextProvider: FC = ({ children, ...props }) => { + if (!props.enabled) { + return <>{children}; + } + + return ( + + {children} + + ); +}; + +export const NULL_USER = 'no-user'; + +export const UserFilterPanel: FC<{}> = () => { + const { bulkGetUserProfiles } = useServices(); + const { euiTheme } = useEuiTheme(); + const componentContext = React.useContext(UserFilterContext); + if (!componentContext) + throw new Error('UserFilterPanel must be used within a UserFilterContextProvider'); + if (!bulkGetUserProfiles) + throw new Error('UserFilterPanel must be used with a bulkGetUserProfiles function'); + + const { onSelectedUsersChange, selectedUsers, showNoUserOption } = componentContext; + + const [isPopoverOpen, setPopoverOpen] = React.useState(false); + const [searchTerm, setSearchTerm] = React.useState(''); + + const query = useQuery({ + queryKey: ['user-filter-suggestions', componentContext.allUsers], + queryFn: () => bulkGetUserProfiles(componentContext.allUsers), + enabled: isPopoverOpen, + }); + + const usersMap = React.useMemo(() => { + if (!query.data) return {}; + return query.data.reduce((acc, user) => { + acc[user.uid] = user; + return acc; + }, {} as Record); + }, [query.data]); + + const visibleOptions = React.useMemo(() => { + if (!query.data || query.data.length === 0) return []; + // attach null to the end of the list to represent the "no creator" option + const users = + showNoUserOption || selectedUsers.includes(NULL_USER) ? [...query.data, null] : query.data; + + if (!searchTerm) { + return users; + } + + return users.filter((user) => { + // keep the "no creator" option if it's selected + if (!user) { + return selectedUsers.includes(NULL_USER); + } + + // keep the user if it's selected + if (selectedUsers.includes(user.uid)) return true; + + // filter only users that match the search term + const searchString = ( + user.uid + + user.user.username + + (user.user.email ?? '') + + (user.user.full_name ?? '') + ).toLowerCase(); + return searchString.includes(searchTerm.toLowerCase()); + }); + }, [query.data, searchTerm, selectedUsers, showNoUserOption]); + + const noUsersTip = ( + + } + /> + ); + + return ( + <> + setPopoverOpen(!isPopoverOpen)} + hasActiveFilters={selectedUsers.length > 0} + numActiveFilters={selectedUsers.length} + grow + > + + + } + isOpen={isPopoverOpen} + closePopover={() => setPopoverOpen(false)} + selectableProps={{ + 'data-test-subj': 'userSelectableList', + isLoading: query.isLoading, + options: visibleOptions, + errorMessage: query.error ? ( + + ) : undefined, + emptyMessage: ( +

+ + {noUsersTip} +

+ ), + nullOptionLabel: i18n.translate( + 'contentManagement.tableList.listing.userFilter.noCreators', + { + defaultMessage: 'No creators', + } + ), + nullOptionProps: { + append: noUsersTip, + }, + clearButtonLabel: ( + + ), + selectedOptions: selectedUsers.map((uid) => + uid === NULL_USER ? null : usersMap[uid] ?? { uid, user: { username: uid } } + ), + onChange: (options) => { + onSelectedUsersChange(options.map((option) => (option ? option.uid : NULL_USER))); + }, + onSearchChange: setSearchTerm, + }} + panelProps={{ css: { minWidth: euiTheme.base * 18 } }} + /> + + ); +}; diff --git a/packages/content-management/table_list_view_table/src/mocks.tsx b/packages/content-management/table_list_view_table/src/mocks.tsx index bbdfc3e9f9c7b..da1bc983c3041 100644 --- a/packages/content-management/table_list_view_table/src/mocks.tsx +++ b/packages/content-management/table_list_view_table/src/mocks.tsx @@ -71,6 +71,7 @@ export const getStoryServices = (params: Params, action: ActionFn = () => {}) => itemHasTags: () => true, getTagManagementUrl: () => '', getTagIdsFromReferences: () => [], + bulkGetUserProfiles: () => Promise.resolve([]), ...params, }; diff --git a/packages/content-management/table_list_view_table/src/reducer.tsx b/packages/content-management/table_list_view_table/src/reducer.tsx index be6b8d4043ebd..bf4e912f84394 100644 --- a/packages/content-management/table_list_view_table/src/reducer.tsx +++ b/packages/content-management/table_list_view_table/src/reducer.tsx @@ -93,6 +93,9 @@ export function getReducer() { const tableSort = action.data.sort ?? state.tableSort; const pageIndex = action.data.page?.pageIndex ?? state.pagination.pageIndex; const pageSize = action.data.page?.pageSize ?? state.pagination.pageSize; + const tableFilter = action.data.filter + ? { ...state.tableFilter, ...action.data.filter } + : state.tableFilter; return { ...state, @@ -102,6 +105,7 @@ export function getReducer() { pageSize, }, tableSort, + tableFilter, }; } case 'showConfirmDeleteItemsModal': { diff --git a/packages/content-management/table_list_view_table/src/services.tsx b/packages/content-management/table_list_view_table/src/services.tsx index 3ed036612d2f9..7aa1109a05cde 100644 --- a/packages/content-management/table_list_view_table/src/services.tsx +++ b/packages/content-management/table_list_view_table/src/services.tsx @@ -6,16 +6,24 @@ * Side Public License, v 1. */ -import React, { FC, useContext, useMemo, useCallback } from 'react'; +import type { FC, PropsWithChildren } from 'react'; +import React, { useCallback, useContext, useMemo } from 'react'; import type { Observable } from 'rxjs'; -import type { FormattedRelative } from '@kbn/i18n-react'; -import type { MountPoint, OverlayRef } from '@kbn/core-mount-utils-browser'; -import type { OverlayFlyoutOpenOptions } from '@kbn/core-overlays-browser'; -import { RedirectAppLinksKibanaProvider } from '@kbn/shared-ux-link-redirect-app'; + import { ContentEditorKibanaProvider, type SavedObjectsReference, } from '@kbn/content-management-content-editor'; +import type { AnalyticsServiceStart } from '@kbn/core-analytics-browser'; +import type { I18nStart } from '@kbn/core-i18n-browser'; +import type { MountPoint, OverlayRef } from '@kbn/core-mount-utils-browser'; +import type { OverlayFlyoutOpenOptions } from '@kbn/core-overlays-browser'; +import type { ThemeServiceStart } from '@kbn/core-theme-browser'; +import type { UserProfileServiceStart } from '@kbn/core-user-profile-browser'; +import type { UserProfile } from '@kbn/core-user-profile-common'; +import type { FormattedRelative } from '@kbn/i18n-react'; +import { toMountPoint } from '@kbn/react-kibana-mount'; +import { RedirectAppLinksKibanaProvider } from '@kbn/shared-ux-link-redirect-app'; import { TAG_MANAGEMENT_APP_URL } from './constants'; import type { Tag } from './types'; @@ -58,6 +66,8 @@ export interface Services { /** Handler to return the url to navigate to the kibana tags management */ getTagManagementUrl: () => string; getTagIdsFromReferences: (references: SavedObjectsReference[]) => string[]; + /** resolve user profiles for the user filter and creator functionality */ + bulkGetUserProfiles: (uids: string[]) => Promise; } const TableListViewContext = React.createContext(null); @@ -65,16 +75,28 @@ const TableListViewContext = React.createContext(null); /** * Abstract external service Provider. */ -export const TableListViewProvider: FC = ({ children, ...services }) => { +export const TableListViewProvider: FC> = ({ + children, + ...services +}) => { return {children}; }; +/** + * Specific services for mounting React + */ +interface TableListViewStartServices { + analytics: Pick; + i18n: I18nStart; + theme: Pick; +} + /** * Kibana-specific service types. */ export interface TableListViewKibanaDependencies { /** CoreStart contract */ - core: { + core: TableListViewStartServices & { application: { capabilities: { [key: string]: Readonly>>; @@ -96,23 +118,10 @@ export interface TableListViewKibanaDependencies { overlays: { openFlyout(mount: MountPoint, options?: OverlayFlyoutOpenOptions): OverlayRef; }; - theme: { - theme$: Observable<{ - readonly darkMode: boolean; - }>; + userProfile: { + bulkGet: UserProfileServiceStart['bulkGet']; }; }; - /** - * Handler from the '@kbn/kibana-react-plugin/public' Plugin - * - * ``` - * import { toMountPoint } from '@kbn/kibana-react-plugin/public'; - * ``` - */ - toMountPoint: ( - node: React.ReactNode, - options?: { theme$: Observable<{ readonly darkMode: boolean }> } - ) => MountPoint; /** * The public API from the savedObjectsTaggingOss plugin. * It is returned by calling `getTaggingApi()` from the SavedObjectTaggingOssPluginStart @@ -159,11 +168,11 @@ export interface TableListViewKibanaDependencies { /** * Kibana-specific Provider that maps to known dependency types. */ -export const TableListViewKibanaProvider: FC = ({ - children, - ...services -}) => { - const { core, toMountPoint, savedObjectsTagging, FormattedRelative } = services; +export const TableListViewKibanaProvider: FC< + PropsWithChildren +> = ({ children, ...services }) => { + const { core, savedObjectsTagging, FormattedRelative } = services; + const { application, http, notifications, ...startServices } = core; const searchQueryParser = useMemo(() => { if (savedObjectsTagging) { @@ -216,32 +225,38 @@ export const TableListViewKibanaProvider: FC = [getTagIdsFromReferences] ); + const bulkGetUserProfiles = useCallback<(userProfileIds: string[]) => Promise>( + async (uids: string[]) => { + if (uids.length === 0) return []; + + return core.userProfile.bulkGet({ uids: new Set(uids), dataPath: 'avatar' }); + }, + [core.userProfile] + ); + return ( - + - core.application.getUrlForApp('management', { + application.getUrlForApp('management', { path: `/kibana/settings?query=savedObjects:listingLimit`, }) } notifyError={(title, text) => { - core.notifications.toasts.addDanger({ title: toMountPoint(title), text }); + notifications.toasts.addDanger({ title: toMountPoint(title, startServices), text }); }} searchQueryParser={searchQueryParser} DateFormatterComp={(props) => } - currentAppId$={core.application.currentAppId$} - navigateToUrl={core.application.navigateToUrl} + currentAppId$={application.currentAppId$} + navigateToUrl={application.navigateToUrl} getTagList={getTagList} TagList={TagList} itemHasTags={itemHasTags} getTagIdsFromReferences={getTagIdsFromReferences} getTagManagementUrl={() => core.http.basePath.prepend(TAG_MANAGEMENT_APP_URL)} + bulkGetUserProfiles={bulkGetUserProfiles} > {children} diff --git a/packages/content-management/table_list_view_table/src/table_list_view_table.tsx b/packages/content-management/table_list_view_table/src/table_list_view_table.tsx index 00f28114f3da0..6c6ed5c9631c4 100644 --- a/packages/content-management/table_list_view_table/src/table_list_view_table.tsx +++ b/packages/content-management/table_list_view_table/src/table_list_view_table.tsx @@ -65,6 +65,7 @@ export interface TableListViewTableProps< /** Add an additional custom column */ customTableColumn?: EuiBasicTableColumn; urlStateEnabled?: boolean; + createdByEnabled?: boolean; /** * Id of the heading element describing the table. This id will be used as `aria-labelledby` of the wrapper element. * If the table is not empty, this component renders its own h1 element using the same id. @@ -140,6 +141,9 @@ export interface State { } } + if (sanitizedParams.created_by) { + stateFromURL.filter = { + createdBy: Array.isArray(sanitizedParams.created_by) + ? sanitizedParams.created_by + : [sanitizedParams.created_by], + }; + } else { + stateFromURL.filter = { createdBy: [] }; + } + return stateFromURL; }; @@ -207,6 +225,7 @@ const urlStateDeserializer = (params: URLQueryParams): URLState => { const urlStateSerializer = (updated: { s?: string; sort?: { field: 'title' | 'updatedAt'; direction: Direction }; + filter?: { createdBy?: string[] }; }) => { const updatedQueryParams: Partial = {}; @@ -225,6 +244,10 @@ const urlStateSerializer = (updated: { updatedQueryParams.title = undefined; } + if (updated.filter?.createdBy) { + updatedQueryParams.created_by = updated.filter.createdBy; + } + return updatedQueryParams; }; @@ -264,6 +287,7 @@ function TableListViewTableComp({ onFetchSuccess, refreshListBouncer, setPageDataTestSubject, + createdByEnabled = false, }: TableListViewTableProps) { useEffect(() => { setPageDataTestSubject(`${entityName}LandingPage`); @@ -345,6 +369,9 @@ function TableListViewTableComp({ field: 'attributes.title' as const, direction: 'asc', }, + tableFilter: { + createdBy: [], + }, }), [initialPageSize] ); @@ -365,6 +392,7 @@ function TableListViewTableComp({ hasUpdatedAtMetadata, pagination, tableSort, + tableFilter, } = state; const showFetchError = Boolean(fetchError); @@ -700,13 +728,14 @@ function TableListViewTableComp({ [updateQuery, buildQueryFromText] ); - const updateTableSortAndPagination = useCallback( + const updateTableSortFilterAndPagination = useCallback( (data: { sort?: State['tableSort']; page?: { pageIndex: number; pageSize: number; }; + filter?: Partial['tableFilter']>; }) => { if (data.sort && urlStateEnabled) { setUrlState({ @@ -717,6 +746,12 @@ function TableListViewTableComp({ }); } + if (data.filter && urlStateEnabled) { + setUrlState({ + filter: data.filter, + }); + } + if (data.page || !urlStateEnabled) { dispatch({ type: 'onTableChange', @@ -729,14 +764,23 @@ function TableListViewTableComp({ const onSortChange = useCallback( (field: SortColumnField, direction: Direction) => { - updateTableSortAndPagination({ + updateTableSortFilterAndPagination({ sort: { field, direction, }, }); }, - [updateTableSortAndPagination] + [updateTableSortFilterAndPagination] + ); + + const onFilterChange = useCallback( + (filter: Partial) => { + updateTableSortFilterAndPagination({ + filter, + }); + }, + [updateTableSortFilterAndPagination] ); const onTableChange = useCallback( @@ -770,9 +814,9 @@ function TableListViewTableComp({ pageSize: criteria.page.size, }; - updateTableSortAndPagination(data); + updateTableSortFilterAndPagination(data); }, - [updateTableSortAndPagination] + [updateTableSortFilterAndPagination] ); const deleteSelectedItems = useCallback(async () => { @@ -911,8 +955,24 @@ function TableListViewTableComp({ }); }; + const updateFilterFromURL = (filter?: URLState['filter']) => { + if (!filter) { + return; + } + + dispatch({ + type: 'onTableChange', + data: { + filter: { + createdBy: filter.createdBy ?? [], + }, + }, + }); + }; + updateQueryFromURL(urlState.s); updateSortFromURL(urlState.sort); + updateFilterFromURL(urlState.filter); }, [urlState, buildQueryFromText, urlStateEnabled]); useEffect(() => { @@ -996,6 +1056,7 @@ function TableListViewTableComp({ tableColumns={tableColumns} hasUpdatedAtMetadata={hasUpdatedAtMetadata} tableSort={tableSort} + tableFilter={tableFilter} tableItemsRowActions={tableItemsRowActions} pagination={pagination} selectedIds={selectedIds} @@ -1007,9 +1068,11 @@ function TableListViewTableComp({ onTableChange={onTableChange} onTableSearchChange={onTableSearchChange} onSortChange={onSortChange} + onFilterChange={onFilterChange} addOrRemoveIncludeTagFilter={addOrRemoveIncludeTagFilter} addOrRemoveExcludeTagFilter={addOrRemoveExcludeTagFilter} clearTagSelection={clearTagSelection} + createdByEnabled={createdByEnabled} /> {/* Delete modal */} diff --git a/packages/content-management/table_list_view_table/tsconfig.json b/packages/content-management/table_list_view_table/tsconfig.json index 6837cecb7eeb9..17d2e03b8635e 100644 --- a/packages/content-management/table_list_view_table/tsconfig.json +++ b/packages/content-management/table_list_view_table/tsconfig.json @@ -8,12 +8,14 @@ "react", "@kbn/ambient-ui-types", "@kbn/ambient-storybook-types", - "@emotion/react/types/css-prop" + "@emotion/react/types/css-prop", + "@testing-library/jest-dom", + "@testing-library/react" ] }, "include": [ "**/*.ts", - "**/*.tsx", + "**/*.tsx" ], "kbn_references": [ "@kbn/i18n", @@ -22,12 +24,19 @@ "@kbn/core-http-browser", "@kbn/core-mount-utils-browser", "@kbn/core-overlays-browser", + "@kbn/core-analytics-browser", + "@kbn/core-i18n-browser", + "@kbn/core-theme-browser", "@kbn/shared-ux-page-kibana-template", "@kbn/shared-ux-link-redirect-app", "@kbn/test-jest-helpers", "@kbn/content-management-table-list-view-common", + "@kbn/user-profile-components", + "@kbn/core-user-profile-browser", + "@kbn/core-user-profile-common", + "@kbn/react-kibana-mount" ], "exclude": [ - "target/**/*", + "target/**/*" ] } diff --git a/packages/core/chrome/core-chrome-browser-internal/src/ui/project/navigation.tsx b/packages/core/chrome/core-chrome-browser-internal/src/ui/project/navigation.tsx index 83b7831831f29..954e7ec6dfb54 100644 --- a/packages/core/chrome/core-chrome-browser-internal/src/ui/project/navigation.tsx +++ b/packages/core/chrome/core-chrome-browser-internal/src/ui/project/navigation.tsx @@ -6,15 +6,15 @@ * Side Public License, v 1. */ -import React, { useEffect, useRef } from 'react'; +import React, { useEffect, useRef, FC, PropsWithChildren } from 'react'; import { EuiCollapsibleNavBeta } from '@elastic/eui'; import useLocalStorage from 'react-use/lib/useLocalStorage'; const LOCAL_STORAGE_IS_COLLAPSED_KEY = 'PROJECT_NAVIGATION_COLLAPSED' as const; -export const ProjectNavigation: React.FC<{ - toggleSideNav: (isVisible: boolean) => void; -}> = ({ children, toggleSideNav }) => { +export const ProjectNavigation: FC< + PropsWithChildren<{ toggleSideNav: (isVisible: boolean) => void }> +> = ({ children, toggleSideNav }) => { const isMounted = useRef(false); const [isCollapsed, setIsCollapsed] = useLocalStorage(LOCAL_STORAGE_IS_COLLAPSED_KEY, false); const onCollapseToggle = (nextIsCollapsed: boolean) => { diff --git a/packages/core/http/core-http-server/src/versioning/types.ts b/packages/core/http/core-http-server/src/versioning/types.ts index baa550b253544..b7aed68438fdb 100644 --- a/packages/core/http/core-http-server/src/versioning/types.ts +++ b/packages/core/http/core-http-server/src/versioning/types.ts @@ -32,7 +32,7 @@ export type VersionedRouteConfig = Omit< RouteConfig, 'validate' | 'options' > & { - options?: Omit, 'access'>; + options?: Omit, 'access' | 'description'>; /** See {@link RouteConfigOptions['access']} */ access: Exclude['access'], undefined>; /** diff --git a/packages/core/i18n/core-i18n-browser-mocks/src/i18n_context_mock.tsx b/packages/core/i18n/core-i18n-browser-mocks/src/i18n_context_mock.tsx index 44fa1645eae95..33a515f7a1a5d 100644 --- a/packages/core/i18n/core-i18n-browser-mocks/src/i18n_context_mock.tsx +++ b/packages/core/i18n/core-i18n-browser-mocks/src/i18n_context_mock.tsx @@ -6,14 +6,15 @@ * Side Public License, v 1. */ -import React from 'react'; +import React, { FC, PropsWithChildren } from 'react'; + // eslint-disable-next-line @kbn/eslint/module_migration import { IntlProvider } from 'react-intl'; import { i18n } from '@kbn/i18n'; const emptyMessages = {}; -export const I18nProviderMock: React.FC = ({ children }) => { +export const I18nProviderMock: FC> = ({ children }) => { return ( ; -}> = ({ chromeVisible$, children }) => { +export const AppWrapper: FC< + PropsWithChildren<{ + chromeVisible$: Observable; + }> +> = ({ chromeVisible$, children }) => { const visible = useObservable(chromeVisible$); return (
= ({ +export const CoreThemeProvider: FC> = ({ theme$, globalStyles, children, diff --git a/packages/home/sample_data_card/src/services.tsx b/packages/home/sample_data_card/src/services.tsx index 919e248875725..e1ee1fdbfb901 100644 --- a/packages/home/sample_data_card/src/services.tsx +++ b/packages/home/sample_data_card/src/services.tsx @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import React, { FC, MouseEventHandler, useContext } from 'react'; +import React, { FC, PropsWithChildren, MouseEventHandler, useContext } from 'react'; import { EuiGlobalToastListToast as EuiToast } from '@elastic/eui'; import { SAMPLE_DATA_API } from './constants'; @@ -41,7 +41,10 @@ const Context = React.createContext(null); /** * A Context Provider that provides services to the component and its dependencies. */ -export const SampleDataCardProvider: FC = ({ children, ...services }) => { +export const SampleDataCardProvider: FC> = ({ + children, + ...services +}) => { const { addBasePath, getAppNavigationHandler, @@ -99,7 +102,7 @@ export interface KibanaDependencies { /** * Kibana-specific Provider that maps dependencies to services. */ -export const SampleDataCardKibanaProvider: FC = ({ +export const SampleDataCardKibanaProvider: FC> = ({ children, ...dependencies }) => { diff --git a/packages/home/sample_data_tab/src/services.tsx b/packages/home/sample_data_tab/src/services.tsx index b78c0f3507a0d..775834691b54e 100644 --- a/packages/home/sample_data_tab/src/services.tsx +++ b/packages/home/sample_data_tab/src/services.tsx @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import React, { FC, useContext } from 'react'; +import React, { FC, PropsWithChildren, useContext } from 'react'; import type { EuiGlobalToastListToast as EuiToast } from '@elastic/eui'; import type { SampleDataSet } from '@kbn/home-sample-data-types'; import { @@ -46,7 +46,10 @@ const Context = React.createContext(null); /** * A Context Provider that provides services to the component and its dependencies. */ -export const SampleDataTabProvider: FC = ({ children, ...services }) => { +export const SampleDataTabProvider: FC> = ({ + children, + ...services +}) => { const { fetchSampleDataSets, notifyError, logClick } = services; return ( @@ -86,10 +89,9 @@ export type SampleDataTabKibanaDependencies = KibanaDependencies & SampleDataCar /** * Kibana-specific Provider that maps dependencies to services. */ -export const SampleDataTabKibanaProvider: FC = ({ - children, - ...dependencies -}) => { +export const SampleDataTabKibanaProvider: FC< + PropsWithChildren +> = ({ children, ...dependencies }) => { const { coreStart, trackUiMetric } = dependencies; const { http, notifications } = coreStart; diff --git a/packages/kbn-alerts-ui-shared/src/maintenance_window_callout/index.test.tsx b/packages/kbn-alerts-ui-shared/src/maintenance_window_callout/index.test.tsx index bb1866c82e90a..9a8cb79f48a5e 100644 --- a/packages/kbn-alerts-ui-shared/src/maintenance_window_callout/index.test.tsx +++ b/packages/kbn-alerts-ui-shared/src/maintenance_window_callout/index.test.tsx @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import React from 'react'; +import React, { FC, PropsWithChildren } from 'react'; import { I18nProvider } from '@kbn/i18n-react'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { render, waitFor, cleanup, screen } from '@testing-library/react'; @@ -24,7 +24,7 @@ jest.mock('./api', () => ({ fetchActiveMaintenanceWindows: jest.fn(() => Promise.resolve([])), })); -const TestProviders: React.FC<{}> = ({ children }) => { +const TestProviders: FC> = ({ children }) => { const queryClient = new QueryClient(); return ( @@ -231,7 +231,7 @@ describe('MaintenanceWindowCallout', () => { warn: console.warn, }, }); - const wrapper: React.FC = ({ children }) => ( + const wrapper: FC> = ({ children }) => ( {children} ); return wrapper; diff --git a/packages/kbn-cell-actions/src/context/cell_actions_context.test.tsx b/packages/kbn-cell-actions/src/context/cell_actions_context.test.tsx index 5c9084d0e81dd..2cec616176372 100644 --- a/packages/kbn-cell-actions/src/context/cell_actions_context.test.tsx +++ b/packages/kbn-cell-actions/src/context/cell_actions_context.test.tsx @@ -7,13 +7,13 @@ */ import { renderHook } from '@testing-library/react-hooks'; -import React from 'react'; +import React, { FC, PropsWithChildren } from 'react'; import { makeAction, makeActionContext } from '../mocks/helpers'; import { CellActionsProvider, useCellActionsContext } from './cell_actions_context'; const action = makeAction('action-1', 'icon', 1); const mockGetTriggerCompatibleActions = jest.fn(async () => [action]); -const ContextWrapper: React.FC = ({ children }) => ( +const ContextWrapper: FC> = ({ children }) => ( {children} diff --git a/packages/kbn-cell-actions/src/types.ts b/packages/kbn-cell-actions/src/types.ts index bd7cb69c38aac..301997618b6f3 100644 --- a/packages/kbn-cell-actions/src/types.ts +++ b/packages/kbn-cell-actions/src/types.ts @@ -5,6 +5,8 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ + +import { PropsWithChildren } from 'react'; import type { Action, ActionExecutionContext, @@ -14,13 +16,13 @@ import type { FieldSpec } from '@kbn/data-views-plugin/common'; import { Serializable } from '@kbn/utility-types'; import type { CellActionsMode } from './constants'; -export interface CellActionsProviderProps { +export type CellActionsProviderProps = PropsWithChildren<{ /** * Please assign `uiActions.getTriggerCompatibleActions` function. * This function should return a list of actions for a triggerId that are compatible with the provided context. */ getTriggerCompatibleActions: UiActionsService['getTriggerCompatibleActions']; -} +}>; type Metadata = Record; @@ -46,7 +48,7 @@ export interface CellActionsData { value: CellActionFieldValue; } -export interface CellActionsProps { +export type CellActionsProps = PropsWithChildren<{ data: CellActionsData | CellActionsData[]; /** @@ -82,7 +84,7 @@ export interface CellActionsProps { metadata?: Metadata; className?: string; -} +}>; export interface CellActionExecutionContext extends ActionExecutionContext { data: CellActionsData[]; diff --git a/packages/kbn-coloring/src/shared_components/coloring/__stories__/customizable_palette.stories.tsx b/packages/kbn-coloring/src/shared_components/coloring/__stories__/customizable_palette.stories.tsx index 5ab2de63f1dca..83accfecd4490 100644 --- a/packages/kbn-coloring/src/shared_components/coloring/__stories__/customizable_palette.stories.tsx +++ b/packages/kbn-coloring/src/shared_components/coloring/__stories__/customizable_palette.stories.tsx @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import React, { FC } from 'react'; +import React, { FC, PropsWithChildren } from 'react'; import { EuiForm } from '@elastic/eui'; import { ComponentStory } from '@storybook/react'; import { CustomizablePalette, CustomizablePaletteProps } from '../palette_configuration'; @@ -18,7 +18,7 @@ export default { decorators: [(story: Function) => {story()}], }; -const Template: ComponentStory> = (args) => ( +const Template: ComponentStory>> = (args) => ( ); diff --git a/packages/kbn-config/src/__fixtures__/en_var_with_defaults.yml b/packages/kbn-config/src/__fixtures__/en_var_with_defaults.yml new file mode 100644 index 0000000000000..250f863ff4ff6 --- /dev/null +++ b/packages/kbn-config/src/__fixtures__/en_var_with_defaults.yml @@ -0,0 +1,4 @@ +foo: 'pre-${KBN_ENV_VAR1}-mid-${KBN_ENV_VAR2:default2}-post' +nested_list: + - id: 'a' + values: ['${KBN_ENV_VAR1:default1}', '${KBN_ENV_VAR2:default2}'] diff --git a/packages/kbn-config/src/raw/read_config.test.ts b/packages/kbn-config/src/raw/read_config.test.ts index 750d10940e5ce..4a3754def8ae7 100644 --- a/packages/kbn-config/src/raw/read_config.test.ts +++ b/packages/kbn-config/src/raw/read_config.test.ts @@ -130,3 +130,26 @@ test('supports unsplittable key syntax on nested list', () => { } `); }); + +test('supports var:default syntax', () => { + process.env.KBN_ENV_VAR1 = 'val1'; + + const config = getConfigFromFiles([fixtureFile('/en_var_with_defaults.yml')]); + + delete process.env.KBN_ENV_VAR1; + + expect(config).toMatchInlineSnapshot(` + Object { + "foo": "pre-val1-mid-default2-post", + "nested_list": Array [ + Object { + "id": "a", + "values": Array [ + "val1", + "default2", + ], + }, + ], + } + `); +}); diff --git a/packages/kbn-config/src/raw/read_config.ts b/packages/kbn-config/src/raw/read_config.ts index 52ffcaa1bd190..1126ac1bc3f81 100644 --- a/packages/kbn-config/src/raw/read_config.ts +++ b/packages/kbn-config/src/raw/read_config.ts @@ -11,21 +11,10 @@ import { safeLoad } from 'js-yaml'; import { set } from '@kbn/safer-lodash-set'; import { isPlainObject } from 'lodash'; import { ensureValidObjectPath } from '@kbn/std'; -import { splitKey, getUnsplittableKey } from './utils'; +import { splitKey, getUnsplittableKey, replaceEnvVarRefs } from './utils'; const readYaml = (path: string) => safeLoad(readFileSync(path, 'utf8')); -function replaceEnvVarRefs(val: string) { - return val.replace(/\$\{(\w+)\}/g, (match, envVarName) => { - const envVarValue = process.env[envVarName]; - if (envVarValue !== undefined) { - return envVarValue; - } - - throw new Error(`Unknown environment variable referenced in config : ${envVarName}`); - }); -} - interface YamlEntry { path: string[]; value: any; diff --git a/packages/kbn-config/src/raw/utils.test.ts b/packages/kbn-config/src/raw/utils.test.ts index 26ecba09d9a98..0dc96ef4593d0 100644 --- a/packages/kbn-config/src/raw/utils.test.ts +++ b/packages/kbn-config/src/raw/utils.test.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { splitKey, getUnsplittableKey } from './utils'; +import { splitKey, getUnsplittableKey, replaceEnvVarRefs } from './utils'; describe('splitKey', () => { it('correctly splits on the dot delimiter', () => { @@ -33,3 +33,39 @@ describe('getUnsplittableKey', () => { expect(getUnsplittableKey('foo.bar')).toEqual(undefined); }); }); + +describe('replaceEnvVarRefs', () => { + it('throws an error if the variable is not defined', () => { + expect(() => replaceEnvVarRefs('${VAR_1}', {})).toThrowErrorMatchingInlineSnapshot( + `"Unknown environment variable referenced in config : VAR_1"` + ); + }); + it('replaces the environment variable with its value', () => { + expect(replaceEnvVarRefs('${VAR_1}', { VAR_1: 'foo' })).toEqual('foo'); + }); + it('replaces the environment variable within a longer string', () => { + expect(replaceEnvVarRefs('hello ${VAR_1} bar', { VAR_1: 'foo' })).toEqual('hello foo bar'); + }); + it('replaces multiple occurrences of the same variable', () => { + expect(replaceEnvVarRefs('${VAR_1}-${VAR_1}', { VAR_1: 'foo' })).toEqual('foo-foo'); + }); + it('replaces multiple occurrences of different variables', () => { + expect(replaceEnvVarRefs('${VAR_1}-${VAR_2}', { VAR_1: 'foo', VAR_2: 'bar' })).toEqual( + 'foo-bar' + ); + }); + it('uses the default value if specified and the var is not defined', () => { + expect(replaceEnvVarRefs('${VAR:default}', {})).toEqual('default'); + }); + it('uses the value from the var if specified even with a default value', () => { + expect(replaceEnvVarRefs('${VAR:default}', { VAR: 'value' })).toEqual('value'); + }); + it('supports defining a default value for multiple variables', () => { + expect(replaceEnvVarRefs('${VAR1:var}:${VAR2:var2}', {})).toEqual('var:var2'); + }); + it('only use default value for variables that are not set', () => { + expect(replaceEnvVarRefs('${VAR1:default1}:${VAR2:default2}', { VAR2: 'var2' })).toEqual( + 'default1:var2' + ); + }); +}); diff --git a/packages/kbn-config/src/raw/utils.ts b/packages/kbn-config/src/raw/utils.ts index b44a3c14477aa..6fbcd767bafc3 100644 --- a/packages/kbn-config/src/raw/utils.ts +++ b/packages/kbn-config/src/raw/utils.ts @@ -19,3 +19,25 @@ export const getUnsplittableKey = (rawKey: string): string | undefined => { } return undefined; }; + +export function replaceEnvVarRefs( + val: string, + env: { + [key: string]: string | undefined; + } = process.env +) { + return val.replace(/\$\{(\w+)(:(\w+))?\}/g, (match, ...groups) => { + const envVarName = groups[0]; + const defaultValue = groups[2]; + + const envVarValue = env[envVarName]; + if (envVarValue !== undefined) { + return envVarValue; + } + if (defaultValue !== undefined) { + return defaultValue; + } + + throw new Error(`Unknown environment variable referenced in config : ${envVarName}`); + }); +} diff --git a/packages/kbn-content-management-utils/src/saved_object_content_storage.ts b/packages/kbn-content-management-utils/src/saved_object_content_storage.ts index 070bb9cd5d739..d462c7a02b8de 100644 --- a/packages/kbn-content-management-utils/src/saved_object_content_storage.ts +++ b/packages/kbn-content-management-utils/src/saved_object_content_storage.ts @@ -65,6 +65,7 @@ function savedObjectToItem( type, updated_at: updatedAt, created_at: createdAt, + created_by: createdBy, attributes, references, error, @@ -79,6 +80,7 @@ function savedObjectToItem( managed, updatedAt, createdAt, + createdBy, attributes: pick(attributes, allowedSavedObjectAttributes), references, error, diff --git a/packages/kbn-content-management-utils/src/types.ts b/packages/kbn-content-management-utils/src/types.ts index f350da4f82cf9..72f2093acf2ad 100644 --- a/packages/kbn-content-management-utils/src/types.ts +++ b/packages/kbn-content-management-utils/src/types.ts @@ -194,6 +194,7 @@ export interface SOWithMetadata { version?: string; createdAt?: string; updatedAt?: string; + createdBy?: string; error?: { error: string; message: string; diff --git a/packages/kbn-discover-utils/index.ts b/packages/kbn-discover-utils/index.ts index 0434a05a9ce92..7a7fedfb5f1e3 100644 --- a/packages/kbn-discover-utils/index.ts +++ b/packages/kbn-discover-utils/index.ts @@ -13,7 +13,6 @@ export { DEFAULT_COLUMNS_SETTING, DOC_HIDE_TIME_COLUMN_SETTING, DOC_TABLE_LEGACY, - ENABLE_ESQL, FIELDS_LIMIT_SETTING, HIDE_ANNOUNCEMENTS, MAX_DOC_FIELDS_DISPLAYED, diff --git a/packages/kbn-discover-utils/src/constants.ts b/packages/kbn-discover-utils/src/constants.ts index 7582b610c6638..acd34290d29de 100644 --- a/packages/kbn-discover-utils/src/constants.ts +++ b/packages/kbn-discover-utils/src/constants.ts @@ -12,7 +12,6 @@ export const CONTEXT_TIE_BREAKER_FIELDS_SETTING = 'context:tieBreakerFields'; export const DEFAULT_COLUMNS_SETTING = 'defaultColumns'; export const DOC_HIDE_TIME_COLUMN_SETTING = 'doc_table:hideTimeColumn'; export const DOC_TABLE_LEGACY = 'doc_table:legacy'; -export const ENABLE_ESQL = 'discover:enableESQL'; export const FIELDS_LIMIT_SETTING = 'fields:popularLimit'; export const HIDE_ANNOUNCEMENTS = 'hideAnnouncements'; export const MAX_DOC_FIELDS_DISPLAYED = 'discover:maxDocFieldsDisplayed'; diff --git a/packages/kbn-doc-links/src/get_doc_links.ts b/packages/kbn-doc-links/src/get_doc_links.ts index 83dea1f64ceec..017d5c6b23305 100644 --- a/packages/kbn-doc-links/src/get_doc_links.ts +++ b/packages/kbn-doc-links/src/get_doc_links.ts @@ -19,6 +19,7 @@ export const getDocLinks = ({ kibanaBranch, buildFlavor }: GetDocLinkOptions): D const meta = getDocLinksMeta({ kibanaBranch, buildFlavor }); const DOC_LINK_VERSION = meta.version; + const ECS_VERSION = meta.ecs_version; const ELASTIC_WEBSITE_URL = meta.elasticWebsiteUrl; const DOCS_WEBSITE_URL = meta.docsWebsiteUrl; const ELASTIC_GITHUB = meta.elasticGithubUrl; @@ -786,7 +787,7 @@ export const getDocLinks = ({ kibanaBranch, buildFlavor }: GetDocLinkOptions): D pipelines: isServerless ? `${SERVERLESS_DOCS}ingest-pipelines` : `${ELASTICSEARCH_DOCS}ingest.html`, - csvPipelines: `${ELASTIC_WEBSITE_URL}guide/en/ecs/${DOC_LINK_VERSION}/ecs-converting.html`, + csvPipelines: `${ELASTIC_WEBSITE_URL}guide/en/ecs/${ECS_VERSION}/ecs-converting.html`, pipelineFailure: `${ELASTICSEARCH_DOCS}ingest.html#handling-pipeline-failures`, processors: `${ELASTICSEARCH_DOCS}processors.html`, remove: `${ELASTICSEARCH_DOCS}remove-processor.html`, @@ -840,7 +841,7 @@ export const getDocLinks = ({ kibanaBranch, buildFlavor }: GetDocLinkOptions): D scalingKubernetesResourcesAndLimits: `${FLEET_DOCS}scaling-on-kubernetes.html#_specifying_resources_and_limits_in_agent_manifests`, }, ecs: { - guide: `${ELASTIC_WEBSITE_URL}guide/en/ecs/current/index.html`, + guide: `${ELASTIC_WEBSITE_URL}guide/en/ecs/${ECS_VERSION}/index.html`, }, clients: { /** Changes to these URLs must also be synched in src/plugins/custom_integrations/server/language_clients/index.ts */ diff --git a/packages/kbn-doc-links/src/get_doc_meta.ts b/packages/kbn-doc-links/src/get_doc_meta.ts index 6e36aef20471f..f449f14096237 100644 --- a/packages/kbn-doc-links/src/get_doc_meta.ts +++ b/packages/kbn-doc-links/src/get_doc_meta.ts @@ -19,6 +19,7 @@ export const getDocLinksMeta = ({ }: GetDocLinksMetaOptions): DocLinksMeta => { return { version: kibanaBranch === 'main' ? 'master' : kibanaBranch, + ecs_version: 'current', elasticWebsiteUrl: 'https://www.elastic.co/', elasticGithubUrl: 'https://github.com/elastic/', docsWebsiteUrl: 'https://docs.elastic.co/', diff --git a/packages/kbn-doc-links/src/types.ts b/packages/kbn-doc-links/src/types.ts index 47de74f6219fd..bd8f353c1c591 100644 --- a/packages/kbn-doc-links/src/types.ts +++ b/packages/kbn-doc-links/src/types.ts @@ -11,6 +11,7 @@ */ export interface DocLinksMeta { version: string; + ecs_version: string; elasticWebsiteUrl: string; elasticGithubUrl: string; docsWebsiteUrl: string; diff --git a/packages/kbn-dom-drag-drop/src/drop_overlay_wrapper.tsx b/packages/kbn-dom-drag-drop/src/drop_overlay_wrapper.tsx index 65470de4d68d8..4c1d61608ba45 100644 --- a/packages/kbn-dom-drag-drop/src/drop_overlay_wrapper.tsx +++ b/packages/kbn-dom-drag-drop/src/drop_overlay_wrapper.tsx @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import React from 'react'; +import React, { PropsWithChildren } from 'react'; import classnames from 'classnames'; /** @@ -27,7 +27,7 @@ export interface DropOverlayWrapperProps { * @param otherProps * @constructor */ -export const DropOverlayWrapper: React.FC = ({ +export const DropOverlayWrapper: React.FC> = ({ isVisible, children, overlayProps, diff --git a/packages/kbn-esql-utils/constants.ts b/packages/kbn-esql-utils/constants.ts index 51dcbab83654b..a50f03ebea1e2 100644 --- a/packages/kbn-esql-utils/constants.ts +++ b/packages/kbn-esql-utils/constants.ts @@ -9,3 +9,4 @@ // we are expecting to retrieve this from an API instead // https://github.com/elastic/elasticsearch/issues/107069 export const ESQL_LATEST_VERSION = '2024.04.01'; +export const ENABLE_ESQL = 'enableESQL'; diff --git a/packages/kbn-esql-utils/index.ts b/packages/kbn-esql-utils/index.ts index 0d04f40b85612..f8e93981d9c24 100644 --- a/packages/kbn-esql-utils/index.ts +++ b/packages/kbn-esql-utils/index.ts @@ -19,4 +19,4 @@ export { TextBasedLanguages, } from './src'; -export { ESQL_LATEST_VERSION } from './constants'; +export { ESQL_LATEST_VERSION, ENABLE_ESQL } from './constants'; diff --git a/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.test.ts b/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.test.ts index 809552f02b999..4be855868d39b 100644 --- a/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.test.ts +++ b/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.test.ts @@ -559,7 +559,10 @@ describe('autocomplete', () => { } describe('sort', () => { - testSuggestions('from a | sort ', getFieldNamesByType('any')); + testSuggestions('from a | sort ', [ + ...getFieldNamesByType('any'), + ...getFunctionSignaturesByReturnType('sort', 'any', { evalMath: true }), + ]); testSuggestions('from a | sort stringField ', ['asc', 'desc', ',', '|']); testSuggestions('from a | sort stringField desc ', ['nulls first', 'nulls last', ',', '|']); // @TODO: improve here diff --git a/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.ts b/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.ts index 6c25afe41f848..71494b64cf790 100644 --- a/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.ts +++ b/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.ts @@ -594,7 +594,11 @@ async function getExpressionSuggestionsByType( option?.name, getFieldsByType, { - functions: canHaveAssignments, + // TODO instead of relying on canHaveAssignments and other command name checks + // we should have a more generic way to determine if a command can have functions. + // I think it comes down to the definition of 'column' since 'any' should always + // include functions. + functions: canHaveAssignments || command.name === 'sort', fields: !argDef.constantOnly, variables: anyVariables, literals: argDef.constantOnly, diff --git a/packages/kbn-esql-validation-autocomplete/src/definitions/builtin.ts b/packages/kbn-esql-validation-autocomplete/src/definitions/builtin.ts index 805bec79b03e4..20adda63602df 100644 --- a/packages/kbn-esql-validation-autocomplete/src/definitions/builtin.ts +++ b/packages/kbn-esql-validation-autocomplete/src/definitions/builtin.ts @@ -19,7 +19,7 @@ function createMathDefinition( type: 'builtin', name, description, - supportedCommands: ['eval', 'where', 'row', 'stats'], + supportedCommands: ['eval', 'where', 'row', 'stats', 'sort'], supportedOptions: ['by'], signatures: types.map((type) => { if (Array.isArray(type)) { @@ -59,7 +59,7 @@ function createComparisonDefinition( type: 'builtin' as const, name, description, - supportedCommands: ['eval', 'where', 'row'], + supportedCommands: ['eval', 'where', 'row', 'sort'], supportedOptions: ['by'], validate, signatures: [ @@ -296,7 +296,7 @@ export const builtinFunctions: FunctionDefinition[] = [ ignoreAsSuggestion: /not/.test(name), name, description, - supportedCommands: ['eval', 'where', 'row'], + supportedCommands: ['eval', 'where', 'row', 'sort'], supportedOptions: ['by'], signatures: [ { @@ -322,7 +322,7 @@ export const builtinFunctions: FunctionDefinition[] = [ ignoreAsSuggestion: /not/.test(name), name, description, - supportedCommands: ['eval', 'where', 'row'], + supportedCommands: ['eval', 'where', 'row', 'sort'], signatures: [ { params: [ @@ -371,7 +371,7 @@ export const builtinFunctions: FunctionDefinition[] = [ type: 'builtin' as const, name, description, - supportedCommands: ['eval', 'where', 'row'], + supportedCommands: ['eval', 'where', 'row', 'sort'], supportedOptions: ['by'], signatures: [ { @@ -410,7 +410,7 @@ export const builtinFunctions: FunctionDefinition[] = [ description: i18n.translate('kbn-esql-validation-autocomplete.esql.definition.notDoc', { defaultMessage: 'Not', }), - supportedCommands: ['eval', 'where', 'row'], + supportedCommands: ['eval', 'where', 'row', 'sort'], supportedOptions: ['by'], signatures: [ { @@ -436,7 +436,7 @@ export const builtinFunctions: FunctionDefinition[] = [ type: 'builtin', name, description, - supportedCommands: ['eval', 'where', 'row'], + supportedCommands: ['eval', 'where', 'row', 'sort'], signatures: [ { params: [{ name: 'left', type: 'any' }], diff --git a/packages/kbn-esql-validation-autocomplete/src/definitions/commands.ts b/packages/kbn-esql-validation-autocomplete/src/definitions/commands.ts index 84e85c4d6e797..861d18110d08f 100644 --- a/packages/kbn-esql-validation-autocomplete/src/definitions/commands.ts +++ b/packages/kbn-esql-validation-autocomplete/src/definitions/commands.ts @@ -330,13 +330,14 @@ export const commandDefinitions: CommandDefinition[] = [ '… | sort a desc, b nulls last, c asc nulls first', '… | sort b nulls last', '… | sort c asc nulls first', + '… | sort a - abs(b)', ], options: [], modes: [], signature: { multipleParams: true, params: [ - { name: 'column', type: 'column' }, + { name: 'expression', type: 'any' }, { name: 'direction', type: 'string', optional: true, values: ['asc', 'desc'] }, { name: 'nulls', type: 'string', optional: true, values: ['nulls first', 'nulls last'] }, ], diff --git a/packages/kbn-esql-validation-autocomplete/src/definitions/functions.ts b/packages/kbn-esql-validation-autocomplete/src/definitions/functions.ts index 57dcb9acf93b1..d0b9110252b49 100644 --- a/packages/kbn-esql-validation-autocomplete/src/definitions/functions.ts +++ b/packages/kbn-esql-validation-autocomplete/src/definitions/functions.ts @@ -1692,7 +1692,7 @@ export const evalFunctionsDefinitions: FunctionDefinition[] = [ .sort(({ name: a }, { name: b }) => a.localeCompare(b)) .map((def) => ({ ...def, - supportedCommands: ['stats', 'eval', 'where', 'row'], + supportedCommands: ['stats', 'eval', 'where', 'row', 'sort'], supportedOptions: ['by'], type: 'eval', })); diff --git a/packages/kbn-esql-validation-autocomplete/src/validation/esql_validation_meta_tests.json b/packages/kbn-esql-validation-autocomplete/src/validation/esql_validation_meta_tests.json index bbe3903c1aa07..6b8c666dd6b5b 100644 --- a/packages/kbn-esql-validation-autocomplete/src/validation/esql_validation_meta_tests.json +++ b/packages/kbn-esql-validation-autocomplete/src/validation/esql_validation_meta_tests.json @@ -16092,6 +16092,509 @@ "error": [], "warning": [] }, + { + "query": "from a_index | sort abs(numberField) - to_long(stringField) desc nulls first", + "error": [], + "warning": [] + }, + { + "query": "from a_index | sort avg(numberField)", + "error": [ + "SORT does not support function avg" + ], + "warning": [] + }, + { + "query": "from a_index | sort sum(numberField)", + "error": [ + "SORT does not support function sum" + ], + "warning": [] + }, + { + "query": "from a_index | sort median(numberField)", + "error": [ + "SORT does not support function median" + ], + "warning": [] + }, + { + "query": "from a_index | sort median_absolute_deviation(numberField)", + "error": [ + "SORT does not support function median_absolute_deviation" + ], + "warning": [] + }, + { + "query": "from a_index | sort percentile(numberField, 5)", + "error": [ + "SORT does not support function percentile" + ], + "warning": [] + }, + { + "query": "from a_index | sort max(numberField)", + "error": [ + "SORT does not support function max" + ], + "warning": [] + }, + { + "query": "from a_index | sort min(numberField)", + "error": [ + "SORT does not support function min" + ], + "warning": [] + }, + { + "query": "from a_index | sort count(stringField)", + "error": [ + "SORT does not support function count" + ], + "warning": [] + }, + { + "query": "from a_index | sort count_distinct(stringField, numberField)", + "error": [ + "SORT does not support function count_distinct" + ], + "warning": [] + }, + { + "query": "from a_index | sort st_centroid_agg(cartesianPointField)", + "error": [ + "SORT does not support function st_centroid_agg" + ], + "warning": [] + }, + { + "query": "from a_index | sort values(stringField)", + "error": [ + "SORT does not support function values" + ], + "warning": [] + }, + { + "query": "from a_index | sort bucket(dateField, 1 year)", + "error": [ + "SORT does not support function bucket" + ], + "warning": [] + }, + { + "query": "from a_index | sort abs(numberField)", + "error": [], + "warning": [] + }, + { + "query": "from a_index | sort acos(numberField)", + "error": [], + "warning": [] + }, + { + "query": "from a_index | sort asin(numberField)", + "error": [], + "warning": [] + }, + { + "query": "from a_index | sort atan(numberField)", + "error": [], + "warning": [] + }, + { + "query": "from a_index | sort atan2(numberField, numberField)", + "error": [], + "warning": [] + }, + { + "query": "from a_index | sort case(booleanField, stringField)", + "error": [], + "warning": [] + }, + { + "query": "from a_index | sort ceil(numberField)", + "error": [], + "warning": [] + }, + { + "query": "from a_index | sort cidr_match(ipField, stringField)", + "error": [], + "warning": [] + }, + { + "query": "from a_index | sort coalesce(stringField)", + "error": [], + "warning": [] + }, + { + "query": "from a_index | sort concat(stringField, stringField)", + "error": [], + "warning": [] + }, + { + "query": "from a_index | sort cos(numberField)", + "error": [], + "warning": [] + }, + { + "query": "from a_index | sort cosh(numberField)", + "error": [], + "warning": [] + }, + { + "query": "from a_index | sort date_extract(\"ALIGNED_DAY_OF_WEEK_IN_MONTH\", dateField)", + "error": [], + "warning": [] + }, + { + "query": "from a_index | sort date_format(dateField, stringField)", + "error": [], + "warning": [] + }, + { + "query": "from a_index | sort date_parse(stringField, stringField)", + "error": [], + "warning": [] + }, + { + "query": "from a_index | sort date_trunc(1 year, dateField)", + "error": [], + "warning": [] + }, + { + "query": "from a_index | sort e()", + "error": [], + "warning": [] + }, + { + "query": "from a_index | sort ends_with(stringField, stringField)", + "error": [], + "warning": [] + }, + { + "query": "from a_index | sort floor(numberField)", + "error": [], + "warning": [] + }, + { + "query": "from a_index | sort greatest(stringField)", + "error": [], + "warning": [] + }, + { + "query": "from a_index | sort least(stringField)", + "error": [], + "warning": [] + }, + { + "query": "from a_index | sort left(stringField, numberField)", + "error": [], + "warning": [] + }, + { + "query": "from a_index | sort length(stringField)", + "error": [], + "warning": [] + }, + { + "query": "from a_index | sort log(numberField, numberField)", + "error": [], + "warning": [] + }, + { + "query": "from a_index | sort log10(numberField)", + "error": [], + "warning": [] + }, + { + "query": "from a_index | sort ltrim(stringField)", + "error": [], + "warning": [] + }, + { + "query": "from a_index | sort mv_avg(numberField)", + "error": [], + "warning": [] + }, + { + "query": "from a_index | sort mv_concat(stringField, stringField)", + "error": [], + "warning": [] + }, + { + "query": "from a_index | sort mv_count(stringField)", + "error": [], + "warning": [] + }, + { + "query": "from a_index | sort mv_dedupe(stringField)", + "error": [], + "warning": [] + }, + { + "query": "from a_index | sort mv_first(stringField)", + "error": [], + "warning": [] + }, + { + "query": "from a_index | sort mv_last(stringField)", + "error": [], + "warning": [] + }, + { + "query": "from a_index | sort mv_max(stringField)", + "error": [], + "warning": [] + }, + { + "query": "from a_index | sort mv_median(numberField)", + "error": [], + "warning": [] + }, + { + "query": "from a_index | sort mv_min(stringField)", + "error": [], + "warning": [] + }, + { + "query": "from a_index | sort mv_slice(stringField, numberField, numberField)", + "error": [], + "warning": [] + }, + { + "query": "from a_index | sort mv_sort(stringField, \"asc\")", + "error": [], + "warning": [] + }, + { + "query": "from a_index | sort mv_sum(numberField)", + "error": [], + "warning": [] + }, + { + "query": "from a_index | sort mv_zip(stringField, stringField, stringField)", + "error": [], + "warning": [] + }, + { + "query": "from a_index | sort now()", + "error": [], + "warning": [] + }, + { + "query": "from a_index | sort pi()", + "error": [], + "warning": [] + }, + { + "query": "from a_index | sort pow(numberField, numberField)", + "error": [], + "warning": [] + }, + { + "query": "from a_index | sort replace(stringField, stringField, stringField)", + "error": [], + "warning": [] + }, + { + "query": "from a_index | sort right(stringField, numberField)", + "error": [], + "warning": [] + }, + { + "query": "from a_index | sort round(numberField, numberField)", + "error": [], + "warning": [] + }, + { + "query": "from a_index | sort rtrim(stringField)", + "error": [], + "warning": [] + }, + { + "query": "from a_index | sort signum(numberField)", + "error": [], + "warning": [] + }, + { + "query": "from a_index | sort sin(numberField)", + "error": [], + "warning": [] + }, + { + "query": "from a_index | sort sinh(numberField)", + "error": [], + "warning": [] + }, + { + "query": "from a_index | sort split(stringField, stringField)", + "error": [], + "warning": [] + }, + { + "query": "from a_index | sort sqrt(numberField)", + "error": [], + "warning": [] + }, + { + "query": "from a_index | sort st_contains(geoPointField, geoPointField)", + "error": [], + "warning": [] + }, + { + "query": "from a_index | sort st_disjoint(geoPointField, geoPointField)", + "error": [], + "warning": [] + }, + { + "query": "from a_index | sort st_intersects(geoPointField, geoPointField)", + "error": [], + "warning": [] + }, + { + "query": "from a_index | sort st_within(geoPointField, geoPointField)", + "error": [], + "warning": [] + }, + { + "query": "from a_index | sort st_x(geoPointField)", + "error": [], + "warning": [] + }, + { + "query": "from a_index | sort st_y(geoPointField)", + "error": [], + "warning": [] + }, + { + "query": "from a_index | sort starts_with(stringField, stringField)", + "error": [], + "warning": [] + }, + { + "query": "from a_index | sort substring(stringField, numberField, numberField)", + "error": [], + "warning": [] + }, + { + "query": "from a_index | sort tan(numberField)", + "error": [], + "warning": [] + }, + { + "query": "from a_index | sort tanh(numberField)", + "error": [], + "warning": [] + }, + { + "query": "from a_index | sort tau()", + "error": [], + "warning": [] + }, + { + "query": "from a_index | sort to_boolean(stringField)", + "error": [], + "warning": [] + }, + { + "query": "from a_index | sort to_cartesianpoint(stringField)", + "error": [], + "warning": [] + }, + { + "query": "from a_index | sort to_cartesianshape(stringField)", + "error": [], + "warning": [] + }, + { + "query": "from a_index | sort to_datetime(stringField)", + "error": [], + "warning": [] + }, + { + "query": "from a_index | sort to_degrees(numberField)", + "error": [], + "warning": [] + }, + { + "query": "from a_index | sort to_double(stringField)", + "error": [], + "warning": [] + }, + { + "query": "from a_index | sort to_geopoint(stringField)", + "error": [], + "warning": [] + }, + { + "query": "from a_index | sort to_geoshape(stringField)", + "error": [], + "warning": [] + }, + { + "query": "from a_index | sort to_integer(stringField)", + "error": [], + "warning": [] + }, + { + "query": "from a_index | sort to_ip(stringField)", + "error": [], + "warning": [] + }, + { + "query": "from a_index | sort to_long(stringField)", + "error": [], + "warning": [] + }, + { + "query": "from a_index | sort to_lower(stringField)", + "error": [], + "warning": [] + }, + { + "query": "from a_index | sort to_radians(numberField)", + "error": [], + "warning": [] + }, + { + "query": "from a_index | sort to_string(stringField)", + "error": [], + "warning": [] + }, + { + "query": "from a_index | sort to_unsigned_long(stringField)", + "error": [], + "warning": [] + }, + { + "query": "from a_index | sort to_upper(stringField)", + "error": [], + "warning": [] + }, + { + "query": "from a_index | sort to_version(stringField)", + "error": [], + "warning": [] + }, + { + "query": "from a_index | sort trim(stringField)", + "error": [], + "warning": [] + }, + { + "query": "from a_index | sort sin(stringField)", + "error": [ + "Argument of [sin] must be [number], found value [stringField] type [string]" + ], + "warning": [] + }, + { + "query": "from a_index | sort numberField + stringField", + "error": [ + "Argument of [+] must be [number], found value [stringField] type [string]" + ], + "warning": [] + }, { "query": "from a_index | enrich", "error": [ diff --git a/packages/kbn-esql-validation-autocomplete/src/validation/validation.test.ts b/packages/kbn-esql-validation-autocomplete/src/validation/validation.test.ts index 63abfc9dff036..dc37f5b2fd1ff 100644 --- a/packages/kbn-esql-validation-autocomplete/src/validation/validation.test.ts +++ b/packages/kbn-esql-validation-autocomplete/src/validation/validation.test.ts @@ -2455,6 +2455,58 @@ describe('validation logic', () => { } testErrorsAndWarnings(`row a = 1 | stats COUNT(*) | sort \`COUNT(*)\``, []); testErrorsAndWarnings(`ROW a = 1 | STATS couNt(*) | SORT \`couNt(*)\``, []); + + describe('sorting by expressions', () => { + // SORT accepts complex expressions + testErrorsAndWarnings( + 'from a_index | sort abs(numberField) - to_long(stringField) desc nulls first', + [] + ); + + // SORT doesn't accept agg or grouping functions + for (const definition of [ + ...statsAggregationFunctionDefinitions, + ...groupingFunctionDefinitions, + ]) { + const { + name, + signatures: [firstSignature], + } = definition; + const fieldMapping = getFieldMapping(firstSignature.params); + const printedInvocation = getFunctionSignatures( + { ...definition, signatures: [{ ...firstSignature, params: fieldMapping }] }, + { withTypes: false } + )[0].declaration; + + testErrorsAndWarnings(`from a_index | sort ${printedInvocation}`, [ + `SORT does not support function ${name}`, + ]); + } + + // But does accept eval functions + for (const definition of evalFunctionsDefinitions) { + const { + signatures: [firstSignature], + } = definition; + const fieldMapping = getFieldMapping(firstSignature.params); + const printedInvocation = getFunctionSignatures( + { ...definition, signatures: [{ ...firstSignature, params: fieldMapping }] }, + { withTypes: false } + )[0].declaration; + + testErrorsAndWarnings(`from a_index | sort ${printedInvocation}`, []); + } + + // Expression parts are also validated + testErrorsAndWarnings('from a_index | sort sin(stringField)', [ + 'Argument of [sin] must be [number], found value [stringField] type [string]', + ]); + + // Expression parts are also validated + testErrorsAndWarnings('from a_index | sort numberField + stringField', [ + 'Argument of [+] must be [number], found value [stringField] type [string]', + ]); + }); }); describe('enrich', () => { diff --git a/packages/kbn-i18n-react/src/provider.tsx b/packages/kbn-i18n-react/src/provider.tsx index 5e60188d3bdd4..f31cc6dfb250c 100644 --- a/packages/kbn-i18n-react/src/provider.tsx +++ b/packages/kbn-i18n-react/src/provider.tsx @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import React from 'react'; +import React, { FC, PropsWithChildren } from 'react'; // eslint-disable-next-line @kbn/eslint/module_migration import { IntlProvider } from 'react-intl'; @@ -20,7 +20,7 @@ import { PseudoLocaleWrapper } from './pseudo_locale_wrapper'; * IntlProvider should wrap react app's root component (inside each react render method). */ -export const I18nProvider: React.FC = ({ children }) => ( +export const I18nProvider: FC> = ({ children }) => ( (null); /** * A Context Provider that provides services to the component and its dependencies. */ -export const SettingsApplicationProvider: FC = ({ +export const SettingsApplicationProvider: FC> = ({ children, ...services }) => { @@ -114,10 +114,9 @@ export const SettingsApplicationProvider: FC = ({ /** * Kibana-specific Provider that maps dependencies to services. */ -export const SettingsApplicationKibanaProvider: FC = ({ - children, - ...dependencies -}) => { +export const SettingsApplicationKibanaProvider: FC< + PropsWithChildren +> = ({ children, ...dependencies }) => { const { docLinks, notifications, diff --git a/packages/kbn-management/settings/components/field_input/services.tsx b/packages/kbn-management/settings/components/field_input/services.tsx index d19278268b2a6..e1ae741f14837 100644 --- a/packages/kbn-management/settings/components/field_input/services.tsx +++ b/packages/kbn-management/settings/components/field_input/services.tsx @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import React, { FC, useContext } from 'react'; +import React, { FC, PropsWithChildren, useContext } from 'react'; import type { FieldInputServices, FieldInputKibanaDependencies } from './types'; const FieldInputContext = React.createContext(null); @@ -14,7 +14,10 @@ const FieldInputContext = React.createContext(null); /** * React Provider that provides services to a {@link FieldInput} component and its dependents. */ -export const FieldInputProvider: FC = ({ children, ...services }) => { +export const FieldInputProvider: FC> = ({ + children, + ...services +}) => { // Typescript types are widened to accept more than what is needed. Take only what is necessary // so the context remains clean. const { showDanger, validateChange } = services; @@ -29,7 +32,7 @@ export const FieldInputProvider: FC = ({ children, ...servic /** * Kibana-specific Provider that maps Kibana plugins and services to a {@link FieldInputProvider}. */ -export const FieldInputKibanaProvider: FC = ({ +export const FieldInputKibanaProvider: FC> = ({ children, notifications: { toasts }, settings: { client }, diff --git a/packages/kbn-management/settings/components/field_row/services.tsx b/packages/kbn-management/settings/components/field_row/services.tsx index 6350b7f7cd103..bcf94c12aa04a 100644 --- a/packages/kbn-management/settings/components/field_row/services.tsx +++ b/packages/kbn-management/settings/components/field_row/services.tsx @@ -10,7 +10,7 @@ import { FieldInputKibanaProvider, FieldInputProvider, } from '@kbn/management-settings-components-field-input/services'; -import React, { FC, useContext } from 'react'; +import React, { FC, PropsWithChildren, useContext } from 'react'; import type { FieldRowServices, FieldRowKibanaDependencies, Services } from './types'; @@ -41,7 +41,7 @@ export const FieldRowProvider = ({ children, ...services }: FieldRowProviderProp /** * Kibana-specific Provider that maps Kibana plugins and services to a {@link FieldRowProvider}. */ -export const FieldRowKibanaProvider: FC = ({ +export const FieldRowKibanaProvider: FC> = ({ children, docLinks, notifications, diff --git a/packages/kbn-management/settings/components/form/services.tsx b/packages/kbn-management/settings/components/form/services.tsx index ae2681fe88fcf..f59adc304db8d 100644 --- a/packages/kbn-management/settings/components/form/services.tsx +++ b/packages/kbn-management/settings/components/form/services.tsx @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import React, { FC, useContext } from 'react'; +import React, { FC, PropsWithChildren, useContext } from 'react'; import { FieldCategoryKibanaProvider, @@ -41,7 +41,10 @@ export const FormProvider = ({ children, ...services }: FormProviderProps) => { /** * Kibana-specific Provider that maps Kibana plugins and services to a {@link FormProvider}. */ -export const FormKibanaProvider: FC = ({ children, ...deps }) => { +export const FormKibanaProvider: FC> = ({ + children, + ...deps +}) => { const { settings, notifications, docLinks, theme, i18n } = deps; const services: Services = { diff --git a/packages/kbn-management/settings/setting_ids/index.ts b/packages/kbn-management/settings/setting_ids/index.ts index bd026c15c54db..f4a30dac6cb12 100644 --- a/packages/kbn-management/settings/setting_ids/index.ts +++ b/packages/kbn-management/settings/setting_ids/index.ts @@ -73,7 +73,7 @@ export const CONTEXT_DEFAULT_SIZE_ID = 'context:defaultSize'; export const CONTEXT_STEP_ID = 'context:step'; export const CONTEXT_TIE_BREAKER_FIELDS_ID = 'context:tieBreakerFields'; export const DEFAULT_COLUMNS_ID = 'defaultColumns'; -export const DISCOVER_ENABLE_SQL_ID = 'discover:enableSql'; +export const ENABLE_ESQL_ID = 'enableESQL'; export const DISCOVER_MAX_DOC_FIELDS_DISPLAYED_ID = 'discover:maxDocFieldsDisplayed'; export const DISCOVER_MODIFY_COLUMNS_ON_SWITCH_ID = 'discover:modifyColumnsOnSwitch'; export const DISCOVER_ROW_HEIGHT_OPTION_ID = 'discover:rowHeightOption'; diff --git a/packages/kbn-monaco/index.ts b/packages/kbn-monaco/index.ts index fad82d4d4a143..e02ae6dbdfe16 100644 --- a/packages/kbn-monaco/index.ts +++ b/packages/kbn-monaco/index.ts @@ -36,7 +36,6 @@ export { CONSOLE_LANG_ID, CONSOLE_OUTPUT_LANG_ID, CONSOLE_THEME_ID, - CONSOLE_OUTPUT_THEME_ID, getParsedRequestsProvider, ConsoleParsedRequestsProvider, } from './src/console'; diff --git a/packages/kbn-monaco/src/console/constants.ts b/packages/kbn-monaco/src/console/constants.ts index c6563efba93ac..ed29ebd93f10a 100644 --- a/packages/kbn-monaco/src/console/constants.ts +++ b/packages/kbn-monaco/src/console/constants.ts @@ -9,5 +9,4 @@ export const CONSOLE_LANG_ID = 'console'; export const CONSOLE_THEME_ID = 'consoleTheme'; export const CONSOLE_OUTPUT_LANG_ID = 'consoleOutput'; -export const CONSOLE_OUTPUT_THEME_ID = 'consoleOutputTheme'; export const CONSOLE_POSTFIX = '.console'; diff --git a/packages/kbn-monaco/src/console/index.ts b/packages/kbn-monaco/src/console/index.ts index 00f6cb68de64a..5b4127191802d 100644 --- a/packages/kbn-monaco/src/console/index.ts +++ b/packages/kbn-monaco/src/console/index.ts @@ -20,14 +20,9 @@ import { consoleOutputLanguageConfiguration, } from './lexer_rules'; -export { - CONSOLE_LANG_ID, - CONSOLE_OUTPUT_LANG_ID, - CONSOLE_THEME_ID, - CONSOLE_OUTPUT_THEME_ID, -} from './constants'; +export { CONSOLE_LANG_ID, CONSOLE_OUTPUT_LANG_ID, CONSOLE_THEME_ID } from './constants'; -export { buildConsoleTheme, buildConsoleOutputTheme } from './theme'; +export { buildConsoleTheme } from './theme'; export const ConsoleLang: LangModuleType = { ID: CONSOLE_LANG_ID, diff --git a/packages/kbn-monaco/src/console/theme.ts b/packages/kbn-monaco/src/console/theme.ts new file mode 100644 index 0000000000000..66e2bfab812bd --- /dev/null +++ b/packages/kbn-monaco/src/console/theme.ts @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { makeHighContrastColor } from '@elastic/eui'; +import { euiThemeVars } from '@kbn/ui-theme'; +import { darkMode } from '@kbn/ui-theme'; +import { buildLightTheme, buildDarkTheme } from '../code_editor'; +import { themeRuleGroupBuilderFactory } from '../common/theme'; +import { monaco } from '../monaco_imports'; + +const buildRuleGroup = themeRuleGroupBuilderFactory(); + +const background = euiThemeVars.euiFormBackgroundColor; +const booleanTextColor = '#585CF6'; +const methodTextColor = '#DD0A73'; +const urlTextColor = '#00A69B'; +export const buildConsoleTheme = (): monaco.editor.IStandaloneThemeData => { + const euiTheme = darkMode ? buildDarkTheme() : buildLightTheme(); + return { + ...euiTheme, + rules: [ + ...euiTheme.rules, + ...buildRuleGroup( + ['string-literal', 'multi-string', 'punctuation.end-triple-quote'], + makeHighContrastColor(euiThemeVars.euiColorDangerText)(background) + ), + ...buildRuleGroup( + ['constant.language.boolean'], + makeHighContrastColor(booleanTextColor)(background) + ), + ...buildRuleGroup( + ['constant.numeric'], + makeHighContrastColor(euiThemeVars.euiColorAccentText)(background) + ), + ...buildRuleGroup(['method'], makeHighContrastColor(methodTextColor)(background)), + ...buildRuleGroup(['url'], makeHighContrastColor(urlTextColor)(background)), + ], + }; +}; diff --git a/packages/kbn-monaco/src/console/theme/editor_theme.ts b/packages/kbn-monaco/src/console/theme/editor_theme.ts deleted file mode 100644 index f01c34919bb09..0000000000000 --- a/packages/kbn-monaco/src/console/theme/editor_theme.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 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 { makeHighContrastColor } from '@elastic/eui'; -import { euiThemeVars } from '@kbn/ui-theme'; - -import { themeRuleGroupBuilderFactory } from '../../common/theme'; -import { monaco } from '../../monaco_imports'; -import { buildConsoleSharedTheme } from './shared'; - -const buildRuleGroup = themeRuleGroupBuilderFactory(); - -const background = euiThemeVars.euiColorLightestShade; -const methodTextColor = '#DD0A73'; -const urlTextColor = '#00A69B'; -export const buildConsoleTheme = (): monaco.editor.IStandaloneThemeData => { - const sharedTheme = buildConsoleSharedTheme(); - return { - ...sharedTheme, - rules: [ - ...sharedTheme.rules, - ...buildRuleGroup(['method'], makeHighContrastColor(methodTextColor)(background)), - ...buildRuleGroup(['url'], makeHighContrastColor(urlTextColor)(background)), - ], - }; -}; diff --git a/packages/kbn-monaco/src/console/theme/shared.ts b/packages/kbn-monaco/src/console/theme/shared.ts deleted file mode 100644 index 3bdb543f6ef9d..0000000000000 --- a/packages/kbn-monaco/src/console/theme/shared.ts +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { makeHighContrastColor } from '@elastic/eui'; -import { darkMode, euiThemeVars } from '@kbn/ui-theme'; - -import { themeRuleGroupBuilderFactory } from '../../common/theme'; -import { monaco } from '../../monaco_imports'; - -const buildRuleGroup = themeRuleGroupBuilderFactory(); - -const background = euiThemeVars.euiColorLightestShade; -const stringTextColor = '#009926'; -const commentTextColor = '#4C886B'; -const variableTextColor = '#0079A5'; -const booleanTextColor = '#585CF6'; -const numericTextColor = variableTextColor; -export const buildConsoleSharedTheme = (): monaco.editor.IStandaloneThemeData => { - return { - base: darkMode ? 'vs-dark' : 'vs', - inherit: true, - rules: [ - ...buildRuleGroup( - ['string', 'string-literal', 'multi-string', 'punctuation.end-triple-quote'], - makeHighContrastColor(stringTextColor)(background) - ), - ...buildRuleGroup(['comment'], makeHighContrastColor(commentTextColor)(background)), - ...buildRuleGroup(['variable'], makeHighContrastColor(variableTextColor)(background)), - ...buildRuleGroup( - ['constant.language.boolean'], - makeHighContrastColor(booleanTextColor)(background) - ), - ...buildRuleGroup(['constant.numeric'], makeHighContrastColor(numericTextColor)(background)), - ], - colors: { - 'editor.background': background, - // color of the line numbers - 'editorLineNumber.foreground': euiThemeVars.euiColorDarkShade, - // color of the active line number - 'editorLineNumber.activeForeground': euiThemeVars.euiColorDarkShade, - // background of the line numbers side panel - 'editorGutter.background': euiThemeVars.euiColorEmptyShade, - }, - }; -}; diff --git a/packages/kbn-monaco/src/register_globals.ts b/packages/kbn-monaco/src/register_globals.ts index 7e12ee24b386a..0b1bcd44f2554 100644 --- a/packages/kbn-monaco/src/register_globals.ts +++ b/packages/kbn-monaco/src/register_globals.ts @@ -13,14 +13,7 @@ import { monaco } from './monaco_imports'; import { ESQL_THEME_ID, ESQLLang, buildESQlTheme } from './esql'; import { YAML_LANG_ID } from './yaml'; import { registerLanguage, registerTheme } from './helpers'; -import { - ConsoleLang, - ConsoleOutputLang, - CONSOLE_THEME_ID, - CONSOLE_OUTPUT_THEME_ID, - buildConsoleTheme, - buildConsoleOutputTheme, -} from './console'; +import { ConsoleLang, ConsoleOutputLang, CONSOLE_THEME_ID, buildConsoleTheme } from './console'; import { CODE_EDITOR_LIGHT_THEME_ID, CODE_EDITOR_DARK_THEME_ID, @@ -58,7 +51,6 @@ registerLanguage(ConsoleOutputLang); */ registerTheme(ESQL_THEME_ID, buildESQlTheme()); registerTheme(CONSOLE_THEME_ID, buildConsoleTheme()); -registerTheme(CONSOLE_OUTPUT_THEME_ID, buildConsoleOutputTheme()); registerTheme(CODE_EDITOR_LIGHT_THEME_ID, buildLightTheme()); registerTheme(CODE_EDITOR_DARK_THEME_ID, buildDarkTheme()); registerTheme(CODE_EDITOR_LIGHT_THEME_TRANSPARENT_ID, buildLightTransparentTheme()); diff --git a/packages/kbn-monaco/tsconfig.json b/packages/kbn-monaco/tsconfig.json index 4df3aa9b486b3..d39ed146027a9 100644 --- a/packages/kbn-monaco/tsconfig.json +++ b/packages/kbn-monaco/tsconfig.json @@ -26,7 +26,7 @@ "@kbn/repo-info", "@kbn/ui-theme", "@kbn/esql-ast", - "@kbn/esql-validation-autocomplete" + "@kbn/esql-validation-autocomplete", ], "exclude": [ "target/**/*", diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index f1b24fb01b556..8ee32095e8789 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -8,7 +8,7 @@ pageLoadAssetSize: assetManager: 25000 banners: 17946 bfetch: 22837 - canvas: 1066647 + canvas: 29355 cases: 180037 charts: 55000 cloud: 21076 @@ -25,13 +25,13 @@ pageLoadAssetSize: core: 435325 crossClusterReplication: 65408 customIntegrations: 22034 - dashboard: 82025 + dashboard: 52967 dashboardEnhanced: 65646 data: 454087 datasetQuality: 50624 dataViewEditor: 28082 dataViewFieldEditor: 27000 - dataViewManagement: 5176 + dataViewManagement: 5300 dataViews: 65000 dataVisualizer: 27530 devTools: 38637 @@ -51,7 +51,7 @@ pageLoadAssetSize: expressionLegacyMetricVis: 23121 expressionMetric: 22238 expressionMetricVis: 23121 - expressionPartitionVis: 29000 + expressionPartitionVis: 29700 expressionRepeatImage: 22341 expressionRevealImage: 25675 expressions: 140958 diff --git a/packages/kbn-reporting/public/context.tsx b/packages/kbn-reporting/public/context.tsx index 667607fc0396d..f8cfcaff68171 100644 --- a/packages/kbn-reporting/public/context.tsx +++ b/packages/kbn-reporting/public/context.tsx @@ -7,7 +7,7 @@ */ import type { HttpSetup } from '@kbn/core/public'; -import React, { createContext, useContext, type FunctionComponent } from 'react'; +import React, { createContext, useContext, type FC, type PropsWithChildren } from 'react'; import { ReportingAPIClient } from './reporting_api_client'; interface ContextValue { @@ -17,10 +17,12 @@ interface ContextValue { const InternalApiClientContext = createContext(undefined); -export const InternalApiClientProvider: FunctionComponent<{ - apiClient: ReportingAPIClient; - http: HttpSetup; -}> = ({ apiClient, http, children }) => { +export const InternalApiClientProvider: FC< + PropsWithChildren<{ + apiClient: ReportingAPIClient; + http: HttpSetup; + }> +> = ({ apiClient, http, children }) => { return ( {children} diff --git a/packages/kbn-search-api-panels/components/overview_panel.tsx b/packages/kbn-search-api-panels/components/overview_panel.tsx index 201c67d5852bc..3890f96ff60d3 100644 --- a/packages/kbn-search-api-panels/components/overview_panel.tsx +++ b/packages/kbn-search-api-panels/components/overview_panel.tsx @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import React, { Fragment } from 'react'; +import React, { Fragment, FC, PropsWithChildren } from 'react'; import { EuiFlexGroup, @@ -29,7 +29,7 @@ interface OverviewPanelProps { overviewPanelProps?: Partial; } -export const OverviewPanel: React.FC = ({ +export const OverviewPanel: FC> = ({ children, description, leftPanelContent, diff --git a/packages/kbn-search-api-panels/components/select_client.tsx b/packages/kbn-search-api-panels/components/select_client.tsx index 59377d384e9d2..1dcd5c0458daf 100644 --- a/packages/kbn-search-api-panels/components/select_client.tsx +++ b/packages/kbn-search-api-panels/components/select_client.tsx @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import React from 'react'; +import React, { FC, PropsWithChildren } from 'react'; import { EuiCallOut, @@ -34,9 +34,10 @@ export interface SelectClientPanelProps { application?: ApplicationStart; consolePlugin?: ConsolePluginStart; sharePlugin: SharePluginStart; + children: React.ReactNode; } -export const SelectClientPanel: React.FC = ({ +export const SelectClientPanel: FC> = ({ docLinks, children, isPanelLeft = true, diff --git a/packages/kbn-search-connectors/components/configuration/connector_configuration.tsx b/packages/kbn-search-connectors/components/configuration/connector_configuration.tsx index 1377626f3cd58..49b98ed6a2ea3 100644 --- a/packages/kbn-search-connectors/components/configuration/connector_configuration.tsx +++ b/packages/kbn-search-connectors/components/configuration/connector_configuration.tsx @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import React, { createContext, useEffect, useRef, useState } from 'react'; +import React, { createContext, useEffect, useRef, useState, FC, PropsWithChildren } from 'react'; import { EuiButton, @@ -46,6 +46,7 @@ interface ConnectorConfigurationProps { saveConfig: (configuration: Record) => void; stackManagementLink?: string; subscriptionLink?: string; + children?: React.ReactNode; } interface ConfigEntry extends ConnectorConfigProperties { @@ -80,7 +81,9 @@ export const LicenseContext = createContext<{ stackManagementLink: undefined, }); -export const ConnectorConfigurationComponent: React.FC = ({ +export const ConnectorConfigurationComponent: FC< + PropsWithChildren +> = ({ children, connector, hasPlatinumLicense, diff --git a/packages/kbn-search-connectors/components/scheduling/connector_scheduling.tsx b/packages/kbn-search-connectors/components/scheduling/connector_scheduling.tsx index 68d3f354a14f8..339c353cbf589 100644 --- a/packages/kbn-search-connectors/components/scheduling/connector_scheduling.tsx +++ b/packages/kbn-search-connectors/components/scheduling/connector_scheduling.tsx @@ -5,7 +5,7 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -import React, { useEffect, useMemo, useState } from 'react'; +import React, { useEffect, useMemo, useState, FC, PropsWithChildren } from 'react'; import { EuiCallOut, EuiFlexGroup, @@ -31,7 +31,11 @@ interface SchedulePanelProps { description: string; title: string; } -export const SchedulePanel: React.FC = ({ title, description, children }) => { +export const SchedulePanel: FC> = ({ + title, + description, + children, +}) => { return ( <> diff --git a/packages/kbn-search-connectors/components/sync_jobs/flyout_panel.tsx b/packages/kbn-search-connectors/components/sync_jobs/flyout_panel.tsx index 699a889150e6b..07cbf64a47949 100644 --- a/packages/kbn-search-connectors/components/sync_jobs/flyout_panel.tsx +++ b/packages/kbn-search-connectors/components/sync_jobs/flyout_panel.tsx @@ -6,15 +6,14 @@ * Side Public License, v 1. */ -import React from 'react'; - +import React, { FC, PropsWithChildren } from 'react'; import { EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui'; interface FlyoutPanelProps { title: string; } -export const FlyoutPanel: React.FC = ({ children, title }) => { +export const FlyoutPanel: FC> = ({ children, title }) => { return ( diff --git a/packages/kbn-shared-ux-utility/src/dynamic/test_component.tsx b/packages/kbn-shared-ux-utility/src/dynamic/test_component.tsx index 058481ea5f8aa..e000468e062c0 100644 --- a/packages/kbn-shared-ux-utility/src/dynamic/test_component.tsx +++ b/packages/kbn-shared-ux-utility/src/dynamic/test_component.tsx @@ -6,20 +6,23 @@ * Side Public License, v 1. */ -import React from 'react'; +import React, { FC, PropsWithChildren } from 'react'; export interface TestComponentProps { customProp?: boolean; + children?: React.ReactNode; } -export const TestComponent: React.FunctionComponent = ({ children }) => { +export const TestComponent: FC> = ({ children }) => { return {children} Test component; }; -export const ForwardeRefTestComponent: React.FunctionComponent = - React.forwardRef(({ children }, ref) => { - return {children} Test component; - }); +export const ForwardeRefTestComponent: FC> = React.forwardRef< + HTMLSpanElement, + PropsWithChildren +>(({ children }, ref) => { + return {children} Test component; +}); // eslint-disable-next-line import/no-default-export export default TestComponent; diff --git a/packages/kbn-sort-predicates/README.md b/packages/kbn-sort-predicates/README.md index bc1a2fa86b028..b2e3e07b8ff75 100644 --- a/packages/kbn-sort-predicates/README.md +++ b/packages/kbn-sort-predicates/README.md @@ -9,6 +9,7 @@ This package contains a flexible sorting function who supports the following typ * dates (both as number or ISO string) * ranges open and closed (number type only for now) * null and undefined (always sorted as last entries, no matter the direction) + * if it matters the difference: null values are sorted always before undefined ones * any multi-value version of the types above (version excluded) * for multi-values with different length it wins the first non-zero comparison (see note at the bottom) diff --git a/packages/kbn-sort-predicates/src/sorting.test.ts b/packages/kbn-sort-predicates/src/sorting.test.ts index e2fd1a9448c58..12a3c815d2412 100644 --- a/packages/kbn-sort-predicates/src/sorting.test.ts +++ b/packages/kbn-sort-predicates/src/sorting.test.ts @@ -46,6 +46,56 @@ function testSorting({ } describe('Data sorting criteria', () => { + describe('null rows', () => { + // in these tests it needs to skip the testSorting utility in order to pass null rows + // mind that [].sort() will never pass `undefined` values to the comparison function + // so we test it with null values instead + it('should not crash with null rows with strings', () => { + const datatable = ['a', 'b', 'c', 'd', '12']; + const datatableWithNulls = datatable.flatMap((v) => [{ a: v }, null]); + const criteria = getSortingCriteria('string', 'a', getMockFormatter()); + expect( + datatableWithNulls + .sort((a, b) => criteria(a, b, 'asc')) + .map((row) => (row == null ? row : row.a)) + ).toEqual(['12', 'a', 'b', 'c', 'd', ...Array(datatable.length).fill(null)]); + expect( + datatableWithNulls + .sort((a, b) => criteria(a, b, 'desc')) + .map((row) => (row == null ? row : row.a)) + ).toEqual(['d', 'c', 'b', 'a', '12', ...Array(datatable.length).fill(null)]); + }); + + it('should not crash with null rows with version', () => { + const datatable = ['1.21.0', '1.1.0', '1.112.0', '1.0.0', '__other__']; + const datatableWithNulls = datatable.flatMap((v) => [{ a: v }, null]); + const criteria = getSortingCriteria('version', 'a', getMockFormatter()); + expect( + datatableWithNulls + .sort((a, b) => criteria(a, b, 'asc')) + .map((row) => (row == null ? row : row.a)) + ).toEqual([ + '1.0.0', + '1.1.0', + '1.21.0', + '1.112.0', + ...Array(datatable.length).fill(null), + '__other__', + ]); + expect( + datatableWithNulls + .sort((a, b) => criteria(a, b, 'desc')) + .map((row) => (row == null ? row : row.a)) + ).toEqual([ + '1.112.0', + '1.21.0', + '1.1.0', + '1.0.0', + ...Array(datatable.length).fill(null), + '__other__', + ]); + }); + }); describe('Date values', () => { for (const direction of ['asc', 'desc'] as const) { it(`should provide the date criteria for date values (${direction})`, () => { @@ -229,7 +279,7 @@ describe('Data sorting criteria', () => { it('should sort non-version stuff to the end', () => { testSorting({ input: ['1.21.0', undefined, '1.1.0', null, '1.112.0', '__other__', '1.0.0'], - output: ['1.0.0', '1.1.0', '1.21.0', '1.112.0', undefined, null, '__other__'], + output: ['1.0.0', '1.1.0', '1.21.0', '1.112.0', null, undefined, '__other__'], direction: 'asc', type: 'version', reverseOutput: false, @@ -237,7 +287,7 @@ describe('Data sorting criteria', () => { testSorting({ input: ['1.21.0', undefined, '1.1.0', null, '1.112.0', '__other__', '1.0.0'], - output: ['1.112.0', '1.21.0', '1.1.0', '1.0.0', undefined, null, '__other__'], + output: ['1.112.0', '1.21.0', '1.1.0', '1.0.0', null, undefined, '__other__'], direction: 'desc', type: 'version', reverseOutput: false, diff --git a/packages/kbn-sort-predicates/src/sorting.ts b/packages/kbn-sort-predicates/src/sorting.ts index e9fed6e5cfc0d..e9b2c1acb9fe6 100644 --- a/packages/kbn-sort-predicates/src/sorting.ts +++ b/packages/kbn-sort-predicates/src/sorting.ts @@ -133,11 +133,17 @@ function getSafeIpAddress(ip: string | undefined, directionFactor: number) { } const versionComparison: CompareFn = (v1, v2, direction) => { - const valueA = String(v1 ?? ''); - const valueB = String(v2 ?? ''); + const valueA = String(v1 == null ? '' : v1); + const valueB = String(v2 == null ? '' : v2); const aInvalid = !valueA || !valid(valueA); const bInvalid = !valueB || !valid(valueB); if (aInvalid && bInvalid) { + if (v1 == null && v1 !== v2) { + return direction * -1; + } + if (v2 == null && v1 !== v2) { + return direction * 1; + } return 0; } // need to fight the direction multiplication of the parent function @@ -164,30 +170,32 @@ const rangeComparison: CompareFn> = (v1, v2) => { function createArrayValuesHandler(sortBy: string, formatter: FieldFormat) { return function (criteriaFn: CompareFn) { return ( - rowA: Record, - rowB: Record, + rowA: Record | undefined | null, + rowB: Record | undefined | null, direction: 'asc' | 'desc' ) => { // handle the direction with a multiply factor. const directionFactor = direction === 'asc' ? 1 : -1; + // make it handle null/undefined values + // this masks null/undefined rows into null/undefined values so it can benefit from shared invalid logic + // and enable custom sorting for invalid values (like for version type) + const valueA = rowA == null ? rowA : rowA[sortBy]; + const valueB = rowB == null ? rowB : rowB[sortBy]; // if either side of the comparison is an array, make it also the other one become one // then perform an array comparison - if (Array.isArray(rowA[sortBy]) || Array.isArray(rowB[sortBy])) { + if (Array.isArray(valueA) || Array.isArray(valueB)) { return ( directionFactor * compareArrays( - (Array.isArray(rowA[sortBy]) ? rowA[sortBy] : [rowA[sortBy]]) as T[], - (Array.isArray(rowB[sortBy]) ? rowB[sortBy] : [rowB[sortBy]]) as T[], + (Array.isArray(valueA) ? valueA : [valueA]) as T[], + (Array.isArray(valueB) ? valueB : [valueB]) as T[], directionFactor, formatter, criteriaFn ) ); } - return ( - directionFactor * - criteriaFn(rowA[sortBy] as T, rowB[sortBy] as T, directionFactor, formatter) - ); + return directionFactor * criteriaFn(valueA as T, valueB as T, directionFactor, formatter); }; }; } @@ -201,12 +209,12 @@ function getUndefinedHandler( ) => number ) { return ( - rowA: Record, - rowB: Record, + rowA: Record | undefined | null, + rowB: Record | undefined | null, direction: 'asc' | 'desc' ) => { - const valueA = rowA[sortBy]; - const valueB = rowB[sortBy]; + const valueA = rowA?.[sortBy]; + const valueB = rowB?.[sortBy]; // do not use the utility above as null at root level is handled differently // than null/undefined within an array type if (valueA == null || Number.isNaN(valueA)) { @@ -218,7 +226,7 @@ function getUndefinedHandler( if (valueB == null || Number.isNaN(valueB)) { return -1; } - return sortingCriteria(rowA, rowB, direction); + return sortingCriteria(rowA!, rowB!, direction); }; } @@ -226,7 +234,11 @@ export function getSortingCriteria( type: string | undefined, sortBy: string, formatter: FieldFormat -) { +): ( + rowA: Record | undefined | null, + rowB: Record | undefined | null, + direction: 'asc' | 'desc' +) => number { const arrayValueHandler = createArrayValuesHandler(sortBy, formatter); if (type === 'date') { diff --git a/packages/kbn-storybook/src/webpack.config.ts b/packages/kbn-storybook/src/webpack.config.ts index 282a41dcbd453..12f419f4d32db 100644 --- a/packages/kbn-storybook/src/webpack.config.ts +++ b/packages/kbn-storybook/src/webpack.config.ts @@ -139,6 +139,9 @@ export default ({ config: storybookConfig }: { config: Configuration }) => { stats, }; + // Override storybookConfig mainFields instead of merging with config + delete storybookConfig.resolve?.mainFields; + const updatedModuleRules = []; // clone and modify the module.rules config provided by storybook so that the default babel plugins run after the typescript preset for (const originalRule of storybookConfig.module?.rules ?? []) { diff --git a/packages/kbn-test/src/jest/setup/polyfills.jsdom.js b/packages/kbn-test/src/jest/setup/polyfills.jsdom.js index dbe4213ca83f7..0867862b2d4ca 100644 --- a/packages/kbn-test/src/jest/setup/polyfills.jsdom.js +++ b/packages/kbn-test/src/jest/setup/polyfills.jsdom.js @@ -46,4 +46,28 @@ if (!global.hasOwnProperty('Worker')) { } global.Worker = Worker; + + // Mocking matchMedia to resolve TypeError: window.matchMedia is not a function + // For more info, see https://jestjs.io/docs/manual-mocks#mocking-methods-which-are-not-implemented-in-jsdom + if (!global.hasOwnProperty('matchMedia')) { + Object.defineProperty(global, 'matchMedia', { + writable: true, + // eslint-disable-next-line no-undef + value: jest.fn().mockImplementation((query) => ({ + matches: false, + media: query, + onchange: null, + // eslint-disable-next-line no-undef + addListener: jest.fn(), // deprecated + // eslint-disable-next-line no-undef + removeListener: jest.fn(), // deprecated + // eslint-disable-next-line no-undef + addEventListener: jest.fn(), + // eslint-disable-next-line no-undef + removeEventListener: jest.fn(), + // eslint-disable-next-line no-undef + dispatchEvent: jest.fn(), + })), + }); + } } diff --git a/packages/kbn-text-based-editor/src/text_based_languages_editor.test.tsx b/packages/kbn-text-based-editor/src/text_based_languages_editor.test.tsx index 6aae293e9ae9b..3744e4acb1984 100644 --- a/packages/kbn-text-based-editor/src/text_based_languages_editor.test.tsx +++ b/packages/kbn-text-based-editor/src/text_based_languages_editor.test.tsx @@ -59,23 +59,7 @@ describe('TextBasedLanguagesEditor', () => { ); } let props: TextBasedLanguagesEditorProps; - beforeAll(() => { - // Mocking matchMedia to resolve TypeError: window.matchMedia is not a function - // For more info, see https://jestjs.io/docs/manual-mocks#mocking-methods-which-are-not-implemented-in-jsdom - Object.defineProperty(window, 'matchMedia', { - writable: true, - value: jest.fn().mockImplementation((query) => ({ - matches: false, - media: query, - onchange: null, - addListener: jest.fn(), // deprecated - removeListener: jest.fn(), // deprecated - addEventListener: jest.fn(), - removeEventListener: jest.fn(), - dispatchEvent: jest.fn(), - })), - }); - }); + beforeEach(() => { props = { query: { esql: 'from test' }, diff --git a/packages/kbn-ui-shared-deps-npm/webpack.config.js b/packages/kbn-ui-shared-deps-npm/webpack.config.js index 817a340dc3a8f..79502207aea00 100644 --- a/packages/kbn-ui-shared-deps-npm/webpack.config.js +++ b/packages/kbn-ui-shared-deps-npm/webpack.config.js @@ -60,6 +60,7 @@ module.exports = (_, argv) => { // modules from npm '@elastic/charts', '@elastic/eui', + '@elastic/eui/optimize/es/components/provider/nested', '@elastic/eui/optimize/es/services', '@elastic/eui/optimize/es/services/format', '@elastic/eui/dist/eui_charts_theme', diff --git a/packages/kbn-ui-shared-deps-src/src/definitions.js b/packages/kbn-ui-shared-deps-src/src/definitions.js index 519c706e723fd..78ee1229da4e9 100644 --- a/packages/kbn-ui-shared-deps-src/src/definitions.js +++ b/packages/kbn-ui-shared-deps-src/src/definitions.js @@ -71,6 +71,8 @@ const externals = { '@elastic/charts': '__kbnSharedDeps__.ElasticCharts', '@kbn/datemath': '__kbnSharedDeps__.KbnDatemath', '@elastic/eui': '__kbnSharedDeps__.ElasticEui', + '@elastic/eui/lib/components/provider/nested': + '__kbnSharedDeps__.ElasticEuiLibComponentsUseIsNestedEuiProvider', '@elastic/eui/lib/services': '__kbnSharedDeps__.ElasticEuiLibServices', '@elastic/eui/lib/services/format': '__kbnSharedDeps__.ElasticEuiLibServicesFormat', '@elastic/eui/dist/eui_charts_theme': '__kbnSharedDeps__.ElasticEuiChartsTheme', diff --git a/packages/kbn-ui-shared-deps-src/src/entry.js b/packages/kbn-ui-shared-deps-src/src/entry.js index 1046f2fed7cb2..b012cb5660113 100644 --- a/packages/kbn-ui-shared-deps-src/src/entry.js +++ b/packages/kbn-ui-shared-deps-src/src/entry.js @@ -39,6 +39,7 @@ export const Rxjs = require('rxjs'); export const ElasticNumeral = require('@elastic/numeral'); export const ElasticCharts = require('@elastic/charts'); export const ElasticEui = require('@elastic/eui'); +export const ElasticEuiLibComponentsUseIsNestedEuiProvider = require('@elastic/eui/optimize/es/components/provider/nested'); export const ElasticEuiLibServices = require('@elastic/eui/optimize/es/services'); export const ElasticEuiLibServicesFormat = require('@elastic/eui/optimize/es/services/format'); export const ElasticEuiChartsTheme = require('@elastic/eui/dist/eui_charts_theme'); diff --git a/packages/kbn-unified-data-table/src/components/compare_documents/comparison_controls.tsx b/packages/kbn-unified-data-table/src/components/compare_documents/comparison_controls.tsx index 6787c9e56dd05..48c4f11960e57 100644 --- a/packages/kbn-unified-data-table/src/components/compare_documents/comparison_controls.tsx +++ b/packages/kbn-unified-data-table/src/components/compare_documents/comparison_controls.tsx @@ -24,7 +24,7 @@ import { useEuiTheme, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import React, { FC, ReactNode, useState } from 'react'; +import React, { FC, PropsWithChildren, ReactNode, useState } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; import type { DocumentDiffMode } from './types'; @@ -368,10 +368,12 @@ const enableShowDiffTooltip = i18n.translate('unifiedDataTable.enableShowDiff', }); const DiffModeEntry: FC< - Pick & { - entryDiffMode: DocumentDiffMode; - disabled?: boolean; - } + PropsWithChildren< + Pick & { + entryDiffMode: DocumentDiffMode; + disabled?: boolean; + } + > > = ({ children, entryDiffMode, diffMode, disabled, setDiffMode }) => { const { euiTheme } = useEuiTheme(); diff --git a/packages/kbn-unified-data-table/src/components/data_table_footer.tsx b/packages/kbn-unified-data-table/src/components/data_table_footer.tsx index 21819a023afed..dc84bbf6d452f 100644 --- a/packages/kbn-unified-data-table/src/components/data_table_footer.tsx +++ b/packages/kbn-unified-data-table/src/components/data_table_footer.tsx @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useState, FC, PropsWithChildren } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; import { EuiButtonEmpty, EuiToolTip, useEuiTheme } from '@elastic/eui'; import { css } from '@emotion/react'; @@ -28,7 +28,9 @@ export interface UnifiedDataTableFooterProps { fieldFormats: FieldFormatsStart; } -export const UnifiedDataTableFooter: React.FC = (props) => { +export const UnifiedDataTableFooter: FC> = ( + props +) => { const { isLoadingMore, rowCount, diff --git a/packages/kbn-unified-data-table/src/utils/get_render_cell_value.test.tsx b/packages/kbn-unified-data-table/src/utils/get_render_cell_value.test.tsx index 4b00a85a29f39..8d18c7d454db7 100644 --- a/packages/kbn-unified-data-table/src/utils/get_render_cell_value.test.tsx +++ b/packages/kbn-unified-data-table/src/utils/get_render_cell_value.test.tsx @@ -38,16 +38,6 @@ jest.mock('@kbn/code-editor', () => { }; }); -window.matchMedia = jest.fn().mockImplementation((query) => { - return { - matches: false, - media: query, - onchange: null, - addListener: jest.fn(), - removeListener: jest.fn(), - }; -}); - const mockServices = { settings: { client: { diff --git a/packages/kbn-unified-field-list/src/components/field_list/field_list.tsx b/packages/kbn-unified-field-list/src/components/field_list/field_list.tsx index 29bc5f00cddf9..47888a6df5dc5 100644 --- a/packages/kbn-unified-field-list/src/components/field_list/field_list.tsx +++ b/packages/kbn-unified-field-list/src/components/field_list/field_list.tsx @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import React from 'react'; +import React, { FC, PropsWithChildren } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiProgress } from '@elastic/eui'; import { css } from '@emotion/react'; @@ -39,7 +39,7 @@ export interface FieldListProps { * @public * @constructor */ -export const FieldList: React.FC = ({ +export const FieldList: FC> = ({ 'data-test-subj': dataTestSubject = 'fieldList', isProcessing, prepend, diff --git a/packages/kbn-user-profile-components/src/services.tsx b/packages/kbn-user-profile-components/src/services.tsx index 71ce5a0ff844f..f70b7e2e65c41 100644 --- a/packages/kbn-user-profile-components/src/services.tsx +++ b/packages/kbn-user-profile-components/src/services.tsx @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import React, { FC, useContext } from 'react'; +import React, { FC, PropsWithChildren, useContext } from 'react'; import type { I18nStart } from '@kbn/core-i18n-browser'; import type { NotificationsStart, ToastOptions } from '@kbn/core-notifications-browser'; @@ -30,7 +30,10 @@ const UserProfilesContext = React.createContext(null); /** * Abstract external service Provider. */ -export const UserProfilesProvider: FC = ({ children, ...services }) => { +export const UserProfilesProvider: FC> = ({ + children, + ...services +}) => { return {children}; }; @@ -60,7 +63,7 @@ export interface UserProfilesKibanaDependencies { /** * Kibana-specific Provider that maps to known dependency types. */ -export const UserProfilesKibanaProvider: FC = ({ +export const UserProfilesKibanaProvider: FC> = ({ children, ...services }) => { diff --git a/packages/kbn-user-profile-components/src/user_profiles_selectable.tsx b/packages/kbn-user-profile-components/src/user_profiles_selectable.tsx index 3f36c0806a56e..1b08d55928b73 100644 --- a/packages/kbn-user-profile-components/src/user_profiles_selectable.tsx +++ b/packages/kbn-user-profile-components/src/user_profiles_selectable.tsx @@ -44,6 +44,7 @@ export interface UserProfilesSelectableProps