From 6ed214a69f75a69305cbb06515bdb5e9e75ac3ae Mon Sep 17 00:00:00 2001 From: Mykola Harmash Date: Fri, 10 Jan 2025 09:44:29 +0100 Subject: [PATCH 01/42] Migrate onboarding Cypress tests to Scout (#205482) This change converts Cypress tests for the custom logs flow into Playwright using [the Scout wrapper](https://github.com/elastic/kibana/tree/main/packages/kbn-scout). > [!NOTE] > As Scout package is still being developed, the PR pipeline configured to runs Playwright tests only when code in certain plugins have been changed and not on every PR. ### How to run tests locally Start the Scout server ```bash node scripts/scout.js start-server --stateful ``` In a separate terminal run the tests ```bash npx playwright test --config x-pack/solutions/observability/plugins/observability_onboarding/ui_tests/playwright.config.ts ``` Playwright runs browsers in a headless mode by default, user `--headed` option if needed --- .../pipelines/pull_request/scout_ui_tests.yml | 4 +- .../pipelines/pull_request/pipeline.ts | 13 +- .../steps/functional/scout_ui_tests.sh | 38 +- .github/CODEOWNERS | 1 - package.json | 1 - tsconfig.base.json | 2 - .../observability_onboarding/e2e/README.md | 51 -- .../e2e/cypress.config.ts | 32 - .../e2e/cypress/e2e/home.cy.ts | 36 - .../e2e/logs/custom_logs/configure.cy.ts | 280 ------- .../custom_logs/install_elastic_agent.cy.ts | 494 ------------- .../e2e/cypress/e2e/logs/feedback.cy.ts | 23 - .../e2e/cypress/e2e/navigation.cy.ts | 54 -- .../e2e/cypress/support/commands.ts | 182 ----- .../e2e/cypress/support/types.d.ts | 29 - .../e2e/cypress_test_runner.ts | 92 --- .../e2e/ftr_config.ts | 58 -- .../e2e/ftr_config_open.ts | 29 - .../e2e/ftr_config_runner.ts | 38 - .../e2e/ftr_kibana.yml | 1 - .../observability_onboarding/e2e/kibana.jsonc | 8 - .../e2e/tsconfig.json | 20 - .../observability_onboarding/tsconfig.json | 6 +- .../ui_tests/README.md | 23 + .../ui_tests/fixtures/index.ts | 50 ++ .../fixtures/page_objects/custom_logs.ts | 195 +++++ .../fixtures/page_objects/index.ts} | 6 +- .../fixtures/page_objects/onboarding_home.ts | 16 + .../playwright.config.ts} | 7 +- .../tests/custom_logs/configuration.spec.ts | 256 +++++++ .../custom_logs/install_elastic_agent.spec.ts | 696 ++++++++++++++++++ .../ui_tests/tsconfig.json | 15 + yarn.lock | 4 - 33 files changed, 1285 insertions(+), 1475 deletions(-) delete mode 100644 x-pack/solutions/observability/plugins/observability_onboarding/e2e/README.md delete mode 100644 x-pack/solutions/observability/plugins/observability_onboarding/e2e/cypress.config.ts delete mode 100644 x-pack/solutions/observability/plugins/observability_onboarding/e2e/cypress/e2e/home.cy.ts delete mode 100644 x-pack/solutions/observability/plugins/observability_onboarding/e2e/cypress/e2e/logs/custom_logs/configure.cy.ts delete mode 100644 x-pack/solutions/observability/plugins/observability_onboarding/e2e/cypress/e2e/logs/custom_logs/install_elastic_agent.cy.ts delete mode 100644 x-pack/solutions/observability/plugins/observability_onboarding/e2e/cypress/e2e/logs/feedback.cy.ts delete mode 100644 x-pack/solutions/observability/plugins/observability_onboarding/e2e/cypress/e2e/navigation.cy.ts delete mode 100644 x-pack/solutions/observability/plugins/observability_onboarding/e2e/cypress/support/commands.ts delete mode 100644 x-pack/solutions/observability/plugins/observability_onboarding/e2e/cypress/support/types.d.ts delete mode 100644 x-pack/solutions/observability/plugins/observability_onboarding/e2e/cypress_test_runner.ts delete mode 100644 x-pack/solutions/observability/plugins/observability_onboarding/e2e/ftr_config.ts delete mode 100644 x-pack/solutions/observability/plugins/observability_onboarding/e2e/ftr_config_open.ts delete mode 100644 x-pack/solutions/observability/plugins/observability_onboarding/e2e/ftr_config_runner.ts delete mode 100644 x-pack/solutions/observability/plugins/observability_onboarding/e2e/ftr_kibana.yml delete mode 100644 x-pack/solutions/observability/plugins/observability_onboarding/e2e/kibana.jsonc delete mode 100644 x-pack/solutions/observability/plugins/observability_onboarding/e2e/tsconfig.json create mode 100644 x-pack/solutions/observability/plugins/observability_onboarding/ui_tests/README.md create mode 100644 x-pack/solutions/observability/plugins/observability_onboarding/ui_tests/fixtures/index.ts create mode 100644 x-pack/solutions/observability/plugins/observability_onboarding/ui_tests/fixtures/page_objects/custom_logs.ts rename x-pack/solutions/observability/plugins/observability_onboarding/{e2e/cypress/support/e2e.ts => ui_tests/fixtures/page_objects/index.ts} (72%) create mode 100644 x-pack/solutions/observability/plugins/observability_onboarding/ui_tests/fixtures/page_objects/onboarding_home.ts rename x-pack/solutions/observability/plugins/observability_onboarding/{e2e/ftr_provider_context.d.ts => ui_tests/playwright.config.ts} (59%) create mode 100644 x-pack/solutions/observability/plugins/observability_onboarding/ui_tests/tests/custom_logs/configuration.spec.ts create mode 100644 x-pack/solutions/observability/plugins/observability_onboarding/ui_tests/tests/custom_logs/install_elastic_agent.spec.ts create mode 100644 x-pack/solutions/observability/plugins/observability_onboarding/ui_tests/tsconfig.json diff --git a/.buildkite/pipelines/pull_request/scout_ui_tests.yml b/.buildkite/pipelines/pull_request/scout_ui_tests.yml index 37ea1567c4f42..16981921f79a9 100644 --- a/.buildkite/pipelines/pull_request/scout_ui_tests.yml +++ b/.buildkite/pipelines/pull_request/scout_ui_tests.yml @@ -2,7 +2,7 @@ steps: - command: .buildkite/scripts/steps/functional/scout_ui_tests.sh label: 'Scout UI Tests' agents: - machineType: n2-standard-4 + machineType: n2-standard-8 preemptible: true depends_on: - build @@ -11,7 +11,7 @@ steps: - linting - linting_with_types - check_types - timeout_in_minutes: 30 + timeout_in_minutes: 60 retry: automatic: - exit_status: '-1' diff --git a/.buildkite/scripts/pipelines/pull_request/pipeline.ts b/.buildkite/scripts/pipelines/pull_request/pipeline.ts index 4353f4f079a9f..fd8ebb042ed46 100644 --- a/.buildkite/scripts/pipelines/pull_request/pipeline.ts +++ b/.buildkite/scripts/pipelines/pull_request/pipeline.ts @@ -109,18 +109,6 @@ const getPipeline = (filename: string, removeSteps = true) => { pipeline.push(getPipeline('.buildkite/pipelines/pull_request/inventory_cypress.yml')); } - if ( - (await doAnyChangesMatch([ - /^x-pack\/solutions\/observability\/plugins\/observability_onboarding/, - /^x-pack\/platform\/plugins\/shared\/fleet/, - ])) || - GITHUB_PR_LABELS.includes('ci:all-cypress-suites') - ) { - pipeline.push( - getPipeline('.buildkite/pipelines/pull_request/observability_onboarding_cypress.yml') - ); - } - if ( (await doAnyChangesMatch([/^x-pack\/solutions\/observability\/plugins\/profiling/])) || GITHUB_PR_LABELS.includes('ci:all-cypress-suites') @@ -405,6 +393,7 @@ const getPipeline = (filename: string, removeSteps = true) => { if ( (await doAnyChangesMatch([ /^x-pack\/platform\/plugins\/private\/discover_enhanced\/ui_tests/, + /^x-pack\/solutions\/observability\/plugins\/observability_onboarding/, /^packages\/kbn-scout/, ])) || GITHUB_PR_LABELS.includes('ci:scout-ui-tests') diff --git a/.buildkite/scripts/steps/functional/scout_ui_tests.sh b/.buildkite/scripts/steps/functional/scout_ui_tests.sh index b568ed9c80b1a..c16c92152684d 100755 --- a/.buildkite/scripts/steps/functional/scout_ui_tests.sh +++ b/.buildkite/scripts/steps/functional/scout_ui_tests.sh @@ -6,29 +6,33 @@ source .buildkite/scripts/steps/functional/common.sh export JOB=kibana-scout-ui-tests -TEST_CONFIG="x-pack/platform/plugins/private/discover_enhanced/ui_tests/playwright.config.ts" KIBANA_DIR="$KIBANA_BUILD_LOCATION" -declare -A TESTS=( - ["Stateful"]="--stateful" - ["Serverless Elasticsearch"]="--serverless=es" - ["Serverless Observability"]="--serverless=oblt" - ["Serverless Security"]="--serverless=security" -) +run_tests() { + local suit_name=$1 + local config_path=$2 + local run_mode=$3 -ORDER=("Stateful" "Serverless Elasticsearch" "Serverless Observability" "Serverless Security") - -EXIT_CODE=0 - -for TEST_NAME in "${ORDER[@]}"; do - RUN_MODE="${TESTS[$TEST_NAME]}" - echo "--- $TEST_NAME: 'discover_enhanced' plugin UI Tests" - if ! node scripts/scout run-tests "$RUN_MODE" --config "$TEST_CONFIG" --kibana-install-dir "$KIBANA_DIR"; then - echo "$TEST_NAME: failed" + echo "--- $suit_name ($run_mode) UI Tests" + if ! node scripts/scout run-tests "$run_mode" --config "$config_path" --kibana-install-dir "$KIBANA_DIR"; then + echo "$suit_name: failed" EXIT_CODE=1 else - echo "$TEST_NAME: passed" + echo "$suit_name: passed" fi +} + +EXIT_CODE=0 + +# Discovery Enhanced +for run_mode in "--stateful" "--serverless=es" "--serverless=oblt" "--serverless=security"; do + run_tests "Discovery Enhanced" "x-pack/platform/plugins/private/discover_enhanced/ui_tests/playwright.config.ts" "$run_mode" +done + +# Observability Onboarding +for run_mode in "--stateful" "--serverless=oblt"; do + run_tests "Observability Onboarding" "x-pack/solutions/observability/plugins/observability_onboarding/ui_tests/playwright.config.ts" "$run_mode" done + exit $EXIT_CODE diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index f0383e9e556d6..c9af0fd8d5c1b 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -932,7 +932,6 @@ x-pack/solutions/observability/plugins/observability_ai_assistant_app @elastic/o x-pack/solutions/observability/plugins/observability_ai_assistant_management @elastic/obs-ai-assistant x-pack/solutions/observability/plugins/observability_logs_explorer @elastic/obs-ux-logs-team x-pack/solutions/observability/plugins/observability_onboarding @elastic/obs-ux-logs-team -x-pack/solutions/observability/plugins/observability_onboarding/e2e @elastic/obs-ux-logs-team x-pack/solutions/observability/plugins/observability_shared @elastic/observability-ui x-pack/solutions/observability/plugins/observability_solution/entities_data_access @elastic/obs-entities x-pack/solutions/observability/plugins/observability_solution/entity_manager_app @elastic/obs-entities diff --git a/package.json b/package.json index 467428ad6404c..44730ac027fe9 100644 --- a/package.json +++ b/package.json @@ -1474,7 +1474,6 @@ "@kbn/manifest": "link:packages/kbn-manifest", "@kbn/mock-idp-plugin": "link:packages/kbn-mock-idp-plugin", "@kbn/mock-idp-utils": "link:packages/kbn-mock-idp-utils", - "@kbn/observability-onboarding-e2e": "link:x-pack/solutions/observability/plugins/observability_onboarding/e2e", "@kbn/openapi-bundler": "link:packages/kbn-openapi-bundler", "@kbn/openapi-generator": "link:packages/kbn-openapi-generator", "@kbn/optimizer": "link:packages/kbn-optimizer", diff --git a/tsconfig.base.json b/tsconfig.base.json index 97d73eaa868f5..2a439e72882bb 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -1336,8 +1336,6 @@ "@kbn/observability-logs-explorer-plugin/*": ["x-pack/solutions/observability/plugins/observability_logs_explorer/*"], "@kbn/observability-logs-overview": ["x-pack/platform/packages/shared/observability/logs_overview"], "@kbn/observability-logs-overview/*": ["x-pack/platform/packages/shared/observability/logs_overview/*"], - "@kbn/observability-onboarding-e2e": ["x-pack/solutions/observability/plugins/observability_onboarding/e2e"], - "@kbn/observability-onboarding-e2e/*": ["x-pack/solutions/observability/plugins/observability_onboarding/e2e/*"], "@kbn/observability-onboarding-plugin": ["x-pack/solutions/observability/plugins/observability_onboarding"], "@kbn/observability-onboarding-plugin/*": ["x-pack/solutions/observability/plugins/observability_onboarding/*"], "@kbn/observability-plugin": ["x-pack/solutions/observability/plugins/observability"], diff --git a/x-pack/solutions/observability/plugins/observability_onboarding/e2e/README.md b/x-pack/solutions/observability/plugins/observability_onboarding/e2e/README.md deleted file mode 100644 index 2c35d48fb5207..0000000000000 --- a/x-pack/solutions/observability/plugins/observability_onboarding/e2e/README.md +++ /dev/null @@ -1,51 +0,0 @@ -# Observability onboarding E2E tests - -Observability onboarding uses [FTR](../../../../../packages/kbn-test/README.mdx) (functional test runner) and [Cypress](https://www.cypress.io/) to run the e2e tests. The tests are located at `kibana/x-pack/solutions/observability/plugins/observability_onboarding/e2e/cypress/e2e`. - -## E2E Tests (Cypress) - -The E2E tests are located in [`x-pack/solutions/observability/plugins/observability_onboarding/e2e`](./cypress/e2e). - -Tests run on buildkite PR pipeline are parallelized (2 parallel jobs) and are orchestrated by the Cypress dashboard service. It can be configured in [.buildkite/pipelines/pull_request/observability_onboarding_cypress.yml](https://github.com/elastic/kibana/blob/main/.buildkite/pipelines/pull_request/observability_onboarding_cypress.yml) with the property `parallelism`. - -```yml - ... - depends_on: build - parallelism: 2 - ... -``` - -## Running it locally - -### Start test server - -``` -node x-pack/solutions/observability/plugins/observability_onboarding/scripts/test/e2e --server -``` - -### Run tests -Runs all tests in the terminal - -``` -node x-pack/solutions/observability/plugins/observability_onboarding/scripts/test/e2e --runner -``` - -### Open cypress dashboard -Opens cypress dashboard, there it's possible to select what test you want to run. - -``` -node x-pack/solutions/observability/plugins/observability_onboarding/scripts/test/e2e --open -``` -### Arguments - -| Option | Description | -| ------------ | ----------------------------------------------- | -| --server | Only start ES and Kibana | -| --runner | Only run tests | -| --spec | Specify the specs to run | -| --times | Repeat the test n number of times | -| --bail | stop tests after the first failure | - -``` -node x-pack/solutions/observability/plugins/observability_onboarding/scripts/test/e2e.js --runner --spec cypress/e2e/home.cy.ts --times 2 -``` diff --git a/x-pack/solutions/observability/plugins/observability_onboarding/e2e/cypress.config.ts b/x-pack/solutions/observability/plugins/observability_onboarding/e2e/cypress.config.ts deleted file mode 100644 index afbad0bac67d0..0000000000000 --- a/x-pack/solutions/observability/plugins/observability_onboarding/e2e/cypress.config.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { defineCypressConfig } from '@kbn/cypress-config'; - -export default defineCypressConfig({ - fileServerFolder: './cypress', - fixturesFolder: './cypress/fixtures', - screenshotsFolder: './cypress/screenshots', - videosFolder: './cypress/videos', - requestTimeout: 10000, - responseTimeout: 40000, - defaultCommandTimeout: 30000, - execTimeout: 120000, - pageLoadTimeout: 120000, - viewportHeight: 1800, - viewportWidth: 1440, - video: false, - screenshotOnRunFailure: false, - retries: { - runMode: 1, - }, - e2e: { - baseUrl: 'http://localhost:5601', - supportFile: './cypress/support/e2e.ts', - specPattern: './cypress/e2e/**/*.cy.{js,jsx,ts,tsx}', - }, -}); diff --git a/x-pack/solutions/observability/plugins/observability_onboarding/e2e/cypress/e2e/home.cy.ts b/x-pack/solutions/observability/plugins/observability_onboarding/e2e/cypress/e2e/home.cy.ts deleted file mode 100644 index 27392c65e9dc7..0000000000000 --- a/x-pack/solutions/observability/plugins/observability_onboarding/e2e/cypress/e2e/home.cy.ts +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -// Failing: See https://github.com/elastic/kibana/issues/183341 -describe.skip('[Observability onboarding] Landing page', () => { - beforeEach(() => { - cy.loginAsElastic(); - }); - - describe('Entry point', () => { - it('when clicking on the logs card the user is navigated to the observability onboarding page', () => { - cy.getByTestSubj('guideButtonRedirect').click(); - cy.getByTestSubj('guide-filter-observability').click(); - cy.getByTestSubj('onboarding--observability--logs').click(); - - cy.url().should('include', '/app/observabilityOnboarding'); - }); - - it('when clicking on observability overview callout the user is navigated to the observability onboarding page', () => { - cy.visitKibana('/app/observability'); - cy.getByTestSubj('observability-onboarding-callout').should('exist'); - cy.getByTestSubj('o11yObservabilityOnboardingGetStartedButton').click(); - - cy.url().should('include', '/app/observabilityOnboarding'); - }); - }); - - it('when user navigates to observability onboarding landing page is showed', () => { - cy.visitKibana('/app/observabilityOnboarding'); - cy.contains('Onboard Observability data'); - }); -}); diff --git a/x-pack/solutions/observability/plugins/observability_onboarding/e2e/cypress/e2e/logs/custom_logs/configure.cy.ts b/x-pack/solutions/observability/plugins/observability_onboarding/e2e/cypress/e2e/logs/custom_logs/configure.cy.ts deleted file mode 100644 index 3a4ceba04f706..0000000000000 --- a/x-pack/solutions/observability/plugins/observability_onboarding/e2e/cypress/e2e/logs/custom_logs/configure.cy.ts +++ /dev/null @@ -1,280 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -describe('[Logs onboarding] Custom logs - configure step', () => { - describe('logFilePaths', () => { - beforeEach(() => { - cy.loginAsViewerUser(); - cy.visitKibana('/app/observabilityOnboarding/customLogs'); - }); - - describe('when user clicks on back button', () => { - beforeEach(() => { - cy.loginAsViewerUser(); - cy.visitKibana('/app/observabilityOnboarding/customLogs'); - }); - - it('navigates to observability logs onboarding page', () => { - cy.getByTestSubj('observabilityOnboardingFlowBackToSelectionButton').click(); - - cy.url().should('include', '/app/observabilityOnboarding'); - }); - }); - - it('Users shouldnt be able to continue if logFilePaths is empty', () => { - cy.getByTestSubj('obltOnboardingLogFilePath-0').find('input').should('not.have.text'); - cy.getByTestSubj('obltOnboardingCustomLogsContinue').should('be.disabled'); - }); - - it('Users should be able to continue if logFilePaths is not empty', () => { - cy.getByTestSubj('obltOnboardingLogFilePath-0').find('input').type('myLogs.log'); - cy.getByTestSubj('obltOnboardingCustomLogsContinue').should('not.be.disabled'); - }); - - it('Users can add multiple logFilePaths', () => { - cy.getByTestSubj('obltOnboardingCustomLogsAddFilePath').click(); - cy.getByTestSubj('obltOnboardingLogFilePath-0').should('exist'); - cy.getByTestSubj('obltOnboardingLogFilePath-1').should('exist'); - }); - - it('Users can delete logFilePaths', () => { - cy.getByTestSubj('obltOnboardingCustomLogsAddFilePath').click(); - cy.get('*[data-test-subj^="obltOnboardingLogFilePath-"]').should('have.length', 2); - - cy.getByTestSubj('obltOnboardingLogFilePathDelete-1').click(); - cy.get('*[data-test-subj^="obltOnboardingLogFilePath-"]').should('have.length', 1); - }); - - describe('when users fill logFilePaths', () => { - it('datasetname and integration name are auto generated if it is the first path', () => { - cy.getByTestSubj('obltOnboardingLogFilePath-0').find('input').type('myLogs.log'); - cy.getByTestSubj('obltOnboardingCustomLogsIntegrationsName').should('have.value', 'mylogs'); - cy.getByTestSubj('obltOnboardingCustomLogsDatasetName').should('have.value', 'mylogs'); - }); - - it('datasetname and integration name are not generated if it is not the first path', () => { - cy.getByTestSubj('obltOnboardingCustomLogsAddFilePath').click(); - cy.getByTestSubj('obltOnboardingLogFilePath-1').find('input').type('myLogs.log'); - cy.getByTestSubj('obltOnboardingCustomLogsIntegrationsName').should('be.empty'); - cy.getByTestSubj('obltOnboardingCustomLogsDatasetName').should('be.empty'); - }); - }); - }); - - describe('serviceName', () => { - beforeEach(() => { - cy.loginAsViewerUser(); - cy.visitKibana('/app/observabilityOnboarding/customLogs'); - - cy.getByTestSubj('obltOnboardingLogFilePath-0').find('input').type('myLogs.log'); - }); - - it('should be optional allowing user to continue if it is empty', () => { - cy.getByTestSubj('obltOnboardingCustomLogsServiceName').should('not.have.text'); - cy.getByTestSubj('obltOnboardingCustomLogsContinue').should('be.enabled'); - }); - }); - - describe('advancedSettings', () => { - beforeEach(() => { - cy.loginAsViewerUser(); - cy.visitKibana('/app/observabilityOnboarding/customLogs'); - - cy.getByTestSubj('obltOnboardingLogFilePath-0').find('input').type('myLogs.log'); - }); - - it('Users should expand the content when clicking it', () => { - cy.getByTestSubj('obltOnboardingCustomLogsAdvancedSettings').click(); - - cy.getByTestSubj('obltOnboardingCustomLogsNamespace').should('be.visible'); - cy.getByTestSubj('obltOnboardingCustomLogsCustomConfig').should('be.visible'); - }); - - it('Users should hide the content when clicking it', () => { - cy.getByTestSubj('obltOnboardingCustomLogsAdvancedSettings').click(); - - cy.getByTestSubj('obltOnboardingCustomLogsNamespace').should('not.be.visible'); - cy.getByTestSubj('obltOnboardingCustomLogsCustomConfig').should('not.be.visible'); - }); - - describe('Namespace', () => { - beforeEach(() => { - cy.getByTestSubj('obltOnboardingCustomLogsAdvancedSettings').click(); - }); - - afterEach(() => { - cy.getByTestSubj('obltOnboardingCustomLogsAdvancedSettings').click(); - }); - - it('Users should see a default namespace', () => { - cy.getByTestSubj('obltOnboardingCustomLogsNamespace').should('have.value', 'default'); - }); - - it('Users should not be able to continue if they do not specify a namespace', () => { - cy.getByTestSubj('obltOnboardingCustomLogsNamespace').clear(); - - cy.getByTestSubj('obltOnboardingCustomLogsContinue').should('be.disabled'); - }); - }); - - describe('customConfig', () => { - beforeEach(() => { - cy.getByTestSubj('obltOnboardingCustomLogsAdvancedSettings').click(); - }); - - afterEach(() => { - cy.getByTestSubj('obltOnboardingCustomLogsAdvancedSettings').click(); - }); - - it('should be optional allowing user to continue if it is empty', () => { - cy.getByTestSubj('obltOnboardingCustomLogsCustomConfig').should('not.have.text'); - cy.getByTestSubj('obltOnboardingCustomLogsContinue').should('be.enabled'); - }); - }); - }); - - describe('integrationName', () => { - beforeEach(() => { - cy.loginAsViewerUser(); - cy.visitKibana('/app/observabilityOnboarding/customLogs'); - - cy.getByTestSubj('obltOnboardingLogFilePath-0').find('input').type('myLogs.log'); - }); - - it('Users should not be able to continue if they do not specify an integrationName', () => { - cy.getByTestSubj('obltOnboardingCustomLogsIntegrationsName').clear(); - - cy.getByTestSubj('obltOnboardingCustomLogsContinue').should('be.disabled'); - }); - - it('value will contain _ instead of special chars', () => { - cy.getByTestSubj('obltOnboardingCustomLogsIntegrationsName').clear().type('hello$world'); - - cy.getByTestSubj('obltOnboardingCustomLogsIntegrationsName').should( - 'have.value', - 'hello_world' - ); - }); - - it('value will be invalid if it is not lowercase', () => { - cy.getByTestSubj('obltOnboardingCustomLogsIntegrationsName').clear().type('H3llowOrld'); - - cy.contains('An integration name should be lowercase.'); - }); - }); - - describe('datasetName', () => { - beforeEach(() => { - cy.loginAsViewerUser(); - cy.visitKibana('/app/observabilityOnboarding/customLogs'); - - cy.getByTestSubj('obltOnboardingLogFilePath-0').find('input').type('myLogs.log'); - }); - - it('Users should not be able to continue if they do not specify a datasetName', () => { - cy.getByTestSubj('obltOnboardingCustomLogsDatasetName').clear(); - - cy.getByTestSubj('obltOnboardingCustomLogsContinue').should('be.disabled'); - }); - - it('value will contain _ instead of special chars', () => { - cy.getByTestSubj('obltOnboardingCustomLogsDatasetName').clear().type('hello$world'); - - cy.getByTestSubj('obltOnboardingCustomLogsDatasetName').should('have.value', 'hello_world'); - }); - - it('value will be invalid if it is not lowercase', () => { - cy.getByTestSubj('obltOnboardingCustomLogsDatasetName').clear().type('H3llowOrld'); - - cy.contains('A dataset name should be lowercase.'); - }); - }); - - describe('custom integration', () => { - const CUSTOM_INTEGRATION_NAME = 'mylogs'; - - beforeEach(() => { - cy.deleteIntegration(CUSTOM_INTEGRATION_NAME); - }); - - describe('when user is missing privileges', () => { - beforeEach(() => { - cy.loginAsViewerUser(); - cy.visitKibana('/app/observabilityOnboarding/customLogs'); - - cy.getByTestSubj('obltOnboardingLogFilePath-0') - .find('input') - .type(`${CUSTOM_INTEGRATION_NAME}.log`); - - cy.getByTestSubj('obltOnboardingCustomLogsContinue').click(); - }); - - it('installation fails', () => { - cy.getByTestSubj('obltOnboardingCustomIntegrationErrorCallout').should('exist'); - }); - }); - - describe('when user has proper privileges', () => { - beforeEach(() => { - cy.loginAsEditorUser(); - cy.visitKibana('/app/observabilityOnboarding/customLogs'); - - cy.getByTestSubj('obltOnboardingLogFilePath-0') - .find('input') - .type(`${CUSTOM_INTEGRATION_NAME}.log`); - - cy.getByTestSubj('obltOnboardingCustomLogsContinue').click(); - }); - - afterEach(() => { - cy.deleteIntegration(CUSTOM_INTEGRATION_NAME); - }); - - it('installation succeed and user is redirected install elastic agent step', () => { - cy.url().should('include', '/app/observabilityOnboarding/customLogs/installElasticAgent'); - }); - }); - - it('installation fails if integration already exists', () => { - cy.loginAsEditorUser(); - cy.visitKibana('/app/observabilityOnboarding/customLogs'); - - cy.installCustomIntegration(CUSTOM_INTEGRATION_NAME); - cy.getByTestSubj('obltOnboardingLogFilePath-0') - .find('input') - .type(`${CUSTOM_INTEGRATION_NAME}.log`); - cy.getByTestSubj('obltOnboardingCustomLogsContinue').click(); - - cy.contains( - 'Failed to create the integration as an installation with the name mylogs already exists.' - ); - }); - - describe('when an error occurred on creation', () => { - before(() => { - cy.intercept('/api/fleet/epm/custom_integrations', { - statusCode: 500, - body: { - message: 'Internal error', - }, - }); - - cy.loginAsEditorUser(); - cy.visitKibana('/app/observabilityOnboarding/customLogs'); - - cy.getByTestSubj('obltOnboardingLogFilePath-0') - .find('input') - .type(`${CUSTOM_INTEGRATION_NAME}.log`); - cy.getByTestSubj('obltOnboardingCustomLogsContinue').click(); - }); - - it('user should see the error displayed', () => { - cy.getByTestSubj('obltOnboardingCustomIntegrationErrorCallout').should('exist'); - }); - }); - }); -}); diff --git a/x-pack/solutions/observability/plugins/observability_onboarding/e2e/cypress/e2e/logs/custom_logs/install_elastic_agent.cy.ts b/x-pack/solutions/observability/plugins/observability_onboarding/e2e/cypress/e2e/logs/custom_logs/install_elastic_agent.cy.ts deleted file mode 100644 index 3b9248dc3c9e0..0000000000000 --- a/x-pack/solutions/observability/plugins/observability_onboarding/e2e/cypress/e2e/logs/custom_logs/install_elastic_agent.cy.ts +++ /dev/null @@ -1,494 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -describe('[Logs onboarding] Custom logs - install elastic agent', () => { - const CUSTOM_INTEGRATION_NAME = 'mylogs'; - - const configureCustomLogs = (loginFn = () => cy.loginAsLogMonitoringUser()) => { - loginFn(); - cy.visitKibana('/app/observabilityOnboarding/customLogs'); - - cy.deleteIntegration(CUSTOM_INTEGRATION_NAME); - - cy.getByTestSubj('obltOnboardingLogFilePath-0').find('input').type('mylogs.log'); - - cy.getByTestSubj('obltOnboardingCustomLogsContinue').click(); - }; - - describe('custom integration', () => { - beforeEach(() => { - configureCustomLogs(() => cy.loginAsEditorUser()); - }); - - it('Users should be able to see the custom integration success callout', () => { - cy.getByTestSubj('obltOnboardingCustomIntegrationInstalled').should('be.visible'); - }); - }); - - describe('ApiKey generation', () => { - describe('when user is missing privileges', () => { - beforeEach(() => { - configureCustomLogs(() => cy.loginAsEditorUser()); - }); - - it('apiKey is not generated', () => { - cy.getByTestSubj('obltOnboardingLogsApiKeyCreationNoPrivileges').should('exist'); - }); - }); - - describe('when user has proper privileges', () => { - beforeEach(() => { - configureCustomLogs(); - }); - - it('apiKey is generated', () => { - cy.getByTestSubj('obltOnboardingLogsApiKeyCreated').should('exist'); - }); - }); - - describe('when an error occurred on creation', () => { - before(() => { - cy.intercept('/internal/observability_onboarding/logs/flow', { - statusCode: 500, - body: { - message: 'Internal error', - }, - }); - - configureCustomLogs(); - }); - - it('apiKey is not generated', () => { - cy.getByTestSubj('obltOnboardingLogsApiKeyCreationFailed').should('exist'); - }); - }); - }); - - describe('Install the Elastic Agent step', () => { - beforeEach(() => { - cy.intercept('POST', '/internal/observability_onboarding/logs/flow').as( - 'createOnboardingFlow' - ); - configureCustomLogs(); - }); - - describe('When user select Linux OS', () => { - it('Auto download config to host is disabled by default', () => { - cy.get('.euiButtonGroup').contains('Linux').click(); - cy.getByTestSubj('obltOnboardingInstallElasticAgentAutoDownloadConfig') - .should('be.enabled') - .should('not.be.checked'); - }); - - it('Installation script is shown', () => { - cy.getByTestSubj('obltOnboardingInstallElasticAgentStep') - .get('.euiCodeBlock') - .should('exist'); - }); - }); - - describe('When user select Mac OS', () => { - beforeEach(() => { - cy.get('.euiButtonGroup').contains('MacOS').click(); - }); - - it('Auto download config to host is disabled by default', () => { - cy.getByTestSubj('obltOnboardingInstallElasticAgentAutoDownloadConfig') - .should('be.enabled') - .should('not.be.checked'); - }); - - it('Installation script is shown', () => { - cy.getByTestSubj('obltOnboardingInstallElasticAgentStep') - .get('.euiCodeBlock') - .should('exist'); - }); - }); - - describe('When user select Windows OS', () => { - beforeEach(() => { - cy.get('.euiButtonGroup').contains('Windows').click(); - }); - - it('Auto download config to host is disabled by default', () => { - cy.getByTestSubj('obltOnboardingInstallElasticAgentAutoDownloadConfig') - .should('be.disabled') - .should('not.be.checked'); - }); - - it('A link to the documentation is shown instead of installation script', () => { - cy.getByTestSubj('obltOnboardingInstallElasticAgentWindowsDocsLink').should('exist'); - - cy.getByTestSubj('obltOnboardingInstallElasticAgentStep') - .get('.euiCodeBlock') - .should('not.exist'); - }); - }); - - describe('When Auto download config', () => { - describe('is selected', () => { - it('autoDownloadConfig flag is added to installation script', () => { - cy.getByTestSubj('obltOnboardingInstallElasticAgentAutoDownloadConfig').click(); - cy.getByTestSubj('obltOnboardingInstallElasticAgentAutoDownloadConfigCallout').should( - 'exist' - ); - cy.getByTestSubj('obltOnboardingInstallElasticAgentStep') - .get('.euiCodeBlock') - .should('contain', 'autoDownloadConfig=1'); - }); - - it('Download config button is disabled', () => { - cy.getByTestSubj('obltOnboardingInstallElasticAgentAutoDownloadConfig').click(); - cy.getByTestSubj('obltOnboardingConfigureElasticAgentStepDownloadConfig').should( - 'be.disabled' - ); - }); - }); - - it('is not selected autoDownloadConfig flag is not added to installation script', () => { - cy.getByTestSubj('obltOnboardingInstallElasticAgentStep') - .get('.euiCodeBlock') - .should('not.contain', 'autoDownloadConfig=1'); - }); - }); - - describe('When user executes the installation script in the host', () => { - let onboardingId: string; - - describe('updates on steps are shown in the flow', () => { - beforeEach(() => { - cy.wait('@createOnboardingFlow') - .its('response.body') - .then((body) => { - onboardingId = body.onboardingId; - }); - }); - - describe('Download elastic Agent step', () => { - it('shows a loading callout when elastic agent is downloading', () => { - cy.updateInstallationStepStatus(onboardingId, 'ea-download', 'loading'); - cy.getByTestSubj('obltOnboardingStepStatus-loading') - .contains('Downloading Elastic Agent') - .should('exist'); - }); - - it('shows a success callout when elastic agent is downloaded', () => { - cy.updateInstallationStepStatus(onboardingId, 'ea-download', 'complete'); - cy.getByTestSubj('obltOnboardingStepStatus-complete') - .contains('Elastic Agent downloaded') - .should('exist'); - }); - - it('shows a danger callout when elastic agent was not downloaded', () => { - cy.updateInstallationStepStatus(onboardingId, 'ea-download', 'danger'); - cy.getByTestSubj('obltOnboardingStepStatus-danger') - .contains('Download Elastic Agent') - .should('exist'); - }); - }); - - describe('Extract elastic Agent step', () => { - beforeEach(() => { - cy.updateInstallationStepStatus(onboardingId, 'ea-download', 'complete'); - }); - - it('shows a loading callout when elastic agent is extracting', () => { - cy.updateInstallationStepStatus(onboardingId, 'ea-extract', 'loading'); - cy.getByTestSubj('obltOnboardingStepStatus-loading') - .contains('Extracting Elastic Agent') - .should('exist'); - }); - - it('shows a success callout when elastic agent is extracted', () => { - cy.updateInstallationStepStatus(onboardingId, 'ea-extract', 'complete'); - cy.getByTestSubj('obltOnboardingStepStatus-complete') - .contains('Elastic Agent extracted') - .should('exist'); - }); - - it('shows a danger callout when elastic agent was not extracted', () => { - cy.updateInstallationStepStatus(onboardingId, 'ea-extract', 'danger'); - cy.getByTestSubj('obltOnboardingStepStatus-danger') - .contains('Extract Elastic Agent') - .should('exist'); - }); - }); - - describe('Install elastic Agent step', () => { - beforeEach(() => { - cy.updateInstallationStepStatus(onboardingId, 'ea-download', 'complete'); - cy.updateInstallationStepStatus(onboardingId, 'ea-extract', 'complete'); - }); - - it('shows a loading callout when elastic agent is installing', () => { - cy.updateInstallationStepStatus(onboardingId, 'ea-install', 'loading'); - cy.getByTestSubj('obltOnboardingStepStatus-loading') - .contains('Installing Elastic Agent') - .should('exist'); - }); - - it('shows a success callout when elastic agent is installed', () => { - cy.updateInstallationStepStatus(onboardingId, 'ea-install', 'complete'); - cy.getByTestSubj('obltOnboardingStepStatus-complete') - .contains('Elastic Agent installed') - .should('exist'); - }); - - it('shows a danger callout when elastic agent was not installed', () => { - cy.updateInstallationStepStatus(onboardingId, 'ea-install', 'danger'); - cy.getByTestSubj('obltOnboardingStepStatus-danger') - .contains('Install Elastic Agent') - .should('exist'); - }); - }); - - describe('Check elastic Agent status step', () => { - beforeEach(() => { - cy.updateInstallationStepStatus(onboardingId, 'ea-download', 'complete'); - cy.updateInstallationStepStatus(onboardingId, 'ea-extract', 'complete'); - cy.updateInstallationStepStatus(onboardingId, 'ea-install', 'complete'); - }); - - it('shows a loading callout when getting elastic agent status', () => { - cy.updateInstallationStepStatus(onboardingId, 'ea-status', 'loading'); - cy.getByTestSubj('obltOnboardingStepStatus-loading') - .contains('Connecting to the Elastic Agent') - .should('exist'); - }); - - it('shows a success callout when elastic agent status is healthy', () => { - cy.updateInstallationStepStatus(onboardingId, 'ea-status', 'complete', { - agentId: 'test-agent-id', - }); - cy.getByTestSubj('obltOnboardingStepStatus-complete') - .contains('Connected to the Elastic Agent') - .should('exist'); - }); - - it('shows a warning callout when elastic agent status is not healthy', () => { - cy.updateInstallationStepStatus(onboardingId, 'ea-status', 'warning'); - cy.getByTestSubj('obltOnboardingStepStatus-warning') - .contains('Connect to the Elastic Agent') - .should('exist'); - }); - }); - }); - }); - }); - - describe('Configure Elastic Agent step', () => { - let onboardingId: string; - - beforeEach(() => { - cy.intercept('POST', '/internal/observability_onboarding/logs/flow').as( - 'createOnboardingFlow' - ); - configureCustomLogs(); - cy.wait('@createOnboardingFlow') - .its('response.body') - .then((body) => { - onboardingId = body.onboardingId; - }); - }); - - describe('When user select Linux OS', () => { - beforeEach(() => { - cy.getByTestSubj('obltOnboardingInstallElasticAgentAutoDownloadConfig').click(); - cy.updateInstallationStepStatus(onboardingId, 'ea-download', 'complete'); - cy.updateInstallationStepStatus(onboardingId, 'ea-extract', 'complete'); - cy.updateInstallationStepStatus(onboardingId, 'ea-install', 'complete'); - cy.updateInstallationStepStatus(onboardingId, 'ea-status', 'complete', { - agentId: 'test-agent-id', - }); - }); - - it('shows loading callout when config is being downloaded to the host', () => { - cy.updateInstallationStepStatus(onboardingId, 'ea-config', 'loading'); - cy.get( - '[data-test-subj="obltOnboardingConfigureElasticAgentStep"] .euiStep__titleWrapper [class$="euiStepNumber-s-loading"]' - ).should('exist'); - cy.getByTestSubj('obltOnboardingStepStatus-loading') - .contains('Downloading Elastic Agent config') - .should('exist'); - }); - - it('shows success callout when the configuration has been written to the host', () => { - cy.updateInstallationStepStatus(onboardingId, 'ea-config', 'complete'); - cy.get( - '[data-test-subj="obltOnboardingConfigureElasticAgentStep"] .euiStep__titleWrapper [class$="euiStepNumber-s-complete"]' - ).should('exist'); - cy.getByTestSubj('obltOnboardingStepStatus-complete') - .contains('Elastic Agent config written to /opt/Elastic/Agent/elastic-agent.yml') - .should('exist'); - }); - - it('shows warning callout when the configuration was not written in the host', () => { - cy.updateInstallationStepStatus(onboardingId, 'ea-config', 'warning'); - cy.get( - '[data-test-subj="obltOnboardingConfigureElasticAgentStep"] .euiStep__titleWrapper [class$="euiStepNumber-s-warning"]' - ).should('exist'); - cy.getByTestSubj('obltOnboardingStepStatus-warning') - .contains('Configure the agent') - .should('exist'); - }); - }); - - describe('When user select Mac OS', () => { - beforeEach(() => { - cy.get('.euiButtonGroup').contains('MacOS').click(); - cy.getByTestSubj('obltOnboardingInstallElasticAgentAutoDownloadConfig').click(); - cy.updateInstallationStepStatus(onboardingId, 'ea-download', 'complete'); - cy.updateInstallationStepStatus(onboardingId, 'ea-extract', 'complete'); - cy.updateInstallationStepStatus(onboardingId, 'ea-install', 'complete'); - cy.updateInstallationStepStatus(onboardingId, 'ea-status', 'complete', { - agentId: 'test-agent-id', - }); - }); - - it('shows loading callout when config is being downloaded to the host', () => { - cy.updateInstallationStepStatus(onboardingId, 'ea-config', 'loading'); - cy.get( - '[data-test-subj="obltOnboardingConfigureElasticAgentStep"] .euiStep__titleWrapper [class$="euiStepNumber-s-loading"]' - ).should('exist'); - cy.getByTestSubj('obltOnboardingStepStatus-loading') - .contains('Downloading Elastic Agent config') - .should('exist'); - }); - - it('shows success callout when the configuration has been written to the host', () => { - cy.updateInstallationStepStatus(onboardingId, 'ea-config', 'complete'); - cy.get( - '[data-test-subj="obltOnboardingConfigureElasticAgentStep"] .euiStep__titleWrapper [class$="euiStepNumber-s-complete"]' - ).should('exist'); - cy.getByTestSubj('obltOnboardingStepStatus-complete') - .contains('Elastic Agent config written to /Library/Elastic/Agent/elastic-agent.yml') - .should('exist'); - }); - - it('shows warning callout when the configuration was not written in the host', () => { - cy.updateInstallationStepStatus(onboardingId, 'ea-config', 'warning'); - cy.get( - '[data-test-subj="obltOnboardingConfigureElasticAgentStep"] .euiStep__titleWrapper [class$="euiStepNumber-s-warning"]' - ).should('exist'); - cy.getByTestSubj('obltOnboardingStepStatus-warning') - .contains('Configure the agent') - .should('exist'); - }); - }); - - describe('When user select Windows', () => { - beforeEach(() => { - cy.get('.euiButtonGroup').contains('Windows').click(); - }); - - it('step is disabled', () => { - cy.get( - '[data-test-subj="obltOnboardingConfigureElasticAgentStep"] .euiStep__titleWrapper [class$="euiStepNumber-s-disabled"]' - ).should('exist'); - }); - }); - }); - - describe('Check logs step', () => { - let onboardingId: string; - - beforeEach(() => { - cy.intercept('POST', '/internal/observability_onboarding/logs/flow').as( - 'createOnboardingFlow' - ); - configureCustomLogs(); - cy.wait('@createOnboardingFlow') - .its('response.body') - .then((body) => { - onboardingId = body.onboardingId; - }); - }); - - describe('When user select Linux OS or MacOS', () => { - describe('When configure Elastic Agent step is not finished', () => { - beforeEach(() => { - cy.updateInstallationStepStatus(onboardingId, 'ea-download', 'complete'); - cy.updateInstallationStepStatus(onboardingId, 'ea-extract', 'complete'); - cy.updateInstallationStepStatus(onboardingId, 'ea-install', 'complete'); - cy.updateInstallationStepStatus(onboardingId, 'ea-status', 'loading'); - }); - - it('check logs is not triggered', () => { - cy.get( - '[data-test-subj="obltOnboardingCheckLogsStep"] .euiStep__titleWrapper [class$="euiStepNumber-s-incomplete"]' - ).should('exist'); - cy.get('.euiStep__title').contains('Ship logs to Elastic Observability').should('exist'); - }); - }); - - describe('When configure Elastic Agent step has finished', () => { - beforeEach(() => { - cy.updateInstallationStepStatus(onboardingId, 'ea-download', 'complete'); - cy.updateInstallationStepStatus(onboardingId, 'ea-extract', 'complete'); - cy.updateInstallationStepStatus(onboardingId, 'ea-install', 'complete'); - cy.updateInstallationStepStatus(onboardingId, 'ea-status', 'complete', { - agentId: 'test-agent-id', - }); - cy.updateInstallationStepStatus(onboardingId, 'ea-config', 'complete'); - }); - - it('shows loading callout when logs are being checked', () => { - cy.get( - '[data-test-subj="obltOnboardingCheckLogsStep"] .euiStep__titleWrapper [class$="euiStepNumber-s-loading"]' - ).should('exist'); - cy.get('.euiStep__title').contains('Waiting for logs to be shipped...').should('exist'); - }); - }); - }); - - describe('When user select Windows', () => { - beforeEach(() => { - cy.get('.euiButtonGroup').contains('Windows').click(); - }); - - it('step is disabled', () => { - cy.get( - '[data-test-subj="obltOnboardingCheckLogsStep"] .euiStep__titleWrapper [class$="euiStepNumber-s-disabled"]' - ).should('exist'); - }); - }); - }); - - describe('When logs are being shipped', () => { - beforeEach(() => { - cy.intercept('GET', '**/progress', { - status: 200, - body: { - progress: { - 'ea-download': { status: 'complete' }, - 'ea-extract': { status: 'complete' }, - 'ea-install': { status: 'complete' }, - 'ea-status': { status: 'complete' }, - 'ea-config': { status: 'complete' }, - 'logs-ingest': { status: 'complete' }, - }, - }, - }).as('checkOnboardingProgress'); - configureCustomLogs(); - }); - - it('shows success callout when logs has arrived to elastic', () => { - cy.wait('@checkOnboardingProgress'); - cy.get( - '[data-test-subj="obltOnboardingCheckLogsStep"] .euiStep__titleWrapper [class$="euiStepNumber-s-complete"]' - ).should('exist'); - cy.get('.euiStep__title').contains('Logs are being shipped!').should('exist'); - }); - - it('when user clicks on Explore Logs it navigates to observability logs explorer', () => { - cy.wait('@checkOnboardingProgress'); - cy.getByTestSubj('obltOnboardingExploreLogs').should('exist').click(); - - cy.url().should('include', '/app/discover'); - }); - }); -}); diff --git a/x-pack/solutions/observability/plugins/observability_onboarding/e2e/cypress/e2e/logs/feedback.cy.ts b/x-pack/solutions/observability/plugins/observability_onboarding/e2e/cypress/e2e/logs/feedback.cy.ts deleted file mode 100644 index 542ca6ac80222..0000000000000 --- a/x-pack/solutions/observability/plugins/observability_onboarding/e2e/cypress/e2e/logs/feedback.cy.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -// Failing: See https://github.com/elastic/kibana/issues/183341 -describe.skip('[Logs onboarding] Give Feedback', () => { - beforeEach(() => { - cy.loginAsElastic(); - cy.visitKibana('/app/observabilityOnboarding'); - }); - - it('feedback button is present in custom logs onboarding', () => { - cy.getByTestSubj('obltOnboardingHomeStartLogFileStream').click(); - cy.getByTestSubj('observabilityOnboardingPageGiveFeedback').should('exist'); - }); - - it('feedback button is not present in the landing page', () => { - cy.getByTestSubj('observabilityOnboardingPageGiveFeedback').should('not.exist'); - }); -}); diff --git a/x-pack/solutions/observability/plugins/observability_onboarding/e2e/cypress/e2e/navigation.cy.ts b/x-pack/solutions/observability/plugins/observability_onboarding/e2e/cypress/e2e/navigation.cy.ts deleted file mode 100644 index ebaa607179791..0000000000000 --- a/x-pack/solutions/observability/plugins/observability_onboarding/e2e/cypress/e2e/navigation.cy.ts +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -// Failing: See https://github.com/elastic/kibana/issues/183341 -describe.skip('[Observability onboarding] Navigation', () => { - beforeEach(() => { - cy.loginAsElastic(); - cy.visitKibana('/app/observabilityOnboarding/'); - }); - - describe('When user clicks on the card', () => { - it('navigates to custom logs onboarding', () => { - cy.getByTestSubj('obltOnboardingHomeStartLogFileStream').click(); - - cy.url().should('include', '/app/observabilityOnboarding/customLogs'); - }); - - it('navigates to apm tutorial', () => { - cy.getByTestSubj('obltOnboardingHomeStartApmTutorial').click(); - - cy.url().should('include', '/app/home#/tutorial/apm'); - }); - - it('navigates to kubernetes integration', () => { - cy.getByTestSubj('obltOnboardingHomeGoToKubernetesIntegration').click(); - - cy.url().should('include', '/app/integrations/detail/kubernetes/overview'); - }); - - it('navigates to integrations', () => { - cy.getByTestSubj('obltOnboardingHomeExploreIntegrations').click(); - - cy.url().should('include', '/app/integrations/browse'); - }); - }); - - describe('When user clicks on Quick links', () => { - it('navigates to use sample data', () => { - cy.getByTestSubj('obltOnboardingHomeUseSampleData').click(); - - cy.url().should('include', '/app/home#/tutorial_directory/sampleData'); - }); - - it('navigates to upload a file', () => { - cy.getByTestSubj('obltOnboardingHomeUploadAFile').click(); - - cy.url().should('include', '/app/home#/tutorial_directory/fileDataViz'); - }); - }); -}); diff --git a/x-pack/solutions/observability/plugins/observability_onboarding/e2e/cypress/support/commands.ts b/x-pack/solutions/observability/plugins/observability_onboarding/e2e/cypress/support/commands.ts deleted file mode 100644 index dd07f4dc3dd4a..0000000000000 --- a/x-pack/solutions/observability/plugins/observability_onboarding/e2e/cypress/support/commands.ts +++ /dev/null @@ -1,182 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import URL from 'url'; -import { ObservabilityOnboardingUsername } from '@kbn/observability-onboarding-plugin/server/test_helpers/create_observability_onboarding_users/authentication'; - -export type InstallationStep = - | 'ea-download' - | 'ea-extract' - | 'ea-install' - | 'ea-status' - | 'ea-config'; - -export type InstallationStepStatus = - | 'incomplete' - | 'complete' - | 'disabled' - | 'loading' - | 'warning' - | 'danger' - | 'current'; - -export interface ElasticAgentStepPayload { - agentId: string; -} - -Cypress.Commands.add('loginAsViewerUser', () => { - return cy.loginAs({ - username: ObservabilityOnboardingUsername.viewerUser, - password: 'changeme', - }); -}); - -Cypress.Commands.add('loginAsEditorUser', () => { - return cy.loginAs({ - username: ObservabilityOnboardingUsername.editorUser, - password: 'changeme', - }); -}); - -Cypress.Commands.add('loginAsLogMonitoringUser', () => { - return cy.loginAs({ - username: ObservabilityOnboardingUsername.logMonitoringUser, - password: 'changeme', - }); -}); - -Cypress.Commands.add('loginAsElastic', () => { - return cy.loginAs({ - username: 'elastic', - password: 'changeme', - }); -}); - -Cypress.Commands.add( - 'loginAs', - ({ username, password }: { username: string; password: string }) => { - const kibanaUrl = Cypress.env('KIBANA_URL'); - cy.log(`Logging in as ${username} on ${kibanaUrl}`); - cy.visit('/'); - cy.request({ - log: true, - method: 'POST', - url: `${kibanaUrl}/internal/security/login`, - body: { - providerType: 'basic', - providerName: 'basic', - currentURL: `${kibanaUrl}/login`, - params: { username, password }, - }, - headers: { - 'kbn-xsrf': 'e2e_test', - }, - }); - cy.visit('/'); - } -); - -Cypress.Commands.add('getByTestSubj', (selector: string) => { - return cy.get(`[data-test-subj="${selector}"]`); -}); - -Cypress.Commands.add('visitKibana', (url: string, rangeFrom?: string, rangeTo?: string) => { - const urlPath = URL.format({ - pathname: url, - query: { rangeFrom, rangeTo }, - }); - - cy.visit(urlPath); - cy.getByTestSubj('kbnLoadingMessage').should('exist'); - cy.getByTestSubj('kbnLoadingMessage').should('not.exist', { - timeout: 50000, - }); -}); - -Cypress.Commands.add('installCustomIntegration', (integrationName: string) => { - const kibanaUrl = Cypress.env('KIBANA_URL'); - - cy.request({ - log: false, - method: 'POST', - url: `${kibanaUrl}/api/fleet/epm/custom_integrations`, - body: { - force: true, - integrationName, - datasets: [ - { name: `${integrationName}.access`, type: 'logs' }, - { name: `${integrationName}.error`, type: 'metrics' }, - { name: `${integrationName}.warning`, type: 'logs' }, - ], - }, - headers: { - 'kbn-xsrf': 'e2e_test', - 'Elastic-Api-Version': '2023-10-31', - }, - auth: { user: 'editor', pass: 'changeme' }, - }); -}); - -Cypress.Commands.add('deleteIntegration', (integrationName: string) => { - const kibanaUrl = Cypress.env('KIBANA_URL'); - - cy.request({ - log: false, - method: 'GET', - url: `${kibanaUrl}/api/fleet/epm/packages/${integrationName}`, - headers: { - 'kbn-xsrf': 'e2e_test', - }, - auth: { user: 'editor', pass: 'changeme' }, - failOnStatusCode: false, - }).then((response) => { - const status = response.body.item?.status; - if (status === 'installed') { - cy.request({ - log: false, - method: 'DELETE', - url: `${kibanaUrl}/api/fleet/epm/packages/${integrationName}`, - body: { - force: false, - }, - headers: { - 'kbn-xsrf': 'e2e_test', - 'Elastic-Api-Version': '2023-10-31', - }, - auth: { user: 'editor', pass: 'changeme' }, - }); - } - }); -}); - -Cypress.Commands.add( - 'updateInstallationStepStatus', - ( - onboardingId: string, - step: InstallationStep, - status: InstallationStepStatus, - payload: ElasticAgentStepPayload | undefined - ) => { - const kibanaUrl = Cypress.env('KIBANA_URL'); - - cy.log(onboardingId, step, status); - - cy.request({ - log: false, - method: 'POST', - url: `${kibanaUrl}/internal/observability_onboarding/flow/${onboardingId}/step/${step}`, - headers: { - 'kbn-xsrf': 'e2e_test', - }, - auth: { user: 'editor', pass: 'changeme' }, - body: { - status, - payload, - }, - }); - } -); diff --git a/x-pack/solutions/observability/plugins/observability_onboarding/e2e/cypress/support/types.d.ts b/x-pack/solutions/observability/plugins/observability_onboarding/e2e/cypress/support/types.d.ts deleted file mode 100644 index 7bb3549a60e7c..0000000000000 --- a/x-pack/solutions/observability/plugins/observability_onboarding/e2e/cypress/support/types.d.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -declare namespace Cypress { - interface Chainable { - loginAs(params: { - username: string; - password: string; - }): Cypress.Chainable>; - loginAsViewerUser(): Cypress.Chainable>; - loginAsEditorUser(): Cypress.Chainable>; - loginAsLogMonitoringUser(): Cypress.Chainable>; - loginAsElastic(): Cypress.Chainable>; - getByTestSubj(selector: string): Chainable>; - visitKibana(url: string, rangeFrom?: string, rangeTo?: string): void; - installCustomIntegration(integrationName: string): void; - deleteIntegration(integrationName: string): void; - updateInstallationStepStatus( - onboardingId: string, - step: InstallationStep, - status: InstallationStepStatus, - payload?: ElasticAgentStepPayload - ): void; - } -} diff --git a/x-pack/solutions/observability/plugins/observability_onboarding/e2e/cypress_test_runner.ts b/x-pack/solutions/observability/plugins/observability_onboarding/e2e/cypress_test_runner.ts deleted file mode 100644 index 4a8326d81b246..0000000000000 --- a/x-pack/solutions/observability/plugins/observability_onboarding/e2e/cypress_test_runner.ts +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import cypress from 'cypress'; -import path from 'path'; -import Url from 'url'; -import { createObservabilityOnboardingUsers } from '@kbn/observability-onboarding-plugin/server/test_helpers/create_observability_onboarding_users'; -import { FtrProviderContext } from './ftr_provider_context'; - -export async function cypressTestRunner({ - ftrProviderContext: { getService }, - cypressExecution, -}: { - ftrProviderContext: FtrProviderContext; - cypressExecution: typeof cypress.run | typeof cypress.open; -}) { - const config = getService('config'); - - const username = config.get('servers.elasticsearch.username'); - const password = config.get('servers.elasticsearch.password'); - - const kibanaUrl = Url.format({ - protocol: config.get('servers.kibana.protocol'), - hostname: config.get('servers.kibana.hostname'), - port: config.get('servers.kibana.port'), - auth: `${username}:${password}`, - }); - - const esNode = Url.format({ - protocol: config.get('servers.elasticsearch.protocol'), - port: config.get('servers.elasticsearch.port'), - hostname: config.get('servers.elasticsearch.hostname'), - auth: `${username}:${password}`, - }); - - // Creates ObservabilityOnboarding users - await createObservabilityOnboardingUsers({ - elasticsearch: { node: esNode, username, password }, - kibana: { hostname: kibanaUrl }, - }); - - const esRequestTimeout = config.get('timeouts.esRequestTimeout'); - - const kibanaUrlWithoutAuth = Url.format({ - protocol: config.get('servers.kibana.protocol'), - hostname: config.get('servers.kibana.hostname'), - port: config.get('servers.kibana.port'), - }); - - const cypressProjectPath = path.join(__dirname); - const { open, ...cypressCliArgs } = getCypressCliArgs(); - - const res = await cypressExecution({ - ...cypressCliArgs, - project: cypressProjectPath, - config: { - e2e: { - baseUrl: kibanaUrlWithoutAuth, - }, - }, - env: { - KIBANA_URL: kibanaUrlWithoutAuth, - ES_NODE: esNode, - ES_REQUEST_TIMEOUT: esRequestTimeout, - TEST_CLOUD: process.env.TEST_CLOUD, - }, - }); - - return res; -} - -function getCypressCliArgs(): Record { - if (!process.env.CYPRESS_CLI_ARGS) { - return {}; - } - - const { $0, _, ...cypressCliArgs } = JSON.parse(process.env.CYPRESS_CLI_ARGS) as Record< - string, - unknown - >; - - const spec = - typeof cypressCliArgs.spec === 'string' && !cypressCliArgs.spec.includes('**') - ? `**/${cypressCliArgs.spec}*` - : cypressCliArgs.spec; - - return { ...cypressCliArgs, spec }; -} diff --git a/x-pack/solutions/observability/plugins/observability_onboarding/e2e/ftr_config.ts b/x-pack/solutions/observability/plugins/observability_onboarding/e2e/ftr_config.ts deleted file mode 100644 index 56cb76e2b2ac7..0000000000000 --- a/x-pack/solutions/observability/plugins/observability_onboarding/e2e/ftr_config.ts +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { FtrConfigProviderContext } from '@kbn/test'; -import { CA_CERT_PATH } from '@kbn/dev-utils'; -import { commonFunctionalServices } from '@kbn/ftr-common-functional-services'; -import { commonFunctionalUIServices } from '@kbn/ftr-common-functional-ui-services'; -import path from 'path'; - -const kibanaYamlFilePath = path.join(__dirname, './ftr_kibana.yml'); - -async function ftrConfig({ readConfigFile }: FtrConfigProviderContext) { - const kibanaCommonTestsConfig = await readConfigFile( - require.resolve('@kbn/test-suites-src/common/config') - ); - const xpackFunctionalTestsConfig = await readConfigFile( - require.resolve('@kbn/test-suites-xpack/functional/config.base') - ); - - return { - ...kibanaCommonTestsConfig.getAll(), - - services: { - ...commonFunctionalServices, - ...commonFunctionalUIServices, - }, - - esTestCluster: { - ...xpackFunctionalTestsConfig.get('esTestCluster'), - serverArgs: [ - ...xpackFunctionalTestsConfig.get('esTestCluster.serverArgs'), - // define custom es server here - // API Keys is enabled at the top level - 'xpack.security.enabled=true', - ], - }, - - kbnTestServer: { - ...xpackFunctionalTestsConfig.get('kbnTestServer'), - serverArgs: [ - ...xpackFunctionalTestsConfig.get('kbnTestServer.serverArgs'), - '--home.disableWelcomeScreen=true', - '--csp.strict=false', - '--csp.warnLegacyBrowsers=false', - // define custom kibana server args here - `--elasticsearch.ssl.certificateAuthorities=${CA_CERT_PATH}`, - `--config=${kibanaYamlFilePath}`, - ], - }, - }; -} - -// eslint-disable-next-line import/no-default-export -export default ftrConfig; diff --git a/x-pack/solutions/observability/plugins/observability_onboarding/e2e/ftr_config_open.ts b/x-pack/solutions/observability/plugins/observability_onboarding/e2e/ftr_config_open.ts deleted file mode 100644 index 18508abf27f0c..0000000000000 --- a/x-pack/solutions/observability/plugins/observability_onboarding/e2e/ftr_config_open.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { FtrConfigProviderContext } from '@kbn/test'; -import cypress from 'cypress'; -import { FtrProviderContext } from './ftr_provider_context'; -import { cypressTestRunner } from './cypress_test_runner'; - -async function ftrConfigOpen({ readConfigFile }: FtrConfigProviderContext) { - const kibanaConfig = await readConfigFile(require.resolve('./ftr_config.ts')); - return { - ...kibanaConfig.getAll(), - testRunner, - }; -} - -export async function testRunner(ftrProviderContext: FtrProviderContext) { - await cypressTestRunner({ - ftrProviderContext, - cypressExecution: cypress.open, - }); -} - -// eslint-disable-next-line import/no-default-export -export default ftrConfigOpen; diff --git a/x-pack/solutions/observability/plugins/observability_onboarding/e2e/ftr_config_runner.ts b/x-pack/solutions/observability/plugins/observability_onboarding/e2e/ftr_config_runner.ts deleted file mode 100644 index 535062fdda0b0..0000000000000 --- a/x-pack/solutions/observability/plugins/observability_onboarding/e2e/ftr_config_runner.ts +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { FtrConfigProviderContext } from '@kbn/test'; -import cypress from 'cypress'; -import { cypressTestRunner } from './cypress_test_runner'; -import { FtrProviderContext } from './ftr_provider_context'; - -async function ftrConfigRun({ readConfigFile }: FtrConfigProviderContext) { - const kibanaConfig = await readConfigFile(require.resolve('./ftr_config.ts')); - - return { - ...kibanaConfig.getAll(), - testRunner, - }; -} - -async function testRunner(ftrProviderContext: FtrProviderContext) { - const result = await cypressTestRunner({ - ftrProviderContext, - cypressExecution: cypress.run, - }); - - if ( - result && - ((result as CypressCommandLine.CypressFailedRunResult)?.status === 'failed' || - (result as CypressCommandLine.CypressRunResult)?.totalFailed) - ) { - process.exit(1); - } -} - -// eslint-disable-next-line import/no-default-export -export default ftrConfigRun; diff --git a/x-pack/solutions/observability/plugins/observability_onboarding/e2e/ftr_kibana.yml b/x-pack/solutions/observability/plugins/observability_onboarding/e2e/ftr_kibana.yml deleted file mode 100644 index 06507182d40fe..0000000000000 --- a/x-pack/solutions/observability/plugins/observability_onboarding/e2e/ftr_kibana.yml +++ /dev/null @@ -1 +0,0 @@ -xpack.cloud.id: 'myDeployment:03ce773830e104e139243c8f053c974d' diff --git a/x-pack/solutions/observability/plugins/observability_onboarding/e2e/kibana.jsonc b/x-pack/solutions/observability/plugins/observability_onboarding/e2e/kibana.jsonc deleted file mode 100644 index 033d72a02427f..0000000000000 --- a/x-pack/solutions/observability/plugins/observability_onboarding/e2e/kibana.jsonc +++ /dev/null @@ -1,8 +0,0 @@ -{ - "type": "test-helper", - "id": "@kbn/observability-onboarding-e2e", - "owner": ["@elastic/obs-ux-logs-team"], - "group": "observability", - "visibility": "private", - "devOnly": true -} diff --git a/x-pack/solutions/observability/plugins/observability_onboarding/e2e/tsconfig.json b/x-pack/solutions/observability/plugins/observability_onboarding/e2e/tsconfig.json deleted file mode 100644 index 33051d690a6f7..0000000000000 --- a/x-pack/solutions/observability/plugins/observability_onboarding/e2e/tsconfig.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "extends": "../../../../../../tsconfig.base.json", - "include": ["**/*"], - "exclude": ["tmp", "target/**/*"], - "compilerOptions": { - "outDir": "target/types", - "types": ["cypress", "node", "cypress-real-events"], - "isolatedModules": false - }, - "kbn_references": [ - { "path": "../../../../test/tsconfig.json" }, - "@kbn/test", - "@kbn/dev-utils", - "@kbn/cypress-config", - "@kbn/observability-onboarding-plugin", - "@kbn/ftr-common-functional-services", - "@kbn/ftr-common-functional-ui-services", - "@kbn/tooling-log" - ] -} diff --git a/x-pack/solutions/observability/plugins/observability_onboarding/tsconfig.json b/x-pack/solutions/observability/plugins/observability_onboarding/tsconfig.json index 391f1d6fee64f..79e54ce203d63 100644 --- a/x-pack/solutions/observability/plugins/observability_onboarding/tsconfig.json +++ b/x-pack/solutions/observability/plugins/observability_onboarding/tsconfig.json @@ -8,7 +8,8 @@ "public/**/*", "../../../../../typings/**/*", "public/**/*.json", - "server/**/*" + "server/**/*", + "e2e/**/*" ], "kbn_references": [ "@kbn/core", @@ -43,7 +44,8 @@ "@kbn/server-route-repository-utils", "@kbn/core-application-browser", "@kbn/core-plugins-server", - "@kbn/logs-shared-plugin" + "@kbn/logs-shared-plugin", + "@kbn/tooling-log" ], "exclude": [ "target/**/*" diff --git a/x-pack/solutions/observability/plugins/observability_onboarding/ui_tests/README.md b/x-pack/solutions/observability/plugins/observability_onboarding/ui_tests/README.md new file mode 100644 index 0000000000000..9a45d498a61eb --- /dev/null +++ b/x-pack/solutions/observability/plugins/observability_onboarding/ui_tests/README.md @@ -0,0 +1,23 @@ +## How to run tests + +First start the servers: + +```bash +// ESS +node scripts/scout.js start-server --stateful + +// Serverless +node scripts/scout.js start-server --serverless=[es|oblt|security] +``` + +Then you can run the tests in another terminal: + +```bash +// ESS +npx playwright test --config x-pack/solutions/observability/plugins/observability_onboarding/ui_tests/playwright.config.ts --grep @ess + +// Serverless +npx playwright test --config x-pack/solutions/observability/plugins/observability_onboarding/ui_tests/playwright.config.ts --grep @svlOblt +``` + +Test results are available in `x-pack/solutions/observability/plugins/observability_onboarding/ui_tests/output` diff --git a/x-pack/solutions/observability/plugins/observability_onboarding/ui_tests/fixtures/index.ts b/x-pack/solutions/observability/plugins/observability_onboarding/ui_tests/fixtures/index.ts new file mode 100644 index 0000000000000..a928c1d6fe34b --- /dev/null +++ b/x-pack/solutions/observability/plugins/observability_onboarding/ui_tests/fixtures/index.ts @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + test as base, + PageObjects, + createLazyPageObject, + ScoutTestFixtures, + ScoutWorkerFixtures, + KibanaUrl, + KbnClient, +} from '@kbn/scout'; +import { OnboardingHomePage } from './page_objects'; +import { CustomLogsPage } from './page_objects/custom_logs'; + +export interface ExtendedScoutTestFixtures extends ScoutTestFixtures { + pageObjects: PageObjects & { + onboardingHomePage: OnboardingHomePage; + customLogsPage: CustomLogsPage; + }; +} + +export const test = base.extend({ + pageObjects: async ( + { + pageObjects, + page, + kbnUrl, + kbnClient, + }: { + pageObjects: ExtendedScoutTestFixtures['pageObjects']; + page: ExtendedScoutTestFixtures['page']; + kbnUrl: KibanaUrl; + kbnClient: KbnClient; + }, + use: (pageObjects: ExtendedScoutTestFixtures['pageObjects']) => Promise + ) => { + const extendedPageObjects = { + ...pageObjects, + onboardingHomePage: createLazyPageObject(OnboardingHomePage, page), + customLogsPage: createLazyPageObject(CustomLogsPage, page, kbnUrl, kbnClient), + }; + + await use(extendedPageObjects); + }, +}); diff --git a/x-pack/solutions/observability/plugins/observability_onboarding/ui_tests/fixtures/page_objects/custom_logs.ts b/x-pack/solutions/observability/plugins/observability_onboarding/ui_tests/fixtures/page_objects/custom_logs.ts new file mode 100644 index 0000000000000..1b2b8119e5ce2 --- /dev/null +++ b/x-pack/solutions/observability/plugins/observability_onboarding/ui_tests/fixtures/page_objects/custom_logs.ts @@ -0,0 +1,195 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ScoutPage, KibanaUrl, KbnClient } from '@kbn/scout'; + +export class CustomLogsPage { + static readonly ASSERTION_MESSAGES = { + INTEGRATION_NAME_CASE_ERROR: 'An integration name should be lowercase.', + DATASET_NAME_CASE_ERROR: 'A dataset name should be lowercase.', + EXISTING_INTEGRATION_ERROR: (name: string) => + `Failed to create the integration as an installation with the name ${name} already exists.`, + + DOWNLOADING_AGENT_STATUS: 'Downloading Elastic Agent', + DOWNLOADED_AGENT_STATUS: 'Elastic Agent downloaded', + DOWNLOAD_AGENT_DANGER_CALLOUT: 'Download Elastic Agent', + EXTRACTING_AGENT_STATUS: 'Extracting Elastic Agent', + EXTRACTED_AGENT_STATUS: 'Elastic Agent extracted', + EXTRACT_AGENT_DANGER_CALLOUT: 'Extract Elastic Agent', + INSTALLING_AGENT_STATUS: 'Installing Elastic Agent', + INSTALLED_AGENT_STATUS: 'Elastic Agent installed', + INSTALL_AGENT_DANGER_CALLOUT: 'Install Elastic Agent', + CONNECTING_TO_AGENT_STATUS: 'Connecting to the Elastic Agent', + CONNECTED_TO_AGENT_STATUS: 'Connected to the Elastic Agent', + CONNECT_AGENT_WARNING_CALLOUT: 'Connect to the Elastic Agent', + DOWNLOAD_AGENT_CONFIG_STATUS: 'Downloading Elastic Agent config', + CONFIGURE_AGENT_WARNING_CALLOUT: 'Configure the agent', + DOWNLOADING_AGENT_CONFIG_STATUS: 'Downloading Elastic Agent config', + AGENT_CONFIGURATION_SUCCESS_CALLOUT_MACOS: + 'Elastic Agent config written to /Library/Elastic/Agent/elastic-agent.yml', + AGENT_CONFIGURATION_SUCCESS_CALLOUT_LINUX: + 'Elastic Agent config written to /opt/Elastic/Agent/elastic-agent.yml', + INSTALLATION_STEP_2_DISABLED: 'Step 2 is disabled', + }; + + public readonly advancedSettingsContent; + public readonly logFilePathList; + public readonly continueButton; + public readonly addLogFilePathButton; + public readonly integrationNameInput; + public readonly datasetNameInput; + public readonly serviceNameInput; + public readonly namespaceInput; + public readonly customConfigInput; + public readonly customIntegrationSuccessCallout; + public readonly customIntegrationErrorCallout; + public readonly apiKeyCreateSuccessCallout; + public readonly apiKeyPrivilegesErrorCallout; + public readonly apiKeyCreateErrorCallout; + public readonly linuxCodeSnippetButton; + public readonly macOSCodeSnippetButton; + public readonly windowsCodeSnippetButton; + public readonly autoDownloadConfigurationToggle; + public readonly autoDownloadConfigurationCallout; + public readonly installCodeSnippet; + public readonly windowsInstallElasticAgentDocLink; + public readonly configureElasticAgentStep; + public readonly downloadConfigurationButton; + public readonly stepStatusLoading; + public readonly stepStatusComplete; + public readonly stepStatusDanger; + public readonly stepStatusWarning; + + constructor( + private readonly page: ScoutPage, + private readonly kbnUrl: KibanaUrl, + private readonly kbnClient: KbnClient + ) { + this.advancedSettingsContent = this.page.testSubj + .locator('obltOnboardingCustomLogsAdvancedSettings') + .getByRole('group'); + this.logFilePathList = this.page.locator(`[data-test-subj^=obltOnboardingLogFilePath-]`); + this.continueButton = this.page.testSubj.locator('obltOnboardingCustomLogsContinue'); + this.addLogFilePathButton = this.page.testSubj.locator('obltOnboardingCustomLogsAddFilePath'); + this.integrationNameInput = this.page.testSubj.locator( + 'obltOnboardingCustomLogsIntegrationsName' + ); + this.datasetNameInput = this.page.testSubj.locator('obltOnboardingCustomLogsDatasetName'); + this.serviceNameInput = this.page.testSubj.locator('obltOnboardingCustomLogsServiceName'); + this.namespaceInput = this.page.testSubj.locator('obltOnboardingCustomLogsNamespace'); + this.customConfigInput = this.page.testSubj.locator('obltOnboardingCustomLogsCustomConfig'); + this.customIntegrationSuccessCallout = this.page.testSubj.locator( + 'obltOnboardingCustomIntegrationInstalled' + ); + this.customIntegrationErrorCallout = this.page.testSubj.locator( + 'obltOnboardingCustomIntegrationErrorCallout' + ); + this.apiKeyCreateSuccessCallout = this.page.testSubj.locator('obltOnboardingLogsApiKeyCreated'); + this.apiKeyPrivilegesErrorCallout = this.page.testSubj.locator( + 'obltOnboardingLogsApiKeyCreationNoPrivileges' + ); + this.apiKeyCreateErrorCallout = this.page.testSubj.locator( + 'obltOnboardingLogsApiKeyCreationFailed' + ); + this.linuxCodeSnippetButton = this.page.testSubj.locator('linux-tar'); + this.macOSCodeSnippetButton = this.page.testSubj.locator('macos'); + this.windowsCodeSnippetButton = this.page.testSubj.locator('windows'); + this.autoDownloadConfigurationToggle = this.page.testSubj.locator( + 'obltOnboardingInstallElasticAgentAutoDownloadConfig' + ); + this.autoDownloadConfigurationCallout = this.page.testSubj.locator( + 'obltOnboardingInstallElasticAgentAutoDownloadConfigCallout' + ); + this.installCodeSnippet = this.page.testSubj + .locator('obltOnboardingInstallElasticAgentStep') + .getByRole('code'); + this.windowsInstallElasticAgentDocLink = this.page.testSubj.locator( + 'obltOnboardingInstallElasticAgentWindowsDocsLink' + ); + this.configureElasticAgentStep = this.page.testSubj.locator( + 'obltOnboardingConfigureElasticAgentStep' + ); + this.downloadConfigurationButton = this.page.testSubj.locator( + 'obltOnboardingConfigureElasticAgentStepDownloadConfig' + ); + this.stepStatusLoading = this.page.testSubj.locator('obltOnboardingStepStatus-loading'); + this.stepStatusComplete = this.page.testSubj.locator('obltOnboardingStepStatus-complete'); + this.stepStatusDanger = this.page.testSubj.locator('obltOnboardingStepStatus-danger'); + this.stepStatusWarning = this.page.testSubj.locator('obltOnboardingStepStatus-warning'); + } + + async goto() { + this.page.goto(`${this.kbnUrl.app('observabilityOnboarding')}/customLogs`); + } + + async clickBackButton() { + await this.page.testSubj.click('observabilityOnboardingFlowBackToSelectionButton'); + } + + logFilePathInput(index: number) { + return this.page.testSubj.locator(`obltOnboardingLogFilePath-${index}`).getByRole('textbox'); + } + + logFilePathDeleteButton(index: number) { + return this.page.testSubj.locator(`obltOnboardingLogFilePathDelete-${index}`); + } + + async clickAdvancedSettingsButton() { + return this.page.testSubj + .locator('obltOnboardingCustomLogsAdvancedSettings') + .getByRole('button') + .first() + .click(); + } + + async installCustomIntegration(name: string) { + await this.kbnClient.request({ + method: 'POST', + path: `/api/fleet/epm/custom_integrations`, + body: { + force: true, + integrationName: name, + datasets: [ + { name: `${name}.access`, type: 'logs' }, + { name: `${name}.error`, type: 'metrics' }, + { name: `${name}.warning`, type: 'logs' }, + ], + }, + }); + } + + async deleteIntegration(name: string) { + const packageInfo = await this.kbnClient.request<{ item: { status: string } }>({ + method: 'GET', + path: `/api/fleet/epm/packages/${name}`, + ignoreErrors: [404], + }); + + if (packageInfo.data.item?.status === 'installed') { + await this.kbnClient.request({ + method: 'DELETE', + path: `/api/fleet/epm/packages/${name}`, + }); + } + } + + async updateInstallationStepStatus( + onboardingId: string, + step: string, + status: string, + payload?: object + ) { + await this.kbnClient.request({ + method: 'POST', + path: `/internal/observability_onboarding/flow/${onboardingId}/step/${step}`, + body: { + status, + payload, + }, + }); + } +} diff --git a/x-pack/solutions/observability/plugins/observability_onboarding/e2e/cypress/support/e2e.ts b/x-pack/solutions/observability/plugins/observability_onboarding/ui_tests/fixtures/page_objects/index.ts similarity index 72% rename from x-pack/solutions/observability/plugins/observability_onboarding/e2e/cypress/support/e2e.ts rename to x-pack/solutions/observability/plugins/observability_onboarding/ui_tests/fixtures/page_objects/index.ts index 5f5d1eb3b3614..c032371659593 100644 --- a/x-pack/solutions/observability/plugins/observability_onboarding/e2e/cypress/support/e2e.ts +++ b/x-pack/solutions/observability/plugins/observability_onboarding/ui_tests/fixtures/page_objects/index.ts @@ -5,8 +5,4 @@ * 2.0. */ -Cypress.on('uncaught:exception', (err, runnable) => { - return false; -}); - -import './commands'; +export { OnboardingHomePage } from './onboarding_home'; diff --git a/x-pack/solutions/observability/plugins/observability_onboarding/ui_tests/fixtures/page_objects/onboarding_home.ts b/x-pack/solutions/observability/plugins/observability_onboarding/ui_tests/fixtures/page_objects/onboarding_home.ts new file mode 100644 index 0000000000000..bd35dc4fbe909 --- /dev/null +++ b/x-pack/solutions/observability/plugins/observability_onboarding/ui_tests/fixtures/page_objects/onboarding_home.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ScoutPage } from '@kbn/scout'; + +export class OnboardingHomePage { + constructor(private readonly page: ScoutPage) {} + + async goto() { + this.page.gotoApp('observabilityOnboarding'); + } +} diff --git a/x-pack/solutions/observability/plugins/observability_onboarding/e2e/ftr_provider_context.d.ts b/x-pack/solutions/observability/plugins/observability_onboarding/ui_tests/playwright.config.ts similarity index 59% rename from x-pack/solutions/observability/plugins/observability_onboarding/e2e/ftr_provider_context.d.ts rename to x-pack/solutions/observability/plugins/observability_onboarding/ui_tests/playwright.config.ts index b87f35adcccf2..34b370396b67e 100644 --- a/x-pack/solutions/observability/plugins/observability_onboarding/e2e/ftr_provider_context.d.ts +++ b/x-pack/solutions/observability/plugins/observability_onboarding/ui_tests/playwright.config.ts @@ -5,6 +5,9 @@ * 2.0. */ -import { GenericFtrProviderContext } from '@kbn/test'; +import { createPlaywrightConfig } from '@kbn/scout'; -export type FtrProviderContext = GenericFtrProviderContext<{}, {}>; +// eslint-disable-next-line import/no-default-export +export default createPlaywrightConfig({ + testDir: './tests', +}); diff --git a/x-pack/solutions/observability/plugins/observability_onboarding/ui_tests/tests/custom_logs/configuration.spec.ts b/x-pack/solutions/observability/plugins/observability_onboarding/ui_tests/tests/custom_logs/configuration.spec.ts new file mode 100644 index 0000000000000..2a2db5af2c7ec --- /dev/null +++ b/x-pack/solutions/observability/plugins/observability_onboarding/ui_tests/tests/custom_logs/configuration.spec.ts @@ -0,0 +1,256 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { expect } from '@kbn/scout'; +import { test } from '../../fixtures'; +import { CustomLogsPage } from '../../fixtures/page_objects/custom_logs'; + +test.describe('Onboarding app - Custom logs configuration', { tag: ['@ess', '@svlOblt'] }, () => { + test.beforeEach(async ({ browserAuth, pageObjects: { customLogsPage } }) => { + await browserAuth.loginAsAdmin(); + await customLogsPage.goto(); + }); + + test('When user clicks the back button user goes to the onboarding home page', async ({ + pageObjects: { customLogsPage }, + page, + }) => { + await customLogsPage.clickBackButton(); + expect(page.url()).toContain('/app/observabilityOnboarding'); + }); + + test(`Users shouldn't be able to continue if logFilePaths is empty`, async ({ + pageObjects: { customLogsPage }, + }) => { + await expect(customLogsPage.logFilePathInput(0)).toHaveValue(''); + await expect(customLogsPage.continueButton).toBeDisabled(); + }); + + test(`Users should be able to continue if logFilePaths is not empty`, async ({ + pageObjects: { customLogsPage }, + }) => { + await customLogsPage.logFilePathInput(0).fill('some/path'); + + await expect(customLogsPage.continueButton).not.toBeDisabled(); + }); + + test('Users can add multiple logFilePaths', async ({ pageObjects: { customLogsPage } }) => { + await customLogsPage.addLogFilePathButton.click(); + await expect(customLogsPage.logFilePathInput(0)).toBeVisible(); + await expect(customLogsPage.logFilePathInput(1)).toBeVisible(); + }); + + test('Users can delete logFilePaths', async ({ pageObjects: { customLogsPage } }) => { + await customLogsPage.addLogFilePathButton.click(); + await expect(customLogsPage.logFilePathList).toHaveCount(2); + + await customLogsPage.logFilePathDeleteButton(1).click(); + await expect(customLogsPage.logFilePathList).toHaveCount(1); + }); + + test('Dataset Name and Integration Name are auto generated if it is the first path', async ({ + pageObjects: { customLogsPage }, + }) => { + await customLogsPage.logFilePathInput(0).fill('myLogs.log'); + + await expect(customLogsPage.integrationNameInput).toHaveValue('mylogs'); + await expect(customLogsPage.datasetNameInput).toHaveValue('mylogs'); + }); + + test('Dataset Name and Integration Name are not generated if it is not the first path', async ({ + pageObjects: { customLogsPage }, + }) => { + await customLogsPage.addLogFilePathButton.click(); + await customLogsPage.logFilePathInput(1).fill('myLogs.log'); + + await expect(customLogsPage.integrationNameInput).toHaveValue(''); + await expect(customLogsPage.datasetNameInput).toHaveValue(''); + }); + + test('Service name input should be optional allowing user to continue if it is empty', async ({ + pageObjects: { customLogsPage }, + }) => { + await customLogsPage.logFilePathInput(0).fill('myLogs.log'); + + await expect(customLogsPage.serviceNameInput).toHaveValue(''); + await expect(customLogsPage.continueButton).not.toBeDisabled(); + }); + + test('Users should expand and collapse the Advanced settings section', async ({ + pageObjects: { customLogsPage }, + }) => { + await expect(customLogsPage.advancedSettingsContent).not.toBeVisible(); + + await customLogsPage.clickAdvancedSettingsButton(); + + await expect(customLogsPage.advancedSettingsContent).toBeVisible(); + + await customLogsPage.clickAdvancedSettingsButton(); + + await expect(customLogsPage.advancedSettingsContent).not.toBeVisible(); + }); + + test('Users should see a default namespace', async ({ pageObjects: { customLogsPage } }) => { + customLogsPage.clickAdvancedSettingsButton(); + + await expect(customLogsPage.namespaceInput).toHaveValue('default'); + }); + + test('Users should not be able to continue if they do not specify a namespace', async ({ + pageObjects: { customLogsPage }, + }) => { + customLogsPage.clickAdvancedSettingsButton(); + + await customLogsPage.namespaceInput.fill(''); + await expect(customLogsPage.continueButton).toBeDisabled(); + }); + + test('should be optional allowing user to continue if it is empty', async ({ + pageObjects: { customLogsPage }, + }) => { + await customLogsPage.clickAdvancedSettingsButton(); + await customLogsPage.logFilePathInput(0).fill('myLogs.log'); + await expect(customLogsPage.customConfigInput).toHaveValue(''); + await expect(customLogsPage.continueButton).not.toBeDisabled(); + }); + + test.describe('Integration name', () => { + test.beforeEach(async ({ pageObjects: { customLogsPage } }) => { + await customLogsPage.logFilePathInput(0).fill('myLogs.log'); + }); + + test('Users should not be able to continue if they do not specify an integrationName', async ({ + pageObjects: { customLogsPage }, + }) => { + await customLogsPage.integrationNameInput.fill(''); + await expect(customLogsPage.continueButton).toBeDisabled(); + }); + + test('value will contain _ instead of special chars', async ({ + pageObjects: { customLogsPage }, + }) => { + await customLogsPage.integrationNameInput.fill('hello$world'); + await expect(customLogsPage.integrationNameInput).toHaveValue('hello_world'); + }); + + test('value will be invalid if it is not lowercase', async ({ + pageObjects: { customLogsPage }, + page, + }) => { + await customLogsPage.integrationNameInput.fill('H3llowOrld'); + await expect( + page.getByText(CustomLogsPage.ASSERTION_MESSAGES.INTEGRATION_NAME_CASE_ERROR) + ).toBeVisible(); + }); + }); + + test.describe('datasetName', () => { + test.beforeEach(async ({ pageObjects: { customLogsPage } }) => { + await customLogsPage.logFilePathInput(0).fill('myLogs.log'); + }); + + test('Users should not be able to continue if they do not specify a datasetName', async ({ + pageObjects: { customLogsPage }, + }) => { + await customLogsPage.datasetNameInput.fill(''); + await expect(customLogsPage.continueButton).toBeDisabled(); + }); + + test('value will contain _ instead of special chars', async ({ + pageObjects: { customLogsPage }, + }) => { + await customLogsPage.datasetNameInput.fill('hello$world'); + await expect(customLogsPage.datasetNameInput).toHaveValue('hello_world'); + }); + + test('value will be invalid if it is not lowercase', async ({ + pageObjects: { customLogsPage }, + page, + }) => { + await customLogsPage.datasetNameInput.fill('H3llowOrld'); + await expect( + page.getByText(CustomLogsPage.ASSERTION_MESSAGES.DATASET_NAME_CASE_ERROR) + ).toBeVisible(); + }); + }); + + test.describe('Custom integration', () => { + const CUSTOM_INTEGRATION_NAME = 'mylogs'; + + test.beforeEach(async ({ pageObjects: { customLogsPage } }) => { + await customLogsPage.deleteIntegration(CUSTOM_INTEGRATION_NAME); + }); + + test.describe('when user is missing privileges', () => { + test.beforeEach(async ({ browserAuth, page }) => { + await browserAuth.loginAsViewer(); + await page.reload(); + }); + + test.afterEach(async ({ browserAuth, page }) => { + await browserAuth.loginAsAdmin(); + await page.reload(); + }); + + test('installation fails', async ({ pageObjects: { customLogsPage } }) => { + await customLogsPage.logFilePathInput(0).fill(`${CUSTOM_INTEGRATION_NAME}.log`); + await customLogsPage.continueButton.click(); + + await expect(customLogsPage.customIntegrationErrorCallout).toBeVisible(); + }); + }); + + test.describe('when user has proper privileges', () => { + test('installation succeed and user is redirected to install elastic agent step', async ({ + page, + pageObjects: { customLogsPage }, + }) => { + await customLogsPage.logFilePathInput(0).fill(`${CUSTOM_INTEGRATION_NAME}.log`); + await customLogsPage.continueButton.click(); + + await page.waitForURL('**/app/observabilityOnboarding/customLogs/installElasticAgent'); + }); + }); + + test('installation fails if integration already exists', async ({ + pageObjects: { customLogsPage }, + page, + }) => { + await customLogsPage.installCustomIntegration(CUSTOM_INTEGRATION_NAME); + await customLogsPage.logFilePathInput(0).fill(`${CUSTOM_INTEGRATION_NAME}.log`); + await customLogsPage.continueButton.click(); + + await expect( + page.getByText( + CustomLogsPage.ASSERTION_MESSAGES.EXISTING_INTEGRATION_ERROR(CUSTOM_INTEGRATION_NAME) + ) + ).toBeVisible(); + }); + + test.describe('when an error occurred on creation', () => { + test('user should see the error displayed', async ({ + pageObjects: { customLogsPage }, + page, + kbnUrl, + }) => { + await page.route(kbnUrl.get('/api/fleet/epm/custom_integrations'), (route) => { + route.fulfill({ + status: 500, + json: { + message: 'Internal error', + }, + }); + }); + + await customLogsPage.logFilePathInput(0).fill(`${CUSTOM_INTEGRATION_NAME}.log`); + await customLogsPage.continueButton.click(); + + await expect(customLogsPage.customIntegrationErrorCallout).toBeVisible(); + }); + }); + }); +}); diff --git a/x-pack/solutions/observability/plugins/observability_onboarding/ui_tests/tests/custom_logs/install_elastic_agent.spec.ts b/x-pack/solutions/observability/plugins/observability_onboarding/ui_tests/tests/custom_logs/install_elastic_agent.spec.ts new file mode 100644 index 0000000000000..2dadb23090418 --- /dev/null +++ b/x-pack/solutions/observability/plugins/observability_onboarding/ui_tests/tests/custom_logs/install_elastic_agent.spec.ts @@ -0,0 +1,696 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { expect } from '@kbn/scout'; +import { type ExtendedScoutTestFixtures, test } from '../../fixtures'; +import { CustomLogsPage } from '../../fixtures/page_objects/custom_logs'; + +const CUSTOM_INTEGRATION_NAME = 'mylogs'; + +function setupPage({ isAdmin }: { isAdmin: boolean }) { + return async ({ + browserAuth, + pageObjects: { customLogsPage }, + page, + }: ExtendedScoutTestFixtures) => { + await browserAuth.loginAsAdmin(); + await customLogsPage.deleteIntegration(CUSTOM_INTEGRATION_NAME); + + if (!isAdmin) { + await browserAuth.loginAsPrivilegedUser(); + await page.reload(); + } + + await customLogsPage.deleteIntegration(CUSTOM_INTEGRATION_NAME); + + await customLogsPage.goto(); + await customLogsPage.logFilePathInput(0).fill('mylogs.log'); + await customLogsPage.continueButton.click(); + }; +} + +test.describe( + 'Onboarding app - Custom logs install Elastic Agent', + { tag: ['@ess', '@svlOblt'] }, + () => { + test.describe('Custom integration', () => { + test.beforeEach(setupPage({ isAdmin: true })); + + test('Users should be able to see the custom integration success callout', async ({ + pageObjects: { customLogsPage }, + }) => { + await expect(customLogsPage.customIntegrationSuccessCallout).toBeVisible(); + }); + }); + + test.describe('ApiKey generation', () => { + test.describe('when user is missing privileges', () => { + test.beforeEach(setupPage({ isAdmin: false })); + + test('apiKey is not generated', async ({ pageObjects: { customLogsPage } }) => { + await expect(customLogsPage.apiKeyPrivilegesErrorCallout).toBeVisible(); + }); + }); + + test.describe('when user has proper privileges', () => { + test.beforeEach(setupPage({ isAdmin: true })); + + test('apiKey is generated', async ({ pageObjects: { customLogsPage } }) => { + await expect(customLogsPage.apiKeyCreateSuccessCallout).toBeVisible(); + }); + }); + + test.describe('when an error occurred on creation', () => { + test.beforeEach(async ({ browserAuth, pageObjects: { customLogsPage }, page }) => { + await page.route('**/observability_onboarding/logs/flow', (route) => { + route.fulfill({ + status: 500, + body: JSON.stringify({ + message: 'Internal error', + }), + }); + }); + }); + + test.beforeEach(setupPage({ isAdmin: true })); + + test('apiKey is not generated', async ({ pageObjects: { customLogsPage } }) => { + await expect(customLogsPage.apiKeyCreateErrorCallout).toBeVisible(); + }); + }); + }); + + test.describe('Install the Elastic Agent step', () => { + test.beforeEach(setupPage({ isAdmin: true })); + + test.describe('When user select Linux OS', () => { + test('Auto download config to host is disabled by default', async ({ + pageObjects: { customLogsPage }, + }) => { + await customLogsPage.linuxCodeSnippetButton.click(); + await expect(customLogsPage.autoDownloadConfigurationToggle).not.toBeChecked(); + }); + + test('Installation script is shown', async ({ pageObjects: { customLogsPage } }) => { + await expect(customLogsPage.installCodeSnippet).toBeVisible(); + }); + }); + + test.describe('When user select Mac OS', () => { + test.beforeEach(async ({ pageObjects: { customLogsPage } }) => { + await customLogsPage.macOSCodeSnippetButton.click(); + }); + + test('Auto download config to host is disabled by default', async ({ + pageObjects: { customLogsPage }, + }) => { + await expect(customLogsPage.autoDownloadConfigurationToggle).not.toBeChecked(); + }); + + test('Installation script is shown', async ({ pageObjects: { customLogsPage } }) => { + await expect(customLogsPage.installCodeSnippet).toBeVisible(); + }); + }); + + test.describe('When user select Windows OS', () => { + test.beforeEach(async ({ pageObjects: { customLogsPage } }) => { + await customLogsPage.windowsCodeSnippetButton.click(); + }); + + test('Auto download config to host is disabled by default', async ({ + pageObjects: { customLogsPage }, + }) => { + await expect(customLogsPage.autoDownloadConfigurationToggle).not.toBeChecked(); + }); + + test('A link to the documentation is shown instead of installation script', async ({ + pageObjects: { customLogsPage }, + }) => { + await expect(customLogsPage.windowsInstallElasticAgentDocLink).toBeVisible(); + await expect(customLogsPage.installCodeSnippet).not.toBeVisible(); + }); + }); + + test.describe('When Auto download config', () => { + test.describe('is selected', () => { + test('autoDownloadConfig flag is added to installation script', async ({ + pageObjects: { customLogsPage }, + }) => { + await customLogsPage.autoDownloadConfigurationToggle.click(); + await expect(customLogsPage.autoDownloadConfigurationCallout).toBeVisible(); + await expect(customLogsPage.installCodeSnippet).toContainText('autoDownloadConfig=1'); + }); + + test('Download config button is disabled', async ({ + pageObjects: { customLogsPage }, + }) => { + await customLogsPage.autoDownloadConfigurationToggle.click(); + await expect(customLogsPage.downloadConfigurationButton).toBeDisabled(); + }); + }); + + test('is not selected autoDownloadConfig flag is not added to installation script', async ({ + pageObjects: { customLogsPage }, + }) => { + await expect(customLogsPage.installCodeSnippet).not.toContainText('autoDownloadConfig=1'); + }); + }); + + test.describe('When user executes the installation script in the host', () => { + let onboardingId: string; + + test.describe('updates on steps are shown in the flow', () => { + test.beforeEach(async ({ page, pageObjects: { customLogsPage } }) => { + await page.route('**/observability_onboarding/logs/flow', async (route) => { + const response = await route.fetch(); + const body = await response.json(); + + onboardingId = body.onboardingId; + + await route.fulfill({ response }); + }); + + await expect(customLogsPage.apiKeyCreateSuccessCallout).toBeVisible(); + }); + + test.describe('Download elastic Agent step', () => { + test('shows a loading callout when elastic agent is downloading', async ({ + pageObjects: { customLogsPage }, + }) => { + await customLogsPage.updateInstallationStepStatus( + onboardingId, + 'ea-download', + 'loading' + ); + await expect( + customLogsPage.stepStatusLoading.getByText( + CustomLogsPage.ASSERTION_MESSAGES.DOWNLOADING_AGENT_STATUS + ) + ).toBeVisible(); + }); + + test('shows a success callout when elastic agent is downloaded', async ({ + pageObjects: { customLogsPage }, + }) => { + await customLogsPage.updateInstallationStepStatus( + onboardingId, + 'ea-download', + 'complete' + ); + await expect( + customLogsPage.stepStatusComplete.getByText( + CustomLogsPage.ASSERTION_MESSAGES.DOWNLOADED_AGENT_STATUS + ) + ).toBeVisible(); + }); + + test('shows a danger callout when elastic agent was not downloaded', async ({ + pageObjects: { customLogsPage }, + }) => { + await customLogsPage.updateInstallationStepStatus( + onboardingId, + 'ea-download', + 'danger' + ); + await expect( + customLogsPage.stepStatusDanger.getByText( + CustomLogsPage.ASSERTION_MESSAGES.DOWNLOAD_AGENT_DANGER_CALLOUT + ) + ).toBeVisible(); + }); + }); + + test.describe('Extract elastic Agent step', () => { + test.beforeEach(async ({ pageObjects: { customLogsPage } }) => { + await customLogsPage.updateInstallationStepStatus( + onboardingId, + 'ea-download', + 'complete' + ); + }); + + test('shows a loading callout when elastic agent is extracting', async ({ + pageObjects: { customLogsPage }, + }) => { + await customLogsPage.updateInstallationStepStatus( + onboardingId, + 'ea-extract', + 'loading' + ); + await expect( + customLogsPage.stepStatusLoading.getByText( + CustomLogsPage.ASSERTION_MESSAGES.EXTRACTING_AGENT_STATUS + ) + ).toBeVisible(); + }); + + test('shows a success callout when elastic agent is extracted', async ({ + pageObjects: { customLogsPage }, + }) => { + await customLogsPage.updateInstallationStepStatus( + onboardingId, + 'ea-extract', + 'complete' + ); + await expect( + customLogsPage.stepStatusComplete.getByText( + CustomLogsPage.ASSERTION_MESSAGES.EXTRACTED_AGENT_STATUS + ) + ).toBeVisible(); + }); + + test('shows a danger callout when elastic agent was not extracted', async ({ + pageObjects: { customLogsPage }, + }) => { + await customLogsPage.updateInstallationStepStatus( + onboardingId, + 'ea-extract', + 'danger' + ); + await expect( + customLogsPage.stepStatusDanger.getByText( + CustomLogsPage.ASSERTION_MESSAGES.EXTRACT_AGENT_DANGER_CALLOUT + ) + ).toBeVisible(); + }); + }); + + test.describe('Install elastic Agent step', () => { + test.beforeEach(async ({ pageObjects: { customLogsPage } }) => { + await customLogsPage.updateInstallationStepStatus( + onboardingId, + 'ea-download', + 'complete' + ); + await customLogsPage.updateInstallationStepStatus( + onboardingId, + 'ea-extract', + 'complete' + ); + }); + + test('shows a loading callout when elastic agent is installing', async ({ + pageObjects: { customLogsPage }, + }) => { + await customLogsPage.updateInstallationStepStatus( + onboardingId, + 'ea-install', + 'loading' + ); + await expect( + customLogsPage.stepStatusLoading.getByText( + CustomLogsPage.ASSERTION_MESSAGES.INSTALLING_AGENT_STATUS + ) + ).toBeVisible(); + }); + + test('shows a success callout when elastic agent is installed', async ({ + pageObjects: { customLogsPage }, + }) => { + await customLogsPage.updateInstallationStepStatus( + onboardingId, + 'ea-install', + 'complete' + ); + await expect( + customLogsPage.stepStatusComplete.getByText( + CustomLogsPage.ASSERTION_MESSAGES.INSTALLED_AGENT_STATUS + ) + ).toBeVisible(); + }); + + test('shows a danger callout when elastic agent was not installed', async ({ + pageObjects: { customLogsPage }, + }) => { + await customLogsPage.updateInstallationStepStatus( + onboardingId, + 'ea-install', + 'danger' + ); + await expect( + customLogsPage.stepStatusDanger.getByText( + CustomLogsPage.ASSERTION_MESSAGES.INSTALL_AGENT_DANGER_CALLOUT + ) + ).toBeVisible(); + }); + }); + + test.describe('Check elastic Agent status step', () => { + test.beforeEach(async ({ pageObjects: { customLogsPage } }) => { + await customLogsPage.updateInstallationStepStatus( + onboardingId, + 'ea-download', + 'complete' + ); + await customLogsPage.updateInstallationStepStatus( + onboardingId, + 'ea-extract', + 'complete' + ); + await customLogsPage.updateInstallationStepStatus( + onboardingId, + 'ea-install', + 'complete' + ); + }); + + test('shows a loading callout when getting elastic agent status', async ({ + pageObjects: { customLogsPage }, + }) => { + await customLogsPage.updateInstallationStepStatus( + onboardingId, + 'ea-status', + 'loading' + ); + await expect( + customLogsPage.stepStatusLoading.getByText( + CustomLogsPage.ASSERTION_MESSAGES.CONNECTING_TO_AGENT_STATUS + ) + ).toBeVisible(); + }); + + test('shows a success callout when elastic agent status is healthy', async ({ + pageObjects: { customLogsPage }, + }) => { + await customLogsPage.updateInstallationStepStatus( + onboardingId, + 'ea-status', + 'complete', + { + agentId: 'test-agent-id', + } + ); + await expect( + customLogsPage.stepStatusComplete.getByText( + CustomLogsPage.ASSERTION_MESSAGES.CONNECTED_TO_AGENT_STATUS + ) + ).toBeVisible(); + }); + + test('shows a warning callout when elastic agent status is not healthy', async ({ + pageObjects: { customLogsPage }, + }) => { + await customLogsPage.updateInstallationStepStatus( + onboardingId, + 'ea-status', + 'warning' + ); + await expect( + customLogsPage.stepStatusWarning.getByText( + CustomLogsPage.ASSERTION_MESSAGES.CONNECT_AGENT_WARNING_CALLOUT + ) + ).toBeVisible(); + }); + }); + }); + }); + }); + + test.describe('Configure Elastic Agent step', () => { + let onboardingId: string; + + test.beforeEach(setupPage({ isAdmin: true })); + + test.beforeEach(async ({ page, pageObjects: { customLogsPage } }) => { + await page.route('**/observability_onboarding/logs/flow', async (route) => { + const response = await route.fetch(); + const body = await response.json(); + + onboardingId = body.onboardingId; + + await route.fulfill({ response }); + }); + + await expect(customLogsPage.apiKeyCreateSuccessCallout).toBeVisible(); + }); + + test.describe('When user select Linux OS', () => { + test.beforeEach(async ({ pageObjects: { customLogsPage } }) => { + await customLogsPage.autoDownloadConfigurationToggle.click(); + await customLogsPage.updateInstallationStepStatus( + onboardingId, + 'ea-download', + 'complete' + ); + await customLogsPage.updateInstallationStepStatus(onboardingId, 'ea-extract', 'complete'); + await customLogsPage.updateInstallationStepStatus(onboardingId, 'ea-install', 'complete'); + await customLogsPage.updateInstallationStepStatus(onboardingId, 'ea-status', 'complete', { + agentId: 'test-agent-id', + }); + }); + + test('shows loading callout when config is being downloaded to the host', async ({ + pageObjects: { customLogsPage }, + }) => { + await customLogsPage.updateInstallationStepStatus(onboardingId, 'ea-config', 'loading'); + await expect( + customLogsPage.stepStatusLoading.getByText( + CustomLogsPage.ASSERTION_MESSAGES.DOWNLOAD_AGENT_CONFIG_STATUS + ) + ).toBeVisible(); + }); + + test('shows success callout when the configuration has been written to the host', async ({ + pageObjects: { customLogsPage }, + }) => { + await customLogsPage.updateInstallationStepStatus(onboardingId, 'ea-config', 'complete'); + await expect( + customLogsPage.stepStatusComplete.getByText( + CustomLogsPage.ASSERTION_MESSAGES.AGENT_CONFIGURATION_SUCCESS_CALLOUT_LINUX + ) + ).toBeVisible(); + }); + + test('shows warning callout when the configuration was not written in the host', async ({ + pageObjects: { customLogsPage }, + }) => { + await customLogsPage.updateInstallationStepStatus(onboardingId, 'ea-config', 'warning'); + await expect( + customLogsPage.stepStatusWarning.getByText( + CustomLogsPage.ASSERTION_MESSAGES.CONFIGURE_AGENT_WARNING_CALLOUT + ) + ).toBeVisible(); + }); + }); + + test.describe('When user select Mac OS', () => { + test.beforeEach(async ({ pageObjects: { customLogsPage } }) => { + await customLogsPage.macOSCodeSnippetButton.click(); + await customLogsPage.autoDownloadConfigurationToggle.click(); + await customLogsPage.updateInstallationStepStatus( + onboardingId, + 'ea-download', + 'complete' + ); + await customLogsPage.updateInstallationStepStatus(onboardingId, 'ea-extract', 'complete'); + await customLogsPage.updateInstallationStepStatus(onboardingId, 'ea-install', 'complete'); + await customLogsPage.updateInstallationStepStatus(onboardingId, 'ea-status', 'complete', { + agentId: 'test-agent-id', + }); + }); + + test('shows loading callout when config is being downloaded to the host', async ({ + pageObjects: { customLogsPage }, + }) => { + await customLogsPage.updateInstallationStepStatus(onboardingId, 'ea-config', 'loading'); + await expect( + customLogsPage.stepStatusLoading.getByText( + CustomLogsPage.ASSERTION_MESSAGES.DOWNLOADING_AGENT_CONFIG_STATUS + ) + ).toBeVisible(); + }); + + test('shows success callout when the configuration has been written to the host', async ({ + pageObjects: { customLogsPage }, + }) => { + await customLogsPage.updateInstallationStepStatus(onboardingId, 'ea-config', 'complete'); + await expect( + customLogsPage.stepStatusComplete.getByText( + CustomLogsPage.ASSERTION_MESSAGES.AGENT_CONFIGURATION_SUCCESS_CALLOUT_MACOS + ) + ).toBeVisible(); + }); + + test('shows warning callout when the configuration was not written in the host', async ({ + pageObjects: { customLogsPage }, + }) => { + await customLogsPage.updateInstallationStepStatus(onboardingId, 'ea-config', 'warning'); + await expect( + customLogsPage.stepStatusWarning.getByText( + CustomLogsPage.ASSERTION_MESSAGES.CONFIGURE_AGENT_WARNING_CALLOUT + ) + ).toBeVisible(); + }); + }); + + test.describe('When user select Windows', () => { + test.beforeEach(async ({ pageObjects: { customLogsPage } }) => { + customLogsPage.windowsCodeSnippetButton.click(); + }); + + test('step is disabled', async ({ pageObjects: { customLogsPage } }) => { + await expect( + customLogsPage.configureElasticAgentStep.getByText( + CustomLogsPage.ASSERTION_MESSAGES.INSTALLATION_STEP_2_DISABLED + ) + ).toBeVisible(); + }); + }); + }); + + test.describe('Check logs step', () => { + let onboardingId: string; + + test.beforeEach(setupPage({ isAdmin: true })); + + test.beforeEach(async ({ page, pageObjects: { customLogsPage } }) => { + await page.route('**/observability_onboarding/logs/flow', async (route) => { + const response = await route.fetch(); + const body = await response.json(); + + onboardingId = body.onboardingId; + + await route.fulfill({ response }); + }); + + await expect(customLogsPage.apiKeyCreateSuccessCallout).toBeVisible(); + }); + + test.describe('When user select Linux OS or MacOS', () => { + test.describe('When configure Elastic Agent step is not finished', () => { + test.beforeEach(async ({ pageObjects: { customLogsPage } }) => { + await customLogsPage.updateInstallationStepStatus( + onboardingId, + 'ea-download', + 'complete' + ); + await customLogsPage.updateInstallationStepStatus( + onboardingId, + 'ea-extract', + 'complete' + ); + await customLogsPage.updateInstallationStepStatus( + onboardingId, + 'ea-install', + 'complete' + ); + await customLogsPage.updateInstallationStepStatus(onboardingId, 'ea-status', 'loading'); + }); + + test('check logs is not triggered', async ({ page }) => { + await expect( + page.locator( + '[data-test-subj="obltOnboardingCheckLogsStep"] .euiStep__titleWrapper [class$="euiStepNumber-s-incomplete"]' + ) + ).toBeVisible(); + await expect( + page.locator('.euiStep__title', { hasText: 'Ship logs to Elastic Observability' }) + ).toBeVisible(); + }); + }); + + test.describe('When configure Elastic Agent step has finished', () => { + test.beforeEach(async ({ pageObjects: { customLogsPage } }) => { + await customLogsPage.updateInstallationStepStatus( + onboardingId, + 'ea-download', + 'complete' + ); + await customLogsPage.updateInstallationStepStatus( + onboardingId, + 'ea-extract', + 'complete' + ); + await customLogsPage.updateInstallationStepStatus( + onboardingId, + 'ea-install', + 'complete' + ); + await customLogsPage.updateInstallationStepStatus( + onboardingId, + 'ea-status', + 'complete', + { + agentId: 'test-agent-id', + } + ); + await customLogsPage.updateInstallationStepStatus( + onboardingId, + 'ea-config', + 'complete' + ); + }); + + test('shows loading callout when logs are being checked', async ({ page }) => { + await expect( + page.locator( + '[data-test-subj="obltOnboardingCheckLogsStep"] .euiStep__titleWrapper [class$="euiStepNumber-s-loading"]' + ) + ).toBeVisible(); + await expect( + page.locator('.euiStep__title', { hasText: 'Waiting for logs to be shipped...' }) + ).toBeVisible(); + }); + }); + }); + + test.describe('When user select Windows', () => { + test.beforeEach(async ({ pageObjects: { customLogsPage } }) => { + await customLogsPage.windowsCodeSnippetButton.click(); + }); + + test('step is disabled', async ({ pageObjects: { customLogsPage }, page }) => { + await expect( + page.locator( + '[data-test-subj="obltOnboardingCheckLogsStep"] .euiStep__titleWrapper [class$="euiStepNumber-s-disabled"]' + ) + ).toBeVisible(); + }); + }); + }); + + test.describe('When logs are being shipped', () => { + test.beforeEach(async ({ page }) => { + await page.route('**/progress', (route) => { + route.fulfill({ + status: 200, + body: JSON.stringify({ + progress: { + 'ea-download': { status: 'complete' }, + 'ea-extract': { status: 'complete' }, + 'ea-install': { status: 'complete' }, + 'ea-status': { status: 'complete' }, + 'ea-config': { status: 'complete' }, + 'logs-ingest': { status: 'complete' }, + }, + }), + }); + }); + }); + + test.beforeEach(setupPage({ isAdmin: true })); + + test('shows success callout when logs have arrived to elastic', async ({ page }) => { + await expect( + page.locator( + '[data-test-subj="obltOnboardingCheckLogsStep"] .euiStep__titleWrapper [class$="euiStepNumber-s-complete"]' + ) + ).toBeVisible(); + await expect( + page.locator('.euiStep__title', { hasText: 'Logs are being shipped!' }) + ).toBeVisible(); + }); + + test('when user clicks on Explore Logs it navigates to observability Discover', async ({ + page, + }) => { + await page.locator('[data-test-subj="obltOnboardingExploreLogs"]').click(); + await expect(page).toHaveURL(/\/app\/discover/); + }); + }); + } +); diff --git a/x-pack/solutions/observability/plugins/observability_onboarding/ui_tests/tsconfig.json b/x-pack/solutions/observability/plugins/observability_onboarding/ui_tests/tsconfig.json new file mode 100644 index 0000000000000..6f9ee7501fef3 --- /dev/null +++ b/x-pack/solutions/observability/plugins/observability_onboarding/ui_tests/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "target/types" + }, + "include": [ + "**/*" + ], + "kbn_references": [ + "@kbn/scout" + ], + "exclude": [ + "target/**/*" + ] +} diff --git a/yarn.lock b/yarn.lock index 2132e341a9a42..3bdf5c9e68c2e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6525,10 +6525,6 @@ version "0.0.0" uid "" -"@kbn/observability-onboarding-e2e@link:x-pack/solutions/observability/plugins/observability_onboarding/e2e": - version "0.0.0" - uid "" - "@kbn/observability-onboarding-plugin@link:x-pack/solutions/observability/plugins/observability_onboarding": version "0.0.0" uid "" From 41950c22df7ae25dc8c342b50b29a7f79b956a53 Mon Sep 17 00:00:00 2001 From: Marco Liberati Date: Fri, 10 Jan 2025 10:29:38 +0100 Subject: [PATCH 02/42] [Lens] Restore embeddable test coverage (#204986) ## Summary Fixes #198754 Restore previous removed tests when performing the refactor. The new tests take advantage of the new architecture to be more modular and close to the logic modules. The `data_loader` tests are not just covering the re-render logic but also some `expression_params` logic, who in the past have proven to be the source of some bugs: specifically the tests will check that the params are correctly passed to the params logic and then stored correctly in the observable. New mocks take advantage of the plain initializers to build some of the API, that will make it in sync with the actual implementation for future maintenance. ### Checklist Check the PR satisfies following conditions. - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Marta Bondyra <4283304+mbondyra@users.noreply.github.com> --- .../lens/public/mocks/data_plugin_mock.ts | 2 +- .../react_embeddable/data_loader.test.ts | 414 ++++++++++++++++++ .../public/react_embeddable/data_loader.ts | 14 +- .../expressions/expression_params.ts | 2 + .../initializers/initialize_edit.test.ts | 71 +++ .../initializers/initialize_edit.tsx | 7 +- .../initializers/initialize_internal_api.ts | 3 + .../initialize_state_management.ts | 2 +- .../public/react_embeddable/mocks/index.tsx | 213 +++++---- .../lens_embeddable_component.test.tsx | 2 +- .../lens/public/react_embeddable/types.ts | 2 + .../user_messages/api.test.ts | 303 +++++++++++++ .../react_embeddable/user_messages/api.ts | 9 +- 13 files changed, 913 insertions(+), 131 deletions(-) create mode 100644 x-pack/platform/plugins/shared/lens/public/react_embeddable/data_loader.test.ts create mode 100644 x-pack/platform/plugins/shared/lens/public/react_embeddable/initializers/initialize_edit.test.ts create mode 100644 x-pack/platform/plugins/shared/lens/public/react_embeddable/user_messages/api.test.ts diff --git a/x-pack/platform/plugins/shared/lens/public/mocks/data_plugin_mock.ts b/x-pack/platform/plugins/shared/lens/public/mocks/data_plugin_mock.ts index 8628cc29c1940..18bf09c9c2a92 100644 --- a/x-pack/platform/plugins/shared/lens/public/mocks/data_plugin_mock.ts +++ b/x-pack/platform/plugins/shared/lens/public/mocks/data_plugin_mock.ts @@ -141,7 +141,7 @@ export function mockDataPlugin( }, search: createMockSearchService(), nowProvider: { - get: jest.fn(), + get: jest.fn(() => new Date()), }, fieldFormats: { deserialize: jest.fn(), diff --git a/x-pack/platform/plugins/shared/lens/public/react_embeddable/data_loader.test.ts b/x-pack/platform/plugins/shared/lens/public/react_embeddable/data_loader.test.ts new file mode 100644 index 0000000000000..e2456abb9fcf3 --- /dev/null +++ b/x-pack/platform/plugins/shared/lens/public/react_embeddable/data_loader.test.ts @@ -0,0 +1,414 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { faker } from '@faker-js/faker'; +import { loadEmbeddableData } from './data_loader'; +import { + createUnifiedSearchApi, + getLensApiMock, + getLensAttributesMock, + getLensInternalApiMock, + makeEmbeddableServices, +} from './mocks'; +import { BehaviorSubject, filter, firstValueFrom } from 'rxjs'; +import { AggregateQuery, Filter, Query, TimeRange } from '@kbn/es-query'; +import { LensDocument } from '../persistence'; +import { + GetStateType, + LensApi, + LensEmbeddableStartServices, + LensInternalApi, + LensOverrides, + LensPublicCallbacks, + LensRuntimeState, +} from './types'; +import { + HasParentApi, + PublishesTimeRange, + PublishesUnifiedSearch, + PublishingSubject, + ViewMode, +} from '@kbn/presentation-publishing'; +import { PublishesSearchSession } from '@kbn/presentation-publishing/interfaces/fetch/publishes_search_session'; +import { isObject } from 'lodash'; +import { defaultDoc } from '../mocks'; + +jest.mock('@kbn/interpreter', () => ({ + toExpression: jest.fn().mockReturnValue('expression'), +})); + +// Mock it for now, later investigate why the real one is not triggering here on tests +jest.mock('@kbn/presentation-publishing', () => { + const original = jest.requireActual('@kbn/presentation-publishing'); + const rx = jest.requireActual('rxjs'); + return { + ...original, + fetch$: jest.fn((api: unknown) => { + const typeApi = api as PublishesTimeRange & + PublishesUnifiedSearch & + HasParentApi; + const emptyObservable = rx.of(undefined); + return rx.merge( + typeApi.timeRange$ ?? emptyObservable, + typeApi.filters$ ?? emptyObservable, + typeApi.query$ ?? emptyObservable, + typeApi.parentApi?.filters$ ?? emptyObservable, + typeApi.parentApi?.query$ ?? emptyObservable, + typeApi.parentApi?.searchSessionId$ ?? emptyObservable, + typeApi.timeRange$ ?? typeApi.parentApi?.timeRange$ ?? emptyObservable, + typeApi.parentApi?.timeslice$ ?? emptyObservable + ); + }), + }; +}); + +// In order to listen the reload function, we need to +// monitor the internalApi dispatchRenderStart spy +type ChangeFnType = ({ + api, + getState, + parentApi, + internalApi, + services, +}: { + api: LensApi; + internalApi: LensInternalApi; + getState: jest.MockedFunction; + parentApi: ReturnType & + LensPublicCallbacks & { + searchSessionId$: BehaviorSubject; + }; + services: LensEmbeddableStartServices; +}) => Promise; + +async function expectRerenderOnDataLoder( + changeFn: ChangeFnType, + runtimeState: LensRuntimeState = { attributes: getLensAttributesMock() }, + parentApiOverrides?: Partial< + { + filters$: BehaviorSubject; + query$: BehaviorSubject; + timeRange$: BehaviorSubject; + } & LensOverrides + > +): Promise { + const parentApi = { + ...createUnifiedSearchApi(), + searchSessionId$: new BehaviorSubject(''), + onLoad: jest.fn(), + onBeforeBadgesRender: jest.fn(), + onBrushEnd: jest.fn(), + onFilter: jest.fn(), + onTableRowClick: jest.fn(), + // Make TS happy + removePanel: jest.fn(), + replacePanel: jest.fn(), + getPanelCount: jest.fn(), + children$: new BehaviorSubject({}), + addNewPanel: jest.fn(), + ...parentApiOverrides, + }; + const api: LensApi = { + ...getLensApiMock(), + parentApi, + }; + const getState = jest.fn(() => runtimeState); + const internalApi = getLensInternalApiMock(); + const services = makeEmbeddableServices(new BehaviorSubject(''), undefined, { + visOverrides: { id: 'lnsXY' }, + dataOverrides: { id: 'form_based' }, + }); + services.documentToExpression = jest.fn().mockResolvedValue({ ast: 'expression_string' }); + const { cleanup } = loadEmbeddableData( + faker.string.uuid(), + getState, + api, + parentApi, + internalApi, + services + ); + // there's a debounce, so skip to the next tick + jest.advanceTimersByTime(100); + expect(internalApi.dispatchRenderStart).toHaveBeenCalledTimes(1); + // change something + const result = await changeFn({ + api, + getState, + parentApi, + internalApi, + services, + }); + // fallback to true if undefined is returned + const expectRerender = result ?? true; + // there's a debounce, so skip to the next tick + jest.advanceTimersByTime(200); + // unsubscribe to all observables before checking + cleanup(); + // now check if the re-render has been dispatched + expect(internalApi.dispatchRenderStart).toHaveBeenCalledTimes(expectRerender ? 2 : 1); +} + +function waitForValue( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + observable: PublishingSubject, + predicate?: (v: NonNullable) => boolean +) { + // Wait for the subject to emit the first non-null value + return firstValueFrom(observable.pipe(filter((v) => v != null && (predicate?.(v) ?? true)))); +} + +describe('Data Loader', () => { + beforeAll(() => { + jest.useFakeTimers(); + }); + afterAll(() => { + jest.useRealTimers(); + }); + + beforeEach(() => jest.clearAllMocks()); + + it('should re-render once on filter change', async () => { + await expectRerenderOnDataLoder(async ({ api }) => { + (api.filters$ as BehaviorSubject).next([ + { meta: { alias: 'test', negate: false, disabled: false } }, + ]); + }); + }); + + it('should re-render once on search session change', async () => { + await expectRerenderOnDataLoder(async ({ api }) => { + // dispatch a new searchSessionId + + ( + api.parentApi as unknown as { searchSessionId$: BehaviorSubject } + ).searchSessionId$.next('newSessionId'); + }); + }); + + it('should re-render once on attributes change', async () => { + await expectRerenderOnDataLoder(async ({ internalApi }) => { + // trigger a change by changing the title in the attributes + (internalApi.attributes$ as BehaviorSubject).next({ + ...internalApi.attributes$.getValue(), + title: faker.lorem.word(), + }); + }); + }); + + it('should re-render when dashboard view/edit mode changes if dynamic actions are set', async () => { + await expectRerenderOnDataLoder(async ({ api, getState }) => { + getState.mockReturnValue({ + attributes: getLensAttributesMock(), + enhancements: { + dynamicActions: { + events: [], + }, + }, + }); + // trigger a change by changing the title in the attributes + (api.viewMode as BehaviorSubject).next('view'); + }); + }); + + it('should not re-render when dashboard view/edit mode changes if dynamic actions are not set', async () => { + await expectRerenderOnDataLoder(async ({ api }) => { + // the default get state does not have dynamic actions + // trigger a change by changing the title in the attributes + (api.viewMode as BehaviorSubject).next('view'); + // should not re-render + return false; + }); + }); + + it('should pass context to embeddable', async () => { + const query: Query = { language: 'kquery', query: '' }; + const filters: Filter[] = [{ meta: { alias: 'test', negate: false, disabled: false } }]; + + await expectRerenderOnDataLoder( + async ({ internalApi }) => { + await waitForValue( + internalApi.expressionParams$, + (v: unknown) => isObject(v) && 'searchContext' in v + ); + + const params = internalApi.expressionParams$.getValue()!; + expect(params.searchContext).toEqual( + expect.objectContaining({ query: [query, defaultDoc.state.query], filters }) + ); + + return false; + }, + undefined, // use default attributes + createUnifiedSearchApi(query, filters) // customize parentApi + ); + }); + + it('should pass render mode to expression', async () => { + await expectRerenderOnDataLoder(async ({ internalApi }) => { + await waitForValue( + internalApi.expressionParams$, + (v: unknown) => isObject(v) && 'renderMode' in v + ); + const params = internalApi.expressionParams$.getValue(); + expect(params?.renderMode).toEqual('view'); + + return false; + }); + }); + + it('should merge external context with query and filters of the saved object', async () => { + const parentApiTimeRange: TimeRange = { from: 'now-15d', to: 'now' }; + const parentApiQuery: Query = { language: 'kquery', query: 'external query' }; + const parentApiFilters: Filter[] = [ + { meta: { alias: 'external filter', negate: false, disabled: false } }, + ]; + + const vizQuery: Query = { language: 'kquery', query: 'saved filter' }; + const vizFilters: Filter[] = [ + { meta: { alias: 'test', negate: false, disabled: false, index: 'filter-0' } }, + ]; + + let attributes = getLensAttributesMock(); + attributes = { + ...attributes, + state: { + ...attributes.state, + query: vizQuery, + filters: vizFilters, + }, + references: [ + { type: 'index-pattern', name: vizFilters[0].meta.index!, id: 'my-index-pattern-id' }, + ], + }; + + await expectRerenderOnDataLoder( + async ({ internalApi }) => { + await waitForValue( + internalApi.expressionParams$, + (v: unknown) => isObject(v) && 'searchContext' in v + ); + + const params = internalApi.expressionParams$.getValue()!; + expect(params.searchContext).toEqual( + expect.objectContaining({ + query: [parentApiQuery, vizQuery], + filters: [ + ...parentApiFilters, + ...vizFilters.map(({ meta }) => ({ meta: { ...meta, index: 'injected!' } })), + ], + }) + ); + + return false; + }, + { attributes }, + createUnifiedSearchApi(parentApiQuery, parentApiFilters, parentApiTimeRange) + ); + }); + + it('should call onload after rerender and onData$ call', async () => { + await expectRerenderOnDataLoder(async ({ parentApi, internalApi, api }) => { + expect(parentApi.onLoad).toHaveBeenLastCalledWith(true); + + await waitForValue( + internalApi.expressionParams$, + (v: unknown) => isObject(v) && 'expression' in v && typeof v.expression != null + ); + + const params = internalApi.expressionParams$.getValue(); + // trigger a onData + params?.onData$?.(1); + expect(parentApi.onLoad).toHaveBeenCalledTimes(2); + expect(parentApi.onLoad).toHaveBeenNthCalledWith(2, false, undefined, api.dataLoading); + + // turn off the re-render check, that will be performed here + return false; + }); + }); + + it('should initialize dateViews api with deduped list of index patterns', async () => { + await expectRerenderOnDataLoder( + async ({ internalApi }) => { + await waitForValue( + internalApi.dataViews, + (v: NonNullable) => Array.isArray(v) && v.length > 0 + ); + const outputIndexPatterns = internalApi.dataViews.getValue() || []; + + expect(outputIndexPatterns.length).toEqual(2); + expect(outputIndexPatterns[0].id).toEqual('123'); + expect(outputIndexPatterns[1].id).toEqual('456'); + + return false; + }, + { + attributes: getLensAttributesMock({ + references: [ + { type: 'index-pattern', id: '123', name: 'abc' }, + { type: 'index-pattern', id: '123', name: 'def' }, + { type: 'index-pattern', id: '456', name: 'ghi' }, + ], + }), + } + ); + }); + + it('should override noPadding in the display options if noPadding is set in the embeddable input', async () => { + await expectRerenderOnDataLoder(async ({ internalApi }) => { + await waitForValue( + internalApi.expressionParams$, + (v: unknown) => isObject(v) && 'expression' in v && typeof v.expression != null + ); + + const params = internalApi.expressionParams$.getValue()!; + expect(params.noPadding).toBeUndefined(); + return false; + }); + }); + + it('should reload only once when the attributes or savedObjectId and the search context change at the same time', async () => { + await expectRerenderOnDataLoder(async ({ internalApi, api }) => { + // trigger a change by changing the title in the attributes + (internalApi.attributes$ as BehaviorSubject).next({ + ...internalApi.attributes$.getValue(), + title: faker.lorem.word(), + }); + (api.savedObjectId as BehaviorSubject).next('newSavedObjectId'); + }); + }); + + it('should pass over the overrides as variables', async () => { + await expectRerenderOnDataLoder( + async ({ internalApi }) => { + await waitForValue( + internalApi.expressionParams$, + (v: unknown) => isObject(v) && 'variables' in v && typeof v.variables != null + ); + + const params = internalApi.expressionParams$.getValue()!; + expect(params.variables).toEqual( + expect.objectContaining({ + overrides: { + settings: { + onBrushEnd: 'ignore', + }, + }, + }) + ); + return false; + }, + // send a runtime state with the overrides + { + attributes: getLensAttributesMock(), + overrides: { + settings: { + onBrushEnd: 'ignore', + }, + }, + } + ); + }); +}); diff --git a/x-pack/platform/plugins/shared/lens/public/react_embeddable/data_loader.ts b/x-pack/platform/plugins/shared/lens/public/react_embeddable/data_loader.ts index fe11a7fe66a16..7fdb8255c52e9 100644 --- a/x-pack/platform/plugins/shared/lens/public/react_embeddable/data_loader.ts +++ b/x-pack/platform/plugins/shared/lens/public/react_embeddable/data_loader.ts @@ -93,14 +93,7 @@ export function loadEmbeddableData( updateWarnings, resetMessages, updateMessages, - } = buildUserMessagesHelpers( - api, - internalApi, - services, - onBeforeBadgesRender, - services.spaces, - metaInfo - ); + } = buildUserMessagesHelpers(api, internalApi, services, onBeforeBadgesRender, metaInfo); const dispatchBlockingErrorIfAny = () => { const blockingErrors = getUserMessages(blockingMessageDisplayLocations, { @@ -136,9 +129,7 @@ export function loadEmbeddableData( internalApi.updateDataLoading(true); // the component is ready to load - if (apiHasLensComponentCallbacks(parentApi)) { - parentApi.onLoad?.(true); - } + onLoad?.(true); const currentState = getState(); @@ -169,6 +160,7 @@ export function loadEmbeddableData( internalApi.updateVisualizationContext({ activeData: adapters?.tables?.tables, }); + // data has loaded internalApi.updateDataLoading(false); // The third argument here is an observable to let the diff --git a/x-pack/platform/plugins/shared/lens/public/react_embeddable/expressions/expression_params.ts b/x-pack/platform/plugins/shared/lens/public/react_embeddable/expressions/expression_params.ts index f3d4a359ca949..580151919ccd5 100644 --- a/x-pack/platform/plugins/shared/lens/public/react_embeddable/expressions/expression_params.ts +++ b/x-pack/platform/plugins/shared/lens/public/react_embeddable/expressions/expression_params.ts @@ -149,6 +149,7 @@ export async function getExpressionRendererParams( onData = noop, logError, api, + renderMode, addUserMessages, updateBlockingErrors, searchContext, @@ -199,6 +200,7 @@ export async function getExpressionRendererParams( syncTooltips, searchSessionId, onRender$: onRender, + renderMode, handleEvent, onData$: onData, // Remove ES|QL query from it diff --git a/x-pack/platform/plugins/shared/lens/public/react_embeddable/initializers/initialize_edit.test.ts b/x-pack/platform/plugins/shared/lens/public/react_embeddable/initializers/initialize_edit.test.ts new file mode 100644 index 0000000000000..2cd9a810bcbaa --- /dev/null +++ b/x-pack/platform/plugins/shared/lens/public/react_embeddable/initializers/initialize_edit.test.ts @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { faker } from '@faker-js/faker'; +import { initializeEditApi } from './initialize_edit'; +import { + getLensApiMock, + getLensInternalApiMock, + getLensRuntimeStateMock, + makeEmbeddableServices, +} from '../mocks'; +import { BehaviorSubject } from 'rxjs'; +import { ApplicationStart } from '@kbn/core/public'; +import { LensEmbeddableStartServices } from '../types'; + +function createEditApi(servicesOverrides: Partial = {}) { + const internalApi = getLensInternalApiMock(); + const runtimeState = getLensRuntimeStateMock(); + const api = getLensApiMock(); + const services = { + ...makeEmbeddableServices(new BehaviorSubject(''), undefined, { + visOverrides: { id: 'lnsXY' }, + dataOverrides: { id: 'formBased' }, + }), + ...servicesOverrides, + }; + return initializeEditApi( + faker.string.uuid(), + runtimeState, + () => runtimeState, + internalApi, + api, + api, + api, + () => false, // DSL based + services, + { getAppContext: () => ({ currentAppId: 'lens' }), viewMode: new BehaviorSubject('edit') } + ); +} + +describe('edit features', () => { + it('should be editable if visualize library privileges allow it', () => { + const editApi = createEditApi(); + expect(editApi.api.isEditingEnabled()).toBe(true); + }); + + it('should not be editable if visualize library privileges do not allow it', () => { + const editApi = createEditApi({ + capabilities: { + visualize: { + // cannot save + save: false, + saveQuery: true, + // cannot see the visualization + show: true, + createShortUrl: true, + }, + dashboard: { + // cannot edit in dashboard + showWriteControls: false, + }, + } as unknown as ApplicationStart['capabilities'], + }); + + expect(editApi.api.isEditingEnabled()).toBe(false); + }); +}); diff --git a/x-pack/platform/plugins/shared/lens/public/react_embeddable/initializers/initialize_edit.tsx b/x-pack/platform/plugins/shared/lens/public/react_embeddable/initializers/initialize_edit.tsx index 4674fb84c2635..bf2d9ded0c80c 100644 --- a/x-pack/platform/plugins/shared/lens/public/react_embeddable/initializers/initialize_edit.tsx +++ b/x-pack/platform/plugins/shared/lens/public/react_embeddable/initializers/initialize_edit.tsx @@ -248,7 +248,12 @@ export function initializeEditApi( * Check everything here: user/app permissions and the current inline editing state */ isEditingEnabled: () => { - return apiHasAppContext(parentApi) && canEdit() && panelManagementApi.isEditingEnabled(); + return Boolean( + parentApi && + apiHasAppContext(parentApi) && + canEdit() && + panelManagementApi.isEditingEnabled() + ); }, getEditHref: async () => { if (!parentApi || !apiHasAppContext(parentApi)) { diff --git a/x-pack/platform/plugins/shared/lens/public/react_embeddable/initializers/initialize_internal_api.ts b/x-pack/platform/plugins/shared/lens/public/react_embeddable/initializers/initialize_internal_api.ts index 2c89ed463e1c5..cf871d6c71a2d 100644 --- a/x-pack/platform/plugins/shared/lens/public/react_embeddable/initializers/initialize_internal_api.ts +++ b/x-pack/platform/plugins/shared/lens/public/react_embeddable/initializers/initialize_internal_api.ts @@ -53,6 +53,7 @@ export function initializeInternalApi( // the isNewPanel won't be serialized so it will be always false after the edit panel closes applying the changes const isNewlyCreated$ = new BehaviorSubject(initialState.isNewPanel || false); + const blockingError$ = new BehaviorSubject(undefined); const visualizationContext$ = new BehaviorSubject({ // doc can point to a different set of attributes for the visualization // i.e. when inline editing or applying a suggestion @@ -78,6 +79,7 @@ export function initializeInternalApi( renderCount$, isNewlyCreated$, dataViews: dataViews$, + blockingError$, messages$, validationMessages$, dispatchError: () => { @@ -103,6 +105,7 @@ export function initializeInternalApi( messages$.next([]); validationMessages$.next([]); }, + updateBlockingError: (blockingError: Error | undefined) => blockingError$.next(blockingError), setAsCreated: () => isNewlyCreated$.next(false), getDisplayOptions: () => { const latestAttributes = attributes$.getValue(); diff --git a/x-pack/platform/plugins/shared/lens/public/react_embeddable/initializers/initialize_state_management.ts b/x-pack/platform/plugins/shared/lens/public/react_embeddable/initializers/initialize_state_management.ts index 1bf7853c0b261..850bace89fae8 100644 --- a/x-pack/platform/plugins/shared/lens/public/react_embeddable/initializers/initialize_state_management.ts +++ b/x-pack/platform/plugins/shared/lens/public/react_embeddable/initializers/initialize_state_management.ts @@ -63,7 +63,7 @@ export function initializeStateManagement( // This is the way to communicate to the embeddable panel to render a blocking error with the // default panel error component - i.e. cannot find a Lens SO type of thing. // For Lens specific errors, we use a Lens specific error component. - const [blockingError$] = buildObservableVariable(undefined); + const [blockingError$] = buildObservableVariable(internalApi.blockingError$); return { api: { updateAttributes: internalApi.updateAttributes, diff --git a/x-pack/platform/plugins/shared/lens/public/react_embeddable/mocks/index.tsx b/x-pack/platform/plugins/shared/lens/public/react_embeddable/mocks/index.tsx index 96ba1b547a5d4..9f88036f1c657 100644 --- a/x-pack/platform/plugins/shared/lens/public/react_embeddable/mocks/index.tsx +++ b/x-pack/platform/plugins/shared/lens/public/react_embeddable/mocks/index.tsx @@ -30,99 +30,104 @@ import { LensRendererProps, LensRuntimeState, LensSerializedState, - VisualizationContext, } from '../types'; import { createMockDatasource, createMockVisualization, makeDefaultServices } from '../../mocks'; import { Datasource, DatasourceMap, Visualization, VisualizationMap } from '../../types'; import { initializeInternalApi } from '../initializers/initialize_internal_api'; -const LensApiMock: LensApi = { - // Static props - type: DOC_TYPE, - uuid: faker.string.uuid(), - // Shared Embeddable Observables - panelTitle: new BehaviorSubject(faker.lorem.words()), - hidePanelTitle: new BehaviorSubject(false), - filters$: new BehaviorSubject([]), - query$: new BehaviorSubject({ - query: 'test', - language: 'kuery', - }), - timeRange$: new BehaviorSubject({ from: 'now-15m', to: 'now' }), - dataLoading: new BehaviorSubject(false), - // Methods - getSavedVis: jest.fn(), - getFullAttributes: jest.fn(), - canViewUnderlyingData$: new BehaviorSubject(false), - loadViewUnderlyingData: jest.fn(), - getViewUnderlyingDataArgs: jest.fn(() => ({ - dataViewSpec: { id: 'index-pattern-id' }, - timeRange: { from: 'now-7d', to: 'now' }, - filters: [], - query: undefined, - columns: [], - })), - isTextBasedLanguage: jest.fn(() => true), - getTextBasedLanguage: jest.fn(), - getInspectorAdapters: jest.fn(() => ({})), - inspect: jest.fn(), - closeInspector: jest.fn(async () => {}), - supportedTriggers: jest.fn(() => []), - canLinkToLibrary: jest.fn(async () => false), - canUnlinkFromLibrary: jest.fn(async () => false), - unlinkFromLibrary: jest.fn(), - checkForDuplicateTitle: jest.fn(), - /** New embeddable api inherited methods */ - resetUnsavedChanges: jest.fn(), - serializeState: jest.fn(), - snapshotRuntimeState: jest.fn(), - saveToLibrary: jest.fn(async () => 'saved-id'), - getByValueRuntimeSnapshot: jest.fn(), - onEdit: jest.fn(), - isEditingEnabled: jest.fn(() => true), - getTypeDisplayName: jest.fn(() => 'Lens'), - setPanelTitle: jest.fn(), - setHidePanelTitle: jest.fn(), - phase$: new BehaviorSubject({ - id: faker.string.uuid(), - status: 'rendered', - timeToEvent: 1000, - }), - unsavedChanges: new BehaviorSubject(undefined), - dataViews: new BehaviorSubject(undefined), - libraryId$: new BehaviorSubject(undefined), - savedObjectId: new BehaviorSubject(undefined), - adapters$: new BehaviorSubject({}), - updateAttributes: jest.fn(), - updateSavedObjectId: jest.fn(), - updateOverrides: jest.fn(), - getByReferenceState: jest.fn(), - getByValueState: jest.fn(), - getTriggerCompatibleActions: jest.fn(), - blockingError: new BehaviorSubject(undefined), - panelDescription: new BehaviorSubject(undefined), - setPanelDescription: jest.fn(), - viewMode: new BehaviorSubject('view'), - disabledActionIds: new BehaviorSubject(undefined), - setDisabledActionIds: jest.fn(), - rendered$: new BehaviorSubject(false), - searchSessionId$: new BehaviorSubject(undefined), -}; +function getDefaultLensApiMock() { + const LensApiMock: LensApi = { + // Static props + type: DOC_TYPE, + uuid: faker.string.uuid(), + // Shared Embeddable Observables + panelTitle: new BehaviorSubject(faker.lorem.words()), + hidePanelTitle: new BehaviorSubject(false), + filters$: new BehaviorSubject([]), + query$: new BehaviorSubject({ + query: 'test', + language: 'kuery', + }), + timeRange$: new BehaviorSubject({ from: 'now-15m', to: 'now' }), + dataLoading: new BehaviorSubject(false), + // Methods + getSavedVis: jest.fn(), + getFullAttributes: jest.fn(), + canViewUnderlyingData$: new BehaviorSubject(false), + loadViewUnderlyingData: jest.fn(), + getViewUnderlyingDataArgs: jest.fn(() => ({ + dataViewSpec: { id: 'index-pattern-id' }, + timeRange: { from: 'now-7d', to: 'now' }, + filters: [], + query: undefined, + columns: [], + })), + isTextBasedLanguage: jest.fn(() => true), + getTextBasedLanguage: jest.fn(), + getInspectorAdapters: jest.fn(() => ({})), + inspect: jest.fn(), + closeInspector: jest.fn(async () => {}), + supportedTriggers: jest.fn(() => []), + canLinkToLibrary: jest.fn(async () => false), + canUnlinkFromLibrary: jest.fn(async () => false), + unlinkFromLibrary: jest.fn(), + checkForDuplicateTitle: jest.fn(), + /** New embeddable api inherited methods */ + resetUnsavedChanges: jest.fn(), + serializeState: jest.fn(), + snapshotRuntimeState: jest.fn(), + saveToLibrary: jest.fn(async () => 'saved-id'), + getByValueRuntimeSnapshot: jest.fn(), + onEdit: jest.fn(), + isEditingEnabled: jest.fn(() => true), + getTypeDisplayName: jest.fn(() => 'Lens'), + setPanelTitle: jest.fn(), + setHidePanelTitle: jest.fn(), + phase$: new BehaviorSubject({ + id: faker.string.uuid(), + status: 'rendered', + timeToEvent: 1000, + }), + unsavedChanges: new BehaviorSubject(undefined), + dataViews: new BehaviorSubject(undefined), + libraryId$: new BehaviorSubject(undefined), + savedObjectId: new BehaviorSubject(undefined), + adapters$: new BehaviorSubject({}), + updateAttributes: jest.fn(), + updateSavedObjectId: jest.fn(), + updateOverrides: jest.fn(), + getByReferenceState: jest.fn(), + getByValueState: jest.fn(), + getTriggerCompatibleActions: jest.fn(), + blockingError: new BehaviorSubject(undefined), + panelDescription: new BehaviorSubject(undefined), + setPanelDescription: jest.fn(), + viewMode: new BehaviorSubject('view'), + disabledActionIds: new BehaviorSubject(undefined), + setDisabledActionIds: jest.fn(), + rendered$: new BehaviorSubject(false), + searchSessionId$: new BehaviorSubject(undefined), + }; + return LensApiMock; +} -const LensSerializedStateMock: LensSerializedState = createEmptyLensState( - 'lnsXY', - faker.lorem.words(), - faker.lorem.text(), - { query: 'test', language: 'kuery' } -); +function getDefaultLensSerializedStateMock() { + const LensSerializedStateMock: LensSerializedState = createEmptyLensState( + 'lnsXY', + faker.lorem.words(), + faker.lorem.text(), + { query: 'test', language: 'kuery' } + ); + return LensSerializedStateMock; +} export function getLensAttributesMock(attributes?: Partial) { - return deepMerge(LensSerializedStateMock.attributes!, attributes ?? {}); + return deepMerge(getDefaultLensSerializedStateMock().attributes!, attributes ?? {}); } export function getLensApiMock(overrides: Partial = {}) { return { - ...LensApiMock, + ...getDefaultLensApiMock(), ...overrides, }; } @@ -130,7 +135,7 @@ export function getLensApiMock(overrides: Partial = {}) { export function getLensSerializedStateMock(overrides: Partial = {}) { return { savedObjectId: faker.string.uuid(), - ...LensSerializedStateMock, + ...getDefaultLensSerializedStateMock(), ...overrides, }; } @@ -139,15 +144,15 @@ export function getLensRuntimeStateMock( overrides: Partial = {} ): LensRuntimeState { return { - ...(LensSerializedStateMock as LensRuntimeState), + ...(getDefaultLensSerializedStateMock() as LensRuntimeState), ...overrides, }; } export function getLensComponentProps(overrides: Partial = {}) { return { - ...LensSerializedStateMock, - ...LensApiMock, + ...getDefaultLensSerializedStateMock(), + ...getDefaultLensApiMock(), ...overrides, }; } @@ -274,38 +279,26 @@ export function getValidExpressionParams( }; } -const LensInternalApiMock = initializeInternalApi( - getLensRuntimeStateMock(), - {}, - makeEmbeddableServices() -); +function getInternalApiWithFunctionWrappers() { + const newApi = initializeInternalApi(getLensRuntimeStateMock(), {}, makeEmbeddableServices()); + const fns: Array = ( + Object.keys(newApi) as Array + ).filter((key) => typeof newApi[key] === 'function'); + for (const fn of fns) { + const originalFn = newApi[fn]; + // @ts-expect-error + newApi[fn] = jest.fn(originalFn); + } + return newApi; +} export function getLensInternalApiMock(overrides: Partial = {}): LensInternalApi { return { - ...LensInternalApiMock, + ...getInternalApiWithFunctionWrappers(), ...overrides, }; } -export function getVisualizationContextHelperMock( - attributesOverrides?: Partial, - contextOverrides?: Omit, 'doc'> -) { - return { - getVisualizationContext: jest.fn(() => ({ - activeAttributes: getLensAttributesMock(attributesOverrides), - mergedSearchContext: {}, - indexPatterns: {}, - indexPatternRefs: [], - activeVisualizationState: undefined, - activeDatasourceState: undefined, - activeData: undefined, - ...contextOverrides, - })), - updateVisualizationContext: jest.fn(), - }; -} - export function createUnifiedSearchApi( query: Query | AggregateQuery = { query: '', diff --git a/x-pack/platform/plugins/shared/lens/public/react_embeddable/renderer/lens_embeddable_component.test.tsx b/x-pack/platform/plugins/shared/lens/public/react_embeddable/renderer/lens_embeddable_component.test.tsx index fd6d271e2c9bb..6d73f24e3dcda 100644 --- a/x-pack/platform/plugins/shared/lens/public/react_embeddable/renderer/lens_embeddable_component.test.tsx +++ b/x-pack/platform/plugins/shared/lens/public/react_embeddable/renderer/lens_embeddable_component.test.tsx @@ -29,7 +29,7 @@ function getDefaultProps({ // provide a valid expression to render internalApi.updateExpressionParams(getValidExpressionParams()); return { - internalApi: getLensInternalApiMock(internalApiOverrides), + internalApi, api: getLensApiMock(apiOverrides), onUnmount: jest.fn(), }; diff --git a/x-pack/platform/plugins/shared/lens/public/react_embeddable/types.ts b/x-pack/platform/plugins/shared/lens/public/react_embeddable/types.ts index 32cdb6728f041..f322f7c2b7383 100644 --- a/x-pack/platform/plugins/shared/lens/public/react_embeddable/types.ts +++ b/x-pack/platform/plugins/shared/lens/public/react_embeddable/types.ts @@ -440,6 +440,8 @@ export type LensInternalApi = Simplify< updateMessages: (newMessages: UserMessage[]) => void; validationMessages$: PublishingSubject; updateValidationMessages: (newMessages: UserMessage[]) => void; + blockingError$: PublishingSubject; + updateBlockingError: (newBlockingError: Error | undefined) => void; resetAllMessages: () => void; getDisplayOptions: () => VisualizationDisplayOptions; } diff --git a/x-pack/platform/plugins/shared/lens/public/react_embeddable/user_messages/api.test.ts b/x-pack/platform/plugins/shared/lens/public/react_embeddable/user_messages/api.test.ts new file mode 100644 index 0000000000000..03717a91228e3 --- /dev/null +++ b/x-pack/platform/plugins/shared/lens/public/react_embeddable/user_messages/api.test.ts @@ -0,0 +1,303 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { buildUserMessagesHelpers } from './api'; // Adjust the import path as necessary +import { + getLensApiMock, + getLensAttributesMock, + getLensInternalApiMock, + makeEmbeddableServices, +} from '../mocks'; +import { faker } from '@faker-js/faker'; +import type { Datasource, SharingSavedObjectProps, UserMessage, Visualization } from '../../types'; +import type { UserMessagesDisplayLocationId } from '../../types'; +import { BehaviorSubject } from 'rxjs'; +import { EDITOR_MISSING_VIS_TYPE, EDITOR_UNKNOWN_DATASOURCE_TYPE } from '../../user_messages_ids'; + +const ALL_LOCATIONS: UserMessagesDisplayLocationId[] = [ + 'toolbar', + 'embeddableBadge', + 'visualization', // blocks render + 'visualizationOnEmbeddable', // blocks render in embeddable only + 'visualizationInEditor', // blocks render in editor only + 'textBasedLanguagesQueryInput', + 'banner', + 'dimensionButton', +]; + +function createUserMessage( + locations: Array> = ['embeddableBadge'], + severity: UserMessage['severity'] = 'error' +): UserMessage { + return { + uniqueId: faker.string.uuid(), + severity: severity || 'error', + shortMessage: faker.lorem.word(), + longMessage: () => faker.lorem.sentence(), + fixableInEditor: false, + displayLocations: locations.map((location) => ({ id: location })), + }; +} + +function buildUserMessagesApi( + metaInfo?: SharingSavedObjectProps, + { + visOverrides, + dataOverrides, + }: { + visOverrides?: { id: string } & Partial; + dataOverrides?: { id: string } & Partial; + } = { + visOverrides: { id: 'lnsXY' }, + dataOverrides: { id: 'formBased' }, + } +) { + const api = getLensApiMock(); + const internalApi = getLensInternalApiMock(); + const services = makeEmbeddableServices(new BehaviorSubject(''), undefined, { + visOverrides, + dataOverrides, + }); + // fill the context with some data + internalApi.updateVisualizationContext({ + activeAttributes: getLensAttributesMock({ + state: { + datasourceStates: { formBased: { something: {} } }, + visualization: { activeId: 'lnsXY', state: {} }, + query: { query: '', language: 'kuery' }, + filters: [], + }, + }), + activeVisualizationState: {}, + activeDatasourceState: {}, + }); + const onBeforeBadgesRender = jest.fn((messages) => messages); + const userMessagesApi = buildUserMessagesHelpers( + api, + internalApi, + services, + onBeforeBadgesRender, + metaInfo + ); + return { api, internalApi, userMessagesApi, onBeforeBadgesRender }; +} + +describe('User Messages API', () => { + describe('resetMessages', () => { + it('should reset the runtime errors', () => { + const { userMessagesApi } = buildUserMessagesApi(); + // add runtime messages + const userMessageError = createUserMessage(); + const userMessageWarning = createUserMessage(['embeddableBadge'], 'warning'); + const userMessageInfo = createUserMessage(['embeddableBadge'], 'info'); + userMessagesApi.addUserMessages([userMessageError, userMessageWarning, userMessageInfo]); + expect(userMessagesApi.getUserMessages('embeddableBadge').length).toEqual(3); + userMessagesApi.resetMessages(); + expect(userMessagesApi.getUserMessages('embeddableBadge').length).toEqual(0); + }); + }); + + describe('updateValidationErrors', () => { + it('should basically work', () => { + const { userMessagesApi, internalApi } = buildUserMessagesApi(); + internalApi.updateValidationMessages = jest.fn(); + const messages = Array(3).fill(createUserMessage()); + userMessagesApi.updateValidationErrors(messages); + expect(internalApi.updateValidationMessages).toHaveBeenCalledWith(messages); + }); + }); + + describe('updateMessages', () => { + it('should avoid to update duplicate messages', () => { + const { userMessagesApi, internalApi } = buildUserMessagesApi(); + // start with these 3 messages + const messages = Array(3) + .fill(1) + .map(() => createUserMessage()); + // update the messages + userMessagesApi.updateMessages(messages); + expect(internalApi.updateMessages).toHaveBeenCalledTimes(1); + // now try again with the same messages + userMessagesApi.updateMessages(messages); + expect(internalApi.updateMessages).toHaveBeenCalledTimes(1); + // now try with one extra message + const messagesWithNewEntry = [...messages, createUserMessage()]; + userMessagesApi.updateMessages(messagesWithNewEntry); + expect(internalApi.updateMessages).toHaveBeenCalledTimes(2); + }); + + it('should update the messages if there are new messages', () => { + const { userMessagesApi, internalApi } = buildUserMessagesApi(); + // start with these 3 messages + const messages = Array(3).fill(createUserMessage()); + // update the messages + userMessagesApi.updateMessages(messages); + expect(internalApi.updateMessages).toHaveBeenCalledWith(messages); + // now try with one extra message + const messagesWithNewEntry = [...messages, createUserMessage()]; + userMessagesApi.updateMessages(messagesWithNewEntry); + expect(internalApi.updateMessages).toHaveBeenCalledWith(messagesWithNewEntry); + }); + + it('should update the messages when changing', () => { + const { userMessagesApi, internalApi } = buildUserMessagesApi(); + // start with these 3 messages + const messages = Array(3).fill(createUserMessage()); + // update the messages + userMessagesApi.updateMessages(messages); + expect(internalApi.updateMessages).toHaveBeenCalledWith(messages); + // update with new messages + const newMessages = Array(3).fill(createUserMessage()); + userMessagesApi.updateMessages(newMessages); + expect(internalApi.updateMessages).toHaveBeenCalledWith(newMessages); + }); + }); + + describe('updateBlockingErrors', () => { + it('should basically work with a regular Error', () => { + const { userMessagesApi, internalApi } = buildUserMessagesApi(); + internalApi.updateBlockingError = jest.fn(); + const error = new Error('Something went wrong'); + userMessagesApi.updateBlockingErrors(error); + expect(internalApi.updateBlockingError).toHaveBeenCalledWith(error); + }); + + it('should work with user messages too', () => { + const { userMessagesApi, internalApi } = buildUserMessagesApi(); + internalApi.updateBlockingError = jest.fn(); + const userMessage = createUserMessage(); + userMessagesApi.updateBlockingErrors([userMessage]); + expect(internalApi.updateBlockingError).toHaveBeenCalledWith( + new Error(userMessage.shortMessage) + ); + }); + + it('should pick only the first error from a list of user messages', () => { + const { userMessagesApi, internalApi } = buildUserMessagesApi(); + internalApi.updateBlockingError = jest.fn(); + const userMessage = createUserMessage(); + userMessagesApi.updateBlockingErrors([userMessage, createUserMessage(), createUserMessage()]); + expect(internalApi.updateBlockingError).toHaveBeenCalledWith( + new Error(userMessage.shortMessage) + ); + }); + + it('should clear out the error when an empty error is passed', () => { + const { userMessagesApi, internalApi } = buildUserMessagesApi(); + internalApi.updateBlockingError = jest.fn(); + userMessagesApi.updateBlockingErrors(new Error('')); + expect(internalApi.updateBlockingError).toHaveBeenCalledWith(undefined); + }); + }); + + describe('getUserMessages', () => { + it('should return empty list for no messages', () => { + const { userMessagesApi } = buildUserMessagesApi(); + for (const locationId of ALL_LOCATIONS) { + expect(userMessagesApi.getUserMessages(locationId)).toEqual([]); + } + }); + + it('should return basic validation for missing parts of the config', () => { + const { userMessagesApi, internalApi } = buildUserMessagesApi(); + // no doc scenario + internalApi.updateVisualizationContext({ + ...internalApi.getVisualizationContext(), + activeAttributes: undefined, + }); + for (const locationId of ALL_LOCATIONS) { + expect(userMessagesApi.getUserMessages(locationId).map(({ uniqueId }) => uniqueId)).toEqual( + [EDITOR_MISSING_VIS_TYPE, EDITOR_UNKNOWN_DATASOURCE_TYPE] + ); + } + }); + + it('should detect a URL conflict', () => { + const { userMessagesApi } = buildUserMessagesApi({ outcome: 'conflict' }); + + for (const locationId of ALL_LOCATIONS.filter((id) => id !== 'visualization')) { + expect(userMessagesApi.getUserMessages(locationId)).toEqual([]); + } + expect(userMessagesApi.getUserMessages('visualization')).toEqual( + expect.arrayContaining([expect.objectContaining({ uniqueId: 'url-conflict' })]) + ); + }); + + it('should filter messages based on severity criteria', () => { + const { userMessagesApi } = buildUserMessagesApi(); + const userMessageError = createUserMessage(); + const userMessageWarning = createUserMessage(['embeddableBadge'], 'warning'); + const userMessageInfo = createUserMessage(['embeddableBadge'], 'info'); + userMessagesApi.addUserMessages([userMessageError, userMessageWarning, userMessageInfo]); + expect(userMessagesApi.getUserMessages('embeddableBadge', { severity: 'error' })).toEqual( + expect.arrayContaining([userMessageError]) + ); + expect(userMessagesApi.getUserMessages('embeddableBadge', { severity: 'warning' })).toEqual( + expect.arrayContaining([userMessageWarning]) + ); + expect(userMessagesApi.getUserMessages('embeddableBadge', { severity: 'info' })).toEqual( + expect.arrayContaining([userMessageInfo]) + ); + }); + + it('should filter messages based on locationId', () => { + const { userMessagesApi } = buildUserMessagesApi(); + const userMessageEmbeddable = createUserMessage(['embeddableBadge']); + const userMessageVisualization = createUserMessage(['visualization']); + const userMessageEmbeddableVisualization = createUserMessage([ + 'visualization', + 'embeddableBadge', + ]); + userMessagesApi.addUserMessages([ + userMessageEmbeddable, + userMessageVisualization, + userMessageEmbeddableVisualization, + ]); + expect(userMessagesApi.getUserMessages('embeddableBadge').length).toEqual(2); + expect(userMessagesApi.getUserMessages('visualization').length).toEqual(2); + expect(userMessagesApi.getUserMessages('visualizationOnEmbeddable').length).toEqual(0); + }); + + it('should return deeper validation messages from both datasource and visualization', () => { + const vizGetUserMessages = jest.fn(); + const datasourceGetUserMessages = jest.fn(); + const { userMessagesApi } = buildUserMessagesApi(undefined, { + visOverrides: { id: 'lnsXY', getUserMessages: vizGetUserMessages }, + dataOverrides: { id: 'formBased', getUserMessages: datasourceGetUserMessages }, + }); + // now add a message, then check that it has been called in both the visualization and datasource + const userMessageVisualization = createUserMessage(['visualization']); + userMessagesApi.addUserMessages([userMessageVisualization]); + userMessagesApi.getUserMessages('visualization'); + expect(vizGetUserMessages).toHaveBeenCalled(); + expect(datasourceGetUserMessages).toHaveBeenCalled(); + }); + + it('should enable consumers to filter the final list of messages', () => { + const { userMessagesApi, onBeforeBadgesRender } = buildUserMessagesApi(); + // it should not be called when no messages are avaialble + userMessagesApi.getUserMessages('embeddableBadge'); + expect(onBeforeBadgesRender).not.toHaveBeenCalled(); + // now add a message, then check that it has been called + const userMessageEmbeddable = createUserMessage(['embeddableBadge']); + userMessagesApi.addUserMessages([userMessageEmbeddable]); + userMessagesApi.getUserMessages('embeddableBadge'); + expect(onBeforeBadgesRender).toHaveBeenCalled(); + }); + }); + + describe('addUserMessages', () => { + it('should basically work', () => { + const { userMessagesApi } = buildUserMessagesApi(); + expect(userMessagesApi.getUserMessages('embeddableBadge').length).toEqual(0); + // now add a message, then check that it has been called + const userMessageEmbeddable = createUserMessage(); + userMessagesApi.addUserMessages([userMessageEmbeddable]); + expect(userMessagesApi.getUserMessages('embeddableBadge').length).toEqual(1); + }); + }); +}); diff --git a/x-pack/platform/plugins/shared/lens/public/react_embeddable/user_messages/api.ts b/x-pack/platform/plugins/shared/lens/public/react_embeddable/user_messages/api.ts index 8058f79619555..6e40049a1573f 100644 --- a/x-pack/platform/plugins/shared/lens/public/react_embeddable/user_messages/api.ts +++ b/x-pack/platform/plugins/shared/lens/public/react_embeddable/user_messages/api.ts @@ -5,9 +5,7 @@ * 2.0. */ -import { SpacesApi } from '@kbn/spaces-plugin/public'; import { Adapters } from '@kbn/inspector-plugin/common'; -import { BehaviorSubject } from 'rxjs'; import { filterAndSortUserMessages, getApplicationUserMessages, @@ -99,9 +97,8 @@ function getWarningMessages( export function buildUserMessagesHelpers( api: LensApi, internalApi: LensInternalApi, - { coreStart, data, visualizationMap, datasourceMap }: LensEmbeddableStartServices, + { coreStart, data, visualizationMap, datasourceMap, spaces }: LensEmbeddableStartServices, onBeforeBadgesRender: LensPublicCallbacks['onBeforeBadgesRender'], - spaces?: SpacesApi, metaInfo?: SharingSavedObjectProps ): { getUserMessages: UserMessagesGetter; @@ -268,9 +265,9 @@ export function buildUserMessagesHelpers( addLog(`Blocking error: ${error?.message}`); } - if (error?.message !== api.blockingError.getValue()?.message) { + if (error?.message !== internalApi.blockingError$.getValue()?.message) { const finalError = error?.message === '' ? undefined : error; - (api.blockingError as BehaviorSubject).next(finalError); + internalApi.updateBlockingError(finalError); } }, updateWarnings: () => { From 6e3c967285a4bf06b0effd4126fa9a7e65341fc0 Mon Sep 17 00:00:00 2001 From: Marco Liberati Date: Fri, 10 Jan 2025 10:52:36 +0100 Subject: [PATCH 03/42] [Lens][Security Solutions] Restore cypress tests (#206025) ## Summary Fixes #198756 ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --- .../cypress/e2e/investigations/alerts/alerts_charts.cy.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/alerts_charts.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/alerts_charts.cy.ts index 53db62751ebde..509ba40e57fc3 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/alerts_charts.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/alerts_charts.cy.ts @@ -103,8 +103,7 @@ describe('KPI visualizations in Alerts Page', { tags: ['@ess', '@serverless'] }, }); }); - // For some reason this suite is failing in CI while I cannot reproduce it locally - context.skip('Histogram legend hover actions', () => { + context('Histogram legend hover actions', () => { it('should should add a filter in to KQL bar', () => { selectAlertsHistogram(); const expectedNumberOfAlerts = 1; From 20fa1a54c12721f368bbac941a5fa2d62e22440a Mon Sep 17 00:00:00 2001 From: Julia Bardi <90178898+juliaElastic@users.noreply.github.com> Date: Fri, 10 Jan 2025 11:24:53 +0100 Subject: [PATCH 04/42] [Fleet] fix bulk actions timing out sometimes (#205735) ## Summary Closes https://github.com/elastic/ingest-dev/issues/4346 Update: changed the implementation to run the first attempt of bulk action execution in the task too. ``` [2025-01-08T11:10:54.139+01:00][INFO ][plugins.fleet] Running action asynchronously, actionId: f8658178-cb1e-485d-9d2f-ad60ccc37bc9, total agents:10001 actionParams { kuery: '( fleet-agents.policy_id : ("d3448ae1-9e55-485e-b74c-83471cb06977")) and (status:online or (status:error or status:degraded) or (status:updating or status:unenrolling or status:enrolling) or status:offline)', revoke: false, force: undefined, batchSize: 10000, showInactive: true, spaceId: 'default', total: 10001, actionId: 'f8658178-cb1e-485d-9d2f-ad60ccc37bc9' } retryParams { pitId: 'gIuaBAEPLmZsZWV0LWFnZW50cy03FmRQN3pDT1gzUnFpNFkwdHJFdzJvbncAARZ5N2R5SnVSelN2bWNMang1THNfVkNRAAEAAAAAAAFpZhZiZnpybjZYSFRRNklYNlBqWHFzVU1nAAEWZFA3ekNPWDNScWk0WTB0ckV3Mm9udwAA', retryCount: 1 } [2025-01-08T11:10:54.154+01:00][INFO ][plugins.fleet] Scheduling task fleet:unenroll_action:retry:f8658178-cb1e-485d-9d2f-ad60ccc37bc9 [2025-01-08T11:10:54.154+01:00][DEBUG][plugins.fleet] [Bulk Agent Unenroll API] Unenroll agents in 647ms [2025-01-08T11:10:55.772+01:00][WARN ][http.server.Kibana] Event loop utilization for /julia/api/fleet/agents exceeded threshold of 250ms (351ms out of 633ms) and 15% (55%) [2025-01-08T11:10:57.235+01:00][INFO ][plugins.fleet] Running bulk action retry task [2025-01-08T11:10:57.235+01:00][DEBUG][plugins.fleet] Running task fleet:unenroll_action:retry:f8658178-cb1e-485d-9d2f-ad60ccc37bc9 [2025-01-08T11:10:57.236+01:00][INFO ][plugins.fleet] Completed bulk action retry task [2025-01-08T11:10:57.251+01:00][INFO ][plugins.fleet] Scheduling task fleet:unenroll_action:retry:check:f8658178-cb1e-485d-9d2f-ad60ccc37bc9 [2025-01-08T11:10:57.251+01:00][DEBUG][plugins.fleet] kuery: ( fleet-agents.policy_id : ("d3448ae1-9e55-485e-b74c-83471cb06977")) and (status:online or (status:error or status:degraded) or (status:updating or status:unenrolling or status:enrolling) or status:offline) [2025-01-08T11:10:57.781+01:00][DEBUG][plugins.fleet] Action not found [2025-01-08T11:10:58.891+01:00][DEBUG][plugins.fleet] Secrets storage requirements already met, turned on in settings [2025-01-08T11:10:59.414+01:00][WARN ][http.server.Kibana] Event loop utilization for /julia/api/fleet/agent_status exceeded threshold of 250ms (294ms out of 348ms) and 15% (85%) [2025-01-08T11:10:59.806+01:00][WARN ][http.server.Kibana] Event loop utilization for /julia/api/fleet/agents exceeded threshold of 250ms (504ms out of 743ms) and 15% (68%) [2025-01-08T11:11:01.532+01:00][INFO ][plugins.fleet] Removing task fleet:unenroll_action:retry:check:f8658178-cb1e-485d-9d2f-ad60ccc37bc9 actionParams { kuery: '( fleet-agents.policy_id : ("d3448ae1-9e55-485e-b74c-83471cb06977")) and (status:online or (status:error or status:degraded) or (status:updating or status:unenrolling or status:enrolling) or status:offline)', revoke: false, batchSize: 10000, showInactive: true, spaceId: 'default', total: 10001, actionId: 'f8658178-cb1e-485d-9d2f-ad60ccc37bc9' } retryParams { pitId: 'gIuaBAEPLmZsZWV0LWFnZW50cy03FmRQN3pDT1gzUnFpNFkwdHJFdzJvbncAARZ5N2R5SnVSelN2bWNMang1THNfVkNRAAEAAAAAAAFpZhZiZnpybjZYSFRRNklYNlBqWHFzVU1nAAEWZFA3ekNPWDNScWk0WTB0ckV3Mm9udwAA', retryCount: 1, taskId: 'fleet:unenroll_action:retry:f8658178-cb1e-485d-9d2f-ad60ccc37bc9', searchAfter: [ 1736331016589, 'online-98', 119770 ] } [2025-01-08T11:11:01.564+01:00][INFO ][plugins.fleet] Scheduling task fleet:unenroll_action:retry:check:f8658178-cb1e-485d-9d2f-ad60ccc37bc9 [2025 ``` ## Old description Bulk actions supposed to run async in a kibana task, and the API to return quickly with an action id. This was implemented [here](https://github.com/elastic/kibana/commit/38e74d7ee6f1089c7c4857b3ad320849ea782415#diff-69fa063ab8857204486203a718ff4bd0cbf9652623279d1959316de8e83233ff) and unintentionally broke when a tslint rule was introduced [here](https://github.com/elastic/kibana/pull/181456/files#diff-69fa063ab8857204486203a718ff4bd0cbf9652623279d1959316de8e83233ff), effectively letting the async code finish before the API returns. This results in the API timing out sometimes when there are many agents. Tested by creating 100k agent docs with the `create_agents` script and bulk unenroll agents. Logs before the change: ``` [2025-01-07T14:38:04.467+01:00][INFO ][plugins.fleet] Running action asynchronously, actionId: 2a57ac7a-0c1b-4a08-8709-6ccb683db995, total agents:100000 [2025-01-07T14:38:04.482+01:00][INFO ][plugins.fleet] Scheduling task fleet:unenroll_action:retry:check:2a57ac7a-0c1b-4a08-8709-6ccb683db995 [2025-01-07T14:38:04.482+01:00][DEBUG][plugins.fleet] kuery: status:* AND (fleet-agents.policy_id : ("d3448ae1-9e55-485e-b74c-83471cb06977")) ... [2025-01-07T14:39:00.264+01:00][INFO ][plugins.fleet] Removing task fleet:unenroll_action:retry:check:2a57ac7a-0c1b-4a08-8709-6ccb683db995 [2025-01-07T14:39:00.290+01:00][INFO ][plugins.fleet] Scheduling task fleet:unenroll_action:retry:check:2a57ac7a-0c1b-4a08-8709-6ccb683db995 [2025-01-07T14:39:00.293+01:00][INFO ][plugins.fleet] processed 100000 agents, took 55811ms [2025-01-07T14:39:00.293+01:00][INFO ][plugins.fleet] Removing task fleet:unenroll_action:retry:check:2a57ac7a-0c1b-4a08-8709-6ccb683db995 [2025-01-07T14:39:00.304+01:00][DEBUG][plugins.fleet] [Bulk Agent Unenroll API] Unenroll agents in 56027ms ``` Logs after the change: ``` [2025-01-07T14:42:10.178+01:00][INFO ][plugins.fleet] Running action asynchronously, actionId: c3e12928-bbfe-4731-bdc2-c47bcac727a7, total agents:100000 [2025-01-07T14:42:10.194+01:00][INFO ][plugins.fleet] Scheduling task fleet:unenroll_action:retry:check:c3e12928-bbfe-4731-bdc2-c47bcac727a7 [2025-01-07T14:42:10.195+01:00][DEBUG][plugins.fleet] kuery: status:* AND (fleet-agents.policy_id : ("d3448ae1-9e55-485e-b74c-83471cb06977")) [2025-01-07T14:42:10.195+01:00][DEBUG][plugins.fleet] [Bulk Agent Unenroll API] Unenroll agents in 196ms [2025-01-07T14:43:00.762+01:00][INFO ][plugins.fleet] processed 100000 agents, took 50567ms [2025-01-07T14:43:00.762+01:00][INFO ][plugins.fleet] Removing task fleet:unenroll_action:retry:check:c3e12928-bbfe-4731-bdc2-c47bcac727a7 ``` --- .../server/services/agents/action_runner.ts | 36 ++++++++-- .../services/agents/bulk_actions_resolver.ts | 6 +- .../fleet/server/services/agents/reassign.ts | 2 +- .../services/agents/request_diagnostics.ts | 2 +- .../fleet/server/services/agents/unenroll.ts | 2 +- .../services/agents/update_agent_tags.test.ts | 2 +- .../services/agents/update_agent_tags.ts | 2 +- .../fleet/server/services/agents/upgrade.ts | 2 +- .../apis/agents/request_diagnostics.ts | 2 +- .../apis/agents/unenroll.ts | 3 +- .../apis/agents/update_agent_tags.ts | 68 +++++++++++-------- .../apis/agents/upgrade.ts | 2 +- .../apis/space_awareness/agents.ts | 56 +++++++++------ 13 files changed, 115 insertions(+), 70 deletions(-) diff --git a/x-pack/platform/plugins/shared/fleet/server/services/agents/action_runner.ts b/x-pack/platform/plugins/shared/fleet/server/services/agents/action_runner.ts index f7eea6f3ac560..0b09658a2aeea 100644 --- a/x-pack/platform/plugins/shared/fleet/server/services/agents/action_runner.ts +++ b/x-pack/platform/plugins/shared/fleet/server/services/agents/action_runner.ts @@ -63,13 +63,8 @@ export abstract class ActionRunner { protected abstract processAgents(agents: Agent[]): Promise<{ actionId: string }>; - /** - * Common runner logic accross all agent bulk actions - * Starts action execution immeditalely, asynchronously - * On errors, starts a task with Task Manager to retry max 3 times - * If the last batch was stored in state, retry continues from there (searchAfter) - */ - public async runActionAsyncWithRetry(): Promise<{ actionId: string }> { + // first attempt to run bulk action async in a task, called from API handlers + public async runActionAsyncTask(): Promise<{ actionId: string }> { appContextService .getLogger() .info( @@ -82,6 +77,33 @@ export abstract class ActionRunner { this.bulkActionsResolver = await appContextService.getBulkActionsResolver(); } + const taskId = this.bulkActionsResolver!.getTaskId( + this.actionParams.actionId!, + this.getTaskType() + ); + await this.bulkActionsResolver!.run( + this.actionParams, + { + ...this.retryParams, + retryCount: (this.retryParams.retryCount ?? 0) + 1, + }, + this.getTaskType(), + taskId + ); + return { actionId: this.actionParams.actionId! }; + } + + /** + * Common runner logic accross all agent bulk actions + * Starts action execution immeditalely, asynchronously + * On errors, starts a task with Task Manager to retry max 3 times + * If the last batch was stored in state, retry continues from there (searchAfter) + */ + public async runActionAsyncWithRetry(): Promise<{ actionId: string }> { + if (!this.bulkActionsResolver) { + this.bulkActionsResolver = await appContextService.getBulkActionsResolver(); + } + // create task to check result with some delay, this runs in case of kibana crash too this.checkTaskId = await this.createCheckResultTask(); diff --git a/x-pack/platform/plugins/shared/fleet/server/services/agents/bulk_actions_resolver.ts b/x-pack/platform/plugins/shared/fleet/server/services/agents/bulk_actions_resolver.ts index b68bae611252a..fdb03ff4edd38 100644 --- a/x-pack/platform/plugins/shared/fleet/server/services/agents/bulk_actions_resolver.ts +++ b/x-pack/platform/plugins/shared/fleet/server/services/agents/bulk_actions_resolver.ts @@ -144,7 +144,11 @@ export function createRetryTask( appContextService .getLogger() - .debug(`Retry #${retryParams.retryCount} of task ${taskInstance.id}`); + .debug( + retryParams.retryCount === 1 + ? `Running task ${taskInstance.id}` + : `Retry #${retryParams.retryCount} of task ${taskInstance.id}` + ); if (retryParams.searchAfter) { appContextService.getLogger().info('Continuing task from batch ' + retryParams.searchAfter); diff --git a/x-pack/platform/plugins/shared/fleet/server/services/agents/reassign.ts b/x-pack/platform/plugins/shared/fleet/server/services/agents/reassign.ts index 89222f4773f90..62cce9fbdeeec 100644 --- a/x-pack/platform/plugins/shared/fleet/server/services/agents/reassign.ts +++ b/x-pack/platform/plugins/shared/fleet/server/services/agents/reassign.ts @@ -143,7 +143,7 @@ export async function reassignAgents( newAgentPolicyId, }, { pitId: await openPointInTime(esClient) } - ).runActionAsyncWithRetry(); + ).runActionAsyncTask(); } } diff --git a/x-pack/platform/plugins/shared/fleet/server/services/agents/request_diagnostics.ts b/x-pack/platform/plugins/shared/fleet/server/services/agents/request_diagnostics.ts index c4f0e1f0591ad..d78c4b191de75 100644 --- a/x-pack/platform/plugins/shared/fleet/server/services/agents/request_diagnostics.ts +++ b/x-pack/platform/plugins/shared/fleet/server/services/agents/request_diagnostics.ts @@ -88,6 +88,6 @@ export async function bulkRequestDiagnostics( spaceId: currentSpaceId, }, { pitId: await openPointInTime(esClient) } - ).runActionAsyncWithRetry(); + ).runActionAsyncTask(); } } diff --git a/x-pack/platform/plugins/shared/fleet/server/services/agents/unenroll.ts b/x-pack/platform/plugins/shared/fleet/server/services/agents/unenroll.ts index 01b26d1f40fcd..ac92cea8fdaca 100644 --- a/x-pack/platform/plugins/shared/fleet/server/services/agents/unenroll.ts +++ b/x-pack/platform/plugins/shared/fleet/server/services/agents/unenroll.ts @@ -116,7 +116,7 @@ export async function unenrollAgents( total: res.total, }, { pitId: await openPointInTime(esClient) } - ).runActionAsyncWithRetry(); + ).runActionAsyncTask(); } } diff --git a/x-pack/platform/plugins/shared/fleet/server/services/agents/update_agent_tags.test.ts b/x-pack/platform/plugins/shared/fleet/server/services/agents/update_agent_tags.test.ts index 6a2410dfa3c16..fe979571ff02e 100644 --- a/x-pack/platform/plugins/shared/fleet/server/services/agents/update_agent_tags.test.ts +++ b/x-pack/platform/plugins/shared/fleet/server/services/agents/update_agent_tags.test.ts @@ -44,7 +44,7 @@ const mockRunAsync = jest.fn().mockResolvedValue({}); jest.mock('./update_agent_tags_action_runner', () => ({ ...jest.requireActual('./update_agent_tags_action_runner'), UpdateAgentTagsActionRunner: jest.fn().mockImplementation(() => { - return { runActionAsyncWithRetry: mockRunAsync }; + return { runActionAsyncWithRetry: mockRunAsync, runActionAsyncTask: mockRunAsync }; }), })); diff --git a/x-pack/platform/plugins/shared/fleet/server/services/agents/update_agent_tags.ts b/x-pack/platform/plugins/shared/fleet/server/services/agents/update_agent_tags.ts index 2293130ed5148..a26c72f4b2b57 100644 --- a/x-pack/platform/plugins/shared/fleet/server/services/agents/update_agent_tags.ts +++ b/x-pack/platform/plugins/shared/fleet/server/services/agents/update_agent_tags.ts @@ -80,7 +80,7 @@ export async function updateAgentTags( total: res.total, }, { pitId } - ).runActionAsyncWithRetry(); + ).runActionAsyncTask(); } return await updateTagsBatch(soClient, esClient, givenAgents, outgoingErrors, { diff --git a/x-pack/platform/plugins/shared/fleet/server/services/agents/upgrade.ts b/x-pack/platform/plugins/shared/fleet/server/services/agents/upgrade.ts index c5e4fdc1134f0..89f77bd9fb46e 100644 --- a/x-pack/platform/plugins/shared/fleet/server/services/agents/upgrade.ts +++ b/x-pack/platform/plugins/shared/fleet/server/services/agents/upgrade.ts @@ -118,7 +118,7 @@ export async function sendUpgradeAgentsActions( spaceId: currentSpaceId, }, { pitId: await openPointInTime(esClient) } - ).runActionAsyncWithRetry(); + ).runActionAsyncTask(); } } diff --git a/x-pack/test/fleet_api_integration/apis/agents/request_diagnostics.ts b/x-pack/test/fleet_api_integration/apis/agents/request_diagnostics.ts index 782104e907dc9..73459c7f6ddb0 100644 --- a/x-pack/test/fleet_api_integration/apis/agents/request_diagnostics.ts +++ b/x-pack/test/fleet_api_integration/apis/agents/request_diagnostics.ts @@ -108,7 +108,7 @@ export default function (providerContext: FtrProviderContext) { await new Promise((resolve, reject) => { let attempts = 0; const intervalId = setInterval(async () => { - if (attempts > 2) { + if (attempts > 5) { clearInterval(intervalId); reject(new Error('action timed out')); } diff --git a/x-pack/test/fleet_api_integration/apis/agents/unenroll.ts b/x-pack/test/fleet_api_integration/apis/agents/unenroll.ts index 68ef8f242b505..66744fd9181cd 100644 --- a/x-pack/test/fleet_api_integration/apis/agents/unenroll.ts +++ b/x-pack/test/fleet_api_integration/apis/agents/unenroll.ts @@ -277,7 +277,7 @@ export default function (providerContext: FtrProviderContext) { await new Promise((resolve, reject) => { let attempts = 0; const intervalId = setInterval(async () => { - if (attempts > 3) { + if (attempts > 10) { clearInterval(intervalId); reject(new Error('action timed out')); } @@ -285,7 +285,6 @@ export default function (providerContext: FtrProviderContext) { const { body: { items: actionStatuses }, } = await supertest.get(`/api/fleet/agents/action_status`).set('kbn-xsrf', 'xxx'); - const action = actionStatuses?.find((a: any) => a.actionId === actionId); if (action && action.nbAgentsActioned === action.nbAgentsActionCreated) { clearInterval(intervalId); diff --git a/x-pack/test/fleet_api_integration/apis/agents/update_agent_tags.ts b/x-pack/test/fleet_api_integration/apis/agents/update_agent_tags.ts index 3a11fc4398aed..bf0843d97d011 100644 --- a/x-pack/test/fleet_api_integration/apis/agents/update_agent_tags.ts +++ b/x-pack/test/fleet_api_integration/apis/agents/update_agent_tags.ts @@ -8,6 +8,42 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../api_integration/ftr_provider_context'; +function getBaseUrl(spaceId?: string) { + return spaceId ? `/s/${spaceId}` : ''; +} + +export async function pollResult( + supertestAgent: any, + actionId: string, + nbAgentsAck: number, + verifyActionResult: Function, + spaceId?: string +) { + await new Promise((resolve, reject) => { + let attempts = 0; + const intervalId = setInterval(async () => { + if (attempts > 4) { + clearInterval(intervalId); + reject(new Error('action timed out')); + } + ++attempts; + const { + body: { items: actionStatuses }, + } = await supertestAgent + .get(`${getBaseUrl(spaceId)}/api/fleet/agents/action_status`) + .set('kbn-xsrf', 'xxx'); + const action = actionStatuses.find((a: any) => a.actionId === actionId); + if (action && action.nbAgentsAck === nbAgentsAck) { + clearInterval(intervalId); + await verifyActionResult(); + resolve({}); + } + }, 1000); + }).catch((e) => { + throw e; + }); +} + export default function (providerContext: FtrProviderContext) { const { getService } = providerContext; const esArchiver = getService('esArchiver'); @@ -66,34 +102,6 @@ export default function (providerContext: FtrProviderContext) { expect(agent2data.body.item.tags).to.eql(['existingTag']); }); - async function pollResult( - actionId: string, - nbAgentsAck: number, - verifyActionResult: Function - ) { - await new Promise((resolve, reject) => { - let attempts = 0; - const intervalId = setInterval(async () => { - if (attempts > 4) { - clearInterval(intervalId); - reject(new Error('action timed out')); - } - ++attempts; - const { - body: { items: actionStatuses }, - } = await supertest.get(`/api/fleet/agents/action_status`).set('kbn-xsrf', 'xxx'); - const action = actionStatuses.find((a: any) => a.actionId === actionId); - if (action && action.nbAgentsAck === nbAgentsAck) { - clearInterval(intervalId); - await verifyActionResult(); - resolve({}); - } - }, 1000); - }).catch((e) => { - throw e; - }); - } - it('should bulk update tags of multiple agents by kuery - add', async () => { const { body: actionBody } = await supertest .post(`/api/fleet/agents/bulk_update_agent_tags`) @@ -114,7 +122,7 @@ export default function (providerContext: FtrProviderContext) { expect(body.total).to.eql(4); }; - await pollResult(actionId, 4, verifyActionResult); + await pollResult(supertest, actionId, 4, verifyActionResult); }); it('should bulk update tags of multiple agents by kuery - remove', async () => { @@ -137,7 +145,7 @@ export default function (providerContext: FtrProviderContext) { expect(body.total).to.eql(0); }; - await pollResult(actionId, 2, verifyActionResult); + await pollResult(supertest, actionId, 2, verifyActionResult); }); it('should return 200 also if the kuery is valid', async () => { diff --git a/x-pack/test/fleet_api_integration/apis/agents/upgrade.ts b/x-pack/test/fleet_api_integration/apis/agents/upgrade.ts index 621c5843d5dc3..e316731602fea 100644 --- a/x-pack/test/fleet_api_integration/apis/agents/upgrade.ts +++ b/x-pack/test/fleet_api_integration/apis/agents/upgrade.ts @@ -1059,7 +1059,7 @@ export default function (providerContext: FtrProviderContext) { await new Promise((resolve, reject) => { let attempts = 0; const intervalId = setInterval(async () => { - if (attempts > 4) { + if (attempts > 10) { clearInterval(intervalId); reject(new Error('action timed out')); } diff --git a/x-pack/test/fleet_api_integration/apis/space_awareness/agents.ts b/x-pack/test/fleet_api_integration/apis/space_awareness/agents.ts index 2b8028a3a45c8..4b27193bc77ab 100644 --- a/x-pack/test/fleet_api_integration/apis/space_awareness/agents.ts +++ b/x-pack/test/fleet_api_integration/apis/space_awareness/agents.ts @@ -21,6 +21,7 @@ import { createFleetAgent, makeAgentsUpgradeable, } from './helpers'; +import { pollResult } from '../agents/update_agent_tags'; export default function (providerContext: FtrProviderContext) { const { getService } = providerContext; @@ -299,7 +300,7 @@ export default function (providerContext: FtrProviderContext) { await verifyNoAgentActions(TEST_SPACE_1); // Add tag - await apiClient.bulkUpdateAgentTags( + let { actionId } = await apiClient.bulkUpdateAgentTags( { agents: '*', tagsToAdd: ['space1'], @@ -307,43 +308,54 @@ export default function (providerContext: FtrProviderContext) { TEST_SPACE_1 ); + let verifyActionResult = async () => { + await verifyAgentsTags( + { + [testSpaceAgent1]: ['tag1', 'space1'], + [testSpaceAgent2]: ['tag1', 'space1'], + [testSpaceAgent3]: ['tag1', 'space1'], + }, + TEST_SPACE_1 + ); + }; + + await pollResult(supertest, actionId, 3, verifyActionResult, TEST_SPACE_1); + + await verifyNoAgentActions(); + let actionStatus = await apiClient.getActionStatus(TEST_SPACE_1); + expect(actionStatus.items.length).to.eql(1); + await verifyAgentsTags({ [defaultSpaceAgent1]: ['tag1'], [defaultSpaceAgent2]: ['tag1'], }); - await verifyAgentsTags( - { - [testSpaceAgent1]: ['tag1', 'space1'], - [testSpaceAgent2]: ['tag1', 'space1'], - [testSpaceAgent3]: ['tag1', 'space1'], - }, - TEST_SPACE_1 - ); - await verifyNoAgentActions(); - let actionStatus = await apiClient.getActionStatus(TEST_SPACE_1); - expect(actionStatus.items.length).to.eql(1); // Remove tag - await apiClient.bulkUpdateAgentTags( + ({ actionId } = await apiClient.bulkUpdateAgentTags( { agents: '', tagsToRemove: ['space1'], }, TEST_SPACE_1 - ); + )); + + verifyActionResult = async () => { + await verifyAgentsTags( + { + [testSpaceAgent1]: ['tag1'], + [testSpaceAgent2]: ['tag1'], + [testSpaceAgent3]: ['tag1'], + }, + TEST_SPACE_1 + ); + }; + + await pollResult(supertest, actionId, 3, verifyActionResult, TEST_SPACE_1); await verifyAgentsTags({ [defaultSpaceAgent1]: ['tag1'], [defaultSpaceAgent2]: ['tag1'], }); - await verifyAgentsTags( - { - [testSpaceAgent1]: ['tag1'], - [testSpaceAgent2]: ['tag1'], - [testSpaceAgent3]: ['tag1'], - }, - TEST_SPACE_1 - ); await verifyNoAgentActions(); actionStatus = await apiClient.getActionStatus(TEST_SPACE_1); expect(actionStatus.items.length).to.eql(2); From 39774bfc480ed2059fa3a11eeb47ffae87193bf4 Mon Sep 17 00:00:00 2001 From: Tomasz Ciecierski Date: Fri, 10 Jan 2025 11:26:41 +0100 Subject: [PATCH 05/42] [EDR Workflows] Add Runscript openApi schema (#206044) --- oas_docs/output/kibana.serverless.yaml | 83 +++++++++++++++++ oas_docs/output/kibana.yaml | 82 +++++++++++++++++ .../run_script/run_script.gen.ts | 82 +++++++++++++++++ .../run_script/run_script.schema.yaml | 89 +++++++++++++++++++ .../run_script/run_script.yaml | 74 --------------- .../common/api/quickstart_client.gen.ts | 23 +++++ ...agement_api_2023_10_31.bundled.schema.yaml | 84 +++++++++++++++++ ...agement_api_2023_10_31.bundled.schema.yaml | 84 +++++++++++++++++ .../services/security_solution_api.gen.ts | 15 ++++ 9 files changed, 542 insertions(+), 74 deletions(-) create mode 100644 x-pack/solutions/security/plugins/security_solution/common/api/endpoint/actions/response_actions/run_script/run_script.gen.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/common/api/endpoint/actions/response_actions/run_script/run_script.schema.yaml delete mode 100644 x-pack/solutions/security/plugins/security_solution/common/api/endpoint/actions/response_actions/run_script/run_script.yaml diff --git a/oas_docs/output/kibana.serverless.yaml b/oas_docs/output/kibana.serverless.yaml index 719e4f3b735a1..40ed1874797c1 100644 --- a/oas_docs/output/kibana.serverless.yaml +++ b/oas_docs/output/kibana.serverless.yaml @@ -9367,6 +9367,27 @@ paths: tags: - Security Endpoint Management API x-beta: true + /api/endpoint/action/runscript: + post: + description: Run a shell command on an endpoint. + operationId: RunScriptAction + requestBody: + content: + application/json; Elastic-Api-Version=2023-10-31: + schema: + $ref: '#/components/schemas/Security_Endpoint_Management_API_RunScriptRouteRequestBody' + required: true + responses: + '200': + content: + application/json; Elastic-Api-Version=2023-10-31: + schema: + $ref: '#/components/schemas/Security_Endpoint_Management_API_SuccessResponse' + description: OK + summary: Run a script + tags: + - Security Endpoint Management API + x-beta: true /api/endpoint/action/scan: post: description: Scan a specific file or directory on an endpoint for malware. @@ -46578,6 +46599,23 @@ components: type: string minItems: 1 type: array + Security_Endpoint_Management_API_CloudFileScriptParameters: + type: object + properties: + cloudFile: + description: Script name in cloud storage. + minLength: 1 + type: string + commandLine: + description: Command line arguments. + minLength: 1 + type: string + timeout: + description: Timeout in seconds. + minimum: 1 + type: integer + required: + - cloudFile Security_Endpoint_Management_API_Command: description: The command to be executed (cannot be an empty string) enum: @@ -46704,6 +46742,23 @@ components: - parameters Security_Endpoint_Management_API_GetProcessesRouteRequestBody: $ref: '#/components/schemas/Security_Endpoint_Management_API_NoParametersRequestSchema' + Security_Endpoint_Management_API_HostPathScriptParameters: + type: object + properties: + commandLine: + description: Command line arguments. + minLength: 1 + type: string + hostPath: + description: Absolute or relative path of script on host machine. + minLength: 1 + type: string + timeout: + description: Timeout in seconds. + minimum: 1 + type: integer + required: + - hostPath Security_Endpoint_Management_API_IsolateRouteRequestBody: $ref: '#/components/schemas/Security_Endpoint_Management_API_NoParametersRequestSchema' Security_Endpoint_Management_API_KillProcessRouteRequestBody: @@ -46857,6 +46912,34 @@ components: properties: note: type: string + Security_Endpoint_Management_API_RawScriptParameters: + type: object + properties: + commandLine: + description: Command line arguments. + minLength: 1 + type: string + raw: + description: Raw script content. + minLength: 1 + type: string + timeout: + description: Timeout in seconds. + minimum: 1 + type: integer + required: + - raw + Security_Endpoint_Management_API_RunScriptRouteRequestBody: + type: object + properties: + parameters: + description: Exactly one of 'Raw', 'HostPath', or 'CloudFile' must be provided. CommandLine and Timeout are optional for all. + oneOf: + - $ref: '#/components/schemas/Security_Endpoint_Management_API_RawScriptParameters' + - $ref: '#/components/schemas/Security_Endpoint_Management_API_HostPathScriptParameters' + - $ref: '#/components/schemas/Security_Endpoint_Management_API_CloudFileScriptParameters' + required: + - parameters Security_Endpoint_Management_API_ScanRouteRequestBody: allOf: - type: object diff --git a/oas_docs/output/kibana.yaml b/oas_docs/output/kibana.yaml index d06897f5db6e2..c398c20361e64 100644 --- a/oas_docs/output/kibana.yaml +++ b/oas_docs/output/kibana.yaml @@ -11549,6 +11549,26 @@ paths: summary: Get running processes tags: - Security Endpoint Management API + /api/endpoint/action/runscript: + post: + description: Run a shell command on an endpoint. + operationId: RunScriptAction + requestBody: + content: + application/json; Elastic-Api-Version=2023-10-31: + schema: + $ref: '#/components/schemas/Security_Endpoint_Management_API_RunScriptRouteRequestBody' + required: true + responses: + '200': + content: + application/json; Elastic-Api-Version=2023-10-31: + schema: + $ref: '#/components/schemas/Security_Endpoint_Management_API_SuccessResponse' + description: OK + summary: Run a script + tags: + - Security Endpoint Management API /api/endpoint/action/scan: post: description: Scan a specific file or directory on an endpoint for malware. @@ -53454,6 +53474,23 @@ components: type: string minItems: 1 type: array + Security_Endpoint_Management_API_CloudFileScriptParameters: + type: object + properties: + cloudFile: + description: Script name in cloud storage. + minLength: 1 + type: string + commandLine: + description: Command line arguments. + minLength: 1 + type: string + timeout: + description: Timeout in seconds. + minimum: 1 + type: integer + required: + - cloudFile Security_Endpoint_Management_API_Command: description: The command to be executed (cannot be an empty string) enum: @@ -53580,6 +53617,23 @@ components: - parameters Security_Endpoint_Management_API_GetProcessesRouteRequestBody: $ref: '#/components/schemas/Security_Endpoint_Management_API_NoParametersRequestSchema' + Security_Endpoint_Management_API_HostPathScriptParameters: + type: object + properties: + commandLine: + description: Command line arguments. + minLength: 1 + type: string + hostPath: + description: Absolute or relative path of script on host machine. + minLength: 1 + type: string + timeout: + description: Timeout in seconds. + minimum: 1 + type: integer + required: + - hostPath Security_Endpoint_Management_API_IsolateRouteRequestBody: $ref: '#/components/schemas/Security_Endpoint_Management_API_NoParametersRequestSchema' Security_Endpoint_Management_API_KillProcessRouteRequestBody: @@ -53733,6 +53787,34 @@ components: properties: note: type: string + Security_Endpoint_Management_API_RawScriptParameters: + type: object + properties: + commandLine: + description: Command line arguments. + minLength: 1 + type: string + raw: + description: Raw script content. + minLength: 1 + type: string + timeout: + description: Timeout in seconds. + minimum: 1 + type: integer + required: + - raw + Security_Endpoint_Management_API_RunScriptRouteRequestBody: + type: object + properties: + parameters: + description: Exactly one of 'Raw', 'HostPath', or 'CloudFile' must be provided. CommandLine and Timeout are optional for all. + oneOf: + - $ref: '#/components/schemas/Security_Endpoint_Management_API_RawScriptParameters' + - $ref: '#/components/schemas/Security_Endpoint_Management_API_HostPathScriptParameters' + - $ref: '#/components/schemas/Security_Endpoint_Management_API_CloudFileScriptParameters' + required: + - parameters Security_Endpoint_Management_API_ScanRouteRequestBody: allOf: - type: object diff --git a/x-pack/solutions/security/plugins/security_solution/common/api/endpoint/actions/response_actions/run_script/run_script.gen.ts b/x-pack/solutions/security/plugins/security_solution/common/api/endpoint/actions/response_actions/run_script/run_script.gen.ts new file mode 100644 index 0000000000000..eab8809e31644 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/common/api/endpoint/actions/response_actions/run_script/run_script.gen.ts @@ -0,0 +1,82 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +/* + * NOTICE: Do not edit this file manually. + * This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator. + * + * info: + * title: RunScript Action Schema + * version: 2023-10-31 + */ + +import { z } from '@kbn/zod'; + +import { SuccessResponse } from '../../../model/schema/common.gen'; + +export type RawScriptParameters = z.infer; +export const RawScriptParameters = z.object({ + /** + * Raw script content. + */ + raw: z.string().min(1), + /** + * Command line arguments. + */ + commandLine: z.string().min(1).optional(), + /** + * Timeout in seconds. + */ + timeout: z.number().int().min(1).optional(), +}); + +export type HostPathScriptParameters = z.infer; +export const HostPathScriptParameters = z.object({ + /** + * Absolute or relative path of script on host machine. + */ + hostPath: z.string().min(1), + /** + * Command line arguments. + */ + commandLine: z.string().min(1).optional(), + /** + * Timeout in seconds. + */ + timeout: z.number().int().min(1).optional(), +}); + +export type CloudFileScriptParameters = z.infer; +export const CloudFileScriptParameters = z.object({ + /** + * Script name in cloud storage. + */ + cloudFile: z.string().min(1), + /** + * Command line arguments. + */ + commandLine: z.string().min(1).optional(), + /** + * Timeout in seconds. + */ + timeout: z.number().int().min(1).optional(), +}); + +export type RunScriptRouteRequestBody = z.infer; +export const RunScriptRouteRequestBody = z.object({ + /** + * Exactly one of 'Raw', 'HostPath', or 'CloudFile' must be provided. CommandLine and Timeout are optional for all. + */ + parameters: z.union([RawScriptParameters, HostPathScriptParameters, CloudFileScriptParameters]), +}); + +export type RunScriptActionRequestBody = z.infer; +export const RunScriptActionRequestBody = RunScriptRouteRequestBody; +export type RunScriptActionRequestBodyInput = z.input; + +export type RunScriptActionResponse = z.infer; +export const RunScriptActionResponse = SuccessResponse; diff --git a/x-pack/solutions/security/plugins/security_solution/common/api/endpoint/actions/response_actions/run_script/run_script.schema.yaml b/x-pack/solutions/security/plugins/security_solution/common/api/endpoint/actions/response_actions/run_script/run_script.schema.yaml new file mode 100644 index 0000000000000..c912f2db72e57 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/common/api/endpoint/actions/response_actions/run_script/run_script.schema.yaml @@ -0,0 +1,89 @@ +openapi: 3.0.0 +info: + title: RunScript Action Schema + version: '2023-10-31' +paths: + /api/endpoint/action/runscript: + post: + summary: Run a script + operationId: RunScriptAction + description: Run a shell command on an endpoint. + x-codegen-enabled: true + x-labels: [ ess, serverless ] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/RunScriptRouteRequestBody' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '../../../model/schema/common.schema.yaml#/components/schemas/SuccessResponse' +components: + schemas: + RunScriptRouteRequestBody: + type: object + required: + - parameters + properties: + parameters: + oneOf: + - $ref: '#/components/schemas/RawScriptParameters' + - $ref: '#/components/schemas/HostPathScriptParameters' + - $ref: '#/components/schemas/CloudFileScriptParameters' + description: Exactly one of 'Raw', 'HostPath', or 'CloudFile' must be provided. CommandLine and Timeout are optional for all. + RawScriptParameters: + type: object + required: + - raw + properties: + raw: + type: string + minLength: 1 + description: Raw script content. + commandLine: + type: string + minLength: 1 + description: Command line arguments. + timeout: + type: integer + minimum: 1 + description: Timeout in seconds. + HostPathScriptParameters: + type: object + required: + - hostPath + properties: + hostPath: + type: string + minLength: 1 + description: Absolute or relative path of script on host machine. + commandLine: + type: string + minLength: 1 + description: Command line arguments. + timeout: + type: integer + minimum: 1 + description: Timeout in seconds. + CloudFileScriptParameters: + type: object + required: + - cloudFile + properties: + cloudFile: + type: string + minLength: 1 + description: Script name in cloud storage. + commandLine: + type: string + minLength: 1 + description: Command line arguments. + timeout: + type: integer + minimum: 1 + description: Timeout in seconds. diff --git a/x-pack/solutions/security/plugins/security_solution/common/api/endpoint/actions/response_actions/run_script/run_script.yaml b/x-pack/solutions/security/plugins/security_solution/common/api/endpoint/actions/response_actions/run_script/run_script.yaml deleted file mode 100644 index 228070e1d0277..0000000000000 --- a/x-pack/solutions/security/plugins/security_solution/common/api/endpoint/actions/response_actions/run_script/run_script.yaml +++ /dev/null @@ -1,74 +0,0 @@ -openapi: 3.0.0 -info: - title: RunScript Action Schema - version: '2023-10-31' -paths: - /api/endpoint/action/runscript: - post: - summary: Run a script - operationId: RunScriptAction - description: Run a shell command on an endpoint. - x-codegen-enabled: true - x-labels: [ ess, serverless ] - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/RunScriptRouteRequestBody' - responses: - '200': - description: OK - content: - application/json: - schema: - $ref: '../../../model/schema/common.schema.yaml#/components/schemas/SuccessResponse' - -components: - schemas: - RunScriptRouteRequestBody: - allOf: - - $ref: '../../../model/schema/common.schema.yaml#/components/schemas/BaseActionSchema' - - type: object - required: - - parameters - properties: - parameters: - oneOf: - - type: object - properties: - Raw: - type: string - minLength: 1 - description: Raw script content. - required: - - Raw - - type: object - properties: - HostPath: - type: string - minLength: 1 - description: Absolute or relative path of script on host machine. - required: - - HostPath - - type: object - properties: - CloudFile: - type: string - minLength: 1 - description: Script name in cloud storage. - required: - - CloudFile - - type: object - properties: - CommandLine: - type: string - minLength: 1 - description: Command line arguments. - required: - - CommandLine - properties: - Timeout: - type: integer - minimum: 1 - description: Timeout in seconds. diff --git a/x-pack/solutions/security/plugins/security_solution/common/api/quickstart_client.gen.ts b/x-pack/solutions/security/plugins/security_solution/common/api/quickstart_client.gen.ts index 86599d48267a7..52147f549edc5 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/api/quickstart_client.gen.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/api/quickstart_client.gen.ts @@ -160,6 +160,10 @@ import type { EndpointKillProcessActionRequestBodyInput, EndpointKillProcessActionResponse, } from './endpoint/actions/response_actions/kill_process/kill_process.gen'; +import type { + RunScriptActionRequestBodyInput, + RunScriptActionResponse, +} from './endpoint/actions/response_actions/run_script/run_script.gen'; import type { EndpointGetProcessesActionRequestBodyInput, EndpointGetProcessesActionResponse, @@ -2071,6 +2075,22 @@ detection engine rules. }) .catch(catchAxiosErrorFormatAndThrow); } + /** + * Run a shell command on an endpoint. + */ + async runScriptAction(props: RunScriptActionProps) { + this.log.info(`${new Date().toISOString()} Calling API RunScriptAction`); + return this.kbnClient + .request({ + path: '/api/endpoint/action/runscript', + headers: { + [ELASTIC_HTTP_VERSION_HEADER]: '2023-10-31', + }, + method: 'POST', + body: props.body, + }) + .catch(catchAxiosErrorFormatAndThrow); + } /** * Schedule the risk scoring engine to run as soon as possible. You can use this to recalculate entity risk scores after updating their asset criticality. */ @@ -2588,6 +2608,9 @@ export interface RulePreviewProps { query: RulePreviewRequestQueryInput; body: RulePreviewRequestBodyInput; } +export interface RunScriptActionProps { + body: RunScriptActionRequestBodyInput; +} export interface SearchAlertsProps { body: SearchAlertsRequestBodyInput; } diff --git a/x-pack/solutions/security/plugins/security_solution/docs/openapi/ess/security_solution_endpoint_management_api_2023_10_31.bundled.schema.yaml b/x-pack/solutions/security/plugins/security_solution/docs/openapi/ess/security_solution_endpoint_management_api_2023_10_31.bundled.schema.yaml index 46b9d014c6267..3c6094cac8589 100644 --- a/x-pack/solutions/security/plugins/security_solution/docs/openapi/ess/security_solution_endpoint_management_api_2023_10_31.bundled.schema.yaml +++ b/x-pack/solutions/security/plugins/security_solution/docs/openapi/ess/security_solution_endpoint_management_api_2023_10_31.bundled.schema.yaml @@ -226,6 +226,26 @@ paths: summary: Get running processes tags: - Security Endpoint Management API + /api/endpoint/action/runscript: + post: + description: Run a shell command on an endpoint. + operationId: RunScriptAction + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/RunScriptRouteRequestBody' + required: true + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessResponse' + description: OK + summary: Run a script + tags: + - Security Endpoint Management API /api/endpoint/action/scan: post: description: Scan a specific file or directory on an endpoint for malware. @@ -500,6 +520,23 @@ components: type: string minItems: 1 type: array + CloudFileScriptParameters: + type: object + properties: + cloudFile: + description: Script name in cloud storage. + minLength: 1 + type: string + commandLine: + description: Command line arguments. + minLength: 1 + type: string + timeout: + description: Timeout in seconds. + minimum: 1 + type: integer + required: + - cloudFile Command: description: The command to be executed (cannot be an empty string) enum: @@ -626,6 +663,23 @@ components: - parameters GetProcessesRouteRequestBody: $ref: '#/components/schemas/NoParametersRequestSchema' + HostPathScriptParameters: + type: object + properties: + commandLine: + description: Command line arguments. + minLength: 1 + type: string + hostPath: + description: Absolute or relative path of script on host machine. + minLength: 1 + type: string + timeout: + description: Timeout in seconds. + minimum: 1 + type: integer + required: + - hostPath IsolateRouteRequestBody: $ref: '#/components/schemas/NoParametersRequestSchema' KillProcessRouteRequestBody: @@ -779,6 +833,36 @@ components: properties: note: type: string + RawScriptParameters: + type: object + properties: + commandLine: + description: Command line arguments. + minLength: 1 + type: string + raw: + description: Raw script content. + minLength: 1 + type: string + timeout: + description: Timeout in seconds. + minimum: 1 + type: integer + required: + - raw + RunScriptRouteRequestBody: + type: object + properties: + parameters: + description: >- + Exactly one of 'Raw', 'HostPath', or 'CloudFile' must be provided. + CommandLine and Timeout are optional for all. + oneOf: + - $ref: '#/components/schemas/RawScriptParameters' + - $ref: '#/components/schemas/HostPathScriptParameters' + - $ref: '#/components/schemas/CloudFileScriptParameters' + required: + - parameters ScanRouteRequestBody: allOf: - type: object diff --git a/x-pack/solutions/security/plugins/security_solution/docs/openapi/serverless/security_solution_endpoint_management_api_2023_10_31.bundled.schema.yaml b/x-pack/solutions/security/plugins/security_solution/docs/openapi/serverless/security_solution_endpoint_management_api_2023_10_31.bundled.schema.yaml index c0a391c686e08..6604fce5a4697 100644 --- a/x-pack/solutions/security/plugins/security_solution/docs/openapi/serverless/security_solution_endpoint_management_api_2023_10_31.bundled.schema.yaml +++ b/x-pack/solutions/security/plugins/security_solution/docs/openapi/serverless/security_solution_endpoint_management_api_2023_10_31.bundled.schema.yaml @@ -226,6 +226,26 @@ paths: summary: Get running processes tags: - Security Endpoint Management API + /api/endpoint/action/runscript: + post: + description: Run a shell command on an endpoint. + operationId: RunScriptAction + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/RunScriptRouteRequestBody' + required: true + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessResponse' + description: OK + summary: Run a script + tags: + - Security Endpoint Management API /api/endpoint/action/scan: post: description: Scan a specific file or directory on an endpoint for malware. @@ -500,6 +520,23 @@ components: type: string minItems: 1 type: array + CloudFileScriptParameters: + type: object + properties: + cloudFile: + description: Script name in cloud storage. + minLength: 1 + type: string + commandLine: + description: Command line arguments. + minLength: 1 + type: string + timeout: + description: Timeout in seconds. + minimum: 1 + type: integer + required: + - cloudFile Command: description: The command to be executed (cannot be an empty string) enum: @@ -626,6 +663,23 @@ components: - parameters GetProcessesRouteRequestBody: $ref: '#/components/schemas/NoParametersRequestSchema' + HostPathScriptParameters: + type: object + properties: + commandLine: + description: Command line arguments. + minLength: 1 + type: string + hostPath: + description: Absolute or relative path of script on host machine. + minLength: 1 + type: string + timeout: + description: Timeout in seconds. + minimum: 1 + type: integer + required: + - hostPath IsolateRouteRequestBody: $ref: '#/components/schemas/NoParametersRequestSchema' KillProcessRouteRequestBody: @@ -779,6 +833,36 @@ components: properties: note: type: string + RawScriptParameters: + type: object + properties: + commandLine: + description: Command line arguments. + minLength: 1 + type: string + raw: + description: Raw script content. + minLength: 1 + type: string + timeout: + description: Timeout in seconds. + minimum: 1 + type: integer + required: + - raw + RunScriptRouteRequestBody: + type: object + properties: + parameters: + description: >- + Exactly one of 'Raw', 'HostPath', or 'CloudFile' must be provided. + CommandLine and Timeout are optional for all. + oneOf: + - $ref: '#/components/schemas/RawScriptParameters' + - $ref: '#/components/schemas/HostPathScriptParameters' + - $ref: '#/components/schemas/CloudFileScriptParameters' + required: + - parameters ScanRouteRequestBody: allOf: - type: object diff --git a/x-pack/test/api_integration/services/security_solution_api.gen.ts b/x-pack/test/api_integration/services/security_solution_api.gen.ts index 5021895c1cb68..273bc342cbc01 100644 --- a/x-pack/test/api_integration/services/security_solution_api.gen.ts +++ b/x-pack/test/api_integration/services/security_solution_api.gen.ts @@ -146,6 +146,7 @@ import { RulePreviewRequestQueryInput, RulePreviewRequestBodyInput, } from '@kbn/security-solution-plugin/common/api/detection_engine/rule_preview/rule_preview.gen'; +import { RunScriptActionRequestBodyInput } from '@kbn/security-solution-plugin/common/api/endpoint/actions/response_actions/run_script/run_script.gen'; import { SearchAlertsRequestBodyInput } from '@kbn/security-solution-plugin/common/api/detection_engine/signals/query_signals/query_signals_route.gen'; import { SetAlertAssigneesRequestBodyInput } from '@kbn/security-solution-plugin/common/api/detection_engine/alert_assignees/set_alert_assignees_route.gen'; import { SetAlertsStatusRequestBodyInput } from '@kbn/security-solution-plugin/common/api/detection_engine/signals/set_signal_status/set_signals_status_route.gen'; @@ -1448,6 +1449,17 @@ detection engine rules. .send(props.body as object) .query(props.query); }, + /** + * Run a shell command on an endpoint. + */ + runScriptAction(props: RunScriptActionProps, kibanaSpace: string = 'default') { + return supertest + .post(routeWithNamespace('/api/endpoint/action/runscript', kibanaSpace)) + .set('kbn-xsrf', 'true') + .set(ELASTIC_HTTP_VERSION_HEADER, '2023-10-31') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') + .send(props.body as object); + }, /** * Schedule the risk scoring engine to run as soon as possible. You can use this to recalculate entity risk scores after updating their asset criticality. */ @@ -1913,6 +1925,9 @@ export interface RulePreviewProps { query: RulePreviewRequestQueryInput; body: RulePreviewRequestBodyInput; } +export interface RunScriptActionProps { + body: RunScriptActionRequestBodyInput; +} export interface SearchAlertsProps { body: SearchAlertsRequestBodyInput; } From 7386e268247048d2717220691d3d48f29a0df17a Mon Sep 17 00:00:00 2001 From: Janki Salvi <117571355+js-jankisalvi@users.noreply.github.com> Date: Fri, 10 Jan 2025 10:31:13 +0000 Subject: [PATCH 06/42] [ResponseOps][Cases] Make case templates GA (#205940) ## Summary This PR makes case templates GA.
Case settings image
Case create image
Case action image
### Checklist Check the PR satisfies following conditions. - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios ### Release Notes Case templates are now GA ### For maintainers - [x] This was checked for breaking API changes and was [labeled appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) --- .../public/components/create/templates.tsx | 18 +----------------- .../public/components/templates/index.test.tsx | 6 ------ .../public/components/templates/index.tsx | 4 ---- 3 files changed, 1 insertion(+), 27 deletions(-) diff --git a/x-pack/platform/plugins/shared/cases/public/components/create/templates.tsx b/x-pack/platform/plugins/shared/cases/public/components/create/templates.tsx index 93dd30cfb40a9..9e707aaf43d74 100644 --- a/x-pack/platform/plugins/shared/cases/public/components/create/templates.tsx +++ b/x-pack/platform/plugins/shared/cases/public/components/create/templates.tsx @@ -7,15 +7,8 @@ import React, { useCallback, useState } from 'react'; import type { EuiSelectOption } from '@elastic/eui'; -import { - EuiFlexItem, - EuiFormRow, - EuiSelect, - EuiFlexGroup, - useIsWithinMaxBreakpoint, -} from '@elastic/eui'; +import { EuiFlexItem, EuiFormRow, EuiSelect, EuiFlexGroup } from '@elastic/eui'; import { css } from '@emotion/react'; -import { ExperimentalBadge } from '../experimental_badge/experimental_badge'; import type { CasesConfigurationUI, CasesConfigurationUITemplate } from '../../containers/types'; import { OptionalFieldLabel } from '../optional_field_label'; import { TEMPLATE_HELP_TEXT, TEMPLATE_LABEL } from './translations'; @@ -39,7 +32,6 @@ export const TemplateSelectorComponent: React.FC = ({ const [selectedTemplate, onSelectTemplate] = useState( initialTemplate?.key ?? undefined ); - const isSmallScreen = useIsWithinMaxBreakpoint('s'); const options: EuiSelectOption[] = templates.map((template) => ({ text: template.name, @@ -72,14 +64,6 @@ export const TemplateSelectorComponent: React.FC = ({ `} responsive={false} > - - - {OptionalFieldLabel} } diff --git a/x-pack/platform/plugins/shared/cases/public/components/templates/index.test.tsx b/x-pack/platform/plugins/shared/cases/public/components/templates/index.test.tsx index 4cfa7a22750b4..516263923b69e 100644 --- a/x-pack/platform/plugins/shared/cases/public/components/templates/index.test.tsx +++ b/x-pack/platform/plugins/shared/cases/public/components/templates/index.test.tsx @@ -111,12 +111,6 @@ describe.skip('Templates', () => { }); }); - it('shows the experimental badge', async () => { - appMockRender.render(); - - expect(await screen.findByTestId('case-experimental-badge')).toBeInTheDocument(); - }); - it('shows error when templates reaches the limit', async () => { const mockTemplates = []; diff --git a/x-pack/platform/plugins/shared/cases/public/components/templates/index.tsx b/x-pack/platform/plugins/shared/cases/public/components/templates/index.tsx index 6c15f1e9ef464..f8c2fcb659a7c 100644 --- a/x-pack/platform/plugins/shared/cases/public/components/templates/index.tsx +++ b/x-pack/platform/plugins/shared/cases/public/components/templates/index.tsx @@ -17,7 +17,6 @@ import { } from '@elastic/eui'; import { MAX_TEMPLATES_LENGTH } from '../../../common/constants'; import type { CasesConfigurationUITemplate } from '../../../common/ui'; -import { ExperimentalBadge } from '../experimental_badge/experimental_badge'; import * as i18n from './translations'; import { TemplatesList } from './templates_list'; @@ -72,9 +71,6 @@ const TemplatesComponent: React.FC = ({ title={ {i18n.TEMPLATE_TITLE} - - - } description={

{i18n.TEMPLATE_DESCRIPTION}

} From 82721b0c25162920dbe2660cccfe9ff0de7dd7d7 Mon Sep 17 00:00:00 2001 From: Maryam Saeidi Date: Fri, 10 Jan 2025 11:39:40 +0100 Subject: [PATCH 07/42] Remove wrong test (#206231) ## Summary This test is not useful and has a bug, so we decided to remove it. --- .../components/alerts_flyout/alerts_flyout.test.tsx | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/x-pack/solutions/observability/plugins/observability/public/components/alerts_flyout/alerts_flyout.test.tsx b/x-pack/solutions/observability/plugins/observability/public/components/alerts_flyout/alerts_flyout.test.tsx index 44889014a05b2..73193374e7e32 100644 --- a/x-pack/solutions/observability/plugins/observability/public/components/alerts_flyout/alerts_flyout.test.tsx +++ b/x-pack/solutions/observability/plugins/observability/public/components/alerts_flyout/alerts_flyout.test.tsx @@ -52,17 +52,6 @@ describe('AlertsFlyout', () => { expect(flyout.getByText('Recovered')).toBeInTheDocument(); }); - - it('should NOT show the Alert details button as the feature flag is disabled', async () => { - const flyout = render( - - ); - expect(flyout.queryByTestId('alertsFlyoutAlertDetailsButton')).not.toBeInTheDocument(); - }); }); const activeAlert: TopAlert = { From 3b42b80bce8240f33486542a4a492a9da33377fd Mon Sep 17 00:00:00 2001 From: Gerard Soldevila Date: Fri, 10 Jan 2025 11:47:47 +0100 Subject: [PATCH 08/42] SKA: Relocate Script v7.1 (#206233) ## Summary * Fix an issue with the `--list` command failing the 1st run. * Allow passing in no filters, and relocate "incorrect" modules (aka modules that are not in the correct folder) in that case. --- packages/kbn-relocate/list.ts | 49 +++++++++++++------------------ packages/kbn-relocate/relocate.ts | 2 ++ 2 files changed, 22 insertions(+), 29 deletions(-) diff --git a/packages/kbn-relocate/list.ts b/packages/kbn-relocate/list.ts index ed1c2072429b5..73fe93f3b6d9f 100644 --- a/packages/kbn-relocate/list.ts +++ b/packages/kbn-relocate/list.ts @@ -11,14 +11,14 @@ import { sortBy } from 'lodash'; import type { ToolingLog } from '@kbn/tooling-log'; import { getPackages } from '@kbn/repo-packages'; import { REPO_ROOT } from '@kbn/repo-info'; +import { join } from 'path'; import type { Package } from './types'; import { BASE_FOLDER, EXCLUDED_MODULES, KIBANA_FOLDER } from './constants'; import { calculateModuleTargetFolder, isInTargetFolder } from './utils/relocate'; import { createModuleTable } from './utils/logging'; +import { safeExec } from './utils/exec'; export const listModules = async (listFlag: string, log: ToolingLog) => { - // get all modules - const modules = getPackages(REPO_ROOT); const devOnly: Package[] = []; const test: Package[] = []; const examples: Package[] = []; @@ -26,47 +26,38 @@ export const listModules = async (listFlag: string, log: ToolingLog) => { const incorrect: Package[] = []; const correct: Package[] = []; + // get all modules + await safeExec('yarn kbn bootstrap'); + const modules = getPackages(REPO_ROOT); + // find modules selected by user filters sortBy(modules, 'directory') // explicit exclusions .filter(({ id }) => !EXCLUDED_MODULES.includes(id)) .forEach((module) => { + const directory = module.directory.startsWith(BASE_FOLDER) + ? module.directory + : join(BASE_FOLDER, module.directory); + if (module.isDevOnly()) { devOnly.push(module); - return; - } - - if ( - module.directory.includes(`/${KIBANA_FOLDER}/test/`) || - module.directory.includes(`/${KIBANA_FOLDER}/x-pack/test/`) + } else if ( + directory.includes(`/${KIBANA_FOLDER}/test/`) || + directory.includes(`/${KIBANA_FOLDER}/x-pack/test/`) ) { test.push(module); - return; - } - - if ( - module.directory.includes(`/${KIBANA_FOLDER}/examples/`) || - module.directory.includes(`/${KIBANA_FOLDER}/x-pack/examples/`) + } else if ( + directory.includes(`/${KIBANA_FOLDER}/examples/`) || + directory.includes(`/${KIBANA_FOLDER}/x-pack/examples/`) ) { examples.push(module); - return; - } - - if (!module.group || module.group === 'common' || !module.visibility) { - // log.warning(`The module ${module.id} does not specify 'group' or 'visibility'. Skipping`); + } else if (!module.group || module.group === 'common' || !module.visibility) { uncategorised.push(module); - return; - } - - if (!isInTargetFolder(module)) { + } else if (!isInTargetFolder(module)) { incorrect.push(module); - // log.warning(dedent`The module ${module.id} is not in the correct folder: - // - ${module.directory} - // - ${calculateModuleTargetFolder(module)}`); - - return; + } else { + correct.push(module); } - correct.push(module); }); if (listFlag === 'all') { diff --git a/packages/kbn-relocate/relocate.ts b/packages/kbn-relocate/relocate.ts index d377ffe35dbdf..98da95f39f98f 100644 --- a/packages/kbn-relocate/relocate.ts +++ b/packages/kbn-relocate/relocate.ts @@ -102,6 +102,7 @@ export interface RelocateModulesParams { const findModules = ({ teams, paths, included, excluded }: FindModulesParams, log: ToolingLog) => { // get all modules const modules = getPackages(REPO_ROOT); + const moduleFilters = teams.length > 0 || paths.length > 0 || included.length > 0; // find modules selected by user filters return ( @@ -121,6 +122,7 @@ const findModules = ({ teams, paths, included, excluded }: FindModulesParams, lo // the module is under the umbrella specified by the user .filter( (module) => + !moduleFilters || included.includes(module.id) || teams.some((team) => belongsTo(module, team)) || paths.some((path) => module.directory.includes(path)) From c7e4249b6bf123ea0fe075c4d12423fd6e4747df Mon Sep 17 00:00:00 2001 From: Bryce Buchanan <75274611+bryce-b@users.noreply.github.com> Date: Fri, 10 Jan 2025 02:50:15 -0800 Subject: [PATCH 09/42] [INFRA] Add additional perf telemetry (#205955) ## Summary Closes https://github.com/elastic/kibana/issues/205394 This PR adds performance telemetry to metric explorer and adds time range to existing telemetry in the host tables. Building on #180309. The time ranges added to the telemetry use the format "now", "now-15m" per the examples in the [performance tutorial docs](https://docs.elastic.dev/kibana-dev-docs/tutorial/performance/adding_custom_performance_metrics#add-time-ranges) example of perf temeletry with the query range metadata can be seen [here](https://telemetry-v2-staging.elastic.dev/s/apm/app/discover#/?_g=(filters:!(),query:(language:kuery,query:''),refreshInterval:(pause:!t,value:60000),time:(from:'2025-01-08T18:30:00.000Z',to:'2025-01-08T19:00:00.000Z'))&_a=(columns:!(),dataSource:(dataViewId:'0d6d7d31-1369-4a53-b36d-fbe97e4e5a0e',type:dataView),filters:!(),interval:auto,query:(language:kuery,query:'eventName%20:%20%22kibana:plugin_render_time%22%20and%20context.pageName%20:%20%22application:metrics:%2Fexplorer%22%20'),sort:!(!(timestamp,desc)))) --- .../pages/metrics/hosts/components/hosts_table.tsx | 6 +++++- .../public/pages/metrics/metrics_explorer/index.tsx | 12 ++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/x-pack/solutions/observability/plugins/infra/public/pages/metrics/hosts/components/hosts_table.tsx b/x-pack/solutions/observability/plugins/infra/public/pages/metrics/hosts/components/hosts_table.tsx index 310ff912d503a..16397805258bf 100644 --- a/x-pack/solutions/observability/plugins/infra/public/pages/metrics/hosts/components/hosts_table.tsx +++ b/x-pack/solutions/observability/plugins/infra/public/pages/metrics/hosts/components/hosts_table.tsx @@ -14,10 +14,10 @@ import type { HostNodeRow } from '../hooks/use_hosts_table'; import { useHostsTableContext } from '../hooks/use_hosts_table'; import { useHostsViewContext } from '../hooks/use_hosts_view'; import { useHostCountContext } from '../hooks/use_host_count'; +import { useUnifiedSearchContext } from '../hooks/use_unified_search'; import { FlyoutWrapper } from './host_details_flyout/flyout_wrapper'; import { DEFAULT_PAGE_SIZE, PAGE_SIZE_OPTIONS } from '../constants'; import { FilterAction } from './table/filter_action'; -import { useUnifiedSearchContext } from '../hooks/use_unified_search'; export const HostsTable = () => { const { loading } = useHostsViewContext(); @@ -44,6 +44,10 @@ export const HostsTable = () => { useEffect(() => { if (!loading && !hostCountLoading) { onPageReady({ + meta: { + rangeFrom: searchCriteria.dateRange.from, + rangeTo: searchCriteria.dateRange.to, + }, customMetrics: { key1: 'num_of_hosts', value1: count, diff --git a/x-pack/solutions/observability/plugins/infra/public/pages/metrics/metrics_explorer/index.tsx b/x-pack/solutions/observability/plugins/infra/public/pages/metrics/metrics_explorer/index.tsx index 64888c8bdf83b..4ddc1d55498c8 100644 --- a/x-pack/solutions/observability/plugins/infra/public/pages/metrics/metrics_explorer/index.tsx +++ b/x-pack/solutions/observability/plugins/infra/public/pages/metrics/metrics_explorer/index.tsx @@ -8,6 +8,7 @@ import { i18n } from '@kbn/i18n'; import React, { useEffect, useState } from 'react'; import { useTrackPageview, FeatureFeedbackButton } from '@kbn/observability-shared-plugin/public'; +import { usePerformanceContext } from '@kbn/ebt-tools'; import { OnboardingFlow } from '../../../components/shared/templates/no_data_config'; import { InfraPageTemplate } from '../../../components/shared/templates/infra_page_template'; import { WithMetricsExplorerOptionsUrlState } from '../../../containers/metrics_explorer/with_metrics_explorer_options_url_state'; @@ -65,6 +66,8 @@ const MetricsExplorerContent = () => { const { kibanaVersion, isCloudEnv, isServerlessEnv } = useKibanaEnvironmentContext(); + const { onPageReady } = usePerformanceContext(); + useTrackPageview({ app: 'infra_metrics', path: 'metrics_explorer' }); useTrackPageview({ app: 'infra_metrics', path: 'metrics_explorer', delay: 15000 }); @@ -93,6 +96,15 @@ const MetricsExplorerContent = () => { currentTimerange: timeRange, }; + if (!isLoading) { + onPageReady({ + meta: { + rangeFrom: timeRange.from, + rangeTo: timeRange.to, + }, + }); + } + return ( Date: Fri, 10 Jan 2025 12:01:55 +0100 Subject: [PATCH 10/42] =?UTF-8?q?[Streams=20=F0=9F=8C=8A]=20Stream=20enric?= =?UTF-8?q?hment=20processors=20management=20(#204793)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 📓 Summary Part of #https://github.com/elastic/streams-program/issues/32 This work implements a UI for basic stream enrichment, supporting grok and dissect processor + detected fields mapping. The main features implemented in this PR consist of: - **Sortable processors list** - **Add new processor - Grok, Dissect** - Ad-hoc forms for each processor - Simulated document outcome with extracted fields - Filter matching documents with parsed fields - Mapping detected fields (only available for wired streams) - **Edit processor** - Change configuration only - Delete processor CTA As a side quest, I added a small package for object utils as @simianhacker suggested. `@kbn/object-utils` exposes `calculateObjectDiff` and `flattenObject` to detect the changed fields in a simulation. ## 🔜 Follow-up work I'll work on minor updates on top of this MVP to make this available for further testing from the team. The next steps will be: - **Tests** for features that consolidate on the functional pov. - Better field mapping detection and UI feedback (infer the type of the detected field, currently always unmapped) - Add better form validation and feedback for processor configuration. As discussed offline, state management is purely based on the built-in react APIs + react-hook-form. It could be improved with different approaches, including a more solid state management library to make it easier to maintain and bulletproof to race conditions. No state syncs with the URL currently. ## 🎥 Demo https://github.com/user-attachments/assets/a48fade9-f5aa-4270-bb19-d91d1eed822b --------- Co-authored-by: Marco Antonio Ghiani Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .github/CODEOWNERS | 1 + package.json | 1 + .../shared/kbn-object-utils/README.md | 76 +++ .../packages/shared/kbn-object-utils/index.ts | 11 + .../shared/kbn-object-utils/jest.config.js | 14 + .../shared/kbn-object-utils/kibana.jsonc | 10 + .../shared/kbn-object-utils/package.json | 8 + .../src/calculate_object_diff.test.ts | 34 ++ .../src/calculate_object_diff.ts | 75 +++ .../src/flatten_object.test.ts | 32 ++ .../kbn-object-utils/src/flatten_object.ts | 33 ++ .../shared/kbn-object-utils/tsconfig.json | 10 + tsconfig.base.json | 2 + .../src/apis/list_streams_response.ts | 4 +- .../kbn-streams-schema/src/helpers/index.ts | 1 + .../src/helpers/processing.ts | 38 ++ .../src/helpers/type_guards.ts | 8 +- .../kbn-streams-schema/src/models/common.ts | 25 +- .../src/models/streams/stream.ts | 4 +- .../lib/streams/errors/simulation_failed.ts | 20 + .../lib/streams/helpers/condition_fields.ts | 13 - .../server/lib/streams/helpers/processing.ts | 26 + .../generate_ingest_pipeline.ts | 37 +- .../plugins/streams/server/routes/index.ts | 2 + .../routes/streams/processing/simulate.ts | 153 ++++++ .../streams/server/routes/streams/sample.ts | 6 +- .../plugins/streams/tsconfig.json | 1 + .../get_mock_streams_app_context.tsx | 2 + .../public/components/app_root/index.tsx | 1 + .../public/components/asset_image/index.tsx | 47 ++ .../asset_image/no_results_dark.png | Bin 0 -> 37719 bytes .../asset_image/no_results_light.png | Bin 0 -> 37158 bytes .../components/asset_image/welcome_dark.png | Bin 0 -> 48993 bytes .../components/asset_image/welcome_light.png | Bin 0 -> 50657 bytes .../public/components/assets/illustration.png | Bin 58425 -> 0 bytes .../components/condition_editor/index.tsx | 7 +- .../management_bottom_bar/index.tsx | 58 +++ .../public/components/preview_table/index.tsx | 10 +- .../stream_detail_enriching/index.tsx | 18 - .../add_processor_button.tsx | 22 + .../enrichment_empty_prompt.tsx | 44 ++ .../flyout/danger_zone.tsx | 89 ++++ .../dissect/dissect_append_separator.tsx | 36 ++ .../dissect/dissect_pattern_definition.tsx | 79 +++ .../flyout/dissect/index.tsx | 50 ++ .../flyout/grok/grok_pattern_definition.tsx | 92 ++++ .../flyout/grok/grok_patterns_editor.tsx | 128 +++++ .../flyout/grok/index.tsx | 50 ++ .../stream_detail_enrichment/flyout/index.tsx | 169 +++++++ .../flyout/optional_fields_accordion.tsx | 38 ++ .../flyout/processor_condition_editor.tsx | 16 + .../flyout/processor_field_selector.tsx | 31 ++ .../flyout/processor_flyout_template.tsx | 62 +++ .../flyout/processor_outcome_preview.tsx | 459 ++++++++++++++++++ .../flyout/processor_type_selector.tsx | 108 +++++ .../flyout/toggle_field.tsx | 40 ++ .../hooks/use_definition.ts | 162 +++++++ .../stream_detail_enrichment/index.tsx | 31 ++ .../stream_detail_enrichment/page_content.tsx | 157 ++++++ .../processors_list.tsx | 114 +++++ .../root_stream_empty_prompt.tsx | 39 ++ .../sortable_list.tsx | 38 ++ .../stream_detail_enrichment/types.ts | 45 ++ .../stream_detail_enrichment/utils.ts | 137 ++++++ .../stream_detail_management/classic.tsx | 6 +- .../stream_detail_management/wired.tsx | 6 +- .../stream_detail_management/wrapper.tsx | 2 +- .../stream_detail_routing/index.tsx | 11 +- .../streams_app_search_bar/index.tsx | 4 +- .../public/hooks/use_discard_confirm.ts | 35 ++ .../streams_app/public/hooks/use_kibana.tsx | 6 +- .../plugins/streams_app/tsconfig.json | 6 + yarn.lock | 4 + 73 files changed, 2998 insertions(+), 106 deletions(-) create mode 100644 src/platform/packages/shared/kbn-object-utils/README.md create mode 100644 src/platform/packages/shared/kbn-object-utils/index.ts create mode 100644 src/platform/packages/shared/kbn-object-utils/jest.config.js create mode 100644 src/platform/packages/shared/kbn-object-utils/kibana.jsonc create mode 100644 src/platform/packages/shared/kbn-object-utils/package.json create mode 100644 src/platform/packages/shared/kbn-object-utils/src/calculate_object_diff.test.ts create mode 100644 src/platform/packages/shared/kbn-object-utils/src/calculate_object_diff.ts create mode 100644 src/platform/packages/shared/kbn-object-utils/src/flatten_object.test.ts create mode 100644 src/platform/packages/shared/kbn-object-utils/src/flatten_object.ts create mode 100644 src/platform/packages/shared/kbn-object-utils/tsconfig.json create mode 100644 x-pack/packages/kbn-streams-schema/src/helpers/processing.ts create mode 100644 x-pack/solutions/observability/plugins/streams/server/lib/streams/errors/simulation_failed.ts create mode 100644 x-pack/solutions/observability/plugins/streams/server/lib/streams/helpers/processing.ts create mode 100644 x-pack/solutions/observability/plugins/streams/server/routes/streams/processing/simulate.ts create mode 100644 x-pack/solutions/observability/plugins/streams_app/public/components/asset_image/index.tsx create mode 100644 x-pack/solutions/observability/plugins/streams_app/public/components/asset_image/no_results_dark.png create mode 100644 x-pack/solutions/observability/plugins/streams_app/public/components/asset_image/no_results_light.png create mode 100644 x-pack/solutions/observability/plugins/streams_app/public/components/asset_image/welcome_dark.png create mode 100644 x-pack/solutions/observability/plugins/streams_app/public/components/asset_image/welcome_light.png delete mode 100644 x-pack/solutions/observability/plugins/streams_app/public/components/assets/illustration.png create mode 100644 x-pack/solutions/observability/plugins/streams_app/public/components/management_bottom_bar/index.tsx delete mode 100644 x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_enriching/index.tsx create mode 100644 x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_enrichment/add_processor_button.tsx create mode 100644 x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_enrichment/enrichment_empty_prompt.tsx create mode 100644 x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_enrichment/flyout/danger_zone.tsx create mode 100644 x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_enrichment/flyout/dissect/dissect_append_separator.tsx create mode 100644 x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_enrichment/flyout/dissect/dissect_pattern_definition.tsx create mode 100644 x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_enrichment/flyout/dissect/index.tsx create mode 100644 x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_enrichment/flyout/grok/grok_pattern_definition.tsx create mode 100644 x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_enrichment/flyout/grok/grok_patterns_editor.tsx create mode 100644 x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_enrichment/flyout/grok/index.tsx create mode 100644 x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_enrichment/flyout/index.tsx create mode 100644 x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_enrichment/flyout/optional_fields_accordion.tsx create mode 100644 x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_enrichment/flyout/processor_condition_editor.tsx create mode 100644 x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_enrichment/flyout/processor_field_selector.tsx create mode 100644 x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_enrichment/flyout/processor_flyout_template.tsx create mode 100644 x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_enrichment/flyout/processor_outcome_preview.tsx create mode 100644 x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_enrichment/flyout/processor_type_selector.tsx create mode 100644 x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_enrichment/flyout/toggle_field.tsx create mode 100644 x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_enrichment/hooks/use_definition.ts create mode 100644 x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_enrichment/index.tsx create mode 100644 x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_enrichment/page_content.tsx create mode 100644 x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_enrichment/processors_list.tsx create mode 100644 x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_enrichment/root_stream_empty_prompt.tsx create mode 100644 x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_enrichment/sortable_list.tsx create mode 100644 x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_enrichment/types.ts create mode 100644 x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_enrichment/utils.ts create mode 100644 x-pack/solutions/observability/plugins/streams_app/public/hooks/use_discard_confirm.ts diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index c9af0fd8d5c1b..3cc282ab49b50 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -497,6 +497,7 @@ src/platform/packages/shared/kbn-management/settings/field_definition @elastic/k src/platform/packages/shared/kbn-management/settings/types @elastic/kibana-management src/platform/packages/shared/kbn-management/settings/utilities @elastic/kibana-management src/platform/packages/shared/kbn-monaco @elastic/appex-sharedux +src/platform/packages/shared/kbn-object-utils @elastic/kibana-core src/platform/packages/shared/kbn-object-versioning @elastic/appex-sharedux src/platform/packages/shared/kbn-object-versioning-utils @elastic/appex-sharedux src/platform/packages/shared/kbn-openapi-common @elastic/security-detection-rule-management diff --git a/package.json b/package.json index 44730ac027fe9..9d592efff0187 100644 --- a/package.json +++ b/package.json @@ -686,6 +686,7 @@ "@kbn/newsfeed-test-plugin": "link:test/common/plugins/newsfeed", "@kbn/no-data-page-plugin": "link:src/platform/plugins/private/no_data_page", "@kbn/notifications-plugin": "link:x-pack/platform/plugins/shared/notifications", + "@kbn/object-utils": "link:src/platform/packages/shared/kbn-object-utils", "@kbn/object-versioning": "link:src/platform/packages/shared/kbn-object-versioning", "@kbn/object-versioning-utils": "link:src/platform/packages/shared/kbn-object-versioning-utils", "@kbn/observability-ai-assistant-app-plugin": "link:x-pack/solutions/observability/plugins/observability_ai_assistant_app", diff --git a/src/platform/packages/shared/kbn-object-utils/README.md b/src/platform/packages/shared/kbn-object-utils/README.md new file mode 100644 index 0000000000000..c3a94576d727f --- /dev/null +++ b/src/platform/packages/shared/kbn-object-utils/README.md @@ -0,0 +1,76 @@ +# @kbn/object-utils + +Utilities for objects manipulation and parsing. + +## Utilities + +### calculateObjectDiff + +This utils compares two JSON objects and calculates the added and removed properties, including nested properties. + +```ts +const oldObject = { + alpha: 1, + beta: { + gamma: 2, + delta: { + sigma: 7, + }, + }, +}; + +const newObject = { + alpha: 1, + beta: { + gamma: 2, + eta: 4, + }, +}; + +const diff = calculateObjectDiff(oldObject, newObject); + +/* +Result: +{ + added: { + beta: { + eta: 4, + }, + }, + removed: { + beta: { + delta: { + sigma: 7, + }, + }, + }, +} +*/ +``` + +### flattenObject + +This utils returns a flattened version of the input object also accounting for nested properties. + +```ts +const flattened = flattenObject({ + alpha: { + gamma: { + sigma: 1, + }, + delta: { + sigma: 2, + }, + }, + beta: 3, +}); + +/* +Result: +{ + 'alpha.gamma.sigma': 1, + 'alpha.delta.sigma': 2, + beta: 3, +} +*/ +``` diff --git a/src/platform/packages/shared/kbn-object-utils/index.ts b/src/platform/packages/shared/kbn-object-utils/index.ts new file mode 100644 index 0000000000000..f5051ab8b042c --- /dev/null +++ b/src/platform/packages/shared/kbn-object-utils/index.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +export * from './src/calculate_object_diff'; +export * from './src/flatten_object'; diff --git a/src/platform/packages/shared/kbn-object-utils/jest.config.js b/src/platform/packages/shared/kbn-object-utils/jest.config.js new file mode 100644 index 0000000000000..cd6727b073b79 --- /dev/null +++ b/src/platform/packages/shared/kbn-object-utils/jest.config.js @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../../../..', + roots: ['/src/platform/packages/shared/kbn-object-utils'], +}; diff --git a/src/platform/packages/shared/kbn-object-utils/kibana.jsonc b/src/platform/packages/shared/kbn-object-utils/kibana.jsonc new file mode 100644 index 0000000000000..ce6365156b592 --- /dev/null +++ b/src/platform/packages/shared/kbn-object-utils/kibana.jsonc @@ -0,0 +1,10 @@ +{ + "type": "shared-common", + "id": "@kbn/object-utils", + "owner": [ + "@elastic/kibana-core" + ], + "group": "platform", + "visibility": "shared", + "devOnly": false +} \ No newline at end of file diff --git a/src/platform/packages/shared/kbn-object-utils/package.json b/src/platform/packages/shared/kbn-object-utils/package.json new file mode 100644 index 0000000000000..86111f76bb09d --- /dev/null +++ b/src/platform/packages/shared/kbn-object-utils/package.json @@ -0,0 +1,8 @@ +{ + "description": "Object utils for Kibana", + "license": "Elastic License 2.0 OR AGPL-3.0-only OR SSPL-1.0", + "name": "@kbn/object-utils", + "private": true, + "version": "1.0.0", + "sideEffects": false +} \ No newline at end of file diff --git a/src/platform/packages/shared/kbn-object-utils/src/calculate_object_diff.test.ts b/src/platform/packages/shared/kbn-object-utils/src/calculate_object_diff.test.ts new file mode 100644 index 0000000000000..340be7783aef3 --- /dev/null +++ b/src/platform/packages/shared/kbn-object-utils/src/calculate_object_diff.test.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { calculateObjectDiff } from './calculate_object_diff'; + +describe('calculateObjectDiff', () => { + it('should return the added and removed parts between 2 objects', () => { + const { added, removed } = calculateObjectDiff({ alpha: 1, beta: 2 }, { alpha: 1, gamma: 3 }); + expect(added).toEqual({ gamma: 3 }); + expect(removed).toEqual({ beta: 2 }); + }); + + it('should work on nested objects', () => { + const { added, removed } = calculateObjectDiff( + { alpha: 1, beta: { gamma: 2, delta: { sigma: 7 } } }, + { alpha: 1, beta: { gamma: 2, eta: 4 } } + ); + + expect(added).toEqual({ beta: { eta: 4 } }); + expect(removed).toEqual({ beta: { delta: { sigma: 7 } } }); + }); + + it('should return empty added/removed when the objects are the same', () => { + const { added, removed } = calculateObjectDiff({ alpha: 1, beta: 2 }, { alpha: 1, beta: 2 }); + expect(added).toEqual({}); + expect(removed).toEqual({}); + }); +}); diff --git a/src/platform/packages/shared/kbn-object-utils/src/calculate_object_diff.ts b/src/platform/packages/shared/kbn-object-utils/src/calculate_object_diff.ts new file mode 100644 index 0000000000000..33fe441de5ac6 --- /dev/null +++ b/src/platform/packages/shared/kbn-object-utils/src/calculate_object_diff.ts @@ -0,0 +1,75 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { isEmpty, isPlainObject } from 'lodash'; + +interface Obj { + [key: PropertyKey]: Obj | unknown; +} + +type DeepPartial = { + [Prop in keyof TInputObj]?: TInputObj[Prop] extends Obj + ? DeepPartial + : TInputObj[Prop]; +}; + +interface ObjectDiffResult { + added: DeepPartial; + removed: DeepPartial; +} + +/** + * Compares two JSON objects and calculates the added and removed properties, including nested properties. + * @param oldObj - The base object. + * @param newObj - The comparison object. + * @returns An object containing added and removed properties. + */ +export function calculateObjectDiff( + oldObj: TBase, + newObj?: TCompare +): ObjectDiffResult { + const added: DeepPartial = {}; + const removed: DeepPartial = {}; + + if (!newObj) return { added, removed }; + + function diffRecursive( + base: Obj, + compare: Obj, + addedMap: DeepPartial, + removedMap: DeepPartial + ): void { + for (const key in compare) { + if (!(key in base)) { + addedMap[key] = compare[key]; + } else if (isPlainObject(base[key]) && isPlainObject(compare[key])) { + addedMap[key] = {}; + removedMap[key] = {}; + diffRecursive( + base[key] as Obj, + compare[key] as Obj, + addedMap[key] as Obj, + removedMap[key] as Obj + ); + if (isEmpty(addedMap[key])) delete addedMap[key]; + if (isEmpty(removedMap[key])) delete removedMap[key]; + } + } + + for (const key in base) { + if (!(key in compare)) { + removedMap[key] = base[key]; + } + } + } + + diffRecursive(oldObj, newObj, added, removed); + + return { added, removed }; +} diff --git a/src/platform/packages/shared/kbn-object-utils/src/flatten_object.test.ts b/src/platform/packages/shared/kbn-object-utils/src/flatten_object.test.ts new file mode 100644 index 0000000000000..80bb6b4503020 --- /dev/null +++ b/src/platform/packages/shared/kbn-object-utils/src/flatten_object.test.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { flattenObject } from './flatten_object'; + +describe('flattenObject', () => { + it('should flat gamma object properties', () => { + const flattened = flattenObject({ + alpha: { + gamma: { + sigma: 1, + }, + delta: { + sigma: 2, + }, + }, + beta: 3, + }); + + expect(flattened).toEqual({ + 'alpha.gamma.sigma': 1, + 'alpha.delta.sigma': 2, + beta: 3, + }); + }); +}); diff --git a/src/platform/packages/shared/kbn-object-utils/src/flatten_object.ts b/src/platform/packages/shared/kbn-object-utils/src/flatten_object.ts new file mode 100644 index 0000000000000..0690012ca2640 --- /dev/null +++ b/src/platform/packages/shared/kbn-object-utils/src/flatten_object.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { isPlainObject } from 'lodash'; + +/** + * Returns a flattened version of the input object also accounting for nested properties. + * @param obj - The input object. + * @param parentKey - The initial key used for recursive flattening. + * @returns An object containing all the flattened properties. + */ +export function flattenObject(obj: Record, parentKey: string = '') { + const result: Record = {}; + + for (const key in obj) { + if (Object.hasOwn(obj, key)) { + const value = obj[key]; + const newKey = parentKey ? `${parentKey}.${key}` : key; + if (isPlainObject(value)) { + Object.assign(result, flattenObject(value as Record, newKey)); + } else { + result[newKey] = value; + } + } + } + return result; +} diff --git a/src/platform/packages/shared/kbn-object-utils/tsconfig.json b/src/platform/packages/shared/kbn-object-utils/tsconfig.json new file mode 100644 index 0000000000000..2e04c4a6f5733 --- /dev/null +++ b/src/platform/packages/shared/kbn-object-utils/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "outDir": "target/types", + "types": ["jest", "node"] + }, + "exclude": ["target/**/*"], + "extends": "../../../../../tsconfig.base.json", + "include": ["**/*.ts"], + "kbn_references": [] +} diff --git a/tsconfig.base.json b/tsconfig.base.json index 2a439e72882bb..75784f7dad582 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -1308,6 +1308,8 @@ "@kbn/no-data-page-plugin/*": ["src/platform/plugins/private/no_data_page/*"], "@kbn/notifications-plugin": ["x-pack/platform/plugins/shared/notifications"], "@kbn/notifications-plugin/*": ["x-pack/platform/plugins/shared/notifications/*"], + "@kbn/object-utils": ["src/platform/packages/shared/kbn-object-utils"], + "@kbn/object-utils/*": ["src/platform/packages/shared/kbn-object-utils/*"], "@kbn/object-versioning": ["src/platform/packages/shared/kbn-object-versioning"], "@kbn/object-versioning/*": ["src/platform/packages/shared/kbn-object-versioning/*"], "@kbn/object-versioning-utils": ["src/platform/packages/shared/kbn-object-versioning-utils"], diff --git a/x-pack/packages/kbn-streams-schema/src/apis/list_streams_response.ts b/x-pack/packages/kbn-streams-schema/src/apis/list_streams_response.ts index f1f42f7e90ae2..57077c98c1ad3 100644 --- a/x-pack/packages/kbn-streams-schema/src/apis/list_streams_response.ts +++ b/x-pack/packages/kbn-streams-schema/src/apis/list_streams_response.ts @@ -5,10 +5,10 @@ * 2.0. */ import { z } from '@kbn/zod'; -import { streamDefintionSchema } from '../models'; +import { streamDefinitionSchema } from '../models'; export const listStreamsResponseSchema = z.object({ - streams: z.array(streamDefintionSchema), + streams: z.array(streamDefinitionSchema), }); export type ListStreamsResponse = z.infer; diff --git a/x-pack/packages/kbn-streams-schema/src/helpers/index.ts b/x-pack/packages/kbn-streams-schema/src/helpers/index.ts index d1f9a8ff0c50a..d9cb94d020d13 100644 --- a/x-pack/packages/kbn-streams-schema/src/helpers/index.ts +++ b/x-pack/packages/kbn-streams-schema/src/helpers/index.ts @@ -5,4 +5,5 @@ * 2.0. */ +export * from './processing'; export * from './type_guards'; diff --git a/x-pack/packages/kbn-streams-schema/src/helpers/processing.ts b/x-pack/packages/kbn-streams-schema/src/helpers/processing.ts new file mode 100644 index 0000000000000..95d7aa0b23aac --- /dev/null +++ b/x-pack/packages/kbn-streams-schema/src/helpers/processing.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Condition, ProcessingDefinition } from '../models'; +import { + isGrokProcessor, + isDissectProcessor, + isFilterCondition, + isAndCondition, + isOrCondition, +} from './type_guards'; + +export function getProcessorType(processor: ProcessingDefinition) { + if (isGrokProcessor(processor.config)) { + return 'grok'; + } + if (isDissectProcessor(processor.config)) { + return 'dissect'; + } + throw new Error('Unknown processor type'); +} + +export function isCompleteCondition(condition: Condition): boolean { + if (isFilterCondition(condition)) { + return condition.field !== undefined && condition.field !== ''; + } + if (isAndCondition(condition)) { + return condition.and.every(isCompleteCondition); + } + if (isOrCondition(condition)) { + return condition.or.every(isCompleteCondition); + } + return false; +} diff --git a/x-pack/packages/kbn-streams-schema/src/helpers/type_guards.ts b/x-pack/packages/kbn-streams-schema/src/helpers/type_guards.ts index 6e3fb3418b853..3c3be21f794d0 100644 --- a/x-pack/packages/kbn-streams-schema/src/helpers/type_guards.ts +++ b/x-pack/packages/kbn-streams-schema/src/helpers/type_guards.ts @@ -10,7 +10,7 @@ import { AndCondition, conditionSchema, dissectProcessingDefinitionSchema, - DissectProcssingDefinition, + DissectProcessingDefinition, FilterCondition, filterConditionSchema, GrokProcessingDefinition, @@ -23,7 +23,7 @@ import { ReadStreamDefinition, readStreamDefinitonSchema, StreamDefinition, - streamDefintionSchema, + streamDefinitionSchema, WiredReadStreamDefinition, wiredReadStreamDefinitonSchema, WiredStreamDefinition, @@ -60,7 +60,7 @@ export function isIngestReadStream(subject: any): subject is IngestReadStreamDef } export function isStream(subject: any): subject is StreamDefinition { - return isSchema(streamDefintionSchema, subject); + return isSchema(streamDefinitionSchema, subject); } export function isIngestStream(subject: StreamDefinition): subject is IngestStreamDefinition { @@ -97,7 +97,7 @@ export function isGrokProcessor(subject: any): subject is GrokProcessingDefiniti return isSchema(grokProcessingDefinitionSchema, subject); } -export function isDissectProcessor(subject: any): subject is DissectProcssingDefinition { +export function isDissectProcessor(subject: any): subject is DissectProcessingDefinition { return isSchema(dissectProcessingDefinitionSchema, subject); } diff --git a/x-pack/packages/kbn-streams-schema/src/models/common.ts b/x-pack/packages/kbn-streams-schema/src/models/common.ts index f3c967e52605a..4b3a8628a38ec 100644 --- a/x-pack/packages/kbn-streams-schema/src/models/common.ts +++ b/x-pack/packages/kbn-streams-schema/src/models/common.ts @@ -52,6 +52,8 @@ export const grokProcessingDefinitionSchema = z.object({ field: z.string(), patterns: z.array(z.string()), pattern_definitions: z.optional(z.record(z.string())), + ignore_failure: z.optional(z.boolean()), + ignore_missing: z.optional(z.boolean()), }), }); @@ -61,10 +63,13 @@ export const dissectProcessingDefinitionSchema = z.object({ dissect: z.object({ field: z.string(), pattern: z.string(), + append_separator: z.optional(z.string()), + ignore_failure: z.optional(z.boolean()), + ignore_missing: z.optional(z.boolean()), }), }); -export type DissectProcssingDefinition = z.infer; +export type DissectProcessingDefinition = z.infer; export const processingConfigSchema = z.union([ grokProcessingDefinitionSchema, @@ -78,8 +83,24 @@ export const processingDefinitionSchema = z.object({ export type ProcessingDefinition = z.infer; +export type ProcessorType = ProcessingDefinition['config'] extends infer U + ? U extends { [key: string]: any } + ? keyof U + : never + : never; + +export const FIELD_DEFINITION_TYPES = [ + 'keyword', + 'match_only_text', + 'long', + 'double', + 'date', + 'boolean', + 'ip', +] as const; + export const fieldDefinitionConfigSchema = z.object({ - type: z.enum(['keyword', 'match_only_text', 'long', 'double', 'date', 'boolean', 'ip']), + type: z.enum(FIELD_DEFINITION_TYPES), format: z.optional(z.string()), }); diff --git a/x-pack/packages/kbn-streams-schema/src/models/streams/stream.ts b/x-pack/packages/kbn-streams-schema/src/models/streams/stream.ts index 152397060e51b..a5e411f1674c6 100644 --- a/x-pack/packages/kbn-streams-schema/src/models/streams/stream.ts +++ b/x-pack/packages/kbn-streams-schema/src/models/streams/stream.ts @@ -9,9 +9,9 @@ import { z } from '@kbn/zod'; import { wiredStreamDefinitonSchema } from './wired_stream'; import { ingestStreamDefinitonSchema } from './ingest_stream'; -export const streamDefintionSchema = z.union([ +export const streamDefinitionSchema = z.union([ wiredStreamDefinitonSchema, ingestStreamDefinitonSchema, ]); -export type StreamDefinition = z.infer; +export type StreamDefinition = z.infer; diff --git a/x-pack/solutions/observability/plugins/streams/server/lib/streams/errors/simulation_failed.ts b/x-pack/solutions/observability/plugins/streams/server/lib/streams/errors/simulation_failed.ts new file mode 100644 index 0000000000000..28140b377403f --- /dev/null +++ b/x-pack/solutions/observability/plugins/streams/server/lib/streams/errors/simulation_failed.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { errors } from '@elastic/elasticsearch'; + +export class SimulationFailed extends Error { + constructor(error: errors.ResponseError) { + super( + error.body?.error?.reason || + error.body?.error?.caused_by?.reason || + error.message || + 'Unknown error' + ); + this.name = 'SimulationFailed'; + } +} diff --git a/x-pack/solutions/observability/plugins/streams/server/lib/streams/helpers/condition_fields.ts b/x-pack/solutions/observability/plugins/streams/server/lib/streams/helpers/condition_fields.ts index b9b1bbfd1948b..5a7a5dd345fdf 100644 --- a/x-pack/solutions/observability/plugins/streams/server/lib/streams/helpers/condition_fields.ts +++ b/x-pack/solutions/observability/plugins/streams/server/lib/streams/helpers/condition_fields.ts @@ -13,19 +13,6 @@ import { isOrCondition, } from '@kbn/streams-schema'; -export function isComplete(condition: Condition): boolean { - if (isFilterCondition(condition)) { - return condition.field !== undefined && condition.field !== ''; - } - if (isAndCondition(condition)) { - return condition.and.every(isComplete); - } - if (isOrCondition(condition)) { - return condition.or.every(isComplete); - } - return false; -} - export function getFields( condition: Condition ): Array<{ name: string; type: 'number' | 'string' }> { diff --git a/x-pack/solutions/observability/plugins/streams/server/lib/streams/helpers/processing.ts b/x-pack/solutions/observability/plugins/streams/server/lib/streams/helpers/processing.ts new file mode 100644 index 0000000000000..111e1f22bdf56 --- /dev/null +++ b/x-pack/solutions/observability/plugins/streams/server/lib/streams/helpers/processing.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ProcessingDefinition, getProcessorType } from '@kbn/streams-schema'; +import { get } from 'lodash'; +import { IngestProcessorContainer } from '@elastic/elasticsearch/lib/api/types'; +import { conditionToPainless } from './condition_to_painless'; + +export function formatToIngestProcessors( + processing: ProcessingDefinition[] +): IngestProcessorContainer[] { + return processing.map((processor) => { + const type = getProcessorType(processor); + const config = get(processor.config, type); + return { + [type]: { + ...config, + if: processor.condition ? conditionToPainless(processor.condition) : undefined, + }, + }; + }); +} diff --git a/x-pack/solutions/observability/plugins/streams/server/lib/streams/ingest_pipelines/generate_ingest_pipeline.ts b/x-pack/solutions/observability/plugins/streams/server/lib/streams/ingest_pipelines/generate_ingest_pipeline.ts index fb03868ed7482..ceeee0bab6866 100644 --- a/x-pack/solutions/observability/plugins/streams/server/lib/streams/ingest_pipelines/generate_ingest_pipeline.ts +++ b/x-pack/solutions/observability/plugins/streams/server/lib/streams/ingest_pipelines/generate_ingest_pipeline.ts @@ -5,48 +5,19 @@ * 2.0. */ -import { - isDissectProcessor, - isGrokProcessor, - ProcessingDefinition, - StreamDefinition, -} from '@kbn/streams-schema'; -import { get } from 'lodash'; +import { StreamDefinition } from '@kbn/streams-schema'; import { ASSET_VERSION } from '../../../../common/constants'; -import { conditionToPainless } from '../helpers/condition_to_painless'; import { logsDefaultPipelineProcessors } from './logs_default_pipeline'; import { isRoot } from '../helpers/hierarchy'; import { getProcessingPipelineName } from './name'; - -function getProcessorType(processor: ProcessingDefinition) { - if (isGrokProcessor(processor.config)) { - return 'grok'; - } - if (isDissectProcessor(processor.config)) { - return 'dissect'; - } - throw new Error('Unknown processor type'); -} - -function generateProcessingSteps(definition: StreamDefinition) { - return definition.stream.ingest.processing.map((processor) => { - const type = getProcessorType(processor); - const config = get(processor.config, type); - return { - [type]: { - ...config, - if: processor.condition ? conditionToPainless(processor.condition) : undefined, - }, - }; - }); -} +import { formatToIngestProcessors } from '../helpers/processing'; export function generateIngestPipeline(id: string, definition: StreamDefinition) { return { id: getProcessingPipelineName(id), processors: [ ...(isRoot(definition.name) ? logsDefaultPipelineProcessors : []), - ...generateProcessingSteps(definition), + ...formatToIngestProcessors(definition.stream.ingest.processing), { pipeline: { name: `${id}@stream.reroutes`, @@ -64,7 +35,7 @@ export function generateIngestPipeline(id: string, definition: StreamDefinition) export function generateClassicIngestPipelineBody(definition: StreamDefinition) { return { - processors: generateProcessingSteps(definition), + processors: formatToIngestProcessors(definition.stream.ingest.processing), _meta: { description: `Stream-managed pipeline for the ${definition.name} stream`, managed: true, diff --git a/x-pack/solutions/observability/plugins/streams/server/routes/index.ts b/x-pack/solutions/observability/plugins/streams/server/routes/index.ts index 3e80d57dfff3c..340900050d1c9 100644 --- a/x-pack/solutions/observability/plugins/streams/server/routes/index.ts +++ b/x-pack/solutions/observability/plugins/streams/server/routes/index.ts @@ -19,6 +19,7 @@ import { resyncStreamsRoute } from './streams/resync'; import { sampleStreamRoute } from './streams/sample'; import { schemaFieldsSimulationRoute } from './streams/schema/fields_simulation'; import { unmappedFieldsRoute } from './streams/schema/unmapped_fields'; +import { simulateProcessorRoute } from './streams/processing/simulate'; import { streamsStatusRoutes } from './streams/settings'; export const streamsRouteRepository = { @@ -36,6 +37,7 @@ export const streamsRouteRepository = { ...sampleStreamRoute, ...streamDetailRoute, ...unmappedFieldsRoute, + ...simulateProcessorRoute, ...schemaFieldsSimulationRoute, }; diff --git a/x-pack/solutions/observability/plugins/streams/server/routes/streams/processing/simulate.ts b/x-pack/solutions/observability/plugins/streams/server/routes/streams/processing/simulate.ts new file mode 100644 index 0000000000000..4b0c7a14ed861 --- /dev/null +++ b/x-pack/solutions/observability/plugins/streams/server/routes/streams/processing/simulate.ts @@ -0,0 +1,153 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { z } from '@kbn/zod'; +import { notFound, internal, badRequest } from '@hapi/boom'; +import { FieldDefinitionConfig, processingDefinitionSchema } from '@kbn/streams-schema'; +import { calculateObjectDiff, flattenObject } from '@kbn/object-utils'; +import { + IngestSimulateResponse, + IngestSimulateSimulateDocumentResult, +} from '@elastic/elasticsearch/lib/api/types'; +import { SimulationFailed } from '../../../lib/streams/errors/simulation_failed'; +import { formatToIngestProcessors } from '../../../lib/streams/helpers/processing'; +import { createServerRoute } from '../../create_server_route'; +import { DefinitionNotFound } from '../../../lib/streams/errors'; +import { checkAccess } from '../../../lib/streams/stream_crud'; + +export const simulateProcessorRoute = createServerRoute({ + endpoint: 'POST /api/streams/{id}/processing/_simulate', + options: { + access: 'internal', + }, + security: { + authz: { + enabled: false, + reason: + 'This API delegates security to the currently logged in user and their Elasticsearch permissions.', + }, + }, + params: z.object({ + path: z.object({ id: z.string() }), + body: z.object({ + processing: z.array(processingDefinitionSchema), + documents: z.array(z.record(z.unknown())), + }), + }), + handler: async ({ params, request, response, getScopedClients }) => { + try { + const { scopedClusterClient } = await getScopedClients({ request }); + + const hasAccess = await checkAccess({ id: params.path.id, scopedClusterClient }); + if (!hasAccess) { + throw new DefinitionNotFound(`Stream definition for ${params.path.id} not found.`); + } + // Normalize processing definition to pipeline processors + const processors = formatToIngestProcessors(params.body.processing); + // Convert input documents to ingest simulation format + const docs = params.body.documents.map((doc) => ({ _source: doc })); + + let simulationResult: IngestSimulateResponse; + try { + simulationResult = await scopedClusterClient.asCurrentUser.ingest.simulate({ + verbose: true, + pipeline: { processors }, + docs, + }); + } catch (error) { + throw new SimulationFailed(error); + } + + const documents = computeSimulationDocuments(simulationResult, docs); + const detectedFields = computeDetectedFields(simulationResult, docs); + const successRate = computeSuccessRate(simulationResult); + const failureRate = 1 - successRate; + + return { + documents, + success_rate: parseFloat(successRate.toFixed(2)), + failure_rate: parseFloat(failureRate.toFixed(2)), + detected_fields: detectedFields, + }; + } catch (error) { + if (error instanceof DefinitionNotFound) { + throw notFound(error); + } + + if (error instanceof SimulationFailed) { + throw badRequest(error); + } + + throw internal(error); + } + }, +}); + +const computeSimulationDocuments = ( + simulation: IngestSimulateResponse, + sampleDocs: Array<{ _source: Record }> +) => { + return simulation.docs.map((doc, id) => { + // If every processor was successful, return and flatten the simulation doc from the last processor + if (isSuccessfulDocument(doc)) { + return { + value: flattenObject(doc.processor_results.at(-1)?.doc?._source ?? sampleDocs[id]._source), + isMatch: true, + }; + } + + return { + value: flattenObject(sampleDocs[id]._source), + isMatch: false, + }; + }); +}; + +const computeDetectedFields = ( + simulation: IngestSimulateResponse, + sampleDocs: Array<{ _source: Record }> +): Array<{ + name: string; + type: FieldDefinitionConfig['type'] | 'unmapped'; +}> => { + // Since we filter out failed documents, we need to map the simulation docs to the sample docs for later retrieval + const samplesToSimulationMap = new Map(simulation.docs.map((doc, id) => [doc, sampleDocs[id]])); + + const diffs = simulation.docs + .filter(isSuccessfulDocument) + .map((doc) => { + const sample = samplesToSimulationMap.get(doc); + if (sample) { + const { added } = calculateObjectDiff( + sample._source, + doc.processor_results.at(-1)?.doc?._source + ); + return flattenObject(added); + } + + return {}; + }) + .map(Object.keys) + .flat(); + + const uniqueFields = [...new Set(diffs)]; + + return uniqueFields.map((name) => ({ name, type: 'unmapped' })); +}; + +const computeSuccessRate = (simulation: IngestSimulateResponse) => { + const successfulCount = simulation.docs.reduce((rate, doc) => { + return (rate += isSuccessfulDocument(doc) ? 1 : 0); + }, 0); + return successfulCount / simulation.docs.length; +}; + +const isSuccessfulDocument = ( + doc: IngestSimulateSimulateDocumentResult +): doc is Required => + doc.processor_results?.every((processorSimulation) => processorSimulation.status === 'success') || + false; diff --git a/x-pack/solutions/observability/plugins/streams/server/routes/streams/sample.ts b/x-pack/solutions/observability/plugins/streams/server/routes/streams/sample.ts index 1e4508149f4e7..7716865d73bb9 100644 --- a/x-pack/solutions/observability/plugins/streams/server/routes/streams/sample.ts +++ b/x-pack/solutions/observability/plugins/streams/server/routes/streams/sample.ts @@ -8,12 +8,12 @@ import { z } from '@kbn/zod'; import { notFound, internal } from '@hapi/boom'; import { errors } from '@elastic/elasticsearch'; -import { conditionSchema } from '@kbn/streams-schema'; +import { conditionSchema, isCompleteCondition } from '@kbn/streams-schema'; import { createServerRoute } from '../create_server_route'; import { DefinitionNotFound } from '../../lib/streams/errors'; import { checkAccess } from '../../lib/streams/stream_crud'; import { conditionToQueryDsl } from '../../lib/streams/helpers/condition_to_query_dsl'; -import { getFields, isComplete } from '../../lib/streams/helpers/condition_fields'; +import { getFields } from '../../lib/streams/helpers/condition_fields'; export const sampleStreamRoute = createServerRoute({ endpoint: 'POST /api/streams/{id}/_sample', @@ -48,7 +48,7 @@ export const sampleStreamRoute = createServerRoute({ query: { bool: { must: [ - isComplete(params.body.condition) + isCompleteCondition(params.body.condition) ? conditionToQueryDsl(params.body.condition) : { match_all: {} }, { diff --git a/x-pack/solutions/observability/plugins/streams/tsconfig.json b/x-pack/solutions/observability/plugins/streams/tsconfig.json index 27743fbd9f70c..f7fb1f0a2d1b2 100644 --- a/x-pack/solutions/observability/plugins/streams/tsconfig.json +++ b/x-pack/solutions/observability/plugins/streams/tsconfig.json @@ -28,6 +28,7 @@ "@kbn/encrypted-saved-objects-plugin", "@kbn/licensing-plugin", "@kbn/server-route-repository-client", + "@kbn/object-utils", "@kbn/observability-utils-server", "@kbn/observability-utils-common", "@kbn/alerting-plugin", diff --git a/x-pack/solutions/observability/plugins/streams_app/.storybook/get_mock_streams_app_context.tsx b/x-pack/solutions/observability/plugins/streams_app/.storybook/get_mock_streams_app_context.tsx index 8e5dad100fc31..c684e65b567f4 100644 --- a/x-pack/solutions/observability/plugins/streams_app/.storybook/get_mock_streams_app_context.tsx +++ b/x-pack/solutions/observability/plugins/streams_app/.storybook/get_mock_streams_app_context.tsx @@ -17,9 +17,11 @@ import type { SavedObjectTaggingPluginStart } from '@kbn/saved-objects-tagging-p import type { StreamsAppKibanaContext } from '../public/hooks/use_kibana'; export function getMockStreamsAppContext(): StreamsAppKibanaContext { + const appParams = coreMock.createAppMountParameters(); const core = coreMock.createStart(); return { + appParams, core, dependencies: { start: { diff --git a/x-pack/solutions/observability/plugins/streams_app/public/components/app_root/index.tsx b/x-pack/solutions/observability/plugins/streams_app/public/components/app_root/index.tsx index c5e8b78ae2155..bbe3e50cf1710 100644 --- a/x-pack/solutions/observability/plugins/streams_app/public/components/app_root/index.tsx +++ b/x-pack/solutions/observability/plugins/streams_app/public/components/app_root/index.tsx @@ -33,6 +33,7 @@ export function AppRoot({ const { history } = appMountParameters; const context = { + appParams: appMountParameters, core: coreStart, dependencies: { start: pluginsStart, diff --git a/x-pack/solutions/observability/plugins/streams_app/public/components/asset_image/index.tsx b/x-pack/solutions/observability/plugins/streams_app/public/components/asset_image/index.tsx new file mode 100644 index 0000000000000..cc9ee24e7ef95 --- /dev/null +++ b/x-pack/solutions/observability/plugins/streams_app/public/components/asset_image/index.tsx @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiImage, EuiImageProps, useEuiTheme } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React, { useEffect } from 'react'; +import { useState } from 'react'; + +const imageSets = { + welcome: { + light: import('./welcome_light.png'), + dark: import('./welcome_dark.png'), + alt: i18n.translate('xpack.streams.streamDetailView.welcomeImage', { + defaultMessage: 'Welcome image for the streams app', + }), + }, + noResults: { + light: import('./no_results_light.png'), + dark: import('./no_results_dark.png'), + alt: i18n.translate('xpack.streams.streamDetailView.noResultsImage', { + defaultMessage: 'No results image for the streams app', + }), + }, +}; + +interface AssetImageProps extends Omit { + type?: keyof typeof imageSets; +} + +export function AssetImage({ type = 'welcome', ...props }: AssetImageProps) { + const { colorMode } = useEuiTheme(); + const { alt, dark, light } = imageSets[type]; + + const [imageSrc, setImageSrc] = useState(); + + useEffect(() => { + const dynamicImageImport = colorMode === 'LIGHT' ? light : dark; + + dynamicImageImport.then((module) => setImageSrc(module.default)); + }, [colorMode, dark, light]); + + return imageSrc ? : null; +} diff --git a/x-pack/solutions/observability/plugins/streams_app/public/components/asset_image/no_results_dark.png b/x-pack/solutions/observability/plugins/streams_app/public/components/asset_image/no_results_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..4ee7c1645aeb3591081e9b3a70ba381999a49ced GIT binary patch literal 37719 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 literal 0 HcmV?d00001 diff --git a/x-pack/solutions/observability/plugins/streams_app/public/components/asset_image/no_results_light.png b/x-pack/solutions/observability/plugins/streams_app/public/components/asset_image/no_results_light.png new file mode 100644 index 0000000000000000000000000000000000000000..d53e0dfde35a6d16ae2919af4774f8f0aa0b9c6f GIT binary patch literal 37158 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 D&efKy literal 0 HcmV?d00001 diff --git a/x-pack/solutions/observability/plugins/streams_app/public/components/asset_image/welcome_dark.png b/x-pack/solutions/observability/plugins/streams_app/public/components/asset_image/welcome_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..d324a8bb4c5718d152f113b674fd5c2d925f267c GIT binary patch literal 48993 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%0DfQKmKB6yQbhVH6po8a( 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 F{{c-&&&dD) literal 0 HcmV?d00001 diff --git a/x-pack/solutions/observability/plugins/streams_app/public/components/asset_image/welcome_light.png b/x-pack/solutions/observability/plugins/streams_app/public/components/asset_image/welcome_light.png new file mode 100644 index 0000000000000000000000000000000000000000..06f4a7154655096f90f4b76a01998d445ec45c91 GIT binary patch literal 50657 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 JM(J(j{{WPBV(|a~ diff --git a/x-pack/solutions/observability/plugins/streams_app/public/components/condition_editor/index.tsx b/x-pack/solutions/observability/plugins/streams_app/public/components/condition_editor/index.tsx index e53c9f9069797..0b08ca68dc6f7 100644 --- a/x-pack/solutions/observability/plugins/streams_app/public/components/condition_editor/index.tsx +++ b/x-pack/solutions/observability/plugins/streams_app/public/components/condition_editor/index.tsx @@ -175,11 +175,8 @@ function FilterForm(props: { }; const newOperator = e.target.value as FilterCondition['operator']; - if ( - 'value' in newCondition && - (newOperator === 'exists' || newOperator === 'notExists') - ) { - delete newCondition.value; + if (newOperator === 'exists' || newOperator === 'notExists') { + if ('value' in newCondition) delete newCondition.value; } else if (!('value' in newCondition)) { (newCondition as BinaryFilterCondition).value = ''; } diff --git a/x-pack/solutions/observability/plugins/streams_app/public/components/management_bottom_bar/index.tsx b/x-pack/solutions/observability/plugins/streams_app/public/components/management_bottom_bar/index.tsx new file mode 100644 index 0000000000000..6e5086ee026c4 --- /dev/null +++ b/x-pack/solutions/observability/plugins/streams_app/public/components/management_bottom_bar/index.tsx @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiBottomBar, EuiButton, EuiButtonEmpty, EuiFlexGroup } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { useDiscardConfirm } from '../../hooks/use_discard_confirm'; + +interface ManagementBottomBarProps { + confirmButtonText?: string; + disabled?: boolean; + isLoading?: boolean; + onCancel: () => void; + onConfirm: () => void; +} + +export function ManagementBottomBar({ + confirmButtonText = defaultConfirmButtonText, + disabled = false, + isLoading = false, + onCancel, + onConfirm, +}: ManagementBottomBarProps) { + const handleCancel = useDiscardConfirm(onCancel); + + return ( + + + + {i18n.translate('xpack.streams.streamDetailView.managementTab.bottomBar.cancel', { + defaultMessage: 'Cancel changes', + })} + + + + {confirmButtonText} + + + + ); +} + +const defaultConfirmButtonText = i18n.translate( + 'xpack.streams.streamDetailView.managementTab.bottomBar.confirm', + { defaultMessage: 'Save changes' } +); diff --git a/x-pack/solutions/observability/plugins/streams_app/public/components/preview_table/index.tsx b/x-pack/solutions/observability/plugins/streams_app/public/components/preview_table/index.tsx index d200e6b3b40be..e98134c42935b 100644 --- a/x-pack/solutions/observability/plugins/streams_app/public/components/preview_table/index.tsx +++ b/x-pack/solutions/observability/plugins/streams_app/public/components/preview_table/index.tsx @@ -6,21 +6,23 @@ */ import { EuiDataGrid } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import React, { useEffect, useMemo, useState } from 'react'; +import React, { CSSProperties, useEffect, useMemo, useState } from 'react'; export function PreviewTable({ documents, displayColumns, + height, }: { documents: unknown[]; displayColumns?: string[]; + height?: CSSProperties['height']; }) { - const [height, setHeight] = useState('100px'); + const [computedHeight, setComputedHeight] = useState('100px'); useEffect(() => { // set height to 100% after a short delay otherwise it doesn't calculate correctly // TODO: figure out a better way to do this setTimeout(() => { - setHeight(`100%`); + setComputedHeight(`100%`); }, 50); }, []); @@ -59,7 +61,7 @@ export function PreviewTable({ }} toolbarVisibility={false} rowCount={documents.length} - height={height} + height={height ?? computedHeight} renderCellValue={({ rowIndex, columnId }) => { const doc = documents[rowIndex]; if (!doc || typeof doc !== 'object') { diff --git a/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_enriching/index.tsx b/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_enriching/index.tsx deleted file mode 100644 index fe532825d970a..0000000000000 --- a/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_enriching/index.tsx +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import { StreamDefinition } from '@kbn/streams-schema'; -import React from 'react'; - -export function StreamDetailEnriching({ - definition: _definition, - refreshDefinition: _refreshDefinition, -}: { - definition?: StreamDefinition; - refreshDefinition: () => void; -}) { - return <>{'TODO'}; -} diff --git a/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_enrichment/add_processor_button.tsx b/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_enrichment/add_processor_button.tsx new file mode 100644 index 0000000000000..34219c9b83781 --- /dev/null +++ b/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_enrichment/add_processor_button.tsx @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiButton } from '@elastic/eui'; +import { EuiButtonPropsForButton } from '@elastic/eui/src/components/button/button'; +import { i18n } from '@kbn/i18n'; + +export function AddProcessorButton(props: EuiButtonPropsForButton) { + return ( + + {i18n.translate( + 'xpack.streams.streamDetailView.managementTab.enrichmentEmptyPrompt.addProcessorAction', + { defaultMessage: 'Add a processor' } + )} + + ); +} diff --git a/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_enrichment/enrichment_empty_prompt.tsx b/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_enrichment/enrichment_empty_prompt.tsx new file mode 100644 index 0000000000000..96c8bddb2c822 --- /dev/null +++ b/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_enrichment/enrichment_empty_prompt.tsx @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiEmptyPrompt } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { AssetImage } from '../asset_image'; +import { AddProcessorButton } from './add_processor_button'; + +interface EnrichmentEmptyPromptProps { + onAddProcessor: () => void; +} + +export const EnrichmentEmptyPrompt = ({ onAddProcessor }: EnrichmentEmptyPromptProps) => { + return ( + } + title={title} + body={body} + actions={[]} + /> + ); +}; + +const title = ( +

+ {i18n.translate('xpack.streams.streamDetailView.managementTab.enrichmentEmptyPrompt.title', { + defaultMessage: 'Start extracting useful fields from your data', + })} +

+); + +const body = ( +

+ {i18n.translate('xpack.streams.streamDetailView.managementTab.enrichmentEmptyPrompt.body', { + defaultMessage: 'Use processors to transform data before indexing', + })} +

+); diff --git a/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_enrichment/flyout/danger_zone.tsx b/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_enrichment/flyout/danger_zone.tsx new file mode 100644 index 0000000000000..5f8d44d0d8962 --- /dev/null +++ b/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_enrichment/flyout/danger_zone.tsx @@ -0,0 +1,89 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { + EuiPanel, + EuiTitle, + EuiSpacer, + useGeneratedHtmlId, + EuiButton, + EuiConfirmModal, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { useBoolean } from '@kbn/react-hooks'; + +export const DangerZone = ({ + onDeleteProcessor, +}: Pick) => { + return ( + + +

+ {i18n.translate( + 'xpack.streams.streamDetailView.managementTab.enrichment.processorFlyout.dangerAreaTitle', + { defaultMessage: 'Danger area' } + )} +

+
+ + +
+ ); +}; + +interface DeleteProcessorButtonProps { + onDeleteProcessor: () => void; +} + +const DeleteProcessorButton = ({ onDeleteProcessor }: DeleteProcessorButtonProps) => { + const [isConfirmModalOpen, { on: openConfirmModal, off: closeConfirmModal }] = useBoolean(); + const confirmModalId = useGeneratedHtmlId(); + + return ( + <> + + {i18n.translate( + 'xpack.streams.streamDetailView.managementTab.enrichment.processorFlyout.dangerAreaTitle', + { defaultMessage: 'Delete processor' } + )} + + {isConfirmModalOpen && ( + +

+ {i18n.translate( + 'xpack.streams.streamDetailView.managementTab.enrichment.processorFlyout.deleteProcessorModalBody', + { + defaultMessage: + 'You can still reset this until the changes are confirmed on the processors list.', + } + )} +

+
+ )} + + ); +}; diff --git a/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_enrichment/flyout/dissect/dissect_append_separator.tsx b/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_enrichment/flyout/dissect/dissect_append_separator.tsx new file mode 100644 index 0000000000000..dc6bf2dbb39cb --- /dev/null +++ b/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_enrichment/flyout/dissect/dissect_append_separator.tsx @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { useFormContext } from 'react-hook-form'; +import { EuiCode, EuiFieldText, EuiFormRow } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; + +export const DissectAppendSeparator = () => { + const { register } = useFormContext(); + const { ref, ...inputProps } = register(`append_separator`); + + return ( + "" }} + /> + } + fullWidth + > + + + ); +}; diff --git a/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_enrichment/flyout/dissect/dissect_pattern_definition.tsx b/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_enrichment/flyout/dissect/dissect_pattern_definition.tsx new file mode 100644 index 0000000000000..751a522c0610b --- /dev/null +++ b/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_enrichment/flyout/dissect/dissect_pattern_definition.tsx @@ -0,0 +1,79 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { useController } from 'react-hook-form'; +import { EuiFormRow, EuiLink } from '@elastic/eui'; +import { CodeEditor } from '@kbn/code-editor'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { useKibana } from '../../../../hooks/use_kibana'; + +export const DissectPatternDefinition = () => { + const { core } = useKibana(); + const esDocUrl = core.docLinks.links.ingest.dissectKeyModifiers; + + const { field, fieldState } = useController({ name: 'pattern' }); + + return ( + + {i18n.translate( + 'xpack.streams.streamDetailView.managementTab.enrichment.processorFlyout.dissectPatternDefinitionsLink', + { defaultMessage: 'key modifier' } + )} + + ), + }} + /> + } + isInvalid={fieldState.invalid} + fullWidth + > + field.onChange(deserialize(value))} + languageId="text" + height={75} + options={{ minimap: { enabled: false } }} + aria-label={i18n.translate( + 'xpack.streams.streamDetailView.managementTab.enrichment.processorFlyout.dissectPatternDefinitionsAriaLabel', + { defaultMessage: 'Pattern editor' } + )} + /> + + ); +}; + +const serialize = (input: string) => { + if (typeof input === 'string') { + const s = JSON.stringify(input); + return s.slice(1, s.length - 1); + } + + return input; +}; + +const deserialize = (input: string) => { + if (typeof input === 'string') { + try { + return JSON.parse(`"${input}"`); + } catch (e) { + return input; + } + } +}; diff --git a/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_enrichment/flyout/dissect/index.tsx b/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_enrichment/flyout/dissect/index.tsx new file mode 100644 index 0000000000000..b88ac46d65789 --- /dev/null +++ b/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_enrichment/flyout/dissect/index.tsx @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiSpacer } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { DissectAppendSeparator } from './dissect_append_separator'; +import { DissectPatternDefinition } from './dissect_pattern_definition'; +import { ProcessorFieldSelector } from '../processor_field_selector'; +import { ToggleField } from '../toggle_field'; +import { OptionalFieldsAccordion } from '../optional_fields_accordion'; +import { ProcessorConditionEditor } from '../processor_condition_editor'; + +export const DissectProcessorForm = () => { + return ( + <> + + + + + + + + + + + + + ); +}; diff --git a/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_enrichment/flyout/grok/grok_pattern_definition.tsx b/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_enrichment/flyout/grok/grok_pattern_definition.tsx new file mode 100644 index 0000000000000..1cfa43a904867 --- /dev/null +++ b/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_enrichment/flyout/grok/grok_pattern_definition.tsx @@ -0,0 +1,92 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { useController } from 'react-hook-form'; +import { EuiFormRow } from '@elastic/eui'; +import { CodeEditor } from '@kbn/code-editor'; +import { i18n } from '@kbn/i18n'; + +export const GrokPatternDefinition = () => { + const { field, fieldState } = useController({ name: 'pattern_definitions' }); + + return ( + + field.onChange(deserialize(value))} + languageId="xjson" + height={200} + aria-label={i18n.translate( + 'xpack.streams.streamDetailView.managementTab.enrichment.processorFlyout.grokPatternDefinitionsAriaLabel', + { defaultMessage: 'Pattern definitions editor' } + )} + /> + + ); +}; + +const serialize = (v: unknown) => { + if (!v) { + return '{}'; + } + if (typeof v === 'string') { + return formatXJsonString(v); + } + return JSON.stringify(v, null, 2); +}; + +const deserialize = (input: string) => { + try { + return JSON.parse(input); + } catch (e) { + return input; + } +}; + +/** + * Format a XJson string input as parsed JSON. Replaces the invalid characters + * with a placeholder, parses the new string in a JSON format with the expected + * indentantion and then replaces the placeholders with the original values. + */ +const formatXJsonString = (input: string) => { + let placeholder = 'PLACEHOLDER'; + const INVALID_STRING_REGEX = /"""(.*?)"""/gs; + while (input.includes(placeholder)) { + placeholder += '_'; + } + const modifiedInput = input.replace(INVALID_STRING_REGEX, () => `"${placeholder}"`); + + let jsonObject; + try { + jsonObject = JSON.parse(modifiedInput); + } catch (error) { + return input; + } + let formattedJsonString = JSON.stringify(jsonObject, null, 2); + const invalidStrings = input.match(INVALID_STRING_REGEX); + if (invalidStrings) { + invalidStrings.forEach((invalidString) => { + formattedJsonString = formattedJsonString.replace(`"${placeholder}"`, invalidString); + }); + } + return formattedJsonString; +}; diff --git a/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_enrichment/flyout/grok/grok_patterns_editor.tsx b/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_enrichment/flyout/grok/grok_patterns_editor.tsx new file mode 100644 index 0000000000000..efefc6870d5b4 --- /dev/null +++ b/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_enrichment/flyout/grok/grok_patterns_editor.tsx @@ -0,0 +1,128 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { useFormContext, useFieldArray, UseFormRegisterReturn } from 'react-hook-form'; +import { + DragDropContextProps, + EuiFormRow, + EuiPanel, + EuiSpacer, + EuiButtonEmpty, + EuiDraggable, + EuiFlexGroup, + EuiIcon, + EuiFieldText, + EuiButtonIcon, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { SortableList } from '../../sortable_list'; +import { GrokFormState } from '../../types'; + +export const GrokPatternsEditor = () => { + const { register } = useFormContext(); + const { fields, append, remove, move } = useFieldArray>({ + name: 'patterns', + }); + + const handlerPatternDrag: DragDropContextProps['onDragEnd'] = ({ source, destination }) => { + if (source && destination) { + move(source.index, destination.index); + } + }; + + const handleAddPattern = () => { + append({ value: '' }); + }; + + const getRemovePatternHandler = (id: number) => (fields.length > 1 ? () => remove(id) : null); + + return ( + + + + {fields.map((field, idx) => ( + + ))} + + + + {i18n.translate( + 'xpack.streams.streamDetailView.managementTab.enrichment.processorFlyout.grokEditor.addPattern', + { defaultMessage: 'Add pattern' } + )} + + + + ); +}; + +interface DraggablePatternInputProps { + idx: number; + inputProps: UseFormRegisterReturn<`patterns.${number}.value`>; + onRemove: ((idx: number) => void) | null; + pattern: GrokFormState['patterns'][number] & { id: string }; +} + +const DraggablePatternInput = ({ + idx, + inputProps, + onRemove, + pattern, +}: DraggablePatternInputProps) => { + const { ref, ...inputPropsWithoutRef } = inputProps; + + return ( + + {(provided) => ( + + + + + + {onRemove && ( + onRemove(idx)} + aria-label={i18n.translate( + 'xpack.streams.streamDetailView.managementTab.enrichment.processorFlyout.grokEditor.removePattern', + { defaultMessage: 'Remove grok pattern' } + )} + /> + )} + + )} + + ); +}; diff --git a/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_enrichment/flyout/grok/index.tsx b/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_enrichment/flyout/grok/index.tsx new file mode 100644 index 0000000000000..0adfe8faa4dfc --- /dev/null +++ b/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_enrichment/flyout/grok/index.tsx @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiSpacer } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { GrokPatternDefinition } from './grok_pattern_definition'; +import { GrokPatternsEditor } from './grok_patterns_editor'; +import { ProcessorFieldSelector } from '../processor_field_selector'; +import { ToggleField } from '../toggle_field'; +import { OptionalFieldsAccordion } from '../optional_fields_accordion'; +import { ProcessorConditionEditor } from '../processor_condition_editor'; + +export const GrokProcessorForm = () => { + return ( + <> + + + + + + + + + + + + + ); +}; diff --git a/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_enrichment/flyout/index.tsx b/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_enrichment/flyout/index.tsx new file mode 100644 index 0000000000000..d8f0d2f0c5ffa --- /dev/null +++ b/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_enrichment/flyout/index.tsx @@ -0,0 +1,169 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useMemo } from 'react'; +import { FormProvider, SubmitHandler, useForm } from 'react-hook-form'; +import { EuiCallOut, EuiForm, EuiButton, EuiSpacer, EuiHorizontalRule } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { ProcessingDefinition, ReadStreamDefinition, getProcessorType } from '@kbn/streams-schema'; +import { isEqual } from 'lodash'; +import { dynamic } from '@kbn/shared-ux-utility'; +import { ProcessorTypeSelector } from './processor_type_selector'; +import { ProcessorFlyoutTemplate } from './processor_flyout_template'; +import { DetectedField, ProcessorDefinition, ProcessorFormState } from '../types'; +import { DangerZone } from './danger_zone'; +import { DissectProcessorForm } from './dissect'; +import { GrokProcessorForm } from './grok'; +import { convertFormStateToProcessing, getDefaultFormState } from '../utils'; + +const ProcessorOutcomePreview = dynamic(() => + import(/* webpackChunkName: "management_processor_outcome" */ './processor_outcome_preview').then( + (mod) => ({ + default: mod.ProcessorOutcomePreview, + }) + ) +); + +export interface ProcessorFlyoutProps { + onClose: () => void; +} + +export interface AddProcessorFlyoutProps extends ProcessorFlyoutProps { + definition: ReadStreamDefinition; + onAddProcessor: (newProcessing: ProcessingDefinition, newFields?: DetectedField[]) => void; +} +export interface EditProcessorFlyoutProps extends ProcessorFlyoutProps { + processor: ProcessorDefinition; + onDeleteProcessor: (id: string) => void; + onUpdateProcessor: (id: string, processor: ProcessorDefinition) => void; +} + +export function AddProcessorFlyout({ + definition, + onAddProcessor, + onClose, +}: AddProcessorFlyoutProps) { + const defaultValues = useMemo(() => getDefaultFormState('grok'), []); + + const methods = useForm({ defaultValues }); + + const formFields = methods.watch(); + + const hasChanges = useMemo( + () => !isEqual(defaultValues, formFields), + [defaultValues, formFields] + ); + + const handleSubmit: SubmitHandler = (data) => { + const processingDefinition = convertFormStateToProcessing(data); + + onAddProcessor(processingDefinition, data.detected_fields); + onClose(); + }; + + return ( + + {i18n.translate( + 'xpack.streams.streamDetailView.managementTab.enrichment.processorFlyout.confirmAddProcessor', + { defaultMessage: 'Add processor' } + )} + + } + > + + + + + {formFields.type === 'grok' && } + {formFields.type === 'dissect' && } + + + + + + ); +} + +export function EditProcessorFlyout({ + onClose, + onDeleteProcessor, + onUpdateProcessor, + processor, +}: EditProcessorFlyoutProps) { + const defaultValues = useMemo( + () => getDefaultFormState(getProcessorType(processor), processor), + [processor] + ); + + const methods = useForm({ defaultValues }); + + const formFields = methods.watch(); + + const hasChanges = useMemo( + () => !isEqual(defaultValues, formFields), + [defaultValues, formFields] + ); + + const handleSubmit: SubmitHandler = (data) => { + const processingDefinition = convertFormStateToProcessing(data); + + onUpdateProcessor(processor.id, { id: processor.id, ...processingDefinition }); + onClose(); + }; + + const handleProcessorDelete = () => { + onDeleteProcessor(processor.id); + onClose(); + }; + + return ( + + } + confirmButton={ + + {i18n.translate( + 'xpack.streams.streamDetailView.managementTab.enrichment.processorFlyout.confirmEditProcessor', + { defaultMessage: 'Update processor' } + )} + + } + > + + + + + {formFields.type === 'grok' && } + {formFields.type === 'dissect' && } + + + + + + ); +} diff --git a/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_enrichment/flyout/optional_fields_accordion.tsx b/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_enrichment/flyout/optional_fields_accordion.tsx new file mode 100644 index 0000000000000..ddba021d10106 --- /dev/null +++ b/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_enrichment/flyout/optional_fields_accordion.tsx @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { PropsWithChildren } from 'react'; +import { EuiAccordion, useEuiTheme } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { css } from '@emotion/react'; + +export const OptionalFieldsAccordion = ({ children }: PropsWithChildren) => { + const { euiTheme } = useEuiTheme(); + + return ( + +
+ {children} +
+
+ ); +}; diff --git a/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_enrichment/flyout/processor_condition_editor.tsx b/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_enrichment/flyout/processor_condition_editor.tsx new file mode 100644 index 0000000000000..51a57bf6b14df --- /dev/null +++ b/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_enrichment/flyout/processor_condition_editor.tsx @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { useController } from 'react-hook-form'; +import { ConditionEditor } from '../../condition_editor'; + +export const ProcessorConditionEditor = () => { + const { field } = useController({ name: 'condition' }); + + return ; +}; diff --git a/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_enrichment/flyout/processor_field_selector.tsx b/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_enrichment/flyout/processor_field_selector.tsx new file mode 100644 index 0000000000000..da4a455f894ba --- /dev/null +++ b/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_enrichment/flyout/processor_field_selector.tsx @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiFormRow, EuiFieldText } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { useFormContext } from 'react-hook-form'; + +export const ProcessorFieldSelector = () => { + const { register } = useFormContext(); + const { ref, ...inputProps } = register(`field`); + + return ( + + + + ); +}; diff --git a/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_enrichment/flyout/processor_flyout_template.tsx b/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_enrichment/flyout/processor_flyout_template.tsx new file mode 100644 index 0000000000000..b20df887fd223 --- /dev/null +++ b/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_enrichment/flyout/processor_flyout_template.tsx @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { PropsWithChildren } from 'react'; +import { + EuiFlyoutResizable, + EuiFlyoutHeader, + EuiTitle, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiFlexGroup, + EuiButtonEmpty, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { useDiscardConfirm } from '../../../hooks/use_discard_confirm'; + +interface ProcessorFlyoutTemplateProps { + banner?: React.ReactNode; + confirmButton?: React.ReactNode; + onClose: () => void; + shouldConfirm?: boolean; + title: string; +} + +export function ProcessorFlyoutTemplate({ + banner, + children, + confirmButton, + onClose, + shouldConfirm = false, + title, +}: PropsWithChildren) { + const handleClose = useDiscardConfirm(onClose); + + const closeHandler = shouldConfirm ? handleClose : onClose; + + return ( + + + +

{title}

+
+
+ {children} + + + + {i18n.translate( + 'xpack.streams.streamDetailView.managementTab.enrichment.processorFlyout.cancel', + { defaultMessage: 'Cancel' } + )} + + {confirmButton} + + +
+ ); +} diff --git a/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_enrichment/flyout/processor_outcome_preview.tsx b/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_enrichment/flyout/processor_outcome_preview.tsx new file mode 100644 index 0000000000000..45e47157dd69a --- /dev/null +++ b/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_enrichment/flyout/processor_outcome_preview.tsx @@ -0,0 +1,459 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useEffect, useMemo, useState } from 'react'; +import { useDateRange } from '@kbn/observability-utils-browser/hooks/use_date_range'; +import { + EuiPanel, + EuiTitle, + EuiSpacer, + EuiFlexGroup, + EuiFilterButton, + EuiFilterGroup, + EuiEmptyPrompt, + EuiLoadingLogo, + EuiButton, + EuiFormRow, + EuiSuperSelectOption, + EuiSuperSelect, + useEuiTheme, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { TimeRange } from '@kbn/es-query'; +import { isEmpty } from 'lodash'; +import { FieldIcon } from '@kbn/react-field'; +import { + FIELD_DEFINITION_TYPES, + ReadStreamDefinition, + isWiredReadStream, +} from '@kbn/streams-schema'; +import { useController, useFieldArray } from 'react-hook-form'; +import { css } from '@emotion/react'; +import { flattenObject } from '@kbn/object-utils'; +import { IHttpFetchError, ResponseErrorBody } from '@kbn/core/public'; +import { useStreamsAppFetch } from '../../../hooks/use_streams_app_fetch'; +import { useKibana } from '../../../hooks/use_kibana'; +import { StreamsAppSearchBar, StreamsAppSearchBarProps } from '../../streams_app_search_bar'; +import { PreviewTable } from '../../preview_table'; +import { convertFormStateToProcessing, isCompleteProcessingDefinition } from '../utils'; +import { DetectedField, ProcessorFormState } from '../types'; + +interface ProcessorOutcomePreviewProps { + definition: ReadStreamDefinition; + formFields: ProcessorFormState; +} + +export const ProcessorOutcomePreview = ({ + definition, + formFields, +}: ProcessorOutcomePreviewProps) => { + const { dependencies } = useKibana(); + const { + data, + streams: { streamsRepositoryClient }, + } = dependencies.start; + + const { + timeRange, + absoluteTimeRange: { start, end }, + setTimeRange, + } = useDateRange({ data }); + + const [selectedDocsFilter, setSelectedDocsFilter] = + useState('outcome_filter_all'); + + const { + value: samples, + loading: isLoadingSamples, + refresh: refreshSamples, + } = useStreamsAppFetch( + ({ signal }) => { + if (!definition || !formFields.field) { + return { documents: [] }; + } + + return streamsRepositoryClient.fetch('POST /api/streams/{id}/_sample', { + signal, + params: { + path: { id: definition.name }, + body: { + condition: { field: formFields.field, operator: 'exists' }, + start: start?.valueOf(), + end: end?.valueOf(), + number: 100, + }, + }, + }); + }, + [definition, formFields.field, streamsRepositoryClient, start, end], + { disableToastOnError: true } + ); + + const { + value: simulation, + loading: isLoadingSimulation, + error, + refresh: refreshSimulation, + } = useStreamsAppFetch( + async ({ signal }) => { + if (!definition || !samples || isEmpty(samples.documents)) { + return Promise.resolve(null); + } + + const processingDefinition = convertFormStateToProcessing(formFields); + + if (!isCompleteProcessingDefinition(processingDefinition)) { + return Promise.resolve(null); + } + + const simulationResult = await streamsRepositoryClient.fetch( + 'POST /api/streams/{id}/processing/_simulate', + { + signal, + params: { + path: { id: definition.name }, + body: { + documents: samples.documents as Array>, + processing: [processingDefinition], + }, + }, + } + ); + + return simulationResult; + }, + [definition, samples, streamsRepositoryClient], + { disableToastOnError: true } + ); + + const simulationError = error as IHttpFetchError | undefined; + + const simulationDocuments = useMemo(() => { + if (!simulation?.documents) { + const docs = (samples?.documents ?? []) as Array>; + return docs.map((doc) => flattenObject(doc)); + } + + const filterDocuments = (filter: DocsFilterOption) => { + switch (filter) { + case 'outcome_filter_matched': + return simulation.documents.filter((doc) => doc.isMatch); + case 'outcome_filter_unmatched': + return simulation.documents.filter((doc) => !doc.isMatch); + case 'outcome_filter_all': + default: + return simulation.documents; + } + }; + + return filterDocuments(selectedDocsFilter).map((doc) => doc.value); + }, [samples?.documents, simulation?.documents, selectedDocsFilter]); + + const detectedFieldsColumns = simulation?.detected_fields + ? simulation.detected_fields.map((field) => field.name) + : []; + + const detectedFieldsEnabled = + isWiredReadStream(definition) && simulation && !isEmpty(simulation.detected_fields); + + return ( + + + +

+ {i18n.translate( + 'xpack.streams.streamDetailView.managementTab.enrichment.processorFlyout.outcomeTitle', + { defaultMessage: 'Outcome' } + )} +

+
+ + {i18n.translate( + 'xpack.streams.streamDetailView.managementTab.enrichment.processorFlyout.runSimulation', + { defaultMessage: 'Run simulation' } + )} + +
+ + {detectedFieldsEnabled && } + + + +
+ ); +}; + +const docsFilterOptions = { + outcome_filter_all: { + id: 'outcome_filter_all', + label: i18n.translate( + 'xpack.streams.streamDetailView.managementTab.enrichment.processorFlyout.outcomeControls.all', + { defaultMessage: 'All samples' } + ), + }, + outcome_filter_matched: { + id: 'outcome_filter_matched', + label: i18n.translate( + 'xpack.streams.streamDetailView.managementTab.enrichment.processorFlyout.outcomeControls.matched', + { defaultMessage: 'Matched' } + ), + }, + outcome_filter_unmatched: { + id: 'outcome_filter_unmatched', + label: i18n.translate( + 'xpack.streams.streamDetailView.managementTab.enrichment.processorFlyout.outcomeControls.unmatched', + { defaultMessage: 'Unmatched' } + ), + }, +} as const; + +type DocsFilterOption = keyof typeof docsFilterOptions; + +interface OutcomeControlsProps { + docsFilter: DocsFilterOption; + timeRange: TimeRange; + onDocsFilterChange: (filter: DocsFilterOption) => void; + onTimeRangeChange: (timeRange: TimeRange) => void; + onTimeRangeRefresh: () => void; + simulationFailureRate?: number; + simulationSuccessRate?: number; +} + +const OutcomeControls = ({ + docsFilter, + timeRange, + onDocsFilterChange, + onTimeRangeChange, + onTimeRangeRefresh, + simulationFailureRate, + simulationSuccessRate, +}: OutcomeControlsProps) => { + const handleQuerySubmit: StreamsAppSearchBarProps['onQuerySubmit'] = ( + { dateRange }, + isUpdate + ) => { + if (!isUpdate) { + return onTimeRangeRefresh(); + } + + if (dateRange) { + onTimeRangeChange({ + from: dateRange.from, + to: dateRange?.to, + mode: dateRange.mode, + }); + } + }; + + return ( + + + onDocsFilterChange(docsFilterOptions.outcome_filter_all.id)} + > + {docsFilterOptions.outcome_filter_all.label} + + onDocsFilterChange(docsFilterOptions.outcome_filter_matched.id)} + badgeColor="success" + numActiveFilters={ + simulationSuccessRate ? parseFloat((simulationSuccessRate * 100).toFixed(2)) : undefined + } + > + {docsFilterOptions.outcome_filter_matched.label} + + onDocsFilterChange(docsFilterOptions.outcome_filter_unmatched.id)} + badgeColor="accent" + numActiveFilters={ + simulationFailureRate ? parseFloat((simulationFailureRate * 100).toFixed(2)) : undefined + } + > + {docsFilterOptions.outcome_filter_unmatched.label} + + + + + ); +}; + +const DetectedFields = ({ detectedFields }: { detectedFields: DetectedField[] }) => { + const { euiTheme } = useEuiTheme(); + const { fields, replace } = useFieldArray<{ detected_fields: DetectedField[] }>({ + name: 'detected_fields', + }); + + useEffect(() => { + replace(detectedFields); + }, [detectedFields, replace]); + + return ( + + + {fields.map((field, id) => ( + + ))} + + + ); +}; + +const DetectedFieldSelector = ({ selectorId }: { selectorId: string }) => { + const { field } = useController({ name: selectorId }); + + const options = useMemo(() => getDetectedFieldSelectOptions(field.value), [field]); + + return ( + field.onChange({ ...field.value, type })} + css={css` + min-inline-size: 180px; + `} + /> + ); +}; + +const getDetectedFieldSelectOptions = ( + fieldValue: DetectedField +): Array> => + [...FIELD_DEFINITION_TYPES, 'unmapped'].map((type) => ({ + value: type, + inputDisplay: ( + + + {fieldValue.name} + + ), + dropdownDisplay: ( + + + {type} + + ), + })); + +interface OutcomePreviewTableProps { + documents?: Array>; + columns: string[]; + error?: IHttpFetchError; + isLoading?: boolean; +} + +const OutcomePreviewTable = ({ + documents = [], + columns, + error, + isLoading, +}: OutcomePreviewTableProps) => { + if (error) { + return ( + + {i18n.translate( + 'xpack.streams.streamDetailView.managementTab.enrichment.processorFlyout.outcomePreviewTable.errorTitle', + { defaultMessage: 'Unable to display the simulation outcome for this processor.' } + )} + + } + body={ + <> +

+ {i18n.translate( + 'xpack.streams.streamDetailView.managementTab.enrichment.processorFlyout.outcomePreviewTable.errorBody', + { defaultMessage: 'The processor did not run correctly.' } + )} +

+ {error.body?.message ?

{error.body.message}

: null} + + } + /> + ); + } + + if (isLoading) { + return ( + } + title={ +

+ {i18n.translate( + 'xpack.streams.streamDetailView.managementTab.enrichment.processorFlyout.outcomePreviewTable.loadingTitle', + { defaultMessage: 'Running processor simulation' } + )} +

+ } + /> + ); + } + + if (documents?.length === 0) { + return ( + + {i18n.translate( + 'xpack.streams.streamDetailView.managementTab.enrichment.processorFlyout.outcomePreviewTable.noDataTitle', + { + defaultMessage: + 'There are no simulation outcome documents for the current selection.', + } + )} +

+ } + /> + ); + } + + return ; +}; diff --git a/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_enrichment/flyout/processor_type_selector.tsx b/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_enrichment/flyout/processor_type_selector.tsx new file mode 100644 index 0000000000000..9f464c3f33319 --- /dev/null +++ b/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_enrichment/flyout/processor_type_selector.tsx @@ -0,0 +1,108 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiLink, EuiFormRow, EuiSuperSelect, EuiSuperSelectProps } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { useController, useFormContext, useWatch } from 'react-hook-form'; +import { ProcessorType } from '@kbn/streams-schema'; +import { useKibana } from '../../../hooks/use_kibana'; +import { getDefaultFormState } from '../utils'; + +interface TAvailableProcessor { + value: ProcessorType; + inputDisplay: string; + getDocUrl: (esDocUrl: string) => React.ReactNode; +} + +type TAvailableProcessors = Record; + +export const ProcessorTypeSelector = ({ + disabled = false, +}: Pick) => { + const { core } = useKibana(); + const esDocUrl = core.docLinks.links.elasticsearch.docsBase; + + const { control, reset } = useFormContext(); + const { field, fieldState } = useController({ name: 'type', control, rules: { required: true } }); + + const processorType = useWatch<{ type: ProcessorType }>({ name: 'type' }); + + const handleChange = (type: ProcessorType) => { + const formState = getDefaultFormState(type); + reset(formState); + }; + + return ( + + + + ); +}; + +const availableProcessors: TAvailableProcessors = { + dissect: { + value: 'dissect', + inputDisplay: 'Dissect', + getDocUrl: (esDocUrl: string) => ( + + dissect + + ), + }} + /> + ), + }, + grok: { + value: 'grok', + inputDisplay: 'Grok', + getDocUrl: (esDocUrl: string) => ( + + grok + + ), + }} + /> + ), + }, +}; + +const getProcessorDescription = (esDocUrl: string) => (type: ProcessorType) => + availableProcessors[type].getDocUrl(esDocUrl); + +const processorTypeSelectorOptions = Object.values(availableProcessors).map( + ({ value, inputDisplay }) => ({ value, inputDisplay }) +); diff --git a/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_enrichment/flyout/toggle_field.tsx b/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_enrichment/flyout/toggle_field.tsx new file mode 100644 index 0000000000000..885934dc48933 --- /dev/null +++ b/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_enrichment/flyout/toggle_field.tsx @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { useController } from 'react-hook-form'; +import { EuiFormRow, EuiSwitch, htmlIdGenerator } from '@elastic/eui'; + +interface ToggleFieldProps { + helpText?: string; + id?: string; + label: string; + name: string; +} + +export const ToggleField = ({ + helpText, + id = createId(), + label, + name, + ...rest +}: ToggleFieldProps) => { + const { field } = useController({ name }); + + return ( + + field.onChange(e.target.checked)} + /> + + ); +}; + +const createId = htmlIdGenerator(); diff --git a/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_enrichment/hooks/use_definition.ts b/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_enrichment/hooks/use_definition.ts new file mode 100644 index 0000000000000..d5ca191bfb16a --- /dev/null +++ b/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_enrichment/hooks/use_definition.ts @@ -0,0 +1,162 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useState, useMemo, useEffect } from 'react'; +import { i18n } from '@kbn/i18n'; +import { useAbortController } from '@kbn/observability-utils-browser/hooks/use_abort_controller'; +import { useBoolean } from '@kbn/react-hooks'; +import { + ReadStreamDefinition, + ProcessingDefinition, + isWiredReadStream, + FieldDefinition, + WiredReadStreamDefinition, +} from '@kbn/streams-schema'; +import { htmlIdGenerator } from '@elastic/eui'; +import { isEqual } from 'lodash'; +import { DetectedField, ProcessorDefinition } from '../types'; +import { useKibana } from '../../../hooks/use_kibana'; + +export const useDefinition = (definition: ReadStreamDefinition, refreshDefinition: () => void) => { + const { core, dependencies } = useKibana(); + + const { toasts } = core.notifications; + const { processing } = definition.stream.ingest; + const { streamsRepositoryClient } = dependencies.start.streams; + + const abortController = useAbortController(); + const [isSavingChanges, { on: startsSaving, off: endsSaving }] = useBoolean(); + + const [processors, setProcessors] = useState(() => createProcessorsList(processing)); + const [fields, setFields] = useState(() => + isWiredReadStream(definition) ? definition.stream.ingest.wired.fields : {} + ); + + const httpProcessing = useMemo(() => processors.map(removeIdFromProcessor), [processors]); + + useEffect(() => { + // Reset processors when definition refreshes + setProcessors(createProcessorsList(definition.stream.ingest.processing)); + }, [definition]); + + const hasChanges = useMemo( + () => !isEqual(processing, httpProcessing), + [processing, httpProcessing] + ); + + const addProcessor = (newProcessing: ProcessingDefinition, newFields?: DetectedField[]) => { + setProcessors((prevProcs) => prevProcs.concat(createProcessorWithId(newProcessing))); + + if (isWiredReadStream(definition) && newFields) { + setFields((currentFields) => mergeFields(definition, currentFields, newFields)); + } + }; + + const updateProcessor = (id: string, processorUpdate: ProcessorDefinition) => { + setProcessors((prevProcs) => + prevProcs.map((proc) => (proc.id === id ? processorUpdate : proc)) + ); + }; + + const deleteProcessor = (id: string) => { + setProcessors((prevProcs) => prevProcs.filter((proc) => proc.id !== id)); + }; + + const resetChanges = () => { + setProcessors(createProcessorsList(processing)); + setFields(isWiredReadStream(definition) ? definition.stream.ingest.wired.fields : {}); + }; + + const saveChanges = async () => { + startsSaving(); + try { + await streamsRepositoryClient.fetch(`PUT /api/streams/{id}`, { + signal: abortController.signal, + params: { + path: { + id: definition.name, + }, + body: { + ingest: { + ...definition.stream.ingest, + processing: httpProcessing, + ...(isWiredReadStream(definition) && { wired: { fields } }), + }, + }, + }, + }); + + toasts.addSuccess( + i18n.translate( + 'xpack.streams.streamDetailView.managementTab.enrichment.saveChangesSuccess', + { defaultMessage: "Stream's processors updated" } + ) + ); + } catch (error) { + toasts.addError(error, { + title: i18n.translate( + 'xpack.streams.streamDetailView.managementTab.enrichment.saveChangesError', + { defaultMessage: "An issue occurred saving processors' changes." } + ), + toastMessage: error.body.message, + }); + } finally { + await refreshDefinition(); + endsSaving(); + } + }; + + return { + // Values + processors, + // Actions + addProcessor, + updateProcessor, + deleteProcessor, + resetChanges, + saveChanges, + setProcessors, + // Flags + hasChanges, + isSavingChanges, + }; +}; + +const createId = htmlIdGenerator(); +const createProcessorsList = (processors: ProcessingDefinition[]): ProcessorDefinition[] => + processors.map(createProcessorWithId); + +const createProcessorWithId = (processor: ProcessingDefinition): ProcessorDefinition => ({ + ...processor, + id: createId(), +}); + +const removeIdFromProcessor = (processor: ProcessorDefinition): ProcessingDefinition => { + const { id, ...rest } = processor; + return rest; +}; + +const mergeFields = ( + definition: WiredReadStreamDefinition, + currentFields: FieldDefinition, + newFields: DetectedField[] +) => { + return { + ...definition.stream.ingest.wired.fields, + ...newFields.reduce((acc, field) => { + // Add only new fields and ignore unmapped ones + if ( + !(field.name in currentFields) && + !(field.name in definition.inherited_fields) && + field.type !== 'unmapped' + ) { + acc[field.name] = { type: field.type }; + } + return acc; + }, {} as FieldDefinition), + }; +}; diff --git a/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_enrichment/index.tsx b/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_enrichment/index.tsx new file mode 100644 index 0000000000000..c0f5644eb1320 --- /dev/null +++ b/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_enrichment/index.tsx @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React from 'react'; +import { dynamic } from '@kbn/shared-ux-utility'; +import { ReadStreamDefinition } from '@kbn/streams-schema'; + +const StreamDetailEnrichmentContent = dynamic(() => + import(/* webpackChunkName: "management_enrichment" */ './page_content').then((mod) => ({ + default: mod.StreamDetailEnrichmentContent, + })) +); + +interface StreamDetailEnrichmentProps { + definition?: ReadStreamDefinition; + refreshDefinition: () => void; +} + +export function StreamDetailEnrichment({ + definition, + refreshDefinition, +}: StreamDetailEnrichmentProps) { + if (!definition) return null; + + return ( + + ); +} diff --git a/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_enrichment/page_content.tsx b/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_enrichment/page_content.tsx new file mode 100644 index 0000000000000..6ded439d24385 --- /dev/null +++ b/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_enrichment/page_content.tsx @@ -0,0 +1,157 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useEffect } from 'react'; +import { + DragDropContextProps, + EuiPanel, + EuiSpacer, + EuiText, + EuiTitle, + euiDragDropReorder, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { ReadStreamDefinition, isRootStream } from '@kbn/streams-schema'; +import { useBoolean } from '@kbn/react-hooks'; +import { useUnsavedChangesPrompt } from '@kbn/unsaved-changes-prompt'; +import { EnrichmentEmptyPrompt } from './enrichment_empty_prompt'; +import { AddProcessorButton } from './add_processor_button'; +import { AddProcessorFlyout } from './flyout'; +import { DraggableProcessorListItem } from './processors_list'; +import { ManagementBottomBar } from '../management_bottom_bar'; +import { SortableList } from './sortable_list'; +import { useDefinition } from './hooks/use_definition'; +import { useKibana } from '../../hooks/use_kibana'; +import { RootStreamEmptyPrompt } from './root_stream_empty_prompt'; + +interface StreamDetailEnrichmentContentProps { + definition: ReadStreamDefinition; + refreshDefinition: () => void; +} + +export function StreamDetailEnrichmentContent({ + definition, + refreshDefinition, +}: StreamDetailEnrichmentContentProps) { + const { appParams, core } = useKibana(); + + const [isBottomBarOpen, { on: openBottomBar, off: closeBottomBar }] = useBoolean(); + const [isAddProcessorOpen, { on: openAddProcessor, off: closeAddProcessor }] = useBoolean(); + + const { + processors, + addProcessor, + updateProcessor, + deleteProcessor, + resetChanges, + saveChanges, + setProcessors, + hasChanges, + isSavingChanges, + } = useDefinition(definition, refreshDefinition); + + const handlerItemDrag: DragDropContextProps['onDragEnd'] = ({ source, destination }) => { + if (source && destination) { + const items = euiDragDropReorder(processors, source.index, destination.index); + setProcessors(items); + } + }; + + useEffect(() => { + if (hasChanges) openBottomBar(); + else closeBottomBar(); + }, [closeBottomBar, hasChanges, openBottomBar]); + + useUnsavedChangesPrompt({ + hasUnsavedChanges: hasChanges, + history: appParams.history, + http: core.http, + navigateToUrl: core.application.navigateToUrl, + openConfirm: core.overlays.openConfirm, + }); + + const handleSaveChanges = async () => { + await saveChanges(); + closeBottomBar(); + }; + + const handleDiscardChanges = async () => { + await resetChanges(); + closeBottomBar(); + }; + + const bottomBar = isBottomBarOpen && ( + + ); + + const addProcessorFlyout = isAddProcessorOpen && ( + + ); + + const hasProcessors = processors.length > 0; + + if (isRootStream(definition)) { + return ; + } + + return ( + <> + {hasProcessors ? ( + + + + + {processors.map((processor, idx) => ( + + ))} + + + + + ) : ( + + )} + {addProcessorFlyout} + {bottomBar} + + ); +} + +const ProcessorsHeader = () => { + return ( + <> + +

+ {i18n.translate('xpack.streams.streamDetailView.managementTab.enrichment.headingTitle', { + defaultMessage: 'Processors for field extraction', + })} +

+
+ + {i18n.translate('xpack.streams.streamDetailView.managementTab.enrichment.headingSubtitle', { + defaultMessage: + 'Use processors to transform data before indexing. Drag and drop existing processors to update their execution order.', + })} + + + ); +}; diff --git a/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_enrichment/processors_list.tsx b/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_enrichment/processors_list.tsx new file mode 100644 index 0000000000000..946a78a80f041 --- /dev/null +++ b/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_enrichment/processors_list.tsx @@ -0,0 +1,114 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { + EuiDraggable, + EuiPanelProps, + EuiPanel, + EuiFlexGroup, + EuiIcon, + EuiText, + EuiFlexItem, + EuiButtonIcon, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { getProcessorType, isDissectProcessor, isGrokProcessor } from '@kbn/streams-schema'; +import { useBoolean } from '@kbn/react-hooks'; +import { css } from '@emotion/react'; +import { EditProcessorFlyout, EditProcessorFlyoutProps } from './flyout'; +import { ProcessorDefinition } from './types'; + +export const DraggableProcessorListItem = ({ + processor, + idx, + ...props +}: Omit & { idx: number }) => ( + + {(_provided, state) => ( + + )} + +); + +interface ProcessorListItemProps { + processor: ProcessorDefinition; + hasShadow: EuiPanelProps['hasShadow']; + onUpdateProcessor: EditProcessorFlyoutProps['onUpdateProcessor']; + onDeleteProcessor: EditProcessorFlyoutProps['onDeleteProcessor']; +} + +const ProcessorListItem = ({ + processor, + hasShadow = false, + onUpdateProcessor, + onDeleteProcessor, +}: ProcessorListItemProps) => { + const [isEditProcessorOpen, { on: openEditProcessor, off: closeEditProcessor }] = useBoolean(); + + const type = getProcessorType(processor); + const description = getProcessorDescription(processor); + + return ( + + + + + {type.toUpperCase()} + + + + {description} + + + + + {isEditProcessorOpen && ( + + )} + + ); +}; + +const getProcessorDescription = (processor: ProcessorDefinition) => { + if (isGrokProcessor(processor.config)) { + return processor.config.grok.patterns.join(' • '); + } else if (isDissectProcessor(processor.config)) { + return processor.config.dissect.pattern; + } + + return ''; +}; diff --git a/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_enrichment/root_stream_empty_prompt.tsx b/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_enrichment/root_stream_empty_prompt.tsx new file mode 100644 index 0000000000000..6feb38a0e736c --- /dev/null +++ b/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_enrichment/root_stream_empty_prompt.tsx @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiEmptyPrompt } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { AssetImage } from '../asset_image'; + +export const RootStreamEmptyPrompt = () => { + return ( + } + title={ +

+ {i18n.translate( + 'xpack.streams.streamDetailView.managementTab.rootStreamEmptyPrompt.title', + { defaultMessage: 'Processing data is not allowed for root streams.' } + )} +

+ } + body={ +

+ {i18n.translate( + 'xpack.streams.streamDetailView.managementTab.rootStreamEmptyPrompt.body', + { + defaultMessage: + 'Root streams are selectively immutable and cannot be enriched with processors. To enrich data, reroute a new child stream and add processors to it.', + } + )} +

+ } + /> + ); +}; diff --git a/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_enrichment/sortable_list.tsx b/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_enrichment/sortable_list.tsx new file mode 100644 index 0000000000000..4847359cec734 --- /dev/null +++ b/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_enrichment/sortable_list.tsx @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { + DragDropContextProps, + EuiDroppableProps, + EuiDragDropContext, + EuiDroppable, + useEuiTheme, +} from '@elastic/eui'; +import { css } from '@emotion/react'; + +interface SortableListProps { + onDragItem: DragDropContextProps['onDragEnd']; + children: EuiDroppableProps['children']; +} + +export const SortableList = ({ onDragItem, children }: SortableListProps) => { + const { euiTheme } = useEuiTheme(); + + return ( + + + {children} + + + ); +}; diff --git a/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_enrichment/types.ts b/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_enrichment/types.ts new file mode 100644 index 0000000000000..6e97f585ed35d --- /dev/null +++ b/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_enrichment/types.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + DissectProcessingDefinition, + FieldDefinitionConfig, + GrokProcessingDefinition, + ProcessingDefinition, +} from '@kbn/streams-schema'; + +export interface ProcessorDefinition extends ProcessingDefinition { + id: string; +} + +export interface ProcessingDefinitionGrok extends Pick { + config: GrokProcessingDefinition; +} + +export interface ProcessingDefinitionDissect extends Pick { + config: DissectProcessingDefinition; +} + +interface BaseFormState extends Pick { + detected_fields?: DetectedField[]; +} + +export type GrokFormState = BaseFormState & + Omit & { + type: 'grok'; + patterns: Array<{ value: string }>; + }; + +export type DissectFormState = DissectProcessingDefinition['dissect'] & + BaseFormState & { type: 'dissect' }; + +export type ProcessorFormState = GrokFormState | DissectFormState; + +export interface DetectedField { + name: string; + type: FieldDefinitionConfig['type'] | 'unmapped'; +} diff --git a/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_enrichment/utils.ts b/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_enrichment/utils.ts new file mode 100644 index 0000000000000..52e995a8619d2 --- /dev/null +++ b/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_enrichment/utils.ts @@ -0,0 +1,137 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + DissectProcessingDefinition, + GrokProcessingDefinition, + ProcessingDefinition, + ProcessorType, + isCompleteCondition, + isDissectProcessor, + isGrokProcessor, +} from '@kbn/streams-schema'; +import { isEmpty } from 'lodash'; +import { DissectFormState, GrokFormState, ProcessorDefinition, ProcessorFormState } from './types'; + +const defaultCondition: ProcessingDefinition['condition'] = { + field: '', + operator: 'eq', + value: '', +}; + +const defaultGrokProcessorFormState: GrokFormState = { + type: 'grok', + field: 'message', + patterns: [{ value: '' }], + pattern_definitions: {}, + ignore_failure: true, + ignore_missing: true, + condition: defaultCondition, +}; + +const defaultDissectProcessorFormState: DissectFormState = { + type: 'dissect', + field: 'message', + pattern: '', + ignore_failure: true, + ignore_missing: true, + condition: defaultCondition, +}; + +const defaultProcessorFormStateByType: Record = { + dissect: defaultDissectProcessorFormState, + grok: defaultGrokProcessorFormState, +}; + +export const getDefaultFormState = ( + type: ProcessorType, + processor?: ProcessorDefinition +): ProcessorFormState => { + if (!processor) return defaultProcessorFormStateByType[type]; + + let configValues: ProcessorFormState = defaultProcessorFormStateByType[type]; + + if (isGrokProcessor(processor.config)) { + const { grok } = processor.config; + + configValues = structuredClone({ + ...grok, + type: 'grok', + patterns: grok.patterns.map((pattern) => ({ value: pattern })), + }); + } + + if (isDissectProcessor(processor.config)) { + const { dissect } = processor.config; + + configValues = structuredClone({ + ...dissect, + type: 'dissect', + }); + } + + return { + condition: processor.condition || defaultCondition, + ...configValues, + }; +}; + +export const convertFormStateToProcessing = ( + formState: ProcessorFormState +): ProcessingDefinition => { + if (formState.type === 'grok') { + const { condition, patterns, ...grokConfig } = formState; + + return { + condition: isCompleteCondition(condition) ? condition : undefined, + config: { + grok: { + patterns: patterns + .filter(({ value }) => value.trim().length > 0) + .map(({ value }) => value), + ...grokConfig, + }, + }, + }; + } + + if (formState.type === 'dissect') { + const { condition, ...dissectConfig } = formState; + + return { + condition: isCompleteCondition(condition) ? condition : undefined, + config: { + dissect: dissectConfig, + }, + }; + } + + throw new Error('Cannot convert form state to processing: unknown type.'); +}; + +export const isCompleteGrokDefinition = (processing: GrokProcessingDefinition) => { + const { patterns } = processing.grok; + + return !isEmpty(patterns); +}; + +export const isCompleteDissectDefinition = (processing: DissectProcessingDefinition) => { + const { pattern } = processing.dissect; + + return !isEmpty(pattern); +}; + +export const isCompleteProcessingDefinition = (processing: ProcessingDefinition) => { + if (isGrokProcessor(processing.config)) { + return isCompleteGrokDefinition(processing.config); + } + if (isDissectProcessor(processing.config)) { + return isCompleteDissectDefinition(processing.config); + } + + return false; +}; diff --git a/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_management/classic.tsx b/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_management/classic.tsx index 9a7b0869b62f1..9d66083db849a 100644 --- a/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_management/classic.tsx +++ b/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_management/classic.tsx @@ -10,7 +10,7 @@ import { ReadStreamDefinition } from '@kbn/streams-schema'; import { EuiFlexGroup, EuiListGroup, EuiText } from '@elastic/eui'; import { useStreamsAppParams } from '../../hooks/use_streams_app_params'; import { RedirectTo } from '../redirect_to'; -import { StreamDetailEnriching } from '../stream_detail_enriching'; +import { StreamDetailEnrichment } from '../stream_detail_enrichment'; import { useKibana } from '../../hooks/use_kibana'; import { Wrapper } from './wrapper'; @@ -40,9 +40,9 @@ export function ClassicStreamDetailManagement({ }, enrich: { content: ( - + ), - label: i18n.translate('xpack.streams.streamDetailView.enrichingTab', { + label: i18n.translate('xpack.streams.streamDetailView.enrichmentTab', { defaultMessage: 'Extract field', }), }, diff --git a/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_management/wired.tsx b/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_management/wired.tsx index 8f4586a1d4207..7abf05797cde4 100644 --- a/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_management/wired.tsx +++ b/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_management/wired.tsx @@ -10,7 +10,7 @@ import { WiredReadStreamDefinition } from '@kbn/streams-schema'; import { useStreamsAppParams } from '../../hooks/use_streams_app_params'; import { RedirectTo } from '../redirect_to'; import { StreamDetailRouting } from '../stream_detail_routing'; -import { StreamDetailEnriching } from '../stream_detail_enriching'; +import { StreamDetailEnrichment } from '../stream_detail_enrichment'; import { StreamDetailSchemaEditor } from '../stream_detail_schema_editor'; import { Wrapper } from './wrapper'; @@ -44,9 +44,9 @@ export function WiredStreamDetailManagement({ }, enrich: { content: ( - + ), - label: i18n.translate('xpack.streams.streamDetailView.enrichingTab', { + label: i18n.translate('xpack.streams.streamDetailView.enrichmentTab', { defaultMessage: 'Extract field', }), }, diff --git a/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_management/wrapper.tsx b/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_management/wrapper.tsx index 92d80924298fc..e6e9d3a6de19c 100644 --- a/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_management/wrapper.tsx +++ b/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_management/wrapper.tsx @@ -23,7 +23,7 @@ export function Wrapper({ return ( - + {previewSampleFetch.loading ? ( diff --git a/x-pack/solutions/observability/plugins/streams_app/public/components/streams_app_search_bar/index.tsx b/x-pack/solutions/observability/plugins/streams_app/public/components/streams_app_search_bar/index.tsx index cd07f540f3ba1..bcb3a9b43e15e 100644 --- a/x-pack/solutions/observability/plugins/streams_app/public/components/streams_app_search_bar/index.tsx +++ b/x-pack/solutions/observability/plugins/streams_app/public/components/streams_app_search_bar/index.tsx @@ -10,7 +10,7 @@ import React, { useMemo } from 'react'; import type { DataView } from '@kbn/data-views-plugin/common'; import { useKibana } from '../../hooks/use_kibana'; -interface Props { +export interface StreamsAppSearchBarProps { query?: string; dateRangeFrom?: string; dateRangeTo?: string; @@ -30,7 +30,7 @@ export function StreamsAppSearchBar({ query, placeholder, dataViews, -}: Props) { +}: StreamsAppSearchBarProps) { const { dependencies: { start: { unifiedSearch }, diff --git a/x-pack/solutions/observability/plugins/streams_app/public/hooks/use_discard_confirm.ts b/x-pack/solutions/observability/plugins/streams_app/public/hooks/use_discard_confirm.ts new file mode 100644 index 0000000000000..8e26e5620a0a9 --- /dev/null +++ b/x-pack/solutions/observability/plugins/streams_app/public/hooks/use_discard_confirm.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import { useKibana } from './use_kibana'; + +export const useDiscardConfirm = (handler: () => void) => { + const { core } = useKibana(); + + return async () => { + const hasCancelled = await core.overlays.openConfirm( + i18n.translate('xpack.streams.cancelModal.message', { + defaultMessage: 'Are you sure you want to discard your changes?', + }), + { + buttonColor: 'danger', + title: i18n.translate('xpack.streams.cancelModal.title', { + defaultMessage: 'Discard changes?', + }), + confirmButtonText: i18n.translate('xpack.streams.cancelModal.confirm', { + defaultMessage: 'Discard', + }), + cancelButtonText: i18n.translate('xpack.streams.cancelModal.cancel', { + defaultMessage: 'Keep editing', + }), + } + ); + + if (hasCancelled) handler(); + }; +}; diff --git a/x-pack/solutions/observability/plugins/streams_app/public/hooks/use_kibana.tsx b/x-pack/solutions/observability/plugins/streams_app/public/hooks/use_kibana.tsx index 9c6b23465fb11..7503e6b0f74d0 100644 --- a/x-pack/solutions/observability/plugins/streams_app/public/hooks/use_kibana.tsx +++ b/x-pack/solutions/observability/plugins/streams_app/public/hooks/use_kibana.tsx @@ -5,13 +5,14 @@ * 2.0. */ -import type { CoreStart } from '@kbn/core/public'; +import type { AppMountParameters, CoreStart } from '@kbn/core/public'; import { useMemo } from 'react'; import { useKibana } from '@kbn/kibana-react-plugin/public'; import type { StreamsAppStartDependencies } from '../types'; import type { StreamsAppServices } from '../services/types'; export interface StreamsAppKibanaContext { + appParams: AppMountParameters; core: CoreStart; dependencies: { start: StreamsAppStartDependencies; @@ -23,9 +24,10 @@ const useTypedKibana = (): StreamsAppKibanaContext => { const context = useKibana>(); return useMemo(() => { - const { dependencies, services, ...core } = context.services; + const { appParams, dependencies, services, ...core } = context.services; return { + appParams, core, dependencies, services, diff --git a/x-pack/solutions/observability/plugins/streams_app/tsconfig.json b/x-pack/solutions/observability/plugins/streams_app/tsconfig.json index ef9abc3a21b51..b1de53dff4ddb 100644 --- a/x-pack/solutions/observability/plugins/streams_app/tsconfig.json +++ b/x-pack/solutions/observability/plugins/streams_app/tsconfig.json @@ -48,5 +48,11 @@ "@kbn/core-notifications-browser", "@kbn/index-lifecycle-management-common-shared", "@kbn/streams-schema", + "@kbn/react-hooks", + "@kbn/i18n-react", + "@kbn/react-field", + "@kbn/shared-ux-utility", + "@kbn/unsaved-changes-prompt", + "@kbn/object-utils" ] } diff --git a/yarn.lock b/yarn.lock index 3bdf5c9e68c2e..072f8db4c9f67 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6469,6 +6469,10 @@ version "0.0.0" uid "" +"@kbn/object-utils@link:src/platform/packages/shared/kbn-object-utils": + version "0.0.0" + uid "" + "@kbn/object-versioning-utils@link:src/platform/packages/shared/kbn-object-versioning-utils": version "0.0.0" uid "" From c88d519bff0c772ba63be6687c198fcc92fc198f Mon Sep 17 00:00:00 2001 From: Artem Shelkovnikov Date: Fri, 10 Jan 2025 12:22:00 +0100 Subject: [PATCH 11/42] Introduce Kibana task to deploy agentless connectors for 9.0 (#203973) ## Closes https://github.com/elastic/search-team/issues/8508 ## Closes https://github.com/elastic/search-team/issues/8465 ## Summary This PR adds a background task for search_connectors plugin. This task checks connector records and agentless package policies and sees if new connector was added/old was deleted, and then adds/deletes package policies for these connectors. Scenario 1: a new connector was added by a user/API call User creates an Elastic-managed connector: https://github.com/user-attachments/assets/38296e48-b281-4b2b-9750-ab0a47334b55 When the user is done, a package policy is created by this background task: https://github.com/user-attachments/assets/12dbc33f-32bf-472d-b854-64588fc1e5b1 Scenario 2: a connector was deleted by a user/API call User deletes an Elastic-managed connector: https://github.com/user-attachments/assets/5997897e-fb9d-4199-8045-abe163264976 ### Checklist Check the PR satisfies following conditions. Reviewers should verify this PR satisfies this list as well. - [ ] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [ ] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [ ] If a plugin configuration key changed, check if it needs to be allowlisted in the cloud and added to the [docker list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker) - [ ] This was checked for breaking HTTP API changes, and any breaking changes have been approved by the breaking-change committee. The `release_note:breaking` label should be applied in these situations. - [ ] [Flaky Test Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was used on any tests changed - [x] The PR description includes the appropriate Release Notes section, and the correct `release_note:*` label is applied per the [guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Jedr Blaszyk --- .../shared/fleet/server/mocks/index.ts | 2 + .../plugins/shared/fleet/server/plugin.ts | 11 +- .../server/routes/agent_policy/handlers.ts | 4 +- .../enrollment_settings_handler.test.ts | 2 +- .../routes/settings/settings_handler.test.ts | 2 +- .../server/services/agent_policy.test.ts | 4 +- .../fleet/server/services/agent_policy.ts | 4 +- .../services/agents/hosted_agent.test.ts | 2 +- .../server/services/agents/hosted_agent.ts | 2 +- .../services/agents/update_agent_tags.test.ts | 2 +- .../services/fleet_server/index.test.ts | 2 +- .../server/services/fleet_server/index.ts | 2 +- .../shared/fleet/server/services/index.ts | 4 +- .../fleet/server/services/output.test.ts | 12 +- .../shared/fleet/server/services/output.ts | 4 +- .../server/services/package_policy.test.ts | 2 +- .../fleet/server/services/package_policy.ts | 4 +- .../uninstall_token_service/index.test.ts | 2 +- .../security/uninstall_token_service/index.ts | 4 +- .../plugins/search_connectors/kibana.jsonc | 9 +- .../search_connectors/server/plugin.ts | 68 +- .../server/services/index.test.ts | 631 ++++++++++++++++++ .../server/services/index.ts | 253 +++++++ .../search_connectors/server/task.test.ts | 241 +++++++ .../plugins/search_connectors/server/task.ts | 196 ++++++ .../plugins/search_connectors/server/types.ts | 20 +- .../plugins/search_connectors/tsconfig.json | 8 + .../check_registered_task_types.ts | 1 + 28 files changed, 1448 insertions(+), 50 deletions(-) create mode 100644 x-pack/solutions/search/plugins/search_connectors/server/services/index.test.ts create mode 100644 x-pack/solutions/search/plugins/search_connectors/server/services/index.ts create mode 100644 x-pack/solutions/search/plugins/search_connectors/server/task.test.ts create mode 100644 x-pack/solutions/search/plugins/search_connectors/server/task.ts diff --git a/x-pack/platform/plugins/shared/fleet/server/mocks/index.ts b/x-pack/platform/plugins/shared/fleet/server/mocks/index.ts index db100c5fcf7ec..8aa3d96ae43b8 100644 --- a/x-pack/platform/plugins/shared/fleet/server/mocks/index.ts +++ b/x-pack/platform/plugins/shared/fleet/server/mocks/index.ts @@ -228,8 +228,10 @@ export const createPackagePolicyServiceMock = (): jest.Mocked => { return { + create: jest.fn().mockReturnValue(Promise.resolve()), get: jest.fn().mockReturnValue(Promise.resolve()), list: jest.fn().mockReturnValue(Promise.resolve()), + delete: jest.fn().mockReturnValue(Promise.resolve()), getFullAgentPolicy: jest.fn().mockReturnValue(Promise.resolve()), getByIds: jest.fn().mockReturnValue(Promise.resolve()), turnOffAgentTamperProtections: jest.fn().mockReturnValue(Promise.resolve()), diff --git a/x-pack/platform/plugins/shared/fleet/server/plugin.ts b/x-pack/platform/plugins/shared/fleet/server/plugin.ts index 1620df27b82c3..d1e4691fb48b5 100644 --- a/x-pack/platform/plugins/shared/fleet/server/plugin.ts +++ b/x-pack/platform/plugins/shared/fleet/server/plugin.ts @@ -816,16 +816,7 @@ export class FleetPlugin core.elasticsearch.client.asInternalUser, internalSoClient ), - agentPolicyService: { - get: agentPolicyService.get, - list: agentPolicyService.list, - getFullAgentPolicy: agentPolicyService.getFullAgentPolicy, - getByIds: agentPolicyService.getByIDs, - turnOffAgentTamperProtections: - agentPolicyService.turnOffAgentTamperProtections.bind(agentPolicyService), - fetchAllAgentPolicies: agentPolicyService.fetchAllAgentPolicies, - fetchAllAgentPolicyIds: agentPolicyService.fetchAllAgentPolicyIds, - }, + agentPolicyService, packagePolicyService, registerExternalCallback: (type: ExternalCallback[0], callback: ExternalCallback[1]) => { return appContextService.addExternalCallback(type, callback); diff --git a/x-pack/platform/plugins/shared/fleet/server/routes/agent_policy/handlers.ts b/x-pack/platform/plugins/shared/fleet/server/routes/agent_policy/handlers.ts index 6d5587e283878..2233d9fc9fa3c 100644 --- a/x-pack/platform/plugins/shared/fleet/server/routes/agent_policy/handlers.ts +++ b/x-pack/platform/plugins/shared/fleet/server/routes/agent_policy/handlers.ts @@ -196,7 +196,7 @@ export const bulkGetAgentPoliciesHandler: FleetRequestHandler< 'full query parameter require agent policies read permissions' ); } - let items = await agentPolicyService.getByIDs(soClient, ids, { + let items = await agentPolicyService.getByIds(soClient, ids, { withPackagePolicies, ignoreMissing, }); @@ -687,7 +687,7 @@ export const GetListAgentPolicyOutputsHandler: FleetRequestHandler< body: { items: [] }, }); } - const agentPolicies = await agentPolicyService.getByIDs(soClient, ids, { + const agentPolicies = await agentPolicyService.getByIds(soClient, ids, { withPackagePolicies: true, }); diff --git a/x-pack/platform/plugins/shared/fleet/server/routes/settings/enrollment_settings_handler.test.ts b/x-pack/platform/plugins/shared/fleet/server/routes/settings/enrollment_settings_handler.test.ts index 029efc146e09b..d8d2bdc2f2411 100644 --- a/x-pack/platform/plugins/shared/fleet/server/routes/settings/enrollment_settings_handler.test.ts +++ b/x-pack/platform/plugins/shared/fleet/server/routes/settings/enrollment_settings_handler.test.ts @@ -22,7 +22,7 @@ import { jest.mock('../../services', () => ({ agentPolicyService: { get: jest.fn(), - getByIDs: jest.fn(), + getByIds: jest.fn(), }, appContextService: { getInternalUserSOClientWithoutSpaceExtension: jest.fn(), diff --git a/x-pack/platform/plugins/shared/fleet/server/routes/settings/settings_handler.test.ts b/x-pack/platform/plugins/shared/fleet/server/routes/settings/settings_handler.test.ts index 151a0a2ba2af8..2d263a4b66686 100644 --- a/x-pack/platform/plugins/shared/fleet/server/routes/settings/settings_handler.test.ts +++ b/x-pack/platform/plugins/shared/fleet/server/routes/settings/settings_handler.test.ts @@ -42,7 +42,7 @@ jest.mock('../../services', () => ({ }, agentPolicyService: { get: jest.fn(), - getByIDs: jest.fn(), + getByIds: jest.fn(), }, })); diff --git a/x-pack/platform/plugins/shared/fleet/server/services/agent_policy.test.ts b/x-pack/platform/plugins/shared/fleet/server/services/agent_policy.test.ts index 76d2727b65e85..68fde28107340 100644 --- a/x-pack/platform/plugins/shared/fleet/server/services/agent_policy.test.ts +++ b/x-pack/platform/plugins/shared/fleet/server/services/agent_policy.test.ts @@ -504,7 +504,7 @@ describe('Agent policy', () => { }); }); - describe('getByIDs', () => { + describe('getByIds', () => { it('should call audit logger', async () => { const soClient = savedObjectsClientMock.create(); @@ -525,7 +525,7 @@ describe('Agent policy', () => { ], }); - await agentPolicyService.getByIDs(soClient, ['test-agent-policy-1', 'test-agent-policy-2']); + await agentPolicyService.getByIds(soClient, ['test-agent-policy-1', 'test-agent-policy-2']); expect(mockedAuditLoggingService.writeCustomSoAuditLog).toHaveBeenNthCalledWith(1, { action: 'get', diff --git a/x-pack/platform/plugins/shared/fleet/server/services/agent_policy.ts b/x-pack/platform/plugins/shared/fleet/server/services/agent_policy.ts index 3a3273383ad38..5cf805008d967 100644 --- a/x-pack/platform/plugins/shared/fleet/server/services/agent_policy.ts +++ b/x-pack/platform/plugins/shared/fleet/server/services/agent_policy.ts @@ -507,7 +507,7 @@ class AgentPolicyService { return agentPolicy; } - public async getByIDs( + public async getByIds( soClient: SavedObjectsClientContract, ids: Array, options: { fields?: string[]; withPackagePolicies?: boolean; ignoreMissing?: boolean } = {} @@ -1345,7 +1345,7 @@ class AgentPolicyService { }); } - const policies = await agentPolicyService.getByIDs(soClient, agentPolicyIds); + const policies = await agentPolicyService.getByIds(soClient, agentPolicyIds); const policiesMap = keyBy(policies, 'id'); const fullPolicies = await pMap( agentPolicyIds, diff --git a/x-pack/platform/plugins/shared/fleet/server/services/agents/hosted_agent.test.ts b/x-pack/platform/plugins/shared/fleet/server/services/agents/hosted_agent.test.ts index 997bb0817351c..f92d2b00656d5 100644 --- a/x-pack/platform/plugins/shared/fleet/server/services/agents/hosted_agent.test.ts +++ b/x-pack/platform/plugins/shared/fleet/server/services/agents/hosted_agent.test.ts @@ -14,7 +14,7 @@ import { getHostedPolicies, isHostedAgent } from './hosted_agent'; jest.mock('../agent_policy', () => { return { agentPolicyService: { - getByIDs: jest.fn().mockResolvedValue([ + getByIds: jest.fn().mockResolvedValue([ { id: 'hosted-policy', is_managed: true }, { id: 'regular-policy', is_managed: false }, ]), diff --git a/x-pack/platform/plugins/shared/fleet/server/services/agents/hosted_agent.ts b/x-pack/platform/plugins/shared/fleet/server/services/agents/hosted_agent.ts index 4acba8551e86d..06c85805d88dd 100644 --- a/x-pack/platform/plugins/shared/fleet/server/services/agents/hosted_agent.ts +++ b/x-pack/platform/plugins/shared/fleet/server/services/agents/hosted_agent.ts @@ -20,7 +20,7 @@ export async function getHostedPolicies( ); // get the agent policies for those ids - const agentPolicies = await agentPolicyService.getByIDs(soClient, Array.from(policyIdsToGet), { + const agentPolicies = await agentPolicyService.getByIds(soClient, Array.from(policyIdsToGet), { fields: ['is_managed'], ignoreMissing: true, }); diff --git a/x-pack/platform/plugins/shared/fleet/server/services/agents/update_agent_tags.test.ts b/x-pack/platform/plugins/shared/fleet/server/services/agents/update_agent_tags.test.ts index fe979571ff02e..2e6348876868d 100644 --- a/x-pack/platform/plugins/shared/fleet/server/services/agents/update_agent_tags.test.ts +++ b/x-pack/platform/plugins/shared/fleet/server/services/agents/update_agent_tags.test.ts @@ -34,7 +34,7 @@ jest.mock('../agent_policy', () => { return { agentPolicyService: { getInactivityTimeouts: jest.fn().mockResolvedValue([]), - getByIDs: jest.fn().mockResolvedValue([{ id: 'hosted-agent-policy', is_managed: true }]), + getByIds: jest.fn().mockResolvedValue([{ id: 'hosted-agent-policy', is_managed: true }]), list: jest.fn().mockResolvedValue({ items: [] }), }, }; diff --git a/x-pack/platform/plugins/shared/fleet/server/services/fleet_server/index.test.ts b/x-pack/platform/plugins/shared/fleet/server/services/fleet_server/index.test.ts index 5b9155a756645..1832ba78e8dd7 100644 --- a/x-pack/platform/plugins/shared/fleet/server/services/fleet_server/index.test.ts +++ b/x-pack/platform/plugins/shared/fleet/server/services/fleet_server/index.test.ts @@ -204,7 +204,7 @@ describe('getFleetServerPolicies', () => { page: 1, perPage: mockPackagePolicies.length, }); - (mockedAgentPolicyService.getByIDs as jest.Mock).mockResolvedValueOnce(mockFleetServerPolicies); + (mockedAgentPolicyService.getByIds as jest.Mock).mockResolvedValueOnce(mockFleetServerPolicies); const result = await getFleetServerPolicies(soClient); expect(result).toEqual(mockFleetServerPolicies); }); diff --git a/x-pack/platform/plugins/shared/fleet/server/services/fleet_server/index.ts b/x-pack/platform/plugins/shared/fleet/server/services/fleet_server/index.ts index e596709523351..034db5fdb0c1d 100644 --- a/x-pack/platform/plugins/shared/fleet/server/services/fleet_server/index.ts +++ b/x-pack/platform/plugins/shared/fleet/server/services/fleet_server/index.ts @@ -41,7 +41,7 @@ export const getFleetServerPolicies = async ( // Retrieve associated agent policies const fleetServerAgentPolicies = fleetServerAgentPolicyIds.length - ? await agentPolicyService.getByIDs( + ? await agentPolicyService.getByIds( soClient, uniqBy(fleetServerAgentPolicyIds, (p) => p.id) ) diff --git a/x-pack/platform/plugins/shared/fleet/server/services/index.ts b/x-pack/platform/plugins/shared/fleet/server/services/index.ts index deb902914a190..9db62cc61c2ed 100644 --- a/x-pack/platform/plugins/shared/fleet/server/services/index.ts +++ b/x-pack/platform/plugins/shared/fleet/server/services/index.ts @@ -15,10 +15,12 @@ export { getRegistryUrl } from './epm/registry/registry_url'; */ export interface AgentPolicyServiceInterface { + create: (typeof agentPolicyService)['create']; get: (typeof agentPolicyService)['get']; list: (typeof agentPolicyService)['list']; + delete: (typeof agentPolicyService)['delete']; getFullAgentPolicy: (typeof agentPolicyService)['getFullAgentPolicy']; - getByIds: (typeof agentPolicyService)['getByIDs']; + getByIds: (typeof agentPolicyService)['getByIds']; turnOffAgentTamperProtections: (typeof agentPolicyService)['turnOffAgentTamperProtections']; fetchAllAgentPolicyIds: (typeof agentPolicyService)['fetchAllAgentPolicyIds']; fetchAllAgentPolicies: (typeof agentPolicyService)['fetchAllAgentPolicies']; diff --git a/x-pack/platform/plugins/shared/fleet/server/services/output.test.ts b/x-pack/platform/plugins/shared/fleet/server/services/output.test.ts index f2dab8401e641..6b7da2744ffc5 100644 --- a/x-pack/platform/plugins/shared/fleet/server/services/output.test.ts +++ b/x-pack/platform/plugins/shared/fleet/server/services/output.test.ts @@ -316,7 +316,7 @@ describe('Output Service', () => { } as unknown as ReturnType; beforeEach(() => { - mockedAgentPolicyService.getByIDs.mockResolvedValue([]); + mockedAgentPolicyService.getByIds.mockResolvedValue([]); mockedAgentPolicyService.list.mockClear(); mockedPackagePolicyService.list.mockReset(); mockedAgentPolicyService.hasAPMIntegration.mockClear(); @@ -334,7 +334,7 @@ describe('Output Service', () => { }); afterEach(() => { - mockedAgentPolicyService.getByIDs.mockClear(); + mockedAgentPolicyService.getByIds.mockClear(); }); describe('create', () => { @@ -688,7 +688,7 @@ describe('Output Service', () => { mockedPackagePolicyService.list.mockResolvedValue( mockedPackagePolicyWithFleetServerResolvedValue ); - mockedAgentPolicyService.getByIDs.mockResolvedValue( + mockedAgentPolicyService.getByIds.mockResolvedValue( (await mockedAgentPolicyWithFleetServerResolvedValue).items ); @@ -727,7 +727,7 @@ describe('Output Service', () => { mockedPackagePolicyService.list.mockResolvedValue( mockedPackagePolicyWithSyntheticsResolvedValue ); - mockedAgentPolicyService.getByIDs.mockResolvedValue( + mockedAgentPolicyService.getByIds.mockResolvedValue( (await mockedAgentPolicyWithSyntheticsResolvedValue).items ); @@ -845,7 +845,7 @@ describe('Output Service', () => { mockedPackagePolicyService.list.mockResolvedValue( mockedPackagePolicyWithFleetServerResolvedValue ); - mockedAgentPolicyService.getByIDs.mockResolvedValue( + mockedAgentPolicyService.getByIds.mockResolvedValue( (await mockedAgentPolicyWithFleetServerResolvedValue).items ); @@ -884,7 +884,7 @@ describe('Output Service', () => { mockedPackagePolicyService.list.mockResolvedValue( mockedPackagePolicyWithSyntheticsResolvedValue ); - mockedAgentPolicyService.getByIDs.mockResolvedValue( + mockedAgentPolicyService.getByIds.mockResolvedValue( (await mockedAgentPolicyWithSyntheticsResolvedValue).items ); diff --git a/x-pack/platform/plugins/shared/fleet/server/services/output.ts b/x-pack/platform/plugins/shared/fleet/server/services/output.ts index 0b30890ee566d..f7f211862a896 100644 --- a/x-pack/platform/plugins/shared/fleet/server/services/output.ts +++ b/x-pack/platform/plugins/shared/fleet/server/services/output.ts @@ -176,7 +176,7 @@ async function getAgentPoliciesPerOutput(outputId?: string, isDefault?: boolean) }, []) ), ]; - const agentPoliciesFromPackagePolicies = await agentPolicyService.getByIDs( + const agentPoliciesFromPackagePolicies = await agentPolicyService.getByIds( internalSoClientWithoutSpaceExtension, agentPolicyIdsFromPackagePolicies ); @@ -245,7 +245,7 @@ async function findPoliciesWithFleetServerOrSynthetics(outputId?: string, isDefa ); const agentPolicyIds = _.uniq(packagePolicies.flatMap((p) => p.policy_ids)); if (agentPolicyIds.length) { - agentPolicies = await agentPolicyService.getByIDs( + agentPolicies = await agentPolicyService.getByIds( internalSoClientWithoutSpaceExtension, agentPolicyIds ); diff --git a/x-pack/platform/plugins/shared/fleet/server/services/package_policy.test.ts b/x-pack/platform/plugins/shared/fleet/server/services/package_policy.test.ts index 7ea6ae290708b..29d9142d13ed8 100644 --- a/x-pack/platform/plugins/shared/fleet/server/services/package_policy.test.ts +++ b/x-pack/platform/plugins/shared/fleet/server/services/package_policy.test.ts @@ -227,7 +227,7 @@ const mockAgentPolicyGet = (spaceIds: string[] = ['default']) => { }); } ); - mockAgentPolicyService.getByIDs.mockImplementation( + mockAgentPolicyService.getByIds.mockImplementation( // @ts-ignore (_soClient: SavedObjectsClientContract, ids: string[]) => { return Promise.resolve( diff --git a/x-pack/platform/plugins/shared/fleet/server/services/package_policy.ts b/x-pack/platform/plugins/shared/fleet/server/services/package_policy.ts index 3ff369994c5c7..7caba78ab862a 100644 --- a/x-pack/platform/plugins/shared/fleet/server/services/package_policy.ts +++ b/x-pack/platform/plugins/shared/fleet/server/services/package_policy.ts @@ -518,7 +518,7 @@ class PackagePolicyClientImpl implements PackagePolicyClient { const agentPolicyIds = new Set(packagePolicies.flatMap((pkgPolicy) => pkgPolicy.policy_ids)); - const agentPolicies = await agentPolicyService.getByIDs(soClient, [...agentPolicyIds]); + const agentPolicies = await agentPolicyService.getByIds(soClient, [...agentPolicyIds]); const agentPoliciesIndexById = indexBy('id', agentPolicies); for (const agentPolicy of agentPolicies) { validateIsNotHostedPolicy(agentPolicy, options?.force); @@ -1551,7 +1551,7 @@ class PackagePolicyClientImpl implements PackagePolicyClient { return acc; }, new Set()); - const agentPolicies = await agentPolicyService.getByIDs(soClient, uniquePolicyIdsR); + const agentPolicies = await agentPolicyService.getByIds(soClient, uniquePolicyIdsR); for (const policyId of uniquePolicyIdsR) { const agentPolicy = agentPolicies.find((p) => p.id === policyId); diff --git a/x-pack/platform/plugins/shared/fleet/server/services/security/uninstall_token_service/index.test.ts b/x-pack/platform/plugins/shared/fleet/server/services/security/uninstall_token_service/index.test.ts index d2daf3ff46ae5..e3f6f3369bfc0 100644 --- a/x-pack/platform/plugins/shared/fleet/server/services/security/uninstall_token_service/index.test.ts +++ b/x-pack/platform/plugins/shared/fleet/server/services/security/uninstall_token_service/index.test.ts @@ -217,7 +217,7 @@ describe('UninstallTokenService', () => { agentPolicyService.deployPolicies = jest.fn(); getAgentPoliciesByIDsMock = jest.fn().mockResolvedValue([]); - agentPolicyService.getByIDs = getAgentPoliciesByIDsMock; + agentPolicyService.getByIds = getAgentPoliciesByIDsMock; if (scoppedInSpace) { soClientMock.getCurrentNamespace.mockReturnValue(scoppedInSpace); diff --git a/x-pack/platform/plugins/shared/fleet/server/services/security/uninstall_token_service/index.ts b/x-pack/platform/plugins/shared/fleet/server/services/security/uninstall_token_service/index.ts index 275dcfc3a281d..4cbb0874a39a5 100644 --- a/x-pack/platform/plugins/shared/fleet/server/services/security/uninstall_token_service/index.ts +++ b/x-pack/platform/plugins/shared/fleet/server/services/security/uninstall_token_service/index.ts @@ -305,7 +305,7 @@ export class UninstallTokenService implements UninstallTokenServiceInterface { } private async getPolicyIdNameDictionary(policyIds: string[]): Promise> { - const agentPolicies = await agentPolicyService.getByIDs(this.soClient, policyIds, { + const agentPolicies = await agentPolicyService.getByIds(this.soClient, policyIds, { ignoreMissing: true, }); @@ -615,7 +615,7 @@ export class UninstallTokenService implements UninstallTokenServiceInterface { const batchSize = config?.setup?.agentPolicySchemaUpgradeBatchSize ?? 100; await asyncForEach(chunk(policyIds, batchSize), async (policyIdsBatch) => { - const policies = await agentPolicyService.getByIDs( + const policies = await agentPolicyService.getByIds( appContextService.getInternalUserSOClientWithoutSpaceExtension(), policyIds.map((id) => ({ id, spaceId: '*' })) ); diff --git a/x-pack/solutions/search/plugins/search_connectors/kibana.jsonc b/x-pack/solutions/search/plugins/search_connectors/kibana.jsonc index 45ae79ec2cb5c..fa0ecadfab86a 100644 --- a/x-pack/solutions/search/plugins/search_connectors/kibana.jsonc +++ b/x-pack/solutions/search/plugins/search_connectors/kibana.jsonc @@ -17,8 +17,11 @@ "search", "connectors" ], - "requiredPlugins": [], - "optionalPlugins": [], - "requiredBundles": [] + "requiredPlugins": [ + "licensing", + "taskManager", + "fleet" + ], + "optionalPlugins": [] } } diff --git a/x-pack/solutions/search/plugins/search_connectors/server/plugin.ts b/x-pack/solutions/search/plugins/search_connectors/server/plugin.ts index fe73afae20b9a..09ccbd31fe660 100644 --- a/x-pack/solutions/search/plugins/search_connectors/server/plugin.ts +++ b/x-pack/solutions/search/plugins/search_connectors/server/plugin.ts @@ -5,40 +5,94 @@ * 2.0. */ -import type { PluginInitializerContext, Plugin, CoreSetup } from '@kbn/core/server'; +import type { + PluginInitializerContext, + Plugin, + CoreStart, + CoreSetup, + Logger, +} from '@kbn/core/server'; import { ConnectorServerSideDefinition } from '@kbn/search-connectors'; +import { isAgentlessEnabled } from '@kbn/fleet-plugin/server/services/utils/agentless'; import { getConnectorTypes } from '../common/lib/connector_types'; import type { SearchConnectorsPluginSetup as SearchConnectorsPluginSetup, SearchConnectorsPluginStart as SearchConnectorsPluginStart, - SetupDependencies, - StartDependencies, + SearchConnectorsPluginSetupDependencies, + SearchConnectorsPluginStartDependencies, } from './types'; +import { AgentlessConnectorDeploymentsSyncService } from './task'; +import { SearchConnectorsConfig } from './config'; + export class SearchConnectorsPlugin implements Plugin< SearchConnectorsPluginSetup, SearchConnectorsPluginStart, - SetupDependencies, - StartDependencies + SearchConnectorsPluginSetupDependencies, + SearchConnectorsPluginStartDependencies > { private connectors: ConnectorServerSideDefinition[]; + private log: Logger; + private readonly config: SearchConnectorsConfig; + private agentlessConnectorDeploymentsSyncService: AgentlessConnectorDeploymentsSyncService; constructor(initializerContext: PluginInitializerContext) { this.connectors = []; + this.log = initializerContext.logger.get(); + this.config = initializerContext.config.get(); + this.agentlessConnectorDeploymentsSyncService = new AgentlessConnectorDeploymentsSyncService( + this.log + ); } - public setup({ getStartServices, http }: CoreSetup) { + public setup( + coreSetup: CoreSetup, + plugins: SearchConnectorsPluginSetupDependencies + ) { + const http = coreSetup.http; + this.connectors = getConnectorTypes(http.staticAssets); + const coreStartServices = coreSetup.getStartServices(); + + // There seems to be no way to check for agentless here + // So we register a task, but do not execute it in `start` method + this.log.debug('Registering agentless connectors infra sync task'); + + coreStartServices + .then(([coreStart, searchConnectorsPluginStartDependencies]) => { + this.agentlessConnectorDeploymentsSyncService.registerInfraSyncTask( + plugins, + coreStart, + searchConnectorsPluginStartDependencies + ); + }) + .catch((err) => { + this.log.error(`Error registering agentless connectors infra sync task`, err); + }); return { getConnectorTypes: () => this.connectors, }; } - public start() { + public start(coreStart: CoreStart, plugins: SearchConnectorsPluginStartDependencies) { + if (isAgentlessEnabled()) { + this.log.info( + 'Agentless is supported, scheduling initial agentless connectors infrastructure watcher task' + ); + this.agentlessConnectorDeploymentsSyncService + .scheduleInfraSyncTask(this.config, plugins.taskManager) + .catch((err) => { + this.log.error(`Error scheduling agentless connectors infra sync task`, err); + }); + } else { + this.log.info( + 'Agentless is not supported, skipping scheduling initial agentless connectors infrastructure watcher task' + ); + } return { getConnectors: () => this.connectors, }; diff --git a/x-pack/solutions/search/plugins/search_connectors/server/services/index.test.ts b/x-pack/solutions/search/plugins/search_connectors/server/services/index.test.ts new file mode 100644 index 0000000000000..e424a4db038b8 --- /dev/null +++ b/x-pack/solutions/search/plugins/search_connectors/server/services/index.test.ts @@ -0,0 +1,631 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { SavedObjectsClientContract } from '@kbn/core-saved-objects-api-server'; +import { + ElasticsearchClientMock, + elasticsearchClientMock, +} from '@kbn/core-elasticsearch-client-server-mocks'; +import { + AgentlessConnectorsInfraService, + ConnectorMetadata, + PackagePolicyMetadata, + getConnectorsWithoutPolicies, + getPoliciesWithoutConnectors, +} from '.'; +import { savedObjectsClientMock } from '@kbn/core/server/mocks'; +import { MockedLogger, loggerMock } from '@kbn/logging-mocks'; +import { + createPackagePolicyServiceMock, + createMockAgentPolicyService, +} from '@kbn/fleet-plugin/server/mocks'; +import { AgentPolicyServiceInterface, PackagePolicyClient } from '@kbn/fleet-plugin/server'; +import { AgentPolicy, PackagePolicy, PackagePolicyInput } from '@kbn/fleet-plugin/common'; +import { createAgentPolicyMock, createPackagePolicyMock } from '@kbn/fleet-plugin/common/mocks'; + +jest.mock('@kbn/fleet-plugin/server/services/epm/packages', () => { + const mockedGetPackageInfo = ({ pkgName }: { pkgName: string }) => { + if (pkgName === 'elastic_connectors') { + const pkg = { + version: '0.0.5', + policy_templates: [ + { + name: 'github_elastic_connectors', + inputs: [ + { + type: 'connectors-py', + vars: [ + { + name: 'connector_id', + required: false, + type: 'string', + }, + { + name: 'connector_name', + required: false, + type: 'string', + }, + { + name: 'service_type', + required: false, + type: 'string', + }, + ], + }, + ], + }, + ], + }; + + return Promise.resolve(pkg); + } + }; + return { + getPackageInfo: jest.fn().mockImplementation(mockedGetPackageInfo), + }; +}); + +describe('AgentlessConnectorsInfraService', () => { + let soClient: jest.Mocked; + let esClient: ElasticsearchClientMock; + let packagePolicyService: jest.Mocked; + let agentPolicyInterface: jest.Mocked; + let logger: MockedLogger; + let service: AgentlessConnectorsInfraService; + + beforeEach(async () => { + soClient = savedObjectsClientMock.create(); + esClient = elasticsearchClientMock.createClusterClient().asInternalUser; + packagePolicyService = createPackagePolicyServiceMock(); + agentPolicyInterface = createMockAgentPolicyService(); + logger = loggerMock.create(); + + service = new AgentlessConnectorsInfraService( + soClient, + esClient, + packagePolicyService, + agentPolicyInterface, + logger + ); + + jest.clearAllMocks(); + }); + + describe('getNativeConnectors', () => { + test('Lists only native connectors', async () => { + const mockResult = { + results: [ + { + id: '00000001', + name: 'Sharepoint Online Production Connector', + service_type: 'sharepoint_online', + is_native: false, + }, + { + id: '00000002', + name: 'Github Connector for ACME Organisation', + service_type: 'github', + is_native: true, + }, + ], + count: 2, + }; + esClient.transport.request.mockResolvedValue(mockResult); + + const nativeConnectors = await service.getNativeConnectors(); + expect(nativeConnectors.length).toBe(1); + expect(nativeConnectors[0].id).toBe(mockResult.results[1].id); + expect(nativeConnectors[0].name).toBe(mockResult.results[1].name); + expect(nativeConnectors[0].service_type).toBe(mockResult.results[1].service_type); + }); + + test('Lists only supported service types', async () => { + const mockResult = { + results: [ + { + id: '00000001', + name: 'Sharepoint Online Production Connector', + service_type: 'sharepoint_online', + is_native: true, + }, + { + id: '00000002', + name: 'Github Connector for ACME Organisation', + service_type: 'github', + is_native: true, + }, + { + id: '00000003', + name: 'Connector with unexpected service_type', + service_type: 'crawler', + is_native: true, + }, + { + id: '00000004', + name: 'Connector with no service_type', + service_type: null, + is_native: true, + }, + ], + count: 4, + }; + esClient.transport.request.mockResolvedValue(mockResult); + + const nativeConnectors = await service.getNativeConnectors(); + expect(nativeConnectors.length).toBe(2); + expect(nativeConnectors[0].id).toBe(mockResult.results[0].id); + expect(nativeConnectors[0].name).toBe(mockResult.results[0].name); + expect(nativeConnectors[0].service_type).toBe(mockResult.results[0].service_type); + expect(nativeConnectors[1].id).toBe(mockResult.results[1].id); + expect(nativeConnectors[1].name).toBe(mockResult.results[1].name); + expect(nativeConnectors[1].service_type).toBe(mockResult.results[1].service_type); + }); + }); + describe('getConnectorPackagePolicies', () => { + const getMockPolicyFetchAllItems = (pages: PackagePolicy[][]) => { + return { + async *[Symbol.asyncIterator]() { + for (const page of pages) { + yield page; + } + }, + } as AsyncIterable; + }; + + test('Lists only policies with expected input', async () => { + const firstPackagePolicy = createPackagePolicyMock(); + firstPackagePolicy.id = 'this-is-package-policy-id'; + firstPackagePolicy.policy_ids = ['this-is-agent-policy-id']; + firstPackagePolicy.supports_agentless = true; + firstPackagePolicy.inputs = [ + { + type: 'connectors-py', + compiled_input: { + connector_id: '00000001', + connector_name: 'Sharepoint Online Production Connector', + service_type: 'sharepoint_online', + }, + } as PackagePolicyInput, + ]; + const secondPackagePolicy = createPackagePolicyMock(); + secondPackagePolicy.supports_agentless = true; + const thirdPackagePolicy = createPackagePolicyMock(); + thirdPackagePolicy.supports_agentless = true; + thirdPackagePolicy.inputs = [ + { + type: 'something-unsupported', + compiled_input: { + connector_id: '00000001', + connector_name: 'Sharepoint Online Production Connector', + service_type: 'sharepoint_online', + }, + } as PackagePolicyInput, + ]; + + packagePolicyService.fetchAllItems.mockResolvedValue( + getMockPolicyFetchAllItems([[firstPackagePolicy, secondPackagePolicy, thirdPackagePolicy]]) + ); + + const policies = await service.getConnectorPackagePolicies(); + + expect(policies.length).toBe(1); + expect(policies[0].package_policy_id).toBe(firstPackagePolicy.id); + expect(policies[0].connector_metadata.id).toBe( + firstPackagePolicy.inputs[0].compiled_input.connector_id + ); + expect(policies[0].connector_metadata.name).toBe( + firstPackagePolicy.inputs[0].compiled_input.connector_name + ); + expect(policies[0].connector_metadata.service_type).toBe( + firstPackagePolicy.inputs[0].compiled_input.service_type + ); + expect(policies[0].agent_policy_ids).toBe(firstPackagePolicy.policy_ids); + }); + + test('Lists policies if they are returned over multiple pages', async () => { + const firstPackagePolicy = createPackagePolicyMock(); + firstPackagePolicy.id = 'this-is-package-policy-id'; + firstPackagePolicy.policy_ids = ['this-is-agent-policy-id']; + firstPackagePolicy.supports_agentless = true; + firstPackagePolicy.inputs = [ + { + type: 'connectors-py', + compiled_input: { + connector_id: '00000001', + connector_name: 'Sharepoint Online Production Connector', + service_type: 'sharepoint_online', + }, + } as PackagePolicyInput, + ]; + const secondPackagePolicy = createPackagePolicyMock(); + secondPackagePolicy.supports_agentless = true; + const thirdPackagePolicy = createPackagePolicyMock(); + thirdPackagePolicy.supports_agentless = true; + thirdPackagePolicy.inputs = [ + { + type: 'connectors-py', + compiled_input: { + connector_id: '00000003', + connector_name: 'Sharepoint Online Production Connector', + service_type: 'github', + }, + } as PackagePolicyInput, + ]; + + packagePolicyService.fetchAllItems.mockResolvedValue( + getMockPolicyFetchAllItems([ + [firstPackagePolicy], + [secondPackagePolicy], + [thirdPackagePolicy], + ]) + ); + + const policies = await service.getConnectorPackagePolicies(); + + expect(policies.length).toBe(2); + expect(policies[0].package_policy_id).toBe(firstPackagePolicy.id); + expect(policies[0].connector_metadata.id).toBe( + firstPackagePolicy.inputs[0].compiled_input.connector_id + ); + expect(policies[0].connector_metadata.name).toBe( + firstPackagePolicy.inputs[0].compiled_input.connector_name + ); + expect(policies[0].connector_metadata.service_type).toBe( + firstPackagePolicy.inputs[0].compiled_input.service_type + ); + expect(policies[0].agent_policy_ids).toBe(firstPackagePolicy.policy_ids); + + expect(policies[1].package_policy_id).toBe(thirdPackagePolicy.id); + expect(policies[1].connector_metadata.id).toBe( + thirdPackagePolicy.inputs[0].compiled_input.connector_id + ); + expect(policies[1].connector_metadata.name).toBe( + thirdPackagePolicy.inputs[0].compiled_input.connector_name + ); + expect(policies[1].connector_metadata.service_type).toBe( + thirdPackagePolicy.inputs[0].compiled_input.service_type + ); + expect(policies[1].agent_policy_ids).toBe(thirdPackagePolicy.policy_ids); + }); + + test('Skips policies that have missing fields', async () => { + const firstPackagePolicy = createPackagePolicyMock(); + firstPackagePolicy.id = 'this-is-package-policy-id'; + firstPackagePolicy.policy_ids = ['this-is-agent-policy-id']; + firstPackagePolicy.inputs = [ + { + type: 'connectors-py', + compiled_input: { + connector_id: '00000001', + connector_name: 'Sharepoint Online Production Connector', + }, + } as PackagePolicyInput, + ]; + const secondPackagePolicy = createPackagePolicyMock(); + secondPackagePolicy.inputs = [ + { + type: 'connectors-py', + compiled_input: { + connector_name: 'Sharepoint Online Production Connector', + service_type: 'github', + }, + } as PackagePolicyInput, + ]; + + packagePolicyService.fetchAllItems.mockResolvedValue( + getMockPolicyFetchAllItems([[firstPackagePolicy], [secondPackagePolicy]]) + ); + + const policies = await service.getConnectorPackagePolicies(); + + expect(policies.length).toBe(0); + }); + }); + describe('deployConnector', () => { + let agentPolicy: AgentPolicy; + let sharepointOnlinePackagePolicy: PackagePolicy; + + beforeAll(() => { + agentPolicy = createAgentPolicyMock(); + + sharepointOnlinePackagePolicy = createPackagePolicyMock(); + sharepointOnlinePackagePolicy.id = 'this-is-package-policy-id'; + sharepointOnlinePackagePolicy.policy_ids = ['this-is-agent-policy-id']; + sharepointOnlinePackagePolicy.inputs = [ + { + type: 'connectors-py', + compiled_input: { + connector_id: '00000001', + connector_name: 'Sharepoint Online Production Connector', + service_type: 'sharepoint_online', + }, + } as PackagePolicyInput, + ]; + }); + + test('Raises an error if connector.id is missing', async () => { + const connector = { + id: '', + name: 'something', + service_type: 'github', + }; + + try { + await service.deployConnector(connector); + expect(true).toBe(false); + } catch (e) { + expect(e.message).toContain('Connector id'); + } + }); + + test('Raises an error if connector.service_type is missing', async () => { + const connector = { + id: '000000001', + name: 'something', + service_type: '', + }; + + try { + await service.deployConnector(connector); + expect(true).toBe(false); + } catch (e) { + expect(e.message).toContain('service_type'); + } + }); + + test('Raises an error if connector.service_type is unsupported', async () => { + const connector = { + id: '000000001', + name: 'something', + service_type: 'crawler', + }; + + try { + await service.deployConnector(connector); + expect(true).toBe(false); + } catch (e) { + expect(e.message).toContain('service_type'); + expect(e.message).toContain('incompatible'); + } + }); + + test('Does not swallow an error if agent policy creation failed', async () => { + const connector = { + id: '000000001', + name: 'something', + service_type: 'github', + }; + const errorMessage = 'Failed to create an agent policy hehe'; + + agentPolicyInterface.create.mockImplementation(() => { + throw new Error(errorMessage); + }); + + try { + await service.deployConnector(connector); + expect(true).toBe(false); + } catch (e) { + expect(e.message).toEqual(errorMessage); + } + }); + + test('Does not swallow an error if package policy creation failed', async () => { + const connector = { + id: '000000001', + name: 'something', + service_type: 'github', + }; + const errorMessage = 'Failed to create a package policy hehe'; + + agentPolicyInterface.create.mockResolvedValue(agentPolicy); + packagePolicyService.create.mockImplementation(() => { + throw new Error(errorMessage); + }); + + try { + await service.deployConnector(connector); + expect(true).toBe(false); + } catch (e) { + expect(e.message).toEqual(errorMessage); + } + }); + + test('Returns a created package policy when all goes well', async () => { + const connector = { + id: '000000001', + name: 'something', + service_type: 'github', + }; + + agentPolicyInterface.create.mockResolvedValue(agentPolicy); + packagePolicyService.create.mockResolvedValue(sharepointOnlinePackagePolicy); + + const result = await service.deployConnector(connector); + expect(result).toBe(sharepointOnlinePackagePolicy); + }); + }); + describe('removeDeployment', () => { + const packagePolicyId = 'this-is-package-policy-id'; + const agentPolicyId = 'this-is-agent-policy-id'; + let sharepointOnlinePackagePolicy: PackagePolicy; + + beforeAll(() => { + sharepointOnlinePackagePolicy = createPackagePolicyMock(); + sharepointOnlinePackagePolicy.id = packagePolicyId; + sharepointOnlinePackagePolicy.policy_ids = [agentPolicyId]; + sharepointOnlinePackagePolicy.inputs = [ + { + type: 'connectors-py', + compiled_input: { + connector_id: '00000001', + connector_name: 'Sharepoint Online Production Connector', + service_type: 'sharepoint_online', + }, + } as PackagePolicyInput, + ]; + }); + + test('Calls for deletion of both agent policy and package policy', async () => { + packagePolicyService.get.mockResolvedValue(sharepointOnlinePackagePolicy); + + await service.removeDeployment(packagePolicyId); + + expect(agentPolicyInterface.delete).toBeCalledWith(soClient, esClient, agentPolicyId); + expect(packagePolicyService.delete).toBeCalledWith(soClient, esClient, [packagePolicyId]); + }); + + test('Raises an error if deletion of agent policy failed', async () => { + packagePolicyService.get.mockResolvedValue(sharepointOnlinePackagePolicy); + + const errorMessage = 'Failed to create a package policy hehe'; + + agentPolicyInterface.delete.mockImplementation(() => { + throw new Error(errorMessage); + }); + + try { + await service.removeDeployment(packagePolicyId); + expect(true).toBe(false); + } catch (e) { + expect(e.message).toEqual(errorMessage); + } + }); + + test('Raises an error if deletion of package policy failed', async () => { + packagePolicyService.get.mockResolvedValue(sharepointOnlinePackagePolicy); + + const errorMessage = 'Failed to create a package policy hehe'; + + packagePolicyService.delete.mockImplementation(() => { + throw new Error(errorMessage); + }); + + try { + await service.removeDeployment(packagePolicyId); + expect(true).toBe(false); + } catch (e) { + expect(e.message).toEqual(errorMessage); + } + }); + + test('Raises an error if a policy is not found', async () => { + packagePolicyService.get.mockResolvedValue(null); + + try { + await service.removeDeployment(packagePolicyId); + expect(true).toBe(false); + } catch (e) { + expect(e.message).toContain('Failed to delete policy'); + expect(e.message).toContain(packagePolicyId); + } + }); + }); +}); + +describe('module', () => { + const githubConnector: ConnectorMetadata = { + id: '000001', + name: 'Github Connector', + service_type: 'github', + }; + + const sharepointConnector: ConnectorMetadata = { + id: '000002', + name: 'Sharepoint Connector', + service_type: 'sharepoint_online', + }; + + const mysqlConnector: ConnectorMetadata = { + id: '000003', + name: 'MySQL Connector', + service_type: 'mysql', + }; + + const githubPackagePolicy: PackagePolicyMetadata = { + package_policy_id: 'agent-001', + agent_policy_ids: ['agent-package-001'], + connector_metadata: githubConnector, + }; + + const sharepointPackagePolicy: PackagePolicyMetadata = { + package_policy_id: 'agent-002', + agent_policy_ids: ['agent-package-002'], + connector_metadata: sharepointConnector, + }; + + const mysqlPackagePolicy: PackagePolicyMetadata = { + package_policy_id: 'agent-003', + agent_policy_ids: ['agent-package-003'], + connector_metadata: mysqlConnector, + }; + + describe('getPoliciesWithoutConnectors', () => { + test('Returns a missing policy if one is missing', async () => { + const missingPolicies = getPoliciesWithoutConnectors( + [githubPackagePolicy, sharepointPackagePolicy, mysqlPackagePolicy], + [githubConnector, sharepointConnector] + ); + + expect(missingPolicies.length).toBe(1); + expect(missingPolicies).toContain(mysqlPackagePolicy); + }); + + test('Returns empty array if no policies are missing', async () => { + const missingPolicies = getPoliciesWithoutConnectors( + [githubPackagePolicy, sharepointPackagePolicy, mysqlPackagePolicy], + [githubConnector, sharepointConnector, mysqlConnector] + ); + + expect(missingPolicies.length).toBe(0); + }); + + test('Returns all policies if all are missing', async () => { + const missingPolicies = getPoliciesWithoutConnectors( + [githubPackagePolicy, sharepointPackagePolicy, mysqlPackagePolicy], + [] + ); + + expect(missingPolicies.length).toBe(3); + expect(missingPolicies).toContain(githubPackagePolicy); + expect(missingPolicies).toContain(sharepointPackagePolicy); + expect(missingPolicies).toContain(mysqlPackagePolicy); + }); + }); + + describe('getConnectorsWithoutPolicies', () => { + test('Returns a missing policy if one is missing', async () => { + const missingConnectors = getConnectorsWithoutPolicies( + [githubPackagePolicy, sharepointPackagePolicy], + [githubConnector, sharepointConnector, mysqlConnector] + ); + + expect(missingConnectors.length).toBe(1); + expect(missingConnectors).toContain(mysqlConnector); + }); + + test('Returns empty array if no policies are missing', async () => { + const missingConnectors = getConnectorsWithoutPolicies( + [githubPackagePolicy, sharepointPackagePolicy, mysqlPackagePolicy], + [githubConnector, sharepointConnector, mysqlConnector] + ); + + expect(missingConnectors.length).toBe(0); + }); + + test('Returns all policies if all are missing', async () => { + const missingConnectors = getConnectorsWithoutPolicies( + [], + [githubConnector, sharepointConnector, mysqlConnector] + ); + + expect(missingConnectors.length).toBe(3); + expect(missingConnectors).toContain(githubConnector); + expect(missingConnectors).toContain(sharepointConnector); + expect(missingConnectors).toContain(mysqlConnector); + }); + }); +}); diff --git a/x-pack/solutions/search/plugins/search_connectors/server/services/index.ts b/x-pack/solutions/search/plugins/search_connectors/server/services/index.ts new file mode 100644 index 0000000000000..2acb4143c14e9 --- /dev/null +++ b/x-pack/solutions/search/plugins/search_connectors/server/services/index.ts @@ -0,0 +1,253 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ElasticsearchClient } from '@kbn/core-elasticsearch-server'; +import { PACKAGE_POLICY_SAVED_OBJECT_TYPE, PackagePolicy } from '@kbn/fleet-plugin/common'; +import { AgentPolicyServiceInterface, PackagePolicyClient } from '@kbn/fleet-plugin/server'; +import type { Logger, SavedObjectsClientContract } from '@kbn/core/server'; +import { NATIVE_CONNECTOR_DEFINITIONS, fetchConnectors } from '@kbn/search-connectors'; +import { getPackageInfo } from '@kbn/fleet-plugin/server/services/epm/packages'; + +export interface ConnectorMetadata { + id: string; + name: string; + service_type: string; +} + +export interface PackagePolicyMetadata { + package_policy_id: string; + agent_policy_ids: string[]; + connector_metadata: ConnectorMetadata; +} + +const connectorsInputName = 'connectors-py'; +const pkgName = 'elastic_connectors'; +const pkgTitle = 'Elastic Connectors'; + +export class AgentlessConnectorsInfraService { + private logger: Logger; + private soClient: SavedObjectsClientContract; + private esClient: ElasticsearchClient; + private packagePolicyService: PackagePolicyClient; + private agentPolicyService: AgentPolicyServiceInterface; + + constructor( + soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, + packagePolicyService: PackagePolicyClient, + agentPolicyService: AgentPolicyServiceInterface, + logger: Logger + ) { + this.logger = logger; + this.soClient = soClient; + this.esClient = esClient; + this.packagePolicyService = packagePolicyService; + this.agentPolicyService = agentPolicyService; + } + + public getNativeConnectors = async (): Promise => { + this.logger.debug(`Fetching all connectors and filtering only to native`); + const nativeConnectors: ConnectorMetadata[] = []; + const allConnectors = await fetchConnectors(this.esClient); + for (const connector of allConnectors) { + if (connector.is_native && connector.service_type != null) { + if (NATIVE_CONNECTOR_DEFINITIONS[connector.service_type] == null) { + this.logger.debug( + `Skipping connector ${connector.id}: unsupported service type ${connector.service_type}` + ); + continue; + } + + nativeConnectors.push({ + id: connector.id, + name: connector.name, + service_type: connector.service_type, + }); + } + } + return nativeConnectors; + }; + + public getConnectorPackagePolicies = async (): Promise => { + const kuery = `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name:${pkgName}`; + this.logger.debug(`Fetching policies with kuery: "${kuery}"`); + const policiesIterator = await this.packagePolicyService.fetchAllItems(this.soClient, { + perPage: 50, + kuery, + }); + const policiesMetadata: PackagePolicyMetadata[] = []; + for await (const policyPage of policiesIterator) { + for (const policy of policyPage) { + for (const input of policy.inputs) { + if (input.type === connectorsInputName) { + if (input.compiled_input != null) { + if (input.compiled_input.service_type == null) { + this.logger.debug(`Policy ${policy.id} is missing service_type, skipping`); + continue; + } + + if (input.compiled_input.connector_id == null) { + this.logger.debug(`Policy ${policy.id} is missing connector_id, skipping`); + continue; + } + + if (input.compiled_input.connector_name == null) { + this.logger.debug(`Policy ${policy.id} is missing connector_name`); + // No need to skip, that's fine + } + + // TODO: We manage all policies here, not only agentless. + // Return this code back once this logic is ironed out + // if (policy.supports_agentless !== true) { + // this.logger.debug(`Policy ${policy.id} does not support agentless, skipping`); + // continue; + // } + + policiesMetadata.push({ + package_policy_id: policy.id, + agent_policy_ids: policy.policy_ids, + connector_metadata: { + id: input.compiled_input.connector_id, + name: input.compiled_input.connector_name || '', + service_type: input.compiled_input.service_type, + }, + }); + } + } + } + } + } + + return policiesMetadata; + }; + + public deployConnector = async (connector: ConnectorMetadata): Promise => { + this.logger.info( + `Connector ${connector.id} has no integration policy associated with it, creating` + ); + + if (connector.id == null || connector.id.trim().length === 0) { + throw new Error(`Connector id is null or empty`); + } + + if (connector.service_type == null || connector.service_type.trim().length === 0) { + throw new Error(`Connector ${connector.id} service_type is null or empty`); + } + + if (NATIVE_CONNECTOR_DEFINITIONS[connector.service_type] == null) { + throw new Error( + `Connector ${connector.id} service_type is incompatible with agentless or unsupported` + ); + } + this.logger.debug(`Getting package version for connectors package ${pkgName}`); + const pkgVersion = await this.getPackageVersion(); + this.logger.debug(`Latest package version for ${pkgName} is ${pkgVersion}`); + + const createdPolicy = await this.agentPolicyService.create(this.soClient, this.esClient, { + name: `${connector.service_type} connector: ${connector.id}`, + description: `Automatically generated on ${new Date(Date.now()).toISOString()}`, + namespace: 'default', + monitoring_enabled: ['logs', 'metrics'], + inactivity_timeout: 1209600, + is_protected: false, + supports_agentless: true, + }); + + this.logger.info( + `Successfully created agent policy ${createdPolicy.id} for agentless connector ${connector.id}` + ); + this.logger.debug(`Creating a package policy for agentless connector ${connector.id}`); + const packagePolicy = await this.packagePolicyService.create(this.soClient, this.esClient, { + policy_ids: [createdPolicy.id], + package: { + title: pkgTitle, + name: pkgName, + version: pkgVersion, + }, + name: `${connector.service_type} connector ${connector.id}`, + description: '', + namespace: '', + enabled: true, + inputs: [ + { + type: connectorsInputName, + enabled: true, + vars: { + connector_id: { type: 'string', value: connector.id }, + connector_name: { type: 'string', value: connector.name }, + service_type: { type: 'string', value: connector.service_type }, + }, + streams: [], + }, + ], + }); + + this.logger.info( + `Successfully created package policy ${packagePolicy.id} for agentless connector ${connector.id}` + ); + + return packagePolicy; + }; + + public removeDeployment = async (packagePolicyId: string): Promise => { + this.logger.info(`Deleting package policy ${packagePolicyId}`); + + const policy = await this.packagePolicyService.get(this.soClient, packagePolicyId); + + if (policy == null) { + throw new Error(`Failed to delete policy ${packagePolicyId}: not found`); + } + + await this.packagePolicyService.delete(this.soClient, this.esClient, [policy.id]); + + this.logger.debug(`Deleting package policies with ids ${policy.policy_ids}`); + + // TODO: can we do it in one go? + // Why not use deleteFleetServerPoliciesForPolicyId? + for (const agentPolicyId of policy.policy_ids) { + this.logger.info(`Deleting agent policy ${agentPolicyId}`); + await this.agentPolicyService.delete(this.soClient, this.esClient, agentPolicyId); + } + }; + + private getPackageVersion = async (): Promise => { + this.logger.debug(`Fetching ${pkgName} version`); + + // This will raise an error if package is not there. + // Situation is exceptional, so we can just show + // the error message from getPackageInfo in this case + const packageInfo = await getPackageInfo({ + savedObjectsClient: this.soClient, + pkgName, + pkgVersion: '', + skipArchive: true, + ignoreUnverified: true, + prerelease: true, + }); + this.logger.debug(`Found ${pkgName} version: ${packageInfo.version}`); + + return packageInfo.version; + }; +} + +export const getConnectorsWithoutPolicies = ( + packagePolicies: PackagePolicyMetadata[], + connectors: ConnectorMetadata[] +): ConnectorMetadata[] => { + return connectors.filter( + (x) => packagePolicies.filter((y) => y.connector_metadata.id === x.id).length === 0 + ); +}; + +export const getPoliciesWithoutConnectors = ( + packagePolicies: PackagePolicyMetadata[], + connectors: ConnectorMetadata[] +): PackagePolicyMetadata[] => { + return packagePolicies.filter( + (x) => connectors.filter((y) => y.id === x.connector_metadata.id).length === 0 + ); +}; diff --git a/x-pack/solutions/search/plugins/search_connectors/server/task.test.ts b/x-pack/solutions/search/plugins/search_connectors/server/task.test.ts new file mode 100644 index 0000000000000..c77f443f9d7ed --- /dev/null +++ b/x-pack/solutions/search/plugins/search_connectors/server/task.test.ts @@ -0,0 +1,241 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { loggerMock, MockedLogger } from '@kbn/logging-mocks'; +import { infraSyncTaskRunner } from './task'; +import { ConcreteTaskInstance, TaskStatus } from '@kbn/task-manager-plugin/server'; +import { + AgentlessConnectorsInfraService, + ConnectorMetadata, + PackagePolicyMetadata, +} from './services'; +import { licensingMock } from '@kbn/licensing-plugin/server/mocks'; +import { LicensingPluginStart } from '@kbn/licensing-plugin/server'; +import { createPackagePolicyMock } from '@kbn/fleet-plugin/common/mocks'; + +const DATE_1970 = '1970-01-01T00:00:00.000Z'; + +describe('infraSyncTaskRunner', () => { + const githubConnector: ConnectorMetadata = { + id: '000001', + name: 'Github Connector', + service_type: 'github', + }; + + const sharepointConnector: ConnectorMetadata = { + id: '000002', + name: 'Sharepoint Connector', + service_type: 'sharepoint_online', + }; + + const mysqlConnector: ConnectorMetadata = { + id: '000003', + name: 'MySQL Connector', + service_type: 'mysql', + }; + + const githubPackagePolicy: PackagePolicyMetadata = { + package_policy_id: 'agent-001', + agent_policy_ids: ['agent-package-001'], + connector_metadata: githubConnector, + }; + + const sharepointPackagePolicy: PackagePolicyMetadata = { + package_policy_id: 'agent-002', + agent_policy_ids: ['agent-package-002'], + connector_metadata: sharepointConnector, + }; + + const mysqlPackagePolicy: PackagePolicyMetadata = { + package_policy_id: 'agent-003', + agent_policy_ids: ['agent-package-003'], + connector_metadata: mysqlConnector, + }; + + let logger: MockedLogger; + let serviceMock: jest.Mocked; + let licensePluginStartMock: jest.Mocked; + + const taskInstanceStub: ConcreteTaskInstance = { + id: '', + attempts: 0, + status: TaskStatus.Running, + version: '123', + runAt: new Date(), + scheduledAt: new Date(), + startedAt: new Date(DATE_1970), + retryAt: new Date(Date.now() + 5 * 60 * 1000), + state: {}, + taskType: 'backfill', + timeoutOverride: '3m', + params: { + adHocRunParamsId: 'abc', + spaceId: 'default', + }, + ownerId: null, + }; + + const invalidLicenseMock = licensingMock.createLicenseMock(); + invalidLicenseMock.check.mockReturnValue({ state: 'invalid' }); + + const validLicenseMock = licensingMock.createLicenseMock(); + validLicenseMock.check.mockReturnValue({ state: 'valid' }); + + beforeAll(async () => { + logger = loggerMock.create(); + serviceMock = { + getNativeConnectors: jest.fn(), + getConnectorPackagePolicies: jest.fn(), + deployConnector: jest.fn(), + removeDeployment: jest.fn(), + } as unknown as jest.Mocked; + + licensePluginStartMock = { + getLicense: jest.fn(), + } as unknown as jest.Mocked; + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('Does nothing if no connectors or policies are configured', async () => { + await infraSyncTaskRunner( + logger, + serviceMock, + licensePluginStartMock + )({ taskInstance: taskInstanceStub }).run(); + + expect(serviceMock.deployConnector).not.toBeCalled(); + expect(serviceMock.removeDeployment).not.toBeCalled(); + }); + + test('Does nothing if connectors or policies requires deployment but license is not supported', async () => { + serviceMock.getNativeConnectors.mockResolvedValue([mysqlConnector, githubConnector]); + serviceMock.getConnectorPackagePolicies.mockResolvedValue([sharepointPackagePolicy]); + licensePluginStartMock.getLicense.mockResolvedValue(invalidLicenseMock); + + await infraSyncTaskRunner( + logger, + serviceMock, + licensePluginStartMock + )({ taskInstance: taskInstanceStub }).run(); + + expect(serviceMock.deployConnector).not.toBeCalled(); + expect(serviceMock.removeDeployment).not.toBeCalled(); + expect(logger.warn).toBeCalledWith(expect.stringMatching(/.*not compatible.*/)); + expect(logger.warn).toBeCalledWith(expect.stringMatching(/.*license.*/)); + }); + + test('Does nothing if all connectors and package policies are in-sync', async () => { + serviceMock.getNativeConnectors.mockResolvedValue([ + mysqlConnector, + githubConnector, + sharepointConnector, + ]); + serviceMock.getConnectorPackagePolicies.mockResolvedValue([ + mysqlPackagePolicy, + githubPackagePolicy, + sharepointPackagePolicy, + ]); + licensePluginStartMock.getLicense.mockResolvedValue(validLicenseMock); + + await infraSyncTaskRunner( + logger, + serviceMock, + licensePluginStartMock + )({ taskInstance: taskInstanceStub }).run(); + + expect(serviceMock.deployConnector).not.toBeCalled(); + expect(serviceMock.removeDeployment).not.toBeCalled(); + expect(logger.warn).not.toBeCalled(); + }); + + test('Deploys connectors if no policies has been created for these connectors', async () => { + serviceMock.getNativeConnectors.mockResolvedValue([mysqlConnector, githubConnector]); + serviceMock.getConnectorPackagePolicies.mockResolvedValue([sharepointPackagePolicy]); + licensePluginStartMock.getLicense.mockResolvedValue(validLicenseMock); + + await infraSyncTaskRunner( + logger, + serviceMock, + licensePluginStartMock + )({ taskInstance: taskInstanceStub }).run(); + + expect(serviceMock.deployConnector).toBeCalledWith(mysqlConnector); + expect(serviceMock.deployConnector).toBeCalledWith(githubConnector); + }); + + test('Deploys connectors even if another connectors failed to be deployed', async () => { + serviceMock.getNativeConnectors.mockResolvedValue([ + mysqlConnector, + githubConnector, + sharepointConnector, + ]); + serviceMock.getConnectorPackagePolicies.mockResolvedValue([]); + licensePluginStartMock.getLicense.mockResolvedValue(validLicenseMock); + serviceMock.deployConnector.mockImplementation(async (connector) => { + if (connector === mysqlConnector || connector === githubConnector) { + throw new Error('Cannot deploy these connectors'); + } + + return createPackagePolicyMock(); + }); + + await infraSyncTaskRunner( + logger, + serviceMock, + licensePluginStartMock + )({ taskInstance: taskInstanceStub }).run(); + + expect(serviceMock.deployConnector).toBeCalledWith(mysqlConnector); + expect(serviceMock.deployConnector).toBeCalledWith(githubConnector); + expect(serviceMock.deployConnector).toBeCalledWith(sharepointConnector); + }); + + test('Removes a package policy if no connectors match the policy', async () => { + serviceMock.getNativeConnectors.mockResolvedValue([mysqlConnector, githubConnector]); + serviceMock.getConnectorPackagePolicies.mockResolvedValue([sharepointPackagePolicy]); + licensePluginStartMock.getLicense.mockResolvedValue(validLicenseMock); + + await infraSyncTaskRunner( + logger, + serviceMock, + licensePluginStartMock + )({ taskInstance: taskInstanceStub }).run(); + + expect(serviceMock.removeDeployment).toBeCalledWith(sharepointPackagePolicy.package_policy_id); + }); + + test('Removes deployments even if another connectors failed to be undeployed', async () => { + serviceMock.getNativeConnectors.mockResolvedValue([]); + serviceMock.getConnectorPackagePolicies.mockResolvedValue([ + sharepointPackagePolicy, + mysqlPackagePolicy, + githubPackagePolicy, + ]); + licensePluginStartMock.getLicense.mockResolvedValue(validLicenseMock); + serviceMock.removeDeployment.mockImplementation(async (policyId) => { + if ( + policyId === sharepointPackagePolicy.package_policy_id || + policyId === mysqlPackagePolicy.package_policy_id + ) { + throw new Error('Cannot deploy these connectors'); + } + }); + + await infraSyncTaskRunner( + logger, + serviceMock, + licensePluginStartMock + )({ taskInstance: taskInstanceStub }).run(); + + expect(serviceMock.removeDeployment).toBeCalledWith(sharepointPackagePolicy.package_policy_id); + expect(serviceMock.removeDeployment).toBeCalledWith(mysqlPackagePolicy.package_policy_id); + expect(serviceMock.removeDeployment).toBeCalledWith(githubPackagePolicy.package_policy_id); + }); +}); diff --git a/x-pack/solutions/search/plugins/search_connectors/server/task.ts b/x-pack/solutions/search/plugins/search_connectors/server/task.ts new file mode 100644 index 0000000000000..7ecd538e0f7ce --- /dev/null +++ b/x-pack/solutions/search/plugins/search_connectors/server/task.ts @@ -0,0 +1,196 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Logger, CoreStart, SavedObjectsClient } from '@kbn/core/server'; + +import type { + ConcreteTaskInstance, + TaskManagerStartContract, + TaskInstance, +} from '@kbn/task-manager-plugin/server'; + +import { LicensingPluginStart } from '@kbn/licensing-plugin/server'; +import type { + SearchConnectorsPluginStartDependencies, + SearchConnectorsPluginSetupDependencies, +} from './types'; +import { + AgentlessConnectorsInfraService, + getConnectorsWithoutPolicies, + getPoliciesWithoutConnectors, +} from './services'; + +import { SearchConnectorsConfig } from './config'; + +const AGENTLESS_CONNECTOR_DEPLOYMENTS_SYNC_TASK_ID = 'search:agentless-connectors-manager-task'; +const AGENTLESS_CONNECTOR_DEPLOYMENTS_SYNC_TASK_TYPE = 'search:agentless-connectors-manager'; + +const SCHEDULE = { interval: '1m' }; + +export function infraSyncTaskRunner( + logger: Logger, + service: AgentlessConnectorsInfraService, + licensingPluginStart: LicensingPluginStart +) { + return ({ taskInstance }: { taskInstance: ConcreteTaskInstance }) => { + return { + run: async () => { + try { + // We fetch some info even if license does not permit actual operations. + // This is done so that we could give a warning to the user only + // if they are actually using the feature. + logger.debug('Checking state of connectors and agentless policies'); + + // Fetch connectors + const nativeConnectors = await service.getNativeConnectors(); + + const policiesMetadata = await service.getConnectorPackagePolicies(); + + // Check license if any native connectors or agentless policies found + if (nativeConnectors.length > 0 || policiesMetadata.length > 0) { + const license = await licensingPluginStart.getLicense(); + + if (license.check('fleet', 'platinum').state !== 'valid') { + logger.warn( + 'Current license is not compatible with agentless connectors. Please upgrade the license to at least platinum' + ); + return; + } + } + + // Deploy Policies + const connectorsWithoutPolicies = getConnectorsWithoutPolicies( + policiesMetadata, + nativeConnectors + ); + + let agentlessConnectorsDeployed = 0; + for (const connectorMetadata of connectorsWithoutPolicies) { + // We try-catch to still be able to deploy other connectors if some fail + try { + await service.deployConnector(connectorMetadata); + + agentlessConnectorsDeployed += 1; + } catch (e) { + logger.warn( + `Error creating an agentless deployment for connector ${connectorMetadata.id}: ${e.message}` + ); + } + } + + // Delete policies + const policiesWithoutConnectors = getPoliciesWithoutConnectors( + policiesMetadata, + nativeConnectors + ); + let agentlessConnectorsRemoved = 0; + + for (const policyMetadata of policiesWithoutConnectors) { + // We try-catch to still be able to deploy other connectors if some fail + try { + await service.removeDeployment(policyMetadata.package_policy_id); + + agentlessConnectorsRemoved += 1; + } catch (e) { + logger.warn( + `Error when deleting a package policy ${policyMetadata.package_policy_id}: ${e.message}` + ); + } + } + return { + state: {}, + schedule: SCHEDULE, + }; + } catch (e) { + logger.warn(`Error executing agentless deployment sync task: ${e.message}`); + return { + state: {}, + schedule: SCHEDULE, + }; + } + }, + cancel: async () => { + logger.warn('timed out'); + }, + }; + }; +} + +export class AgentlessConnectorDeploymentsSyncService { + private logger: Logger; + + constructor(logger: Logger) { + this.logger = logger; + } + public registerInfraSyncTask( + plugins: SearchConnectorsPluginSetupDependencies, + coreStart: CoreStart, + searchConnectorsPluginStartDependencies: SearchConnectorsPluginStartDependencies + ) { + const taskManager = plugins.taskManager; + + const esClient = coreStart.elasticsearch.client.asInternalUser; + const savedObjects = coreStart.savedObjects; + + const agentPolicyService = searchConnectorsPluginStartDependencies.fleet.agentPolicyService; + const packagePolicyService = searchConnectorsPluginStartDependencies.fleet.packagePolicyService; + + const soClient = new SavedObjectsClient(savedObjects.createInternalRepository()); + + const service = new AgentlessConnectorsInfraService( + soClient, + esClient, + packagePolicyService, + agentPolicyService, + this.logger + ); + + taskManager.registerTaskDefinitions({ + [AGENTLESS_CONNECTOR_DEPLOYMENTS_SYNC_TASK_TYPE]: { + title: 'Agentless Connector Deployment Manager', + description: + 'This task peridocally checks native connectors, agent policies and syncs them if they are out of sync', + timeout: '1m', + maxAttempts: 3, + createTaskRunner: infraSyncTaskRunner( + this.logger, + service, + searchConnectorsPluginStartDependencies.licensing + ), + }, + }); + } + + public async scheduleInfraSyncTask( + config: SearchConnectorsConfig, + taskManager: TaskManagerStartContract + ): Promise { + this.logger.info(`Scheduling ${AGENTLESS_CONNECTOR_DEPLOYMENTS_SYNC_TASK_ID}`); + try { + await taskManager.removeIfExists(AGENTLESS_CONNECTOR_DEPLOYMENTS_SYNC_TASK_ID); + const taskInstance = await taskManager.ensureScheduled({ + id: AGENTLESS_CONNECTOR_DEPLOYMENTS_SYNC_TASK_ID, + taskType: AGENTLESS_CONNECTOR_DEPLOYMENTS_SYNC_TASK_TYPE, + schedule: SCHEDULE, + params: {}, + state: {}, + scope: ['search', 'connectors'], + }); + + this.logger.info( + `Scheduled ${AGENTLESS_CONNECTOR_DEPLOYMENTS_SYNC_TASK_ID} with interval ${taskInstance.schedule?.interval}` + ); + + return taskInstance; + } catch (e) { + this.logger.error( + `Error scheduling ${AGENTLESS_CONNECTOR_DEPLOYMENTS_SYNC_TASK_ID}, received ${e.message}` + ); + return null; + } + } +} diff --git a/x-pack/solutions/search/plugins/search_connectors/server/types.ts b/x-pack/solutions/search/plugins/search_connectors/server/types.ts index 36b5aa877fd1e..cfd29370fee5f 100644 --- a/x-pack/solutions/search/plugins/search_connectors/server/types.ts +++ b/x-pack/solutions/search/plugins/search_connectors/server/types.ts @@ -6,6 +6,13 @@ */ import { ConnectorServerSideDefinition } from '@kbn/search-connectors'; +import type { FleetStartContract, FleetSetupContract } from '@kbn/fleet-plugin/server'; +import { + TaskManagerSetupContract, + TaskManagerStartContract, +} from '@kbn/task-manager-plugin/server'; +import { SavedObjectsServiceSetup, SavedObjectsServiceStart } from '@kbn/core-saved-objects-server'; +import { LicensingPluginStart } from '@kbn/licensing-plugin/server'; /* eslint-disable @typescript-eslint/no-empty-interface */ @@ -15,5 +22,14 @@ export interface SearchConnectorsPluginSetup { export interface SearchConnectorsPluginStart {} -export interface StartDependencies {} -export interface SetupDependencies {} +export interface SearchConnectorsPluginStartDependencies { + fleet: FleetStartContract; + taskManager: TaskManagerStartContract; + soClient: SavedObjectsServiceStart; + licensing: LicensingPluginStart; +} +export interface SearchConnectorsPluginSetupDependencies { + fleet: FleetSetupContract; + taskManager: TaskManagerSetupContract; + soClient: SavedObjectsServiceSetup; +} diff --git a/x-pack/solutions/search/plugins/search_connectors/tsconfig.json b/x-pack/solutions/search/plugins/search_connectors/tsconfig.json index ba6f6d85f5102..db177b77269ab 100644 --- a/x-pack/solutions/search/plugins/search_connectors/tsconfig.json +++ b/x-pack/solutions/search/plugins/search_connectors/tsconfig.json @@ -19,5 +19,13 @@ "@kbn/config-schema", "@kbn/core-http-browser", "@kbn/search-connectors", + "@kbn/task-manager-plugin", + "@kbn/fleet-plugin", + "@kbn/core-saved-objects-api-server", + "@kbn/core-elasticsearch-client-server-mocks", + "@kbn/logging-mocks", + "@kbn/core-elasticsearch-server", + "@kbn/licensing-plugin", + "@kbn/core-saved-objects-server", ] } diff --git a/x-pack/test/plugin_api_integration/test_suites/task_manager/check_registered_task_types.ts b/x-pack/test/plugin_api_integration/test_suites/task_manager/check_registered_task_types.ts index 682b2b9ec5f7d..1f9664907b40c 100644 --- a/x-pack/test/plugin_api_integration/test_suites/task_manager/check_registered_task_types.ts +++ b/x-pack/test/plugin_api_integration/test_suites/task_manager/check_registered_task_types.ts @@ -161,6 +161,7 @@ export default function ({ getService }: FtrProviderContext) { 'osquery:telemetry-saved-queries', 'report:execute', 'risk_engine:risk_scoring', + 'search:agentless-connectors-manager', 'security-solution-ea-asset-criticality-ecs-migration', 'security:endpoint-diagnostics', 'security:endpoint-meta-telemetry', From 022900809a9f3c291bea2957cc3572701c5f1589 Mon Sep 17 00:00:00 2001 From: Irene Blanco Date: Fri, 10 Jan 2025 12:30:00 +0100 Subject: [PATCH 12/42] [APM] Fix Cypress flaky test in Custom Links (#206108) ## Summary Fixes https://github.com/elastic/kibana/issues/206016 This PR aims to fix a flaky test that waits for an empty state screen, which sometimes fails to appear. The issue might happen because the data deletion action, which triggers the empty state, is not completed properly. This action takes place in the previous test and does not wait for the deletion to finish. The proposed solution ensures that the test responsible for deleting the data waits until the empty state appears after deletion, preventing the next test from running too soon. ### Test locally ````` node x-pack/solutions/observability/plugins/apm/scripts/test/e2e.js --server node x-pack/solutions/observability/plugins/apm/scripts/test/e2e.js --runner --open ````` Run the [custom_links.cy.ts](http://localhost:5620/__/#/specs/runner?file=cypress/e2e/settings/custom_links.cy.ts) test. --- .../apm/ftr_e2e/cypress/e2e/settings/custom_links.cy.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/x-pack/solutions/observability/plugins/apm/ftr_e2e/cypress/e2e/settings/custom_links.cy.ts b/x-pack/solutions/observability/plugins/apm/ftr_e2e/cypress/e2e/settings/custom_links.cy.ts index 567820ea6b70b..4023d00ceb960 100644 --- a/x-pack/solutions/observability/plugins/apm/ftr_e2e/cypress/e2e/settings/custom_links.cy.ts +++ b/x-pack/solutions/observability/plugins/apm/ftr_e2e/cypress/e2e/settings/custom_links.cy.ts @@ -109,14 +109,12 @@ describe('Custom links', () => { cy.visitKibana(basePath); cy.getByTestSubj('editCustomLink').click(); cy.contains('Delete').click(); + cy.getByTestSubj('customLinksEmptyPrompt').should('be.visible'); }); it('clears filter values when field is selected', () => { cy.visitKibana(basePath); - // wait for empty prompt - cy.getByTestSubj('customLinksEmptyPrompt').should('be.visible'); - cy.contains('Create custom link').click(); cy.getByTestSubj('filter-0').select('service.name'); cy.get('[data-test-subj="service.name.value"] [data-test-subj="comboBoxSearchInput"]').type( From 64b6a1a5e7f779e61799186ff3605beaeb7227e3 Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Fri, 10 Jan 2025 12:30:37 +0100 Subject: [PATCH 13/42] Sustainable Kibana Architecture: Move the rest of shared-ux modules (#205924) ## Summary This PR aims at relocating some of the Kibana modules (plugins and packages) into a new folder structure, according to the _Sustainable Kibana Architecture_ initiative. > [!IMPORTANT] > * We kindly ask you to: > * Manually fix the errors in the error section below (if there are any). > * Search for the `packages[\/\\]` and `plugins[\/\\]` patterns in the source code (Babel and Eslint config files), and update them appropriately. > * Manually review `.buildkite/scripts/pipelines/pull_request/pipeline.ts` to ensure that any CI pipeline customizations continue to be correctly applied after the changed path names > * Review all of the updated files, specially the `.ts` and `.js` files listed in the sections below, as some of them contain relative paths that have been updated. > * Think of potential impact of the move, including tooling and configuration files that can be pointing to the relocated modules. E.g.: > * customised eslint rules > * docs pointing to source code > [!NOTE] > * This PR has been auto-generated. > * Any manual contributions will be lost if the 'relocate' script is re-run. > * Try to obtain the missing reviews / approvals before applying manual fixes, and/or keep your changes in a .patch / git stash. > * Please use [#sustainable_kibana_architecture](https://elastic.slack.com/archives/C07TCKTA22E) Slack channel for feedback. Are you trying to rebase this PR to solve merge conflicts? Please follow the steps describe [here](https://elastic.slack.com/archives/C07TCKTA22E/p1734019532879269?thread_ts=1734019339.935419&cid=C07TCKTA22E). #### 1 plugin(s) are going to be relocated: | Id | Target folder | | -- | ------------- | | `@kbn/url-drilldown-plugin` | `x-pack/platform/plugins/private/drilldowns/url_drilldown` | #### 21 packages(s) are going to be relocated: | Id | Target folder | | -- | ------------- | | `@kbn/core-chrome-browser` | `src/core/packages/chrome/browser` | | `@kbn/deeplinks-shared` | `src/platform/packages/shared/deeplinks/shared` | | `@kbn/home-sample-data-card` | `src/platform/packages/shared/home/sample_data_card` | | `@kbn/home-sample-data-tab` | `src/platform/packages/shared/home/sample_data_tab` | | `@kbn/home-sample-data-types` | `src/platform/packages/shared/home/sample_data_types` | | `@kbn/guided-onboarding` | `src/platform/packages/shared/kbn-guided-onboarding` | | `@kbn/item-buffer` | `src/platform/packages/private/kbn-item-buffer` | | `@kbn/management-settings-section-registry` | `src/platform/packages/shared/kbn-management/settings/section_registry` | | `@kbn/management-settings-ids` | `src/platform/packages/shared/kbn-management/settings/setting_ids` | | `@kbn/react-mute-legacy-root-warning` | `src/platform/packages/private/kbn-react-mute-legacy-root-warning` | | `@kbn/saved-objects-settings` | `src/platform/packages/private/kbn-saved-objects-settings` | | `@kbn/react-kibana-context-common` | `src/platform/packages/shared/react/kibana_context/common` | | `@kbn/react-kibana-context-render` | `src/platform/packages/shared/react/kibana_context/render` | | `@kbn/react-kibana-context-root` | `src/platform/packages/shared/react/kibana_context/root` | | `@kbn/react-kibana-context-styled` | `src/platform/packages/shared/react/kibana_context/styled` | | `@kbn/react-kibana-context-theme` | `src/platform/packages/shared/react/kibana_context/theme` | | `@kbn/react-kibana-mount` | `src/platform/packages/shared/react/kibana_mount` | | `@kbn/serverless-project-switcher` | `src/platform/packages/private/serverless/project_switcher` | | `@kbn/serverless-common-settings` | `src/platform/packages/private/serverless/settings/common` | | `@kbn/serverless-observability-settings` | `src/platform/packages/shared/serverless/settings/observability_project` | | `@kbn/serverless-types` | `src/platform/packages/private/serverless/types` |
Updated relative paths ``` src/core/packages/chrome/browser/jest.config.js:12 src/core/packages/chrome/browser/tsconfig.json:2 src/core/packages/chrome/browser/tsconfig.type_check.json:2 src/core/packages/chrome/browser/tsconfig.type_check.json:21 src/core/packages/chrome/browser/tsconfig.type_check.json:24 src/core/packages/chrome/browser/tsconfig.type_check.json:27 src/core/packages/chrome/browser/tsconfig.type_check.json:30 src/core/packages/chrome/browser/tsconfig.type_check.json:33 src/core/packages/chrome/browser/tsconfig.type_check.json:36 src/core/packages/chrome/browser/tsconfig.type_check.json:39 src/core/packages/chrome/browser/tsconfig.type_check.json:42 src/core/packages/chrome/browser/tsconfig.type_check.json:45 src/core/packages/chrome/browser/tsconfig.type_check.json:48 src/core/packages/chrome/browser/tsconfig.type_check.json:51 src/core/packages/chrome/browser/tsconfig.type_check.json:54 src/platform/packages/private/kbn-item-buffer/jest.config.js:12 src/platform/packages/private/kbn-item-buffer/tsconfig.json:2 src/platform/packages/private/kbn-react-mute-legacy-root-warning/jest.config.js:12 src/platform/packages/private/kbn-react-mute-legacy-root-warning/tsconfig.json:2 src/platform/packages/private/kbn-saved-objects-settings/jest.config.js:12 src/platform/packages/private/kbn-saved-objects-settings/tsconfig.json:2 src/platform/packages/private/kbn-saved-objects-settings/tsconfig.type_check.json:2 src/platform/packages/private/serverless/project_switcher/jest.config.js:12 src/platform/packages/private/serverless/project_switcher/tsconfig.json:2 src/platform/packages/private/serverless/project_switcher/tsconfig.type_check.json:2 src/platform/packages/private/serverless/project_switcher/tsconfig.type_check.json:23 src/platform/packages/private/serverless/settings/common/tsconfig.json:2 src/platform/packages/private/serverless/settings/common/tsconfig.type_check.json:2 src/platform/packages/private/serverless/settings/common/tsconfig.type_check.json:20 src/platform/packages/private/serverless/types/tsconfig.json:2 src/platform/packages/private/serverless/types/tsconfig.type_check.json:2 src/platform/packages/shared/deeplinks/shared/jest.config.js:12 src/platform/packages/shared/deeplinks/shared/tsconfig.json:2 src/platform/packages/shared/deeplinks/shared/tsconfig.type_check.json:2 src/platform/packages/shared/home/sample_data_card/jest.config.js:12 src/platform/packages/shared/home/sample_data_card/tsconfig.json:2 src/platform/packages/shared/home/sample_data_card/tsconfig.type_check.json:2 src/platform/packages/shared/home/sample_data_card/tsconfig.type_check.json:23 src/platform/packages/shared/home/sample_data_card/tsconfig.type_check.json:29 src/platform/packages/shared/home/sample_data_tab/jest.config.js:12 src/platform/packages/shared/home/sample_data_tab/tsconfig.json:2 src/platform/packages/shared/home/sample_data_tab/tsconfig.type_check.json:2 src/platform/packages/shared/home/sample_data_tab/tsconfig.type_check.json:23 src/platform/packages/shared/home/sample_data_types/jest.config.js:12 src/platform/packages/shared/home/sample_data_types/tsconfig.json:2 src/platform/packages/shared/home/sample_data_types/tsconfig.type_check.json:2 src/platform/packages/shared/kbn-guided-onboarding/jest.config.js:12 src/platform/packages/shared/kbn-guided-onboarding/tsconfig.json:2 src/platform/packages/shared/kbn-guided-onboarding/tsconfig.type_check.json:2 src/platform/packages/shared/kbn-management/settings/section_registry/tsconfig.json:2 src/platform/packages/shared/kbn-management/settings/section_registry/tsconfig.type_check.json:2 src/platform/packages/shared/kbn-management/settings/section_registry/tsconfig.type_check.json:22 src/platform/packages/shared/kbn-management/settings/section_registry/tsconfig.type_check.json:25 src/platform/packages/shared/kbn-management/settings/setting_ids/tsconfig.json:2 src/platform/packages/shared/kbn-management/settings/setting_ids/tsconfig.type_check.json:2 src/platform/packages/shared/react/kibana_context/common/jest.config.js:12 src/platform/packages/shared/react/kibana_context/common/tsconfig.json:2 src/platform/packages/shared/react/kibana_context/common/tsconfig.type_check.json:2 src/platform/packages/shared/react/kibana_context/render/jest.config.js:12 src/platform/packages/shared/react/kibana_context/render/tsconfig.json:2 src/platform/packages/shared/react/kibana_context/render/tsconfig.type_check.json:2 src/platform/packages/shared/react/kibana_context/render/tsconfig.type_check.json:25 src/platform/packages/shared/react/kibana_context/root/jest.config.js:12 src/platform/packages/shared/react/kibana_context/root/tsconfig.json:2 src/platform/packages/shared/react/kibana_context/root/tsconfig.type_check.json:2 src/platform/packages/shared/react/kibana_context/root/tsconfig.type_check.json:22 src/platform/packages/shared/react/kibana_context/root/tsconfig.type_check.json:28 src/platform/packages/shared/react/kibana_context/root/tsconfig.type_check.json:31 src/platform/packages/shared/react/kibana_context/root/tsconfig.type_check.json:34 src/platform/packages/shared/react/kibana_context/root/tsconfig.type_check.json:37 src/platform/packages/shared/react/kibana_context/root/tsconfig.type_check.json:40 src/platform/packages/shared/react/kibana_context/styled/jest.config.js:12 src/platform/packages/shared/react/kibana_context/styled/tsconfig.json:2 src/platform/packages/shared/react/kibana_context/styled/tsconfig.type_check.json:2 src/platform/packages/shared/react/kibana_context/styled/tsconfig.type_check.json:22 src/platform/packages/shared/react/kibana_context/theme/jest.config.js:12 src/platform/packages/shared/react/kibana_context/theme/tsconfig.json:2 src/platform/packages/shared/react/kibana_context/theme/tsconfig.type_check.json:2 src/platform/packages/shared/react/kibana_context/theme/tsconfig.type_check.json:22 src/platform/packages/shared/react/kibana_mount/jest.config.js:12 src/platform/packages/shared/react/kibana_mount/tsconfig.json:2 src/platform/packages/shared/react/kibana_mount/tsconfig.type_check.json:2 src/platform/packages/shared/react/kibana_mount/tsconfig.type_check.json:22 src/platform/packages/shared/react/kibana_mount/tsconfig.type_check.json:25 src/platform/packages/shared/react/kibana_mount/tsconfig.type_check.json:28 src/platform/packages/shared/react/kibana_mount/tsconfig.type_check.json:34 src/platform/packages/shared/serverless/settings/observability_project/tsconfig.json:2 src/platform/packages/shared/serverless/settings/observability_project/tsconfig.type_check.json:2 src/platform/packages/shared/serverless/settings/observability_project/tsconfig.type_check.json:20 x-pack/platform/plugins/private/drilldowns/url_drilldown/tsconfig.json:2 ```
--------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Elastic Machine --- .github/CODEOWNERS | 44 ++++----- .i18nrc.json | 6 +- docs/developer/plugin-list.asciidoc | 2 +- package.json | 44 ++++----- .../styled_components_files.js | 2 +- packages/kbn-test/jest-preset.js | 2 +- .../kibana_context/common/jest.config.js | 14 --- .../kibana_context/render/jest.config.js | 14 --- .../react/kibana_context/root/jest.config.js | 14 --- .../kibana_context/styled/jest.config.js | 14 --- .../react/kibana_context/theme/jest.config.js | 14 --- packages/react/kibana_mount/jest.config.js | 14 --- .../project_switcher/jest.config.js | 14 --- .../core/packages/chrome/browser}/README.md | 0 .../core/packages/chrome/browser}/index.ts | 0 .../packages/chrome/browser}/jest.config.js | 4 +- .../packages/chrome/browser}/kibana.jsonc | 0 .../packages/chrome/browser}/package.json | 0 .../chrome/browser}/src/breadcrumb.ts | 0 .../packages/chrome/browser}/src/contracts.ts | 0 .../packages/chrome/browser}/src/doc_title.ts | 0 .../chrome/browser}/src/help_extension.ts | 0 .../packages/chrome/browser}/src/index.ts | 0 .../chrome/browser}/src/nav_controls.ts | 0 .../packages/chrome/browser}/src/nav_links.ts | 0 .../chrome/browser}/src/project_navigation.ts | 0 .../chrome/browser}/src/recently_accessed.ts | 0 .../packages/chrome/browser}/src/types.ts | 0 .../packages/chrome/browser}/tsconfig.json | 2 +- .../private}/kbn-item-buffer/README.md | 0 .../private}/kbn-item-buffer/index.ts | 0 .../private/kbn-item-buffer}/jest.config.js | 4 +- .../private}/kbn-item-buffer/kibana.jsonc | 0 .../private}/kbn-item-buffer/package.json | 0 .../src/__test__/run_item_buffer_tests.ts | 0 .../private}/kbn-item-buffer/src/index.ts | 0 .../kbn-item-buffer/src/item_buffer.test.ts | 0 .../kbn-item-buffer/src/item_buffer.ts | 0 .../src/timed_item_buffer.test.ts | 0 .../kbn-item-buffer/src/timed_item_buffer.ts | 0 .../private/kbn-item-buffer}/tsconfig.json | 2 +- .../README.md | 0 .../index.ts | 0 .../jest.config.js | 14 +++ .../kibana.jsonc | 0 .../package.json | 0 .../tsconfig.json | 2 +- .../kbn-saved-objects-settings/README.md | 0 .../kbn-saved-objects-settings/index.ts | 0 .../jest.config.js | 4 +- .../kbn-saved-objects-settings/kibana.jsonc | 0 .../kbn-saved-objects-settings/package.json | 0 .../kbn-saved-objects-settings/tsconfig.json | 2 +- .../kbn-ui-shared-deps-src/BUILD.bazel | 8 +- .../serverless/project_switcher/README.mdx | 0 .../serverless/project_switcher/index.ts | 0 .../project_switcher/jest.config.js | 14 +++ .../serverless/project_switcher/kibana.jsonc | 0 .../project_switcher/mocks/jest.mock.ts | 0 .../project_switcher/mocks/storybook.mock.ts | 0 .../serverless/project_switcher/package.json | 0 .../project_switcher/src/constants.ts | 0 .../project_switcher/src/header_button.tsx | 0 .../serverless/project_switcher/src/index.ts | 0 .../serverless/project_switcher/src/item.tsx | 0 .../project_switcher/src/loader.tsx | 0 .../serverless/project_switcher/src/logo.tsx | 0 .../project_switcher/src/services.tsx | 0 .../src/switcher.component.tsx | 0 .../project_switcher/src/switcher.stories.tsx | 0 .../project_switcher/src/switcher.test.tsx | 0 .../project_switcher/src/switcher.tsx | 0 .../serverless/project_switcher/src/types.ts | 0 .../serverless/project_switcher/tsconfig.json | 2 +- .../serverless/settings/common/README.mdx | 0 .../serverless/settings/common/index.ts | 0 .../serverless/settings/common/kibana.jsonc | 0 .../serverless/settings/common/package.json | 0 .../serverless/settings/common/tsconfig.json | 2 +- .../private}/serverless/types/README.mdx | 0 .../private}/serverless/types/index.d.ts | 0 .../private}/serverless/types/kibana.jsonc | 0 .../private}/serverless/types/package.json | 0 .../private}/serverless/types/tsconfig.json | 2 +- .../shared}/deeplinks/shared/README.md | 0 .../shared}/deeplinks/shared/deep_links.ts | 0 .../shared}/deeplinks/shared/index.ts | 0 .../shared/deeplinks/shared}/jest.config.js | 4 +- .../shared}/deeplinks/shared/kibana.jsonc | 0 .../shared}/deeplinks/shared/package.json | 0 .../shared}/deeplinks/shared/tsconfig.json | 2 +- .../shared}/home/sample_data_card/README.mdx | 0 .../shared}/home/sample_data_card/index.ts | 0 .../home/sample_data_card/jest.config.js | 14 +++ .../home/sample_data_card/kibana.jsonc | 0 .../home/sample_data_card/package.json | 0 .../sample_data_card.test.tsx.snap | 0 .../home/sample_data_card/src/constants.ts | 0 .../disabled_footer.test.tsx.snap | 0 .../install_footer.test.tsx.snap | 0 .../__snapshots__/remove_footer.test.tsx.snap | 0 .../__snapshots__/view_button.test.tsx.snap | 0 .../src/footer/disabled_footer.test.tsx | 0 .../src/footer/disabled_footer.tsx | 0 .../src/footer/footer.stories.tsx | 0 .../sample_data_card/src/footer/index.tsx | 0 .../src/footer/install_footer.test.tsx | 0 .../src/footer/install_footer.tsx | 0 .../src/footer/remove_footer.test.tsx | 0 .../src/footer/remove_footer.tsx | 0 .../src/footer/view_button.test.tsx | 0 .../src/footer/view_button.tsx | 0 .../home/sample_data_card/src/hooks/index.ts | 0 .../sample_data_card/src/hooks/use_install.ts | 0 .../sample_data_card/src/hooks/use_remove.ts | 0 .../sample_data_card/src/mocks/dashboard.png | Bin .../src/mocks/dashboard_dark.png | Bin .../home/sample_data_card/src/mocks/icon.svg | 0 .../home/sample_data_card/src/mocks/index.ts | 0 .../src/sample_data_card.component.tsx | 0 .../src/sample_data_card.stories.tsx | 0 .../src/sample_data_card.test.tsx | 0 .../sample_data_card/src/sample_data_card.tsx | 0 .../home/sample_data_card/src/services.tsx | 0 .../home/sample_data_card/tsconfig.json | 2 +- .../shared}/home/sample_data_tab/README.mdx | 0 .../shared}/home/sample_data_tab/index.ts | 0 .../home/sample_data_tab/jest.config.js | 4 +- .../shared}/home/sample_data_tab/kibana.jsonc | 0 .../shared}/home/sample_data_tab/package.json | 0 .../src/assets/welcome_dark.png | Bin .../src/assets/welcome_light.png | Bin .../home/sample_data_tab/src/constants.ts | 0 .../src/demo_env_panel.stories.tsx | 0 .../sample_data_tab/src/demo_env_panel.tsx | 0 .../home/sample_data_tab/src/hooks/index.ts | 0 .../sample_data_tab/src/hooks/use_list.ts | 0 .../shared}/home/sample_data_tab/src/mocks.ts | 0 .../sample_data_tab/src/sample_data_cards.tsx | 0 .../src/sample_data_tab.stories.tsx | 0 .../sample_data_tab/src/sample_data_tab.tsx | 0 .../home/sample_data_tab/src/services.tsx | 0 .../home/sample_data_tab/tsconfig.json | 2 +- .../shared}/home/sample_data_types/README.mdx | 0 .../shared}/home/sample_data_types/index.d.ts | 0 .../home/sample_data_types}/jest.config.js | 4 +- .../home/sample_data_types/kibana.jsonc | 0 .../home/sample_data_types/package.json | 0 .../home/sample_data_types/tsconfig.json | 2 +- .../shared}/kbn-guided-onboarding/README.md | 0 .../kbn-guided-onboarding/classic/index.ts | 0 .../kbn-guided-onboarding/guide/index.ts | 0 .../shared}/kbn-guided-onboarding/index.ts | 0 .../kbn-guided-onboarding}/jest.config.js | 4 +- .../kbn-guided-onboarding/kibana.jsonc | 0 .../kbn-guided-onboarding/package.json | 0 .../src/common/test_guide_config.ts | 0 .../__snapshots__/guide_cards.test.tsx.snap | 0 .../classic_version/guide_card.tsx | 0 .../classic_version/guide_cards.constants.tsx | 0 .../classic_version/guide_cards.test.tsx | 0 .../classic_version/guide_cards.tsx | 0 .../classic_version/guide_filters.tsx | 0 .../landing_page/guide/guide_card.tsx | 0 .../guide/guide_cards.constants.tsx | 0 .../landing_page/guide/guide_cards.tsx | 0 .../landing_page/guide/guide_filters.tsx | 0 .../kbn-guided-onboarding/src/types.ts | 0 .../kbn-guided-onboarding/tsconfig.json | 2 +- .../settings/section_registry/README.mdx | 0 .../settings/section_registry/index.ts | 0 .../settings/section_registry/kibana.jsonc | 0 .../settings/section_registry/package.json | 0 .../section_registry.test.tsx | 0 .../section_registry/section_registry.ts | 0 .../settings/section_registry/tsconfig.json | 2 +- .../settings/setting_ids/README.mdx | 0 .../settings/setting_ids/index.ts | 0 .../settings/setting_ids/kibana.jsonc | 0 .../settings/setting_ids/package.json | 0 .../settings/setting_ids/tsconfig.json | 2 +- .../react/kibana_context/common/BUILD.bazel | 0 .../react/kibana_context/common/README.mdx | 0 .../kibana_context/common/color_mode.test.ts | 0 .../react/kibana_context/common/color_mode.ts | 0 .../react/kibana_context/common/index.ts | 0 .../kibana_context/common}/jest.config.js | 4 +- .../react/kibana_context/common/kibana.jsonc | 0 .../react/kibana_context/common/package.json | 0 .../react/kibana_context/common/theme.ts | 0 .../react/kibana_context/common/tsconfig.json | 2 +- .../react/kibana_context/common/types.ts | 0 .../react/kibana_context/render/BUILD.bazel | 4 +- .../react/kibana_context/render/README.mdx | 0 .../react/kibana_context/render/index.ts | 0 .../kibana_context/render/jest.config.js | 14 +++ .../react/kibana_context/render/kibana.jsonc | 0 .../react/kibana_context/render/package.json | 0 .../kibana_context/render/render_provider.tsx | 0 .../react/kibana_context/render/tsconfig.json | 2 +- .../react/kibana_context/root/BUILD.bazel | 0 .../react/kibana_context/root/README.mdx | 0 .../kibana_context/root/eui_provider.test.tsx | 0 .../kibana_context/root/eui_provider.tsx | 0 .../react/kibana_context/root/index.ts | 0 .../react/kibana_context/root/jest.config.js | 14 +++ .../react/kibana_context/root/kibana.jsonc | 4 +- .../react/kibana_context/root/package.json | 0 .../root/root_provider.test.tsx | 0 .../kibana_context/root/root_provider.tsx | 0 .../react/kibana_context/root/tsconfig.json | 2 +- .../react/kibana_context/styled/README.mdx | 0 .../react/kibana_context/styled/index.ts | 0 .../kibana_context/styled/jest.config.js | 14 +++ .../react/kibana_context/styled/kibana.jsonc | 0 .../react/kibana_context/styled/package.json | 0 .../kibana_context/styled/styled_provider.tsx | 0 .../react/kibana_context/styled/tsconfig.json | 2 +- .../react/kibana_context/theme/BUILD.bazel | 4 +- .../react/kibana_context/theme/README.mdx | 0 .../react/kibana_context/theme/index.ts | 0 .../react/kibana_context/theme/jest.config.js | 14 +++ .../react/kibana_context/theme/kibana.jsonc | 0 .../react/kibana_context/theme/package.json | 0 .../theme/theme_provider.test.tsx | 0 .../kibana_context/theme/theme_provider.tsx | 0 .../react/kibana_context/theme/tsconfig.json | 2 +- .../react/kibana_context/theme/with_theme.tsx | 0 .../shared}/react/kibana_mount/README.md | 0 .../shared}/react/kibana_mount/index.ts | 0 .../shared/react/kibana_mount}/jest.config.js | 4 +- .../shared}/react/kibana_mount/kibana.jsonc | 0 .../kibana_mount/mount_point_portal.test.tsx | 0 .../react/kibana_mount/mount_point_portal.tsx | 0 .../shared}/react/kibana_mount/package.json | 0 .../test_helpers/react_mount_serializer.ts | 0 .../kibana_mount/to_mount_point.test.tsx | 0 .../react/kibana_mount/to_mount_point.tsx | 0 .../shared}/react/kibana_mount/tsconfig.json | 2 +- .../shared}/react/kibana_mount/utils.ts | 0 .../settings/observability_project/README.mdx | 0 .../settings/observability_project/index.ts | 0 .../observability_project/kibana.jsonc | 0 .../observability_project/package.json | 0 .../observability_project/tsconfig.json | 2 +- .../shared/guided_onboarding/README.md | 6 +- .../plugins/shared/home/.storybook/main.ts | 2 +- tsconfig.base.json | 88 +++++++++--------- x-pack/.i18nrc.json | 2 +- .../drilldowns/url_drilldown/README.md | 0 .../drilldowns/url_drilldown/jest.config.js | 18 ++++ .../drilldowns/url_drilldown/kibana.jsonc | 0 .../drilldowns/url_drilldown/public/index.ts | 0 .../url_drilldown/public/lib/i18n.ts | 0 .../url_drilldown/public/lib/index.ts | 0 .../url_drilldown/public/lib/test/data.ts | 0 .../public/lib/url_drilldown.test.tsx | 0 .../public/lib/url_drilldown.tsx | 0 .../lib/variables/context_variables.test.ts | 0 .../public/lib/variables/context_variables.ts | 0 .../lib/variables/event_variables.test.ts | 0 .../public/lib/variables/event_variables.ts | 0 .../public/lib/variables/global_variables.ts | 0 .../public/lib/variables/i18n.ts | 0 .../public/lib/variables/util.ts | 0 .../drilldowns/url_drilldown/public/plugin.ts | 0 .../drilldowns/url_drilldown/tsconfig.json | 2 +- .../public/trigger_actions/trigger_utils.ts | 2 +- x-pack/plugins/drilldowns/jest.config.js | 15 --- .../test_suites/common/core/ui_settings.ts | 2 +- yarn.lock | 44 ++++----- 271 files changed, 289 insertions(+), 286 deletions(-) delete mode 100644 packages/react/kibana_context/common/jest.config.js delete mode 100644 packages/react/kibana_context/render/jest.config.js delete mode 100644 packages/react/kibana_context/root/jest.config.js delete mode 100644 packages/react/kibana_context/styled/jest.config.js delete mode 100644 packages/react/kibana_context/theme/jest.config.js delete mode 100644 packages/react/kibana_mount/jest.config.js delete mode 100644 packages/serverless/project_switcher/jest.config.js rename {packages/core/chrome/core-chrome-browser => src/core/packages/chrome/browser}/README.md (100%) rename {packages/core/chrome/core-chrome-browser => src/core/packages/chrome/browser}/index.ts (100%) rename {packages/kbn-guided-onboarding => src/core/packages/chrome/browser}/jest.config.js (85%) rename {packages/core/chrome/core-chrome-browser => src/core/packages/chrome/browser}/kibana.jsonc (100%) rename {packages/core/chrome/core-chrome-browser => src/core/packages/chrome/browser}/package.json (100%) rename {packages/core/chrome/core-chrome-browser => src/core/packages/chrome/browser}/src/breadcrumb.ts (100%) rename {packages/core/chrome/core-chrome-browser => src/core/packages/chrome/browser}/src/contracts.ts (100%) rename {packages/core/chrome/core-chrome-browser => src/core/packages/chrome/browser}/src/doc_title.ts (100%) rename {packages/core/chrome/core-chrome-browser => src/core/packages/chrome/browser}/src/help_extension.ts (100%) rename {packages/core/chrome/core-chrome-browser => src/core/packages/chrome/browser}/src/index.ts (100%) rename {packages/core/chrome/core-chrome-browser => src/core/packages/chrome/browser}/src/nav_controls.ts (100%) rename {packages/core/chrome/core-chrome-browser => src/core/packages/chrome/browser}/src/nav_links.ts (100%) rename {packages/core/chrome/core-chrome-browser => src/core/packages/chrome/browser}/src/project_navigation.ts (100%) rename {packages/core/chrome/core-chrome-browser => src/core/packages/chrome/browser}/src/recently_accessed.ts (100%) rename {packages/core/chrome/core-chrome-browser => src/core/packages/chrome/browser}/src/types.ts (100%) rename {packages/core/chrome/core-chrome-browser => src/core/packages/chrome/browser}/tsconfig.json (92%) rename {packages => src/platform/packages/private}/kbn-item-buffer/README.md (100%) rename {packages => src/platform/packages/private}/kbn-item-buffer/index.ts (100%) rename {packages/home/sample_data_types => src/platform/packages/private/kbn-item-buffer}/jest.config.js (84%) rename {packages => src/platform/packages/private}/kbn-item-buffer/kibana.jsonc (100%) rename {packages => src/platform/packages/private}/kbn-item-buffer/package.json (100%) rename {packages => src/platform/packages/private}/kbn-item-buffer/src/__test__/run_item_buffer_tests.ts (100%) rename {packages => src/platform/packages/private}/kbn-item-buffer/src/index.ts (100%) rename {packages => src/platform/packages/private}/kbn-item-buffer/src/item_buffer.test.ts (100%) rename {packages => src/platform/packages/private}/kbn-item-buffer/src/item_buffer.ts (100%) rename {packages => src/platform/packages/private}/kbn-item-buffer/src/timed_item_buffer.test.ts (100%) rename {packages => src/platform/packages/private}/kbn-item-buffer/src/timed_item_buffer.ts (100%) rename {packages/kbn-react-mute-legacy-root-warning => src/platform/packages/private/kbn-item-buffer}/tsconfig.json (80%) rename {packages => src/platform/packages/private}/kbn-react-mute-legacy-root-warning/README.md (100%) rename {packages => src/platform/packages/private}/kbn-react-mute-legacy-root-warning/index.ts (100%) create mode 100644 src/platform/packages/private/kbn-react-mute-legacy-root-warning/jest.config.js rename {packages => src/platform/packages/private}/kbn-react-mute-legacy-root-warning/kibana.jsonc (100%) rename {packages => src/platform/packages/private}/kbn-react-mute-legacy-root-warning/package.json (100%) rename {packages/kbn-item-buffer => src/platform/packages/private/kbn-react-mute-legacy-root-warning}/tsconfig.json (80%) rename {packages => src/platform/packages/private}/kbn-saved-objects-settings/README.md (100%) rename {packages => src/platform/packages/private}/kbn-saved-objects-settings/index.ts (100%) rename {packages/kbn-react-mute-legacy-root-warning => src/platform/packages/private/kbn-saved-objects-settings}/jest.config.js (82%) rename {packages => src/platform/packages/private}/kbn-saved-objects-settings/kibana.jsonc (100%) rename {packages => src/platform/packages/private}/kbn-saved-objects-settings/package.json (100%) rename {packages => src/platform/packages/private}/kbn-saved-objects-settings/tsconfig.json (80%) rename {packages => src/platform/packages/private}/serverless/project_switcher/README.mdx (100%) rename {packages => src/platform/packages/private}/serverless/project_switcher/index.ts (100%) create mode 100644 src/platform/packages/private/serverless/project_switcher/jest.config.js rename {packages => src/platform/packages/private}/serverless/project_switcher/kibana.jsonc (100%) rename {packages => src/platform/packages/private}/serverless/project_switcher/mocks/jest.mock.ts (100%) rename {packages => src/platform/packages/private}/serverless/project_switcher/mocks/storybook.mock.ts (100%) rename {packages => src/platform/packages/private}/serverless/project_switcher/package.json (100%) rename {packages => src/platform/packages/private}/serverless/project_switcher/src/constants.ts (100%) rename {packages => src/platform/packages/private}/serverless/project_switcher/src/header_button.tsx (100%) rename {packages => src/platform/packages/private}/serverless/project_switcher/src/index.ts (100%) rename {packages => src/platform/packages/private}/serverless/project_switcher/src/item.tsx (100%) rename {packages => src/platform/packages/private}/serverless/project_switcher/src/loader.tsx (100%) rename {packages => src/platform/packages/private}/serverless/project_switcher/src/logo.tsx (100%) rename {packages => src/platform/packages/private}/serverless/project_switcher/src/services.tsx (100%) rename {packages => src/platform/packages/private}/serverless/project_switcher/src/switcher.component.tsx (100%) rename {packages => src/platform/packages/private}/serverless/project_switcher/src/switcher.stories.tsx (100%) rename {packages => src/platform/packages/private}/serverless/project_switcher/src/switcher.test.tsx (100%) rename {packages => src/platform/packages/private}/serverless/project_switcher/src/switcher.tsx (100%) rename {packages => src/platform/packages/private}/serverless/project_switcher/src/types.ts (100%) rename {packages => src/platform/packages/private}/serverless/project_switcher/tsconfig.json (86%) rename {packages => src/platform/packages/private}/serverless/settings/common/README.mdx (100%) rename {packages => src/platform/packages/private}/serverless/settings/common/index.ts (100%) rename {packages => src/platform/packages/private}/serverless/settings/common/kibana.jsonc (100%) rename {packages => src/platform/packages/private}/serverless/settings/common/package.json (100%) rename {packages => src/platform/packages/private}/serverless/settings/common/tsconfig.json (81%) rename {packages => src/platform/packages/private}/serverless/types/README.mdx (100%) rename {packages => src/platform/packages/private}/serverless/types/index.d.ts (100%) rename {packages => src/platform/packages/private}/serverless/types/kibana.jsonc (100%) rename {packages => src/platform/packages/private}/serverless/types/package.json (100%) rename {packages => src/platform/packages/private}/serverless/types/tsconfig.json (79%) rename {packages => src/platform/packages/shared}/deeplinks/shared/README.md (100%) rename {packages => src/platform/packages/shared}/deeplinks/shared/deep_links.ts (100%) rename {packages => src/platform/packages/shared}/deeplinks/shared/index.ts (100%) rename {packages/core/chrome/core-chrome-browser => src/platform/packages/shared/deeplinks/shared}/jest.config.js (83%) rename {packages => src/platform/packages/shared}/deeplinks/shared/kibana.jsonc (100%) rename {packages => src/platform/packages/shared}/deeplinks/shared/package.json (100%) rename {packages => src/platform/packages/shared}/deeplinks/shared/tsconfig.json (81%) rename {packages => src/platform/packages/shared}/home/sample_data_card/README.mdx (100%) rename {packages => src/platform/packages/shared}/home/sample_data_card/index.ts (100%) create mode 100644 src/platform/packages/shared/home/sample_data_card/jest.config.js rename {packages => src/platform/packages/shared}/home/sample_data_card/kibana.jsonc (100%) rename {packages => src/platform/packages/shared}/home/sample_data_card/package.json (100%) rename {packages => src/platform/packages/shared}/home/sample_data_card/src/__snapshots__/sample_data_card.test.tsx.snap (100%) rename {packages => src/platform/packages/shared}/home/sample_data_card/src/constants.ts (100%) rename {packages => src/platform/packages/shared}/home/sample_data_card/src/footer/__snapshots__/disabled_footer.test.tsx.snap (100%) rename {packages => src/platform/packages/shared}/home/sample_data_card/src/footer/__snapshots__/install_footer.test.tsx.snap (100%) rename {packages => src/platform/packages/shared}/home/sample_data_card/src/footer/__snapshots__/remove_footer.test.tsx.snap (100%) rename {packages => src/platform/packages/shared}/home/sample_data_card/src/footer/__snapshots__/view_button.test.tsx.snap (100%) rename {packages => src/platform/packages/shared}/home/sample_data_card/src/footer/disabled_footer.test.tsx (100%) rename {packages => src/platform/packages/shared}/home/sample_data_card/src/footer/disabled_footer.tsx (100%) rename {packages => src/platform/packages/shared}/home/sample_data_card/src/footer/footer.stories.tsx (100%) rename {packages => src/platform/packages/shared}/home/sample_data_card/src/footer/index.tsx (100%) rename {packages => src/platform/packages/shared}/home/sample_data_card/src/footer/install_footer.test.tsx (100%) rename {packages => src/platform/packages/shared}/home/sample_data_card/src/footer/install_footer.tsx (100%) rename {packages => src/platform/packages/shared}/home/sample_data_card/src/footer/remove_footer.test.tsx (100%) rename {packages => src/platform/packages/shared}/home/sample_data_card/src/footer/remove_footer.tsx (100%) rename {packages => src/platform/packages/shared}/home/sample_data_card/src/footer/view_button.test.tsx (100%) rename {packages => src/platform/packages/shared}/home/sample_data_card/src/footer/view_button.tsx (100%) rename {packages => src/platform/packages/shared}/home/sample_data_card/src/hooks/index.ts (100%) rename {packages => src/platform/packages/shared}/home/sample_data_card/src/hooks/use_install.ts (100%) rename {packages => src/platform/packages/shared}/home/sample_data_card/src/hooks/use_remove.ts (100%) rename {packages => src/platform/packages/shared}/home/sample_data_card/src/mocks/dashboard.png (100%) rename {packages => src/platform/packages/shared}/home/sample_data_card/src/mocks/dashboard_dark.png (100%) rename {packages => src/platform/packages/shared}/home/sample_data_card/src/mocks/icon.svg (100%) rename {packages => src/platform/packages/shared}/home/sample_data_card/src/mocks/index.ts (100%) rename {packages => src/platform/packages/shared}/home/sample_data_card/src/sample_data_card.component.tsx (100%) rename {packages => src/platform/packages/shared}/home/sample_data_card/src/sample_data_card.stories.tsx (100%) rename {packages => src/platform/packages/shared}/home/sample_data_card/src/sample_data_card.test.tsx (100%) rename {packages => src/platform/packages/shared}/home/sample_data_card/src/sample_data_card.tsx (100%) rename {packages => src/platform/packages/shared}/home/sample_data_card/src/services.tsx (100%) rename {packages => src/platform/packages/shared}/home/sample_data_card/tsconfig.json (86%) rename {packages => src/platform/packages/shared}/home/sample_data_tab/README.mdx (100%) rename {packages => src/platform/packages/shared}/home/sample_data_tab/index.ts (100%) rename {packages => src/platform/packages/shared}/home/sample_data_tab/jest.config.js (82%) rename {packages => src/platform/packages/shared}/home/sample_data_tab/kibana.jsonc (100%) rename {packages => src/platform/packages/shared}/home/sample_data_tab/package.json (100%) rename {packages => src/platform/packages/shared}/home/sample_data_tab/src/assets/welcome_dark.png (100%) rename {packages => src/platform/packages/shared}/home/sample_data_tab/src/assets/welcome_light.png (100%) rename {packages => src/platform/packages/shared}/home/sample_data_tab/src/constants.ts (100%) rename {packages => src/platform/packages/shared}/home/sample_data_tab/src/demo_env_panel.stories.tsx (100%) rename {packages => src/platform/packages/shared}/home/sample_data_tab/src/demo_env_panel.tsx (100%) rename {packages => src/platform/packages/shared}/home/sample_data_tab/src/hooks/index.ts (100%) rename {packages => src/platform/packages/shared}/home/sample_data_tab/src/hooks/use_list.ts (100%) rename {packages => src/platform/packages/shared}/home/sample_data_tab/src/mocks.ts (100%) rename {packages => src/platform/packages/shared}/home/sample_data_tab/src/sample_data_cards.tsx (100%) rename {packages => src/platform/packages/shared}/home/sample_data_tab/src/sample_data_tab.stories.tsx (100%) rename {packages => src/platform/packages/shared}/home/sample_data_tab/src/sample_data_tab.tsx (100%) rename {packages => src/platform/packages/shared}/home/sample_data_tab/src/services.tsx (100%) rename {packages => src/platform/packages/shared}/home/sample_data_tab/tsconfig.json (87%) rename {packages => src/platform/packages/shared}/home/sample_data_types/README.mdx (100%) rename {packages => src/platform/packages/shared}/home/sample_data_types/index.d.ts (100%) rename {packages/kbn-item-buffer => src/platform/packages/shared/home/sample_data_types}/jest.config.js (82%) rename {packages => src/platform/packages/shared}/home/sample_data_types/kibana.jsonc (100%) rename {packages => src/platform/packages/shared}/home/sample_data_types/package.json (100%) rename {packages => src/platform/packages/shared}/home/sample_data_types/tsconfig.json (73%) rename {packages => src/platform/packages/shared}/kbn-guided-onboarding/README.md (100%) rename {packages => src/platform/packages/shared}/kbn-guided-onboarding/classic/index.ts (100%) rename {packages => src/platform/packages/shared}/kbn-guided-onboarding/guide/index.ts (100%) rename {packages => src/platform/packages/shared}/kbn-guided-onboarding/index.ts (100%) rename {packages/home/sample_data_card => src/platform/packages/shared/kbn-guided-onboarding}/jest.config.js (83%) rename {packages => src/platform/packages/shared}/kbn-guided-onboarding/kibana.jsonc (100%) rename {packages => src/platform/packages/shared}/kbn-guided-onboarding/package.json (100%) rename {packages => src/platform/packages/shared}/kbn-guided-onboarding/src/common/test_guide_config.ts (100%) rename {packages => src/platform/packages/shared}/kbn-guided-onboarding/src/components/landing_page/classic_version/__snapshots__/guide_cards.test.tsx.snap (100%) rename {packages => src/platform/packages/shared}/kbn-guided-onboarding/src/components/landing_page/classic_version/guide_card.tsx (100%) rename {packages => src/platform/packages/shared}/kbn-guided-onboarding/src/components/landing_page/classic_version/guide_cards.constants.tsx (100%) rename {packages => src/platform/packages/shared}/kbn-guided-onboarding/src/components/landing_page/classic_version/guide_cards.test.tsx (100%) rename {packages => src/platform/packages/shared}/kbn-guided-onboarding/src/components/landing_page/classic_version/guide_cards.tsx (100%) rename {packages => src/platform/packages/shared}/kbn-guided-onboarding/src/components/landing_page/classic_version/guide_filters.tsx (100%) rename {packages => src/platform/packages/shared}/kbn-guided-onboarding/src/components/landing_page/guide/guide_card.tsx (100%) rename {packages => src/platform/packages/shared}/kbn-guided-onboarding/src/components/landing_page/guide/guide_cards.constants.tsx (100%) rename {packages => src/platform/packages/shared}/kbn-guided-onboarding/src/components/landing_page/guide/guide_cards.tsx (100%) rename {packages => src/platform/packages/shared}/kbn-guided-onboarding/src/components/landing_page/guide/guide_filters.tsx (100%) rename {packages => src/platform/packages/shared}/kbn-guided-onboarding/src/types.ts (100%) rename {packages => src/platform/packages/shared}/kbn-guided-onboarding/tsconfig.json (87%) rename {packages => src/platform/packages/shared}/kbn-management/settings/section_registry/README.mdx (100%) rename {packages => src/platform/packages/shared}/kbn-management/settings/section_registry/index.ts (100%) rename {packages => src/platform/packages/shared}/kbn-management/settings/section_registry/kibana.jsonc (100%) rename {packages => src/platform/packages/shared}/kbn-management/settings/section_registry/package.json (100%) rename {packages => src/platform/packages/shared}/kbn-management/settings/section_registry/section_registry.test.tsx (100%) rename {packages => src/platform/packages/shared}/kbn-management/settings/section_registry/section_registry.ts (100%) rename {packages => src/platform/packages/shared}/kbn-management/settings/section_registry/tsconfig.json (84%) rename {packages => src/platform/packages/shared}/kbn-management/settings/setting_ids/README.mdx (100%) rename {packages => src/platform/packages/shared}/kbn-management/settings/setting_ids/index.ts (100%) rename {packages => src/platform/packages/shared}/kbn-management/settings/setting_ids/kibana.jsonc (100%) rename {packages => src/platform/packages/shared}/kbn-management/settings/setting_ids/package.json (100%) rename {packages => src/platform/packages/shared}/kbn-management/settings/setting_ids/tsconfig.json (79%) rename {packages => src/platform/packages/shared}/react/kibana_context/common/BUILD.bazel (100%) rename {packages => src/platform/packages/shared}/react/kibana_context/common/README.mdx (100%) rename {packages => src/platform/packages/shared}/react/kibana_context/common/color_mode.test.ts (100%) rename {packages => src/platform/packages/shared}/react/kibana_context/common/color_mode.ts (100%) rename {packages => src/platform/packages/shared}/react/kibana_context/common/index.ts (100%) rename {packages/kbn-saved-objects-settings => src/platform/packages/shared/react/kibana_context/common}/jest.config.js (81%) rename {packages => src/platform/packages/shared}/react/kibana_context/common/kibana.jsonc (100%) rename {packages => src/platform/packages/shared}/react/kibana_context/common/package.json (100%) rename {packages => src/platform/packages/shared}/react/kibana_context/common/theme.ts (100%) rename {packages => src/platform/packages/shared}/react/kibana_context/common/tsconfig.json (78%) rename {packages => src/platform/packages/shared}/react/kibana_context/common/types.ts (100%) rename {packages => src/platform/packages/shared}/react/kibana_context/render/BUILD.bazel (83%) rename {packages => src/platform/packages/shared}/react/kibana_context/render/README.mdx (100%) rename {packages => src/platform/packages/shared}/react/kibana_context/render/index.ts (100%) create mode 100644 src/platform/packages/shared/react/kibana_context/render/jest.config.js rename {packages => src/platform/packages/shared}/react/kibana_context/render/kibana.jsonc (100%) rename {packages => src/platform/packages/shared}/react/kibana_context/render/package.json (100%) rename {packages => src/platform/packages/shared}/react/kibana_context/render/render_provider.tsx (100%) rename {packages => src/platform/packages/shared}/react/kibana_context/render/tsconfig.json (84%) rename {packages => src/platform/packages/shared}/react/kibana_context/root/BUILD.bazel (100%) rename {packages => src/platform/packages/shared}/react/kibana_context/root/README.mdx (100%) rename {packages => src/platform/packages/shared}/react/kibana_context/root/eui_provider.test.tsx (100%) rename {packages => src/platform/packages/shared}/react/kibana_context/root/eui_provider.tsx (100%) rename {packages => src/platform/packages/shared}/react/kibana_context/root/index.ts (100%) create mode 100644 src/platform/packages/shared/react/kibana_context/root/jest.config.js rename {packages => src/platform/packages/shared}/react/kibana_context/root/kibana.jsonc (84%) rename {packages => src/platform/packages/shared}/react/kibana_context/root/package.json (100%) rename {packages => src/platform/packages/shared}/react/kibana_context/root/root_provider.test.tsx (100%) rename {packages => src/platform/packages/shared}/react/kibana_context/root/root_provider.tsx (100%) rename {packages => src/platform/packages/shared}/react/kibana_context/root/tsconfig.json (91%) rename {packages => src/platform/packages/shared}/react/kibana_context/styled/README.mdx (100%) rename {packages => src/platform/packages/shared}/react/kibana_context/styled/index.ts (100%) create mode 100644 src/platform/packages/shared/react/kibana_context/styled/jest.config.js rename {packages => src/platform/packages/shared}/react/kibana_context/styled/kibana.jsonc (100%) rename {packages => src/platform/packages/shared}/react/kibana_context/styled/package.json (100%) rename {packages => src/platform/packages/shared}/react/kibana_context/styled/styled_provider.tsx (100%) rename {packages => src/platform/packages/shared}/react/kibana_context/styled/tsconfig.json (82%) rename {packages => src/platform/packages/shared}/react/kibana_context/theme/BUILD.bazel (84%) rename {packages => src/platform/packages/shared}/react/kibana_context/theme/README.mdx (100%) rename {packages => src/platform/packages/shared}/react/kibana_context/theme/index.ts (100%) create mode 100644 src/platform/packages/shared/react/kibana_context/theme/jest.config.js rename {packages => src/platform/packages/shared}/react/kibana_context/theme/kibana.jsonc (100%) rename {packages => src/platform/packages/shared}/react/kibana_context/theme/package.json (100%) rename {packages => src/platform/packages/shared}/react/kibana_context/theme/theme_provider.test.tsx (100%) rename {packages => src/platform/packages/shared}/react/kibana_context/theme/theme_provider.tsx (100%) rename {packages => src/platform/packages/shared}/react/kibana_context/theme/tsconfig.json (88%) rename {packages => src/platform/packages/shared}/react/kibana_context/theme/with_theme.tsx (100%) rename {packages => src/platform/packages/shared}/react/kibana_mount/README.md (100%) rename {packages => src/platform/packages/shared}/react/kibana_mount/index.ts (100%) rename {packages/deeplinks/shared => src/platform/packages/shared/react/kibana_mount}/jest.config.js (83%) rename {packages => src/platform/packages/shared}/react/kibana_mount/kibana.jsonc (100%) rename {packages => src/platform/packages/shared}/react/kibana_mount/mount_point_portal.test.tsx (100%) rename {packages => src/platform/packages/shared}/react/kibana_mount/mount_point_portal.tsx (100%) rename {packages => src/platform/packages/shared}/react/kibana_mount/package.json (100%) rename {packages => src/platform/packages/shared}/react/kibana_mount/test_helpers/react_mount_serializer.ts (100%) rename {packages => src/platform/packages/shared}/react/kibana_mount/to_mount_point.test.tsx (100%) rename {packages => src/platform/packages/shared}/react/kibana_mount/to_mount_point.tsx (100%) rename {packages => src/platform/packages/shared}/react/kibana_mount/tsconfig.json (90%) rename {packages => src/platform/packages/shared}/react/kibana_mount/utils.ts (100%) rename {packages => src/platform/packages/shared}/serverless/settings/observability_project/README.mdx (100%) rename {packages => src/platform/packages/shared}/serverless/settings/observability_project/index.ts (100%) rename {packages => src/platform/packages/shared}/serverless/settings/observability_project/kibana.jsonc (100%) rename {packages => src/platform/packages/shared}/serverless/settings/observability_project/package.json (100%) rename {packages => src/platform/packages/shared}/serverless/settings/observability_project/tsconfig.json (81%) rename x-pack/{plugins => platform/plugins/private}/drilldowns/url_drilldown/README.md (100%) create mode 100644 x-pack/platform/plugins/private/drilldowns/url_drilldown/jest.config.js rename x-pack/{plugins => platform/plugins/private}/drilldowns/url_drilldown/kibana.jsonc (100%) rename x-pack/{plugins => platform/plugins/private}/drilldowns/url_drilldown/public/index.ts (100%) rename x-pack/{plugins => platform/plugins/private}/drilldowns/url_drilldown/public/lib/i18n.ts (100%) rename x-pack/{plugins => platform/plugins/private}/drilldowns/url_drilldown/public/lib/index.ts (100%) rename x-pack/{plugins => platform/plugins/private}/drilldowns/url_drilldown/public/lib/test/data.ts (100%) rename x-pack/{plugins => platform/plugins/private}/drilldowns/url_drilldown/public/lib/url_drilldown.test.tsx (100%) rename x-pack/{plugins => platform/plugins/private}/drilldowns/url_drilldown/public/lib/url_drilldown.tsx (100%) rename x-pack/{plugins => platform/plugins/private}/drilldowns/url_drilldown/public/lib/variables/context_variables.test.ts (100%) rename x-pack/{plugins => platform/plugins/private}/drilldowns/url_drilldown/public/lib/variables/context_variables.ts (100%) rename x-pack/{plugins => platform/plugins/private}/drilldowns/url_drilldown/public/lib/variables/event_variables.test.ts (100%) rename x-pack/{plugins => platform/plugins/private}/drilldowns/url_drilldown/public/lib/variables/event_variables.ts (100%) rename x-pack/{plugins => platform/plugins/private}/drilldowns/url_drilldown/public/lib/variables/global_variables.ts (100%) rename x-pack/{plugins => platform/plugins/private}/drilldowns/url_drilldown/public/lib/variables/i18n.ts (100%) rename x-pack/{plugins => platform/plugins/private}/drilldowns/url_drilldown/public/lib/variables/util.ts (100%) rename x-pack/{plugins => platform/plugins/private}/drilldowns/url_drilldown/public/plugin.ts (100%) rename x-pack/{plugins => platform/plugins/private}/drilldowns/url_drilldown/tsconfig.json (92%) delete mode 100644 x-pack/plugins/drilldowns/jest.config.js diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 3cc282ab49b50..bb3c25078ecda 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -49,7 +49,6 @@ packages/core/base/core-base-browser-mocks @elastic/kibana-core packages/core/base/core-base-server-mocks @elastic/kibana-core packages/core/capabilities/core-capabilities-browser-mocks @elastic/kibana-core packages/core/capabilities/core-capabilities-server-mocks @elastic/kibana-core -packages/core/chrome/core-chrome-browser @elastic/appex-sharedux packages/core/chrome/core-chrome-browser-mocks @elastic/appex-sharedux packages/core/deprecations/core-deprecations-browser-mocks @elastic/kibana-core packages/core/deprecations/core-deprecations-server-mocks @elastic/kibana-core @@ -100,10 +99,6 @@ packages/core/usage-data/core-usage-data-server-mocks @elastic/kibana-core packages/core/user-settings/core-user-settings-server @elastic/kibana-security packages/core/user-settings/core-user-settings-server-internal @elastic/kibana-security packages/core/user-settings/core-user-settings-server-mocks @elastic/kibana-security -packages/deeplinks/shared @elastic/appex-sharedux -packages/home/sample_data_card @elastic/appex-sharedux -packages/home/sample_data_tab @elastic/appex-sharedux -packages/home/sample_data_types @elastic/appex-sharedux packages/kbn-ambient-common-types @elastic/kibana-operations packages/kbn-ambient-ftr-types @elastic/kibana-operations @elastic/appex-qa packages/kbn-ambient-storybook-types @elastic/kibana-operations @@ -154,10 +149,8 @@ packages/kbn-generate @elastic/kibana-operations packages/kbn-generate-console-definitions @elastic/kibana-management packages/kbn-get-repo-files @elastic/kibana-operations packages/kbn-grid-layout @elastic/kibana-presentation -packages/kbn-guided-onboarding @elastic/appex-sharedux packages/kbn-import-locator @elastic/kibana-operations packages/kbn-import-resolver @elastic/kibana-operations -packages/kbn-item-buffer @elastic/appex-sharedux packages/kbn-jest-serializers @elastic/kibana-operations packages/kbn-journeys @elastic/kibana-operations @elastic/appex-qa packages/kbn-json-ast @elastic/kibana-operations @@ -166,8 +159,6 @@ packages/kbn-lint-packages-cli @elastic/kibana-operations packages/kbn-lint-ts-projects-cli @elastic/kibana-operations packages/kbn-managed-vscode-config @elastic/kibana-operations packages/kbn-managed-vscode-config-cli @elastic/kibana-operations -packages/kbn-management/settings/section_registry @elastic/appex-sharedux @elastic/kibana-management -packages/kbn-management/settings/setting_ids @elastic/appex-sharedux @elastic/kibana-management packages/kbn-management/storybook/config @elastic/kibana-management packages/kbn-manifest @elastic/kibana-core packages/kbn-mock-idp-plugin @elastic/kibana-security @@ -184,14 +175,12 @@ packages/kbn-picomatcher @elastic/kibana-operations packages/kbn-plugin-check @elastic/appex-sharedux packages/kbn-plugin-generator @elastic/kibana-operations packages/kbn-plugin-helpers @elastic/kibana-operations -packages/kbn-react-mute-legacy-root-warning @elastic/appex-sharedux packages/kbn-relocate @elastic/kibana-core packages/kbn-repo-file-maps @elastic/kibana-operations packages/kbn-repo-linter @elastic/kibana-operations packages/kbn-repo-path @elastic/kibana-operations packages/kbn-repo-source-classifier @elastic/kibana-operations packages/kbn-repo-source-classifier-cli @elastic/kibana-operations -packages/kbn-saved-objects-settings @elastic/appex-sharedux packages/kbn-saved-search-component @elastic/obs-ux-logs-team packages/kbn-scout @elastic/appex-qa packages/kbn-scout-info @elastic/appex-qa @@ -215,18 +204,8 @@ packages/kbn-validate-next-docs-cli @elastic/kibana-operations packages/kbn-web-worker-stub @elastic/kibana-operations packages/kbn-whereis-pkg-cli @elastic/kibana-operations packages/kbn-yarn-lock-validator @elastic/kibana-operations -packages/react/kibana_context/common @elastic/appex-sharedux -packages/react/kibana_context/render @elastic/appex-sharedux -packages/react/kibana_context/root @elastic/appex-sharedux -packages/react/kibana_context/styled @elastic/appex-sharedux -packages/react/kibana_context/theme @elastic/appex-sharedux -packages/react/kibana_mount @elastic/appex-sharedux packages/response-ops/rule_form @elastic/response-ops -packages/serverless/project_switcher @elastic/appex-sharedux -packages/serverless/settings/common @elastic/appex-sharedux @elastic/kibana-management -packages/serverless/settings/observability_project @elastic/appex-sharedux @elastic/kibana-management @elastic/obs-ux-management-team packages/serverless/storybook/config @elastic/appex-sharedux -packages/serverless/types @elastic/appex-sharedux src/core @elastic/kibana-core src/core/packages/analytics/browser @elastic/kibana-core src/core/packages/analytics/browser-internal @elastic/kibana-core @@ -245,6 +224,7 @@ src/core/packages/capabilities/browser-internal @elastic/kibana-core src/core/packages/capabilities/common @elastic/kibana-core src/core/packages/capabilities/server @elastic/kibana-core src/core/packages/capabilities/server-internal @elastic/kibana-core +src/core/packages/chrome/browser @elastic/appex-sharedux src/core/packages/chrome/browser-internal @elastic/appex-sharedux src/core/packages/config/server-internal @elastic/kibana-core src/core/packages/custom-branding/browser @elastic/appex-sharedux @@ -387,6 +367,7 @@ src/platform/packages/private/kbn-generate-csv @elastic/appex-sharedux src/platform/packages/private/kbn-handlebars @elastic/kibana-security src/platform/packages/private/kbn-hapi-mocks @elastic/kibana-core src/platform/packages/private/kbn-health-gateway-server @elastic/kibana-core +src/platform/packages/private/kbn-item-buffer @elastic/appex-sharedux src/platform/packages/private/kbn-language-documentation @elastic/kibana-esql src/platform/packages/private/kbn-lens-formula-docs @elastic/kibana-visualizations src/platform/packages/private/kbn-managed-content-badge @elastic/kibana-visualizations @@ -395,6 +376,7 @@ src/platform/packages/private/kbn-management/settings/components/field_category src/platform/packages/private/kbn-management/settings/components/form @elastic/kibana-management src/platform/packages/private/kbn-mapbox-gl @elastic/kibana-presentation src/platform/packages/private/kbn-panel-loader @elastic/kibana-presentation +src/platform/packages/private/kbn-react-mute-legacy-root-warning @elastic/appex-sharedux src/platform/packages/private/kbn-repo-packages @elastic/kibana-operations src/platform/packages/private/kbn-reporting/common @elastic/appex-sharedux src/platform/packages/private/kbn-reporting/export_types/csv @elastic/appex-sharedux @@ -407,6 +389,7 @@ src/platform/packages/private/kbn-reporting/get_csv_panel_actions @elastic/appex src/platform/packages/private/kbn-reporting/mocks_server @elastic/appex-sharedux src/platform/packages/private/kbn-reporting/public @elastic/appex-sharedux src/platform/packages/private/kbn-reporting/server @elastic/appex-sharedux +src/platform/packages/private/kbn-saved-objects-settings @elastic/appex-sharedux src/platform/packages/private/kbn-screenshotting-server @elastic/appex-sharedux src/platform/packages/private/kbn-timelion-grammar @elastic/kibana-visualizations src/platform/packages/private/kbn-tinymath @elastic/kibana-visualizations @@ -414,6 +397,9 @@ src/platform/packages/private/kbn-transpose-utils @elastic/kibana-visualizations src/platform/packages/private/kbn-ui-shared-deps-npm @elastic/kibana-operations src/platform/packages/private/kbn-ui-shared-deps-src @elastic/kibana-operations src/platform/packages/private/kbn-unsaved-changes-badge @elastic/kibana-data-discovery +src/platform/packages/private/serverless/project_switcher @elastic/appex-sharedux +src/platform/packages/private/serverless/settings/common @elastic/appex-sharedux @elastic/kibana-management +src/platform/packages/private/serverless/types @elastic/appex-sharedux src/platform/packages/private/shared-ux/page/analytics_no_data/impl @elastic/appex-sharedux src/platform/packages/private/shared-ux/page/analytics_no_data/mocks @elastic/appex-sharedux src/platform/packages/private/shared-ux/page/analytics_no_data/types @elastic/appex-sharedux @@ -442,6 +428,10 @@ src/platform/packages/shared/deeplinks/ml @elastic/ml-ui src/platform/packages/shared/deeplinks/observability @elastic/obs-ux-management-team src/platform/packages/shared/deeplinks/search @elastic/search-kibana src/platform/packages/shared/deeplinks/security @elastic/security-solution +src/platform/packages/shared/deeplinks/shared @elastic/appex-sharedux +src/platform/packages/shared/home/sample_data_card @elastic/appex-sharedux +src/platform/packages/shared/home/sample_data_tab @elastic/appex-sharedux +src/platform/packages/shared/home/sample_data_types @elastic/appex-sharedux src/platform/packages/shared/kbn-actions-types @elastic/response-ops src/platform/packages/shared/kbn-alerting-types @elastic/response-ops src/platform/packages/shared/kbn-alerts-as-data-utils @elastic/response-ops @@ -483,6 +473,7 @@ src/platform/packages/shared/kbn-field-types @elastic/kibana-data-discovery src/platform/packages/shared/kbn-field-utils @elastic/kibana-data-discovery src/platform/packages/shared/kbn-flot-charts @elastic/kibana-presentation @elastic/stack-monitoring src/platform/packages/shared/kbn-grouping @elastic/response-ops +src/platform/packages/shared/kbn-guided-onboarding @elastic/appex-sharedux src/platform/packages/shared/kbn-i18n @elastic/kibana-core src/platform/packages/shared/kbn-i18n-react @elastic/kibana-core src/platform/packages/shared/kbn-interpreter @elastic/kibana-visualizations @@ -494,6 +485,8 @@ src/platform/packages/shared/kbn-management/cards_navigation @elastic/kibana-man src/platform/packages/shared/kbn-management/settings/components/field_input @elastic/kibana-management src/platform/packages/shared/kbn-management/settings/components/field_row @elastic/kibana-management src/platform/packages/shared/kbn-management/settings/field_definition @elastic/kibana-management +src/platform/packages/shared/kbn-management/settings/section_registry @elastic/appex-sharedux @elastic/kibana-management +src/platform/packages/shared/kbn-management/settings/setting_ids @elastic/appex-sharedux @elastic/kibana-management src/platform/packages/shared/kbn-management/settings/types @elastic/kibana-management src/platform/packages/shared/kbn-management/settings/utilities @elastic/kibana-management src/platform/packages/shared/kbn-monaco @elastic/appex-sharedux @@ -557,7 +550,14 @@ src/platform/packages/shared/kbn-zod @elastic/kibana-core src/platform/packages/shared/kbn-zod-helpers @elastic/security-detection-rule-management src/platform/packages/shared/presentation/presentation_containers @elastic/kibana-presentation src/platform/packages/shared/presentation/presentation_publishing @elastic/kibana-presentation +src/platform/packages/shared/react/kibana_context/common @elastic/appex-sharedux +src/platform/packages/shared/react/kibana_context/render @elastic/appex-sharedux +src/platform/packages/shared/react/kibana_context/root @elastic/appex-sharedux +src/platform/packages/shared/react/kibana_context/styled @elastic/appex-sharedux +src/platform/packages/shared/react/kibana_context/theme @elastic/appex-sharedux +src/platform/packages/shared/react/kibana_mount @elastic/appex-sharedux src/platform/packages/shared/response-ops/rule_params @elastic/response-ops +src/platform/packages/shared/serverless/settings/observability_project @elastic/appex-sharedux @elastic/kibana-management @elastic/obs-ux-management-team src/platform/packages/shared/serverless/settings/search_project @elastic/search-kibana @elastic/kibana-management src/platform/packages/shared/serverless/settings/security_project @elastic/security-solution @elastic/kibana-management src/platform/packages/shared/shared-ux/avatar/solution @elastic/appex-sharedux @@ -839,6 +839,7 @@ x-pack/platform/plugins/private/custom_branding @elastic/appex-sharedux x-pack/platform/plugins/private/data_usage @elastic/obs-ai-assistant @elastic/security-solution x-pack/platform/plugins/private/data_visualizer @elastic/ml-ui x-pack/platform/plugins/private/discover_enhanced @elastic/kibana-data-discovery +x-pack/platform/plugins/private/drilldowns/url_drilldown @elastic/appex-sharedux x-pack/platform/plugins/private/file_upload @elastic/kibana-presentation @elastic/ml-ui x-pack/platform/plugins/private/global_search_bar @elastic/appex-sharedux x-pack/platform/plugins/private/global_search_providers @elastic/appex-sharedux @@ -903,7 +904,6 @@ x-pack/platform/plugins/shared/stack_alerts @elastic/response-ops x-pack/platform/plugins/shared/stack_connectors @elastic/response-ops x-pack/platform/plugins/shared/task_manager @elastic/response-ops x-pack/platform/plugins/shared/triggers_actions_ui @elastic/response-ops -x-pack/plugins/drilldowns/url_drilldown @elastic/appex-sharedux x-pack/solutions/observability/packages/alert_details @elastic/obs-ux-management-team x-pack/solutions/observability/packages/alerting_test_data @elastic/obs-ux-management-team x-pack/solutions/observability/packages/get_padded_alert_time_range_util @elastic/obs-ux-management-team diff --git a/.i18nrc.json b/.i18nrc.json index df5deb402b915..3e5f8512c3b9d 100644 --- a/.i18nrc.json +++ b/.i18nrc.json @@ -65,9 +65,9 @@ "grouping": "src/platform/packages/shared/kbn-grouping/src", "alertsGrouping": "x-pack/solutions/observability/packages/kbn-alerts-grouping", "guidedOnboarding": "src/platform/plugins/shared/guided_onboarding", - "guidedOnboardingPackage": "packages/kbn-guided-onboarding", + "guidedOnboardingPackage": "src/platform/packages/shared/kbn-guided-onboarding", "home": "src/platform/plugins/shared/home", - "homePackages": "packages/home", + "homePackages": "src/platform/packages/shared/home", "indexPatternEditor": "src/platform/plugins/shared/data_view_editor", "indexPatternFieldEditor": "src/platform/plugins/shared/data_view_field_editor", "indexPatternManagement": "src/platform/plugins/shared/data_view_management", @@ -99,7 +99,7 @@ "newsfeed": "src/platform/plugins/shared/newsfeed", "presentationUtil": "src/platform/plugins/shared/presentation_util", "randomSampling": "x-pack/platform/packages/private/kbn-random-sampling", - "reactPackages": "packages/react", + "reactPackages": ["src/platform/packages/shared/react", "src/platform/packages/private/react"], "esqlEditor": "src/platform/packages/private/kbn-esql-editor", "esqlUtils": "src/platform/packages/shared/kbn-esql-utils", "reporting": "src/platform/packages/private/kbn-reporting", diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index 1f90e36cd7ce3..727a3aec35369 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -966,7 +966,7 @@ As a developer you can reuse and extend built-in alerts and actions UI functiona in their infrastructure. -|{kib-repo}blob/{branch}/x-pack/plugins/drilldowns/url_drilldown/README.md[urlDrilldown] +|{kib-repo}blob/{branch}/x-pack/platform/plugins/private/drilldowns/url_drilldown/README.md[urlDrilldown] |NOTE: This plugin contains implementation of URL drilldown. For drilldowns infrastructure code refer to ui_actions_enhanced plugin. diff --git a/package.json b/package.json index 9d592efff0187..3f965e6fd97a7 100644 --- a/package.json +++ b/package.json @@ -262,7 +262,7 @@ "@kbn/core-capabilities-common": "link:src/core/packages/capabilities/common", "@kbn/core-capabilities-server": "link:src/core/packages/capabilities/server", "@kbn/core-capabilities-server-internal": "link:src/core/packages/capabilities/server-internal", - "@kbn/core-chrome-browser": "link:packages/core/chrome/core-chrome-browser", + "@kbn/core-chrome-browser": "link:src/core/packages/chrome/browser", "@kbn/core-chrome-browser-internal": "link:src/core/packages/chrome/browser-internal", "@kbn/core-config-server-internal": "link:src/core/packages/config/server-internal", "@kbn/core-custom-branding-browser": "link:src/core/packages/custom-branding/browser", @@ -445,7 +445,7 @@ "@kbn/deeplinks-observability": "link:src/platform/packages/shared/deeplinks/observability", "@kbn/deeplinks-search": "link:src/platform/packages/shared/deeplinks/search", "@kbn/deeplinks-security": "link:src/platform/packages/shared/deeplinks/security", - "@kbn/deeplinks-shared": "link:packages/deeplinks/shared", + "@kbn/deeplinks-shared": "link:src/platform/packages/shared/deeplinks/shared", "@kbn/default-nav-analytics": "link:src/platform/packages/private/default-nav/analytics", "@kbn/default-nav-devtools": "link:src/platform/packages/private/default-nav/devtools", "@kbn/default-nav-management": "link:src/platform/packages/private/default-nav/management", @@ -551,7 +551,7 @@ "@kbn/grid-layout": "link:packages/kbn-grid-layout", "@kbn/grokdebugger-plugin": "link:x-pack/platform/plugins/private/grokdebugger", "@kbn/grouping": "link:src/platform/packages/shared/kbn-grouping", - "@kbn/guided-onboarding": "link:packages/kbn-guided-onboarding", + "@kbn/guided-onboarding": "link:src/platform/packages/shared/kbn-guided-onboarding", "@kbn/guided-onboarding-example-plugin": "link:examples/guided_onboarding_example", "@kbn/guided-onboarding-plugin": "link:src/platform/plugins/shared/guided_onboarding", "@kbn/handlebars": "link:src/platform/packages/private/kbn-handlebars", @@ -560,9 +560,9 @@ "@kbn/health-gateway-server": "link:src/platform/packages/private/kbn-health-gateway-server", "@kbn/hello-world-plugin": "link:examples/hello_world", "@kbn/home-plugin": "link:src/platform/plugins/shared/home", - "@kbn/home-sample-data-card": "link:packages/home/sample_data_card", - "@kbn/home-sample-data-tab": "link:packages/home/sample_data_tab", - "@kbn/home-sample-data-types": "link:packages/home/sample_data_types", + "@kbn/home-sample-data-card": "link:src/platform/packages/shared/home/sample_data_card", + "@kbn/home-sample-data-tab": "link:src/platform/packages/shared/home/sample_data_tab", + "@kbn/home-sample-data-types": "link:src/platform/packages/shared/home/sample_data_types", "@kbn/i18n": "link:src/platform/packages/shared/kbn-i18n", "@kbn/i18n-react": "link:src/platform/packages/shared/kbn-i18n-react", "@kbn/iframe-embedded-plugin": "link:x-pack/test/functional_embedded/plugins/iframe_embedded", @@ -592,7 +592,7 @@ "@kbn/investigation-shared": "link:x-pack/solutions/observability/packages/kbn-investigation-shared", "@kbn/io-ts-utils": "link:src/platform/packages/shared/kbn-io-ts-utils", "@kbn/ipynb": "link:x-pack/solutions/search/packages/kbn-ipynb", - "@kbn/item-buffer": "link:packages/kbn-item-buffer", + "@kbn/item-buffer": "link:src/platform/packages/private/kbn-item-buffer", "@kbn/json-schemas": "link:x-pack/platform/packages/private/ml/json_schemas", "@kbn/kbn-health-gateway-status-plugin": "link:test/health_gateway/plugins/status", "@kbn/kbn-sample-panel-action-plugin": "link:test/plugin_functional/plugins/kbn_sample_panel_action", @@ -635,8 +635,8 @@ "@kbn/management-settings-components-field-row": "link:src/platform/packages/shared/kbn-management/settings/components/field_row", "@kbn/management-settings-components-form": "link:src/platform/packages/private/kbn-management/settings/components/form", "@kbn/management-settings-field-definition": "link:src/platform/packages/shared/kbn-management/settings/field_definition", - "@kbn/management-settings-ids": "link:packages/kbn-management/settings/setting_ids", - "@kbn/management-settings-section-registry": "link:packages/kbn-management/settings/section_registry", + "@kbn/management-settings-ids": "link:src/platform/packages/shared/kbn-management/settings/setting_ids", + "@kbn/management-settings-section-registry": "link:src/platform/packages/shared/kbn-management/settings/section_registry", "@kbn/management-settings-types": "link:src/platform/packages/shared/kbn-management/settings/types", "@kbn/management-settings-utilities": "link:src/platform/packages/shared/kbn-management/settings/utilities", "@kbn/management-test-plugin": "link:test/plugin_functional/plugins/management_test_plugin", @@ -731,13 +731,13 @@ "@kbn/random-sampling": "link:x-pack/platform/packages/private/kbn-random-sampling", "@kbn/react-field": "link:src/platform/packages/shared/kbn-react-field", "@kbn/react-hooks": "link:src/platform/packages/shared/kbn-react-hooks", - "@kbn/react-kibana-context-common": "link:packages/react/kibana_context/common", - "@kbn/react-kibana-context-render": "link:packages/react/kibana_context/render", - "@kbn/react-kibana-context-root": "link:packages/react/kibana_context/root", - "@kbn/react-kibana-context-styled": "link:packages/react/kibana_context/styled", - "@kbn/react-kibana-context-theme": "link:packages/react/kibana_context/theme", - "@kbn/react-kibana-mount": "link:packages/react/kibana_mount", - "@kbn/react-mute-legacy-root-warning": "link:packages/kbn-react-mute-legacy-root-warning", + "@kbn/react-kibana-context-common": "link:src/platform/packages/shared/react/kibana_context/common", + "@kbn/react-kibana-context-render": "link:src/platform/packages/shared/react/kibana_context/render", + "@kbn/react-kibana-context-root": "link:src/platform/packages/shared/react/kibana_context/root", + "@kbn/react-kibana-context-styled": "link:src/platform/packages/shared/react/kibana_context/styled", + "@kbn/react-kibana-context-theme": "link:src/platform/packages/shared/react/kibana_context/theme", + "@kbn/react-kibana-mount": "link:src/platform/packages/shared/react/kibana_mount", + "@kbn/react-mute-legacy-root-warning": "link:src/platform/packages/private/kbn-react-mute-legacy-root-warning", "@kbn/recently-accessed": "link:src/platform/packages/shared/kbn-recently-accessed", "@kbn/remote-clusters-plugin": "link:x-pack/platform/plugins/private/remote_clusters", "@kbn/rendering-plugin": "link:test/plugin_functional/plugins/rendering_plugin", @@ -783,7 +783,7 @@ "@kbn/saved-objects-hidden-type-plugin": "link:test/plugin_functional/plugins/saved_objects_hidden_type", "@kbn/saved-objects-management-plugin": "link:src/platform/plugins/shared/saved_objects_management", "@kbn/saved-objects-plugin": "link:src/platform/plugins/shared/saved_objects", - "@kbn/saved-objects-settings": "link:packages/kbn-saved-objects-settings", + "@kbn/saved-objects-settings": "link:src/platform/packages/private/kbn-saved-objects-settings", "@kbn/saved-objects-tagging-oss-plugin": "link:src/platform/plugins/shared/saved_objects_tagging_oss", "@kbn/saved-objects-tagging-plugin": "link:x-pack/platform/plugins/shared/saved_objects_tagging", "@kbn/saved-search-component": "link:packages/kbn-saved-search-component", @@ -860,14 +860,14 @@ "@kbn/server-route-repository-client": "link:src/platform/packages/shared/kbn-server-route-repository-client", "@kbn/server-route-repository-utils": "link:src/platform/packages/shared/kbn-server-route-repository-utils", "@kbn/serverless": "link:x-pack/platform/plugins/shared/serverless", - "@kbn/serverless-common-settings": "link:packages/serverless/settings/common", + "@kbn/serverless-common-settings": "link:src/platform/packages/private/serverless/settings/common", "@kbn/serverless-observability": "link:x-pack/solutions/observability/plugins/serverless_observability", - "@kbn/serverless-observability-settings": "link:packages/serverless/settings/observability_project", - "@kbn/serverless-project-switcher": "link:packages/serverless/project_switcher", + "@kbn/serverless-observability-settings": "link:src/platform/packages/shared/serverless/settings/observability_project", + "@kbn/serverless-project-switcher": "link:src/platform/packages/private/serverless/project_switcher", "@kbn/serverless-search": "link:x-pack/solutions/search/plugins/serverless_search", "@kbn/serverless-search-settings": "link:src/platform/packages/shared/serverless/settings/search_project", "@kbn/serverless-security-settings": "link:src/platform/packages/shared/serverless/settings/security_project", - "@kbn/serverless-types": "link:packages/serverless/types", + "@kbn/serverless-types": "link:src/platform/packages/private/serverless/types", "@kbn/session-notifications-plugin": "link:test/plugin_functional/plugins/session_notifications", "@kbn/session-view-plugin": "link:x-pack/solutions/security/plugins/session_view", "@kbn/share-examples-plugin": "link:examples/share_examples", @@ -990,7 +990,7 @@ "@kbn/unsaved-changes-prompt": "link:src/platform/packages/shared/kbn-unsaved-changes-prompt", "@kbn/upgrade-assistant-plugin": "link:x-pack/platform/plugins/private/upgrade_assistant", "@kbn/uptime-plugin": "link:x-pack/solutions/observability/plugins/uptime", - "@kbn/url-drilldown-plugin": "link:x-pack/plugins/drilldowns/url_drilldown", + "@kbn/url-drilldown-plugin": "link:x-pack/platform/plugins/private/drilldowns/url_drilldown", "@kbn/url-forwarding-plugin": "link:src/platform/plugins/private/url_forwarding", "@kbn/usage-collection-plugin": "link:src/platform/plugins/shared/usage_collection", "@kbn/usage-collection-test-plugin": "link:test/plugin_functional/plugins/usage_collection", diff --git a/packages/kbn-babel-preset/styled_components_files.js b/packages/kbn-babel-preset/styled_components_files.js index eb105084f426c..60251771689bb 100644 --- a/packages/kbn-babel-preset/styled_components_files.js +++ b/packages/kbn-babel-preset/styled_components_files.js @@ -578,6 +578,7 @@ module.exports = { /x-pack[\/\\]solutions[\/\\]security[\/\\]plugins[\/\\]timelines[\/\\]public[\/\\]components[\/\\]loading[\/\\]index.tsx/, /x-pack[\/\\]solutions[\/\\]security[\/\\]plugins[\/\\]timelines[\/\\]public[\/\\]mock[\/\\]test_providers.tsx/, /x-pack[\/\\]test[\/\\]plugin_functional[\/\\]plugins[\/\\]resolver_test[\/\\]public[\/\\]applications[\/\\]resolver_test[\/\\]index.tsx/, + /src[\/\\]platform[\/\\]packages[\/\\]shared[\/\\]react[\/\\]kibana_context[\/\\]styled[\/\\]styled_provider.tsx/, /src[\/\\]platform[\/\\]plugins[\/\\]shared[\/\\]custom_integrations[\/\\]public[\/\\]components[\/\\]fleet_integration[\/\\]elasticsearch_dotnet[\/\\]elasticsearch_dotnet_readme.tsx/, /src[\/\\]platform[\/\\]plugins[\/\\]shared[\/\\]custom_integrations[\/\\]public[\/\\]components[\/\\]fleet_integration[\/\\]elasticsearch_go[\/\\]elasticsearch_go_readme.tsx/, /src[\/\\]platform[\/\\]plugins[\/\\]shared[\/\\]custom_integrations[\/\\]public[\/\\]components[\/\\]fleet_integration[\/\\]elasticsearch_java[\/\\]elasticsearch_java_readme.tsx/, @@ -586,6 +587,5 @@ module.exports = { /src[\/\\]platform[\/\\]plugins[\/\\]shared[\/\\]custom_integrations[\/\\]public[\/\\]components[\/\\]fleet_integration[\/\\]elasticsearch_py[\/\\]elasticsearch_py_readme.tsx/, /src[\/\\]platform[\/\\]plugins[\/\\]shared[\/\\]custom_integrations[\/\\]public[\/\\]components[\/\\]fleet_integration[\/\\]elasticsearch_ruby[\/\\]elasticsearch_ruby_readme.tsx/, /src[\/\\]platform[\/\\]plugins[\/\\]shared[\/\\]custom_integrations[\/\\]public[\/\\]components[\/\\]fleet_integration[\/\\]sample[\/\\]sample_client_readme.tsx/, - /packages[\/\\]react[\/\\]kibana_context[\/\\]styled[\/\\]styled_provider.tsx/, ], }; diff --git a/packages/kbn-test/jest-preset.js b/packages/kbn-test/jest-preset.js index 5f60f52d04181..82781c3d018d4 100644 --- a/packages/kbn-test/jest-preset.js +++ b/packages/kbn-test/jest-preset.js @@ -78,7 +78,7 @@ module.exports = { // A list of paths to snapshot serializer modules Jest should use for snapshot testing snapshotSerializers: [ - '/packages/react/kibana_mount/test_helpers/react_mount_serializer.ts', + '/src/platform/packages/shared/react/kibana_mount/test_helpers/react_mount_serializer.ts', 'enzyme-to-json/serializer', '/packages/kbn-test/src/jest/setup/emotion.js', ], diff --git a/packages/react/kibana_context/common/jest.config.js b/packages/react/kibana_context/common/jest.config.js deleted file mode 100644 index 708e43e399a34..0000000000000 --- a/packages/react/kibana_context/common/jest.config.js +++ /dev/null @@ -1,14 +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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -module.exports = { - preset: '@kbn/test/jest_node', - rootDir: '../../../..', - roots: ['/packages/react/kibana_context/common'], -}; diff --git a/packages/react/kibana_context/render/jest.config.js b/packages/react/kibana_context/render/jest.config.js deleted file mode 100644 index c02768dbba623..0000000000000 --- a/packages/react/kibana_context/render/jest.config.js +++ /dev/null @@ -1,14 +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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -module.exports = { - preset: '@kbn/test', - rootDir: '../../../..', - roots: ['/packages/react/kibana_context/render'], -}; diff --git a/packages/react/kibana_context/root/jest.config.js b/packages/react/kibana_context/root/jest.config.js deleted file mode 100644 index b38b84714318d..0000000000000 --- a/packages/react/kibana_context/root/jest.config.js +++ /dev/null @@ -1,14 +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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -module.exports = { - preset: '@kbn/test', - rootDir: '../../../..', - roots: ['/packages/react/kibana_context/root'], -}; diff --git a/packages/react/kibana_context/styled/jest.config.js b/packages/react/kibana_context/styled/jest.config.js deleted file mode 100644 index d71954a615f5b..0000000000000 --- a/packages/react/kibana_context/styled/jest.config.js +++ /dev/null @@ -1,14 +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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -module.exports = { - preset: '@kbn/test', - rootDir: '../../../..', - roots: ['/packages/react/kibana_context/styled'], -}; diff --git a/packages/react/kibana_context/theme/jest.config.js b/packages/react/kibana_context/theme/jest.config.js deleted file mode 100644 index 3d6452df29f45..0000000000000 --- a/packages/react/kibana_context/theme/jest.config.js +++ /dev/null @@ -1,14 +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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -module.exports = { - preset: '@kbn/test', - rootDir: '../../../..', - roots: ['/packages/react/kibana_context/theme'], -}; diff --git a/packages/react/kibana_mount/jest.config.js b/packages/react/kibana_mount/jest.config.js deleted file mode 100644 index bb607856d9376..0000000000000 --- a/packages/react/kibana_mount/jest.config.js +++ /dev/null @@ -1,14 +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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -module.exports = { - preset: '@kbn/test', - rootDir: '../../..', - roots: ['/packages/react/kibana_mount'], -}; diff --git a/packages/serverless/project_switcher/jest.config.js b/packages/serverless/project_switcher/jest.config.js deleted file mode 100644 index 9f223dca5b719..0000000000000 --- a/packages/serverless/project_switcher/jest.config.js +++ /dev/null @@ -1,14 +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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -module.exports = { - preset: '@kbn/test', - rootDir: '../../..', - roots: ['/packages/serverless/project_switcher'], -}; diff --git a/packages/core/chrome/core-chrome-browser/README.md b/src/core/packages/chrome/browser/README.md similarity index 100% rename from packages/core/chrome/core-chrome-browser/README.md rename to src/core/packages/chrome/browser/README.md diff --git a/packages/core/chrome/core-chrome-browser/index.ts b/src/core/packages/chrome/browser/index.ts similarity index 100% rename from packages/core/chrome/core-chrome-browser/index.ts rename to src/core/packages/chrome/browser/index.ts diff --git a/packages/kbn-guided-onboarding/jest.config.js b/src/core/packages/chrome/browser/jest.config.js similarity index 85% rename from packages/kbn-guided-onboarding/jest.config.js rename to src/core/packages/chrome/browser/jest.config.js index 19b9e73d70359..37b0603d800df 100644 --- a/packages/kbn-guided-onboarding/jest.config.js +++ b/src/core/packages/chrome/browser/jest.config.js @@ -9,6 +9,6 @@ module.exports = { preset: '@kbn/test', - rootDir: '../..', - roots: ['/packages/kbn-guided-onboarding'], + rootDir: '../../../../..', + roots: ['/src/core/packages/chrome/browser'], }; diff --git a/packages/core/chrome/core-chrome-browser/kibana.jsonc b/src/core/packages/chrome/browser/kibana.jsonc similarity index 100% rename from packages/core/chrome/core-chrome-browser/kibana.jsonc rename to src/core/packages/chrome/browser/kibana.jsonc diff --git a/packages/core/chrome/core-chrome-browser/package.json b/src/core/packages/chrome/browser/package.json similarity index 100% rename from packages/core/chrome/core-chrome-browser/package.json rename to src/core/packages/chrome/browser/package.json diff --git a/packages/core/chrome/core-chrome-browser/src/breadcrumb.ts b/src/core/packages/chrome/browser/src/breadcrumb.ts similarity index 100% rename from packages/core/chrome/core-chrome-browser/src/breadcrumb.ts rename to src/core/packages/chrome/browser/src/breadcrumb.ts diff --git a/packages/core/chrome/core-chrome-browser/src/contracts.ts b/src/core/packages/chrome/browser/src/contracts.ts similarity index 100% rename from packages/core/chrome/core-chrome-browser/src/contracts.ts rename to src/core/packages/chrome/browser/src/contracts.ts diff --git a/packages/core/chrome/core-chrome-browser/src/doc_title.ts b/src/core/packages/chrome/browser/src/doc_title.ts similarity index 100% rename from packages/core/chrome/core-chrome-browser/src/doc_title.ts rename to src/core/packages/chrome/browser/src/doc_title.ts diff --git a/packages/core/chrome/core-chrome-browser/src/help_extension.ts b/src/core/packages/chrome/browser/src/help_extension.ts similarity index 100% rename from packages/core/chrome/core-chrome-browser/src/help_extension.ts rename to src/core/packages/chrome/browser/src/help_extension.ts diff --git a/packages/core/chrome/core-chrome-browser/src/index.ts b/src/core/packages/chrome/browser/src/index.ts similarity index 100% rename from packages/core/chrome/core-chrome-browser/src/index.ts rename to src/core/packages/chrome/browser/src/index.ts diff --git a/packages/core/chrome/core-chrome-browser/src/nav_controls.ts b/src/core/packages/chrome/browser/src/nav_controls.ts similarity index 100% rename from packages/core/chrome/core-chrome-browser/src/nav_controls.ts rename to src/core/packages/chrome/browser/src/nav_controls.ts diff --git a/packages/core/chrome/core-chrome-browser/src/nav_links.ts b/src/core/packages/chrome/browser/src/nav_links.ts similarity index 100% rename from packages/core/chrome/core-chrome-browser/src/nav_links.ts rename to src/core/packages/chrome/browser/src/nav_links.ts diff --git a/packages/core/chrome/core-chrome-browser/src/project_navigation.ts b/src/core/packages/chrome/browser/src/project_navigation.ts similarity index 100% rename from packages/core/chrome/core-chrome-browser/src/project_navigation.ts rename to src/core/packages/chrome/browser/src/project_navigation.ts diff --git a/packages/core/chrome/core-chrome-browser/src/recently_accessed.ts b/src/core/packages/chrome/browser/src/recently_accessed.ts similarity index 100% rename from packages/core/chrome/core-chrome-browser/src/recently_accessed.ts rename to src/core/packages/chrome/browser/src/recently_accessed.ts diff --git a/packages/core/chrome/core-chrome-browser/src/types.ts b/src/core/packages/chrome/browser/src/types.ts similarity index 100% rename from packages/core/chrome/core-chrome-browser/src/types.ts rename to src/core/packages/chrome/browser/src/types.ts diff --git a/packages/core/chrome/core-chrome-browser/tsconfig.json b/src/core/packages/chrome/browser/tsconfig.json similarity index 92% rename from packages/core/chrome/core-chrome-browser/tsconfig.json rename to src/core/packages/chrome/browser/tsconfig.json index 03ae62928c03e..d2bbfd79414c7 100644 --- a/packages/core/chrome/core-chrome-browser/tsconfig.json +++ b/src/core/packages/chrome/browser/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../../tsconfig.base.json", + "extends": "../../../../../tsconfig.base.json", "compilerOptions": { "outDir": "target/types", "types": [ diff --git a/packages/kbn-item-buffer/README.md b/src/platform/packages/private/kbn-item-buffer/README.md similarity index 100% rename from packages/kbn-item-buffer/README.md rename to src/platform/packages/private/kbn-item-buffer/README.md diff --git a/packages/kbn-item-buffer/index.ts b/src/platform/packages/private/kbn-item-buffer/index.ts similarity index 100% rename from packages/kbn-item-buffer/index.ts rename to src/platform/packages/private/kbn-item-buffer/index.ts diff --git a/packages/home/sample_data_types/jest.config.js b/src/platform/packages/private/kbn-item-buffer/jest.config.js similarity index 84% rename from packages/home/sample_data_types/jest.config.js rename to src/platform/packages/private/kbn-item-buffer/jest.config.js index a1d6c3cbb0bd1..2403741c95ba1 100644 --- a/packages/home/sample_data_types/jest.config.js +++ b/src/platform/packages/private/kbn-item-buffer/jest.config.js @@ -9,6 +9,6 @@ module.exports = { preset: '@kbn/test/jest_node', - rootDir: '../../..', - roots: ['/packages/home/sample_data_types'], + rootDir: '../../../../..', + roots: ['/src/platform/packages/private/kbn-item-buffer'], }; diff --git a/packages/kbn-item-buffer/kibana.jsonc b/src/platform/packages/private/kbn-item-buffer/kibana.jsonc similarity index 100% rename from packages/kbn-item-buffer/kibana.jsonc rename to src/platform/packages/private/kbn-item-buffer/kibana.jsonc diff --git a/packages/kbn-item-buffer/package.json b/src/platform/packages/private/kbn-item-buffer/package.json similarity index 100% rename from packages/kbn-item-buffer/package.json rename to src/platform/packages/private/kbn-item-buffer/package.json diff --git a/packages/kbn-item-buffer/src/__test__/run_item_buffer_tests.ts b/src/platform/packages/private/kbn-item-buffer/src/__test__/run_item_buffer_tests.ts similarity index 100% rename from packages/kbn-item-buffer/src/__test__/run_item_buffer_tests.ts rename to src/platform/packages/private/kbn-item-buffer/src/__test__/run_item_buffer_tests.ts diff --git a/packages/kbn-item-buffer/src/index.ts b/src/platform/packages/private/kbn-item-buffer/src/index.ts similarity index 100% rename from packages/kbn-item-buffer/src/index.ts rename to src/platform/packages/private/kbn-item-buffer/src/index.ts diff --git a/packages/kbn-item-buffer/src/item_buffer.test.ts b/src/platform/packages/private/kbn-item-buffer/src/item_buffer.test.ts similarity index 100% rename from packages/kbn-item-buffer/src/item_buffer.test.ts rename to src/platform/packages/private/kbn-item-buffer/src/item_buffer.test.ts diff --git a/packages/kbn-item-buffer/src/item_buffer.ts b/src/platform/packages/private/kbn-item-buffer/src/item_buffer.ts similarity index 100% rename from packages/kbn-item-buffer/src/item_buffer.ts rename to src/platform/packages/private/kbn-item-buffer/src/item_buffer.ts diff --git a/packages/kbn-item-buffer/src/timed_item_buffer.test.ts b/src/platform/packages/private/kbn-item-buffer/src/timed_item_buffer.test.ts similarity index 100% rename from packages/kbn-item-buffer/src/timed_item_buffer.test.ts rename to src/platform/packages/private/kbn-item-buffer/src/timed_item_buffer.test.ts diff --git a/packages/kbn-item-buffer/src/timed_item_buffer.ts b/src/platform/packages/private/kbn-item-buffer/src/timed_item_buffer.ts similarity index 100% rename from packages/kbn-item-buffer/src/timed_item_buffer.ts rename to src/platform/packages/private/kbn-item-buffer/src/timed_item_buffer.ts diff --git a/packages/kbn-react-mute-legacy-root-warning/tsconfig.json b/src/platform/packages/private/kbn-item-buffer/tsconfig.json similarity index 80% rename from packages/kbn-react-mute-legacy-root-warning/tsconfig.json rename to src/platform/packages/private/kbn-item-buffer/tsconfig.json index 2f9ddddbeea23..7aba1b1a9378a 100644 --- a/packages/kbn-react-mute-legacy-root-warning/tsconfig.json +++ b/src/platform/packages/private/kbn-item-buffer/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../../../../tsconfig.base.json", "compilerOptions": { "outDir": "target/types", "types": [ diff --git a/packages/kbn-react-mute-legacy-root-warning/README.md b/src/platform/packages/private/kbn-react-mute-legacy-root-warning/README.md similarity index 100% rename from packages/kbn-react-mute-legacy-root-warning/README.md rename to src/platform/packages/private/kbn-react-mute-legacy-root-warning/README.md diff --git a/packages/kbn-react-mute-legacy-root-warning/index.ts b/src/platform/packages/private/kbn-react-mute-legacy-root-warning/index.ts similarity index 100% rename from packages/kbn-react-mute-legacy-root-warning/index.ts rename to src/platform/packages/private/kbn-react-mute-legacy-root-warning/index.ts diff --git a/src/platform/packages/private/kbn-react-mute-legacy-root-warning/jest.config.js b/src/platform/packages/private/kbn-react-mute-legacy-root-warning/jest.config.js new file mode 100644 index 0000000000000..da6175c9c6c3b --- /dev/null +++ b/src/platform/packages/private/kbn-react-mute-legacy-root-warning/jest.config.js @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +module.exports = { + preset: '@kbn/test/jest_node', + rootDir: '../../../../..', + roots: ['/src/platform/packages/private/kbn-react-mute-legacy-root-warning'], +}; diff --git a/packages/kbn-react-mute-legacy-root-warning/kibana.jsonc b/src/platform/packages/private/kbn-react-mute-legacy-root-warning/kibana.jsonc similarity index 100% rename from packages/kbn-react-mute-legacy-root-warning/kibana.jsonc rename to src/platform/packages/private/kbn-react-mute-legacy-root-warning/kibana.jsonc diff --git a/packages/kbn-react-mute-legacy-root-warning/package.json b/src/platform/packages/private/kbn-react-mute-legacy-root-warning/package.json similarity index 100% rename from packages/kbn-react-mute-legacy-root-warning/package.json rename to src/platform/packages/private/kbn-react-mute-legacy-root-warning/package.json diff --git a/packages/kbn-item-buffer/tsconfig.json b/src/platform/packages/private/kbn-react-mute-legacy-root-warning/tsconfig.json similarity index 80% rename from packages/kbn-item-buffer/tsconfig.json rename to src/platform/packages/private/kbn-react-mute-legacy-root-warning/tsconfig.json index 2f9ddddbeea23..7aba1b1a9378a 100644 --- a/packages/kbn-item-buffer/tsconfig.json +++ b/src/platform/packages/private/kbn-react-mute-legacy-root-warning/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../../../../tsconfig.base.json", "compilerOptions": { "outDir": "target/types", "types": [ diff --git a/packages/kbn-saved-objects-settings/README.md b/src/platform/packages/private/kbn-saved-objects-settings/README.md similarity index 100% rename from packages/kbn-saved-objects-settings/README.md rename to src/platform/packages/private/kbn-saved-objects-settings/README.md diff --git a/packages/kbn-saved-objects-settings/index.ts b/src/platform/packages/private/kbn-saved-objects-settings/index.ts similarity index 100% rename from packages/kbn-saved-objects-settings/index.ts rename to src/platform/packages/private/kbn-saved-objects-settings/index.ts diff --git a/packages/kbn-react-mute-legacy-root-warning/jest.config.js b/src/platform/packages/private/kbn-saved-objects-settings/jest.config.js similarity index 82% rename from packages/kbn-react-mute-legacy-root-warning/jest.config.js rename to src/platform/packages/private/kbn-saved-objects-settings/jest.config.js index 00d62deb5770f..0e76068f71342 100644 --- a/packages/kbn-react-mute-legacy-root-warning/jest.config.js +++ b/src/platform/packages/private/kbn-saved-objects-settings/jest.config.js @@ -9,6 +9,6 @@ module.exports = { preset: '@kbn/test/jest_node', - rootDir: '../..', - roots: ['/packages/kbn-react-mute-legacy-root-warning'], + rootDir: '../../../../..', + roots: ['/src/platform/packages/private/kbn-saved-objects-settings'], }; diff --git a/packages/kbn-saved-objects-settings/kibana.jsonc b/src/platform/packages/private/kbn-saved-objects-settings/kibana.jsonc similarity index 100% rename from packages/kbn-saved-objects-settings/kibana.jsonc rename to src/platform/packages/private/kbn-saved-objects-settings/kibana.jsonc diff --git a/packages/kbn-saved-objects-settings/package.json b/src/platform/packages/private/kbn-saved-objects-settings/package.json similarity index 100% rename from packages/kbn-saved-objects-settings/package.json rename to src/platform/packages/private/kbn-saved-objects-settings/package.json diff --git a/packages/kbn-saved-objects-settings/tsconfig.json b/src/platform/packages/private/kbn-saved-objects-settings/tsconfig.json similarity index 80% rename from packages/kbn-saved-objects-settings/tsconfig.json rename to src/platform/packages/private/kbn-saved-objects-settings/tsconfig.json index 2f9ddddbeea23..7aba1b1a9378a 100644 --- a/packages/kbn-saved-objects-settings/tsconfig.json +++ b/src/platform/packages/private/kbn-saved-objects-settings/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../../../../tsconfig.base.json", "compilerOptions": { "outDir": "target/types", "types": [ diff --git a/src/platform/packages/private/kbn-ui-shared-deps-src/BUILD.bazel b/src/platform/packages/private/kbn-ui-shared-deps-src/BUILD.bazel index 0cccba9b65a1a..f14402dfa88c1 100644 --- a/src/platform/packages/private/kbn-ui-shared-deps-src/BUILD.bazel +++ b/src/platform/packages/private/kbn-ui-shared-deps-src/BUILD.bazel @@ -39,10 +39,10 @@ webpack_cli( "//src/platform/packages/shared/shared-ux/error_boundary", "//src/platform/packages/shared/kbn-rison", "//src/platform/packages/shared/shared-ux/code_editor/impl:code_editor", - "//packages/react/kibana_context/common", - "//packages/react/kibana_context/root", - "//packages/react/kibana_context/render", - "//packages/react/kibana_context/theme", + "//src/platform/packages/shared/react/kibana_context/common", + "//src/platform/packages/shared/react/kibana_context/root", + "//src/platform/packages/shared/react/kibana_context/render", + "//src/platform/packages/shared/react/kibana_context/theme", "//src/platform/packages/shared/shared-ux/router/impl:shared-ux-router", ], output_dir = True, diff --git a/packages/serverless/project_switcher/README.mdx b/src/platform/packages/private/serverless/project_switcher/README.mdx similarity index 100% rename from packages/serverless/project_switcher/README.mdx rename to src/platform/packages/private/serverless/project_switcher/README.mdx diff --git a/packages/serverless/project_switcher/index.ts b/src/platform/packages/private/serverless/project_switcher/index.ts similarity index 100% rename from packages/serverless/project_switcher/index.ts rename to src/platform/packages/private/serverless/project_switcher/index.ts diff --git a/src/platform/packages/private/serverless/project_switcher/jest.config.js b/src/platform/packages/private/serverless/project_switcher/jest.config.js new file mode 100644 index 0000000000000..17c8a482cb94b --- /dev/null +++ b/src/platform/packages/private/serverless/project_switcher/jest.config.js @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../../../../..', + roots: ['/src/platform/packages/private/serverless/project_switcher'], +}; diff --git a/packages/serverless/project_switcher/kibana.jsonc b/src/platform/packages/private/serverless/project_switcher/kibana.jsonc similarity index 100% rename from packages/serverless/project_switcher/kibana.jsonc rename to src/platform/packages/private/serverless/project_switcher/kibana.jsonc diff --git a/packages/serverless/project_switcher/mocks/jest.mock.ts b/src/platform/packages/private/serverless/project_switcher/mocks/jest.mock.ts similarity index 100% rename from packages/serverless/project_switcher/mocks/jest.mock.ts rename to src/platform/packages/private/serverless/project_switcher/mocks/jest.mock.ts diff --git a/packages/serverless/project_switcher/mocks/storybook.mock.ts b/src/platform/packages/private/serverless/project_switcher/mocks/storybook.mock.ts similarity index 100% rename from packages/serverless/project_switcher/mocks/storybook.mock.ts rename to src/platform/packages/private/serverless/project_switcher/mocks/storybook.mock.ts diff --git a/packages/serverless/project_switcher/package.json b/src/platform/packages/private/serverless/project_switcher/package.json similarity index 100% rename from packages/serverless/project_switcher/package.json rename to src/platform/packages/private/serverless/project_switcher/package.json diff --git a/packages/serverless/project_switcher/src/constants.ts b/src/platform/packages/private/serverless/project_switcher/src/constants.ts similarity index 100% rename from packages/serverless/project_switcher/src/constants.ts rename to src/platform/packages/private/serverless/project_switcher/src/constants.ts diff --git a/packages/serverless/project_switcher/src/header_button.tsx b/src/platform/packages/private/serverless/project_switcher/src/header_button.tsx similarity index 100% rename from packages/serverless/project_switcher/src/header_button.tsx rename to src/platform/packages/private/serverless/project_switcher/src/header_button.tsx diff --git a/packages/serverless/project_switcher/src/index.ts b/src/platform/packages/private/serverless/project_switcher/src/index.ts similarity index 100% rename from packages/serverless/project_switcher/src/index.ts rename to src/platform/packages/private/serverless/project_switcher/src/index.ts diff --git a/packages/serverless/project_switcher/src/item.tsx b/src/platform/packages/private/serverless/project_switcher/src/item.tsx similarity index 100% rename from packages/serverless/project_switcher/src/item.tsx rename to src/platform/packages/private/serverless/project_switcher/src/item.tsx diff --git a/packages/serverless/project_switcher/src/loader.tsx b/src/platform/packages/private/serverless/project_switcher/src/loader.tsx similarity index 100% rename from packages/serverless/project_switcher/src/loader.tsx rename to src/platform/packages/private/serverless/project_switcher/src/loader.tsx diff --git a/packages/serverless/project_switcher/src/logo.tsx b/src/platform/packages/private/serverless/project_switcher/src/logo.tsx similarity index 100% rename from packages/serverless/project_switcher/src/logo.tsx rename to src/platform/packages/private/serverless/project_switcher/src/logo.tsx diff --git a/packages/serverless/project_switcher/src/services.tsx b/src/platform/packages/private/serverless/project_switcher/src/services.tsx similarity index 100% rename from packages/serverless/project_switcher/src/services.tsx rename to src/platform/packages/private/serverless/project_switcher/src/services.tsx diff --git a/packages/serverless/project_switcher/src/switcher.component.tsx b/src/platform/packages/private/serverless/project_switcher/src/switcher.component.tsx similarity index 100% rename from packages/serverless/project_switcher/src/switcher.component.tsx rename to src/platform/packages/private/serverless/project_switcher/src/switcher.component.tsx diff --git a/packages/serverless/project_switcher/src/switcher.stories.tsx b/src/platform/packages/private/serverless/project_switcher/src/switcher.stories.tsx similarity index 100% rename from packages/serverless/project_switcher/src/switcher.stories.tsx rename to src/platform/packages/private/serverless/project_switcher/src/switcher.stories.tsx diff --git a/packages/serverless/project_switcher/src/switcher.test.tsx b/src/platform/packages/private/serverless/project_switcher/src/switcher.test.tsx similarity index 100% rename from packages/serverless/project_switcher/src/switcher.test.tsx rename to src/platform/packages/private/serverless/project_switcher/src/switcher.test.tsx diff --git a/packages/serverless/project_switcher/src/switcher.tsx b/src/platform/packages/private/serverless/project_switcher/src/switcher.tsx similarity index 100% rename from packages/serverless/project_switcher/src/switcher.tsx rename to src/platform/packages/private/serverless/project_switcher/src/switcher.tsx diff --git a/packages/serverless/project_switcher/src/types.ts b/src/platform/packages/private/serverless/project_switcher/src/types.ts similarity index 100% rename from packages/serverless/project_switcher/src/types.ts rename to src/platform/packages/private/serverless/project_switcher/src/types.ts diff --git a/packages/serverless/project_switcher/tsconfig.json b/src/platform/packages/private/serverless/project_switcher/tsconfig.json similarity index 86% rename from packages/serverless/project_switcher/tsconfig.json rename to src/platform/packages/private/serverless/project_switcher/tsconfig.json index 8fd6b54236754..d477303d65059 100644 --- a/packages/serverless/project_switcher/tsconfig.json +++ b/src/platform/packages/private/serverless/project_switcher/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.base.json", + "extends": "../../../../../../tsconfig.base.json", "compilerOptions": { "outDir": "target/types", "types": [ diff --git a/packages/serverless/settings/common/README.mdx b/src/platform/packages/private/serverless/settings/common/README.mdx similarity index 100% rename from packages/serverless/settings/common/README.mdx rename to src/platform/packages/private/serverless/settings/common/README.mdx diff --git a/packages/serverless/settings/common/index.ts b/src/platform/packages/private/serverless/settings/common/index.ts similarity index 100% rename from packages/serverless/settings/common/index.ts rename to src/platform/packages/private/serverless/settings/common/index.ts diff --git a/packages/serverless/settings/common/kibana.jsonc b/src/platform/packages/private/serverless/settings/common/kibana.jsonc similarity index 100% rename from packages/serverless/settings/common/kibana.jsonc rename to src/platform/packages/private/serverless/settings/common/kibana.jsonc diff --git a/packages/serverless/settings/common/package.json b/src/platform/packages/private/serverless/settings/common/package.json similarity index 100% rename from packages/serverless/settings/common/package.json rename to src/platform/packages/private/serverless/settings/common/package.json diff --git a/packages/serverless/settings/common/tsconfig.json b/src/platform/packages/private/serverless/settings/common/tsconfig.json similarity index 81% rename from packages/serverless/settings/common/tsconfig.json rename to src/platform/packages/private/serverless/settings/common/tsconfig.json index 16d6022e3d9bc..16a9ba9ba4b5d 100644 --- a/packages/serverless/settings/common/tsconfig.json +++ b/src/platform/packages/private/serverless/settings/common/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../../tsconfig.base.json", + "extends": "../../../../../../../tsconfig.base.json", "compilerOptions": { "outDir": "target/types", "types": [ diff --git a/packages/serverless/types/README.mdx b/src/platform/packages/private/serverless/types/README.mdx similarity index 100% rename from packages/serverless/types/README.mdx rename to src/platform/packages/private/serverless/types/README.mdx diff --git a/packages/serverless/types/index.d.ts b/src/platform/packages/private/serverless/types/index.d.ts similarity index 100% rename from packages/serverless/types/index.d.ts rename to src/platform/packages/private/serverless/types/index.d.ts diff --git a/packages/serverless/types/kibana.jsonc b/src/platform/packages/private/serverless/types/kibana.jsonc similarity index 100% rename from packages/serverless/types/kibana.jsonc rename to src/platform/packages/private/serverless/types/kibana.jsonc diff --git a/packages/serverless/types/package.json b/src/platform/packages/private/serverless/types/package.json similarity index 100% rename from packages/serverless/types/package.json rename to src/platform/packages/private/serverless/types/package.json diff --git a/packages/serverless/types/tsconfig.json b/src/platform/packages/private/serverless/types/tsconfig.json similarity index 79% rename from packages/serverless/types/tsconfig.json rename to src/platform/packages/private/serverless/types/tsconfig.json index 6d27b06d5f8ba..63f0b5ff33faa 100644 --- a/packages/serverless/types/tsconfig.json +++ b/src/platform/packages/private/serverless/types/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.base.json", + "extends": "../../../../../../tsconfig.base.json", "compilerOptions": { "outDir": "target/types", "types": [ diff --git a/packages/deeplinks/shared/README.md b/src/platform/packages/shared/deeplinks/shared/README.md similarity index 100% rename from packages/deeplinks/shared/README.md rename to src/platform/packages/shared/deeplinks/shared/README.md diff --git a/packages/deeplinks/shared/deep_links.ts b/src/platform/packages/shared/deeplinks/shared/deep_links.ts similarity index 100% rename from packages/deeplinks/shared/deep_links.ts rename to src/platform/packages/shared/deeplinks/shared/deep_links.ts diff --git a/packages/deeplinks/shared/index.ts b/src/platform/packages/shared/deeplinks/shared/index.ts similarity index 100% rename from packages/deeplinks/shared/index.ts rename to src/platform/packages/shared/deeplinks/shared/index.ts diff --git a/packages/core/chrome/core-chrome-browser/jest.config.js b/src/platform/packages/shared/deeplinks/shared/jest.config.js similarity index 83% rename from packages/core/chrome/core-chrome-browser/jest.config.js rename to src/platform/packages/shared/deeplinks/shared/jest.config.js index c8f9d2b1fbcaf..65434efdc3715 100644 --- a/packages/core/chrome/core-chrome-browser/jest.config.js +++ b/src/platform/packages/shared/deeplinks/shared/jest.config.js @@ -9,6 +9,6 @@ module.exports = { preset: '@kbn/test', - rootDir: '../../../..', - roots: ['/packages/core/chrome/core-chrome-browser'], + rootDir: '../../../../../..', + roots: ['/src/platform/packages/shared/deeplinks/shared'], }; diff --git a/packages/deeplinks/shared/kibana.jsonc b/src/platform/packages/shared/deeplinks/shared/kibana.jsonc similarity index 100% rename from packages/deeplinks/shared/kibana.jsonc rename to src/platform/packages/shared/deeplinks/shared/kibana.jsonc diff --git a/packages/deeplinks/shared/package.json b/src/platform/packages/shared/deeplinks/shared/package.json similarity index 100% rename from packages/deeplinks/shared/package.json rename to src/platform/packages/shared/deeplinks/shared/package.json diff --git a/packages/deeplinks/shared/tsconfig.json b/src/platform/packages/shared/deeplinks/shared/tsconfig.json similarity index 81% rename from packages/deeplinks/shared/tsconfig.json rename to src/platform/packages/shared/deeplinks/shared/tsconfig.json index d1414086f2187..18d16ae2e8837 100644 --- a/packages/deeplinks/shared/tsconfig.json +++ b/src/platform/packages/shared/deeplinks/shared/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.base.json", + "extends": "../../../../../../tsconfig.base.json", "compilerOptions": { "outDir": "target/types", "types": [ diff --git a/packages/home/sample_data_card/README.mdx b/src/platform/packages/shared/home/sample_data_card/README.mdx similarity index 100% rename from packages/home/sample_data_card/README.mdx rename to src/platform/packages/shared/home/sample_data_card/README.mdx diff --git a/packages/home/sample_data_card/index.ts b/src/platform/packages/shared/home/sample_data_card/index.ts similarity index 100% rename from packages/home/sample_data_card/index.ts rename to src/platform/packages/shared/home/sample_data_card/index.ts diff --git a/src/platform/packages/shared/home/sample_data_card/jest.config.js b/src/platform/packages/shared/home/sample_data_card/jest.config.js new file mode 100644 index 0000000000000..0491810279eac --- /dev/null +++ b/src/platform/packages/shared/home/sample_data_card/jest.config.js @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../../../../..', + roots: ['/src/platform/packages/shared/home/sample_data_card'], +}; diff --git a/packages/home/sample_data_card/kibana.jsonc b/src/platform/packages/shared/home/sample_data_card/kibana.jsonc similarity index 100% rename from packages/home/sample_data_card/kibana.jsonc rename to src/platform/packages/shared/home/sample_data_card/kibana.jsonc diff --git a/packages/home/sample_data_card/package.json b/src/platform/packages/shared/home/sample_data_card/package.json similarity index 100% rename from packages/home/sample_data_card/package.json rename to src/platform/packages/shared/home/sample_data_card/package.json diff --git a/packages/home/sample_data_card/src/__snapshots__/sample_data_card.test.tsx.snap b/src/platform/packages/shared/home/sample_data_card/src/__snapshots__/sample_data_card.test.tsx.snap similarity index 100% rename from packages/home/sample_data_card/src/__snapshots__/sample_data_card.test.tsx.snap rename to src/platform/packages/shared/home/sample_data_card/src/__snapshots__/sample_data_card.test.tsx.snap diff --git a/packages/home/sample_data_card/src/constants.ts b/src/platform/packages/shared/home/sample_data_card/src/constants.ts similarity index 100% rename from packages/home/sample_data_card/src/constants.ts rename to src/platform/packages/shared/home/sample_data_card/src/constants.ts diff --git a/packages/home/sample_data_card/src/footer/__snapshots__/disabled_footer.test.tsx.snap b/src/platform/packages/shared/home/sample_data_card/src/footer/__snapshots__/disabled_footer.test.tsx.snap similarity index 100% rename from packages/home/sample_data_card/src/footer/__snapshots__/disabled_footer.test.tsx.snap rename to src/platform/packages/shared/home/sample_data_card/src/footer/__snapshots__/disabled_footer.test.tsx.snap diff --git a/packages/home/sample_data_card/src/footer/__snapshots__/install_footer.test.tsx.snap b/src/platform/packages/shared/home/sample_data_card/src/footer/__snapshots__/install_footer.test.tsx.snap similarity index 100% rename from packages/home/sample_data_card/src/footer/__snapshots__/install_footer.test.tsx.snap rename to src/platform/packages/shared/home/sample_data_card/src/footer/__snapshots__/install_footer.test.tsx.snap diff --git a/packages/home/sample_data_card/src/footer/__snapshots__/remove_footer.test.tsx.snap b/src/platform/packages/shared/home/sample_data_card/src/footer/__snapshots__/remove_footer.test.tsx.snap similarity index 100% rename from packages/home/sample_data_card/src/footer/__snapshots__/remove_footer.test.tsx.snap rename to src/platform/packages/shared/home/sample_data_card/src/footer/__snapshots__/remove_footer.test.tsx.snap diff --git a/packages/home/sample_data_card/src/footer/__snapshots__/view_button.test.tsx.snap b/src/platform/packages/shared/home/sample_data_card/src/footer/__snapshots__/view_button.test.tsx.snap similarity index 100% rename from packages/home/sample_data_card/src/footer/__snapshots__/view_button.test.tsx.snap rename to src/platform/packages/shared/home/sample_data_card/src/footer/__snapshots__/view_button.test.tsx.snap diff --git a/packages/home/sample_data_card/src/footer/disabled_footer.test.tsx b/src/platform/packages/shared/home/sample_data_card/src/footer/disabled_footer.test.tsx similarity index 100% rename from packages/home/sample_data_card/src/footer/disabled_footer.test.tsx rename to src/platform/packages/shared/home/sample_data_card/src/footer/disabled_footer.test.tsx diff --git a/packages/home/sample_data_card/src/footer/disabled_footer.tsx b/src/platform/packages/shared/home/sample_data_card/src/footer/disabled_footer.tsx similarity index 100% rename from packages/home/sample_data_card/src/footer/disabled_footer.tsx rename to src/platform/packages/shared/home/sample_data_card/src/footer/disabled_footer.tsx diff --git a/packages/home/sample_data_card/src/footer/footer.stories.tsx b/src/platform/packages/shared/home/sample_data_card/src/footer/footer.stories.tsx similarity index 100% rename from packages/home/sample_data_card/src/footer/footer.stories.tsx rename to src/platform/packages/shared/home/sample_data_card/src/footer/footer.stories.tsx diff --git a/packages/home/sample_data_card/src/footer/index.tsx b/src/platform/packages/shared/home/sample_data_card/src/footer/index.tsx similarity index 100% rename from packages/home/sample_data_card/src/footer/index.tsx rename to src/platform/packages/shared/home/sample_data_card/src/footer/index.tsx diff --git a/packages/home/sample_data_card/src/footer/install_footer.test.tsx b/src/platform/packages/shared/home/sample_data_card/src/footer/install_footer.test.tsx similarity index 100% rename from packages/home/sample_data_card/src/footer/install_footer.test.tsx rename to src/platform/packages/shared/home/sample_data_card/src/footer/install_footer.test.tsx diff --git a/packages/home/sample_data_card/src/footer/install_footer.tsx b/src/platform/packages/shared/home/sample_data_card/src/footer/install_footer.tsx similarity index 100% rename from packages/home/sample_data_card/src/footer/install_footer.tsx rename to src/platform/packages/shared/home/sample_data_card/src/footer/install_footer.tsx diff --git a/packages/home/sample_data_card/src/footer/remove_footer.test.tsx b/src/platform/packages/shared/home/sample_data_card/src/footer/remove_footer.test.tsx similarity index 100% rename from packages/home/sample_data_card/src/footer/remove_footer.test.tsx rename to src/platform/packages/shared/home/sample_data_card/src/footer/remove_footer.test.tsx diff --git a/packages/home/sample_data_card/src/footer/remove_footer.tsx b/src/platform/packages/shared/home/sample_data_card/src/footer/remove_footer.tsx similarity index 100% rename from packages/home/sample_data_card/src/footer/remove_footer.tsx rename to src/platform/packages/shared/home/sample_data_card/src/footer/remove_footer.tsx diff --git a/packages/home/sample_data_card/src/footer/view_button.test.tsx b/src/platform/packages/shared/home/sample_data_card/src/footer/view_button.test.tsx similarity index 100% rename from packages/home/sample_data_card/src/footer/view_button.test.tsx rename to src/platform/packages/shared/home/sample_data_card/src/footer/view_button.test.tsx diff --git a/packages/home/sample_data_card/src/footer/view_button.tsx b/src/platform/packages/shared/home/sample_data_card/src/footer/view_button.tsx similarity index 100% rename from packages/home/sample_data_card/src/footer/view_button.tsx rename to src/platform/packages/shared/home/sample_data_card/src/footer/view_button.tsx diff --git a/packages/home/sample_data_card/src/hooks/index.ts b/src/platform/packages/shared/home/sample_data_card/src/hooks/index.ts similarity index 100% rename from packages/home/sample_data_card/src/hooks/index.ts rename to src/platform/packages/shared/home/sample_data_card/src/hooks/index.ts diff --git a/packages/home/sample_data_card/src/hooks/use_install.ts b/src/platform/packages/shared/home/sample_data_card/src/hooks/use_install.ts similarity index 100% rename from packages/home/sample_data_card/src/hooks/use_install.ts rename to src/platform/packages/shared/home/sample_data_card/src/hooks/use_install.ts diff --git a/packages/home/sample_data_card/src/hooks/use_remove.ts b/src/platform/packages/shared/home/sample_data_card/src/hooks/use_remove.ts similarity index 100% rename from packages/home/sample_data_card/src/hooks/use_remove.ts rename to src/platform/packages/shared/home/sample_data_card/src/hooks/use_remove.ts diff --git a/packages/home/sample_data_card/src/mocks/dashboard.png b/src/platform/packages/shared/home/sample_data_card/src/mocks/dashboard.png similarity index 100% rename from packages/home/sample_data_card/src/mocks/dashboard.png rename to src/platform/packages/shared/home/sample_data_card/src/mocks/dashboard.png diff --git a/packages/home/sample_data_card/src/mocks/dashboard_dark.png b/src/platform/packages/shared/home/sample_data_card/src/mocks/dashboard_dark.png similarity index 100% rename from packages/home/sample_data_card/src/mocks/dashboard_dark.png rename to src/platform/packages/shared/home/sample_data_card/src/mocks/dashboard_dark.png diff --git a/packages/home/sample_data_card/src/mocks/icon.svg b/src/platform/packages/shared/home/sample_data_card/src/mocks/icon.svg similarity index 100% rename from packages/home/sample_data_card/src/mocks/icon.svg rename to src/platform/packages/shared/home/sample_data_card/src/mocks/icon.svg diff --git a/packages/home/sample_data_card/src/mocks/index.ts b/src/platform/packages/shared/home/sample_data_card/src/mocks/index.ts similarity index 100% rename from packages/home/sample_data_card/src/mocks/index.ts rename to src/platform/packages/shared/home/sample_data_card/src/mocks/index.ts diff --git a/packages/home/sample_data_card/src/sample_data_card.component.tsx b/src/platform/packages/shared/home/sample_data_card/src/sample_data_card.component.tsx similarity index 100% rename from packages/home/sample_data_card/src/sample_data_card.component.tsx rename to src/platform/packages/shared/home/sample_data_card/src/sample_data_card.component.tsx diff --git a/packages/home/sample_data_card/src/sample_data_card.stories.tsx b/src/platform/packages/shared/home/sample_data_card/src/sample_data_card.stories.tsx similarity index 100% rename from packages/home/sample_data_card/src/sample_data_card.stories.tsx rename to src/platform/packages/shared/home/sample_data_card/src/sample_data_card.stories.tsx diff --git a/packages/home/sample_data_card/src/sample_data_card.test.tsx b/src/platform/packages/shared/home/sample_data_card/src/sample_data_card.test.tsx similarity index 100% rename from packages/home/sample_data_card/src/sample_data_card.test.tsx rename to src/platform/packages/shared/home/sample_data_card/src/sample_data_card.test.tsx diff --git a/packages/home/sample_data_card/src/sample_data_card.tsx b/src/platform/packages/shared/home/sample_data_card/src/sample_data_card.tsx similarity index 100% rename from packages/home/sample_data_card/src/sample_data_card.tsx rename to src/platform/packages/shared/home/sample_data_card/src/sample_data_card.tsx diff --git a/packages/home/sample_data_card/src/services.tsx b/src/platform/packages/shared/home/sample_data_card/src/services.tsx similarity index 100% rename from packages/home/sample_data_card/src/services.tsx rename to src/platform/packages/shared/home/sample_data_card/src/services.tsx diff --git a/packages/home/sample_data_card/tsconfig.json b/src/platform/packages/shared/home/sample_data_card/tsconfig.json similarity index 86% rename from packages/home/sample_data_card/tsconfig.json rename to src/platform/packages/shared/home/sample_data_card/tsconfig.json index 531dd52379f12..6f2e89c5019f9 100644 --- a/packages/home/sample_data_card/tsconfig.json +++ b/src/platform/packages/shared/home/sample_data_card/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.base.json", + "extends": "../../../../../../tsconfig.base.json", "compilerOptions": { "outDir": "target/types", "types": [ diff --git a/packages/home/sample_data_tab/README.mdx b/src/platform/packages/shared/home/sample_data_tab/README.mdx similarity index 100% rename from packages/home/sample_data_tab/README.mdx rename to src/platform/packages/shared/home/sample_data_tab/README.mdx diff --git a/packages/home/sample_data_tab/index.ts b/src/platform/packages/shared/home/sample_data_tab/index.ts similarity index 100% rename from packages/home/sample_data_tab/index.ts rename to src/platform/packages/shared/home/sample_data_tab/index.ts diff --git a/packages/home/sample_data_tab/jest.config.js b/src/platform/packages/shared/home/sample_data_tab/jest.config.js similarity index 82% rename from packages/home/sample_data_tab/jest.config.js rename to src/platform/packages/shared/home/sample_data_tab/jest.config.js index 1d081809f82b7..1372916903c7e 100644 --- a/packages/home/sample_data_tab/jest.config.js +++ b/src/platform/packages/shared/home/sample_data_tab/jest.config.js @@ -9,6 +9,6 @@ module.exports = { preset: '@kbn/test', - rootDir: '../../..', - roots: ['/packages/home/sample_data_tab'], + rootDir: '../../../../../..', + roots: ['/src/platform/packages/shared/home/sample_data_tab'], }; diff --git a/packages/home/sample_data_tab/kibana.jsonc b/src/platform/packages/shared/home/sample_data_tab/kibana.jsonc similarity index 100% rename from packages/home/sample_data_tab/kibana.jsonc rename to src/platform/packages/shared/home/sample_data_tab/kibana.jsonc diff --git a/packages/home/sample_data_tab/package.json b/src/platform/packages/shared/home/sample_data_tab/package.json similarity index 100% rename from packages/home/sample_data_tab/package.json rename to src/platform/packages/shared/home/sample_data_tab/package.json diff --git a/packages/home/sample_data_tab/src/assets/welcome_dark.png b/src/platform/packages/shared/home/sample_data_tab/src/assets/welcome_dark.png similarity index 100% rename from packages/home/sample_data_tab/src/assets/welcome_dark.png rename to src/platform/packages/shared/home/sample_data_tab/src/assets/welcome_dark.png diff --git a/packages/home/sample_data_tab/src/assets/welcome_light.png b/src/platform/packages/shared/home/sample_data_tab/src/assets/welcome_light.png similarity index 100% rename from packages/home/sample_data_tab/src/assets/welcome_light.png rename to src/platform/packages/shared/home/sample_data_tab/src/assets/welcome_light.png diff --git a/packages/home/sample_data_tab/src/constants.ts b/src/platform/packages/shared/home/sample_data_tab/src/constants.ts similarity index 100% rename from packages/home/sample_data_tab/src/constants.ts rename to src/platform/packages/shared/home/sample_data_tab/src/constants.ts diff --git a/packages/home/sample_data_tab/src/demo_env_panel.stories.tsx b/src/platform/packages/shared/home/sample_data_tab/src/demo_env_panel.stories.tsx similarity index 100% rename from packages/home/sample_data_tab/src/demo_env_panel.stories.tsx rename to src/platform/packages/shared/home/sample_data_tab/src/demo_env_panel.stories.tsx diff --git a/packages/home/sample_data_tab/src/demo_env_panel.tsx b/src/platform/packages/shared/home/sample_data_tab/src/demo_env_panel.tsx similarity index 100% rename from packages/home/sample_data_tab/src/demo_env_panel.tsx rename to src/platform/packages/shared/home/sample_data_tab/src/demo_env_panel.tsx diff --git a/packages/home/sample_data_tab/src/hooks/index.ts b/src/platform/packages/shared/home/sample_data_tab/src/hooks/index.ts similarity index 100% rename from packages/home/sample_data_tab/src/hooks/index.ts rename to src/platform/packages/shared/home/sample_data_tab/src/hooks/index.ts diff --git a/packages/home/sample_data_tab/src/hooks/use_list.ts b/src/platform/packages/shared/home/sample_data_tab/src/hooks/use_list.ts similarity index 100% rename from packages/home/sample_data_tab/src/hooks/use_list.ts rename to src/platform/packages/shared/home/sample_data_tab/src/hooks/use_list.ts diff --git a/packages/home/sample_data_tab/src/mocks.ts b/src/platform/packages/shared/home/sample_data_tab/src/mocks.ts similarity index 100% rename from packages/home/sample_data_tab/src/mocks.ts rename to src/platform/packages/shared/home/sample_data_tab/src/mocks.ts diff --git a/packages/home/sample_data_tab/src/sample_data_cards.tsx b/src/platform/packages/shared/home/sample_data_tab/src/sample_data_cards.tsx similarity index 100% rename from packages/home/sample_data_tab/src/sample_data_cards.tsx rename to src/platform/packages/shared/home/sample_data_tab/src/sample_data_cards.tsx diff --git a/packages/home/sample_data_tab/src/sample_data_tab.stories.tsx b/src/platform/packages/shared/home/sample_data_tab/src/sample_data_tab.stories.tsx similarity index 100% rename from packages/home/sample_data_tab/src/sample_data_tab.stories.tsx rename to src/platform/packages/shared/home/sample_data_tab/src/sample_data_tab.stories.tsx diff --git a/packages/home/sample_data_tab/src/sample_data_tab.tsx b/src/platform/packages/shared/home/sample_data_tab/src/sample_data_tab.tsx similarity index 100% rename from packages/home/sample_data_tab/src/sample_data_tab.tsx rename to src/platform/packages/shared/home/sample_data_tab/src/sample_data_tab.tsx diff --git a/packages/home/sample_data_tab/src/services.tsx b/src/platform/packages/shared/home/sample_data_tab/src/services.tsx similarity index 100% rename from packages/home/sample_data_tab/src/services.tsx rename to src/platform/packages/shared/home/sample_data_tab/src/services.tsx diff --git a/packages/home/sample_data_tab/tsconfig.json b/src/platform/packages/shared/home/sample_data_tab/tsconfig.json similarity index 87% rename from packages/home/sample_data_tab/tsconfig.json rename to src/platform/packages/shared/home/sample_data_tab/tsconfig.json index 3cdeb5489c22a..2397a4d1e7c9b 100644 --- a/packages/home/sample_data_tab/tsconfig.json +++ b/src/platform/packages/shared/home/sample_data_tab/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.base.json", + "extends": "../../../../../../tsconfig.base.json", "compilerOptions": { "outDir": "target/types", "types": [ diff --git a/packages/home/sample_data_types/README.mdx b/src/platform/packages/shared/home/sample_data_types/README.mdx similarity index 100% rename from packages/home/sample_data_types/README.mdx rename to src/platform/packages/shared/home/sample_data_types/README.mdx diff --git a/packages/home/sample_data_types/index.d.ts b/src/platform/packages/shared/home/sample_data_types/index.d.ts similarity index 100% rename from packages/home/sample_data_types/index.d.ts rename to src/platform/packages/shared/home/sample_data_types/index.d.ts diff --git a/packages/kbn-item-buffer/jest.config.js b/src/platform/packages/shared/home/sample_data_types/jest.config.js similarity index 82% rename from packages/kbn-item-buffer/jest.config.js rename to src/platform/packages/shared/home/sample_data_types/jest.config.js index f553426f800bb..5f72cf7a026bc 100644 --- a/packages/kbn-item-buffer/jest.config.js +++ b/src/platform/packages/shared/home/sample_data_types/jest.config.js @@ -9,6 +9,6 @@ module.exports = { preset: '@kbn/test/jest_node', - rootDir: '../..', - roots: ['/packages/kbn-item-buffer'], + rootDir: '../../../../../..', + roots: ['/src/platform/packages/shared/home/sample_data_types'], }; diff --git a/packages/home/sample_data_types/kibana.jsonc b/src/platform/packages/shared/home/sample_data_types/kibana.jsonc similarity index 100% rename from packages/home/sample_data_types/kibana.jsonc rename to src/platform/packages/shared/home/sample_data_types/kibana.jsonc diff --git a/packages/home/sample_data_types/package.json b/src/platform/packages/shared/home/sample_data_types/package.json similarity index 100% rename from packages/home/sample_data_types/package.json rename to src/platform/packages/shared/home/sample_data_types/package.json diff --git a/packages/home/sample_data_types/tsconfig.json b/src/platform/packages/shared/home/sample_data_types/tsconfig.json similarity index 73% rename from packages/home/sample_data_types/tsconfig.json rename to src/platform/packages/shared/home/sample_data_types/tsconfig.json index 493400e55a76f..48a59d313f003 100644 --- a/packages/home/sample_data_types/tsconfig.json +++ b/src/platform/packages/shared/home/sample_data_types/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.base.json", + "extends": "../../../../../../tsconfig.base.json", "compilerOptions": { "outDir": "target/types", "types": [] diff --git a/packages/kbn-guided-onboarding/README.md b/src/platform/packages/shared/kbn-guided-onboarding/README.md similarity index 100% rename from packages/kbn-guided-onboarding/README.md rename to src/platform/packages/shared/kbn-guided-onboarding/README.md diff --git a/packages/kbn-guided-onboarding/classic/index.ts b/src/platform/packages/shared/kbn-guided-onboarding/classic/index.ts similarity index 100% rename from packages/kbn-guided-onboarding/classic/index.ts rename to src/platform/packages/shared/kbn-guided-onboarding/classic/index.ts diff --git a/packages/kbn-guided-onboarding/guide/index.ts b/src/platform/packages/shared/kbn-guided-onboarding/guide/index.ts similarity index 100% rename from packages/kbn-guided-onboarding/guide/index.ts rename to src/platform/packages/shared/kbn-guided-onboarding/guide/index.ts diff --git a/packages/kbn-guided-onboarding/index.ts b/src/platform/packages/shared/kbn-guided-onboarding/index.ts similarity index 100% rename from packages/kbn-guided-onboarding/index.ts rename to src/platform/packages/shared/kbn-guided-onboarding/index.ts diff --git a/packages/home/sample_data_card/jest.config.js b/src/platform/packages/shared/kbn-guided-onboarding/jest.config.js similarity index 83% rename from packages/home/sample_data_card/jest.config.js rename to src/platform/packages/shared/kbn-guided-onboarding/jest.config.js index 0d1af72780e0a..b1ca25d2a3f74 100644 --- a/packages/home/sample_data_card/jest.config.js +++ b/src/platform/packages/shared/kbn-guided-onboarding/jest.config.js @@ -9,6 +9,6 @@ module.exports = { preset: '@kbn/test', - rootDir: '../../..', - roots: ['/packages/home/sample_data_card'], + rootDir: '../../../../..', + roots: ['/src/platform/packages/shared/kbn-guided-onboarding'], }; diff --git a/packages/kbn-guided-onboarding/kibana.jsonc b/src/platform/packages/shared/kbn-guided-onboarding/kibana.jsonc similarity index 100% rename from packages/kbn-guided-onboarding/kibana.jsonc rename to src/platform/packages/shared/kbn-guided-onboarding/kibana.jsonc diff --git a/packages/kbn-guided-onboarding/package.json b/src/platform/packages/shared/kbn-guided-onboarding/package.json similarity index 100% rename from packages/kbn-guided-onboarding/package.json rename to src/platform/packages/shared/kbn-guided-onboarding/package.json diff --git a/packages/kbn-guided-onboarding/src/common/test_guide_config.ts b/src/platform/packages/shared/kbn-guided-onboarding/src/common/test_guide_config.ts similarity index 100% rename from packages/kbn-guided-onboarding/src/common/test_guide_config.ts rename to src/platform/packages/shared/kbn-guided-onboarding/src/common/test_guide_config.ts diff --git a/packages/kbn-guided-onboarding/src/components/landing_page/classic_version/__snapshots__/guide_cards.test.tsx.snap b/src/platform/packages/shared/kbn-guided-onboarding/src/components/landing_page/classic_version/__snapshots__/guide_cards.test.tsx.snap similarity index 100% rename from packages/kbn-guided-onboarding/src/components/landing_page/classic_version/__snapshots__/guide_cards.test.tsx.snap rename to src/platform/packages/shared/kbn-guided-onboarding/src/components/landing_page/classic_version/__snapshots__/guide_cards.test.tsx.snap diff --git a/packages/kbn-guided-onboarding/src/components/landing_page/classic_version/guide_card.tsx b/src/platform/packages/shared/kbn-guided-onboarding/src/components/landing_page/classic_version/guide_card.tsx similarity index 100% rename from packages/kbn-guided-onboarding/src/components/landing_page/classic_version/guide_card.tsx rename to src/platform/packages/shared/kbn-guided-onboarding/src/components/landing_page/classic_version/guide_card.tsx diff --git a/packages/kbn-guided-onboarding/src/components/landing_page/classic_version/guide_cards.constants.tsx b/src/platform/packages/shared/kbn-guided-onboarding/src/components/landing_page/classic_version/guide_cards.constants.tsx similarity index 100% rename from packages/kbn-guided-onboarding/src/components/landing_page/classic_version/guide_cards.constants.tsx rename to src/platform/packages/shared/kbn-guided-onboarding/src/components/landing_page/classic_version/guide_cards.constants.tsx diff --git a/packages/kbn-guided-onboarding/src/components/landing_page/classic_version/guide_cards.test.tsx b/src/platform/packages/shared/kbn-guided-onboarding/src/components/landing_page/classic_version/guide_cards.test.tsx similarity index 100% rename from packages/kbn-guided-onboarding/src/components/landing_page/classic_version/guide_cards.test.tsx rename to src/platform/packages/shared/kbn-guided-onboarding/src/components/landing_page/classic_version/guide_cards.test.tsx diff --git a/packages/kbn-guided-onboarding/src/components/landing_page/classic_version/guide_cards.tsx b/src/platform/packages/shared/kbn-guided-onboarding/src/components/landing_page/classic_version/guide_cards.tsx similarity index 100% rename from packages/kbn-guided-onboarding/src/components/landing_page/classic_version/guide_cards.tsx rename to src/platform/packages/shared/kbn-guided-onboarding/src/components/landing_page/classic_version/guide_cards.tsx diff --git a/packages/kbn-guided-onboarding/src/components/landing_page/classic_version/guide_filters.tsx b/src/platform/packages/shared/kbn-guided-onboarding/src/components/landing_page/classic_version/guide_filters.tsx similarity index 100% rename from packages/kbn-guided-onboarding/src/components/landing_page/classic_version/guide_filters.tsx rename to src/platform/packages/shared/kbn-guided-onboarding/src/components/landing_page/classic_version/guide_filters.tsx diff --git a/packages/kbn-guided-onboarding/src/components/landing_page/guide/guide_card.tsx b/src/platform/packages/shared/kbn-guided-onboarding/src/components/landing_page/guide/guide_card.tsx similarity index 100% rename from packages/kbn-guided-onboarding/src/components/landing_page/guide/guide_card.tsx rename to src/platform/packages/shared/kbn-guided-onboarding/src/components/landing_page/guide/guide_card.tsx diff --git a/packages/kbn-guided-onboarding/src/components/landing_page/guide/guide_cards.constants.tsx b/src/platform/packages/shared/kbn-guided-onboarding/src/components/landing_page/guide/guide_cards.constants.tsx similarity index 100% rename from packages/kbn-guided-onboarding/src/components/landing_page/guide/guide_cards.constants.tsx rename to src/platform/packages/shared/kbn-guided-onboarding/src/components/landing_page/guide/guide_cards.constants.tsx diff --git a/packages/kbn-guided-onboarding/src/components/landing_page/guide/guide_cards.tsx b/src/platform/packages/shared/kbn-guided-onboarding/src/components/landing_page/guide/guide_cards.tsx similarity index 100% rename from packages/kbn-guided-onboarding/src/components/landing_page/guide/guide_cards.tsx rename to src/platform/packages/shared/kbn-guided-onboarding/src/components/landing_page/guide/guide_cards.tsx diff --git a/packages/kbn-guided-onboarding/src/components/landing_page/guide/guide_filters.tsx b/src/platform/packages/shared/kbn-guided-onboarding/src/components/landing_page/guide/guide_filters.tsx similarity index 100% rename from packages/kbn-guided-onboarding/src/components/landing_page/guide/guide_filters.tsx rename to src/platform/packages/shared/kbn-guided-onboarding/src/components/landing_page/guide/guide_filters.tsx diff --git a/packages/kbn-guided-onboarding/src/types.ts b/src/platform/packages/shared/kbn-guided-onboarding/src/types.ts similarity index 100% rename from packages/kbn-guided-onboarding/src/types.ts rename to src/platform/packages/shared/kbn-guided-onboarding/src/types.ts diff --git a/packages/kbn-guided-onboarding/tsconfig.json b/src/platform/packages/shared/kbn-guided-onboarding/tsconfig.json similarity index 87% rename from packages/kbn-guided-onboarding/tsconfig.json rename to src/platform/packages/shared/kbn-guided-onboarding/tsconfig.json index b47b2dad14429..09caf676e195f 100644 --- a/packages/kbn-guided-onboarding/tsconfig.json +++ b/src/platform/packages/shared/kbn-guided-onboarding/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../../../../tsconfig.base.json", "compilerOptions": { "outDir": "target/types", "types": [ diff --git a/packages/kbn-management/settings/section_registry/README.mdx b/src/platform/packages/shared/kbn-management/settings/section_registry/README.mdx similarity index 100% rename from packages/kbn-management/settings/section_registry/README.mdx rename to src/platform/packages/shared/kbn-management/settings/section_registry/README.mdx diff --git a/packages/kbn-management/settings/section_registry/index.ts b/src/platform/packages/shared/kbn-management/settings/section_registry/index.ts similarity index 100% rename from packages/kbn-management/settings/section_registry/index.ts rename to src/platform/packages/shared/kbn-management/settings/section_registry/index.ts diff --git a/packages/kbn-management/settings/section_registry/kibana.jsonc b/src/platform/packages/shared/kbn-management/settings/section_registry/kibana.jsonc similarity index 100% rename from packages/kbn-management/settings/section_registry/kibana.jsonc rename to src/platform/packages/shared/kbn-management/settings/section_registry/kibana.jsonc diff --git a/packages/kbn-management/settings/section_registry/package.json b/src/platform/packages/shared/kbn-management/settings/section_registry/package.json similarity index 100% rename from packages/kbn-management/settings/section_registry/package.json rename to src/platform/packages/shared/kbn-management/settings/section_registry/package.json diff --git a/packages/kbn-management/settings/section_registry/section_registry.test.tsx b/src/platform/packages/shared/kbn-management/settings/section_registry/section_registry.test.tsx similarity index 100% rename from packages/kbn-management/settings/section_registry/section_registry.test.tsx rename to src/platform/packages/shared/kbn-management/settings/section_registry/section_registry.test.tsx diff --git a/packages/kbn-management/settings/section_registry/section_registry.ts b/src/platform/packages/shared/kbn-management/settings/section_registry/section_registry.ts similarity index 100% rename from packages/kbn-management/settings/section_registry/section_registry.ts rename to src/platform/packages/shared/kbn-management/settings/section_registry/section_registry.ts diff --git a/packages/kbn-management/settings/section_registry/tsconfig.json b/src/platform/packages/shared/kbn-management/settings/section_registry/tsconfig.json similarity index 84% rename from packages/kbn-management/settings/section_registry/tsconfig.json rename to src/platform/packages/shared/kbn-management/settings/section_registry/tsconfig.json index 2cf1893be5d55..a33d8c8631b73 100644 --- a/packages/kbn-management/settings/section_registry/tsconfig.json +++ b/src/platform/packages/shared/kbn-management/settings/section_registry/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../../tsconfig.base.json", + "extends": "../../../../../../../tsconfig.base.json", "compilerOptions": { "outDir": "target/types", "types": [ diff --git a/packages/kbn-management/settings/setting_ids/README.mdx b/src/platform/packages/shared/kbn-management/settings/setting_ids/README.mdx similarity index 100% rename from packages/kbn-management/settings/setting_ids/README.mdx rename to src/platform/packages/shared/kbn-management/settings/setting_ids/README.mdx diff --git a/packages/kbn-management/settings/setting_ids/index.ts b/src/platform/packages/shared/kbn-management/settings/setting_ids/index.ts similarity index 100% rename from packages/kbn-management/settings/setting_ids/index.ts rename to src/platform/packages/shared/kbn-management/settings/setting_ids/index.ts diff --git a/packages/kbn-management/settings/setting_ids/kibana.jsonc b/src/platform/packages/shared/kbn-management/settings/setting_ids/kibana.jsonc similarity index 100% rename from packages/kbn-management/settings/setting_ids/kibana.jsonc rename to src/platform/packages/shared/kbn-management/settings/setting_ids/kibana.jsonc diff --git a/packages/kbn-management/settings/setting_ids/package.json b/src/platform/packages/shared/kbn-management/settings/setting_ids/package.json similarity index 100% rename from packages/kbn-management/settings/setting_ids/package.json rename to src/platform/packages/shared/kbn-management/settings/setting_ids/package.json diff --git a/packages/kbn-management/settings/setting_ids/tsconfig.json b/src/platform/packages/shared/kbn-management/settings/setting_ids/tsconfig.json similarity index 79% rename from packages/kbn-management/settings/setting_ids/tsconfig.json rename to src/platform/packages/shared/kbn-management/settings/setting_ids/tsconfig.json index 53e5c76cbab87..8636a00aae12f 100644 --- a/packages/kbn-management/settings/setting_ids/tsconfig.json +++ b/src/platform/packages/shared/kbn-management/settings/setting_ids/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../../tsconfig.base.json", + "extends": "../../../../../../../tsconfig.base.json", "compilerOptions": { "outDir": "target/types", "types": [ diff --git a/packages/react/kibana_context/common/BUILD.bazel b/src/platform/packages/shared/react/kibana_context/common/BUILD.bazel similarity index 100% rename from packages/react/kibana_context/common/BUILD.bazel rename to src/platform/packages/shared/react/kibana_context/common/BUILD.bazel diff --git a/packages/react/kibana_context/common/README.mdx b/src/platform/packages/shared/react/kibana_context/common/README.mdx similarity index 100% rename from packages/react/kibana_context/common/README.mdx rename to src/platform/packages/shared/react/kibana_context/common/README.mdx diff --git a/packages/react/kibana_context/common/color_mode.test.ts b/src/platform/packages/shared/react/kibana_context/common/color_mode.test.ts similarity index 100% rename from packages/react/kibana_context/common/color_mode.test.ts rename to src/platform/packages/shared/react/kibana_context/common/color_mode.test.ts diff --git a/packages/react/kibana_context/common/color_mode.ts b/src/platform/packages/shared/react/kibana_context/common/color_mode.ts similarity index 100% rename from packages/react/kibana_context/common/color_mode.ts rename to src/platform/packages/shared/react/kibana_context/common/color_mode.ts diff --git a/packages/react/kibana_context/common/index.ts b/src/platform/packages/shared/react/kibana_context/common/index.ts similarity index 100% rename from packages/react/kibana_context/common/index.ts rename to src/platform/packages/shared/react/kibana_context/common/index.ts diff --git a/packages/kbn-saved-objects-settings/jest.config.js b/src/platform/packages/shared/react/kibana_context/common/jest.config.js similarity index 81% rename from packages/kbn-saved-objects-settings/jest.config.js rename to src/platform/packages/shared/react/kibana_context/common/jest.config.js index c2d39b4e3cdbd..8954155118131 100644 --- a/packages/kbn-saved-objects-settings/jest.config.js +++ b/src/platform/packages/shared/react/kibana_context/common/jest.config.js @@ -9,6 +9,6 @@ module.exports = { preset: '@kbn/test/jest_node', - rootDir: '../..', - roots: ['/packages/kbn-saved-objects-settings'], + rootDir: '../../../../../../..', + roots: ['/src/platform/packages/shared/react/kibana_context/common'], }; diff --git a/packages/react/kibana_context/common/kibana.jsonc b/src/platform/packages/shared/react/kibana_context/common/kibana.jsonc similarity index 100% rename from packages/react/kibana_context/common/kibana.jsonc rename to src/platform/packages/shared/react/kibana_context/common/kibana.jsonc diff --git a/packages/react/kibana_context/common/package.json b/src/platform/packages/shared/react/kibana_context/common/package.json similarity index 100% rename from packages/react/kibana_context/common/package.json rename to src/platform/packages/shared/react/kibana_context/common/package.json diff --git a/packages/react/kibana_context/common/theme.ts b/src/platform/packages/shared/react/kibana_context/common/theme.ts similarity index 100% rename from packages/react/kibana_context/common/theme.ts rename to src/platform/packages/shared/react/kibana_context/common/theme.ts diff --git a/packages/react/kibana_context/common/tsconfig.json b/src/platform/packages/shared/react/kibana_context/common/tsconfig.json similarity index 78% rename from packages/react/kibana_context/common/tsconfig.json rename to src/platform/packages/shared/react/kibana_context/common/tsconfig.json index 994137ade7df2..2d893153b8fdd 100644 --- a/packages/react/kibana_context/common/tsconfig.json +++ b/src/platform/packages/shared/react/kibana_context/common/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../../tsconfig.base.json", + "extends": "../../../../../../../tsconfig.base.json", "compilerOptions": { "outDir": "target/types", "types": [ diff --git a/packages/react/kibana_context/common/types.ts b/src/platform/packages/shared/react/kibana_context/common/types.ts similarity index 100% rename from packages/react/kibana_context/common/types.ts rename to src/platform/packages/shared/react/kibana_context/common/types.ts diff --git a/packages/react/kibana_context/render/BUILD.bazel b/src/platform/packages/shared/react/kibana_context/render/BUILD.bazel similarity index 83% rename from packages/react/kibana_context/render/BUILD.bazel rename to src/platform/packages/shared/react/kibana_context/render/BUILD.bazel index f1e3f4894d144..f4c0c575e27d8 100644 --- a/packages/react/kibana_context/render/BUILD.bazel +++ b/src/platform/packages/shared/react/kibana_context/render/BUILD.bazel @@ -23,8 +23,8 @@ SRCS = glob( DEPS = [ "@npm//react", - "//packages/react/kibana_context/common", - "//packages/react/kibana_context/root", + "//src/platform/packages/shared/react/kibana_context/common", + "//src/platform/packages/shared/react/kibana_context/root", ] js_library( diff --git a/packages/react/kibana_context/render/README.mdx b/src/platform/packages/shared/react/kibana_context/render/README.mdx similarity index 100% rename from packages/react/kibana_context/render/README.mdx rename to src/platform/packages/shared/react/kibana_context/render/README.mdx diff --git a/packages/react/kibana_context/render/index.ts b/src/platform/packages/shared/react/kibana_context/render/index.ts similarity index 100% rename from packages/react/kibana_context/render/index.ts rename to src/platform/packages/shared/react/kibana_context/render/index.ts diff --git a/src/platform/packages/shared/react/kibana_context/render/jest.config.js b/src/platform/packages/shared/react/kibana_context/render/jest.config.js new file mode 100644 index 0000000000000..161ae1d54dda7 --- /dev/null +++ b/src/platform/packages/shared/react/kibana_context/render/jest.config.js @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../../../../../..', + roots: ['/src/platform/packages/shared/react/kibana_context/render'], +}; diff --git a/packages/react/kibana_context/render/kibana.jsonc b/src/platform/packages/shared/react/kibana_context/render/kibana.jsonc similarity index 100% rename from packages/react/kibana_context/render/kibana.jsonc rename to src/platform/packages/shared/react/kibana_context/render/kibana.jsonc diff --git a/packages/react/kibana_context/render/package.json b/src/platform/packages/shared/react/kibana_context/render/package.json similarity index 100% rename from packages/react/kibana_context/render/package.json rename to src/platform/packages/shared/react/kibana_context/render/package.json diff --git a/packages/react/kibana_context/render/render_provider.tsx b/src/platform/packages/shared/react/kibana_context/render/render_provider.tsx similarity index 100% rename from packages/react/kibana_context/render/render_provider.tsx rename to src/platform/packages/shared/react/kibana_context/render/render_provider.tsx diff --git a/packages/react/kibana_context/render/tsconfig.json b/src/platform/packages/shared/react/kibana_context/render/tsconfig.json similarity index 84% rename from packages/react/kibana_context/render/tsconfig.json rename to src/platform/packages/shared/react/kibana_context/render/tsconfig.json index 479f1a0ea50b1..65684da360349 100644 --- a/packages/react/kibana_context/render/tsconfig.json +++ b/src/platform/packages/shared/react/kibana_context/render/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../../tsconfig.base.json", + "extends": "../../../../../../../tsconfig.base.json", "compilerOptions": { "outDir": "target/types", "types": [ diff --git a/packages/react/kibana_context/root/BUILD.bazel b/src/platform/packages/shared/react/kibana_context/root/BUILD.bazel similarity index 100% rename from packages/react/kibana_context/root/BUILD.bazel rename to src/platform/packages/shared/react/kibana_context/root/BUILD.bazel diff --git a/packages/react/kibana_context/root/README.mdx b/src/platform/packages/shared/react/kibana_context/root/README.mdx similarity index 100% rename from packages/react/kibana_context/root/README.mdx rename to src/platform/packages/shared/react/kibana_context/root/README.mdx diff --git a/packages/react/kibana_context/root/eui_provider.test.tsx b/src/platform/packages/shared/react/kibana_context/root/eui_provider.test.tsx similarity index 100% rename from packages/react/kibana_context/root/eui_provider.test.tsx rename to src/platform/packages/shared/react/kibana_context/root/eui_provider.test.tsx diff --git a/packages/react/kibana_context/root/eui_provider.tsx b/src/platform/packages/shared/react/kibana_context/root/eui_provider.tsx similarity index 100% rename from packages/react/kibana_context/root/eui_provider.tsx rename to src/platform/packages/shared/react/kibana_context/root/eui_provider.tsx diff --git a/packages/react/kibana_context/root/index.ts b/src/platform/packages/shared/react/kibana_context/root/index.ts similarity index 100% rename from packages/react/kibana_context/root/index.ts rename to src/platform/packages/shared/react/kibana_context/root/index.ts diff --git a/src/platform/packages/shared/react/kibana_context/root/jest.config.js b/src/platform/packages/shared/react/kibana_context/root/jest.config.js new file mode 100644 index 0000000000000..519dad66314cb --- /dev/null +++ b/src/platform/packages/shared/react/kibana_context/root/jest.config.js @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../../../../../..', + roots: ['/src/platform/packages/shared/react/kibana_context/root'], +}; diff --git a/packages/react/kibana_context/root/kibana.jsonc b/src/platform/packages/shared/react/kibana_context/root/kibana.jsonc similarity index 84% rename from packages/react/kibana_context/root/kibana.jsonc rename to src/platform/packages/shared/react/kibana_context/root/kibana.jsonc index 1dc0779f86f0d..0df961f8135a4 100644 --- a/packages/react/kibana_context/root/kibana.jsonc +++ b/src/platform/packages/shared/react/kibana_context/root/kibana.jsonc @@ -5,5 +5,5 @@ "@elastic/appex-sharedux" ], "group": "platform", - "visibility": "private" -} \ No newline at end of file + "visibility": "shared" +} diff --git a/packages/react/kibana_context/root/package.json b/src/platform/packages/shared/react/kibana_context/root/package.json similarity index 100% rename from packages/react/kibana_context/root/package.json rename to src/platform/packages/shared/react/kibana_context/root/package.json diff --git a/packages/react/kibana_context/root/root_provider.test.tsx b/src/platform/packages/shared/react/kibana_context/root/root_provider.test.tsx similarity index 100% rename from packages/react/kibana_context/root/root_provider.test.tsx rename to src/platform/packages/shared/react/kibana_context/root/root_provider.test.tsx diff --git a/packages/react/kibana_context/root/root_provider.tsx b/src/platform/packages/shared/react/kibana_context/root/root_provider.tsx similarity index 100% rename from packages/react/kibana_context/root/root_provider.tsx rename to src/platform/packages/shared/react/kibana_context/root/root_provider.tsx diff --git a/packages/react/kibana_context/root/tsconfig.json b/src/platform/packages/shared/react/kibana_context/root/tsconfig.json similarity index 91% rename from packages/react/kibana_context/root/tsconfig.json rename to src/platform/packages/shared/react/kibana_context/root/tsconfig.json index 8035f8379e390..b934a5be91352 100644 --- a/packages/react/kibana_context/root/tsconfig.json +++ b/src/platform/packages/shared/react/kibana_context/root/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../../tsconfig.base.json", + "extends": "../../../../../../../tsconfig.base.json", "compilerOptions": { "outDir": "target/types", "types": [ diff --git a/packages/react/kibana_context/styled/README.mdx b/src/platform/packages/shared/react/kibana_context/styled/README.mdx similarity index 100% rename from packages/react/kibana_context/styled/README.mdx rename to src/platform/packages/shared/react/kibana_context/styled/README.mdx diff --git a/packages/react/kibana_context/styled/index.ts b/src/platform/packages/shared/react/kibana_context/styled/index.ts similarity index 100% rename from packages/react/kibana_context/styled/index.ts rename to src/platform/packages/shared/react/kibana_context/styled/index.ts diff --git a/src/platform/packages/shared/react/kibana_context/styled/jest.config.js b/src/platform/packages/shared/react/kibana_context/styled/jest.config.js new file mode 100644 index 0000000000000..4939ca31ed107 --- /dev/null +++ b/src/platform/packages/shared/react/kibana_context/styled/jest.config.js @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../../../../../..', + roots: ['/src/platform/packages/shared/react/kibana_context/styled'], +}; diff --git a/packages/react/kibana_context/styled/kibana.jsonc b/src/platform/packages/shared/react/kibana_context/styled/kibana.jsonc similarity index 100% rename from packages/react/kibana_context/styled/kibana.jsonc rename to src/platform/packages/shared/react/kibana_context/styled/kibana.jsonc diff --git a/packages/react/kibana_context/styled/package.json b/src/platform/packages/shared/react/kibana_context/styled/package.json similarity index 100% rename from packages/react/kibana_context/styled/package.json rename to src/platform/packages/shared/react/kibana_context/styled/package.json diff --git a/packages/react/kibana_context/styled/styled_provider.tsx b/src/platform/packages/shared/react/kibana_context/styled/styled_provider.tsx similarity index 100% rename from packages/react/kibana_context/styled/styled_provider.tsx rename to src/platform/packages/shared/react/kibana_context/styled/styled_provider.tsx diff --git a/packages/react/kibana_context/styled/tsconfig.json b/src/platform/packages/shared/react/kibana_context/styled/tsconfig.json similarity index 82% rename from packages/react/kibana_context/styled/tsconfig.json rename to src/platform/packages/shared/react/kibana_context/styled/tsconfig.json index fa8b369cdf3cb..6f3c23804f09d 100644 --- a/packages/react/kibana_context/styled/tsconfig.json +++ b/src/platform/packages/shared/react/kibana_context/styled/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../../tsconfig.base.json", + "extends": "../../../../../../../tsconfig.base.json", "compilerOptions": { "outDir": "target/types", "types": [ diff --git a/packages/react/kibana_context/theme/BUILD.bazel b/src/platform/packages/shared/react/kibana_context/theme/BUILD.bazel similarity index 84% rename from packages/react/kibana_context/theme/BUILD.bazel rename to src/platform/packages/shared/react/kibana_context/theme/BUILD.bazel index 504477ad7a0ed..34d7dcc924f32 100644 --- a/packages/react/kibana_context/theme/BUILD.bazel +++ b/src/platform/packages/shared/react/kibana_context/theme/BUILD.bazel @@ -25,8 +25,8 @@ BUNDLER_DEPS = [ "@npm//react", "@npm//tslib", "@npm//react-use", - "//packages/react/kibana_context/common", - "//packages/react/kibana_context/root", + "//src/platform/packages/shared/react/kibana_context/common", + "//src/platform/packages/shared/react/kibana_context/root", ] js_library( diff --git a/packages/react/kibana_context/theme/README.mdx b/src/platform/packages/shared/react/kibana_context/theme/README.mdx similarity index 100% rename from packages/react/kibana_context/theme/README.mdx rename to src/platform/packages/shared/react/kibana_context/theme/README.mdx diff --git a/packages/react/kibana_context/theme/index.ts b/src/platform/packages/shared/react/kibana_context/theme/index.ts similarity index 100% rename from packages/react/kibana_context/theme/index.ts rename to src/platform/packages/shared/react/kibana_context/theme/index.ts diff --git a/src/platform/packages/shared/react/kibana_context/theme/jest.config.js b/src/platform/packages/shared/react/kibana_context/theme/jest.config.js new file mode 100644 index 0000000000000..1963334905278 --- /dev/null +++ b/src/platform/packages/shared/react/kibana_context/theme/jest.config.js @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../../../../../..', + roots: ['/src/platform/packages/shared/react/kibana_context/theme'], +}; diff --git a/packages/react/kibana_context/theme/kibana.jsonc b/src/platform/packages/shared/react/kibana_context/theme/kibana.jsonc similarity index 100% rename from packages/react/kibana_context/theme/kibana.jsonc rename to src/platform/packages/shared/react/kibana_context/theme/kibana.jsonc diff --git a/packages/react/kibana_context/theme/package.json b/src/platform/packages/shared/react/kibana_context/theme/package.json similarity index 100% rename from packages/react/kibana_context/theme/package.json rename to src/platform/packages/shared/react/kibana_context/theme/package.json diff --git a/packages/react/kibana_context/theme/theme_provider.test.tsx b/src/platform/packages/shared/react/kibana_context/theme/theme_provider.test.tsx similarity index 100% rename from packages/react/kibana_context/theme/theme_provider.test.tsx rename to src/platform/packages/shared/react/kibana_context/theme/theme_provider.test.tsx diff --git a/packages/react/kibana_context/theme/theme_provider.tsx b/src/platform/packages/shared/react/kibana_context/theme/theme_provider.tsx similarity index 100% rename from packages/react/kibana_context/theme/theme_provider.tsx rename to src/platform/packages/shared/react/kibana_context/theme/theme_provider.tsx diff --git a/packages/react/kibana_context/theme/tsconfig.json b/src/platform/packages/shared/react/kibana_context/theme/tsconfig.json similarity index 88% rename from packages/react/kibana_context/theme/tsconfig.json rename to src/platform/packages/shared/react/kibana_context/theme/tsconfig.json index cfc672666e4c0..720e386873d7b 100644 --- a/packages/react/kibana_context/theme/tsconfig.json +++ b/src/platform/packages/shared/react/kibana_context/theme/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../../tsconfig.base.json", + "extends": "../../../../../../../tsconfig.base.json", "compilerOptions": { "outDir": "target/types", "types": [ diff --git a/packages/react/kibana_context/theme/with_theme.tsx b/src/platform/packages/shared/react/kibana_context/theme/with_theme.tsx similarity index 100% rename from packages/react/kibana_context/theme/with_theme.tsx rename to src/platform/packages/shared/react/kibana_context/theme/with_theme.tsx diff --git a/packages/react/kibana_mount/README.md b/src/platform/packages/shared/react/kibana_mount/README.md similarity index 100% rename from packages/react/kibana_mount/README.md rename to src/platform/packages/shared/react/kibana_mount/README.md diff --git a/packages/react/kibana_mount/index.ts b/src/platform/packages/shared/react/kibana_mount/index.ts similarity index 100% rename from packages/react/kibana_mount/index.ts rename to src/platform/packages/shared/react/kibana_mount/index.ts diff --git a/packages/deeplinks/shared/jest.config.js b/src/platform/packages/shared/react/kibana_mount/jest.config.js similarity index 83% rename from packages/deeplinks/shared/jest.config.js rename to src/platform/packages/shared/react/kibana_mount/jest.config.js index 5849db1143d72..c855005ddf41e 100644 --- a/packages/deeplinks/shared/jest.config.js +++ b/src/platform/packages/shared/react/kibana_mount/jest.config.js @@ -9,6 +9,6 @@ module.exports = { preset: '@kbn/test', - rootDir: '../../..', - roots: ['/packages/deeplinks/shared'], + rootDir: '../../../../../..', + roots: ['/src/platform/packages/shared/react/kibana_mount'], }; diff --git a/packages/react/kibana_mount/kibana.jsonc b/src/platform/packages/shared/react/kibana_mount/kibana.jsonc similarity index 100% rename from packages/react/kibana_mount/kibana.jsonc rename to src/platform/packages/shared/react/kibana_mount/kibana.jsonc diff --git a/packages/react/kibana_mount/mount_point_portal.test.tsx b/src/platform/packages/shared/react/kibana_mount/mount_point_portal.test.tsx similarity index 100% rename from packages/react/kibana_mount/mount_point_portal.test.tsx rename to src/platform/packages/shared/react/kibana_mount/mount_point_portal.test.tsx diff --git a/packages/react/kibana_mount/mount_point_portal.tsx b/src/platform/packages/shared/react/kibana_mount/mount_point_portal.tsx similarity index 100% rename from packages/react/kibana_mount/mount_point_portal.tsx rename to src/platform/packages/shared/react/kibana_mount/mount_point_portal.tsx diff --git a/packages/react/kibana_mount/package.json b/src/platform/packages/shared/react/kibana_mount/package.json similarity index 100% rename from packages/react/kibana_mount/package.json rename to src/platform/packages/shared/react/kibana_mount/package.json diff --git a/packages/react/kibana_mount/test_helpers/react_mount_serializer.ts b/src/platform/packages/shared/react/kibana_mount/test_helpers/react_mount_serializer.ts similarity index 100% rename from packages/react/kibana_mount/test_helpers/react_mount_serializer.ts rename to src/platform/packages/shared/react/kibana_mount/test_helpers/react_mount_serializer.ts diff --git a/packages/react/kibana_mount/to_mount_point.test.tsx b/src/platform/packages/shared/react/kibana_mount/to_mount_point.test.tsx similarity index 100% rename from packages/react/kibana_mount/to_mount_point.test.tsx rename to src/platform/packages/shared/react/kibana_mount/to_mount_point.test.tsx diff --git a/packages/react/kibana_mount/to_mount_point.tsx b/src/platform/packages/shared/react/kibana_mount/to_mount_point.tsx similarity index 100% rename from packages/react/kibana_mount/to_mount_point.tsx rename to src/platform/packages/shared/react/kibana_mount/to_mount_point.tsx diff --git a/packages/react/kibana_mount/tsconfig.json b/src/platform/packages/shared/react/kibana_mount/tsconfig.json similarity index 90% rename from packages/react/kibana_mount/tsconfig.json rename to src/platform/packages/shared/react/kibana_mount/tsconfig.json index 15dc55d36b052..6f882c96aa274 100644 --- a/packages/react/kibana_mount/tsconfig.json +++ b/src/platform/packages/shared/react/kibana_mount/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.base.json", + "extends": "../../../../../../tsconfig.base.json", "compilerOptions": { "outDir": "target/types", "types": [ diff --git a/packages/react/kibana_mount/utils.ts b/src/platform/packages/shared/react/kibana_mount/utils.ts similarity index 100% rename from packages/react/kibana_mount/utils.ts rename to src/platform/packages/shared/react/kibana_mount/utils.ts diff --git a/packages/serverless/settings/observability_project/README.mdx b/src/platform/packages/shared/serverless/settings/observability_project/README.mdx similarity index 100% rename from packages/serverless/settings/observability_project/README.mdx rename to src/platform/packages/shared/serverless/settings/observability_project/README.mdx diff --git a/packages/serverless/settings/observability_project/index.ts b/src/platform/packages/shared/serverless/settings/observability_project/index.ts similarity index 100% rename from packages/serverless/settings/observability_project/index.ts rename to src/platform/packages/shared/serverless/settings/observability_project/index.ts diff --git a/packages/serverless/settings/observability_project/kibana.jsonc b/src/platform/packages/shared/serverless/settings/observability_project/kibana.jsonc similarity index 100% rename from packages/serverless/settings/observability_project/kibana.jsonc rename to src/platform/packages/shared/serverless/settings/observability_project/kibana.jsonc diff --git a/packages/serverless/settings/observability_project/package.json b/src/platform/packages/shared/serverless/settings/observability_project/package.json similarity index 100% rename from packages/serverless/settings/observability_project/package.json rename to src/platform/packages/shared/serverless/settings/observability_project/package.json diff --git a/packages/serverless/settings/observability_project/tsconfig.json b/src/platform/packages/shared/serverless/settings/observability_project/tsconfig.json similarity index 81% rename from packages/serverless/settings/observability_project/tsconfig.json rename to src/platform/packages/shared/serverless/settings/observability_project/tsconfig.json index 16d6022e3d9bc..16a9ba9ba4b5d 100644 --- a/packages/serverless/settings/observability_project/tsconfig.json +++ b/src/platform/packages/shared/serverless/settings/observability_project/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../../tsconfig.base.json", + "extends": "../../../../../../../tsconfig.base.json", "compilerOptions": { "outDir": "target/types", "types": [ diff --git a/src/platform/plugins/shared/guided_onboarding/README.md b/src/platform/plugins/shared/guided_onboarding/README.md index e9d8ac5215b60..1d0f87b32ff8f 100755 --- a/src/platform/plugins/shared/guided_onboarding/README.md +++ b/src/platform/plugins/shared/guided_onboarding/README.md @@ -29,7 +29,7 @@ For example, when a user goes through the SIEM guide they are first taken to the There is also a server side in the guided onboarding plugin that creates several endpoints for plugin only internal use. The endpoints are for fetching the guide configs, the state of the guided onboarding and to update the state. The server side also exposes a function ([link](https://github.com/elastic/kibana/blob/main/src/platform/plugins/shared/guided_onboarding/server/plugin.ts#L40)) that is used by consumers to register their guide configs. That way the config files are a part of the consumers code and the guided onboarding is only used as a framework. -Another part of the guided onboarding code is in the home plugin where the code for the landing page ([link](https://github.com/elastic/kibana/tree/main/src/platform/plugins/shared/home/public/application/components/guided_onboarding)) is situated. The landing page can be found under `/app/home#/getting_started` and there is some logic ([link](https://github.com/elastic/kibana/blob/main/src/platform/plugins/shared/home/public/application/components/home.tsx#L200)) that redirects the user to the landing page when the deployment is new (i.e. there is no data in the deployment). Some of the static components for the landing page were extracted to the `kbn-guided-onboarding` package ([link](https://github.com/elastic/kibana/tree/main/packages/kbn-guided-onboarding)). +Another part of the guided onboarding code is in the home plugin where the code for the landing page ([link](https://github.com/elastic/kibana/tree/main/src/platform/plugins/shared/home/public/application/components/guided_onboarding)) is situated. The landing page can be found under `/app/home#/getting_started` and there is some logic ([link](https://github.com/elastic/kibana/blob/main/src/platform/plugins/shared/home/public/application/components/home.tsx#L200)) that redirects the user to the landing page when the deployment is new (i.e. there is no data in the deployment). Some of the static components for the landing page were extracted to the `kbn-guided-onboarding` package ([link](https://github.com/elastic/kibana/tree/main/src/platform/packages/shared/kbn-guided-onboarding)). When starting Kibana with `yarn start --run-examples` the `guided_onboarding_example` plugin ([link](https://github.com/elastic/kibana/tree/main/examples/guided_onboarding_example)) can be found under `/app/guidedOnboardingExample`. This page displays the current state of the guided onboarding and allows setting the state to any point in the guide. Otherwise, it can be difficult and time consuming to reach a specific step in a production guide during dev work. The example plugin also registers a config for a test guide that can be completed on the pages of the example plugin. The test guide is also used for unit and functional tests of the guided onboarding plugin. @@ -133,8 +133,8 @@ The guided onboarding exposes a function `registerGuideConfig(guideId: GuideId, Follow these simple steps to add a new guide to the guided onboarding framework. For more detailed information about framework functionality and architecture and about API services exposed by the plugin, please read the full readme. 1. Declare the `guidedOnboarding` plugin as an optional dependency in your plugin's `kibana.json` file. Add the guided onboarding plugin's client-side start contract to your plugin's client-side start dependencies and the guided onboarding plugin's server-side setup contract to your plugin's server-side dependencies. -2. Define the configuration for your guide. At a high level, this includes a title, description, and list of steps. See this [example config](https://github.com/elastic/kibana/blob/main/packages/kbn-guided-onboarding/src/common/test_guide_config.ts) or consult the `GuideConfig` interface. +2. Define the configuration for your guide. At a high level, this includes a title, description, and list of steps. See this [example config](https://github.com/elastic/kibana/blob/main/src/platform/packages/shared/kbn-guided-onboarding/src/common/test_guide_config.ts) or consult the `GuideConfig` interface. 3. Register your guide during your plugin's server-side setup by calling a function exposed by the guided onboarding plugin: `registerGuideConfig(guideId: GuideId, guideConfig: GuideConfig)`. For an example, see this [example plugin](https://github.com/elastic/kibana/blob/main/examples/guided_onboarding_example/server/plugin.ts). -4. Update the cards on the landing page to include your guide in the use case selection. Make sure that the card doesn't have the property `navigateTo` because that is only used for cards that redirect to Kibana pages and don't start a guide. Also add the same value to the property `guideId` as used in the guide config. Landing page cards are configured in this [kbn-guided-onboarding package](https://github.com/elastic/kibana/blob/main/packages/kbn-guided-onboarding/src/components/landing_page/guide_cards.constants.tsx). +4. Update the cards on the landing page to include your guide in the use case selection. Make sure that the card doesn't have the property `navigateTo` because that is only used for cards that redirect to Kibana pages and don't start a guide. Also add the same value to the property `guideId` as used in the guide config. Landing page cards are configured in this [kbn-guided-onboarding package](https://github.com/elastic/kibana/blob/main/src/platform/packages/shared/kbn-guided-onboarding/src/components/landing_page/guide_cards.constants.tsx). 5. Integrate the new guide into your Kibana pages by using the guided onboarding client-side API service. Make sure your Kibana pages correctly display UI elements depending on the active guide step and the UI flow is straight forward to complete the guide. See existing guides for an example and read more about the API service in this file, the section "Client-side: API service". 6. Optionally, update the example plugin's [form](https://github.com/elastic/kibana/blob/main/examples/guided_onboarding_example/public/components/main.tsx#L38) to be able to start your guide from that page and activate any step in your guide (useful to test your guide steps). diff --git a/src/platform/plugins/shared/home/.storybook/main.ts b/src/platform/plugins/shared/home/.storybook/main.ts index dcf2d844567bd..5a6718ce1ab4a 100644 --- a/src/platform/plugins/shared/home/.storybook/main.ts +++ b/src/platform/plugins/shared/home/.storybook/main.ts @@ -12,7 +12,7 @@ import { defaultConfig } from '@kbn/storybook'; module.exports = { ...defaultConfig, stories: [ - '../../../../../../packages/home/**/*.stories.+(tsx|mdx)', + '../../../../../../src/platform/packages/shared/home/**/*.stories.+(tsx|mdx)', '../**/*.stories.+(tsx|mdx)', ], reactOptions: { diff --git a/tsconfig.base.json b/tsconfig.base.json index 75784f7dad582..cc0418edae2f0 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -282,8 +282,8 @@ "@kbn/core-capabilities-server-internal/*": ["src/core/packages/capabilities/server-internal/*"], "@kbn/core-capabilities-server-mocks": ["packages/core/capabilities/core-capabilities-server-mocks"], "@kbn/core-capabilities-server-mocks/*": ["packages/core/capabilities/core-capabilities-server-mocks/*"], - "@kbn/core-chrome-browser": ["packages/core/chrome/core-chrome-browser"], - "@kbn/core-chrome-browser/*": ["packages/core/chrome/core-chrome-browser/*"], + "@kbn/core-chrome-browser": ["src/core/packages/chrome/browser"], + "@kbn/core-chrome-browser/*": ["src/core/packages/chrome/browser/*"], "@kbn/core-chrome-browser-internal": ["src/core/packages/chrome/browser-internal"], "@kbn/core-chrome-browser-internal/*": ["src/core/packages/chrome/browser-internal/*"], "@kbn/core-chrome-browser-mocks": ["packages/core/chrome/core-chrome-browser-mocks"], @@ -744,8 +744,8 @@ "@kbn/deeplinks-search/*": ["src/platform/packages/shared/deeplinks/search/*"], "@kbn/deeplinks-security": ["src/platform/packages/shared/deeplinks/security"], "@kbn/deeplinks-security/*": ["src/platform/packages/shared/deeplinks/security/*"], - "@kbn/deeplinks-shared": ["packages/deeplinks/shared"], - "@kbn/deeplinks-shared/*": ["packages/deeplinks/shared/*"], + "@kbn/deeplinks-shared": ["src/platform/packages/shared/deeplinks/shared"], + "@kbn/deeplinks-shared/*": ["src/platform/packages/shared/deeplinks/shared/*"], "@kbn/default-nav-analytics": ["src/platform/packages/private/default-nav/analytics"], "@kbn/default-nav-analytics/*": ["src/platform/packages/private/default-nav/analytics/*"], "@kbn/default-nav-devtools": ["src/platform/packages/private/default-nav/devtools"], @@ -1008,8 +1008,8 @@ "@kbn/grokdebugger-plugin/*": ["x-pack/platform/plugins/private/grokdebugger/*"], "@kbn/grouping": ["src/platform/packages/shared/kbn-grouping"], "@kbn/grouping/*": ["src/platform/packages/shared/kbn-grouping/*"], - "@kbn/guided-onboarding": ["packages/kbn-guided-onboarding"], - "@kbn/guided-onboarding/*": ["packages/kbn-guided-onboarding/*"], + "@kbn/guided-onboarding": ["src/platform/packages/shared/kbn-guided-onboarding"], + "@kbn/guided-onboarding/*": ["src/platform/packages/shared/kbn-guided-onboarding/*"], "@kbn/guided-onboarding-example-plugin": ["examples/guided_onboarding_example"], "@kbn/guided-onboarding-example-plugin/*": ["examples/guided_onboarding_example/*"], "@kbn/guided-onboarding-plugin": ["src/platform/plugins/shared/guided_onboarding"], @@ -1026,12 +1026,12 @@ "@kbn/hello-world-plugin/*": ["examples/hello_world/*"], "@kbn/home-plugin": ["src/platform/plugins/shared/home"], "@kbn/home-plugin/*": ["src/platform/plugins/shared/home/*"], - "@kbn/home-sample-data-card": ["packages/home/sample_data_card"], - "@kbn/home-sample-data-card/*": ["packages/home/sample_data_card/*"], - "@kbn/home-sample-data-tab": ["packages/home/sample_data_tab"], - "@kbn/home-sample-data-tab/*": ["packages/home/sample_data_tab/*"], - "@kbn/home-sample-data-types": ["packages/home/sample_data_types"], - "@kbn/home-sample-data-types/*": ["packages/home/sample_data_types/*"], + "@kbn/home-sample-data-card": ["src/platform/packages/shared/home/sample_data_card"], + "@kbn/home-sample-data-card/*": ["src/platform/packages/shared/home/sample_data_card/*"], + "@kbn/home-sample-data-tab": ["src/platform/packages/shared/home/sample_data_tab"], + "@kbn/home-sample-data-tab/*": ["src/platform/packages/shared/home/sample_data_tab/*"], + "@kbn/home-sample-data-types": ["src/platform/packages/shared/home/sample_data_types"], + "@kbn/home-sample-data-types/*": ["src/platform/packages/shared/home/sample_data_types/*"], "@kbn/i18n": ["src/platform/packages/shared/kbn-i18n"], "@kbn/i18n/*": ["src/platform/packages/shared/kbn-i18n/*"], "@kbn/i18n-react": ["src/platform/packages/shared/kbn-i18n-react"], @@ -1096,8 +1096,8 @@ "@kbn/io-ts-utils/*": ["src/platform/packages/shared/kbn-io-ts-utils/*"], "@kbn/ipynb": ["x-pack/solutions/search/packages/kbn-ipynb"], "@kbn/ipynb/*": ["x-pack/solutions/search/packages/kbn-ipynb/*"], - "@kbn/item-buffer": ["packages/kbn-item-buffer"], - "@kbn/item-buffer/*": ["packages/kbn-item-buffer/*"], + "@kbn/item-buffer": ["src/platform/packages/private/kbn-item-buffer"], + "@kbn/item-buffer/*": ["src/platform/packages/private/kbn-item-buffer/*"], "@kbn/jest-serializers": ["packages/kbn-jest-serializers"], "@kbn/jest-serializers/*": ["packages/kbn-jest-serializers/*"], "@kbn/journeys": ["packages/kbn-journeys"], @@ -1198,10 +1198,10 @@ "@kbn/management-settings-components-form/*": ["src/platform/packages/private/kbn-management/settings/components/form/*"], "@kbn/management-settings-field-definition": ["src/platform/packages/shared/kbn-management/settings/field_definition"], "@kbn/management-settings-field-definition/*": ["src/platform/packages/shared/kbn-management/settings/field_definition/*"], - "@kbn/management-settings-ids": ["packages/kbn-management/settings/setting_ids"], - "@kbn/management-settings-ids/*": ["packages/kbn-management/settings/setting_ids/*"], - "@kbn/management-settings-section-registry": ["packages/kbn-management/settings/section_registry"], - "@kbn/management-settings-section-registry/*": ["packages/kbn-management/settings/section_registry/*"], + "@kbn/management-settings-ids": ["src/platform/packages/shared/kbn-management/settings/setting_ids"], + "@kbn/management-settings-ids/*": ["src/platform/packages/shared/kbn-management/settings/setting_ids/*"], + "@kbn/management-settings-section-registry": ["src/platform/packages/shared/kbn-management/settings/section_registry"], + "@kbn/management-settings-section-registry/*": ["src/platform/packages/shared/kbn-management/settings/section_registry/*"], "@kbn/management-settings-types": ["src/platform/packages/shared/kbn-management/settings/types"], "@kbn/management-settings-types/*": ["src/platform/packages/shared/kbn-management/settings/types/*"], "@kbn/management-settings-utilities": ["src/platform/packages/shared/kbn-management/settings/utilities"], @@ -1422,20 +1422,20 @@ "@kbn/react-field/*": ["src/platform/packages/shared/kbn-react-field/*"], "@kbn/react-hooks": ["src/platform/packages/shared/kbn-react-hooks"], "@kbn/react-hooks/*": ["src/platform/packages/shared/kbn-react-hooks/*"], - "@kbn/react-kibana-context-common": ["packages/react/kibana_context/common"], - "@kbn/react-kibana-context-common/*": ["packages/react/kibana_context/common/*"], - "@kbn/react-kibana-context-render": ["packages/react/kibana_context/render"], - "@kbn/react-kibana-context-render/*": ["packages/react/kibana_context/render/*"], - "@kbn/react-kibana-context-root": ["packages/react/kibana_context/root"], - "@kbn/react-kibana-context-root/*": ["packages/react/kibana_context/root/*"], - "@kbn/react-kibana-context-styled": ["packages/react/kibana_context/styled"], - "@kbn/react-kibana-context-styled/*": ["packages/react/kibana_context/styled/*"], - "@kbn/react-kibana-context-theme": ["packages/react/kibana_context/theme"], - "@kbn/react-kibana-context-theme/*": ["packages/react/kibana_context/theme/*"], - "@kbn/react-kibana-mount": ["packages/react/kibana_mount"], - "@kbn/react-kibana-mount/*": ["packages/react/kibana_mount/*"], - "@kbn/react-mute-legacy-root-warning": ["packages/kbn-react-mute-legacy-root-warning"], - "@kbn/react-mute-legacy-root-warning/*": ["packages/kbn-react-mute-legacy-root-warning/*"], + "@kbn/react-kibana-context-common": ["src/platform/packages/shared/react/kibana_context/common"], + "@kbn/react-kibana-context-common/*": ["src/platform/packages/shared/react/kibana_context/common/*"], + "@kbn/react-kibana-context-render": ["src/platform/packages/shared/react/kibana_context/render"], + "@kbn/react-kibana-context-render/*": ["src/platform/packages/shared/react/kibana_context/render/*"], + "@kbn/react-kibana-context-root": ["src/platform/packages/shared/react/kibana_context/root"], + "@kbn/react-kibana-context-root/*": ["src/platform/packages/shared/react/kibana_context/root/*"], + "@kbn/react-kibana-context-styled": ["src/platform/packages/shared/react/kibana_context/styled"], + "@kbn/react-kibana-context-styled/*": ["src/platform/packages/shared/react/kibana_context/styled/*"], + "@kbn/react-kibana-context-theme": ["src/platform/packages/shared/react/kibana_context/theme"], + "@kbn/react-kibana-context-theme/*": ["src/platform/packages/shared/react/kibana_context/theme/*"], + "@kbn/react-kibana-mount": ["src/platform/packages/shared/react/kibana_mount"], + "@kbn/react-kibana-mount/*": ["src/platform/packages/shared/react/kibana_mount/*"], + "@kbn/react-mute-legacy-root-warning": ["src/platform/packages/private/kbn-react-mute-legacy-root-warning"], + "@kbn/react-mute-legacy-root-warning/*": ["src/platform/packages/private/kbn-react-mute-legacy-root-warning/*"], "@kbn/recently-accessed": ["src/platform/packages/shared/kbn-recently-accessed"], "@kbn/recently-accessed/*": ["src/platform/packages/shared/kbn-recently-accessed/*"], "@kbn/relocate": ["packages/kbn-relocate"], @@ -1538,8 +1538,8 @@ "@kbn/saved-objects-management-plugin/*": ["src/platform/plugins/shared/saved_objects_management/*"], "@kbn/saved-objects-plugin": ["src/platform/plugins/shared/saved_objects"], "@kbn/saved-objects-plugin/*": ["src/platform/plugins/shared/saved_objects/*"], - "@kbn/saved-objects-settings": ["packages/kbn-saved-objects-settings"], - "@kbn/saved-objects-settings/*": ["packages/kbn-saved-objects-settings/*"], + "@kbn/saved-objects-settings": ["src/platform/packages/private/kbn-saved-objects-settings"], + "@kbn/saved-objects-settings/*": ["src/platform/packages/private/kbn-saved-objects-settings/*"], "@kbn/saved-objects-tagging-oss-plugin": ["src/platform/plugins/shared/saved_objects_tagging_oss"], "@kbn/saved-objects-tagging-oss-plugin/*": ["src/platform/plugins/shared/saved_objects_tagging_oss/*"], "@kbn/saved-objects-tagging-plugin": ["x-pack/platform/plugins/shared/saved_objects_tagging"], @@ -1700,14 +1700,14 @@ "@kbn/server-route-repository-utils/*": ["src/platform/packages/shared/kbn-server-route-repository-utils/*"], "@kbn/serverless": ["x-pack/platform/plugins/shared/serverless"], "@kbn/serverless/*": ["x-pack/platform/plugins/shared/serverless/*"], - "@kbn/serverless-common-settings": ["packages/serverless/settings/common"], - "@kbn/serverless-common-settings/*": ["packages/serverless/settings/common/*"], + "@kbn/serverless-common-settings": ["src/platform/packages/private/serverless/settings/common"], + "@kbn/serverless-common-settings/*": ["src/platform/packages/private/serverless/settings/common/*"], "@kbn/serverless-observability": ["x-pack/solutions/observability/plugins/serverless_observability"], "@kbn/serverless-observability/*": ["x-pack/solutions/observability/plugins/serverless_observability/*"], - "@kbn/serverless-observability-settings": ["packages/serverless/settings/observability_project"], - "@kbn/serverless-observability-settings/*": ["packages/serverless/settings/observability_project/*"], - "@kbn/serverless-project-switcher": ["packages/serverless/project_switcher"], - "@kbn/serverless-project-switcher/*": ["packages/serverless/project_switcher/*"], + "@kbn/serverless-observability-settings": ["src/platform/packages/shared/serverless/settings/observability_project"], + "@kbn/serverless-observability-settings/*": ["src/platform/packages/shared/serverless/settings/observability_project/*"], + "@kbn/serverless-project-switcher": ["src/platform/packages/private/serverless/project_switcher"], + "@kbn/serverless-project-switcher/*": ["src/platform/packages/private/serverless/project_switcher/*"], "@kbn/serverless-search": ["x-pack/solutions/search/plugins/serverless_search"], "@kbn/serverless-search/*": ["x-pack/solutions/search/plugins/serverless_search/*"], "@kbn/serverless-search-settings": ["src/platform/packages/shared/serverless/settings/search_project"], @@ -1716,8 +1716,8 @@ "@kbn/serverless-security-settings/*": ["src/platform/packages/shared/serverless/settings/security_project/*"], "@kbn/serverless-storybook-config": ["packages/serverless/storybook/config"], "@kbn/serverless-storybook-config/*": ["packages/serverless/storybook/config/*"], - "@kbn/serverless-types": ["packages/serverless/types"], - "@kbn/serverless-types/*": ["packages/serverless/types/*"], + "@kbn/serverless-types": ["src/platform/packages/private/serverless/types"], + "@kbn/serverless-types/*": ["src/platform/packages/private/serverless/types/*"], "@kbn/session-notifications-plugin": ["test/plugin_functional/plugins/session_notifications"], "@kbn/session-notifications-plugin/*": ["test/plugin_functional/plugins/session_notifications/*"], "@kbn/session-view-plugin": ["x-pack/solutions/security/plugins/session_view"], @@ -2002,8 +2002,8 @@ "@kbn/upgrade-assistant-plugin/*": ["x-pack/platform/plugins/private/upgrade_assistant/*"], "@kbn/uptime-plugin": ["x-pack/solutions/observability/plugins/uptime"], "@kbn/uptime-plugin/*": ["x-pack/solutions/observability/plugins/uptime/*"], - "@kbn/url-drilldown-plugin": ["x-pack/plugins/drilldowns/url_drilldown"], - "@kbn/url-drilldown-plugin/*": ["x-pack/plugins/drilldowns/url_drilldown/*"], + "@kbn/url-drilldown-plugin": ["x-pack/platform/plugins/private/drilldowns/url_drilldown"], + "@kbn/url-drilldown-plugin/*": ["x-pack/platform/plugins/private/drilldowns/url_drilldown/*"], "@kbn/url-forwarding-plugin": ["src/platform/plugins/private/url_forwarding"], "@kbn/url-forwarding-plugin/*": ["src/platform/plugins/private/url_forwarding/*"], "@kbn/usage-collection-plugin": ["src/platform/plugins/shared/usage_collection"], diff --git a/x-pack/.i18nrc.json b/x-pack/.i18nrc.json index e90a31e284c07..2f5ba408dcdd5 100644 --- a/x-pack/.i18nrc.json +++ b/x-pack/.i18nrc.json @@ -176,7 +176,7 @@ "xpack.ux": [ "solutions/observability/plugins/ux" ], - "xpack.urlDrilldown": "plugins/drilldowns/url_drilldown", + "xpack.urlDrilldown": "platform/plugins/private/drilldowns/url_drilldown", "xpack.watcher": "platform/plugins/private/watcher" }, "exclude": [ diff --git a/x-pack/plugins/drilldowns/url_drilldown/README.md b/x-pack/platform/plugins/private/drilldowns/url_drilldown/README.md similarity index 100% rename from x-pack/plugins/drilldowns/url_drilldown/README.md rename to x-pack/platform/plugins/private/drilldowns/url_drilldown/README.md diff --git a/x-pack/platform/plugins/private/drilldowns/url_drilldown/jest.config.js b/x-pack/platform/plugins/private/drilldowns/url_drilldown/jest.config.js new file mode 100644 index 0000000000000..0dfd29be8e328 --- /dev/null +++ b/x-pack/platform/plugins/private/drilldowns/url_drilldown/jest.config.js @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../../../../..', + roots: ['/x-pack/platform/plugins/private/drilldowns/url_drilldown'], + coverageDirectory: + '/target/kibana-coverage/jest/x-pack/platform/plugins/private/drilldowns/url_drilldown', + coverageReporters: ['text', 'html'], + collectCoverageFrom: [ + '/x-pack/platform/plugins/private/drilldowns/url_drilldown/{common,public,server}/**/*.{ts,tsx}', + ], +}; diff --git a/x-pack/plugins/drilldowns/url_drilldown/kibana.jsonc b/x-pack/platform/plugins/private/drilldowns/url_drilldown/kibana.jsonc similarity index 100% rename from x-pack/plugins/drilldowns/url_drilldown/kibana.jsonc rename to x-pack/platform/plugins/private/drilldowns/url_drilldown/kibana.jsonc diff --git a/x-pack/plugins/drilldowns/url_drilldown/public/index.ts b/x-pack/platform/plugins/private/drilldowns/url_drilldown/public/index.ts similarity index 100% rename from x-pack/plugins/drilldowns/url_drilldown/public/index.ts rename to x-pack/platform/plugins/private/drilldowns/url_drilldown/public/index.ts diff --git a/x-pack/plugins/drilldowns/url_drilldown/public/lib/i18n.ts b/x-pack/platform/plugins/private/drilldowns/url_drilldown/public/lib/i18n.ts similarity index 100% rename from x-pack/plugins/drilldowns/url_drilldown/public/lib/i18n.ts rename to x-pack/platform/plugins/private/drilldowns/url_drilldown/public/lib/i18n.ts diff --git a/x-pack/plugins/drilldowns/url_drilldown/public/lib/index.ts b/x-pack/platform/plugins/private/drilldowns/url_drilldown/public/lib/index.ts similarity index 100% rename from x-pack/plugins/drilldowns/url_drilldown/public/lib/index.ts rename to x-pack/platform/plugins/private/drilldowns/url_drilldown/public/lib/index.ts diff --git a/x-pack/plugins/drilldowns/url_drilldown/public/lib/test/data.ts b/x-pack/platform/plugins/private/drilldowns/url_drilldown/public/lib/test/data.ts similarity index 100% rename from x-pack/plugins/drilldowns/url_drilldown/public/lib/test/data.ts rename to x-pack/platform/plugins/private/drilldowns/url_drilldown/public/lib/test/data.ts diff --git a/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown.test.tsx b/x-pack/platform/plugins/private/drilldowns/url_drilldown/public/lib/url_drilldown.test.tsx similarity index 100% rename from x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown.test.tsx rename to x-pack/platform/plugins/private/drilldowns/url_drilldown/public/lib/url_drilldown.test.tsx diff --git a/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown.tsx b/x-pack/platform/plugins/private/drilldowns/url_drilldown/public/lib/url_drilldown.tsx similarity index 100% rename from x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown.tsx rename to x-pack/platform/plugins/private/drilldowns/url_drilldown/public/lib/url_drilldown.tsx diff --git a/x-pack/plugins/drilldowns/url_drilldown/public/lib/variables/context_variables.test.ts b/x-pack/platform/plugins/private/drilldowns/url_drilldown/public/lib/variables/context_variables.test.ts similarity index 100% rename from x-pack/plugins/drilldowns/url_drilldown/public/lib/variables/context_variables.test.ts rename to x-pack/platform/plugins/private/drilldowns/url_drilldown/public/lib/variables/context_variables.test.ts diff --git a/x-pack/plugins/drilldowns/url_drilldown/public/lib/variables/context_variables.ts b/x-pack/platform/plugins/private/drilldowns/url_drilldown/public/lib/variables/context_variables.ts similarity index 100% rename from x-pack/plugins/drilldowns/url_drilldown/public/lib/variables/context_variables.ts rename to x-pack/platform/plugins/private/drilldowns/url_drilldown/public/lib/variables/context_variables.ts diff --git a/x-pack/plugins/drilldowns/url_drilldown/public/lib/variables/event_variables.test.ts b/x-pack/platform/plugins/private/drilldowns/url_drilldown/public/lib/variables/event_variables.test.ts similarity index 100% rename from x-pack/plugins/drilldowns/url_drilldown/public/lib/variables/event_variables.test.ts rename to x-pack/platform/plugins/private/drilldowns/url_drilldown/public/lib/variables/event_variables.test.ts diff --git a/x-pack/plugins/drilldowns/url_drilldown/public/lib/variables/event_variables.ts b/x-pack/platform/plugins/private/drilldowns/url_drilldown/public/lib/variables/event_variables.ts similarity index 100% rename from x-pack/plugins/drilldowns/url_drilldown/public/lib/variables/event_variables.ts rename to x-pack/platform/plugins/private/drilldowns/url_drilldown/public/lib/variables/event_variables.ts diff --git a/x-pack/plugins/drilldowns/url_drilldown/public/lib/variables/global_variables.ts b/x-pack/platform/plugins/private/drilldowns/url_drilldown/public/lib/variables/global_variables.ts similarity index 100% rename from x-pack/plugins/drilldowns/url_drilldown/public/lib/variables/global_variables.ts rename to x-pack/platform/plugins/private/drilldowns/url_drilldown/public/lib/variables/global_variables.ts diff --git a/x-pack/plugins/drilldowns/url_drilldown/public/lib/variables/i18n.ts b/x-pack/platform/plugins/private/drilldowns/url_drilldown/public/lib/variables/i18n.ts similarity index 100% rename from x-pack/plugins/drilldowns/url_drilldown/public/lib/variables/i18n.ts rename to x-pack/platform/plugins/private/drilldowns/url_drilldown/public/lib/variables/i18n.ts diff --git a/x-pack/plugins/drilldowns/url_drilldown/public/lib/variables/util.ts b/x-pack/platform/plugins/private/drilldowns/url_drilldown/public/lib/variables/util.ts similarity index 100% rename from x-pack/plugins/drilldowns/url_drilldown/public/lib/variables/util.ts rename to x-pack/platform/plugins/private/drilldowns/url_drilldown/public/lib/variables/util.ts diff --git a/x-pack/plugins/drilldowns/url_drilldown/public/plugin.ts b/x-pack/platform/plugins/private/drilldowns/url_drilldown/public/plugin.ts similarity index 100% rename from x-pack/plugins/drilldowns/url_drilldown/public/plugin.ts rename to x-pack/platform/plugins/private/drilldowns/url_drilldown/public/plugin.ts diff --git a/x-pack/plugins/drilldowns/url_drilldown/tsconfig.json b/x-pack/platform/plugins/private/drilldowns/url_drilldown/tsconfig.json similarity index 92% rename from x-pack/plugins/drilldowns/url_drilldown/tsconfig.json rename to x-pack/platform/plugins/private/drilldowns/url_drilldown/tsconfig.json index 7da816978edcf..8c018bb451e39 100644 --- a/x-pack/plugins/drilldowns/url_drilldown/tsconfig.json +++ b/x-pack/platform/plugins/private/drilldowns/url_drilldown/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../../tsconfig.base.json", + "extends": "../../../../../../tsconfig.base.json", "compilerOptions": { "outDir": "target/types", }, diff --git a/x-pack/platform/plugins/shared/maps/public/trigger_actions/trigger_utils.ts b/x-pack/platform/plugins/shared/maps/public/trigger_actions/trigger_utils.ts index 0f85a7b332e37..f1d1ed5d32904 100644 --- a/x-pack/platform/plugins/shared/maps/public/trigger_actions/trigger_utils.ts +++ b/x-pack/platform/plugins/shared/maps/public/trigger_actions/trigger_utils.ts @@ -15,7 +15,7 @@ export function isUrlDrilldown(action: Action) { // VALUE_CLICK_TRIGGER is coupled with expressions and Datatable type // URL drilldown parses event scope from Datatable -// https://github.com/elastic/kibana/blob/7.10/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown_scope.ts#L140 +// https://github.com/elastic/kibana/blob/7.10/x-pack/platform/plugins/private/drilldowns/url_drilldown/public/lib/url_drilldown_scope.ts#L140 // In order to use URL drilldown, maps has to package its data in Datatable compatiable format. export function toValueClickDataFormat(key: string, value: RawValue) { return [ diff --git a/x-pack/plugins/drilldowns/jest.config.js b/x-pack/plugins/drilldowns/jest.config.js deleted file mode 100644 index a5db93a916ebb..0000000000000 --- a/x-pack/plugins/drilldowns/jest.config.js +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -module.exports = { - preset: '@kbn/test', - rootDir: '../../..', - roots: ['/x-pack/plugins/drilldowns'], - coverageDirectory: '/target/kibana-coverage/jest/x-pack/plugins/drilldowns', - coverageReporters: ['text', 'html'], - collectCoverageFrom: ['/x-pack/plugins/drilldowns/url_drilldown/public/**/*.{ts,tsx}'], -}; diff --git a/x-pack/test_serverless/api_integration/test_suites/common/core/ui_settings.ts b/x-pack/test_serverless/api_integration/test_suites/common/core/ui_settings.ts index 228ba1f3ad28f..a359c0de62db4 100644 --- a/x-pack/test_serverless/api_integration/test_suites/common/core/ui_settings.ts +++ b/x-pack/test_serverless/api_integration/test_suites/common/core/ui_settings.ts @@ -10,7 +10,7 @@ import { SupertestWithRoleScopeType } from '@kbn/test-suites-xpack/api_integrati import { FtrProviderContext } from '../../../ftr_provider_context'; // To test setting validations we are using the existing 'defaultColumns' setting that is available in all serverless projects -// (See list of common serverless settings in /packages/serverless/settings/common/index.ts) +// (See list of common serverless settings in /src/platform/packages/private/serverless/settings/common/index.ts) // The 'defaultColumns' setting is of type array of strings const DEFAULT_COLUMNS_SETTING = 'defaultColumns'; diff --git a/yarn.lock b/yarn.lock index 072f8db4c9f67..c4a383bb41de2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4421,7 +4421,7 @@ version "0.0.0" uid "" -"@kbn/core-chrome-browser@link:packages/core/chrome/core-chrome-browser": +"@kbn/core-chrome-browser@link:src/core/packages/chrome/browser": version "0.0.0" uid "" @@ -5341,7 +5341,7 @@ version "0.0.0" uid "" -"@kbn/deeplinks-shared@link:packages/deeplinks/shared": +"@kbn/deeplinks-shared@link:src/platform/packages/shared/deeplinks/shared": version "0.0.0" uid "" @@ -5877,7 +5877,7 @@ version "0.0.0" uid "" -"@kbn/guided-onboarding@link:packages/kbn-guided-onboarding": +"@kbn/guided-onboarding@link:src/platform/packages/shared/kbn-guided-onboarding": version "0.0.0" uid "" @@ -5905,15 +5905,15 @@ version "0.0.0" uid "" -"@kbn/home-sample-data-card@link:packages/home/sample_data_card": +"@kbn/home-sample-data-card@link:src/platform/packages/shared/home/sample_data_card": version "0.0.0" uid "" -"@kbn/home-sample-data-tab@link:packages/home/sample_data_tab": +"@kbn/home-sample-data-tab@link:src/platform/packages/shared/home/sample_data_tab": version "0.0.0" uid "" -"@kbn/home-sample-data-types@link:packages/home/sample_data_types": +"@kbn/home-sample-data-types@link:src/platform/packages/shared/home/sample_data_types": version "0.0.0" uid "" @@ -6045,7 +6045,7 @@ version "0.0.0" uid "" -"@kbn/item-buffer@link:packages/kbn-item-buffer": +"@kbn/item-buffer@link:src/platform/packages/private/kbn-item-buffer": version "0.0.0" uid "" @@ -6249,11 +6249,11 @@ version "0.0.0" uid "" -"@kbn/management-settings-ids@link:packages/kbn-management/settings/setting_ids": +"@kbn/management-settings-ids@link:src/platform/packages/shared/kbn-management/settings/setting_ids": version "0.0.0" uid "" -"@kbn/management-settings-section-registry@link:packages/kbn-management/settings/section_registry": +"@kbn/management-settings-section-registry@link:src/platform/packages/shared/kbn-management/settings/section_registry": version "0.0.0" uid "" @@ -6697,31 +6697,31 @@ version "0.0.0" uid "" -"@kbn/react-kibana-context-common@link:packages/react/kibana_context/common": +"@kbn/react-kibana-context-common@link:src/platform/packages/shared/react/kibana_context/common": version "0.0.0" uid "" -"@kbn/react-kibana-context-render@link:packages/react/kibana_context/render": +"@kbn/react-kibana-context-render@link:src/platform/packages/shared/react/kibana_context/render": version "0.0.0" uid "" -"@kbn/react-kibana-context-root@link:packages/react/kibana_context/root": +"@kbn/react-kibana-context-root@link:src/platform/packages/shared/react/kibana_context/root": version "0.0.0" uid "" -"@kbn/react-kibana-context-styled@link:packages/react/kibana_context/styled": +"@kbn/react-kibana-context-styled@link:src/platform/packages/shared/react/kibana_context/styled": version "0.0.0" uid "" -"@kbn/react-kibana-context-theme@link:packages/react/kibana_context/theme": +"@kbn/react-kibana-context-theme@link:src/platform/packages/shared/react/kibana_context/theme": version "0.0.0" uid "" -"@kbn/react-kibana-mount@link:packages/react/kibana_mount": +"@kbn/react-kibana-mount@link:src/platform/packages/shared/react/kibana_mount": version "0.0.0" uid "" -"@kbn/react-mute-legacy-root-warning@link:packages/kbn-react-mute-legacy-root-warning": +"@kbn/react-mute-legacy-root-warning@link:src/platform/packages/private/kbn-react-mute-legacy-root-warning": version "0.0.0" uid "" @@ -6929,7 +6929,7 @@ version "0.0.0" uid "" -"@kbn/saved-objects-settings@link:packages/kbn-saved-objects-settings": +"@kbn/saved-objects-settings@link:src/platform/packages/private/kbn-saved-objects-settings": version "0.0.0" uid "" @@ -7249,11 +7249,11 @@ version "0.0.0" uid "" -"@kbn/serverless-common-settings@link:packages/serverless/settings/common": +"@kbn/serverless-common-settings@link:src/platform/packages/private/serverless/settings/common": version "0.0.0" uid "" -"@kbn/serverless-observability-settings@link:packages/serverless/settings/observability_project": +"@kbn/serverless-observability-settings@link:src/platform/packages/shared/serverless/settings/observability_project": version "0.0.0" uid "" @@ -7261,7 +7261,7 @@ version "0.0.0" uid "" -"@kbn/serverless-project-switcher@link:packages/serverless/project_switcher": +"@kbn/serverless-project-switcher@link:src/platform/packages/private/serverless/project_switcher": version "0.0.0" uid "" @@ -7281,7 +7281,7 @@ version "0.0.0" uid "" -"@kbn/serverless-types@link:packages/serverless/types": +"@kbn/serverless-types@link:src/platform/packages/private/serverless/types": version "0.0.0" uid "" @@ -7857,7 +7857,7 @@ version "0.0.0" uid "" -"@kbn/url-drilldown-plugin@link:x-pack/plugins/drilldowns/url_drilldown": +"@kbn/url-drilldown-plugin@link:x-pack/platform/plugins/private/drilldowns/url_drilldown": version "0.0.0" uid "" From a3f07db7b63e29f0f0a08f20e8d90de1bcab6186 Mon Sep 17 00:00:00 2001 From: Giorgos Bamparopoulos Date: Fri, 10 Jan 2025 11:51:12 +0000 Subject: [PATCH 14/42] Remove log stream and settings (#204115) Removes the code used to render the log stream and settings pages. The following areas have been checked: - Log stream embeddable (dashboard of the `cisco_meraki` integration) - Log stream shared component - Log categories - Log anomalies https://github.com/user-attachments/assets/2bc0763d-3def-4c4b-b50a-21c17976a596 Closes https://github.com/elastic/kibana/issues/204005 --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .github/CODEOWNERS | 3 +- .../translations/translations/fr-FR.json | 76 ---- .../translations/translations/ja-JP.json | 75 ---- .../translations/translations/zh-CN.json | 73 ---- .../logs_shared/common/http_api/index.ts | 1 - .../logs_shared/common/http_api/latest.ts | 1 - .../http_api/log_entries/v1/highlights.ts | 70 ---- .../common/http_api/log_entries/v1/index.ts | 10 - .../common/http_api/log_entries/v1/summary.ts | 39 -- .../log_entries/v1/summary_highlights.ts | 47 --- .../shared/logs_shared/common/index.ts | 13 - .../logs_shared/common/time/time_key.ts | 24 +- .../api/fetch_log_entries_highlights.ts | 30 -- .../api/fetch_log_summary_highlights.ts | 29 -- .../containers/logs/log_highlights/index.ts | 8 - .../log_highlights/log_entry_highlights.tsx | 104 ----- .../logs/log_highlights/log_highlights.tsx | 93 ----- .../log_highlights/log_summary_highlights.ts | 93 ----- .../logs/log_highlights/next_and_previous.tsx | 105 ----- .../logs/log_summary/api/fetch_log_summary.ts | 29 -- .../logs/log_summary/bucket_size.ts | 24 -- .../containers/logs/log_summary/index.ts | 9 - .../logs/log_summary/log_summary.test.tsx | 215 ----------- .../logs/log_summary/log_summary.tsx | 74 ---- .../logs/log_summary/with_summary.ts | 43 --- .../shared/logs_shared/public/index.ts | 6 - .../public/utils/log_entry/log_entry.ts | 23 -- .../logs_shared/public/utils/typed_react.tsx | 11 - .../public/utils/use_observable.ts | 25 +- .../log_entries/kibana_log_entries_adapter.ts | 148 ------- .../log_entries_domain.mock.ts | 3 - .../log_entries_domain/log_entries_domain.ts | 214 ----------- .../logs_shared/server/logs_shared_server.ts | 9 +- .../server/routes/log_entries/highlights.ts | 118 ------ .../server/routes/log_entries/index.ts | 10 - .../server/routes/log_entries/summary.ts | 88 ----- .../routes/log_entries/summary_highlights.ts | 83 ---- .../infra/common/log_search_result/index.ts | 13 - .../log_search_result/log_search_result.ts | 32 -- .../infra/common/log_search_summary/index.ts | 8 - .../log_search_summary/log_search_summary.ts | 15 - .../infra/common/log_text_scale/index.ts | 8 - .../common/log_text_scale/log_text_scale.ts | 12 - .../plugins/infra/common/time/time_key.ts | 24 +- .../infra/common/url_state_storage_service.ts | 62 --- .../logging/inline_log_view_splash_page.tsx | 72 ---- .../logging/log_customization_menu.tsx | 79 ---- .../components/logging/log_datepicker.tsx | 82 ---- .../logging/log_highlights_menu.tsx | 190 --------- .../logging/log_minimap/density_chart.tsx | 80 ---- .../log_minimap/highlighted_interval.tsx | 65 ---- .../components/logging/log_minimap/index.ts | 8 - .../logging/log_minimap/log_minimap.tsx | 184 --------- .../logging/log_minimap/search_marker.tsx | 125 ------ .../log_minimap/search_marker_tooltip.tsx | 70 ---- .../logging/log_minimap/search_markers.tsx | 56 --- .../log_minimap/time_label_formatter.tsx | 24 -- .../logging/log_minimap/time_ruler.tsx | 77 ---- .../logging/log_search_controls/index.ts | 8 - .../log_search_buttons.tsx | 77 ---- .../log_search_controls.tsx | 64 --- .../log_search_controls/log_search_input.tsx | 85 ---- .../components/logging/log_statusbar.tsx | 33 -- .../logging/log_text_scale_controls.tsx | 65 ---- .../logging/log_text_wrap_controls.tsx | 47 --- .../logs/log_view_configuration.test.tsx | 55 --- .../logs/log_view_configuration.tsx | 36 -- .../containers/logs/with_log_textview.tsx | 60 --- .../infra/public/hooks/use_observable.ts | 18 +- .../log_stream_page/state/README.md | 3 - .../log_stream_page/state/index.ts | 8 - .../log_stream_page/state/src/index.ts | 11 - .../state/src/initial_parameters_service.ts | 89 ----- .../log_stream_page/state/src/provider.tsx | 54 --- .../log_stream_page/state/src/selectors.ts | 19 - .../state/src/state_machine.ts | 355 ----------------- .../log_stream_page/state/src/types.ts | 116 ------ .../log_stream_position_state/index.ts | 9 - .../log_stream_position_state/src/defaults.ts | 9 - .../src/notifications.ts | 44 --- .../src/state_machine.ts | 241 ------------ .../log_stream_position_state/src/types.ts | 65 ---- .../src/url_state_storage_service.ts | 100 ----- .../log_stream_query_state/index.ts | 8 - .../log_stream_query_state/src/defaults.ts | 20 - .../log_stream_query_state/src/errors.ts | 21 - .../log_stream_query_state/src/index.ts | 13 - .../src/notifications.ts | 56 --- .../src/search_bar_state_service.ts | 61 --- .../src/state_machine.ts | 362 ----------------- .../src/time_filter_state_service.ts | 192 --------- .../log_stream_query_state/src/types.ts | 160 -------- .../src/url_state_storage_service.ts | 284 -------------- .../src/validate_query_service.ts | 71 ---- .../xstate_helpers/README.md | 3 - .../xstate_helpers/index.ts | 8 - .../xstate_helpers/src/index.ts | 9 - .../src/invalid_state_callout.tsx | 33 -- .../src/state_machine_playground.tsx | 81 ---- .../category_example_message.tsx | 4 +- .../sections/anomalies/log_entry_example.tsx | 8 +- .../infra/public/pages/logs/page_content.tsx | 26 +- .../logs/settings/add_log_column_popover.tsx | 161 -------- .../pages/logs/settings/form_elements.tsx | 244 ------------ .../pages/logs/settings/form_field_props.tsx | 39 -- .../infra/public/pages/logs/settings/index.ts | 8 - .../index_names_configuration_panel.tsx | 86 ----- .../index_pattern_configuration_panel.tsx | 115 ------ .../logs/settings/index_pattern_selector.tsx | 89 ----- .../indices_configuration_form_state.ts | 93 ----- .../indices_configuration_panel.stories.tsx | 148 ------- .../settings/indices_configuration_panel.tsx | 246 ------------ .../logs/settings/inline_log_view_callout.tsx | 49 --- ...a_advanced_setting_configuration_panel.tsx | 37 -- .../log_columns_configuration_form_state.tsx | 21 - .../log_columns_configuration_panel.tsx | 336 ---------------- .../name_configuration_form_state.tsx | 20 - .../settings/name_configuration_panel.tsx | 69 ---- .../source_configuration_form_errors.tsx | 125 ------ .../source_configuration_form_state.tsx | 53 --- .../source_configuration_settings.tsx | 214 ----------- .../pages/logs/settings/validation_errors.ts | 130 ------- .../pages/logs/shared/page_log_view_error.tsx | 24 +- .../stream/components/stream_live_button.tsx | 43 --- .../components/stream_page_template.tsx | 87 ----- .../infra/public/pages/logs/stream/index.ts | 8 - .../infra/public/pages/logs/stream/page.tsx | 63 --- .../public/pages/logs/stream/page_content.tsx | 98 ----- .../pages/logs/stream/page_logs_content.tsx | 363 ------------------ .../stream/page_missing_indices_content.tsx | 13 - .../pages/logs/stream/page_providers.tsx | 129 ------- .../public/pages/logs/stream/page_toolbar.tsx | 111 ------ .../plugins/infra/public/translations.ts | 4 - .../public/utils/theme_utils/with_attrs.tsx | 19 - .../observability/plugins/infra/tsconfig.json | 1 - .../api_integration/apis/logs_ui/index.ts | 2 - .../apis/logs_ui/log_entry_highlights.ts | 213 ---------- .../apis/logs_ui/log_summary.ts | 79 ---- x-pack/test/functional/apps/infra/index.ts | 3 - .../functional/apps/infra/logs/log_stream.ts | 88 ----- .../apps/infra/logs/log_stream_date_nano.ts | 87 ----- .../infra/logs/logs_source_configuration.ts | 195 ---------- .../page_objects/infra_logs_page.ts | 35 +- .../infra_source_configuration_form.ts | 82 ---- .../test/functional/services/logs_ui/index.ts | 2 - .../functional/services/logs_ui/log_stream.ts | 80 ---- x-pack/test/upgrade/apps/logs/index.ts | 14 - .../upgrade/apps/logs/logs_smoke_tests.ts | 40 -- x-pack/test/upgrade/config.ts | 1 - 149 files changed, 17 insertions(+), 10553 deletions(-) delete mode 100644 x-pack/platform/plugins/shared/logs_shared/common/http_api/log_entries/v1/highlights.ts delete mode 100644 x-pack/platform/plugins/shared/logs_shared/common/http_api/log_entries/v1/index.ts delete mode 100644 x-pack/platform/plugins/shared/logs_shared/common/http_api/log_entries/v1/summary.ts delete mode 100644 x-pack/platform/plugins/shared/logs_shared/common/http_api/log_entries/v1/summary_highlights.ts delete mode 100644 x-pack/platform/plugins/shared/logs_shared/public/containers/logs/log_highlights/api/fetch_log_entries_highlights.ts delete mode 100644 x-pack/platform/plugins/shared/logs_shared/public/containers/logs/log_highlights/api/fetch_log_summary_highlights.ts delete mode 100644 x-pack/platform/plugins/shared/logs_shared/public/containers/logs/log_highlights/index.ts delete mode 100644 x-pack/platform/plugins/shared/logs_shared/public/containers/logs/log_highlights/log_entry_highlights.tsx delete mode 100644 x-pack/platform/plugins/shared/logs_shared/public/containers/logs/log_highlights/log_highlights.tsx delete mode 100644 x-pack/platform/plugins/shared/logs_shared/public/containers/logs/log_highlights/log_summary_highlights.ts delete mode 100644 x-pack/platform/plugins/shared/logs_shared/public/containers/logs/log_highlights/next_and_previous.tsx delete mode 100644 x-pack/platform/plugins/shared/logs_shared/public/containers/logs/log_summary/api/fetch_log_summary.ts delete mode 100644 x-pack/platform/plugins/shared/logs_shared/public/containers/logs/log_summary/bucket_size.ts delete mode 100644 x-pack/platform/plugins/shared/logs_shared/public/containers/logs/log_summary/index.ts delete mode 100644 x-pack/platform/plugins/shared/logs_shared/public/containers/logs/log_summary/log_summary.test.tsx delete mode 100644 x-pack/platform/plugins/shared/logs_shared/public/containers/logs/log_summary/log_summary.tsx delete mode 100644 x-pack/platform/plugins/shared/logs_shared/public/containers/logs/log_summary/with_summary.ts delete mode 100644 x-pack/platform/plugins/shared/logs_shared/public/utils/typed_react.tsx delete mode 100644 x-pack/platform/plugins/shared/logs_shared/server/routes/log_entries/highlights.ts delete mode 100644 x-pack/platform/plugins/shared/logs_shared/server/routes/log_entries/index.ts delete mode 100644 x-pack/platform/plugins/shared/logs_shared/server/routes/log_entries/summary.ts delete mode 100644 x-pack/platform/plugins/shared/logs_shared/server/routes/log_entries/summary_highlights.ts delete mode 100644 x-pack/solutions/observability/plugins/infra/common/log_search_result/index.ts delete mode 100644 x-pack/solutions/observability/plugins/infra/common/log_search_result/log_search_result.ts delete mode 100644 x-pack/solutions/observability/plugins/infra/common/log_search_summary/index.ts delete mode 100644 x-pack/solutions/observability/plugins/infra/common/log_search_summary/log_search_summary.ts delete mode 100644 x-pack/solutions/observability/plugins/infra/common/log_text_scale/index.ts delete mode 100644 x-pack/solutions/observability/plugins/infra/common/log_text_scale/log_text_scale.ts delete mode 100644 x-pack/solutions/observability/plugins/infra/public/components/logging/inline_log_view_splash_page.tsx delete mode 100644 x-pack/solutions/observability/plugins/infra/public/components/logging/log_customization_menu.tsx delete mode 100644 x-pack/solutions/observability/plugins/infra/public/components/logging/log_datepicker.tsx delete mode 100644 x-pack/solutions/observability/plugins/infra/public/components/logging/log_highlights_menu.tsx delete mode 100644 x-pack/solutions/observability/plugins/infra/public/components/logging/log_minimap/density_chart.tsx delete mode 100644 x-pack/solutions/observability/plugins/infra/public/components/logging/log_minimap/highlighted_interval.tsx delete mode 100644 x-pack/solutions/observability/plugins/infra/public/components/logging/log_minimap/index.ts delete mode 100644 x-pack/solutions/observability/plugins/infra/public/components/logging/log_minimap/log_minimap.tsx delete mode 100644 x-pack/solutions/observability/plugins/infra/public/components/logging/log_minimap/search_marker.tsx delete mode 100644 x-pack/solutions/observability/plugins/infra/public/components/logging/log_minimap/search_marker_tooltip.tsx delete mode 100644 x-pack/solutions/observability/plugins/infra/public/components/logging/log_minimap/search_markers.tsx delete mode 100644 x-pack/solutions/observability/plugins/infra/public/components/logging/log_minimap/time_label_formatter.tsx delete mode 100644 x-pack/solutions/observability/plugins/infra/public/components/logging/log_minimap/time_ruler.tsx delete mode 100644 x-pack/solutions/observability/plugins/infra/public/components/logging/log_search_controls/index.ts delete mode 100644 x-pack/solutions/observability/plugins/infra/public/components/logging/log_search_controls/log_search_buttons.tsx delete mode 100644 x-pack/solutions/observability/plugins/infra/public/components/logging/log_search_controls/log_search_controls.tsx delete mode 100644 x-pack/solutions/observability/plugins/infra/public/components/logging/log_search_controls/log_search_input.tsx delete mode 100644 x-pack/solutions/observability/plugins/infra/public/components/logging/log_statusbar.tsx delete mode 100644 x-pack/solutions/observability/plugins/infra/public/components/logging/log_text_scale_controls.tsx delete mode 100644 x-pack/solutions/observability/plugins/infra/public/components/logging/log_text_wrap_controls.tsx delete mode 100644 x-pack/solutions/observability/plugins/infra/public/containers/logs/log_view_configuration.test.tsx delete mode 100644 x-pack/solutions/observability/plugins/infra/public/containers/logs/log_view_configuration.tsx delete mode 100644 x-pack/solutions/observability/plugins/infra/public/containers/logs/with_log_textview.tsx delete mode 100644 x-pack/solutions/observability/plugins/infra/public/observability_logs/log_stream_page/state/README.md delete mode 100644 x-pack/solutions/observability/plugins/infra/public/observability_logs/log_stream_page/state/index.ts delete mode 100644 x-pack/solutions/observability/plugins/infra/public/observability_logs/log_stream_page/state/src/index.ts delete mode 100644 x-pack/solutions/observability/plugins/infra/public/observability_logs/log_stream_page/state/src/initial_parameters_service.ts delete mode 100644 x-pack/solutions/observability/plugins/infra/public/observability_logs/log_stream_page/state/src/provider.tsx delete mode 100644 x-pack/solutions/observability/plugins/infra/public/observability_logs/log_stream_page/state/src/selectors.ts delete mode 100644 x-pack/solutions/observability/plugins/infra/public/observability_logs/log_stream_page/state/src/state_machine.ts delete mode 100644 x-pack/solutions/observability/plugins/infra/public/observability_logs/log_stream_page/state/src/types.ts delete mode 100644 x-pack/solutions/observability/plugins/infra/public/observability_logs/log_stream_position_state/index.ts delete mode 100644 x-pack/solutions/observability/plugins/infra/public/observability_logs/log_stream_position_state/src/defaults.ts delete mode 100644 x-pack/solutions/observability/plugins/infra/public/observability_logs/log_stream_position_state/src/notifications.ts delete mode 100644 x-pack/solutions/observability/plugins/infra/public/observability_logs/log_stream_position_state/src/state_machine.ts delete mode 100644 x-pack/solutions/observability/plugins/infra/public/observability_logs/log_stream_position_state/src/types.ts delete mode 100644 x-pack/solutions/observability/plugins/infra/public/observability_logs/log_stream_position_state/src/url_state_storage_service.ts delete mode 100644 x-pack/solutions/observability/plugins/infra/public/observability_logs/log_stream_query_state/index.ts delete mode 100644 x-pack/solutions/observability/plugins/infra/public/observability_logs/log_stream_query_state/src/defaults.ts delete mode 100644 x-pack/solutions/observability/plugins/infra/public/observability_logs/log_stream_query_state/src/errors.ts delete mode 100644 x-pack/solutions/observability/plugins/infra/public/observability_logs/log_stream_query_state/src/index.ts delete mode 100644 x-pack/solutions/observability/plugins/infra/public/observability_logs/log_stream_query_state/src/notifications.ts delete mode 100644 x-pack/solutions/observability/plugins/infra/public/observability_logs/log_stream_query_state/src/search_bar_state_service.ts delete mode 100644 x-pack/solutions/observability/plugins/infra/public/observability_logs/log_stream_query_state/src/state_machine.ts delete mode 100644 x-pack/solutions/observability/plugins/infra/public/observability_logs/log_stream_query_state/src/time_filter_state_service.ts delete mode 100644 x-pack/solutions/observability/plugins/infra/public/observability_logs/log_stream_query_state/src/types.ts delete mode 100644 x-pack/solutions/observability/plugins/infra/public/observability_logs/log_stream_query_state/src/url_state_storage_service.ts delete mode 100644 x-pack/solutions/observability/plugins/infra/public/observability_logs/log_stream_query_state/src/validate_query_service.ts delete mode 100644 x-pack/solutions/observability/plugins/infra/public/observability_logs/xstate_helpers/README.md delete mode 100644 x-pack/solutions/observability/plugins/infra/public/observability_logs/xstate_helpers/index.ts delete mode 100644 x-pack/solutions/observability/plugins/infra/public/observability_logs/xstate_helpers/src/index.ts delete mode 100644 x-pack/solutions/observability/plugins/infra/public/observability_logs/xstate_helpers/src/invalid_state_callout.tsx delete mode 100644 x-pack/solutions/observability/plugins/infra/public/observability_logs/xstate_helpers/src/state_machine_playground.tsx delete mode 100644 x-pack/solutions/observability/plugins/infra/public/pages/logs/settings/add_log_column_popover.tsx delete mode 100644 x-pack/solutions/observability/plugins/infra/public/pages/logs/settings/form_elements.tsx delete mode 100644 x-pack/solutions/observability/plugins/infra/public/pages/logs/settings/form_field_props.tsx delete mode 100644 x-pack/solutions/observability/plugins/infra/public/pages/logs/settings/index.ts delete mode 100644 x-pack/solutions/observability/plugins/infra/public/pages/logs/settings/index_names_configuration_panel.tsx delete mode 100644 x-pack/solutions/observability/plugins/infra/public/pages/logs/settings/index_pattern_configuration_panel.tsx delete mode 100644 x-pack/solutions/observability/plugins/infra/public/pages/logs/settings/index_pattern_selector.tsx delete mode 100644 x-pack/solutions/observability/plugins/infra/public/pages/logs/settings/indices_configuration_form_state.ts delete mode 100644 x-pack/solutions/observability/plugins/infra/public/pages/logs/settings/indices_configuration_panel.stories.tsx delete mode 100644 x-pack/solutions/observability/plugins/infra/public/pages/logs/settings/indices_configuration_panel.tsx delete mode 100644 x-pack/solutions/observability/plugins/infra/public/pages/logs/settings/inline_log_view_callout.tsx delete mode 100644 x-pack/solutions/observability/plugins/infra/public/pages/logs/settings/kibana_advanced_setting_configuration_panel.tsx delete mode 100644 x-pack/solutions/observability/plugins/infra/public/pages/logs/settings/log_columns_configuration_form_state.tsx delete mode 100644 x-pack/solutions/observability/plugins/infra/public/pages/logs/settings/log_columns_configuration_panel.tsx delete mode 100644 x-pack/solutions/observability/plugins/infra/public/pages/logs/settings/name_configuration_form_state.tsx delete mode 100644 x-pack/solutions/observability/plugins/infra/public/pages/logs/settings/name_configuration_panel.tsx delete mode 100644 x-pack/solutions/observability/plugins/infra/public/pages/logs/settings/source_configuration_form_errors.tsx delete mode 100644 x-pack/solutions/observability/plugins/infra/public/pages/logs/settings/source_configuration_form_state.tsx delete mode 100644 x-pack/solutions/observability/plugins/infra/public/pages/logs/settings/source_configuration_settings.tsx delete mode 100644 x-pack/solutions/observability/plugins/infra/public/pages/logs/settings/validation_errors.ts delete mode 100644 x-pack/solutions/observability/plugins/infra/public/pages/logs/stream/components/stream_live_button.tsx delete mode 100644 x-pack/solutions/observability/plugins/infra/public/pages/logs/stream/components/stream_page_template.tsx delete mode 100644 x-pack/solutions/observability/plugins/infra/public/pages/logs/stream/index.ts delete mode 100644 x-pack/solutions/observability/plugins/infra/public/pages/logs/stream/page.tsx delete mode 100644 x-pack/solutions/observability/plugins/infra/public/pages/logs/stream/page_content.tsx delete mode 100644 x-pack/solutions/observability/plugins/infra/public/pages/logs/stream/page_logs_content.tsx delete mode 100644 x-pack/solutions/observability/plugins/infra/public/pages/logs/stream/page_missing_indices_content.tsx delete mode 100644 x-pack/solutions/observability/plugins/infra/public/pages/logs/stream/page_providers.tsx delete mode 100644 x-pack/solutions/observability/plugins/infra/public/pages/logs/stream/page_toolbar.tsx delete mode 100644 x-pack/solutions/observability/plugins/infra/public/utils/theme_utils/with_attrs.tsx delete mode 100644 x-pack/test/api_integration/apis/logs_ui/log_entry_highlights.ts delete mode 100644 x-pack/test/api_integration/apis/logs_ui/log_summary.ts delete mode 100644 x-pack/test/functional/apps/infra/logs/log_stream.ts delete mode 100644 x-pack/test/functional/apps/infra/logs/log_stream_date_nano.ts delete mode 100644 x-pack/test/functional/apps/infra/logs/logs_source_configuration.ts delete mode 100644 x-pack/test/functional/services/logs_ui/log_stream.ts delete mode 100644 x-pack/test/upgrade/apps/logs/index.ts delete mode 100644 x-pack/test/upgrade/apps/logs/logs_smoke_tests.ts diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index bb3c25078ecda..d9002f9b4b8fb 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1304,7 +1304,6 @@ packages/kbn-monaco/src/esql @elastic/kibana-esql ## Logs UI code exceptions -> @elastic/obs-ux-logs-team /x-pack/test/api_integration/deployment_agnostic/apis/observability/data_quality/ @elastic/obs-ux-logs-team -/x-pack/test/upgrade/apps/logs @elastic/obs-ux-logs-team /x-pack/test_serverless/functional/page_objects/svl_oblt_onboarding_stream_log_file.ts @elastic/obs-ux-logs-team /x-pack/test_serverless/functional/page_objects/svl_oblt_onboarding_page.ts @elastic/obs-ux-logs-team /x-pack/solutions/observability/plugins/infra/common/http_api/log_alerts @elastic/obs-ux-logs-team @@ -1419,7 +1418,7 @@ packages/kbn-monaco/src/esql @elastic/kibana-esql /x-pack/test/functional/page_objects/dataset_quality.ts @elastic/obs-ux-logs-team /x-pack/test_serverless/functional/test_suites/observability/index* @elastic/obs-ux-logs-team /x-pack/test_serverless/functional/test_suites/observability/cypress @elastic/obs-ux-logs-team -/x-pack/test/functional/services/infra_source_configuration_form.ts @elastic/obs-ux-logs-team +/x-pack/test/functional/services/infra_source_configuration_form.ts @elastic/obs-ux-infra_services-team /x-pack/test/functional/services/logs_ui @elastic/obs-ux-logs-team /x-pack/test/functional/page_objects/observability_logs_explorer.ts @elastic/obs-ux-logs-team /test/functional/services/selectable.ts @elastic/obs-ux-logs-team diff --git a/x-pack/platform/plugins/private/translations/translations/fr-FR.json b/x-pack/platform/plugins/private/translations/translations/fr-FR.json index 3cc5b90ac84c2..9c894d9c411ed 100644 --- a/x-pack/platform/plugins/private/translations/translations/fr-FR.json +++ b/x-pack/platform/plugins/private/translations/translations/fr-FR.json @@ -21965,7 +21965,6 @@ "xpack.infra.analysisSetup.steps.setupProcess.viewResultsButton": "Afficher les résultats", "xpack.infra.analysisSetup.timeRangeDescription": "Par défaut, le Machine Learning analyse les messages de logs dans vos index de logs n'excédant pas quatre semaines et continue indéfiniment. Vous pouvez spécifier une date de début, de fin ou les deux différente.", "xpack.infra.analysisSetup.timeRangeTitle": "Choisir une plage temporelle", - "xpack.infra.appName": "Logs Infra", "xpack.infra.assetDetails.alerts.tooltip.alertsLabel": "Affichage des alertes pour cet hôte. Vous pouvez créer et gérer les alertes dans {alerts}", "xpack.infra.assetDetails.alerts.tooltip.documentationLabel": "Pour en savoir plus, consultez la {documentation}", "xpack.infra.assetDetails.alerts.tooltip.documentationLink": "documentation", @@ -22375,7 +22374,6 @@ "xpack.infra.logs.analysis.logEntryCategoriesModuleName": "Catégorisation", "xpack.infra.logs.analysis.logEntryExamplesViewAnomalyInMlLabel": "Afficher l'anomalie dans le Machine Learning", "xpack.infra.logs.analysis.logEntryExamplesViewDetailsLabel": "Afficher les détails", - "xpack.infra.logs.analysis.logEntryExamplesViewInStreamLabel": "Afficher dans le flux", "xpack.infra.logs.analysis.logEntryRateModuleDescription": "Utilisez le Machine Learning pour détecter automatiquement les taux d'entrées de logs anormaux.", "xpack.infra.logs.analysis.logEntryRateModuleName": "Taux du log", "xpack.infra.logs.analysis.manageMlJobsButtonLabel": "Gérer les tâches ML", @@ -22399,23 +22397,9 @@ "xpack.infra.logs.analysisPage.unavailable.mlAppLink": "Application de Machine Learning", "xpack.infra.logs.anomaliesPageTitle": "Anomalies", "xpack.infra.logs.categoryExample.viewInContextText": "Afficher en contexte", - "xpack.infra.logs.categoryExample.viewInStreamText": "Afficher dans le flux", - "xpack.infra.logs.common.invalidStateCalloutTitle": "État non valide rencontré", - "xpack.infra.logs.common.invalidStateMessage": "Impossible de traiter l'état {stateValue}.", - "xpack.infra.logs.customizeLogs.customizeButtonLabel": "Personnaliser", - "xpack.infra.logs.customizeLogs.lineWrappingFormRowLabel": "Retour automatique à la ligne", - "xpack.infra.logs.customizeLogs.textSizeFormRowLabel": "Taille du texte", - "xpack.infra.logs.customizeLogs.textSizeRadioGroup": "{textScale, select, small {Petit} medium {Moyen} large {Grand} other {{textScale}} }", - "xpack.infra.logs.customizeLogs.wrapLongLinesSwitchLabel": "Formater les longues lignes", - "xpack.infra.logs.highlights.clearHighlightTermsButtonLabel": "Effacer les termes à mettre en surbrillance", - "xpack.infra.logs.highlights.goToNextHighlightButtonLabel": "Passer au surlignage suivant", - "xpack.infra.logs.highlights.goToPreviousHighlightButtonLabel": "Passer au surlignage précédent", - "xpack.infra.logs.highlights.highlightsPopoverButtonLabel": "Surlignages", - "xpack.infra.logs.highlights.highlightTermsFieldLabel": "Termes à mettre en surbrillance", "xpack.infra.logs.index.anomaliesTabTitle": "Anomalies des logs", "xpack.infra.logs.index.logCategoriesBetaBadgeTitle": "Catégories de logs", "xpack.infra.logs.index.settingsTabTitle": "Paramètres", - "xpack.infra.logs.index.streamTabTitle": "Flux", "xpack.infra.logs.logCategoriesTitle": "Catégories", "xpack.infra.logs.logEntryCategories.analyzeCategoryInMlButtonLabel": "Analyse dans ML", "xpack.infra.logs.logEntryCategories.analyzeCategoryInMlTooltipDescription": "Analysez cette catégorie dans l'application ML.", @@ -22451,42 +22435,10 @@ "xpack.infra.logs.noDataConfig.beatsCard.title": "Ajouter une intégration au logging", "xpack.infra.logs.noDataConfig.solutionName": "Observabilité", "xpack.infra.logs.pluginTitle": "Logs", - "xpack.infra.logs.search.nextButtonLabel": "Suivant", - "xpack.infra.logs.search.previousButtonLabel": "Précédent", - "xpack.infra.logs.search.searchInLogsAriaLabel": "rechercher", - "xpack.infra.logs.search.searchInLogsPlaceholder": "Recherche", - "xpack.infra.logs.searchResultTooltip": "{bucketCount, plural, one {# entrée mise en surbrillance} other {# entrées mises en surbrillance}}", - "xpack.infra.logs.settings.inlineLogViewCalloutButtonText": "Revenir à la vue par défaut du flux de logs", - "xpack.infra.logs.settings.inlineLogViewCalloutWidgetDescription": "Les modifications seront synchronisées avec l'URL, mais elles ne seront pas conservées dans la vue par défaut du flux de logs.", - "xpack.infra.logs.settings.inlineLogViewCalloutWidgetTitle": "Vous configurez un widget intégré", - "xpack.infra.logs.startStreamingButtonLabel": "Diffuser en direct", - "xpack.infra.logs.stopStreamingButtonLabel": "Arrêter la diffusion", - "xpack.infra.logs.streamPageTitle": "Flux", "xpack.infra.logs.viewInContext.logsFromContainerTitle": "Les logs affichés proviennent du conteneur {container}", "xpack.infra.logs.viewInContext.logsFromFileTitle": "Les logs affichés proviennent du fichier {file} et de l'hôte {host}", "xpack.infra.logsDeprecationCallout.euiCallOut.discoverANewLogLabel": "Il existe une nouvelle (et meilleure) façon d'explorer vos logs !", "xpack.infra.logsHeaderAddDataButtonLabel": "Ajouter des données", - "xpack.infra.logSourceConfiguration.childFormElementErrorMessage": "L'état d'au moins un champ du formulaire est non valide.", - "xpack.infra.logSourceConfiguration.dataViewDescription": "Vue de données contenant les données de log", - "xpack.infra.logSourceConfiguration.dataViewLabel": "Vue de données de log", - "xpack.infra.logSourceConfiguration.dataViewSectionTitle": "Vue de données (déclassée)", - "xpack.infra.logSourceConfiguration.dataViewSelectorPlaceholder": "Choisir une vue de données", - "xpack.infra.logSourceConfiguration.dataViewsManagementLinkText": "écran de gestion des vues de données", - "xpack.infra.logSourceConfiguration.dataViewTitle": "Vue de données de log", - "xpack.infra.logSourceConfiguration.emptyColumnListErrorMessage": "La liste de colonnes ne doit pas être vide.", - "xpack.infra.logSourceConfiguration.emptyFieldErrorMessage": "Le champ \"{fieldName}\" ne doit pas être vide.", - "xpack.infra.logSourceConfiguration.includesSpacesErrorMessage": "Le champ \"{fieldName}\" ne doit pas contenir d’espaces.", - "xpack.infra.logSourceConfiguration.invalidMessageFieldTypeErrorMessage": "Le champ {messageField} doit être un champ textuel.", - "xpack.infra.logSourceConfiguration.kibanaAdvancedSettingSectionTitle": "Paramètres avancés des sources de logs Kibana", - "xpack.infra.logSourceConfiguration.logDataViewHelpText": "Les vues de données sont partagées entre les applications dans l'espace Kibana et peuvent être gérées via l’{dataViewsManagementLink}. Une vue de données peut cibler plusieurs index.", - "xpack.infra.logSourceConfiguration.logSourceConfigurationFormErrorsCalloutTitle": "Configuration de la source incohérente", - "xpack.infra.logSourceConfiguration.logSourcesTitle": "Sources de log", - "xpack.infra.logSourceConfiguration.missingDataViewErrorMessage": "La vue de données {dataViewId} doit exister.", - "xpack.infra.logSourceConfiguration.missingDataViewsLabel": "Vue de données {indexPatternId} manquante", - "xpack.infra.logSourceConfiguration.missingMessageFieldErrorMessage": "La vue de données doit contenir un champ {messageField}.", - "xpack.infra.logSourceConfiguration.missingTimestampFieldErrorMessage": "La vue de données doit être basée sur le temps.", - "xpack.infra.logSourceConfiguration.rollupIndexPatternErrorMessage": "La vue de données ne doit pas être un modèle d'indexation de cumul.", - "xpack.infra.logSourceConfiguration.unsavedFormPromptMessage": "Voulez-vous vraiment quitter ? Les modifications seront perdues", "xpack.infra.logSourceErrorPage.failedToLoadSourceMessage": "Des erreurs se sont produites lors du chargement de la configuration. Réessayez ou modifiez la configuration pour résoudre le problème.", "xpack.infra.logSourceErrorPage.failedToLoadSourceTitle": "Impossible de charger la configuration", "xpack.infra.logSourceErrorPage.fetchLogSourceConfigurationErrorTitle": "Impossible de charger la configuration de la source de logs", @@ -22495,13 +22447,7 @@ "xpack.infra.logSourceErrorPage.resolveLogSourceConfigurationErrorTitle": "Impossible de résoudre la configuration de la source de logs", "xpack.infra.logSourceErrorPage.savedObjectNotFoundErrorMessage": "Impossible de localiser ce {savedObjectType} : {savedObjectId}", "xpack.infra.logSourceErrorPage.tryAgainButtonLabel": "Réessayer", - "xpack.infra.logsPage.toolbar.kqlSearchFieldPlaceholder": "Recherche d'entrées de log… (par ex. host.name:host-1)", - "xpack.infra.logsPage.toolbar.logFilterErrorToastTitle": "Erreur de filtrage du log", - "xpack.infra.logsSettingsPage.loadingButtonLabel": "Chargement", "xpack.infra.logsStreamEmbeddable.deprecationWarningDescription": "La maintenance des panneaux de flux de logs n'est plus assurée. Essayez d'utiliser {savedSearchDocsLink} pour une visualisation similaire.", - "xpack.infra.logStreamPageTemplate.backtoLogsStream": "Retour au flux de logs", - "xpack.infra.logStreamPageTemplate.widgetBadge": "Widget", - "xpack.infra.logStreamPageTemplate.widgetDescription": "Vous visionnez un widget intégré. Les modifications seront synchronisées avec l'URL, mais elles ne seront pas conservées dans la vue par défaut du flux de logs.", "xpack.infra.metadata.pinAriaLabel": "Champ épinglé", "xpack.infra.metadataEmbeddable.AddFilterAriaLabel": "Ajouter un filtre", "xpack.infra.metadataEmbeddable.errorAction": "récupérer de nouveau les métadonnées", @@ -22856,9 +22802,6 @@ "xpack.infra.ml.aomalyFlyout.jobSetup.flyoutHeader": "Activer le Machine Learning pour {nodeType}", "xpack.infra.ml.metricsHostModuleDescription": "Utilisez le Machine Learning pour détecter automatiquement les taux d'entrées de logs anormaux.", "xpack.infra.ml.metricsModuleName": "Détection des anomalies d'indicateurs", - "xpack.infra.ml.splash.inlineLogView.buttonText": "Revenir à la vue de log (persistante) par défaut", - "xpack.infra.ml.splash.inlineLogView.description": "Cette fonctionnalité ne prend pas en charge les vues de journal en ligne.", - "xpack.infra.ml.splash.inlineLogView.title": "Basculer vers la vue de log persistante", "xpack.infra.ml.splash.loadingMessage": "Vérification de la licence...", "xpack.infra.ml.splash.startTrialCta": "Démarrer l'essai", "xpack.infra.ml.splash.startTrialDescription": "Notre essai gratuit inclut les fonctionnalités de Machine Learning, qui vous permettent de détecter les anomalies dans vos logs.", @@ -22949,28 +22892,14 @@ "xpack.infra.savedView.updateView": "Mettre à jour la vue", "xpack.infra.showHistory": "Afficher l'historique", "xpack.infra.snapshot.missingSnapshotMetricError": "L'agrégation de {metric} pour {nodeType} n'est pas disponible.", - "xpack.infra.sourceConfiguration.addLogColumnButtonLabel": "Ajouter une colonne", "xpack.infra.sourceConfiguration.anomalyThresholdDescription": "Définit le score de sévérité minimal requis pour afficher les anomalies dans l'application Metrics.", "xpack.infra.sourceConfiguration.anomalyThresholdLabel": "Score de sévérité minimal", "xpack.infra.sourceConfiguration.anomalyThresholdTitle": "Seuil de sévérité d'anomalie", - "xpack.infra.sourceConfiguration.applySettingsButtonLabel": "Appliquer", - "xpack.infra.sourceConfiguration.discardSettingsButtonLabel": "Abandonner", "xpack.infra.sourceConfiguration.featuresSectionTitle": "Fonctionnalités", "xpack.infra.sourceConfiguration.fieldContainEmptyEntryErrorMessage": "Le champ ne doit pas inclure de valeurs vides séparées par des virgules.", "xpack.infra.sourceConfiguration.fieldContainSpacesErrorMessage": "Le champ ne doit pas contenir d'espaces.", "xpack.infra.sourceConfiguration.fieldEmptyErrorMessage": "Le champ ne doit pas être vide.", - "xpack.infra.sourceConfiguration.fieldLogColumnTitle": "Champ", "xpack.infra.sourceConfiguration.indicesSectionTitle": "Index", - "xpack.infra.sourceConfiguration.logColumnsSectionTitle": "Colonnes de log", - "xpack.infra.sourceConfiguration.logIndices.viewAffectedRulesLink": "Afficher les règles affectées", - "xpack.infra.sourceConfiguration.logIndicesDescription": "Modèle d'indexation pour la correspondance d'index contenant des données de log", - "xpack.infra.sourceConfiguration.logIndicesLabel": "Index de log", - "xpack.infra.sourceConfiguration.logIndicesRecommendedValue": "La valeur recommandée est {defaultValue}", - "xpack.infra.sourceConfiguration.logIndicesTitle": "Index de log", - "xpack.infra.sourceConfiguration.logsIndicesSectionTitle": "Index (déclassés)", - "xpack.infra.sourceConfiguration.logsIndicesUsedByRulesMessage": "Une ou plusieurs règles d’alerte reposent sur ce paramètre de source de données. La modification de ce paramètre modifiera les données utilisées pour générer des alertes.", - "xpack.infra.sourceConfiguration.logsIndicesUsedByRulesTitle": "Les règles d'alerte utilisent ce paramètre de source de données", - "xpack.infra.sourceConfiguration.messageLogColumnDescription": "Ce champ système affiche le message d'entrée de log dérivé des champs de document.", "xpack.infra.sourceConfiguration.metricIndices.viewAffectedRulesLink": "Afficher les règles affectées", "xpack.infra.sourceConfiguration.metricIndicesDescription": "Modèle d'indexation pour la correspondance d'index contenant des données d'indicateurs", "xpack.infra.sourceConfiguration.metricIndicesDoNotExist": "Impossible de trouver des données d’indicateurs car le modèle saisi ne correspond à aucun index.", @@ -22985,16 +22914,11 @@ "xpack.infra.sourceConfiguration.nameDescription": "Nom descriptif pour la configuration de la source", "xpack.infra.sourceConfiguration.nameLabel": "Nom", "xpack.infra.sourceConfiguration.nameSectionTitle": "Nom", - "xpack.infra.sourceConfiguration.noLogColumnsDescription": "Ajoutez une colonne à cette liste à l'aide du bouton ci-dessus.", - "xpack.infra.sourceConfiguration.noLogColumnsTitle": "Aucune colonne", "xpack.infra.sourceConfiguration.noRemoteClusterMessage": "Nous ne parvenons pas à nous connecter au cluster distant, ce qui nous empêche de récupérer les indicateurs et les données dont vous avez besoin. Pour résoudre ce problème, vérifiez la configuration de vos index et assurez-vous qu'elle est correctement effectuée.", "xpack.infra.sourceConfiguration.noRemoteClusterTitle": "Impossible de se connecter au serveur distant", "xpack.infra.sourceConfiguration.remoteClusterConnectionDoNotExist": "Vérifiez que le cluster distant est disponible ou que les paramètres de connexion à distance sont corrects.", "xpack.infra.sourceConfiguration.remoteClusterConnectionDoNotExistTitle": "Impossible de se connecter au cluster distant", - "xpack.infra.sourceConfiguration.removeLogColumnButtonLabel": "Retirer la colonne {columnDescription}", "xpack.infra.sourceConfiguration.saveButton": "Enregistrer les modifications", - "xpack.infra.sourceConfiguration.systemColumnBadgeLabel": "Système", - "xpack.infra.sourceConfiguration.timestampLogColumnDescription": "Ce champ système affiche l'heure de l'entrée de log telle que déterminée par le paramètre du champ {timestampSetting}.", "xpack.infra.sourceConfiguration.unsavedFormPrompt": "Voulez-vous vraiment quitter ? Les modifications seront perdues", "xpack.infra.sourceConfiguration.updateFailureBody": "Nous n'avons pas pu appliquer les modifications à la configuration des indicateurs. Réessayez plus tard.", "xpack.infra.sourceConfiguration.updateFailureTitle": "La mise à jour de la configuration a échoué", diff --git a/x-pack/platform/plugins/private/translations/translations/ja-JP.json b/x-pack/platform/plugins/private/translations/translations/ja-JP.json index b3350c31ef305..4a003c530cea9 100644 --- a/x-pack/platform/plugins/private/translations/translations/ja-JP.json +++ b/x-pack/platform/plugins/private/translations/translations/ja-JP.json @@ -21826,7 +21826,6 @@ "xpack.infra.analysisSetup.steps.setupProcess.viewResultsButton": "結果を表示", "xpack.infra.analysisSetup.timeRangeDescription": "デフォルトで、機械学習は 4 週間以内のログインデックスのログメッセージを分析し、永久に継続します。別の開始日、終了日、または両方を指定できます。", "xpack.infra.analysisSetup.timeRangeTitle": "時間範囲の選択", - "xpack.infra.appName": "インフラログ", "xpack.infra.assetDetails.alerts.tooltip.alertsLabel": "このホストのアラートを表示しています。{alerts}でアラートを作成および管理できます", "xpack.infra.assetDetails.alerts.tooltip.documentationLabel": "詳細については、{documentation}を参照してください", "xpack.infra.assetDetails.alerts.tooltip.documentationLink": "ドキュメンテーション", @@ -22238,7 +22237,6 @@ "xpack.infra.logs.analysis.logEntryCategoriesModuleName": "カテゴリー分け", "xpack.infra.logs.analysis.logEntryExamplesViewAnomalyInMlLabel": "機械学習で異常を表示", "xpack.infra.logs.analysis.logEntryExamplesViewDetailsLabel": "詳細を表示", - "xpack.infra.logs.analysis.logEntryExamplesViewInStreamLabel": "ストリームで表示", "xpack.infra.logs.analysis.logEntryRateModuleDescription": "機械学習を使用して自動的に異常ログエントリ率を検出します。", "xpack.infra.logs.analysis.logEntryRateModuleName": "ログレート", "xpack.infra.logs.analysis.manageMlJobsButtonLabel": "MLジョブの管理", @@ -22262,23 +22260,9 @@ "xpack.infra.logs.analysisPage.unavailable.mlAppLink": "機械学習アプリ", "xpack.infra.logs.anomaliesPageTitle": "異常", "xpack.infra.logs.categoryExample.viewInContextText": "コンテキストで表示", - "xpack.infra.logs.categoryExample.viewInStreamText": "ストリームで表示", - "xpack.infra.logs.common.invalidStateCalloutTitle": "無効な状態が発生しました", - "xpack.infra.logs.common.invalidStateMessage": "状態{stateValue}を処理できません。", - "xpack.infra.logs.customizeLogs.customizeButtonLabel": "カスタマイズ", - "xpack.infra.logs.customizeLogs.lineWrappingFormRowLabel": "改行", - "xpack.infra.logs.customizeLogs.textSizeFormRowLabel": "テキストサイズ", - "xpack.infra.logs.customizeLogs.textSizeRadioGroup": "{textScale, select, small {小} Medium {中} Large {大} other {{textScale}} }", - "xpack.infra.logs.customizeLogs.wrapLongLinesSwitchLabel": "長い行を改行", - "xpack.infra.logs.highlights.clearHighlightTermsButtonLabel": "ハイライトする用語をクリア", - "xpack.infra.logs.highlights.goToNextHighlightButtonLabel": "次のハイライトにスキップ", - "xpack.infra.logs.highlights.goToPreviousHighlightButtonLabel": "前のハイライトにスキップ", - "xpack.infra.logs.highlights.highlightsPopoverButtonLabel": "ハイライト", - "xpack.infra.logs.highlights.highlightTermsFieldLabel": "ハイライトする用語", "xpack.infra.logs.index.anomaliesTabTitle": "Logs異常", "xpack.infra.logs.index.logCategoriesBetaBadgeTitle": "Logsカテゴリ", "xpack.infra.logs.index.settingsTabTitle": "設定", - "xpack.infra.logs.index.streamTabTitle": "ストリーム", "xpack.infra.logs.logCategoriesTitle": "カテゴリー", "xpack.infra.logs.logEntryCategories.analyzeCategoryInMlButtonLabel": "ML で分析", "xpack.infra.logs.logEntryCategories.analyzeCategoryInMlTooltipDescription": "ML アプリでこのカテゴリーを分析します。", @@ -22314,41 +22298,10 @@ "xpack.infra.logs.noDataConfig.beatsCard.title": "ロギング統合を追加", "xpack.infra.logs.noDataConfig.solutionName": "Observability", "xpack.infra.logs.pluginTitle": "ログ", - "xpack.infra.logs.search.nextButtonLabel": "次へ", - "xpack.infra.logs.search.previousButtonLabel": "前へ", - "xpack.infra.logs.search.searchInLogsAriaLabel": "検索", - "xpack.infra.logs.search.searchInLogsPlaceholder": "検索", - "xpack.infra.logs.searchResultTooltip": "{bucketCount, plural, other {# 件のハイライトされたエントリー}}", - "xpack.infra.logs.settings.inlineLogViewCalloutButtonText": "デフォルトログストリームビューに戻す", - "xpack.infra.logs.settings.inlineLogViewCalloutWidgetDescription": "変更はURLと同期されますが、デフォルトログストリームビューには永続しません。", - "xpack.infra.logs.settings.inlineLogViewCalloutWidgetTitle": "埋め込まれたウィジェットを構成しています", - "xpack.infra.logs.startStreamingButtonLabel": "ライブストリーム", - "xpack.infra.logs.stopStreamingButtonLabel": "ストリーム停止", - "xpack.infra.logs.streamPageTitle": "ストリーム", "xpack.infra.logs.viewInContext.logsFromContainerTitle": "表示されたログはコンテナー{container}から取得されました", "xpack.infra.logs.viewInContext.logsFromFileTitle": "表示されたログは、ファイル{file}およびホスト{host}から取得されました", "xpack.infra.logsDeprecationCallout.euiCallOut.discoverANewLogLabel": "ログの探索には、新しく、もっと効果的な方法があります。", "xpack.infra.logsHeaderAddDataButtonLabel": "データの追加", - "xpack.infra.logSourceConfiguration.childFormElementErrorMessage": "1つ以上のフォームフィールドが無効な状態です。", - "xpack.infra.logSourceConfiguration.dataViewDescription": "ログデータを含むデータビュー", - "xpack.infra.logSourceConfiguration.dataViewLabel": "ログデータビュー", - "xpack.infra.logSourceConfiguration.dataViewSectionTitle": "データビュー(廃止予定)", - "xpack.infra.logSourceConfiguration.dataViewSelectorPlaceholder": "データビューを選択", - "xpack.infra.logSourceConfiguration.dataViewsManagementLinkText": "データビュー管理画面", - "xpack.infra.logSourceConfiguration.dataViewTitle": "ログデータビュー", - "xpack.infra.logSourceConfiguration.emptyColumnListErrorMessage": "列リストは未入力のままにできません。", - "xpack.infra.logSourceConfiguration.emptyFieldErrorMessage": "フィールド''{fieldName}''は未入力のままにできません。", - "xpack.infra.logSourceConfiguration.includesSpacesErrorMessage": "フィールド''{fieldName}''にはスペースを入力できません。", - "xpack.infra.logSourceConfiguration.invalidMessageFieldTypeErrorMessage": "{messageField}フィールドはテキストフィールドでなければなりません。", - "xpack.infra.logSourceConfiguration.kibanaAdvancedSettingSectionTitle": "Kibanaログソース詳細設定", - "xpack.infra.logSourceConfiguration.logSourceConfigurationFormErrorsCalloutTitle": "一貫しないソース構成", - "xpack.infra.logSourceConfiguration.logSourcesTitle": "ログソース", - "xpack.infra.logSourceConfiguration.missingDataViewErrorMessage": "データビュー{dataViewId}が存在する必要があります。", - "xpack.infra.logSourceConfiguration.missingDataViewsLabel": "データビュー{indexPatternId}が見つかりません", - "xpack.infra.logSourceConfiguration.missingMessageFieldErrorMessage": "データビューには{messageField}フィールドが必要です。", - "xpack.infra.logSourceConfiguration.missingTimestampFieldErrorMessage": "データビューは時間に基づく必要があります。", - "xpack.infra.logSourceConfiguration.rollupIndexPatternErrorMessage": "データビューがロールアップインデックスパターンであってはなりません。", - "xpack.infra.logSourceConfiguration.unsavedFormPromptMessage": "終了してよろしいですか?変更内容は失われます", "xpack.infra.logSourceErrorPage.failedToLoadSourceMessage": "構成の読み込み試行中にエラーが発生しました。再試行するか、構成を変更して問題を修正してください。", "xpack.infra.logSourceErrorPage.failedToLoadSourceTitle": "構成を読み込めませんでした", "xpack.infra.logSourceErrorPage.fetchLogSourceConfigurationErrorTitle": "ログソース構成を読み込めませんでした", @@ -22357,13 +22310,7 @@ "xpack.infra.logSourceErrorPage.resolveLogSourceConfigurationErrorTitle": "ログソース構成を解決できませんでした", "xpack.infra.logSourceErrorPage.savedObjectNotFoundErrorMessage": "{savedObjectType}:{savedObjectId}が見つかりませんでした", "xpack.infra.logSourceErrorPage.tryAgainButtonLabel": "再試行", - "xpack.infra.logsPage.toolbar.kqlSearchFieldPlaceholder": "ログエントリーを検索中…(例:host.name:host-1)", - "xpack.infra.logsPage.toolbar.logFilterErrorToastTitle": "ログフィルターエラー", - "xpack.infra.logsSettingsPage.loadingButtonLabel": "読み込み中", "xpack.infra.logsStreamEmbeddable.deprecationWarningDescription": "ログストリームパネルは管理されていません。{savedSearchDocsLink}を同様の視覚化に活用してください。", - "xpack.infra.logStreamPageTemplate.backtoLogsStream": "ログストリームに戻る", - "xpack.infra.logStreamPageTemplate.widgetBadge": "ウィジェット", - "xpack.infra.logStreamPageTemplate.widgetDescription": "埋め込まれたウィジェットを表示しています。変更はURLと同期されますが、デフォルトログストリームビューには永続しません。", "xpack.infra.metadata.pinAriaLabel": "固定されたフィールド", "xpack.infra.metadataEmbeddable.AddFilterAriaLabel": "フィルターを追加", "xpack.infra.metadataEmbeddable.errorAction": "メタデータを再取得", @@ -22717,9 +22664,6 @@ "xpack.infra.ml.aomalyFlyout.jobSetup.flyoutHeader": "{nodeType}の機械学習を有効にする", "xpack.infra.ml.metricsHostModuleDescription": "機械学習を使用して自動的に異常ログエントリ率を検出します。", "xpack.infra.ml.metricsModuleName": "メトリック異常検知", - "xpack.infra.ml.splash.inlineLogView.buttonText": "デフォルト(永続)ログビューに戻す", - "xpack.infra.ml.splash.inlineLogView.description": "この機能はインラインログビューをサポートしていません", - "xpack.infra.ml.splash.inlineLogView.title": "永続ログビューに切り替える", "xpack.infra.ml.splash.loadingMessage": "ライセンスを確認しています...", "xpack.infra.ml.splash.startTrialCta": "トライアルを開始", "xpack.infra.ml.splash.startTrialDescription": "無料の試用版には、機械学習機能が含まれており、ログで異常を検出することができます。", @@ -22809,28 +22753,14 @@ "xpack.infra.savedView.updateView": "ビューの更新", "xpack.infra.showHistory": "履歴を表示", "xpack.infra.snapshot.missingSnapshotMetricError": "{nodeType}の{metric}の集約を使用できません。", - "xpack.infra.sourceConfiguration.addLogColumnButtonLabel": "列を追加", "xpack.infra.sourceConfiguration.anomalyThresholdDescription": "メトリックアプリケーションで異常値を表示するために必要な最低重要度スコアを設定します。", "xpack.infra.sourceConfiguration.anomalyThresholdLabel": "最低重要度スコア", "xpack.infra.sourceConfiguration.anomalyThresholdTitle": "異常重要度しきい値", - "xpack.infra.sourceConfiguration.applySettingsButtonLabel": "適用", - "xpack.infra.sourceConfiguration.discardSettingsButtonLabel": "破棄", "xpack.infra.sourceConfiguration.featuresSectionTitle": "機能", "xpack.infra.sourceConfiguration.fieldContainEmptyEntryErrorMessage": "フィールドには空のカンマ区切り値を含めることはできません。", "xpack.infra.sourceConfiguration.fieldContainSpacesErrorMessage": "フィールドにはスペースを入力できません。", "xpack.infra.sourceConfiguration.fieldEmptyErrorMessage": "このフィールドは未入力のままにできません。", - "xpack.infra.sourceConfiguration.fieldLogColumnTitle": "フィールド", "xpack.infra.sourceConfiguration.indicesSectionTitle": "インデックス", - "xpack.infra.sourceConfiguration.logColumnsSectionTitle": "ログ列", - "xpack.infra.sourceConfiguration.logIndices.viewAffectedRulesLink": "影響を受けるルールを表示", - "xpack.infra.sourceConfiguration.logIndicesDescription": "ログデータを含む一致するインデックスのインデックスパターンです", - "xpack.infra.sourceConfiguration.logIndicesLabel": "ログインデックス", - "xpack.infra.sourceConfiguration.logIndicesRecommendedValue": "推奨値は {defaultValue} です", - "xpack.infra.sourceConfiguration.logIndicesTitle": "ログインデックス", - "xpack.infra.sourceConfiguration.logsIndicesSectionTitle": "インデックス(廃止予定)", - "xpack.infra.sourceConfiguration.logsIndicesUsedByRulesMessage": "1つ以上のアラートルールがこのデータソース設定に依存しています。この設定を変更すると、アラートを生成するために使用されるデータが変更されます。", - "xpack.infra.sourceConfiguration.logsIndicesUsedByRulesTitle": "アラートルールはこのデータソース設定を使用します。", - "xpack.infra.sourceConfiguration.messageLogColumnDescription": "このシステムフィールドは、ドキュメントフィールドから取得されたログエントリーメッセージを表示します。", "xpack.infra.sourceConfiguration.metricIndices.viewAffectedRulesLink": "影響を受けるルールを表示", "xpack.infra.sourceConfiguration.metricIndicesDescription": "メトリックデータを含む一致するインデックスのインデックスパターンです", "xpack.infra.sourceConfiguration.metricIndicesDoNotExist": "入力されたパターンがインデックスと一致しないため、メトリックデータを検索できませんでした。", @@ -22845,16 +22775,11 @@ "xpack.infra.sourceConfiguration.nameDescription": "ソース構成を説明する名前です", "xpack.infra.sourceConfiguration.nameLabel": "名前", "xpack.infra.sourceConfiguration.nameSectionTitle": "名前", - "xpack.infra.sourceConfiguration.noLogColumnsDescription": "上のボタンでこのリストに列を追加します。", - "xpack.infra.sourceConfiguration.noLogColumnsTitle": "列がありません", "xpack.infra.sourceConfiguration.noRemoteClusterMessage": "リモートクラスターに接続できません。このため、必要なメトリックとデータを取得できません。この問題を解決するには、インデックス構成を確認し、構成が正しいことを確かめてください。", "xpack.infra.sourceConfiguration.noRemoteClusterTitle": "リモートクラスターに接続できませんでした", "xpack.infra.sourceConfiguration.remoteClusterConnectionDoNotExist": "リモートクラスターが利用可能であるか、リモート接続の設定が正しいことを確認します。", "xpack.infra.sourceConfiguration.remoteClusterConnectionDoNotExistTitle": "リモートクラスターに接続できませんでした", - "xpack.infra.sourceConfiguration.removeLogColumnButtonLabel": "{columnDescription} 列を削除", "xpack.infra.sourceConfiguration.saveButton": "変更を保存", - "xpack.infra.sourceConfiguration.systemColumnBadgeLabel": "システム", - "xpack.infra.sourceConfiguration.timestampLogColumnDescription": "このシステムフィールドは、{timestampSetting} フィールド設定から判断されたログエントリーの時刻を表示します。", "xpack.infra.sourceConfiguration.unsavedFormPrompt": "終了してよろしいですか?変更内容は失われます", "xpack.infra.sourceConfiguration.updateFailureBody": "変更をメトリック構成に適用できませんでした。しばらくたってから再試行してください。", "xpack.infra.sourceConfiguration.updateFailureTitle": "構成の更新が失敗しました", diff --git a/x-pack/platform/plugins/private/translations/translations/zh-CN.json b/x-pack/platform/plugins/private/translations/translations/zh-CN.json index 515d8bb135659..f98bbce64ce64 100644 --- a/x-pack/platform/plugins/private/translations/translations/zh-CN.json +++ b/x-pack/platform/plugins/private/translations/translations/zh-CN.json @@ -21475,7 +21475,6 @@ "xpack.infra.analysisSetup.steps.setupProcess.viewResultsButton": "查看结果", "xpack.infra.analysisSetup.timeRangeDescription": "默认情况下,Machine Learning 分析日志索引中不超过 4 周的日志消息,并无限持续下去。您可以指定不同的开始日期或/和结束日期。", "xpack.infra.analysisSetup.timeRangeTitle": "选择时间范围", - "xpack.infra.appName": "基础架构日志", "xpack.infra.assetDetails.alerts.tooltip.alertsLabel": "正在显示此主机的告警。可以在 {alerts} 中创建和管理告警", "xpack.infra.assetDetails.alerts.tooltip.documentationLabel": "请参阅{documentation}了解更多信息", "xpack.infra.assetDetails.alerts.tooltip.documentationLink": "文档", @@ -21879,7 +21878,6 @@ "xpack.infra.logs.analysis.logEntryCategoriesModuleName": "归类", "xpack.infra.logs.analysis.logEntryExamplesViewAnomalyInMlLabel": "在 Machine Learning 中查看异常", "xpack.infra.logs.analysis.logEntryExamplesViewDetailsLabel": "查看详情", - "xpack.infra.logs.analysis.logEntryExamplesViewInStreamLabel": "在流中查看", "xpack.infra.logs.analysis.logEntryRateModuleDescription": "使用 Machine Learning 自动检测异常日志条目速率。", "xpack.infra.logs.analysis.logEntryRateModuleName": "日志速率", "xpack.infra.logs.analysis.manageMlJobsButtonLabel": "管理 ML 作业", @@ -21903,23 +21901,9 @@ "xpack.infra.logs.analysisPage.unavailable.mlAppLink": "Machine Learning 应用", "xpack.infra.logs.anomaliesPageTitle": "异常", "xpack.infra.logs.categoryExample.viewInContextText": "在上下文中查看", - "xpack.infra.logs.categoryExample.viewInStreamText": "在流中查看", - "xpack.infra.logs.common.invalidStateCalloutTitle": "遇到无效状态", - "xpack.infra.logs.common.invalidStateMessage": "无法处理状态 {stateValue}。", - "xpack.infra.logs.customizeLogs.customizeButtonLabel": "定制", - "xpack.infra.logs.customizeLogs.lineWrappingFormRowLabel": "换行", - "xpack.infra.logs.customizeLogs.textSizeFormRowLabel": "文本大小", - "xpack.infra.logs.customizeLogs.textSizeRadioGroup": "{textScale, select, small {小} medium {Medium} large {Large} other {{textScale}} }", - "xpack.infra.logs.customizeLogs.wrapLongLinesSwitchLabel": "长行换行", - "xpack.infra.logs.highlights.clearHighlightTermsButtonLabel": "清除要突出显示的词", - "xpack.infra.logs.highlights.goToNextHighlightButtonLabel": "跳转到下一高亮条目", - "xpack.infra.logs.highlights.goToPreviousHighlightButtonLabel": "跳转到上一高亮条目", - "xpack.infra.logs.highlights.highlightsPopoverButtonLabel": "突出显示", - "xpack.infra.logs.highlights.highlightTermsFieldLabel": "要突出显示的词", "xpack.infra.logs.index.anomaliesTabTitle": "日志异常", "xpack.infra.logs.index.logCategoriesBetaBadgeTitle": "日志类别", "xpack.infra.logs.index.settingsTabTitle": "设置", - "xpack.infra.logs.index.streamTabTitle": "流式传输", "xpack.infra.logs.logCategoriesTitle": "类别", "xpack.infra.logs.logEntryCategories.analyzeCategoryInMlButtonLabel": "在 ML 中分析", "xpack.infra.logs.logEntryCategories.analyzeCategoryInMlTooltipDescription": "在 ML 应用中分析此类别。", @@ -21955,40 +21939,10 @@ "xpack.infra.logs.noDataConfig.beatsCard.title": "添加日志记录集成", "xpack.infra.logs.noDataConfig.solutionName": "Observability", "xpack.infra.logs.pluginTitle": "日志", - "xpack.infra.logs.search.nextButtonLabel": "下一步", - "xpack.infra.logs.search.previousButtonLabel": "上一页", - "xpack.infra.logs.search.searchInLogsAriaLabel": "搜索", - "xpack.infra.logs.search.searchInLogsPlaceholder": "搜索", - "xpack.infra.logs.searchResultTooltip": "{bucketCount, plural, other {# 个高亮条目}}", - "xpack.infra.logs.settings.inlineLogViewCalloutButtonText": "恢复为默认日志流视图", - "xpack.infra.logs.settings.inlineLogViewCalloutWidgetDescription": "更改将同步到 URL,但不会持续存在于默认日志流视图。", - "xpack.infra.logs.settings.inlineLogViewCalloutWidgetTitle": "您正在配置嵌入式小组件", - "xpack.infra.logs.startStreamingButtonLabel": "实时流式传输", - "xpack.infra.logs.stopStreamingButtonLabel": "停止流式传输", - "xpack.infra.logs.streamPageTitle": "流式传输", "xpack.infra.logs.viewInContext.logsFromContainerTitle": "显示的日志来自容器 {container}", "xpack.infra.logs.viewInContext.logsFromFileTitle": "显示的日志来自文件 {file} 和主机 {host}", "xpack.infra.logsDeprecationCallout.euiCallOut.discoverANewLogLabel": "这是浏览日志的更有效的新方法!", "xpack.infra.logsHeaderAddDataButtonLabel": "添加数据", - "xpack.infra.logSourceConfiguration.childFormElementErrorMessage": "至少一个表单字段处于无效状态。", - "xpack.infra.logSourceConfiguration.dataViewDescription": "包含日志数据的数据视图", - "xpack.infra.logSourceConfiguration.dataViewLabel": "日志数据视图", - "xpack.infra.logSourceConfiguration.dataViewSectionTitle": "数据视图(已过时)", - "xpack.infra.logSourceConfiguration.dataViewSelectorPlaceholder": "选择数据视图", - "xpack.infra.logSourceConfiguration.dataViewsManagementLinkText": "数据视图管理屏幕", - "xpack.infra.logSourceConfiguration.dataViewTitle": "日志数据视图", - "xpack.infra.logSourceConfiguration.emptyColumnListErrorMessage": "列列表不得为空。", - "xpack.infra.logSourceConfiguration.invalidMessageFieldTypeErrorMessage": "{messageField} 字段必须是文本字段。", - "xpack.infra.logSourceConfiguration.kibanaAdvancedSettingSectionTitle": "Kibana 日志源高级设置", - "xpack.infra.logSourceConfiguration.logDataViewHelpText": "数据视图在 Kibana 工作区中的应用间共享,并可以通过 {dataViewsManagementLink} 进行管理。单一数据视图可以针对多个索引。", - "xpack.infra.logSourceConfiguration.logSourceConfigurationFormErrorsCalloutTitle": "内容配置不一致", - "xpack.infra.logSourceConfiguration.logSourcesTitle": "日志源", - "xpack.infra.logSourceConfiguration.missingDataViewErrorMessage": "数据视图 {dataViewId} 必须存在。", - "xpack.infra.logSourceConfiguration.missingDataViewsLabel": "缺少数据视图 {indexPatternId}", - "xpack.infra.logSourceConfiguration.missingMessageFieldErrorMessage": "数据视图必须包含 {messageField} 字段。", - "xpack.infra.logSourceConfiguration.missingTimestampFieldErrorMessage": "数据视图必须基于时间。", - "xpack.infra.logSourceConfiguration.rollupIndexPatternErrorMessage": "数据视图不得为汇总/打包索引模式。", - "xpack.infra.logSourceConfiguration.unsavedFormPromptMessage": "是否确定要离开?更改将丢失", "xpack.infra.logSourceErrorPage.failedToLoadSourceMessage": "尝试加载配置时出错。请重试或更改配置以解决问题。", "xpack.infra.logSourceErrorPage.failedToLoadSourceTitle": "无法加载配置", "xpack.infra.logSourceErrorPage.fetchLogSourceConfigurationErrorTitle": "无法加载日志源配置", @@ -21997,13 +21951,7 @@ "xpack.infra.logSourceErrorPage.resolveLogSourceConfigurationErrorTitle": "无法解决日志源配置", "xpack.infra.logSourceErrorPage.savedObjectNotFoundErrorMessage": "无法找到该{savedObjectType}:{savedObjectId}", "xpack.infra.logSourceErrorPage.tryAgainButtonLabel": "重试", - "xpack.infra.logsPage.toolbar.kqlSearchFieldPlaceholder": "搜索日志条目……(例如 host.name:host-1)", - "xpack.infra.logsPage.toolbar.logFilterErrorToastTitle": "日志筛选错误", - "xpack.infra.logsSettingsPage.loadingButtonLabel": "正在加载", "xpack.infra.logsStreamEmbeddable.deprecationWarningDescription": "将不再维护日志流面板。尝试将 {savedSearchDocsLink} 用于类似可视化。", - "xpack.infra.logStreamPageTemplate.backtoLogsStream": "返回到日志流", - "xpack.infra.logStreamPageTemplate.widgetBadge": "小组件", - "xpack.infra.logStreamPageTemplate.widgetDescription": "您正在查看嵌入式小组件。更改将同步到 URL,但不会持续存在于默认日志流视图。", "xpack.infra.metadata.pinAriaLabel": "已固定字段", "xpack.infra.metadataEmbeddable.AddFilterAriaLabel": "添加筛选", "xpack.infra.metadataEmbeddable.errorAction": "重新提取元数据", @@ -22355,9 +22303,6 @@ "xpack.infra.ml.aomalyFlyout.jobSetup.flyoutHeader": "为 {nodeType} 启用 Machine Learning", "xpack.infra.ml.metricsHostModuleDescription": "使用 Machine Learning 自动检测异常日志条目速率。", "xpack.infra.ml.metricsModuleName": "指标异常检测", - "xpack.infra.ml.splash.inlineLogView.buttonText": "恢复到默认(持久化)日志视图", - "xpack.infra.ml.splash.inlineLogView.description": "此功能不支持内联日志视图", - "xpack.infra.ml.splash.inlineLogView.title": "切换到持久化日志视图", "xpack.infra.ml.splash.loadingMessage": "正在检查许可证......", "xpack.infra.ml.splash.startTrialCta": "开始试用", "xpack.infra.ml.splash.startTrialDescription": "我们的免费试用版包含 Machine Learning 功能,可用于检测日志中的异常。", @@ -22448,28 +22393,14 @@ "xpack.infra.savedView.updateView": "更新视图", "xpack.infra.showHistory": "显示历史记录", "xpack.infra.snapshot.missingSnapshotMetricError": "{nodeType} 的 {metric} 聚合不可用。", - "xpack.infra.sourceConfiguration.addLogColumnButtonLabel": "添加列", "xpack.infra.sourceConfiguration.anomalyThresholdDescription": "设置在 Metrics 应用程序中显示异常所需的最低严重性分数。", "xpack.infra.sourceConfiguration.anomalyThresholdLabel": "最低严重性分数", "xpack.infra.sourceConfiguration.anomalyThresholdTitle": "异常严重性阈值", - "xpack.infra.sourceConfiguration.applySettingsButtonLabel": "应用", - "xpack.infra.sourceConfiguration.discardSettingsButtonLabel": "丢弃", "xpack.infra.sourceConfiguration.featuresSectionTitle": "功能", "xpack.infra.sourceConfiguration.fieldContainEmptyEntryErrorMessage": "字段不得包含逗号分隔的空值。", "xpack.infra.sourceConfiguration.fieldContainSpacesErrorMessage": "字段不得包括空格。", "xpack.infra.sourceConfiguration.fieldEmptyErrorMessage": "字段不得为空。", - "xpack.infra.sourceConfiguration.fieldLogColumnTitle": "字段", "xpack.infra.sourceConfiguration.indicesSectionTitle": "索引", - "xpack.infra.sourceConfiguration.logColumnsSectionTitle": "日志列", - "xpack.infra.sourceConfiguration.logIndices.viewAffectedRulesLink": "查看受影响的规则", - "xpack.infra.sourceConfiguration.logIndicesDescription": "用于匹配包含日志数据的索引的索引模式", - "xpack.infra.sourceConfiguration.logIndicesLabel": "日志索引", - "xpack.infra.sourceConfiguration.logIndicesRecommendedValue": "推荐值为 {defaultValue}", - "xpack.infra.sourceConfiguration.logIndicesTitle": "日志索引", - "xpack.infra.sourceConfiguration.logsIndicesSectionTitle": "索引(已过时)", - "xpack.infra.sourceConfiguration.logsIndicesUsedByRulesMessage": "一个或多个告警规则依赖于此数据源设置。更改此设置会更改用于生成告警的数据。", - "xpack.infra.sourceConfiguration.logsIndicesUsedByRulesTitle": "告警规则使用此数据源设置", - "xpack.infra.sourceConfiguration.messageLogColumnDescription": "此系统字段显示派生自文档字段的日志条目消息。", "xpack.infra.sourceConfiguration.metricIndices.viewAffectedRulesLink": "查看受影响的规则", "xpack.infra.sourceConfiguration.metricIndicesDescription": "用于匹配包含指标数据的索引的索引模式", "xpack.infra.sourceConfiguration.metricIndicesDoNotExist": "找不到任何指标数据,因为输入的模式不匹配任何索引。", @@ -22484,15 +22415,11 @@ "xpack.infra.sourceConfiguration.nameDescription": "源配置的描述性名称", "xpack.infra.sourceConfiguration.nameLabel": "名称", "xpack.infra.sourceConfiguration.nameSectionTitle": "名称", - "xpack.infra.sourceConfiguration.noLogColumnsDescription": "使用上面的按钮将列添加到此列表。", - "xpack.infra.sourceConfiguration.noLogColumnsTitle": "无列", "xpack.infra.sourceConfiguration.noRemoteClusterMessage": "无法连接到远程集群,这导致我们无法检索您所需的指标和数据。要解决此问题,请检查您的索引配置,并确保进行了正确配置。", "xpack.infra.sourceConfiguration.noRemoteClusterTitle": "无法连接到远程集群", "xpack.infra.sourceConfiguration.remoteClusterConnectionDoNotExist": "检查远程集群是否可用,或远程连接设置是否正确。", "xpack.infra.sourceConfiguration.remoteClusterConnectionDoNotExistTitle": "无法连接到远程集群", "xpack.infra.sourceConfiguration.saveButton": "保存更改", - "xpack.infra.sourceConfiguration.systemColumnBadgeLabel": "系统", - "xpack.infra.sourceConfiguration.timestampLogColumnDescription": "此系统字段显示 {timestampSetting} 字段设置所确定的日志条目时间。", "xpack.infra.sourceConfiguration.unsavedFormPrompt": "是否确定要离开?更改将丢失", "xpack.infra.sourceConfiguration.updateFailureBody": "无法对指标配置应用更改。请稍后重试。", "xpack.infra.sourceConfiguration.updateFailureTitle": "配置更新失败", diff --git a/x-pack/platform/plugins/shared/logs_shared/common/http_api/index.ts b/x-pack/platform/plugins/shared/logs_shared/common/http_api/index.ts index 939f72786183b..25de1a66b6de6 100644 --- a/x-pack/platform/plugins/shared/logs_shared/common/http_api/index.ts +++ b/x-pack/platform/plugins/shared/logs_shared/common/http_api/index.ts @@ -9,5 +9,4 @@ * Exporting versioned APIs types */ export * from './latest'; -export * as logEntriesV1 from './log_entries/v1'; export * as logViewsV1 from './log_views/v1'; diff --git a/x-pack/platform/plugins/shared/logs_shared/common/http_api/latest.ts b/x-pack/platform/plugins/shared/logs_shared/common/http_api/latest.ts index 63f58bf4f92c0..84ece561371f6 100644 --- a/x-pack/platform/plugins/shared/logs_shared/common/http_api/latest.ts +++ b/x-pack/platform/plugins/shared/logs_shared/common/http_api/latest.ts @@ -5,5 +5,4 @@ * 2.0. */ -export * from './log_entries/v1'; export * from './log_views/v1'; diff --git a/x-pack/platform/plugins/shared/logs_shared/common/http_api/log_entries/v1/highlights.ts b/x-pack/platform/plugins/shared/logs_shared/common/http_api/log_entries/v1/highlights.ts deleted file mode 100644 index f879ca3f87961..0000000000000 --- a/x-pack/platform/plugins/shared/logs_shared/common/http_api/log_entries/v1/highlights.ts +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import * as rt from 'io-ts'; -import { logEntryCursorRT, logEntryRT } from '../../../log_entry'; -import { logViewColumnConfigurationRT } from '../../../log_views'; -import { logViewReferenceRT } from '../../../log_views'; - -export const LOG_ENTRIES_HIGHLIGHTS_PATH = '/api/log_entries/highlights'; - -export const logEntriesHighlightsBaseRequestRT = rt.intersection([ - rt.type({ - logView: logViewReferenceRT, - startTimestamp: rt.number, - endTimestamp: rt.number, - highlightTerms: rt.array(rt.string), - }), - rt.partial({ - query: rt.union([rt.string, rt.null]), - size: rt.number, - columns: rt.array(logViewColumnConfigurationRT), - }), -]); - -export const logEntriesHighlightsBeforeRequestRT = rt.intersection([ - logEntriesHighlightsBaseRequestRT, - rt.type({ before: rt.union([logEntryCursorRT, rt.literal('last')]) }), -]); - -export const logEntriesHighlightsAfterRequestRT = rt.intersection([ - logEntriesHighlightsBaseRequestRT, - rt.type({ after: rt.union([logEntryCursorRT, rt.literal('first')]) }), -]); - -export const logEntriesHighlightsCenteredRequestRT = rt.intersection([ - logEntriesHighlightsBaseRequestRT, - rt.type({ center: logEntryCursorRT }), -]); - -export const logEntriesHighlightsRequestRT = rt.union([ - logEntriesHighlightsBaseRequestRT, - logEntriesHighlightsBeforeRequestRT, - logEntriesHighlightsAfterRequestRT, - logEntriesHighlightsCenteredRequestRT, -]); - -export type LogEntriesHighlightsRequest = rt.TypeOf; - -export const logEntriesHighlightsResponseRT = rt.type({ - data: rt.array( - rt.union([ - rt.type({ - topCursor: rt.null, - bottomCursor: rt.null, - entries: rt.array(logEntryRT), - }), - rt.type({ - topCursor: logEntryCursorRT, - bottomCursor: logEntryCursorRT, - entries: rt.array(logEntryRT), - }), - ]) - ), -}); - -export type LogEntriesHighlightsResponse = rt.TypeOf; diff --git a/x-pack/platform/plugins/shared/logs_shared/common/http_api/log_entries/v1/index.ts b/x-pack/platform/plugins/shared/logs_shared/common/http_api/log_entries/v1/index.ts deleted file mode 100644 index 83d240ca8f273..0000000000000 --- a/x-pack/platform/plugins/shared/logs_shared/common/http_api/log_entries/v1/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export * from './highlights'; -export * from './summary'; -export * from './summary_highlights'; diff --git a/x-pack/platform/plugins/shared/logs_shared/common/http_api/log_entries/v1/summary.ts b/x-pack/platform/plugins/shared/logs_shared/common/http_api/log_entries/v1/summary.ts deleted file mode 100644 index 318ff44b296c0..0000000000000 --- a/x-pack/platform/plugins/shared/logs_shared/common/http_api/log_entries/v1/summary.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import * as rt from 'io-ts'; -import { logViewReferenceRT } from '../../../log_views'; - -export const LOG_ENTRIES_SUMMARY_PATH = '/api/log_entries/summary'; - -export const logEntriesSummaryRequestRT = rt.type({ - logView: logViewReferenceRT, - startTimestamp: rt.number, - endTimestamp: rt.number, - bucketSize: rt.number, - query: rt.union([rt.string, rt.undefined, rt.null]), -}); - -export type LogEntriesSummaryRequest = rt.TypeOf; - -export const logEntriesSummaryBucketRT = rt.type({ - start: rt.number, - end: rt.number, - entriesCount: rt.number, -}); - -export type LogEntriesSummaryBucket = rt.TypeOf; - -export const logEntriesSummaryResponseRT = rt.type({ - data: rt.type({ - start: rt.number, - end: rt.number, - buckets: rt.array(logEntriesSummaryBucketRT), - }), -}); - -export type LogEntriesSummaryResponse = rt.TypeOf; diff --git a/x-pack/platform/plugins/shared/logs_shared/common/http_api/log_entries/v1/summary_highlights.ts b/x-pack/platform/plugins/shared/logs_shared/common/http_api/log_entries/v1/summary_highlights.ts deleted file mode 100644 index f12bc72fc12f5..0000000000000 --- a/x-pack/platform/plugins/shared/logs_shared/common/http_api/log_entries/v1/summary_highlights.ts +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import * as rt from 'io-ts'; -import { logEntryCursorRT } from '../../../log_entry'; -import { logEntriesSummaryRequestRT, logEntriesSummaryBucketRT } from './summary'; - -export const LOG_ENTRIES_SUMMARY_HIGHLIGHTS_PATH = '/api/log_entries/summary_highlights'; - -export const logEntriesSummaryHighlightsRequestRT = rt.intersection([ - logEntriesSummaryRequestRT, - rt.type({ - highlightTerms: rt.array(rt.string), - }), -]); - -export type LogEntriesSummaryHighlightsRequest = rt.TypeOf< - typeof logEntriesSummaryHighlightsRequestRT ->; - -export const logEntriesSummaryHighlightsBucketRT = rt.intersection([ - logEntriesSummaryBucketRT, - rt.type({ - representativeKey: logEntryCursorRT, - }), -]); - -export type LogEntriesSummaryHighlightsBucket = rt.TypeOf< - typeof logEntriesSummaryHighlightsBucketRT ->; - -export const logEntriesSummaryHighlightsResponseRT = rt.type({ - data: rt.array( - rt.type({ - start: rt.number, - end: rt.number, - buckets: rt.array(logEntriesSummaryHighlightsBucketRT), - }) - ), -}); -export type LogEntriesSummaryHighlightsResponse = rt.TypeOf< - typeof logEntriesSummaryHighlightsResponseRT ->; diff --git a/x-pack/platform/plugins/shared/logs_shared/common/index.ts b/x-pack/platform/plugins/shared/logs_shared/common/index.ts index d5287460b5de8..fab9fa213790d 100644 --- a/x-pack/platform/plugins/shared/logs_shared/common/index.ts +++ b/x-pack/platform/plugins/shared/logs_shared/common/index.ts @@ -47,19 +47,6 @@ export * from './log_entry'; export { convertISODateToNanoPrecision } from './utils'; -// Http types -export type { LogEntriesSummaryBucket, LogEntriesSummaryHighlightsBucket } from './http_api'; - -// Http runtime -export { - LOG_ENTRIES_HIGHLIGHTS_PATH, - LOG_ENTRIES_SUMMARY_PATH, - logEntriesHighlightsRequestRT, - logEntriesHighlightsResponseRT, - logEntriesSummaryRequestRT, - logEntriesSummaryResponseRT, -} from './http_api'; - // Locators export { type LogsLocatorParams, diff --git a/x-pack/platform/plugins/shared/logs_shared/common/time/time_key.ts b/x-pack/platform/plugins/shared/logs_shared/common/time/time_key.ts index 1ff661af69d0a..fa56f74590828 100644 --- a/x-pack/platform/plugins/shared/logs_shared/common/time/time_key.ts +++ b/x-pack/platform/plugins/shared/logs_shared/common/time/time_key.ts @@ -6,7 +6,7 @@ */ import type { TimeKey } from '@kbn/io-ts-utils'; -import { ascending, bisector } from 'd3-array'; +import { ascending } from 'd3-array'; export type Comparator = (firstValue: any, secondValue: any) => number; @@ -38,25 +38,3 @@ export const compareToTimeKey = (keyAccessor: (value: Value) => TimeKey, compareValues?: Comparator) => (value: Value, key: TimeKey) => compareTimeKeys(keyAccessor(value), key, compareValues); - -export const getIndexAtTimeKey = ( - keyAccessor: (value: Value) => TimeKey, - compareValues?: Comparator -) => { - const comparator = compareToTimeKey(keyAccessor, compareValues); - const collectionBisector = bisector(comparator); - - return (collection: Value[], key: TimeKey): number | null => { - const index = collectionBisector.left(collection, key); - - if (index >= collection.length) { - return null; - } - - if (comparator(collection[index], key) !== 0) { - return null; - } - - return index; - }; -}; diff --git a/x-pack/platform/plugins/shared/logs_shared/public/containers/logs/log_highlights/api/fetch_log_entries_highlights.ts b/x-pack/platform/plugins/shared/logs_shared/public/containers/logs/log_highlights/api/fetch_log_entries_highlights.ts deleted file mode 100644 index 91f418f943247..0000000000000 --- a/x-pack/platform/plugins/shared/logs_shared/public/containers/logs/log_highlights/api/fetch_log_entries_highlights.ts +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { HttpHandler } from '@kbn/core/public'; - -import { decodeOrThrow } from '../../../../../common/runtime_types'; - -import { - LOG_ENTRIES_HIGHLIGHTS_PATH, - LogEntriesHighlightsRequest, - logEntriesHighlightsRequestRT, - logEntriesHighlightsResponseRT, -} from '../../../../../common/http_api'; - -export const fetchLogEntriesHighlights = async ( - requestArgs: LogEntriesHighlightsRequest, - fetch: HttpHandler -) => { - const response = await fetch(LOG_ENTRIES_HIGHLIGHTS_PATH, { - method: 'POST', - body: JSON.stringify(logEntriesHighlightsRequestRT.encode(requestArgs)), - version: '1', - }); - - return decodeOrThrow(logEntriesHighlightsResponseRT)(response); -}; diff --git a/x-pack/platform/plugins/shared/logs_shared/public/containers/logs/log_highlights/api/fetch_log_summary_highlights.ts b/x-pack/platform/plugins/shared/logs_shared/public/containers/logs/log_highlights/api/fetch_log_summary_highlights.ts deleted file mode 100644 index 8753926fceb8d..0000000000000 --- a/x-pack/platform/plugins/shared/logs_shared/public/containers/logs/log_highlights/api/fetch_log_summary_highlights.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { HttpHandler } from '@kbn/core/public'; -import { decodeOrThrow } from '../../../../../common/runtime_types'; - -import { - LOG_ENTRIES_SUMMARY_HIGHLIGHTS_PATH, - LogEntriesSummaryHighlightsRequest, - logEntriesSummaryHighlightsRequestRT, - logEntriesSummaryHighlightsResponseRT, -} from '../../../../../common/http_api'; - -export const fetchLogSummaryHighlights = async ( - requestArgs: LogEntriesSummaryHighlightsRequest, - fetch: HttpHandler -) => { - const response = await fetch(LOG_ENTRIES_SUMMARY_HIGHLIGHTS_PATH, { - method: 'POST', - body: JSON.stringify(logEntriesSummaryHighlightsRequestRT.encode(requestArgs)), - version: '1', - }); - - return decodeOrThrow(logEntriesSummaryHighlightsResponseRT)(response); -}; diff --git a/x-pack/platform/plugins/shared/logs_shared/public/containers/logs/log_highlights/index.ts b/x-pack/platform/plugins/shared/logs_shared/public/containers/logs/log_highlights/index.ts deleted file mode 100644 index e448689aa281f..0000000000000 --- a/x-pack/platform/plugins/shared/logs_shared/public/containers/logs/log_highlights/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export * from './log_highlights'; diff --git a/x-pack/platform/plugins/shared/logs_shared/public/containers/logs/log_highlights/log_entry_highlights.tsx b/x-pack/platform/plugins/shared/logs_shared/public/containers/logs/log_highlights/log_entry_highlights.tsx deleted file mode 100644 index 117a78d619596..0000000000000 --- a/x-pack/platform/plugins/shared/logs_shared/public/containers/logs/log_highlights/log_entry_highlights.tsx +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { TimeKey } from '@kbn/io-ts-utils'; -import { useEffect, useMemo, useState } from 'react'; -import { LogViewReference } from '../../../../common'; -import { LogEntriesHighlightsResponse } from '../../../../common/http_api'; -import { LogEntry } from '../../../../common/log_entry'; -import { useKibanaContextForPlugin } from '../../../hooks/use_kibana'; -import { useTrackedPromise } from '../../../utils/use_tracked_promise'; -import { fetchLogEntriesHighlights } from './api/fetch_log_entries_highlights'; - -export const useLogEntryHighlights = ( - logViewReference: LogViewReference, - sourceVersion: string | undefined, - startTimestamp: number | null, - endTimestamp: number | null, - centerPoint: TimeKey | null, - size: number, - filterQuery: string | null, - highlightTerms: string[] -) => { - const { services } = useKibanaContextForPlugin(); - const [logEntryHighlights, setLogEntryHighlights] = useState< - LogEntriesHighlightsResponse['data'] - >([]); - const [loadLogEntryHighlightsRequest, loadLogEntryHighlights] = useTrackedPromise( - { - cancelPreviousOn: 'resolution', - createPromise: async () => { - if (!startTimestamp || !endTimestamp || !centerPoint || !highlightTerms.length) { - throw new Error('Skipping request: Insufficient parameters'); - } - - return await fetchLogEntriesHighlights( - { - logView: logViewReference, - startTimestamp, - endTimestamp, - center: centerPoint, - size, - query: filterQuery || undefined, - highlightTerms, - }, - services.http.fetch - ); - }, - onResolve: (response) => { - setLogEntryHighlights(response.data); - }, - }, - [logViewReference, startTimestamp, endTimestamp, centerPoint, size, filterQuery, highlightTerms] - ); - - useEffect(() => { - setLogEntryHighlights([]); - }, [highlightTerms]); - - useEffect(() => { - if ( - highlightTerms.filter((highlightTerm) => highlightTerm.length > 0).length && - startTimestamp && - endTimestamp - ) { - loadLogEntryHighlights(); - } else { - setLogEntryHighlights([]); - } - }, [ - endTimestamp, - filterQuery, - highlightTerms, - loadLogEntryHighlights, - sourceVersion, - startTimestamp, - ]); - - const logEntryHighlightsById = useMemo( - () => - logEntryHighlights.reduce>( - (accumulatedLogEntryHighlightsById, highlightData) => { - return highlightData.entries.reduce((singleHighlightLogEntriesById, entry) => { - const highlightsForId = singleHighlightLogEntriesById[entry.id] || []; - return { - ...singleHighlightLogEntriesById, - [entry.id]: [...highlightsForId, entry], - }; - }, accumulatedLogEntryHighlightsById); - }, - {} - ), - [logEntryHighlights] - ); - - return { - logEntryHighlights, - logEntryHighlightsById, - loadLogEntryHighlightsRequest, - }; -}; diff --git a/x-pack/platform/plugins/shared/logs_shared/public/containers/logs/log_highlights/log_highlights.tsx b/x-pack/platform/plugins/shared/logs_shared/public/containers/logs/log_highlights/log_highlights.tsx deleted file mode 100644 index 3317f02d034d3..0000000000000 --- a/x-pack/platform/plugins/shared/logs_shared/public/containers/logs/log_highlights/log_highlights.tsx +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import createContainer from 'constate'; -import { useState } from 'react'; -import useThrottle from 'react-use/lib/useThrottle'; -import { TimeKey } from '@kbn/io-ts-utils'; -import { LogViewReference } from '../../../../common'; -import { useLogEntryHighlights } from './log_entry_highlights'; -import { useLogSummaryHighlights } from './log_summary_highlights'; -import { useNextAndPrevious } from './next_and_previous'; -import { useLogPositionStateContext } from '../log_position'; - -const FETCH_THROTTLE_INTERVAL = 3000; - -interface UseLogHighlightsStateProps { - logViewReference: LogViewReference; - sourceVersion: string | undefined; - centerCursor: TimeKey | null; - size: number; - filterQuery: string | null; -} - -const useLogHighlightsState = ({ - logViewReference, - sourceVersion, - centerCursor, - size, - filterQuery, -}: UseLogHighlightsStateProps) => { - const [highlightTerms, setHighlightTerms] = useState([]); - const { visibleMidpoint, jumpToTargetPosition, startTimestamp, endTimestamp } = - useLogPositionStateContext(); - - const throttledStartTimestamp = useThrottle(startTimestamp, FETCH_THROTTLE_INTERVAL); - const throttledEndTimestamp = useThrottle(endTimestamp, FETCH_THROTTLE_INTERVAL); - - const { logEntryHighlights, logEntryHighlightsById, loadLogEntryHighlightsRequest } = - useLogEntryHighlights( - logViewReference, - sourceVersion, - throttledStartTimestamp, - throttledEndTimestamp, - centerCursor, - size, - filterQuery, - highlightTerms - ); - - const { logSummaryHighlights, loadLogSummaryHighlightsRequest } = useLogSummaryHighlights( - logViewReference, - sourceVersion, - throttledStartTimestamp, - throttledEndTimestamp, - filterQuery, - highlightTerms - ); - - const { - currentHighlightKey, - hasPreviousHighlight, - hasNextHighlight, - goToPreviousHighlight, - goToNextHighlight, - } = useNextAndPrevious({ - visibleMidpoint, - logEntryHighlights, - highlightTerms, - jumpToTargetPosition, - }); - - return { - highlightTerms, - setHighlightTerms, - logEntryHighlights, - logEntryHighlightsById, - logSummaryHighlights, - loadLogEntryHighlightsRequest, - loadLogSummaryHighlightsRequest, - currentHighlightKey, - hasPreviousHighlight, - hasNextHighlight, - goToPreviousHighlight, - goToNextHighlight, - }; -}; - -export const [LogHighlightsStateProvider, useLogHighlightsStateContext] = - createContainer(useLogHighlightsState); diff --git a/x-pack/platform/plugins/shared/logs_shared/public/containers/logs/log_highlights/log_summary_highlights.ts b/x-pack/platform/plugins/shared/logs_shared/public/containers/logs/log_highlights/log_summary_highlights.ts deleted file mode 100644 index 61a1a02618e7a..0000000000000 --- a/x-pack/platform/plugins/shared/logs_shared/public/containers/logs/log_highlights/log_summary_highlights.ts +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { useEffect, useMemo, useState } from 'react'; -import { debounce } from 'lodash'; - -import { LogViewReference } from '../../../../common'; -import { useTrackedPromise } from '../../../utils/use_tracked_promise'; -import { fetchLogSummaryHighlights } from './api/fetch_log_summary_highlights'; -import { LogEntriesSummaryHighlightsResponse } from '../../../../common/http_api'; -import { useBucketSize } from '../log_summary/bucket_size'; -import { useKibanaContextForPlugin } from '../../../hooks/use_kibana'; - -export const useLogSummaryHighlights = ( - logViewReference: LogViewReference, - sourceVersion: string | undefined, - startTimestamp: number | null, - endTimestamp: number | null, - filterQuery: string | null, - highlightTerms: string[] -) => { - const { services } = useKibanaContextForPlugin(); - const [logSummaryHighlights, setLogSummaryHighlights] = useState< - LogEntriesSummaryHighlightsResponse['data'] - >([]); - - const bucketSize = useBucketSize(startTimestamp, endTimestamp); - - const [loadLogSummaryHighlightsRequest, loadLogSummaryHighlights] = useTrackedPromise( - { - cancelPreviousOn: 'resolution', - createPromise: async () => { - if (!startTimestamp || !endTimestamp || !bucketSize || !highlightTerms.length) { - throw new Error('Skipping request: Insufficient parameters'); - } - - return await fetchLogSummaryHighlights( - { - logView: logViewReference, - startTimestamp, - endTimestamp, - bucketSize, - query: filterQuery, - highlightTerms, - }, - services.http.fetch - ); - }, - onResolve: (response) => { - setLogSummaryHighlights(response.data); - }, - }, - [logViewReference, startTimestamp, endTimestamp, bucketSize, filterQuery, highlightTerms] - ); - - const debouncedLoadSummaryHighlights = useMemo( - () => debounce(loadLogSummaryHighlights, 275), - [loadLogSummaryHighlights] - ); - - useEffect(() => { - setLogSummaryHighlights([]); - }, [highlightTerms]); - - useEffect(() => { - if ( - highlightTerms.filter((highlightTerm) => highlightTerm.length > 0).length && - startTimestamp && - endTimestamp - ) { - debouncedLoadSummaryHighlights(); - } else { - setLogSummaryHighlights([]); - } - }, [ - bucketSize, - debouncedLoadSummaryHighlights, - filterQuery, - highlightTerms, - sourceVersion, - startTimestamp, - endTimestamp, - ]); - - return { - logSummaryHighlights, - loadLogSummaryHighlightsRequest, - }; -}; diff --git a/x-pack/platform/plugins/shared/logs_shared/public/containers/logs/log_highlights/next_and_previous.tsx b/x-pack/platform/plugins/shared/logs_shared/public/containers/logs/log_highlights/next_and_previous.tsx deleted file mode 100644 index c76d66711b928..0000000000000 --- a/x-pack/platform/plugins/shared/logs_shared/public/containers/logs/log_highlights/next_and_previous.tsx +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { isNumber } from 'lodash'; -import { useCallback, useEffect, useMemo, useState } from 'react'; -import { TimeKey, UniqueTimeKey } from '@kbn/io-ts-utils'; -import { - getLogEntryIndexAtTime, - getLogEntryIndexBeforeTime, - getUniqueLogEntryKey, -} from '../../../utils/log_entry'; -import { LogEntriesHighlightsResponse } from '../../../../common/http_api'; - -export const useNextAndPrevious = ({ - highlightTerms, - jumpToTargetPosition, - logEntryHighlights, - visibleMidpoint, -}: { - highlightTerms: string[]; - jumpToTargetPosition: (target: TimeKey) => void; - logEntryHighlights: LogEntriesHighlightsResponse['data'] | undefined; - visibleMidpoint: TimeKey | null; -}) => { - const [currentTimeKey, setCurrentTimeKey] = useState(null); - - const entries = useMemo( - // simplification, because we only support one highlight phrase for now - () => - logEntryHighlights && logEntryHighlights.length > 0 ? logEntryHighlights[0].entries : [], - [logEntryHighlights] - ); - - useEffect(() => { - setCurrentTimeKey(null); - }, [highlightTerms]); - - useEffect(() => { - if (currentTimeKey) { - jumpToTargetPosition(currentTimeKey); - } - }, [currentTimeKey, jumpToTargetPosition]); - - useEffect(() => { - if (currentTimeKey === null && entries.length > 0) { - const initialIndex = visibleMidpoint - ? clampValue(getLogEntryIndexBeforeTime(entries, visibleMidpoint), 0, entries.length - 1) - : 0; - const initialTimeKey = getUniqueLogEntryKey(entries[initialIndex]); - setCurrentTimeKey(initialTimeKey); - } - }, [currentTimeKey, entries, setCurrentTimeKey, visibleMidpoint]); - - const indexOfCurrentTimeKey = useMemo(() => { - if (currentTimeKey && entries.length > 0) { - return getLogEntryIndexAtTime(entries, currentTimeKey); - } else { - return null; - } - }, [currentTimeKey, entries]); - - const hasPreviousHighlight = useMemo( - () => isNumber(indexOfCurrentTimeKey) && indexOfCurrentTimeKey > 0, - [indexOfCurrentTimeKey] - ); - - const hasNextHighlight = useMemo( - () => - entries.length > 0 && - isNumber(indexOfCurrentTimeKey) && - indexOfCurrentTimeKey < entries.length - 1, - [indexOfCurrentTimeKey, entries] - ); - - const goToPreviousHighlight = useCallback(() => { - if (entries.length && isNumber(indexOfCurrentTimeKey)) { - const previousIndex = indexOfCurrentTimeKey - 1; - const entryTimeKey = getUniqueLogEntryKey(entries[previousIndex]); - setCurrentTimeKey(entryTimeKey); - } - }, [indexOfCurrentTimeKey, entries]); - - const goToNextHighlight = useCallback(() => { - if (entries.length > 0 && isNumber(indexOfCurrentTimeKey)) { - const nextIndex = indexOfCurrentTimeKey + 1; - const entryTimeKey = getUniqueLogEntryKey(entries[nextIndex]); - setCurrentTimeKey(entryTimeKey); - } - }, [indexOfCurrentTimeKey, entries]); - - return { - currentHighlightKey: currentTimeKey, - hasPreviousHighlight, - hasNextHighlight, - goToPreviousHighlight, - goToNextHighlight, - }; -}; - -const clampValue = (value: number, minValue: number, maxValue: number) => - Math.min(Math.max(value, minValue), maxValue); diff --git a/x-pack/platform/plugins/shared/logs_shared/public/containers/logs/log_summary/api/fetch_log_summary.ts b/x-pack/platform/plugins/shared/logs_shared/public/containers/logs/log_summary/api/fetch_log_summary.ts deleted file mode 100644 index 5d89b50e6eaae..0000000000000 --- a/x-pack/platform/plugins/shared/logs_shared/public/containers/logs/log_summary/api/fetch_log_summary.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { HttpHandler } from '@kbn/core/public'; -import { decodeOrThrow } from '../../../../../common/runtime_types'; - -import { - LOG_ENTRIES_SUMMARY_PATH, - LogEntriesSummaryRequest, - logEntriesSummaryRequestRT, - logEntriesSummaryResponseRT, -} from '../../../../../common/http_api'; - -export const fetchLogSummary = async ( - requestArgs: LogEntriesSummaryRequest, - fetch: HttpHandler -) => { - const response = await fetch(LOG_ENTRIES_SUMMARY_PATH, { - method: 'POST', - body: JSON.stringify(logEntriesSummaryRequestRT.encode(requestArgs)), - version: '1', - }); - - return decodeOrThrow(logEntriesSummaryResponseRT)(response); -}; diff --git a/x-pack/platform/plugins/shared/logs_shared/public/containers/logs/log_summary/bucket_size.ts b/x-pack/platform/plugins/shared/logs_shared/public/containers/logs/log_summary/bucket_size.ts deleted file mode 100644 index ad048d2f8082e..0000000000000 --- a/x-pack/platform/plugins/shared/logs_shared/public/containers/logs/log_summary/bucket_size.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { useMemo } from 'react'; - -const SUMMARY_BUCKET_COUNT = 100; - -export function useBucketSize( - startTimestamp: number | null, - endTimestamp: number | null -): number | null { - const bucketSize = useMemo(() => { - if (!startTimestamp || !endTimestamp) { - return null; - } - return (endTimestamp - startTimestamp) / SUMMARY_BUCKET_COUNT; - }, [startTimestamp, endTimestamp]); - - return bucketSize; -} diff --git a/x-pack/platform/plugins/shared/logs_shared/public/containers/logs/log_summary/index.ts b/x-pack/platform/plugins/shared/logs_shared/public/containers/logs/log_summary/index.ts deleted file mode 100644 index 935bda868e23b..0000000000000 --- a/x-pack/platform/plugins/shared/logs_shared/public/containers/logs/log_summary/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export * from './log_summary'; -export * from './with_summary'; diff --git a/x-pack/platform/plugins/shared/logs_shared/public/containers/logs/log_summary/log_summary.test.tsx b/x-pack/platform/plugins/shared/logs_shared/public/containers/logs/log_summary/log_summary.test.tsx deleted file mode 100644 index 8635df14731a5..0000000000000 --- a/x-pack/platform/plugins/shared/logs_shared/public/containers/logs/log_summary/log_summary.test.tsx +++ /dev/null @@ -1,215 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { waitFor, renderHook } from '@testing-library/react'; -// We are using this inside a `jest.mock` call. Jest requires dynamic dependencies to be prefixed with `mock` -import { coreMock as mockCoreMock } from '@kbn/core/public/mocks'; - -import { useLogSummary } from './log_summary'; - -import { fetchLogSummary } from './api/fetch_log_summary'; -import { datemathToEpochMillis } from '../../../utils/datemath'; - -const LOG_VIEW_REFERENCE = { type: 'log-view-reference' as const, logViewId: 'LOG_VIEW_ID' }; -const CHANGED_LOG_VIEW_REFERENCE = { - type: 'log-view-reference' as const, - logViewId: 'CHANGED_LOG_VIEW_ID', -}; - -// Typescript doesn't know that `fetchLogSummary` is a jest mock. -// We use a second variable with a type cast to help the compiler further down the line. -jest.mock('./api/fetch_log_summary', () => ({ fetchLogSummary: jest.fn() })); -const fetchLogSummaryMock = fetchLogSummary as jest.MockedFunction; - -jest.mock('../../../hooks/use_kibana', () => { - const services = mockCoreMock.createStart(); - return { - useKibanaContextForPlugin: () => ({ services }), - }; -}); - -describe('useLogSummary hook', () => { - beforeEach(() => { - fetchLogSummaryMock.mockClear(); - }); - - it('provides an empty list of buckets by default', () => { - const { result } = renderHook(() => useLogSummary(LOG_VIEW_REFERENCE, null, null, null)); - expect(result.current.buckets).toEqual([]); - }); - - it('queries for new summary buckets when the source id changes', async () => { - const { startTimestamp, endTimestamp } = createMockDateRange(); - - const firstMockResponse = createMockResponse([ - { start: startTimestamp, end: endTimestamp, entriesCount: 1 }, - ]); - const secondMockResponse = createMockResponse([ - { start: startTimestamp, end: endTimestamp, entriesCount: 2 }, - ]); - - fetchLogSummaryMock - .mockResolvedValueOnce(firstMockResponse) - .mockResolvedValueOnce(secondMockResponse); - - const { result, rerender } = renderHook( - ({ logViewReference }) => useLogSummary(logViewReference, startTimestamp, endTimestamp, null), - { - initialProps: { logViewReference: LOG_VIEW_REFERENCE }, - } - ); - - await waitFor(() => new Promise((resolve) => resolve(null))); - - expect(fetchLogSummaryMock).toHaveBeenCalledTimes(1); - expect(fetchLogSummaryMock).toHaveBeenLastCalledWith( - expect.objectContaining({ - logView: LOG_VIEW_REFERENCE, - }), - expect.anything() - ); - expect(result.current.buckets).toEqual(firstMockResponse.data.buckets); - - rerender({ logViewReference: CHANGED_LOG_VIEW_REFERENCE }); - await waitFor(() => new Promise((resolve) => resolve(null))); - - expect(fetchLogSummaryMock).toHaveBeenCalledTimes(2); - expect(fetchLogSummaryMock).toHaveBeenLastCalledWith( - expect.objectContaining({ - logView: CHANGED_LOG_VIEW_REFERENCE, - }), - expect.anything() - ); - expect(result.current.buckets).toEqual(secondMockResponse.data.buckets); - }); - - it('queries for new summary buckets when the filter query changes', async () => { - const { startTimestamp, endTimestamp } = createMockDateRange(); - - const firstMockResponse = createMockResponse([ - { start: startTimestamp, end: endTimestamp, entriesCount: 1 }, - ]); - const secondMockResponse = createMockResponse([ - { start: startTimestamp, end: endTimestamp, entriesCount: 2 }, - ]); - - fetchLogSummaryMock - .mockResolvedValueOnce(firstMockResponse) - .mockResolvedValueOnce(secondMockResponse); - - const { result, rerender } = renderHook( - ({ filterQuery }) => - useLogSummary(LOG_VIEW_REFERENCE, startTimestamp, endTimestamp, filterQuery), - { - initialProps: { filterQuery: 'INITIAL_FILTER_QUERY' }, - } - ); - - await waitFor(() => new Promise((resolve) => resolve(null))); - - expect(fetchLogSummaryMock).toHaveBeenCalledTimes(1); - expect(fetchLogSummaryMock).toHaveBeenLastCalledWith( - expect.objectContaining({ - query: 'INITIAL_FILTER_QUERY', - }), - expect.anything() - ); - expect(result.current.buckets).toEqual(firstMockResponse.data.buckets); - - rerender({ filterQuery: 'CHANGED_FILTER_QUERY' }); - await waitFor(() => new Promise((resolve) => resolve(null))); - - expect(fetchLogSummaryMock).toHaveBeenCalledTimes(2); - expect(fetchLogSummaryMock).toHaveBeenLastCalledWith( - expect.objectContaining({ - query: 'CHANGED_FILTER_QUERY', - }), - expect.anything() - ); - expect(result.current.buckets).toEqual(secondMockResponse.data.buckets); - }); - - it('queries for new summary buckets when the start and end date changes', async () => { - fetchLogSummaryMock - .mockResolvedValueOnce(createMockResponse([])) - .mockResolvedValueOnce(createMockResponse([])); - - const firstRange = createMockDateRange(); - const { rerender } = renderHook( - ({ startTimestamp, endTimestamp }) => - useLogSummary(LOG_VIEW_REFERENCE, startTimestamp, endTimestamp, null), - { - initialProps: firstRange, - } - ); - - await waitFor(() => new Promise((resolve) => resolve(null))); - expect(fetchLogSummaryMock).toHaveBeenCalledTimes(1); - expect(fetchLogSummaryMock).toHaveBeenLastCalledWith( - expect.objectContaining({ - startTimestamp: firstRange.startTimestamp, - endTimestamp: firstRange.endTimestamp, - }), - expect.anything() - ); - - const secondRange = createMockDateRange('now-20s', 'now'); - - rerender(secondRange); - await waitFor(() => new Promise((resolve) => resolve(null))); - - expect(fetchLogSummaryMock).toHaveBeenCalledTimes(2); - expect(fetchLogSummaryMock).toHaveBeenLastCalledWith( - expect.objectContaining({ - startTimestamp: secondRange.startTimestamp, - endTimestamp: secondRange.endTimestamp, - }), - expect.anything() - ); - }); - - it("doesn't query for new summary buckets when the previous request is still in flight", async () => { - fetchLogSummaryMock.mockResolvedValueOnce(createMockResponse([])); - - const firstRange = createMockDateRange(); - const { rerender } = renderHook( - ({ startTimestamp, endTimestamp }) => - useLogSummary(LOG_VIEW_REFERENCE, startTimestamp, endTimestamp, null), - { - initialProps: firstRange, - } - ); - - const secondRange = createMockDateRange('now-20s', 'now'); - - // intentionally don't wait for an update to test the throttling - rerender(secondRange); - await waitFor(() => new Promise((resolve) => resolve(null))); - - expect(fetchLogSummaryMock).toHaveBeenCalledTimes(1); - expect(fetchLogSummaryMock).toHaveBeenLastCalledWith( - expect.objectContaining({ - startTimestamp: firstRange.startTimestamp, - endTimestamp: firstRange.endTimestamp, - }), - expect.anything() - ); - }); -}); - -const createMockResponse = ( - buckets: Array<{ start: number; end: number; entriesCount: number }> -) => ({ data: { buckets, start: Number.NEGATIVE_INFINITY, end: Number.POSITIVE_INFINITY } }); - -const createMockDateRange = (startDate = 'now-10s', endDate = 'now') => { - return { - startDate, - endDate, - startTimestamp: datemathToEpochMillis(startDate)!, - endTimestamp: datemathToEpochMillis(endDate, 'up')!, - }; -}; diff --git a/x-pack/platform/plugins/shared/logs_shared/public/containers/logs/log_summary/log_summary.tsx b/x-pack/platform/plugins/shared/logs_shared/public/containers/logs/log_summary/log_summary.tsx deleted file mode 100644 index 0360d6d381ada..0000000000000 --- a/x-pack/platform/plugins/shared/logs_shared/public/containers/logs/log_summary/log_summary.tsx +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { useEffect } from 'react'; -import { exhaustMap, map, Observable } from 'rxjs'; -import { HttpHandler } from '@kbn/core-http-browser'; -import { LogViewReference } from '../../../../common'; -import { useObservableState, useReplaySubject } from '../../../utils/use_observable'; -import { fetchLogSummary } from './api/fetch_log_summary'; -import { LogEntriesSummaryRequest, LogEntriesSummaryResponse } from '../../../../common/http_api'; -import { useBucketSize } from './bucket_size'; -import { useKibanaContextForPlugin } from '../../../hooks/use_kibana'; - -export type LogSummaryBuckets = LogEntriesSummaryResponse['data']['buckets']; - -export const useLogSummary = ( - logViewReference: LogViewReference, - startTimestamp: number | null, - endTimestamp: number | null, - filterQuery: string | null -) => { - const { services } = useKibanaContextForPlugin(); - const bucketSize = useBucketSize(startTimestamp, endTimestamp); - - const [logSummaryBuckets$, pushLogSummaryBucketsArgs] = useReplaySubject(fetchLogSummary$); - const { latestValue: logSummaryBuckets } = useObservableState(logSummaryBuckets$, NO_BUCKETS); - - useEffect(() => { - if (startTimestamp === null || endTimestamp === null || bucketSize === null) { - return; - } - - pushLogSummaryBucketsArgs([ - { - logView: logViewReference, - startTimestamp, - endTimestamp, - bucketSize, - query: filterQuery, - }, - services.http.fetch, - ]); - }, [ - bucketSize, - endTimestamp, - filterQuery, - pushLogSummaryBucketsArgs, - services.http.fetch, - logViewReference, - startTimestamp, - ]); - - return { - buckets: logSummaryBuckets, - start: startTimestamp, - end: endTimestamp, - }; -}; - -const NO_BUCKETS: LogSummaryBuckets = []; - -type FetchLogSummaryArgs = [args: LogEntriesSummaryRequest, fetch: HttpHandler]; - -const fetchLogSummary$ = ( - fetchArguments$: Observable -): Observable => - fetchArguments$.pipe( - exhaustMap(([args, fetch]) => fetchLogSummary(args, fetch)), - map(({ data: { buckets } }) => buckets) - ); diff --git a/x-pack/platform/plugins/shared/logs_shared/public/containers/logs/log_summary/with_summary.ts b/x-pack/platform/plugins/shared/logs_shared/public/containers/logs/log_summary/with_summary.ts deleted file mode 100644 index bb8fe69d4356e..0000000000000 --- a/x-pack/platform/plugins/shared/logs_shared/public/containers/logs/log_summary/with_summary.ts +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import useThrottle from 'react-use/lib/useThrottle'; -import { useLogPositionStateContext, useLogViewContext } from '../../..'; -import { RendererFunction } from '../../../utils/typed_react'; -import { LogSummaryBuckets, useLogSummary } from './log_summary'; - -const FETCH_THROTTLE_INTERVAL = 3000; - -export interface WithSummaryProps { - serializedParsedQuery: string | null; - children: RendererFunction<{ - buckets: LogSummaryBuckets; - start: number | null; - end: number | null; - }>; -} - -export const WithSummary = ({ serializedParsedQuery, children }: WithSummaryProps) => { - const { logViewReference } = useLogViewContext(); - const { startTimestamp, endTimestamp } = useLogPositionStateContext(); - - // Keep it reasonably updated for the `now` case, but don't reload all the time when the user scrolls - const throttledStartTimestamp = useThrottle(startTimestamp, FETCH_THROTTLE_INTERVAL); - const throttledEndTimestamp = useThrottle(endTimestamp, FETCH_THROTTLE_INTERVAL); - - const { buckets, start, end } = useLogSummary( - logViewReference, - throttledStartTimestamp, - throttledEndTimestamp, - serializedParsedQuery - ); - - return children({ buckets, start, end }); -}; - -// eslint-disable-next-line import/no-default-export -export default WithSummary; diff --git a/x-pack/platform/plugins/shared/logs_shared/public/index.ts b/x-pack/platform/plugins/shared/logs_shared/public/index.ts index 3d601c9936f2d..be05abd06a69e 100644 --- a/x-pack/platform/plugins/shared/logs_shared/public/index.ts +++ b/x-pack/platform/plugins/shared/logs_shared/public/index.ts @@ -28,11 +28,6 @@ export { LogPositionStateProvider, useLogPositionStateContext, } from './containers/logs/log_position'; -export { - LogHighlightsStateProvider, - useLogHighlightsStateContext, -} from './containers/logs/log_highlights'; -export type { LogSummaryBuckets, WithSummaryProps } from './containers/logs/log_summary'; // Shared components export type { LogAIAssistantDocument } from './components/log_ai_assistant/log_ai_assistant'; @@ -52,7 +47,6 @@ export type { } from './components/logging/log_text_stream/scrollable_log_text_stream_view'; export type { LogsOverviewProps } from './components/logs_overview'; -export const WithSummary = dynamic(() => import('./containers/logs/log_summary/with_summary')); export const LogEntryFlyout = dynamic( () => import('./components/logging/log_entry_flyout/log_entry_flyout') ); diff --git a/x-pack/platform/plugins/shared/logs_shared/public/utils/log_entry/log_entry.ts b/x-pack/platform/plugins/shared/logs_shared/public/utils/log_entry/log_entry.ts index e1027aeeb5799..fa49d3ba27a6b 100644 --- a/x-pack/platform/plugins/shared/logs_shared/public/utils/log_entry/log_entry.ts +++ b/x-pack/platform/plugins/shared/logs_shared/public/utils/log_entry/log_entry.ts @@ -5,11 +5,7 @@ * 2.0. */ -import type { TimeKey, UniqueTimeKey } from '@kbn/io-ts-utils'; -import { bisector } from 'd3-array'; -import { compareToTimeKey, getIndexAtTimeKey } from '../../../common/time'; import { - LogEntry, LogColumn, LogTimestampColumn, LogFieldColumn, @@ -19,25 +15,6 @@ import { LogMessageConstantPart, } from '../../../common/log_entry'; -export const getLogEntryKey = (entry: { cursor: TimeKey }) => entry.cursor; - -export const getUniqueLogEntryKey = (entry: { id: string; cursor: TimeKey }): UniqueTimeKey => ({ - ...entry.cursor, - gid: entry.id, -}); - -const logEntryTimeBisector = bisector(compareToTimeKey(getLogEntryKey)); - -export const getLogEntryIndexBeforeTime = logEntryTimeBisector.left; -export const getLogEntryIndexAfterTime = logEntryTimeBisector.right; -export const getLogEntryIndexAtTime = getIndexAtTimeKey(getLogEntryKey); - -export const getLogEntryAtTime = (entries: LogEntry[], time: TimeKey) => { - const entryIndex = getLogEntryIndexAtTime(entries, time); - - return entryIndex !== null ? entries[entryIndex] : null; -}; - export const isTimestampColumn = (column: LogColumn): column is LogTimestampColumn => column != null && 'time' in column; diff --git a/x-pack/platform/plugins/shared/logs_shared/public/utils/typed_react.tsx b/x-pack/platform/plugins/shared/logs_shared/public/utils/typed_react.tsx deleted file mode 100644 index 664894e1bf05c..0000000000000 --- a/x-pack/platform/plugins/shared/logs_shared/public/utils/typed_react.tsx +++ /dev/null @@ -1,11 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; - -export type RendererResult = React.ReactElement | null; -export type RendererFunction = (args: RenderArgs) => Result; diff --git a/x-pack/platform/plugins/shared/logs_shared/public/utils/use_observable.ts b/x-pack/platform/plugins/shared/logs_shared/public/utils/use_observable.ts index 2634a156be986..2899d9f50b764 100644 --- a/x-pack/platform/plugins/shared/logs_shared/public/utils/use_observable.ts +++ b/x-pack/platform/plugins/shared/logs_shared/public/utils/use_observable.ts @@ -6,14 +6,7 @@ */ import { useEffect, useRef, useState } from 'react'; -import { - BehaviorSubject, - Observable, - OperatorFunction, - PartialObserver, - ReplaySubject, - Subscription, -} from 'rxjs'; +import { BehaviorSubject, Observable, OperatorFunction, PartialObserver, Subscription } from 'rxjs'; import { switchMap } from 'rxjs'; export const useLatest = (value: Value) => { @@ -58,22 +51,6 @@ export const useBehaviorSubject = < return [output$, next] as const; }; -export const useReplaySubject = < - InputValue, - OutputValue, - OutputObservable extends Observable ->( - deriveObservableOnce: (input$: Observable) => OutputObservable -) => { - const [[subject$, next], _] = useState(() => { - const newSubject$ = new ReplaySubject(); - const newNext = newSubject$.next.bind(newSubject$); - return [newSubject$, newNext] as const; - }); - const [output$] = useState(() => deriveObservableOnce(subject$)); - return [output$, next] as const; -}; - export const useObservableState = ( state$: Observable, initialState: InitialState | (() => InitialState) diff --git a/x-pack/platform/plugins/shared/logs_shared/server/lib/adapters/log_entries/kibana_log_entries_adapter.ts b/x-pack/platform/plugins/shared/logs_shared/server/lib/adapters/log_entries/kibana_log_entries_adapter.ts index f8102e8f0dffa..9760eab9687b9 100644 --- a/x-pack/platform/plugins/shared/logs_shared/server/lib/adapters/log_entries/kibana_log_entries_adapter.ts +++ b/x-pack/platform/plugins/shared/logs_shared/server/lib/adapters/log_entries/kibana_log_entries_adapter.ts @@ -5,11 +5,6 @@ * 2.0. */ -import { timeMilliseconds } from 'd3-time'; -import { fold, map } from 'fp-ts/lib/Either'; -import { constant, identity } from 'fp-ts/lib/function'; -import { pipe } from 'fp-ts/lib/pipeable'; -import * as runtimeTypes from 'io-ts'; import { JsonArray } from '@kbn/utility-types'; import { compact } from 'lodash'; import type { LogsSharedPluginRequestHandlerContext } from '../../../types'; @@ -18,7 +13,6 @@ import { LogEntriesParams, LogEntryDocument, LogEntryQuery, - LogSummaryBucket, LOG_ENTRIES_PAGE_SIZE, } from '../../domains/log_entries_domain'; import { SortedSearchHit } from '../framework'; @@ -28,21 +22,6 @@ import { TIMESTAMP_FIELD, TIEBREAKER_FIELD } from '../../../../common/constants' const TIMESTAMP_FORMAT = 'epoch_millis'; -const MAX_BUCKETS = 1000; - -function getBucketIntervalStarts( - startTimestamp: number, - endTimestamp: number, - bucketSize: number -): Date[] { - // estimated number of buckets - const bucketCount = Math.ceil((endTimestamp - startTimestamp) / bucketSize); - if (bucketCount > MAX_BUCKETS) { - throw new Error(`Requested too many buckets: ${bucketCount} > ${MAX_BUCKETS}`); - } - return timeMilliseconds(new Date(startTimestamp), new Date(endTimestamp), bucketSize); -} - export class LogsSharedKibanaLogEntriesAdapter implements LogEntriesAdapter { constructor(private readonly framework: KibanaFramework) {} @@ -140,86 +119,6 @@ export class LogsSharedKibanaLogEntriesAdapter implements LogEntriesAdapter { hasMoreAfter: sortDirection === 'asc' ? hasMore : undefined, }; } - - public async getContainedLogSummaryBuckets( - requestContext: LogsSharedPluginRequestHandlerContext, - resolvedLogView: ResolvedLogView, - startTimestamp: number, - endTimestamp: number, - bucketSize: number, - filterQuery?: LogEntryQuery - ): Promise { - const bucketIntervalStarts = getBucketIntervalStarts(startTimestamp, endTimestamp, bucketSize); - - const query = { - allow_no_indices: true, - index: resolvedLogView.indices, - ignore_unavailable: true, - body: { - aggregations: { - count_by_date: { - date_range: { - field: TIMESTAMP_FIELD, - format: TIMESTAMP_FORMAT, - ranges: bucketIntervalStarts.map((bucketIntervalStart) => ({ - from: bucketIntervalStart.getTime(), - to: bucketIntervalStart.getTime() + bucketSize, - })), - }, - aggregations: { - top_hits_by_key: { - top_hits: { - size: 1, - sort: [ - { - [TIMESTAMP_FIELD]: { - order: 'asc', - format: 'strict_date_optional_time_nanos', - numeric_type: 'date_nanos', - }, - }, - { [TIEBREAKER_FIELD]: 'asc' }, - ], - _source: false, - }, - }, - }, - }, - }, - query: { - bool: { - filter: [ - ...createQueryFilterClauses(filterQuery), - { - range: { - [TIMESTAMP_FIELD]: { - gte: startTimestamp, - lte: endTimestamp, - format: TIMESTAMP_FORMAT, - }, - }, - }, - ], - }, - }, - runtime_mappings: resolvedLogView.runtimeMappings, - size: 0, - track_total_hits: false, - }, - }; - - const response = await this.framework.callWithRequest(requestContext, 'search', query); - - return pipe( - LogSummaryResponseRuntimeType.decode(response), - map((logSummaryResponse) => - logSummaryResponse.aggregations.count_by_date.buckets.map( - convertDateRangeBucketToSummaryBucket - ) - ), - fold(constant([]), identity) - ); - } } function mapHitsToLogEntryDocuments(hits: SortedSearchHit[], fields: string[]): LogEntryDocument[] { @@ -245,18 +144,6 @@ function mapHitsToLogEntryDocuments(hits: SortedSearchHit[], fields: string[]): }); } -const convertDateRangeBucketToSummaryBucket = ( - bucket: LogSummaryDateRangeBucket -): LogSummaryBucket => ({ - entriesCount: bucket.doc_count, - start: bucket.from || 0, - end: bucket.to || 0, - topEntryKeys: bucket.top_hits_by_key.hits.hits.map((hit) => ({ - tiebreaker: hit.sort[1], - time: hit.sort[0], - })), -}); - const createHighlightQuery = ( highlightTerm: string | undefined, fields: string[] @@ -284,9 +171,6 @@ const createFilterClauses = ( return compact([filterQuery, highlightQuery]) as LogEntryQuery[]; }; -const createQueryFilterClauses = (filterQuery: LogEntryQuery | undefined) => - filterQuery ? [filterQuery] : []; - function processCursor(cursor: LogEntriesParams['cursor']): { sortDirection: 'asc' | 'desc'; searchAfterClause: { search_after?: readonly [string, number] }; @@ -310,35 +194,3 @@ function processCursor(cursor: LogEntriesParams['cursor']): { return { sortDirection: 'asc', searchAfterClause: {} }; } - -const LogSummaryDateRangeBucketRuntimeType = runtimeTypes.intersection([ - runtimeTypes.type({ - doc_count: runtimeTypes.number, - key: runtimeTypes.string, - top_hits_by_key: runtimeTypes.type({ - hits: runtimeTypes.type({ - hits: runtimeTypes.array( - runtimeTypes.type({ - sort: runtimeTypes.tuple([runtimeTypes.string, runtimeTypes.number]), - }) - ), - }), - }), - }), - runtimeTypes.partial({ - from: runtimeTypes.number, - to: runtimeTypes.number, - }), -]); - -export type LogSummaryDateRangeBucket = runtimeTypes.TypeOf< - typeof LogSummaryDateRangeBucketRuntimeType ->; - -const LogSummaryResponseRuntimeType = runtimeTypes.type({ - aggregations: runtimeTypes.type({ - count_by_date: runtimeTypes.type({ - buckets: runtimeTypes.array(LogSummaryDateRangeBucketRuntimeType), - }), - }), -}); diff --git a/x-pack/platform/plugins/shared/logs_shared/server/lib/domains/log_entries_domain/log_entries_domain.mock.ts b/x-pack/platform/plugins/shared/logs_shared/server/lib/domains/log_entries_domain/log_entries_domain.mock.ts index 74509f11ae4a7..dd739de03f4a9 100644 --- a/x-pack/platform/plugins/shared/logs_shared/server/lib/domains/log_entries_domain/log_entries_domain.mock.ts +++ b/x-pack/platform/plugins/shared/logs_shared/server/lib/domains/log_entries_domain/log_entries_domain.mock.ts @@ -10,10 +10,7 @@ import { ILogsSharedLogEntriesDomain } from './log_entries_domain'; export const createLogsSharedLogEntriesDomainMock = (): jest.Mocked => { return { - getLogEntriesAround: jest.fn(), getLogEntries: jest.fn(), - getLogSummaryBucketsBetween: jest.fn(), - getLogSummaryHighlightBucketsBetween: jest.fn(), getLogEntryDatasets: jest.fn(), }; }; diff --git a/x-pack/platform/plugins/shared/logs_shared/server/lib/domains/log_entries_domain/log_entries_domain.ts b/x-pack/platform/plugins/shared/logs_shared/server/lib/domains/log_entries_domain/log_entries_domain.ts index f3cbfb57b09c4..e10d7d87fd0bf 100644 --- a/x-pack/platform/plugins/shared/logs_shared/server/lib/domains/log_entries_domain/log_entries_domain.ts +++ b/x-pack/platform/plugins/shared/logs_shared/server/lib/domains/log_entries_domain/log_entries_domain.ts @@ -7,11 +7,6 @@ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { JsonObject } from '@kbn/utility-types'; -import { subtractMillisecondsFromDate } from '../../../../common/utils'; -import { - LogEntriesSummaryBucket, - LogEntriesSummaryHighlightsBucket, -} from '../../../../common/http_api'; import { LogColumn, LogEntry, LogEntryCursor } from '../../../../common/log_entry'; import { LogViewColumnConfiguration, @@ -45,15 +40,6 @@ export interface LogEntriesParams { highlightTerm?: string; } -export interface LogEntriesAroundParams { - startTimestamp: number; - endTimestamp: number; - size?: number; - center: LogEntryCursor; - query?: JsonObject; - highlightTerm?: string; -} - export const LOG_ENTRIES_PAGE_SIZE = 200; const FIELDS_FROM_CONTEXT = ['log.file.path', 'host.name', 'container.id'] as const; @@ -61,35 +47,12 @@ const FIELDS_FROM_CONTEXT = ['log.file.path', 'host.name', 'container.id'] as co const COMPOSITE_AGGREGATION_BATCH_SIZE = 1000; export interface ILogsSharedLogEntriesDomain { - getLogEntriesAround( - requestContext: LogsSharedPluginRequestHandlerContext, - logView: LogViewReference, - params: LogEntriesAroundParams, - columnOverrides?: LogViewColumnConfiguration[] - ): Promise<{ entries: LogEntry[]; hasMoreBefore?: boolean; hasMoreAfter?: boolean }>; getLogEntries( requestContext: LogsSharedPluginRequestHandlerContext, logView: LogViewReference, params: LogEntriesParams, columnOverrides?: LogViewColumnConfiguration[] ): Promise<{ entries: LogEntry[]; hasMoreBefore?: boolean; hasMoreAfter?: boolean }>; - getLogSummaryBucketsBetween( - requestContext: LogsSharedPluginRequestHandlerContext, - logView: LogViewReference, - start: number, - end: number, - bucketSize: number, - filterQuery?: LogEntryQuery - ): Promise; - getLogSummaryHighlightBucketsBetween( - requestContext: LogsSharedPluginRequestHandlerContext, - logView: LogViewReference, - startTimestamp: number, - endTimestamp: number, - bucketSize: number, - highlightQueries: string[], - filterQuery?: LogEntryQuery - ): Promise; getLogEntryDatasets( requestContext: LogsSharedPluginRequestHandlerContext, timestampField: string, @@ -106,66 +69,6 @@ export class LogsSharedLogEntriesDomain implements ILogsSharedLogEntriesDomain { private readonly libs: Pick ) {} - public async getLogEntriesAround( - requestContext: LogsSharedPluginRequestHandlerContext, - logView: LogViewReference, - params: LogEntriesAroundParams, - columnOverrides?: LogViewColumnConfiguration[] - ): Promise<{ entries: LogEntry[]; hasMoreBefore?: boolean; hasMoreAfter?: boolean }> { - const { startTimestamp, endTimestamp, center, query, size, highlightTerm } = params; - - /* - * For odd sizes we will round this value down for the first half, and up - * for the second. This keeps the center cursor right in the center. - * - * For even sizes the half before is one entry bigger than the half after. - * [1, 2, 3, 4, 5, *6*, 7, 8, 9, 10] - * | 5 entries | |4 entries| - */ - const halfSize = (size || LOG_ENTRIES_PAGE_SIZE) / 2; - - const { entries: entriesBefore, hasMoreBefore } = await this.getLogEntries( - requestContext, - logView, - { - startTimestamp, - endTimestamp, - query, - cursor: { before: center }, - size: Math.floor(halfSize), - highlightTerm, - }, - columnOverrides - ); - - /* - * Elasticsearch's `search_after` returns documents after the specified cursor. - * - If we have documents before the center, we search after the last of - * those. The first document of the new group is the center. - * - If there were no documents, we search one milisecond before the - * center. It then becomes the first document. - */ - const cursorAfter = - entriesBefore.length > 0 - ? entriesBefore[entriesBefore.length - 1].cursor - : { time: subtractMillisecondsFromDate(center.time, 1), tiebreaker: 0 }; - - const { entries: entriesAfter, hasMoreAfter } = await this.getLogEntries( - requestContext, - logView, - { - startTimestamp, - endTimestamp, - query, - cursor: { after: cursorAfter }, - size: Math.ceil(halfSize), - highlightTerm, - } - ); - - return { entries: [...entriesBefore, ...entriesAfter], hasMoreBefore, hasMoreAfter }; - } - public async getLogEntries( requestContext: LogsSharedPluginRequestHandlerContext, logView: LogViewReference, @@ -227,86 +130,6 @@ export class LogsSharedLogEntriesDomain implements ILogsSharedLogEntriesDomain { return { entries, hasMoreBefore, hasMoreAfter }; } - public async getLogSummaryBucketsBetween( - requestContext: LogsSharedPluginRequestHandlerContext, - logView: LogViewReference, - start: number, - end: number, - bucketSize: number, - filterQuery?: LogEntryQuery - ): Promise { - const [, { logsDataAccess }, { logViews }] = await this.libs.getStartServices(); - const { savedObjects, elasticsearch } = await requestContext.core; - const logSourcesService = logsDataAccess.services.logSourcesServiceFactory.getLogSourcesService( - savedObjects.client - ); - const resolvedLogView = await logViews - .getClient(savedObjects.client, elasticsearch.client.asCurrentUser, logSourcesService) - .getResolvedLogView(logView); - - const dateRangeBuckets = await this.adapter.getContainedLogSummaryBuckets( - requestContext, - resolvedLogView, - start, - end, - bucketSize, - filterQuery - ); - return dateRangeBuckets; - } - - public async getLogSummaryHighlightBucketsBetween( - requestContext: LogsSharedPluginRequestHandlerContext, - logView: LogViewReference, - startTimestamp: number, - endTimestamp: number, - bucketSize: number, - highlightQueries: string[], - filterQuery?: LogEntryQuery - ): Promise { - const [, { logsDataAccess }, { logViews }] = await this.libs.getStartServices(); - const { savedObjects, elasticsearch } = await requestContext.core; - const logSourcesService = logsDataAccess.services.logSourcesServiceFactory.getLogSourcesService( - savedObjects.client - ); - - const resolvedLogView = await logViews - .getClient(savedObjects.client, elasticsearch.client.asCurrentUser, logSourcesService) - .getResolvedLogView(logView); - - const messageFormattingRules = compileFormattingRules( - getBuiltinRules(resolvedLogView.messageField) - ); - const requiredFields = getRequiredFields(resolvedLogView, messageFormattingRules); - - const summaries = await Promise.all( - highlightQueries.map(async (highlightQueryPhrase) => { - const highlightQuery = createHighlightQueryDsl(highlightQueryPhrase, requiredFields); - const query = filterQuery - ? { - bool: { - must: [filterQuery, highlightQuery], - }, - } - : highlightQuery; - const summaryBuckets = await this.adapter.getContainedLogSummaryBuckets( - requestContext, - resolvedLogView, - startTimestamp, - endTimestamp, - bucketSize, - query - ); - const summaryHighlightBuckets = summaryBuckets - .filter(logSummaryBucketHasEntries) - .map(convertLogSummaryBucketToSummaryHighlightBucket); - return summaryHighlightBuckets; - }) - ); - - return summaries; - } - public async getLogEntryDatasets( requestContext: LogsSharedPluginRequestHandlerContext, timestampField: string, @@ -356,15 +179,6 @@ export interface LogEntriesAdapter { fields: string[], params: LogEntriesParams ): Promise<{ documents: LogEntryDocument[]; hasMoreBefore?: boolean; hasMoreAfter?: boolean }>; - - getContainedLogSummaryBuckets( - requestContext: LogsSharedPluginRequestHandlerContext, - resolvedLogView: ResolvedLogView, - startTimestamp: number, - endTimestamp: number, - bucketSize: number, - filterQuery?: LogEntryQuery - ): Promise; } export type LogEntryQuery = JsonObject; @@ -377,25 +191,6 @@ export interface LogEntryDocument { cursor: LogEntryCursor; } -export interface LogSummaryBucket { - entriesCount: number; - start: number; - end: number; - topEntryKeys: LogEntryCursor[]; -} - -const logSummaryBucketHasEntries = (bucket: LogSummaryBucket) => - bucket.entriesCount > 0 && bucket.topEntryKeys.length > 0; - -const convertLogSummaryBucketToSummaryHighlightBucket = ( - bucket: LogSummaryBucket -): LogEntriesSummaryHighlightsBucket => ({ - entriesCount: bucket.entriesCount, - start: bucket.start, - end: bucket.end, - representativeKey: bucket.topEntryKeys[0], -}); - const getRequiredFields = ( configuration: ResolvedLogView, messageFormattingRules: CompiledLogMessageFormattingRule @@ -416,15 +211,6 @@ const getRequiredFields = ( ); }; -const createHighlightQueryDsl = (phrase: string, fields: string[]) => ({ - multi_match: { - fields, - lenient: true, - query: phrase, - type: 'phrase', - }, -}); - const getContextFromDoc = (doc: LogEntryDocument): LogEntry['context'] => { // Get all context fields, then test for the presence and type of the ones that go together const containerId = doc.fields['container.id']?.[0]; diff --git a/x-pack/platform/plugins/shared/logs_shared/server/logs_shared_server.ts b/x-pack/platform/plugins/shared/logs_shared/server/logs_shared_server.ts index 9bb643c8dd617..d49904866a489 100644 --- a/x-pack/platform/plugins/shared/logs_shared/server/logs_shared_server.ts +++ b/x-pack/platform/plugins/shared/logs_shared/server/logs_shared_server.ts @@ -6,18 +6,11 @@ */ import { LogsSharedBackendLibs } from './lib/logs_shared_types'; -import { - initLogEntriesHighlightsRoute, - initLogEntriesSummaryHighlightsRoute, - initLogEntriesSummaryRoute, -} from './routes/log_entries'; + import { initLogViewRoutes } from './routes/log_views'; import { initMigrateLogViewSettingsRoute } from './routes/deprecations'; export const initLogsSharedServer = (libs: LogsSharedBackendLibs) => { - initLogEntriesHighlightsRoute(libs); - initLogEntriesSummaryRoute(libs); - initLogEntriesSummaryHighlightsRoute(libs); initLogViewRoutes(libs); initMigrateLogViewSettingsRoute(libs); }; diff --git a/x-pack/platform/plugins/shared/logs_shared/server/routes/log_entries/highlights.ts b/x-pack/platform/plugins/shared/logs_shared/server/routes/log_entries/highlights.ts deleted file mode 100644 index dedf3ced27f28..0000000000000 --- a/x-pack/platform/plugins/shared/logs_shared/server/routes/log_entries/highlights.ts +++ /dev/null @@ -1,118 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import Boom from '@hapi/boom'; - -import { pipe } from 'fp-ts/lib/pipeable'; -import { fold } from 'fp-ts/lib/Either'; -import { identity } from 'fp-ts/lib/function'; -import { schema } from '@kbn/config-schema'; - -import { i18n } from '@kbn/i18n'; -import { logEntriesV1 } from '../../../common/http_api'; -import { throwErrors } from '../../../common/runtime_types'; - -import { LogsSharedBackendLibs } from '../../lib/logs_shared_types'; - -import { parseFilterQuery } from '../../utils/serialized_query'; -import { LogEntriesParams } from '../../lib/domains/log_entries_domain'; - -const escapeHatch = schema.object({}, { unknowns: 'allow' }); - -export const initLogEntriesHighlightsRoute = ({ framework, logEntries }: LogsSharedBackendLibs) => { - framework - .registerVersionedRoute({ - access: 'internal', - method: 'post', - path: logEntriesV1.LOG_ENTRIES_HIGHLIGHTS_PATH, - }) - .addVersion( - { - version: '1', - validate: { request: { body: escapeHatch } }, - options: { - deprecated: { - documentationUrl: '', - severity: 'warning', - message: i18n.translate( - 'xpack.logsShared.deprecations.postLogEntriesHighlightsRoute.message', - { - defaultMessage: - 'Given the deprecation of the LogStream feature, this API will not return reliable data in upcoming versions of Kibana.', - } - ), - reason: { type: 'deprecate' }, - }, - }, - }, - async (requestContext, request, response) => { - const payload = pipe( - logEntriesV1.logEntriesHighlightsRequestRT.decode(request.body), - fold(throwErrors(Boom.badRequest), identity) - ); - - const { startTimestamp, endTimestamp, logView, query, size, highlightTerms } = payload; - - let entriesPerHighlightTerm; - - if ('center' in payload) { - entriesPerHighlightTerm = await Promise.all( - highlightTerms.map((highlightTerm) => - logEntries.getLogEntriesAround(requestContext, logView, { - startTimestamp, - endTimestamp, - query: parseFilterQuery(query), - center: payload.center, - size, - highlightTerm, - }) - ) - ); - } else { - let cursor: LogEntriesParams['cursor']; - if ('before' in payload) { - cursor = { before: payload.before }; - } else if ('after' in payload) { - cursor = { after: payload.after }; - } - - entriesPerHighlightTerm = await Promise.all( - highlightTerms.map((highlightTerm) => - logEntries.getLogEntries(requestContext, logView, { - startTimestamp, - endTimestamp, - query: parseFilterQuery(query), - cursor, - size, - highlightTerm, - }) - ) - ); - } - - return response.ok({ - body: logEntriesV1.logEntriesHighlightsResponseRT.encode({ - data: entriesPerHighlightTerm.map(({ entries }) => { - if (entries.length > 0) { - return { - entries, - topCursor: entries[0].cursor, - bottomCursor: entries[entries.length - 1].cursor, - }; - } else { - return { - entries, - topCursor: null, - bottomCursor: null, - }; - } - }), - }), - }); - } - ); -}; diff --git a/x-pack/platform/plugins/shared/logs_shared/server/routes/log_entries/index.ts b/x-pack/platform/plugins/shared/logs_shared/server/routes/log_entries/index.ts deleted file mode 100644 index 83d240ca8f273..0000000000000 --- a/x-pack/platform/plugins/shared/logs_shared/server/routes/log_entries/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export * from './highlights'; -export * from './summary'; -export * from './summary_highlights'; diff --git a/x-pack/platform/plugins/shared/logs_shared/server/routes/log_entries/summary.ts b/x-pack/platform/plugins/shared/logs_shared/server/routes/log_entries/summary.ts deleted file mode 100644 index 68c9d5ea0e166..0000000000000 --- a/x-pack/platform/plugins/shared/logs_shared/server/routes/log_entries/summary.ts +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import Boom from '@hapi/boom'; - -import { pipe } from 'fp-ts/lib/pipeable'; -import { fold } from 'fp-ts/lib/Either'; -import { identity } from 'fp-ts/lib/function'; -import { schema } from '@kbn/config-schema'; - -import { i18n } from '@kbn/i18n'; -import { logEntriesV1 } from '../../../common/http_api'; -import { throwErrors } from '../../../common/runtime_types'; - -import { LogsSharedBackendLibs } from '../../lib/logs_shared_types'; - -import { parseFilterQuery } from '../../utils/serialized_query'; - -const escapeHatch = schema.object({}, { unknowns: 'allow' }); - -export const initLogEntriesSummaryRoute = ({ - framework, - logEntries, - getUsageCollector, -}: LogsSharedBackendLibs) => { - framework - .registerVersionedRoute({ - access: 'internal', - method: 'post', - path: logEntriesV1.LOG_ENTRIES_SUMMARY_PATH, - }) - .addVersion( - { - version: '1', - validate: { request: { body: escapeHatch } }, - options: { - deprecated: { - documentationUrl: '', - severity: 'warning', - message: i18n.translate( - 'xpack.logsShared.deprecations.postLogEntriesSummaryRoute.message', - { - defaultMessage: - 'Given the deprecation of the LogStream feature, this API will not return reliable data in upcoming versions of Kibana.', - } - ), - reason: { type: 'deprecate' }, - }, - }, - }, - async (requestContext, request, response) => { - const payload = pipe( - logEntriesV1.logEntriesSummaryRequestRT.decode(request.body), - fold(throwErrors(Boom.badRequest), identity) - ); - const { logView, startTimestamp, endTimestamp, bucketSize, query } = payload; - - const usageCollector = getUsageCollector(); - - const buckets = await logEntries.getLogSummaryBucketsBetween( - requestContext, - logView, - startTimestamp, - endTimestamp, - bucketSize, - parseFilterQuery(query) - ); - - if (typeof usageCollector.countLogs === 'function') { - usageCollector.countLogs(); - } - - return response.ok({ - body: logEntriesV1.logEntriesSummaryResponseRT.encode({ - data: { - start: startTimestamp, - end: endTimestamp, - buckets, - }, - }), - }); - } - ); -}; diff --git a/x-pack/platform/plugins/shared/logs_shared/server/routes/log_entries/summary_highlights.ts b/x-pack/platform/plugins/shared/logs_shared/server/routes/log_entries/summary_highlights.ts deleted file mode 100644 index b8f030b91b715..0000000000000 --- a/x-pack/platform/plugins/shared/logs_shared/server/routes/log_entries/summary_highlights.ts +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import Boom from '@hapi/boom'; - -import { pipe } from 'fp-ts/lib/pipeable'; -import { fold } from 'fp-ts/lib/Either'; -import { identity } from 'fp-ts/lib/function'; -import { schema } from '@kbn/config-schema'; - -import { i18n } from '@kbn/i18n'; -import { logEntriesV1 } from '../../../common/http_api'; -import { throwErrors } from '../../../common/runtime_types'; - -import { LogsSharedBackendLibs } from '../../lib/logs_shared_types'; - -import { parseFilterQuery } from '../../utils/serialized_query'; - -const escapeHatch = schema.object({}, { unknowns: 'allow' }); - -export const initLogEntriesSummaryHighlightsRoute = ({ - framework, - logEntries, -}: LogsSharedBackendLibs) => { - framework - .registerVersionedRoute({ - access: 'internal', - method: 'post', - path: logEntriesV1.LOG_ENTRIES_SUMMARY_HIGHLIGHTS_PATH, - }) - .addVersion( - { - version: '1', - validate: { request: { body: escapeHatch } }, - options: { - deprecated: { - documentationUrl: '', - severity: 'warning', - message: i18n.translate( - 'xpack.logsShared.deprecations.postLogEntriesSummaryHighlightsRoute.message', - { - defaultMessage: - 'Given the deprecation of the LogStream feature, this API will not return reliable data in upcoming versions of Kibana.', - } - ), - reason: { type: 'deprecate' }, - }, - }, - }, - async (requestContext, request, response) => { - const payload = pipe( - logEntriesV1.logEntriesSummaryHighlightsRequestRT.decode(request.body), - fold(throwErrors(Boom.badRequest), identity) - ); - const { logView, startTimestamp, endTimestamp, bucketSize, query, highlightTerms } = - payload; - - const bucketsPerHighlightTerm = await logEntries.getLogSummaryHighlightBucketsBetween( - requestContext, - logView, - startTimestamp, - endTimestamp, - bucketSize, - highlightTerms, - parseFilterQuery(query) - ); - - return response.ok({ - body: logEntriesV1.logEntriesSummaryHighlightsResponseRT.encode({ - data: bucketsPerHighlightTerm.map((buckets) => ({ - start: startTimestamp, - end: endTimestamp, - buckets, - })), - }), - }); - } - ); -}; diff --git a/x-pack/solutions/observability/plugins/infra/common/log_search_result/index.ts b/x-pack/solutions/observability/plugins/infra/common/log_search_result/index.ts deleted file mode 100644 index 592b23dfc70ad..0000000000000 --- a/x-pack/solutions/observability/plugins/infra/common/log_search_result/index.ts +++ /dev/null @@ -1,13 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export type { SearchResult } from './log_search_result'; -export { - getSearchResultIndexBeforeTime, - getSearchResultIndexAfterTime, - getSearchResultKey, -} from './log_search_result'; diff --git a/x-pack/solutions/observability/plugins/infra/common/log_search_result/log_search_result.ts b/x-pack/solutions/observability/plugins/infra/common/log_search_result/log_search_result.ts deleted file mode 100644 index 18d0990a80be5..0000000000000 --- a/x-pack/solutions/observability/plugins/infra/common/log_search_result/log_search_result.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { bisector } from 'd3-array'; - -import type { TimeKey } from '../time'; -import { compareToTimeKey } from '../time'; - -export interface SearchResult { - gid: string; - fields: TimeKey; - matches: SearchResultFieldMatches; -} - -export interface SearchResultFieldMatches { - [field: string]: string[]; -} - -export const getSearchResultKey = (result: SearchResult) => - ({ - gid: result.gid, - tiebreaker: result.fields.tiebreaker, - time: result.fields.time, - } as TimeKey); - -const searchResultTimeBisector = bisector(compareToTimeKey(getSearchResultKey)); -export const getSearchResultIndexBeforeTime = searchResultTimeBisector.left; -export const getSearchResultIndexAfterTime = searchResultTimeBisector.right; diff --git a/x-pack/solutions/observability/plugins/infra/common/log_search_summary/index.ts b/x-pack/solutions/observability/plugins/infra/common/log_search_summary/index.ts deleted file mode 100644 index 32652753f7799..0000000000000 --- a/x-pack/solutions/observability/plugins/infra/common/log_search_summary/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export type { SearchSummaryBucket } from './log_search_summary'; diff --git a/x-pack/solutions/observability/plugins/infra/common/log_search_summary/log_search_summary.ts b/x-pack/solutions/observability/plugins/infra/common/log_search_summary/log_search_summary.ts deleted file mode 100644 index 09132029261a2..0000000000000 --- a/x-pack/solutions/observability/plugins/infra/common/log_search_summary/log_search_summary.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { SearchResult } from '../log_search_result'; - -export interface SearchSummaryBucket { - start: number; - end: number; - count: number; - representative: SearchResult; -} diff --git a/x-pack/solutions/observability/plugins/infra/common/log_text_scale/index.ts b/x-pack/solutions/observability/plugins/infra/common/log_text_scale/index.ts deleted file mode 100644 index 11ae6d0adc7ff..0000000000000 --- a/x-pack/solutions/observability/plugins/infra/common/log_text_scale/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export * from './log_text_scale'; diff --git a/x-pack/solutions/observability/plugins/infra/common/log_text_scale/log_text_scale.ts b/x-pack/solutions/observability/plugins/infra/common/log_text_scale/log_text_scale.ts deleted file mode 100644 index 7fc774c4b59e0..0000000000000 --- a/x-pack/solutions/observability/plugins/infra/common/log_text_scale/log_text_scale.ts +++ /dev/null @@ -1,12 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export type TextScale = 'small' | 'medium' | 'large'; - -export function isTextScale(maybeTextScale: string): maybeTextScale is TextScale { - return ['small', 'medium', 'large'].includes(maybeTextScale); -} diff --git a/x-pack/solutions/observability/plugins/infra/common/time/time_key.ts b/x-pack/solutions/observability/plugins/infra/common/time/time_key.ts index 7acbef8b25025..9cfa14357255c 100644 --- a/x-pack/solutions/observability/plugins/infra/common/time/time_key.ts +++ b/x-pack/solutions/observability/plugins/infra/common/time/time_key.ts @@ -6,7 +6,7 @@ */ import { DateFromStringOrNumber } from '@kbn/io-ts-utils'; -import { ascending, bisector } from 'd3-array'; +import { ascending } from 'd3-array'; import * as rt from 'io-ts'; import { pick } from 'lodash'; @@ -69,28 +69,6 @@ export const compareToTimeKey = (value: Value, key: TimeKey) => compareTimeKeys(keyAccessor(value), key, compareValues); -export const getIndexAtTimeKey = ( - keyAccessor: (value: Value) => TimeKey, - compareValues?: Comparator -) => { - const comparator = compareToTimeKey(keyAccessor, compareValues); - const collectionBisector = bisector(comparator); - - return (collection: Value[], key: TimeKey): number | null => { - const index = collectionBisector.left(collection, key); - - if (index >= collection.length) { - return null; - } - - if (comparator(collection[index], key) !== 0) { - return null; - } - - return index; - }; -}; - export const timeKeyIsBetween = (min: TimeKey, max: TimeKey, operand: TimeKey) => compareTimeKeys(min, operand) <= 0 && compareTimeKeys(max, operand) >= 0; diff --git a/x-pack/solutions/observability/plugins/infra/common/url_state_storage_service.ts b/x-pack/solutions/observability/plugins/infra/common/url_state_storage_service.ts index 9eeaa86b9aebf..de065e25026d0 100644 --- a/x-pack/solutions/observability/plugins/infra/common/url_state_storage_service.ts +++ b/x-pack/solutions/observability/plugins/infra/common/url_state_storage_service.ts @@ -7,39 +7,12 @@ import { url } from '@kbn/kibana-utils-plugin/common'; import { encode } from '@kbn/rison'; -import type { Query } from '@kbn/es-query'; import { parse, stringify } from 'query-string'; -import type { DurationInputObject } from 'moment'; -import moment from 'moment'; -import type { LogViewReference } from '@kbn/logs-shared-plugin/common'; -import { - defaultFilterStateKey, - defaultPositionStateKey, - DEFAULT_REFRESH_INTERVAL, -} from '@kbn/logs-shared-plugin/common'; -import type { FilterStateInUrl } from '../public/observability_logs/log_stream_query_state'; -import type { PositionStateInUrl } from '../public/observability_logs/log_stream_position_state/src/url_state_storage_service'; -import type { TimeRange } from './time'; export const defaultLogViewKey = 'logView'; const encodeRisonUrlState = (state: any) => encode(state); -// Used by Locator components -export const replaceLogPositionInQueryString = (time?: number) => - Number.isNaN(time) || time == null - ? (value: string) => value - : replaceStateKeyInQueryString(defaultPositionStateKey, { - position: { - time: moment(time).toISOString(), - tiebreaker: 0, - }, - }); - -// NOTE: Used by Locator components -export const replaceLogViewInQueryString = (logViewReference: LogViewReference) => - replaceStateKeyInQueryString(defaultLogViewKey, logViewReference); - export const replaceStateKeyInQueryString = (stateKey: string, urlState: UrlState | undefined) => (queryString: string) => { @@ -53,38 +26,3 @@ export const replaceStateKeyInQueryString = }; return stringify(url.encodeQuery(newValue), { sort: false, encode: false }); }; - -export const replaceLogFilterInQueryString = (query: Query, time?: number, timeRange?: TimeRange) => - replaceStateKeyInQueryString(defaultFilterStateKey, { - query, - ...getTimeRange(time, timeRange), - refreshInterval: DEFAULT_REFRESH_INTERVAL, - }); - -const getTimeRange = (time?: number, timeRange?: TimeRange) => { - if (timeRange) { - return { - timeRange: { - from: new Date(timeRange.startTime).toISOString(), - to: new Date(timeRange.endTime).toISOString(), - }, - }; - } else if (time) { - return { - timeRange: { - from: getTimeRangeStartFromTime(time), - to: getTimeRangeEndFromTime(time), - }, - }; - } else { - return {}; - } -}; - -const defaultTimeRangeFromPositionOffset: DurationInputObject = { hours: 1 }; - -export const getTimeRangeStartFromTime = (time: number): string => - moment(time).subtract(defaultTimeRangeFromPositionOffset).toISOString(); - -export const getTimeRangeEndFromTime = (time: number): string => - moment(time).add(defaultTimeRangeFromPositionOffset).toISOString(); diff --git a/x-pack/solutions/observability/plugins/infra/public/components/logging/inline_log_view_splash_page.tsx b/x-pack/solutions/observability/plugins/infra/public/components/logging/inline_log_view_splash_page.tsx deleted file mode 100644 index 579290f45051c..0000000000000 --- a/x-pack/solutions/observability/plugins/infra/public/components/logging/inline_log_view_splash_page.tsx +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { EuiButton } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n-react'; -import type { LazyObservabilityPageTemplateProps } from '@kbn/observability-shared-plugin/public'; -import { EuiEmptyPrompt } from '@elastic/eui'; -import { EuiText } from '@elastic/eui'; -import { PageTemplate } from '../page_template'; - -type InlineLogViewSplashPageProps = { - revertToDefaultLogView: () => void; -} & LazyObservabilityPageTemplateProps; - -export const InlineLogViewSplashPage: React.FC = (props) => { - const { revertToDefaultLogView, ...templateProps } = props; - return ( - - - - ); -}; - -export const InlineLogViewSplashPrompt: React.FC<{ - revertToDefaultLogView: InlineLogViewSplashPageProps['revertToDefaultLogView']; -}> = ({ revertToDefaultLogView }) => { - const title = ( - - ); - - const ctaButton = ( - - - - ); - - const description = ( - - ); - - return ( - {title}} - body={ - -

{description}

-
- } - actions={ctaButton} - /> - ); -}; diff --git a/x-pack/solutions/observability/plugins/infra/public/components/logging/log_customization_menu.tsx b/x-pack/solutions/observability/plugins/infra/public/components/logging/log_customization_menu.tsx deleted file mode 100644 index e39053a489a49..0000000000000 --- a/x-pack/solutions/observability/plugins/infra/public/components/logging/log_customization_menu.tsx +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EuiButtonEmpty, EuiPopover } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n-react'; -import * as React from 'react'; -import styled from '@emotion/styled'; - -interface LogCustomizationMenuState { - isShown: boolean; -} - -export class LogCustomizationMenu extends React.Component< - React.PropsWithChildren<{}>, - LogCustomizationMenuState -> { - public readonly state = { - isShown: false, - }; - - public show = () => { - this.setState({ - isShown: true, - }); - }; - - public hide = () => { - this.setState({ - isShown: false, - }); - }; - - public toggleVisibility = () => { - this.setState((state) => ({ - isShown: !state.isShown, - })); - }; - - public render() { - const { children } = this.props; - const { isShown } = this.state; - - const menuButton = ( - - - - ); - - return ( - - {children} - - ); - } -} - -const CustomizationMenuContent = styled.div` - min-width: 200px; -`; diff --git a/x-pack/solutions/observability/plugins/infra/public/components/logging/log_datepicker.tsx b/x-pack/solutions/observability/plugins/infra/public/components/logging/log_datepicker.tsx deleted file mode 100644 index 214085dc6c6ce..0000000000000 --- a/x-pack/solutions/observability/plugins/infra/public/components/logging/log_datepicker.tsx +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { useCallback } from 'react'; -import type { OnTimeChangeProps } from '@elastic/eui'; -import { EuiFlexGroup, EuiFlexItem, EuiSuperDatePicker, EuiButton } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n-react'; - -interface LogDatepickerProps { - startDateExpression: string; - endDateExpression: string; - isStreaming: boolean; - onUpdateDateRange?: (range: { startDateExpression: string; endDateExpression: string }) => void; - onStartStreaming?: () => void; - onStopStreaming?: () => void; -} - -export const LogDatepicker: React.FC = ({ - startDateExpression, - endDateExpression, - isStreaming, - onUpdateDateRange, - onStartStreaming, - onStopStreaming, -}) => { - const handleTimeChange = useCallback( - ({ start, end, isInvalid }: OnTimeChangeProps) => { - if (onUpdateDateRange && !isInvalid) { - onUpdateDateRange({ startDateExpression: start, endDateExpression: end }); - } - }, - [onUpdateDateRange] - ); - - return ( - - - - - - {isStreaming ? ( - - - - ) : ( - - - - )} - - - ); -}; diff --git a/x-pack/solutions/observability/plugins/infra/public/components/logging/log_highlights_menu.tsx b/x-pack/solutions/observability/plugins/infra/public/components/logging/log_highlights_menu.tsx deleted file mode 100644 index 1657fdc51450d..0000000000000 --- a/x-pack/solutions/observability/plugins/infra/public/components/logging/log_highlights_menu.tsx +++ /dev/null @@ -1,190 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { - EuiButtonEmpty, - EuiButtonIcon, - EuiFieldText, - EuiFlexGroup, - EuiFlexItem, - EuiIcon, - EuiPopover, -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n-react'; -import { debounce } from 'lodash'; -import React, { useCallback, useMemo, useState } from 'react'; -import styled from '@emotion/styled'; -import { useVisibilityState } from '../../hooks/use_visibility_state'; -import { withAttrs } from '../../utils/theme_utils/with_attrs'; - -interface LogHighlightsMenuProps { - onChange: (highlightTerms: string[]) => void; - isLoading: boolean; - activeHighlights: boolean; - hasPreviousHighlight: boolean; - hasNextHighlight: boolean; - goToPreviousHighlight: () => void; - goToNextHighlight: () => void; -} - -export const LogHighlightsMenu: React.FC = ({ - onChange, - isLoading, - activeHighlights, - hasPreviousHighlight, - goToPreviousHighlight, - hasNextHighlight, - goToNextHighlight, -}) => { - const { - isVisible: isPopoverOpen, - hide: closePopover, - toggle: togglePopover, - } = useVisibilityState(false); - - // Input field state - const [highlightTerm, _setHighlightTerm] = useState(''); - - const debouncedOnChange = useMemo(() => debounce(onChange, 275), [onChange]); - const setHighlightTerm = useCallback( - (valueOrUpdater) => - _setHighlightTerm((previousHighlightTerm) => { - const newHighlightTerm = - typeof valueOrUpdater === 'function' - ? valueOrUpdater(previousHighlightTerm) - : valueOrUpdater; - - if (newHighlightTerm !== previousHighlightTerm) { - debouncedOnChange([newHighlightTerm]); - } - - return newHighlightTerm; - }), - [debouncedOnChange] - ); - const changeHighlightTerm = useCallback( - (e: React.ChangeEvent) => { - const value = e.target.value; - setHighlightTerm(value); - }, - [setHighlightTerm] - ); - const clearHighlightTerm = useCallback(() => setHighlightTerm(''), [setHighlightTerm]); - - const button = ( - - - {activeHighlights ? : null} - - ); - return ( - - - - - - - - - - - - - - - - - - - ); -}; - -const termsFieldLabel = i18n.translate('xpack.infra.logs.highlights.highlightTermsFieldLabel', { - defaultMessage: 'Terms to highlight', -}); - -const clearTermsButtonLabel = i18n.translate( - 'xpack.infra.logs.highlights.clearHighlightTermsButtonLabel', - { - defaultMessage: 'Clear terms to highlight', - } -); - -const goToPreviousHighlightLabel = i18n.translate( - 'xpack.infra.logs.highlights.goToPreviousHighlightButtonLabel', - { - defaultMessage: 'Jump to previous highlight', - } -); - -const goToNextHighlightLabel = i18n.translate( - 'xpack.infra.logs.highlights.goToNextHighlightButtonLabel', - { - defaultMessage: 'Jump to next highlight', - } -); - -const ActiveHighlightsIndicator = withAttrs( - styled(EuiIcon)` - padding-left: ${(props) => props.theme.euiTheme.size.xs}; - `, - ({ theme }) => ({ - type: 'checkInCircleFilled', - size: 'm', - color: theme?.euiTheme.colors.accent, - }) -); - -const LogHighlightsMenuContent = styled.div` - width: 300px; -`; diff --git a/x-pack/solutions/observability/plugins/infra/public/components/logging/log_minimap/density_chart.tsx b/x-pack/solutions/observability/plugins/infra/public/components/logging/log_minimap/density_chart.tsx deleted file mode 100644 index bcf9b4338b1c4..0000000000000 --- a/x-pack/solutions/observability/plugins/infra/public/components/logging/log_minimap/density_chart.tsx +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { scaleLinear, scaleTime } from 'd3-scale'; -import { area, curveMonotoneY } from 'd3-shape'; -import { max } from 'lodash'; -import * as React from 'react'; -import styled from '@emotion/styled'; -import type { LogEntriesSummaryBucket } from '@kbn/logs-shared-plugin/common'; -import { COLOR_MODES_STANDARD } from '@elastic/eui'; - -interface DensityChartProps { - buckets: LogEntriesSummaryBucket[]; - end: number; - start: number; - width: number; - height: number; -} - -export const DensityChart: React.FC = ({ - buckets, - start, - end, - width, - height, -}) => { - if (start >= end || height <= 0 || width <= 0 || buckets.length <= 0) { - return null; - } - - const yScale = scaleTime().domain([start, end]).range([0, height]); - - const xMax = max(buckets.map((bucket) => bucket.entriesCount)) || 0; - const xScale = scaleLinear().domain([0, xMax]).range([0, width]); - - const path = area() - .x0(xScale(0) ?? 0) - .x1((bucket) => xScale(bucket.entriesCount) ?? 0) - .y0((bucket) => yScale(bucket.start) ?? 0) - .y1((bucket) => yScale(bucket.end) ?? 0) - .curve(curveMonotoneY); - - const firstBucket = buckets[0]; - const lastBucket = buckets[buckets.length - 1]; - const pathBuckets = [ - // Make sure the graph starts at the count of the first point - { start, end: start, entriesCount: firstBucket.entriesCount }, - ...buckets, - // Make sure the line ends at the height of the last point - { start: lastBucket.end, end: lastBucket.end, entriesCount: lastBucket.entriesCount }, - // If the last point is not at the end of the minimap, make sure it doesn't extend indefinitely and goes to 0 - { start: end, end, entriesCount: 0 }, - ]; - const pathData = path(pathBuckets); - - return ( - - - - - ); -}; - -const DensityChartPositiveBackground = styled.rect` - fill: ${(props) => - props.theme.colorMode === COLOR_MODES_STANDARD.dark - ? props.theme.euiTheme.colors.lightShade - : props.theme.euiTheme.colors.lightestShade}; -`; - -const PositiveAreaPath = styled.path` - fill: ${(props) => - props.theme.colorMode === COLOR_MODES_STANDARD.dark - ? props.theme.euiTheme.colors.mediumShade - : props.theme.euiTheme.colors.lightShade}; -`; diff --git a/x-pack/solutions/observability/plugins/infra/public/components/logging/log_minimap/highlighted_interval.tsx b/x-pack/solutions/observability/plugins/infra/public/components/logging/log_minimap/highlighted_interval.tsx deleted file mode 100644 index a672488cc2c5e..0000000000000 --- a/x-pack/solutions/observability/plugins/infra/public/components/logging/log_minimap/highlighted_interval.tsx +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import * as React from 'react'; -import styled from '@emotion/styled'; - -interface HighlightedIntervalProps { - className?: string; - getPositionOfTime: (time: number) => number; - start: number; - end: number; - targetWidth: number; - width: number; - target: number | null; -} - -export const HighlightedInterval: React.FC = ({ - className, - end, - getPositionOfTime, - start, - targetWidth, - width, - target, -}) => { - const yStart = getPositionOfTime(start); - const yEnd = getPositionOfTime(end); - const yTarget = target && getPositionOfTime(target); - - return ( - <> - {yTarget && ( - - )} - - - ); -}; - -HighlightedInterval.displayName = 'HighlightedInterval'; - -const HighlightTargetMarker = styled.line` - stroke: ${(props) => props.theme.euiTheme.colors.primary}; - stroke-width: 1; -`; - -const HighlightPolygon = styled.polygon` - fill: ${(props) => props.theme.euiTheme.colors.primary}; - fill-opacity: 0.3; - stroke: ${(props) => props.theme.euiTheme.colors.primary}; - stroke-width: 1; -`; diff --git a/x-pack/solutions/observability/plugins/infra/public/components/logging/log_minimap/index.ts b/x-pack/solutions/observability/plugins/infra/public/components/logging/log_minimap/index.ts deleted file mode 100644 index e6b9c59414aa7..0000000000000 --- a/x-pack/solutions/observability/plugins/infra/public/components/logging/log_minimap/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export { LogMinimap } from './log_minimap'; diff --git a/x-pack/solutions/observability/plugins/infra/public/components/logging/log_minimap/log_minimap.tsx b/x-pack/solutions/observability/plugins/infra/public/components/logging/log_minimap/log_minimap.tsx deleted file mode 100644 index bc9c61db300da..0000000000000 --- a/x-pack/solutions/observability/plugins/infra/public/components/logging/log_minimap/log_minimap.tsx +++ /dev/null @@ -1,184 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import styled from '@emotion/styled'; -import type { - LogEntriesSummaryBucket, - LogEntriesSummaryHighlightsBucket, - LogEntryTime, -} from '@kbn/logs-shared-plugin/common'; -import { scaleLinear } from 'd3-scale'; -import moment from 'moment'; -import * as React from 'react'; -import { COLOR_MODES_STANDARD } from '@elastic/eui'; -import { DensityChart } from './density_chart'; -import { HighlightedInterval } from './highlighted_interval'; -import { SearchMarkers } from './search_markers'; -import { TimeRuler } from './time_ruler'; - -interface Interval { - end: number; - start: number; -} - -interface LogMinimapProps { - className?: string; - height: number; - highlightedInterval: Interval | null; - jumpToTarget: (params: LogEntryTime) => any; - summaryBuckets: LogEntriesSummaryBucket[]; - summaryHighlightBuckets?: LogEntriesSummaryHighlightsBucket[]; - target: number | null; - start: number | null; - end: number | null; - width: number; -} - -interface LogMinimapState { - target: number | null; - timeCursorY: number; -} - -// Wide enough to fit "September" -const TIMERULER_WIDTH = 50; - -function calculateYScale(start: number | null, end: number | null, height: number) { - return scaleLinear() - .domain([start || 0, end || 0]) - .range([0, height]); -} - -export class LogMinimap extends React.Component { - constructor(props: LogMinimapProps) { - super(props); - this.state = { - timeCursorY: 0, - target: props.target, - }; - } - - public handleClick: React.MouseEventHandler = (event) => { - const minimapTop = event.currentTarget.getBoundingClientRect().top; - const clickedYPosition = event.clientY - minimapTop; - - const clickedTime = Math.floor(this.getYScale().invert(clickedYPosition)); - - this.props.jumpToTarget({ - tiebreaker: 0, - time: moment(clickedTime).toISOString(), - }); - }; - - public getYScale = () => { - const { start, end, height } = this.props; - return calculateYScale(start, end, height); - }; - - public getPositionOfTime = (time: number) => { - return this.getYScale()(time) ?? 0; - }; - - private updateTimeCursor: React.MouseEventHandler = (event) => { - const svgPosition = event.currentTarget.getBoundingClientRect(); - const timeCursorY = event.clientY - svgPosition.top; - - this.setState({ timeCursorY }); - }; - - public render() { - const { - start, - end, - className, - height, - highlightedInterval, - jumpToTarget, - summaryBuckets, - summaryHighlightBuckets, - width, - } = this.props; - const { timeCursorY, target } = this.state; - const [minTime, maxTime] = calculateYScale(start, end, height).domain(); - const tickCount = height ? Math.floor(height / 50) : 12; - - return ( - - - - - - - - - - {highlightedInterval ? ( - - ) : null} - - - ); - } -} - -const MinimapBorder = styled.line` - stroke: ${(props) => props.theme.euiTheme.colors.mediumShade}; - stroke-width: 1px; -`; - -const TimeCursor = styled.line` - pointer-events: none; - stroke-width: 1px; - stroke: ${(props) => - props.theme.colorMode === COLOR_MODES_STANDARD.dark - ? props.theme.euiTheme.colors.darkestShade - : props.theme.euiTheme.colors.darkShade}; -`; - -const MinimapWrapper = styled.svg` - cursor: pointer; - fill: ${(props) => props.theme.euiTheme.colors.emptyShade}; - & ${TimeCursor} { - visibility: hidden; - } - &:hover ${TimeCursor} { - visibility: visible; - } -`; diff --git a/x-pack/solutions/observability/plugins/infra/public/components/logging/log_minimap/search_marker.tsx b/x-pack/solutions/observability/plugins/infra/public/components/logging/log_minimap/search_marker.tsx deleted file mode 100644 index 12ec50da28eaa..0000000000000 --- a/x-pack/solutions/observability/plugins/infra/public/components/logging/log_minimap/search_marker.tsx +++ /dev/null @@ -1,125 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { FormattedMessage } from '@kbn/i18n-react'; -import styled from '@emotion/styled'; -import { keyframes } from '@emotion/react'; -import type { - LogEntriesSummaryHighlightsBucket, - LogEntryTime, -} from '@kbn/logs-shared-plugin/common'; -import * as React from 'react'; -import { SearchMarkerTooltip } from './search_marker_tooltip'; - -interface SearchMarkerProps { - bucket: LogEntriesSummaryHighlightsBucket; - height: number; - width: number; - jumpToTarget: (target: LogEntryTime) => void; -} - -interface SearchMarkerState { - hoveredPosition: ClientRect | null; -} - -export class SearchMarker extends React.PureComponent { - public readonly state: SearchMarkerState = { - hoveredPosition: null, - }; - - public handleClick: React.MouseEventHandler = (evt) => { - evt.stopPropagation(); - - this.props.jumpToTarget(this.props.bucket.representativeKey); - }; - - public handleMouseEnter: React.MouseEventHandler = (evt) => { - this.setState({ - hoveredPosition: evt.currentTarget.getBoundingClientRect(), - }); - }; - - public handleMouseLeave: React.MouseEventHandler = () => { - this.setState({ - hoveredPosition: null, - }); - }; - - public render() { - const { bucket, height, width } = this.props; - const { hoveredPosition } = this.state; - - const bulge = - bucket.entriesCount > 1 ? ( - - ) : ( - <> - - - - ); - - return ( - <> - {hoveredPosition ? ( - - - - ) : null} - - - {bulge} - - - ); - } -} - -const fadeInAnimation = keyframes` - from { - opacity: 0; - } - to { - opacity: 1; - } -`; - -const SearchMarkerGroup = styled.g` - animation: ${fadeInAnimation} ${(props) => props.theme.euiTheme.animation.extraSlow} ease-in both; -`; - -const SearchMarkerBackgroundRect = styled.rect` - fill: ${(props) => props.theme.euiTheme.colors.accent}; - opacity: 0; - transition: opacity ${(props) => props.theme.euiTheme.animation.normal} ease-in; - cursor: pointer; - - ${SearchMarkerGroup}:hover & { - opacity: 0.3; - } -`; - -const SearchMarkerForegroundRect = styled.rect` - fill: ${(props) => props.theme.euiTheme.colors.accent}; -`; diff --git a/x-pack/solutions/observability/plugins/infra/public/components/logging/log_minimap/search_marker_tooltip.tsx b/x-pack/solutions/observability/plugins/infra/public/components/logging/log_minimap/search_marker_tooltip.tsx deleted file mode 100644 index 64561a528051d..0000000000000 --- a/x-pack/solutions/observability/plugins/infra/public/components/logging/log_minimap/search_marker_tooltip.tsx +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { WithEuiThemeProps } from '@elastic/eui'; -import { calculatePopoverPosition, EuiPortal, withEuiTheme } from '@elastic/eui'; -// @ts-expect-error style types not defined -import { euiToolTipStyles } from '@elastic/eui/lib/components/tool_tip/tool_tip.styles'; -import { css } from '@emotion/react'; -import * as React from 'react'; - -import { AutoSizer } from '../../auto_sizer'; - -const POPOVER_ARROW_SIZE = 12; // px, to position it later - -interface SearchMarkerTooltipProps { - markerPosition: ClientRect; - children: React.ReactNode; -} - -export class _SearchMarkerTooltip extends React.PureComponent< - SearchMarkerTooltipProps & WithEuiThemeProps -> { - public render() { - const { children, markerPosition, theme } = this.props; - const styles = euiToolTipStyles(theme); - - return ( - -
- - {({ measureRef, bounds: { width, height } }) => { - const { top, left } = - width && height - ? calculatePopoverPosition(markerPosition, { width, height }, 'left', 16, [ - 'left', - ]) - : { - left: -9999, // render off-screen before the first measurement - top: 0, - }; - - return ( -
-
-
{children}
-
- ); - }} - -
- - ); - } -} - -export const SearchMarkerTooltip = withEuiTheme(_SearchMarkerTooltip); diff --git a/x-pack/solutions/observability/plugins/infra/public/components/logging/log_minimap/search_markers.tsx b/x-pack/solutions/observability/plugins/infra/public/components/logging/log_minimap/search_markers.tsx deleted file mode 100644 index 3fc7da615492e..0000000000000 --- a/x-pack/solutions/observability/plugins/infra/public/components/logging/log_minimap/search_markers.tsx +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { - LogEntriesSummaryHighlightsBucket, - LogEntryTime, -} from '@kbn/logs-shared-plugin/common'; -import classNames from 'classnames'; -import { scaleTime } from 'd3-scale'; -import * as React from 'react'; -import { SearchMarker } from './search_marker'; - -interface SearchMarkersProps { - buckets: LogEntriesSummaryHighlightsBucket[]; - className?: string; - end: number; - start: number; - width: number; - height: number; - jumpToTarget: (target: LogEntryTime) => void; -} - -export class SearchMarkers extends React.PureComponent { - public render() { - const { buckets, start, end, width, height, jumpToTarget, className } = this.props; - const classes = classNames('minimapSearchMarkers', className); - - if (start >= end || height <= 0 || Object.keys(buckets).length <= 0) { - return null; - } - - const yScale = scaleTime().domain([start, end]).range([0, height]); - - return ( - - {buckets.map((bucket) => ( - - - - ))} - - ); - } -} diff --git a/x-pack/solutions/observability/plugins/infra/public/components/logging/log_minimap/time_label_formatter.tsx b/x-pack/solutions/observability/plugins/infra/public/components/logging/log_minimap/time_label_formatter.tsx deleted file mode 100644 index 0fd3cd131da21..0000000000000 --- a/x-pack/solutions/observability/plugins/infra/public/components/logging/log_minimap/time_label_formatter.tsx +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -// The default d3-time-format is a bit strange for small ranges, so we will specify our own -export function getTimeLabelFormat(start: number, end: number): string | undefined { - const diff = Math.abs(end - start); - - // 15 seconds - if (diff < 15 * 1000) { - return ':%S.%L'; - } - - // 16 minutes - if (diff < 16 * 60 * 1000) { - return '%I:%M:%S'; - } - - // Use D3's default - return; -} diff --git a/x-pack/solutions/observability/plugins/infra/public/components/logging/log_minimap/time_ruler.tsx b/x-pack/solutions/observability/plugins/infra/public/components/logging/log_minimap/time_ruler.tsx deleted file mode 100644 index 93df67958144a..0000000000000 --- a/x-pack/solutions/observability/plugins/infra/public/components/logging/log_minimap/time_ruler.tsx +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { scaleTime } from 'd3-scale'; -import * as React from 'react'; -import styled from '@emotion/styled'; -import { COLOR_MODES_STANDARD, useEuiFontSize } from '@elastic/eui'; -import { useKibanaTimeZoneSetting } from '../../../hooks/use_kibana_time_zone_setting'; -import { getTimeLabelFormat } from './time_label_formatter'; - -interface TimeRulerProps { - end: number; - height: number; - start: number; - tickCount: number; - width: number; -} - -const useZonedDate = (timestamp: number) => { - const timeZone = useKibanaTimeZoneSetting(); - - const options = timeZone !== 'local' ? { timeZone } : undefined; - return new Date(new Date(timestamp).toLocaleString('en-US', options)); -}; - -export const TimeRuler: React.FC = ({ end, height, start, tickCount, width }) => { - const startWithOffset = useZonedDate(start); - const endWithOffset = useZonedDate(end); - - const yScale = scaleTime().domain([startWithOffset, endWithOffset]).range([0, height]); - - const ticks = yScale.ticks(tickCount); - const formatTick = yScale.tickFormat( - tickCount, - getTimeLabelFormat(startWithOffset.getTime(), endWithOffset.getTime()) - ); - - return ( - - {ticks.map((tick, tickIndex) => { - const y = yScale(tick) ?? 0; - - return ( - - - {formatTick(tick)} - - - - ); - })} - - ); -}; - -TimeRuler.displayName = 'TimeRuler'; - -const TimeRulerTickLabel = styled.text` - font-size: ${() => useEuiFontSize('xxxs').fontSize}; - line-height: ${() => useEuiFontSize('s').lineHeight}; - fill: ${(props) => props.theme.euiTheme.colors.textSubdued}; - user-select: none; - pointer-events: none; -`; - -const TimeRulerGridLine = styled.line` - stroke: ${(props) => - props.theme.colorMode === COLOR_MODES_STANDARD.dark - ? props.theme.euiTheme.colors.darkestShade - : props.theme.euiTheme.colors.darkShade}; - stroke-opacity: 0.5; - stroke-width: 1px; -`; diff --git a/x-pack/solutions/observability/plugins/infra/public/components/logging/log_search_controls/index.ts b/x-pack/solutions/observability/plugins/infra/public/components/logging/log_search_controls/index.ts deleted file mode 100644 index 5122c14e6f108..0000000000000 --- a/x-pack/solutions/observability/plugins/infra/public/components/logging/log_search_controls/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export { LogSearchControls } from './log_search_controls'; diff --git a/x-pack/solutions/observability/plugins/infra/public/components/logging/log_search_controls/log_search_buttons.tsx b/x-pack/solutions/observability/plugins/infra/public/components/logging/log_search_controls/log_search_buttons.tsx deleted file mode 100644 index dacbbb78eb93f..0000000000000 --- a/x-pack/solutions/observability/plugins/infra/public/components/logging/log_search_controls/log_search_buttons.tsx +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n-react'; -import type { LogEntryTime } from '@kbn/logs-shared-plugin/common'; -import classNames from 'classnames'; -import * as React from 'react'; - -interface LogSearchButtonsProps { - className?: string; - jumpToTarget: (target: LogEntryTime) => void; - previousSearchResult: LogEntryTime | null; - nextSearchResult: LogEntryTime | null; -} - -export class LogSearchButtons extends React.PureComponent { - public handleJumpToPreviousSearchResult: React.MouseEventHandler = () => { - const { jumpToTarget, previousSearchResult } = this.props; - - if (previousSearchResult) { - jumpToTarget(previousSearchResult); - } - }; - - public handleJumpToNextSearchResult: React.MouseEventHandler = () => { - const { jumpToTarget, nextSearchResult } = this.props; - - if (nextSearchResult) { - jumpToTarget(nextSearchResult); - } - }; - - public render() { - const { className, previousSearchResult, nextSearchResult } = this.props; - - const classes = classNames('searchButtons', className); - const hasPreviousSearchResult = !!previousSearchResult; - const hasNextSearchResult = !!nextSearchResult; - - return ( - - - - - - - - - - - - - ); - } -} diff --git a/x-pack/solutions/observability/plugins/infra/public/components/logging/log_search_controls/log_search_controls.tsx b/x-pack/solutions/observability/plugins/infra/public/components/logging/log_search_controls/log_search_controls.tsx deleted file mode 100644 index a834408037d93..0000000000000 --- a/x-pack/solutions/observability/plugins/infra/public/components/logging/log_search_controls/log_search_controls.tsx +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import classNames from 'classnames'; -import * as React from 'react'; - -import type { LogEntryTime } from '@kbn/logs-shared-plugin/common'; -import { LogSearchButtons } from './log_search_buttons'; -import { LogSearchInput } from './log_search_input'; - -interface LogSearchControlsProps { - className?: string; - clearSearch: () => any; - isLoadingSearchResults: boolean; - previousSearchResult: LogEntryTime | null; - nextSearchResult: LogEntryTime | null; - jumpToTarget: (target: LogEntryTime) => any; - search: (query: string) => any; -} - -export class LogSearchControls extends React.PureComponent { - public render() { - const { - className, - clearSearch, - isLoadingSearchResults, - previousSearchResult, - nextSearchResult, - jumpToTarget, - search, - } = this.props; - - const classes = classNames('searchControls', className); - - return ( - - - - - - - - - ); - } -} diff --git a/x-pack/solutions/observability/plugins/infra/public/components/logging/log_search_controls/log_search_input.tsx b/x-pack/solutions/observability/plugins/infra/public/components/logging/log_search_controls/log_search_input.tsx deleted file mode 100644 index 25aa19d556cbe..0000000000000 --- a/x-pack/solutions/observability/plugins/infra/public/components/logging/log_search_controls/log_search_input.tsx +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EuiFieldSearch } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import styled from '@emotion/styled'; -import classNames from 'classnames'; -import * as React from 'react'; - -interface LogSearchInputProps { - className?: string; - isLoading: boolean; - onSearch: (query: string) => void; - onClear: () => void; -} - -interface LogSearchInputState { - query: string; -} - -export const LogSearchInput = class extends React.PureComponent< - LogSearchInputProps, - LogSearchInputState -> { - public static displayName = 'LogSearchInput'; - public readonly state = { - query: '', - }; - - public handleSubmit: React.FormEventHandler = (evt) => { - evt.preventDefault(); - - const { query } = this.state; - - if (query === '') { - this.props.onClear(); - } else { - this.props.onSearch(this.state.query); - } - }; - - public handleChangeQuery: React.ChangeEventHandler = (evt) => { - this.setState({ - query: evt.target.value, - }); - }; - - public render() { - const { className, isLoading } = this.props; - const { query } = this.state; - - const classes = classNames('loggingSearchInput', className); - - return ( -
- - - ); - } -}; - -const PlainSearchField = styled(EuiFieldSearch)` - background: transparent; - box-shadow: none; - - &:focus { - box-shadow: inset 0 -2px 0 0 ${(props) => props.theme.euiTheme.colors.primary}; - } -`; diff --git a/x-pack/solutions/observability/plugins/infra/public/components/logging/log_statusbar.tsx b/x-pack/solutions/observability/plugins/infra/public/components/logging/log_statusbar.tsx deleted file mode 100644 index 4aa8508f3285f..0000000000000 --- a/x-pack/solutions/observability/plugins/infra/public/components/logging/log_statusbar.tsx +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EuiFlexGroup, type EuiFlexGroupProps, EuiFlexItem, useEuiTheme } from '@elastic/eui'; -import { css } from '@emotion/css'; -import React from 'react'; - -export const LogStatusbar = (props: EuiFlexGroupProps) => { - const { euiTheme } = useEuiTheme(); - - return ( - - ); -}; - -export const LogStatusbarItem = EuiFlexItem; diff --git a/x-pack/solutions/observability/plugins/infra/public/components/logging/log_text_scale_controls.tsx b/x-pack/solutions/observability/plugins/infra/public/components/logging/log_text_scale_controls.tsx deleted file mode 100644 index eb00a998cf42a..0000000000000 --- a/x-pack/solutions/observability/plugins/infra/public/components/logging/log_text_scale_controls.tsx +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EuiFormRow, EuiRadioGroup } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n-react'; -import * as React from 'react'; - -import type { TextScale } from '../../../common/log_text_scale'; -import { isTextScale } from '../../../common/log_text_scale'; - -interface LogTextScaleControlsProps { - availableTextScales: TextScale[]; - textScale: TextScale; - setTextScale: (scale: TextScale) => any; -} - -export class LogTextScaleControls extends React.PureComponent { - public setTextScale = (textScale: string) => { - if (isTextScale(textScale)) { - this.props.setTextScale(textScale); - } - }; - - public render() { - const { availableTextScales, textScale } = this.props; - - return ( - - } - > - ({ - id: availableTextScale.toString(), - label: ( - - ), - }))} - idSelected={textScale} - onChange={this.setTextScale} - /> - - ); - } -} diff --git a/x-pack/solutions/observability/plugins/infra/public/components/logging/log_text_wrap_controls.tsx b/x-pack/solutions/observability/plugins/infra/public/components/logging/log_text_wrap_controls.tsx deleted file mode 100644 index c84643a6f394a..0000000000000 --- a/x-pack/solutions/observability/plugins/infra/public/components/logging/log_text_wrap_controls.tsx +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EuiFormRow, EuiSwitch } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n-react'; -import * as React from 'react'; - -interface LogTextWrapControlsProps { - wrap: boolean; - setTextWrap: (scale: boolean) => any; -} - -export class LogTextWrapControls extends React.PureComponent { - public toggleWrap = () => { - this.props.setTextWrap(!this.props.wrap); - }; - - public render() { - const { wrap } = this.props; - - return ( - - } - > - - } - checked={wrap} - onChange={this.toggleWrap} - /> - - ); - } -} diff --git a/x-pack/solutions/observability/plugins/infra/public/containers/logs/log_view_configuration.test.tsx b/x-pack/solutions/observability/plugins/infra/public/containers/logs/log_view_configuration.test.tsx deleted file mode 100644 index 1fe14d9b2965b..0000000000000 --- a/x-pack/solutions/observability/plugins/infra/public/containers/logs/log_view_configuration.test.tsx +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { mountHook } from '@kbn/test-jest-helpers'; - -import { useLogViewConfiguration } from './log_view_configuration'; - -describe('useLogViewConfiguration hook', () => { - describe('textScale state', () => { - it('has a default value', () => { - const { getLastHookValue } = mountHook(() => useLogViewConfiguration().textScale); - - expect(getLastHookValue()).toEqual('medium'); - }); - - it('can be updated', () => { - const { act, getLastHookValue } = mountHook(() => useLogViewConfiguration()); - - act(({ setTextScale }) => { - setTextScale('small'); - }); - - expect(getLastHookValue().textScale).toEqual('small'); - }); - }); - - describe('textWrap state', () => { - it('has a default value', () => { - const { getLastHookValue } = mountHook(() => useLogViewConfiguration().textWrap); - - expect(getLastHookValue()).toEqual(true); - }); - - it('can be updated', () => { - const { act, getLastHookValue } = mountHook(() => useLogViewConfiguration()); - - act(({ setTextWrap }) => { - setTextWrap(false); - }); - - expect(getLastHookValue().textWrap).toEqual(false); - }); - }); - - it('provides the available text scales', () => { - const { getLastHookValue } = mountHook(() => useLogViewConfiguration().availableTextScales); - - expect(getLastHookValue()).toEqual(expect.any(Array)); - expect(getLastHookValue().length).toBeGreaterThan(0); - }); -}); diff --git a/x-pack/solutions/observability/plugins/infra/public/containers/logs/log_view_configuration.tsx b/x-pack/solutions/observability/plugins/infra/public/containers/logs/log_view_configuration.tsx deleted file mode 100644 index 543cc61d78977..0000000000000 --- a/x-pack/solutions/observability/plugins/infra/public/containers/logs/log_view_configuration.tsx +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import createContainer from 'constate'; -import { useState } from 'react'; - -export type TextScale = 'small' | 'medium' | 'large'; - -export const useLogViewConfiguration = () => { - // text scale - const [textScale, setTextScale] = useState('medium'); - - // text wrap - const [textWrap, setTextWrap] = useState(true); - - return { - availableTextScales, - setTextScale, - setTextWrap, - textScale, - textWrap, - }; -}; - -export const [LogViewConfigurationProvider, useLogViewConfigurationContext] = - createContainer(useLogViewConfiguration); - -/** - * constants - */ - -export const availableTextScales: TextScale[] = ['large', 'medium', 'small']; diff --git a/x-pack/solutions/observability/plugins/infra/public/containers/logs/with_log_textview.tsx b/x-pack/solutions/observability/plugins/infra/public/containers/logs/with_log_textview.tsx deleted file mode 100644 index 7a394cd94fc13..0000000000000 --- a/x-pack/solutions/observability/plugins/infra/public/containers/logs/with_log_textview.tsx +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { useMemo } from 'react'; - -import { UrlStateContainer } from '../../utils/url_state'; -import type { TextScale } from './log_view_configuration'; -import { availableTextScales, useLogViewConfigurationContext } from './log_view_configuration'; - -interface LogTextviewUrlState { - textScale?: TextScale; - wrap?: boolean; -} - -export const WithLogTextviewUrlState = () => { - const { textScale, textWrap, setTextScale, setTextWrap } = useLogViewConfigurationContext(); - - const urlState = useMemo(() => ({ textScale, wrap: textWrap }), [textScale, textWrap]); - - return ( - { - if (newUrlState && newUrlState.textScale) { - setTextScale(newUrlState.textScale); - } - if (newUrlState && typeof newUrlState.wrap !== 'undefined') { - setTextWrap(newUrlState.wrap); - } - }} - onInitialize={(newUrlState) => { - if (newUrlState && newUrlState.textScale) { - setTextScale(newUrlState.textScale); - } - if (newUrlState && typeof newUrlState.wrap !== 'undefined') { - setTextWrap(newUrlState.wrap); - } - }} - /> - ); -}; - -const mapToUrlState = (value: any): LogTextviewUrlState | undefined => - value - ? { - textScale: mapToTextScaleUrlState(value.textScale), - wrap: mapToWrapUrlState(value.wrap), - } - : undefined; - -const mapToTextScaleUrlState = (value: any) => - availableTextScales.includes(value) ? (value as TextScale) : undefined; - -const mapToWrapUrlState = (value: any) => (typeof value === 'boolean' ? value : undefined); diff --git a/x-pack/solutions/observability/plugins/infra/public/hooks/use_observable.ts b/x-pack/solutions/observability/plugins/infra/public/hooks/use_observable.ts index 167f55dfea546..d75acb84160a0 100644 --- a/x-pack/solutions/observability/plugins/infra/public/hooks/use_observable.ts +++ b/x-pack/solutions/observability/plugins/infra/public/hooks/use_observable.ts @@ -7,7 +7,7 @@ import { useEffect, useRef, useState } from 'react'; import type { OperatorFunction, PartialObserver, Subscription } from 'rxjs'; -import { BehaviorSubject, Observable, ReplaySubject } from 'rxjs'; +import { BehaviorSubject, Observable } from 'rxjs'; import { switchMap } from 'rxjs'; export const useLatest = (value: Value) => { @@ -52,22 +52,6 @@ export const useBehaviorSubject = < return [output$, next] as const; }; -export const useReplaySubject = < - InputValue, - OutputValue, - OutputObservable extends Observable ->( - deriveObservableOnce: (input$: Observable) => OutputObservable -) => { - const [[subject$, next], _] = useState(() => { - const newSubject$ = new ReplaySubject(); - const newNext = newSubject$.next.bind(newSubject$); - return [newSubject$, newNext] as const; - }); - const [output$] = useState(() => deriveObservableOnce(subject$)); - return [output$, next] as const; -}; - export const useObservableState = ( state$: Observable, initialState: InitialState | (() => InitialState) diff --git a/x-pack/solutions/observability/plugins/infra/public/observability_logs/log_stream_page/state/README.md b/x-pack/solutions/observability/plugins/infra/public/observability_logs/log_stream_page/state/README.md deleted file mode 100644 index c2f9154a08638..0000000000000 --- a/x-pack/solutions/observability/plugins/infra/public/observability_logs/log_stream_page/state/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# @kbn/observability-logs-log-stream-page-state - -The state machine of the Observability Logs UI stream page. diff --git a/x-pack/solutions/observability/plugins/infra/public/observability_logs/log_stream_page/state/index.ts b/x-pack/solutions/observability/plugins/infra/public/observability_logs/log_stream_page/state/index.ts deleted file mode 100644 index 3b2a320ae181f..0000000000000 --- a/x-pack/solutions/observability/plugins/infra/public/observability_logs/log_stream_page/state/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export * from './src'; diff --git a/x-pack/solutions/observability/plugins/infra/public/observability_logs/log_stream_page/state/src/index.ts b/x-pack/solutions/observability/plugins/infra/public/observability_logs/log_stream_page/state/src/index.ts deleted file mode 100644 index 22f33aa56174c..0000000000000 --- a/x-pack/solutions/observability/plugins/infra/public/observability_logs/log_stream_page/state/src/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export * from './provider'; -export * from './selectors'; -export * from './state_machine'; -export * from './types'; diff --git a/x-pack/solutions/observability/plugins/infra/public/observability_logs/log_stream_page/state/src/initial_parameters_service.ts b/x-pack/solutions/observability/plugins/infra/public/observability_logs/log_stream_page/state/src/initial_parameters_service.ts deleted file mode 100644 index 4f307e3a3152e..0000000000000 --- a/x-pack/solutions/observability/plugins/infra/public/observability_logs/log_stream_page/state/src/initial_parameters_service.ts +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { RefreshInterval } from '@kbn/data-plugin/public'; -import type { InvokeCreator, Receiver } from 'xstate'; -import type { TimeKey } from '../../../../../common/time'; -import type { VisiblePositions } from '../../../log_stream_position_state'; -import type { ExtendedTimeRange, ParsedQuery, Timestamps } from '../../../log_stream_query_state'; -import type { LogStreamPageContext, LogStreamPageEvent } from './types'; - -export const waitForInitialQueryParameters = - (): InvokeCreator => - (_context, _event) => - (send, onEvent: Receiver) => { - // constituents of the set of initial parameters - let latestValidQuery: ParsedQuery | undefined; - let latestTimeRange: ExtendedTimeRange | undefined; - let latestRefreshInterval: RefreshInterval | undefined; - let latestTimestamps: Timestamps | undefined; - - onEvent((event) => { - switch (event.type) { - // event types that deliver the parameters - case 'VALID_QUERY_CHANGED': - case 'INVALID_QUERY_CHANGED': - latestValidQuery = event.parsedQuery; - break; - case 'TIME_CHANGED': - latestTimeRange = event.timeRange; - latestRefreshInterval = event.refreshInterval; - latestTimestamps = event.timestamps; - break; - } - - // if all constituents of the parameters have been delivered - if ( - latestValidQuery !== undefined && - latestTimeRange !== undefined && - latestRefreshInterval !== undefined && - latestTimestamps !== undefined - ) { - send({ - type: 'RECEIVED_INITIAL_QUERY_PARAMETERS', - validatedQuery: latestValidQuery, - timeRange: latestTimeRange, - refreshInterval: latestRefreshInterval, - timestamps: latestTimestamps, - }); - } - }); - }; - -export const waitForInitialPositionParameters = - (): InvokeCreator => - (_context, _event) => - (send, onEvent: Receiver) => { - // constituents of the set of initial parameters - let latestTargetPosition: TimeKey | null; - let latestLatestPosition: TimeKey | null; - let latestVisiblePositions: VisiblePositions; - - onEvent((event) => { - switch (event.type) { - case 'POSITIONS_CHANGED': - latestTargetPosition = event.targetPosition; - latestLatestPosition = event.latestPosition; - latestVisiblePositions = event.visiblePositions; - break; - } - - // if all constituents of the parameters have been delivered - if ( - latestTargetPosition !== undefined && - latestLatestPosition !== undefined && - latestVisiblePositions !== undefined - ) { - send({ - type: 'RECEIVED_INITIAL_POSITION_PARAMETERS', - targetPosition: latestTargetPosition, - latestPosition: latestLatestPosition, - visiblePositions: latestVisiblePositions, - }); - } - }); - }; diff --git a/x-pack/solutions/observability/plugins/infra/public/observability_logs/log_stream_page/state/src/provider.tsx b/x-pack/solutions/observability/plugins/infra/public/observability_logs/log_stream_page/state/src/provider.tsx deleted file mode 100644 index 0da26759b6a04..0000000000000 --- a/x-pack/solutions/observability/plugins/infra/public/observability_logs/log_stream_page/state/src/provider.tsx +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { useInterpret } from '@xstate/react'; -import createContainer from 'constate'; -import useMount from 'react-use/lib/useMount'; -import { isDevMode } from '../../../../utils/dev_mode'; -import { - createLogStreamPageStateMachine, - type LogStreamPageStateMachineDependencies, -} from './state_machine'; - -export const useLogStreamPageState = ({ - kibanaQuerySettings, - logViewStateNotifications, - queryStringService, - toastsService, - filterManagerService, - urlStateStorage, - useDevTools = isDevMode(), - timeFilterService, -}: { - useDevTools?: boolean; -} & LogStreamPageStateMachineDependencies) => { - useMount(() => { - // eslint-disable-next-line no-console - console.log( - "A warning in console stating: 'The result of getSnapshot should be cached to avoid an infinite loop' is expected. This will be fixed once we can upgrade versions." - ); - }); - - const logStreamPageStateService = useInterpret( - () => - createLogStreamPageStateMachine({ - kibanaQuerySettings, - logViewStateNotifications, - queryStringService, - toastsService, - filterManagerService, - urlStateStorage, - timeFilterService, - }), - { devTools: useDevTools } - ); - - return logStreamPageStateService; -}; - -export const [LogStreamPageStateProvider, useLogStreamPageStateContext] = - createContainer(useLogStreamPageState); diff --git a/x-pack/solutions/observability/plugins/infra/public/observability_logs/log_stream_page/state/src/selectors.ts b/x-pack/solutions/observability/plugins/infra/public/observability_logs/log_stream_page/state/src/selectors.ts deleted file mode 100644 index b173620892478..0000000000000 --- a/x-pack/solutions/observability/plugins/infra/public/observability_logs/log_stream_page/state/src/selectors.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { MatchedStateFromActor } from '@kbn/xstate-utils'; -import type { LogStreamQueryActorRef } from '../../../log_stream_query_state'; -import type { LogStreamPageActorRef } from './state_machine'; - -type LogStreamPageStateWithLogViewIndices = - | MatchedStateFromActor - | MatchedStateFromActor - | MatchedStateFromActor; - -export const selectLogStreamQueryChildService = ( - state: LogStreamPageStateWithLogViewIndices -): LogStreamQueryActorRef => state.children.logStreamQuery; diff --git a/x-pack/solutions/observability/plugins/infra/public/observability_logs/log_stream_page/state/src/state_machine.ts b/x-pack/solutions/observability/plugins/infra/public/observability_logs/log_stream_page/state/src/state_machine.ts deleted file mode 100644 index 0797d629885f6..0000000000000 --- a/x-pack/solutions/observability/plugins/infra/public/observability_logs/log_stream_page/state/src/state_machine.ts +++ /dev/null @@ -1,355 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { RefreshInterval } from '@kbn/data-plugin/public'; -import type { TimeRange } from '@kbn/es-query'; -import type { ActorRefFrom, EmittedFrom } from 'xstate'; -import { actions, createMachine } from 'xstate'; -import { DEFAULT_REFRESH_INTERVAL } from '@kbn/logs-shared-plugin/common'; -import type { LogViewNotificationChannel } from '@kbn/logs-shared-plugin/public'; -import type { OmitDeprecatedState } from '@kbn/xstate-utils'; -import { datemathToEpochMillis } from '../../../../utils/datemath'; -import { createLogStreamPositionStateMachine } from '../../../log_stream_position_state/src/state_machine'; -import type { LogStreamQueryStateMachineDependencies } from '../../../log_stream_query_state'; -import { - createLogStreamQueryStateMachine, - DEFAULT_TIMERANGE, -} from '../../../log_stream_query_state'; -import { - waitForInitialQueryParameters, - waitForInitialPositionParameters, -} from './initial_parameters_service'; -import type { - LogStreamPageContext, - LogStreamPageContextWithLogView, - LogStreamPageContextWithLogViewError, - LogStreamPageContextWithPositions, - LogStreamPageContextWithQuery, - LogStreamPageContextWithTime, - LogStreamPageEvent, - LogStreamPageTypestate, -} from './types'; - -export const createPureLogStreamPageStateMachine = (initialContext: LogStreamPageContext = {}) => - /** @xstate-layout N4IgpgJg5mDOIC5QBsD2UDKAXATmAhgLYAK+M2+WYAdAK4B2Alk1o-sowF6QDEAMgHkAggBEAkgDkA4gH1BsgGpiAogHUZGACpCASpuUiA2gAYAuolAAHVLEatU9CyAAeiALQAmAJzUPHgGwArIEALAAcHmHhXsEhADQgAJ6IAQDM1IHGYQDs2ZnZxl5e-l6pAL5lCWiYuAQkZGAUVHRMLGwc3BD8wuLScgKKKuoAYkJifAYm5kgg1rb2jjOuCJ4hAIzUqZklHgX+xqlrkQnJCKlB1P4loYH+qV7hHoEVVejYeESk5FiUNAzMdnaXF4glEklk8hkSjUGgAqgBheHKAyTMxOOaAhxOZbZNbZajGXLBVIkrJrYInRBHYzUEIeVIhVJhNZZLws7IhF4garvOpfRo-Zr-NrsYFdUG9CEDKFDOGI5EiSZraZWGyYxagHEhEIEwkeI7Zc7GO6UhCZHzZML7MKBDwhQp0zmVblvWqfBpNGhofAQZhQPjoBSMMAAd26YL6kOhIzGEyMaJmGIW2JS4VpJTCWXp5K82Q8pvN1Et1tt9oedq5PLd9W+v2o3t99H9geDYYl4P6gxhGARSJR8ZVszVyaWiEZPnt-m8Ry8xjta38pqN1FK+uy+w8xhCgS8lddHxrArrDb9AagQdD4clnZl3d7CqVg6TjCxo4QISumy2MWNMWiAVNO58WMFkImMOc50CMI9xqA9+U9etUB9U8W1DYZ8EYZAQR6Dso1lLRdH0Ad0WHF8NRcdxvF8AJYgiKIwhiUIC1SDwMgNcC8g5O1nmdKs4I9QUaAAC3wWAzwvEMxHoX0AGM4GoAFWFFTg-QARVoMAcESHgdGUJExAUAwZEkMRNDEIQ+BkVTYWUHQAE0ZGIXQhAAWWUfQdAwKYSPmMinFOKcNgKXYwnOPMbRYhJlhCYoYN5d1a2aESxNQyTpMYOTYAUkUOjUjStJ4BQLLEEQrJs+yZHhAAJIRpFRJ9SNfTUx2KAtDSLOdLTCyJAhYuLq3gwTqGS8TWyk2T5MUoEVKbdTNO0yQir4EqytshzqtqqR6p89UU3fVqkkQS1WNKXEoP8cLeo8fr+MS4TRNG0Nxoyyacq4PL5p4My3Mqmq6uIxNGvI6KDtOW1Ag6kLuoi67eP3PkBLrEbUuezLssBZS-WIIHYB0vTlAMoyTLMizHIEDBTLEAQJEc5y3I8ryE1VXymoo-bF0OhAGI2EDbVCi6er6uHYIRu7hoelH0rRqbMabbGWfoXHiHJynqYwX7Nu2wGFb2mKOdOIofEKBksgFmGbtFo8kol88xql16MY6XglpW6y1o1-7vO13a3wXPJaTWCJbSne0SQLOcV3XJ5t0yXq1jWC2Eqt+6Uttp77aymWna6b7lA9raAeZn3moQBdCV8b99hiQ0rnzTntx1XMpxuWPDgT4X4sPBDkbTtKJszt7Oh4ZWKbMtX861ouRxLsv8XpHcq8CGup0A+OCVCVJskY4w49h14RaT7ubYk1GHaU7OeAAKVhFziBkTQBHv3Qts0MnR6piQvanvzff2OfK8KEvc4K967Gkjs3GOO826Jy7kNHuJ8M7o3PmKPGys9AygpgAIQmG-VWEhGYNR1r-cu89iiAOXnXU4WwwjgOjsEKB8cYGDSRsfO2-ckHTV4LCYgIghD6HvmIH6OhNZfyHEQmef8K4L3IcAyhR01g+DWCxTIkDd5MMRtbVOCD2FZxQdw3h-DdLDF0hgKqxkJAeSWqI58rNlizykWQ6usilwsloS3Bh7d96d2YZox6fcXoD0digpyW0ZDKAkKVTBsJhjDFsjIXSQhqqTzEcXNm9jSGLwoaaAIJ0o7uLju3Z09BUAQDgE4PiltPQ7WnmzTwDFNjbC8LsY0BwlGmjcCxAk9IOSZgXDuUotx1Fi2FEEzo1Sf4lzpKaNYdJqDknAlkG0bIghbiGcnRCyEmx+PGbYlI+JYhXCtDaXIP54icyZDQ+4-gjgCy2H4HiXiBoaK9EhRszZe7oUwpAHZwNEAQ2ZHaJpdwLpsiWYBHcRZN72njlubw0EO5PLFvAthASfl7TcOEaZEcQhR3WDaGKXVPEugPrAlhWiUXS0Hh9LSaK3zGEAvqSGXUzZXTWUfcl6cdFUrljjWlJd6WcwiKxTcUMWVC0ebddZyLOUBI4cpb53sal2KZDqPI5IllANrgWJ4TL+aXXFcS7xzzqCEEYLAWwWzJb9z5Wkw4-hfCFD8NvG0F1wUWk3pvXEXqDjlAqGUIAA */ - createMachine( - { - context: initialContext, - predictableActionArguments: true, - invoke: { - src: 'logViewNotifications', - }, - id: 'logStreamPageState', - initial: 'uninitialized', - states: { - uninitialized: { - on: { - LOADING_LOG_VIEW_STARTED: { - target: 'loadingLogView', - }, - LOADING_LOG_VIEW_FAILED: { - target: 'loadingLogViewFailed', - actions: 'storeLogViewError', - }, - LOADING_LOG_VIEW_SUCCEEDED: [ - { - target: 'hasLogViewIndices', - cond: 'hasLogViewIndices', - actions: 'storeResolvedLogView', - }, - { - target: 'missingLogViewIndices', - actions: 'storeResolvedLogView', - }, - ], - }, - }, - loadingLogView: { - on: { - LOADING_LOG_VIEW_FAILED: { - target: 'loadingLogViewFailed', - actions: 'storeLogViewError', - }, - LOADING_LOG_VIEW_SUCCEEDED: [ - { - target: 'hasLogViewIndices', - cond: 'hasLogViewIndices', - actions: 'storeResolvedLogView', - }, - { - target: 'missingLogViewIndices', - actions: 'storeResolvedLogView', - }, - ], - }, - }, - loadingLogViewFailed: { - on: { - LOADING_LOG_VIEW_STARTED: { - target: 'loadingLogView', - }, - }, - }, - hasLogViewIndices: { - initial: 'initializingQuery', - - states: { - initializingQuery: { - meta: { - _DX_warning_: - "The Query machine must be invoked and complete initialisation before the Position machine is invoked. This is due to legacy URL dependencies on the 'logPosition' key, we need to read the key before it is reset by the Position machine.", - }, - - invoke: { - src: 'waitForInitialQueryParameters', - id: 'waitForInitialQueryParameters', - }, - - on: { - RECEIVED_INITIAL_QUERY_PARAMETERS: { - target: 'initializingPositions', - actions: ['storeQuery', 'storeTime', 'forwardToLogPosition'], - }, - - VALID_QUERY_CHANGED: { - target: 'initializingQuery', - internal: true, - actions: 'forwardToInitialQueryParameters', - }, - - INVALID_QUERY_CHANGED: { - target: 'initializingQuery', - internal: true, - actions: 'forwardToInitialQueryParameters', - }, - TIME_CHANGED: { - target: 'initializingQuery', - internal: true, - actions: 'forwardToInitialQueryParameters', - }, - }, - }, - initializingPositions: { - meta: { - _DX_warning_: - "The Position machine must be invoked after the Query machine has been invoked and completed initialisation. This is due to the Query machine having some legacy URL dependencies on the 'logPosition' key, we don't want the Position machine to reset the URL parameters before the Query machine has had a chance to read them.", - }, - invoke: [ - { - src: 'waitForInitialPositionParameters', - id: 'waitForInitialPositionParameters', - }, - ], - on: { - RECEIVED_INITIAL_POSITION_PARAMETERS: { - target: 'initialized', - actions: ['storePositions'], - }, - - POSITIONS_CHANGED: { - target: 'initializingPositions', - internal: true, - actions: 'forwardToInitialPositionParameters', - }, - }, - }, - initialized: { - on: { - VALID_QUERY_CHANGED: { - target: 'initialized', - internal: true, - actions: 'storeQuery', - }, - TIME_CHANGED: { - target: 'initialized', - internal: true, - actions: ['storeTime', 'forwardToLogPosition'], - }, - POSITIONS_CHANGED: { - target: 'initialized', - internal: true, - actions: ['storePositions'], - }, - JUMP_TO_TARGET_POSITION: { - target: 'initialized', - internal: true, - actions: ['forwardToLogPosition'], - }, - REPORT_VISIBLE_POSITIONS: { - target: 'initialized', - internal: true, - actions: ['forwardToLogPosition'], - }, - UPDATE_TIME_RANGE: { - target: 'initialized', - internal: true, - actions: ['forwardToLogStreamQuery'], - }, - UPDATE_REFRESH_INTERVAL: { - target: 'initialized', - internal: true, - actions: ['forwardToLogStreamQuery'], - }, - PAGE_END_BUFFER_REACHED: { - target: 'initialized', - internal: true, - actions: ['forwardToLogStreamQuery'], - }, - }, - }, - }, - - invoke: [ - { - src: 'logStreamQuery', - id: 'logStreamQuery', - }, - { - src: 'logStreamPosition', - id: 'logStreamPosition', - }, - ], - }, - missingLogViewIndices: {}, - }, - }, - { - actions: { - forwardToInitialQueryParameters: actions.forwardTo('waitForInitialQueryParameters'), - forwardToInitialPositionParameters: actions.forwardTo('waitForInitialPositionParameters'), - forwardToLogPosition: actions.forwardTo('logStreamPosition'), - forwardToLogStreamQuery: actions.forwardTo('logStreamQuery'), - storeLogViewError: actions.assign((_context, event) => - event.type === 'LOADING_LOG_VIEW_FAILED' - ? ({ logViewError: event.error } as LogStreamPageContextWithLogViewError) - : {} - ), - storeResolvedLogView: actions.assign((_context, event) => - event.type === 'LOADING_LOG_VIEW_SUCCEEDED' - ? ({ - logViewStatus: event.status, - resolvedLogView: event.resolvedLogView, - } as LogStreamPageContextWithLogView) - : {} - ), - storeQuery: actions.assign((_context, event) => - event.type === 'RECEIVED_INITIAL_QUERY_PARAMETERS' - ? ({ - parsedQuery: event.validatedQuery, - } as LogStreamPageContextWithQuery) - : event.type === 'VALID_QUERY_CHANGED' - ? ({ - parsedQuery: event.parsedQuery, - } as LogStreamPageContextWithQuery) - : {} - ), - storeTime: actions.assign((_context, event) => { - return 'timeRange' in event && 'refreshInterval' in event && 'timestamps' in event - ? ({ - timeRange: event.timeRange, - refreshInterval: event.refreshInterval, - timestamps: event.timestamps, - } as LogStreamPageContextWithTime) - : {}; - }), - storePositions: actions.assign((_context, event) => { - return 'targetPosition' in event && - 'visiblePositions' in event && - 'latestPosition' in event - ? ({ - targetPosition: event.targetPosition, - visiblePositions: event.visiblePositions, - latestPosition: event.latestPosition, - } as LogStreamPageContextWithPositions) - : {}; - }), - }, - guards: { - hasLogViewIndices: (_context, event) => - event.type === 'LOADING_LOG_VIEW_SUCCEEDED' && - ['empty', 'available'].includes(event.status.index), - }, - } - ); - -export type LogStreamPageStateMachine = ReturnType; -export type LogStreamPageActorRef = OmitDeprecatedState>; -export type LogStreamPageState = EmittedFrom; -export type LogStreamPageSend = LogStreamPageActorRef['send']; - -export type LogStreamPageStateMachineDependencies = { - logViewStateNotifications: LogViewNotificationChannel; -} & LogStreamQueryStateMachineDependencies; - -export const createLogStreamPageStateMachine = ({ - kibanaQuerySettings, - logViewStateNotifications, - queryStringService, - toastsService, - filterManagerService, - urlStateStorage, - timeFilterService, -}: LogStreamPageStateMachineDependencies) => - createPureLogStreamPageStateMachine().withConfig({ - services: { - logViewNotifications: () => logViewStateNotifications.createService(), - logStreamQuery: (context) => { - if (!('resolvedLogView' in context)) { - throw new Error('Failed to spawn log stream query service: no LogView in context'); - } - - const nowTimestamp = Date.now(); - const initialTimeRangeExpression: TimeRange = DEFAULT_TIMERANGE; - const initialRefreshInterval: RefreshInterval = DEFAULT_REFRESH_INTERVAL; - - return createLogStreamQueryStateMachine( - { - dataViews: [context.resolvedLogView.dataViewReference], - timeRange: { - ...initialTimeRangeExpression, - lastChangedCompletely: nowTimestamp, - }, - timestamps: { - startTimestamp: datemathToEpochMillis(initialTimeRangeExpression.from, 'down') ?? 0, - endTimestamp: datemathToEpochMillis(initialTimeRangeExpression.to, 'up') ?? 0, - lastChangedTimestamp: nowTimestamp, - }, - refreshInterval: initialRefreshInterval, - }, - { - kibanaQuerySettings, - queryStringService, - toastsService, - filterManagerService, - urlStateStorage, - timeFilterService, - } - ); - }, - logStreamPosition: (context) => { - return createLogStreamPositionStateMachine( - { - targetPosition: null, - latestPosition: null, - visiblePositions: { - endKey: null, - middleKey: null, - startKey: null, - pagesBeforeStart: Infinity, - pagesAfterEnd: Infinity, - }, - }, - { - urlStateStorage, - toastsService, - } - ); - }, - waitForInitialQueryParameters: waitForInitialQueryParameters(), - waitForInitialPositionParameters: waitForInitialPositionParameters(), - }, - }); diff --git a/x-pack/solutions/observability/plugins/infra/public/observability_logs/log_stream_page/state/src/types.ts b/x-pack/solutions/observability/plugins/infra/public/observability_logs/log_stream_page/state/src/types.ts deleted file mode 100644 index 1a705bcc5c380..0000000000000 --- a/x-pack/solutions/observability/plugins/infra/public/observability_logs/log_stream_page/state/src/types.ts +++ /dev/null @@ -1,116 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { TimeRange } from '@kbn/es-query'; -import type { LogViewStatus } from '@kbn/logs-shared-plugin/common'; -import type { - LogViewContextWithError, - LogViewContextWithResolvedLogView, - LogViewNotificationEvent, -} from '@kbn/logs-shared-plugin/public'; -import type { TimeKey } from '../../../../../common/time'; -import type { - JumpToTargetPositionEvent, - LogStreamPositionContext, - ReportVisiblePositionsEvent, - VisiblePositions, -} from '../../../log_stream_position_state'; -import type { LogStreamPositionNotificationEvent } from '../../../log_stream_position_state/src/notifications'; -import type { - LogStreamQueryContextWithTime, - ParsedQuery, - UpdateRefreshIntervalEvent, - UpdateTimeRangeEvent, -} from '../../../log_stream_query_state'; -import type { LogStreamQueryNotificationEvent } from '../../../log_stream_query_state/src/notifications'; - -export interface ReceivedInitialQueryParametersEvent { - type: 'RECEIVED_INITIAL_QUERY_PARAMETERS'; - validatedQuery: ParsedQuery; - timeRange: LogStreamPageContextWithTime['timeRange']; - refreshInterval: LogStreamPageContextWithTime['refreshInterval']; - timestamps: LogStreamPageContextWithTime['timestamps']; -} - -export interface ReceivedInitialPositionParametersEvent { - type: 'RECEIVED_INITIAL_POSITION_PARAMETERS'; - targetPosition: LogStreamPageContextWithPositions['targetPosition']; - latestPosition: LogStreamPageContextWithPositions['latestPosition']; - visiblePositions: LogStreamPageContextWithPositions['visiblePositions']; -} - -export type LogStreamPageEvent = - | LogViewNotificationEvent - | LogStreamQueryNotificationEvent - | LogStreamPositionNotificationEvent - | ReceivedInitialQueryParametersEvent - | ReceivedInitialPositionParametersEvent - | JumpToTargetPositionEvent - | ReportVisiblePositionsEvent - | UpdateTimeRangeEvent - | UpdateRefreshIntervalEvent; - -export interface LogStreamPageContextWithLogView { - logViewStatus: LogViewStatus; - resolvedLogView: LogViewContextWithResolvedLogView['resolvedLogView']; -} - -export interface LogStreamPageContextWithLogViewError { - logViewError: LogViewContextWithError['error']; -} - -export interface LogStreamPageContextWithQuery { - parsedQuery: ParsedQuery; -} - -export type LogStreamPageContextWithTime = LogStreamQueryContextWithTime; -export type LogStreamPageContextWithPositions = LogStreamPositionContext; - -export type LogStreamPageTypestate = - | { - value: 'uninitialized'; - context: {}; - } - | { - value: 'loadingLogView'; - context: {}; - } - | { - value: 'loadingLogViewFailed'; - context: LogStreamPageContextWithLogViewError; - } - | { - value: 'hasLogViewIndices'; - context: LogStreamPageContextWithLogView; - } - | { - value: { hasLogViewIndices: 'uninitialized' }; - context: LogStreamPageContextWithLogView; - } - | { - value: { hasLogViewIndices: 'initialized' }; - context: LogStreamPageContextWithLogView & - LogStreamPageContextWithQuery & - LogStreamPageContextWithTime & - LogStreamPageContextWithPositions; - } - | { - value: 'missingLogViewIndices'; - context: LogStreamPageContextWithLogView; - }; - -export type LogStreamPageStateValue = LogStreamPageTypestate['value']; -export type LogStreamPageContext = LogStreamPageTypestate['context']; - -export interface LogStreamPageCallbacks { - updateTimeRange: (timeRange: Partial) => void; - jumpToTargetPosition: (targetPosition: TimeKey | null) => void; - jumpToTargetPositionTime: (time: string) => void; - reportVisiblePositions: (visiblePositions: VisiblePositions) => void; - startLiveStreaming: () => void; - stopLiveStreaming: () => void; -} diff --git a/x-pack/solutions/observability/plugins/infra/public/observability_logs/log_stream_position_state/index.ts b/x-pack/solutions/observability/plugins/infra/public/observability_logs/log_stream_position_state/index.ts deleted file mode 100644 index ddc7e818615b6..0000000000000 --- a/x-pack/solutions/observability/plugins/infra/public/observability_logs/log_stream_position_state/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export * from './src/types'; -export * from './src/defaults'; diff --git a/x-pack/solutions/observability/plugins/infra/public/observability_logs/log_stream_position_state/src/defaults.ts b/x-pack/solutions/observability/plugins/infra/public/observability_logs/log_stream_position_state/src/defaults.ts deleted file mode 100644 index 15556d8f2b4d7..0000000000000 --- a/x-pack/solutions/observability/plugins/infra/public/observability_logs/log_stream_position_state/src/defaults.ts +++ /dev/null @@ -1,9 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export const DESIRED_BUFFER_PAGES = 2; -export const RELATIVE_END_UPDATE_DELAY = 1000; diff --git a/x-pack/solutions/observability/plugins/infra/public/observability_logs/log_stream_position_state/src/notifications.ts b/x-pack/solutions/observability/plugins/infra/public/observability_logs/log_stream_position_state/src/notifications.ts deleted file mode 100644 index f036bec8cb512..0000000000000 --- a/x-pack/solutions/observability/plugins/infra/public/observability_logs/log_stream_position_state/src/notifications.ts +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { - LogStreamPositionContext, - LogStreamPositionContextWithLatestPosition, - LogStreamPositionContextWithTargetPosition, - LogStreamPositionContextWithVisiblePositions, -} from './types'; - -export type PositionsChangedEvent = { - type: 'POSITIONS_CHANGED'; -} & LogStreamPositionContextWithTargetPosition & - LogStreamPositionContextWithLatestPosition & - LogStreamPositionContextWithVisiblePositions; - -export interface PageEndBufferReachedEvent { - type: 'PAGE_END_BUFFER_REACHED'; -} - -export type LogStreamPositionNotificationEvent = PositionsChangedEvent | PageEndBufferReachedEvent; - -export const LogStreamPositionNotificationEventSelectors = { - positionsChanged: (context: LogStreamPositionContext) => { - return 'targetPosition' in context && - 'latestPosition' in context && - 'visiblePositions' in context - ? ({ - type: 'POSITIONS_CHANGED', - targetPosition: context.targetPosition, - latestPosition: context.latestPosition, - visiblePositions: context.visiblePositions, - } as LogStreamPositionNotificationEvent) - : undefined; - }, - pageEndBufferReached: (context: LogStreamPositionContext) => - ({ - type: 'PAGE_END_BUFFER_REACHED', - } as LogStreamPositionNotificationEvent), -}; diff --git a/x-pack/solutions/observability/plugins/infra/public/observability_logs/log_stream_position_state/src/state_machine.ts b/x-pack/solutions/observability/plugins/infra/public/observability_logs/log_stream_position_state/src/state_machine.ts deleted file mode 100644 index 87d0cb8e4ec69..0000000000000 --- a/x-pack/solutions/observability/plugins/infra/public/observability_logs/log_stream_position_state/src/state_machine.ts +++ /dev/null @@ -1,241 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { IToasts } from '@kbn/core-notifications-browser'; -import type { IKbnUrlStateStorage } from '@kbn/kibana-utils-plugin/public'; -import { convertISODateToNanoPrecision } from '@kbn/logs-shared-plugin/common'; -import moment from 'moment'; -import type { ActorRefFrom, EmittedFrom } from 'xstate'; -import { actions, createMachine, SpecialTargets } from 'xstate'; -import type { OmitDeprecatedState } from '@kbn/xstate-utils'; -import { sendIfDefined } from '@kbn/xstate-utils'; -import { isSameTimeKey } from '../../../../common/time'; -import { DESIRED_BUFFER_PAGES, RELATIVE_END_UPDATE_DELAY } from './defaults'; -import { LogStreamPositionNotificationEventSelectors } from './notifications'; -import type { - LogStreamPositionContext, - LogStreamPositionContextWithLatestPosition, - LogStreamPositionContextWithTargetPosition, - LogStreamPositionContextWithVisiblePositions, - LogStreamPositionEvent, - LogStreamPositionTypestate, -} from './types'; -import { initializeFromUrl, updateContextInUrl } from './url_state_storage_service'; - -export const createPureLogStreamPositionStateMachine = (initialContext: LogStreamPositionContext) => - /** @xstate-layout N4IgpgJg5mDOIC5QBsD2UDKAXATmAhgLYAKqsAlluagHbb5ZgB0Arjee1fsuQF6QBiAEoBRAMIiAkgDURAEQD6kgHKSAKpICCAGQUBFAKoihATQXFNQzQFkRa4xgDaABgC6iUAAcylajQ8gAB6IALQAbACsTAAcAEzRzgCcACzJccmxAIzR0QA0IACeiNkAzExhJSUA7FURsSXRYYklEVUAvm35aJi4BCQ+VLT0jEwcvtx8HFAAYjiohAY4yAIq6lrakgBa8grTQgDy1goGQtou7kgg3hSD-pfBCCGxTBHREYnOTelZ0bX5RQg0okmD8miVMhEwmEqmFGh0uuhsHgiKQbn5hswxlwePwIExrr5aLBRpxyBNcQIAFIGazEBRqfb0ywAcTs5n2GDW+2U5wCBNuAQeVUSz2SULCmQ+zlScQi-0QyWcVSYVU+QIhzlejQi8JA3SRfVRhLoWAYmNJ5Mg+IGfmJWLJOMEomI+yEagU0kknIAQtoROzORpuU43HybbRBYg6mEmGloskqrEqjkwrFYvLAdEyhkItLMlVqtVc+1OnrEb0UeGTWaSeNHXj+bba9i+IINLYFGIABKaZSsuS8y6NiP3RDC57ChOpZzStKxOWFRBZMqpqr5xIpT6xEXRXX6iv9NFDU0je2WvFYAAWcywWB4NCgxHwMBENAgylQVAAZuQAMYMJtyAgZAwGEEQXTdD0vUkX1-RdQNJGDQcvCrSMEHeTJygSWJkg+X5FQhDN4xjNNIk1V4xRKWIwj3ctkUPY0MWbB1Wwva9PzvKYnxfN8P2-P8AKJJgrxvTiHzAiD3U9H0-QDLllBDC4UKPO5QAeTJJTKVVITTRpt3iDMaiiCINUSDTMlw-Nklonp6KNW4mLPethPY2970fZ8wFfd9P3IH9-1uYkRI49yBECWAT2YfAv0YHAAApRG0TQNFkBQRGURQDGIORkv9OQRCSkwAEoBH3Oyq0ci1nOCtyuM87y+L8gTApc0T3OQq5UNHBAzOiFVohFEo5xKZx6mSDNeqYDTakyVMlWiTIhpsg1KxUyq61Y1qQrqnifP4gKmxqsSoDCiKa2i2KEoK5KZH9dLMuy3KFHywqSrKw0Ksi5jzy22qH24rzeN8-zBJoILXOOxxMiUzqVLQ2plTqEoRWnGcUgmpopvjRUYWSbJWhKZaD3s9EvqczajvcgGGuB5qmxoYGCimAQOuHVSggVGpymhMJnAhVUEjqDNMiVKJaiG+dfgG6prN1BmIDgAJ3tWxjIrDOHupCDSyjiBIUjnDJsjyRdHjSGJEwWjd8zeNGifKtavrYcncXV400IyIjMI3b36lVXmIgTO2PodmtnamWZ5kWZBXYFTXnmlWE-hNka+uoxIA6zRJZUJ0tlYYhyyaq1iY78NCSkiFULLnSFkYlaEMwacok2yGcRZF1pMiDlWC9DovcWtFT4CHLq1IVRaYnjapEmTRNnHBYXqKm5vhWaBIJTqLv89J3uNv7tm7T7yAS5HUfAQlCfkinmfYjnzIM23KI04z5Hs83knjx3lt+8pnbAb2pqDpEmPuzB4eMtJV1lBURIdcqgZlhH1PmJkcJDTqItEsCJbLB1Vp-Fi38IZU3qkDfaoM7TATAMA92581wynnFAmBRExQgmormCIFEKjUTfp9HBP0f7-UIf-EGLVeFQAod1cyzx4isKqHOfMkIGEkWYeRYiVEaK5zolgnup5D5sTar-GmxCWoM2-EzB8ojT5t2BK8HClFFqREIibduzgVQRCGiNJUyRWEig6B0IAA */ - createMachine( - { - context: initialContext, - predictableActionArguments: true, - id: 'logStreamPositionState', - initial: 'uninitialized', - states: { - uninitialized: { - meta: { - _DX_warning_: - "The Position machine cannot initializeFromUrl until after the Query machine has initialized, this is due to a dual dependency on the 'logPosition' URL parameter for legacy reasons.", - }, - on: { - RECEIVED_INITIAL_QUERY_PARAMETERS: { - target: 'initializingFromUrl', - }, - }, - }, - initializingFromUrl: { - on: { - INITIALIZED_FROM_URL: [ - { - target: 'initialized', - actions: ['storeTargetPosition', 'storeLatestPosition'], - }, - ], - }, - invoke: { - src: 'initializeFromUrl', - }, - }, - initialized: { - type: 'parallel', - states: { - positions: { - initial: 'initialized', - states: { - initialized: { - entry: ['updateContextInUrl', 'notifyPositionsChanged'], - on: { - JUMP_TO_TARGET_POSITION: { - target: 'initialized', - actions: ['updateTargetPosition'], - }, - REPORT_VISIBLE_POSITIONS: { - target: 'initialized', - actions: ['updateVisiblePositions'], - }, - TIME_CHANGED: { - target: 'initialized', - actions: ['updatePositionsFromTimeChange'], - }, - }, - }, - }, - }, - throttlingPageEndNotifications: { - initial: 'idle', - states: { - idle: { - on: { - REPORT_VISIBLE_POSITIONS: { - target: 'throttling', - }, - }, - }, - throttling: { - after: { - RELATIVE_END_UPDATE_DELAY: [ - { - target: 'notifying', - cond: 'hasReachedPageEndBuffer', - }, - { - target: 'idle', - }, - ], - }, - on: { - REPORT_VISIBLE_POSITIONS: { - target: 'throttling', - }, - }, - }, - notifying: { - entry: ['notifyPageEndBufferReached'], - always: 'idle', - }, - }, - }, - }, - }, - }, - }, - { - actions: { - notifyPositionsChanged: actions.pure(() => undefined), - notifyPageEndBufferReached: actions.pure(() => undefined), - storeTargetPosition: actions.assign((_context, event) => - 'targetPosition' in event - ? ({ - targetPosition: event.targetPosition, - } as LogStreamPositionContextWithTargetPosition) - : {} - ), - storeLatestPosition: actions.assign((_context, event) => - 'latestPosition' in event - ? ({ - latestPosition: event.latestPosition, - } as LogStreamPositionContextWithLatestPosition) - : {} - ), - updateTargetPosition: actions.assign((_context, event) => { - if (!('targetPosition' in event)) return {}; - - const nextTargetPosition = event.targetPosition?.time - ? { - time: event.targetPosition.time, - tiebreaker: event.targetPosition.tiebreaker ?? 0, - } - : null; - - const nextLatestPosition = !isSameTimeKey(_context.targetPosition, nextTargetPosition) - ? nextTargetPosition - : _context.latestPosition; - - return { - targetPosition: nextTargetPosition, - latestPosition: nextLatestPosition, - } as LogStreamPositionContextWithLatestPosition & - LogStreamPositionContextWithTargetPosition; - }), - updatePositionsFromTimeChange: actions.assign((_context, event) => { - if (!('timeRange' in event)) return {}; - - const { - timestamps: { startTimestamp, endTimestamp }, - } = event; - - // Reset the target position if it doesn't fall within the new range. - const targetPositionNanoTime = - _context.targetPosition && convertISODateToNanoPrecision(_context.targetPosition.time); - const startNanoDate = convertISODateToNanoPrecision(moment(startTimestamp).toISOString()); - const endNanoDate = convertISODateToNanoPrecision(moment(endTimestamp).toISOString()); - - const targetPositionShouldReset = - targetPositionNanoTime && - (startNanoDate > targetPositionNanoTime || endNanoDate < targetPositionNanoTime); - - return { - targetPosition: targetPositionShouldReset ? null : _context.targetPosition, - latestPosition: targetPositionShouldReset ? null : _context.latestPosition, - } as LogStreamPositionContextWithLatestPosition & - LogStreamPositionContextWithTargetPosition; - }), - updateVisiblePositions: actions.assign((_context, event) => - 'visiblePositions' in event - ? ({ - visiblePositions: event.visiblePositions, - latestPosition: !isSameTimeKey( - _context.visiblePositions.middleKey, - event.visiblePositions.middleKey - ) - ? event.visiblePositions.middleKey - : _context.visiblePositions.middleKey, - } as LogStreamPositionContextWithVisiblePositions) - : {} - ), - }, - delays: { - RELATIVE_END_UPDATE_DELAY, - }, - guards: { - // User is close to the bottom of the page. - hasReachedPageEndBuffer: (context, event) => - context.visiblePositions.pagesAfterEnd < DESIRED_BUFFER_PAGES, - }, - } - ); - -export type LogStreamPositionStateMachine = ReturnType< - typeof createPureLogStreamPositionStateMachine ->; -export type LogStreamPositionActorRef = OmitDeprecatedState< - ActorRefFrom ->; -export type LogStreamPositionState = EmittedFrom; - -export interface LogStreamPositionStateMachineDependencies { - urlStateStorage: IKbnUrlStateStorage; - toastsService: IToasts; -} - -export const createLogStreamPositionStateMachine = ( - initialContext: LogStreamPositionContext, - { urlStateStorage, toastsService }: LogStreamPositionStateMachineDependencies -) => - createPureLogStreamPositionStateMachine(initialContext).withConfig({ - actions: { - updateContextInUrl: updateContextInUrl({ toastsService, urlStateStorage }), - notifyPositionsChanged: sendIfDefined(SpecialTargets.Parent)( - LogStreamPositionNotificationEventSelectors.positionsChanged - ), - notifyPageEndBufferReached: sendIfDefined(SpecialTargets.Parent)( - LogStreamPositionNotificationEventSelectors.pageEndBufferReached - ), - }, - services: { - initializeFromUrl: initializeFromUrl({ toastsService, urlStateStorage }), - }, - }); diff --git a/x-pack/solutions/observability/plugins/infra/public/observability_logs/log_stream_position_state/src/types.ts b/x-pack/solutions/observability/plugins/infra/public/observability_logs/log_stream_position_state/src/types.ts deleted file mode 100644 index 398d7e8addbf5..0000000000000 --- a/x-pack/solutions/observability/plugins/infra/public/observability_logs/log_stream_position_state/src/types.ts +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { TimeKey } from '../../../../common/time'; -import type { ReceivedInitialQueryParametersEvent } from '../../log_stream_page/state'; -import type { TimeChangedEvent } from '../../log_stream_query_state/src/notifications'; - -export interface VisiblePositions { - startKey: TimeKey | null; - middleKey: TimeKey | null; - endKey: TimeKey | null; - pagesAfterEnd: number; - pagesBeforeStart: number; -} - -export interface LogStreamPositionContextWithTargetPosition { - targetPosition: TimeKey | null; -} - -export interface LogStreamPositionContextWithLatestPosition { - latestPosition: TimeKey | null; -} -export interface LogStreamPositionContextWithVisiblePositions { - visiblePositions: VisiblePositions; -} -export type LogStreamPositionState = LogStreamPositionContextWithTargetPosition & - LogStreamPositionContextWithLatestPosition & - LogStreamPositionContextWithVisiblePositions; - -export type LogStreamPositionTypestate = - | { - value: 'uninitialized'; - context: LogStreamPositionState; - } - | { - value: 'initialized'; - context: LogStreamPositionState; - }; -export type LogStreamPositionContext = LogStreamPositionTypestate['context']; -export type LogStreamPositionStateValue = LogStreamPositionTypestate['value']; - -export interface JumpToTargetPositionEvent { - type: 'JUMP_TO_TARGET_POSITION'; - targetPosition: Partial | null; -} - -export interface ReportVisiblePositionsEvent { - type: 'REPORT_VISIBLE_POSITIONS'; - visiblePositions: VisiblePositions; -} - -export type LogStreamPositionEvent = - | { - type: 'INITIALIZED_FROM_URL'; - latestPosition: TimeKey | null; - targetPosition: TimeKey | null; - } - | ReceivedInitialQueryParametersEvent - | JumpToTargetPositionEvent - | ReportVisiblePositionsEvent - | TimeChangedEvent; diff --git a/x-pack/solutions/observability/plugins/infra/public/observability_logs/log_stream_position_state/src/url_state_storage_service.ts b/x-pack/solutions/observability/plugins/infra/public/observability_logs/log_stream_position_state/src/url_state_storage_service.ts deleted file mode 100644 index d23d708e84877..0000000000000 --- a/x-pack/solutions/observability/plugins/infra/public/observability_logs/log_stream_position_state/src/url_state_storage_service.ts +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import * as rt from 'io-ts'; -import type { IToasts } from '@kbn/core-notifications-browser'; -import type { IKbnUrlStateStorage } from '@kbn/kibana-utils-plugin/public'; -import { withNotifyOnErrors } from '@kbn/kibana-utils-plugin/public'; -import * as Either from 'fp-ts/lib/Either'; -import { pipe } from 'fp-ts/lib/function'; -import type { InvokeCreator } from 'xstate'; -import { createPlainError, formatErrors } from '@kbn/io-ts-utils'; -import { minimalTimeKeyRT, pickTimeKey } from '../../../../common/time'; -import type { LogStreamPositionContext, LogStreamPositionEvent } from './types'; -interface LogStreamPositionUrlStateDependencies { - positionStateKey?: string; - toastsService: IToasts; - urlStateStorage: IKbnUrlStateStorage; -} - -export const defaultPositionStateKey = 'logPosition'; - -export const updateContextInUrl = - ({ - urlStateStorage, - positionStateKey = defaultPositionStateKey, - }: LogStreamPositionUrlStateDependencies) => - (context: LogStreamPositionContext, _event: LogStreamPositionEvent) => { - if (!('latestPosition' in context)) { - throw new Error('Missing keys from context needed to sync to the URL'); - } - - urlStateStorage.set( - positionStateKey, - positionStateInUrlRT.encode({ - position: context.latestPosition ? pickTimeKey(context.latestPosition) : null, - }), - { replace: true } - ); - }; - -export const initializeFromUrl = - ({ - positionStateKey = defaultPositionStateKey, - urlStateStorage, - toastsService, - }: LogStreamPositionUrlStateDependencies): InvokeCreator< - LogStreamPositionContext, - LogStreamPositionEvent - > => - (_context, _event) => - (send) => { - const positionQueryValueFromUrl = urlStateStorage.get(positionStateKey) ?? {}; - - const initialUrlValues = pipe( - decodePositionQueryValueFromUrl(positionQueryValueFromUrl), - Either.map(({ position }) => ({ - targetPosition: position?.time - ? { - time: position.time, - tiebreaker: position.tiebreaker ?? 0, - } - : null, - })), - Either.map(({ targetPosition }) => ({ - targetPosition, - latestPosition: targetPosition, - })) - ); - - if (Either.isLeft(initialUrlValues)) { - withNotifyOnErrors(toastsService).onGetError( - createPlainError(formatErrors(initialUrlValues.left)) - ); - - send({ - type: 'INITIALIZED_FROM_URL', - targetPosition: null, - latestPosition: null, - }); - } else { - send({ - type: 'INITIALIZED_FROM_URL', - targetPosition: initialUrlValues.right.targetPosition ?? null, - latestPosition: initialUrlValues.right.latestPosition ?? null, - }); - } - }; - -export const positionStateInUrlRT = rt.partial({ - position: rt.union([rt.partial(minimalTimeKeyRT.props), rt.null]), -}); - -export type PositionStateInUrl = rt.TypeOf; - -const decodePositionQueryValueFromUrl = (queryValueFromUrl: unknown) => { - return positionStateInUrlRT.decode(queryValueFromUrl); -}; diff --git a/x-pack/solutions/observability/plugins/infra/public/observability_logs/log_stream_query_state/index.ts b/x-pack/solutions/observability/plugins/infra/public/observability_logs/log_stream_query_state/index.ts deleted file mode 100644 index 3b2a320ae181f..0000000000000 --- a/x-pack/solutions/observability/plugins/infra/public/observability_logs/log_stream_query_state/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export * from './src'; diff --git a/x-pack/solutions/observability/plugins/infra/public/observability_logs/log_stream_query_state/src/defaults.ts b/x-pack/solutions/observability/plugins/infra/public/observability_logs/log_stream_query_state/src/defaults.ts deleted file mode 100644 index b9f4eb4f5f2a5..0000000000000 --- a/x-pack/solutions/observability/plugins/infra/public/observability_logs/log_stream_query_state/src/defaults.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export const DEFAULT_QUERY = { - language: 'kuery', - query: '', -}; - -export const DEFAULT_FILTERS = []; - -export const DEFAULT_TIMERANGE = { - from: 'now-1d', - to: 'now', -}; - -export const DEFAULT_REFRESH_TIME_RANGE = DEFAULT_TIMERANGE; diff --git a/x-pack/solutions/observability/plugins/infra/public/observability_logs/log_stream_query_state/src/errors.ts b/x-pack/solutions/observability/plugins/infra/public/observability_logs/log_stream_query_state/src/errors.ts deleted file mode 100644 index 09e3af3e241a4..0000000000000 --- a/x-pack/solutions/observability/plugins/infra/public/observability_logs/log_stream_query_state/src/errors.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -/* eslint-disable max-classes-per-file */ -export class UnsupportedLanguageError extends Error { - constructor(message?: string) { - super(message); - Object.setPrototypeOf(this, new.target.prototype); - } -} - -export class QueryParsingError extends Error { - constructor(message?: string) { - super(message); - Object.setPrototypeOf(this, new.target.prototype); - } -} diff --git a/x-pack/solutions/observability/plugins/infra/public/observability_logs/log_stream_query_state/src/index.ts b/x-pack/solutions/observability/plugins/infra/public/observability_logs/log_stream_query_state/src/index.ts deleted file mode 100644 index b9f6065f99092..0000000000000 --- a/x-pack/solutions/observability/plugins/infra/public/observability_logs/log_stream_query_state/src/index.ts +++ /dev/null @@ -1,13 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export * from './errors'; -export * from './state_machine'; -export * from './types'; -export * from './url_state_storage_service'; -export * from './time_filter_state_service'; -export * from './defaults'; diff --git a/x-pack/solutions/observability/plugins/infra/public/observability_logs/log_stream_query_state/src/notifications.ts b/x-pack/solutions/observability/plugins/infra/public/observability_logs/log_stream_query_state/src/notifications.ts deleted file mode 100644 index 928671d4d67ca..0000000000000 --- a/x-pack/solutions/observability/plugins/infra/public/observability_logs/log_stream_query_state/src/notifications.ts +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { RefreshInterval } from '@kbn/data-plugin/public'; -import type { ExtendedTimeRange, LogStreamQueryContext, ParsedQuery, Timestamps } from './types'; - -export interface TimeChangedEvent { - type: 'TIME_CHANGED'; - timeRange: ExtendedTimeRange; - refreshInterval: RefreshInterval; - timestamps: Timestamps; -} - -export type LogStreamQueryNotificationEvent = - | { - type: 'VALID_QUERY_CHANGED'; - parsedQuery: ParsedQuery; - } - | { - type: 'INVALID_QUERY_CHANGED'; - parsedQuery: ParsedQuery; - error: Error; - } - | TimeChangedEvent; - -export const logStreamQueryNotificationEventSelectors = { - validQueryChanged: (context: LogStreamQueryContext) => - 'parsedQuery' in context - ? ({ - type: 'VALID_QUERY_CHANGED', - parsedQuery: context.parsedQuery, - } as LogStreamQueryNotificationEvent) - : undefined, - invalidQueryChanged: (context: LogStreamQueryContext) => - 'validationError' in context - ? ({ - type: 'INVALID_QUERY_CHANGED', - parsedQuery: context.parsedQuery, - error: context.validationError, - } as LogStreamQueryNotificationEvent) - : undefined, - timeChanged: (context: LogStreamQueryContext) => { - return 'timeRange' in context && 'refreshInterval' in context && 'timestamps' in context - ? ({ - type: 'TIME_CHANGED', - timeRange: context.timeRange, - refreshInterval: context.refreshInterval, - timestamps: context.timestamps, - } as LogStreamQueryNotificationEvent) - : undefined; - }, -}; diff --git a/x-pack/solutions/observability/plugins/infra/public/observability_logs/log_stream_query_state/src/search_bar_state_service.ts b/x-pack/solutions/observability/plugins/infra/public/observability_logs/log_stream_query_state/src/search_bar_state_service.ts deleted file mode 100644 index d7bb762a63b6b..0000000000000 --- a/x-pack/solutions/observability/plugins/infra/public/observability_logs/log_stream_query_state/src/search_bar_state_service.ts +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { FilterManager, QueryStringContract } from '@kbn/data-plugin/public'; -import { map } from 'rxjs'; -import type { InvokeCreator } from 'xstate'; -import type { LogStreamQueryContext, LogStreamQueryEvent } from './types'; - -export const subscribeToQuerySearchBarChanges = - ({ - queryStringService, - }: { - queryStringService: QueryStringContract; - }): InvokeCreator => - (context) => - queryStringService.getUpdates$().pipe( - map(() => queryStringService.getQuery()), - map((query): LogStreamQueryEvent => { - return { - type: 'QUERY_FROM_SEARCH_BAR_CHANGED', - query, - }; - }) - ); - -export const updateQueryInSearchBar = - ({ queryStringService }: { queryStringService: QueryStringContract }) => - (context: LogStreamQueryContext, event: LogStreamQueryEvent) => { - if ('query' in context) { - queryStringService.setQuery(context.query); - } - }; - -export const subscribeToFilterSearchBarChanges = - ({ - filterManagerService, - }: { - filterManagerService: FilterManager; - }): InvokeCreator => - (context) => - filterManagerService.getUpdates$().pipe( - map(() => filterManagerService.getFilters()), - map((filters): LogStreamQueryEvent => { - return { - type: 'FILTERS_FROM_SEARCH_BAR_CHANGED', - filters, - }; - }) - ); - -export const updateFiltersInSearchBar = - ({ filterManagerService }: { filterManagerService: FilterManager }) => - (context: LogStreamQueryContext, event: LogStreamQueryEvent) => { - if ('filters' in context) { - filterManagerService.setFilters(context.filters); - } - }; diff --git a/x-pack/solutions/observability/plugins/infra/public/observability_logs/log_stream_query_state/src/state_machine.ts b/x-pack/solutions/observability/plugins/infra/public/observability_logs/log_stream_query_state/src/state_machine.ts deleted file mode 100644 index 6ad01bd1e8598..0000000000000 --- a/x-pack/solutions/observability/plugins/infra/public/observability_logs/log_stream_query_state/src/state_machine.ts +++ /dev/null @@ -1,362 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { IToasts } from '@kbn/core-notifications-browser'; -import type { - FilterManager, - QueryStringContract, - TimefilterContract, -} from '@kbn/data-plugin/public'; -import type { EsQueryConfig } from '@kbn/es-query'; -import type { IKbnUrlStateStorage } from '@kbn/kibana-utils-plugin/public'; -import type { ActorRefFrom } from 'xstate'; -import { actions, createMachine, SpecialTargets, send } from 'xstate'; -import { DEFAULT_REFRESH_INTERVAL } from '@kbn/logs-shared-plugin/common'; -import type { OmitDeprecatedState } from '@kbn/xstate-utils'; -import { sendIfDefined } from '@kbn/xstate-utils'; -import { logStreamQueryNotificationEventSelectors } from './notifications'; -import { - subscribeToFilterSearchBarChanges, - subscribeToQuerySearchBarChanges, - updateFiltersInSearchBar, - updateQueryInSearchBar, -} from './search_bar_state_service'; -import type { - LogStreamQueryContext, - LogStreamQueryContextWithDataViews, - LogStreamQueryContextWithFilters, - LogStreamQueryContextWithParsedQuery, - LogStreamQueryContextWithQuery, - LogStreamQueryContextWithRefreshInterval, - LogStreamQueryContextWithTime, - LogStreamQueryContextWithTimeRange, - LogStreamQueryContextWithValidationError, - LogStreamQueryEvent, - LogStreamQueryTypestate, -} from './types'; -import { - initializeFromUrl, - safeDefaultParsedQuery, - updateContextInUrl, -} from './url_state_storage_service'; -import { - initializeFromTimeFilterService, - subscribeToTimeFilterServiceChanges, - updateTimeContextFromTimeFilterService, - updateTimeContextFromTimeRangeUpdate, - updateTimeContextFromRefreshIntervalUpdate, - updateTimeInTimeFilterService, - updateTimeContextFromUrl, -} from './time_filter_state_service'; -import { showValidationErrorToast, validateQuery } from './validate_query_service'; -import { DEFAULT_REFRESH_TIME_RANGE } from './defaults'; - -export const createPureLogStreamQueryStateMachine = ( - initialContext: LogStreamQueryContextWithDataViews & LogStreamQueryContextWithTime -) => - /** @xstate-layout N4IgpgJg5mDOIC5QEUCuYBOBPAdKgdgJZEAuhAhgDaEBekAxANoAMAuoqAA4D2shZ3fBxAAPRAEYAHAGYck8QCYArAoAsC8QHZNygJySANCCwTpszc2njpANnH2FN1UtUBfV0bSZcxfhWo0xFAAYhjcALYAqhiU9ACSAHJxACpxAIIAMnEAWgCiACIA+sEASgDyALKFkSUZLOxIIDx8AkKNYghKzJJyCvJOurqqA4bGiPI4XczTdjbM2kq64u6e6Ng4vmRUtEGhEcmE4WDBhJQkmADKmABuhADGYPFJqZk5BcXlVakVucVxGclciVChcgQA1OIAYVy9WEzT8gmEHWkkhsOGmmnkqnUumkzBsmmkRhMCF0mjRunxCjM9nx0iUkhWIC8602-lokBwAEc1lh6MhIkCAJofSog3JpEqQgAShQAQpLCjK0gkAOIFWGNeGtJHjcRKHA2aR9BT45hKOxKIljBDiZiqcRyAmmySqV2aJSYpksnykdl0CDc3n0YL-QElC6iqqgyUy+WK5VqjVsOG8BFtUAdeQGo0ms0W-XWklLHrDZi6K3SbQ2SSe728jZ+7YBoPeej5NLJNKFCG5ADqkcT6vymq4aZ17T1OeNklNcwLVuJ4wUCnR83kgzd7vr3kbfmbnJ5u+u2wg5DI+Cg9DBrw7qTKCRBkUh0IKyYaY5ahERk9tDtUODSJSdj6uW4i6EupKqMwgGKEoijMCaZKMh4zINmyB6Bke6wntQZ4XleN5ZHecQPsUaT-O+qZfj+mYSP+gHAfYXRLBBNr2HMOBaOBfQbto5o7qyTYBIeDYYGAuGEPhQTXrenakY+wQURkVFauO34ZqI9HiABQH4sxYFsSSdq2Dg5IWoSmiKDY+jSIJvr7iJWFiRJp7njJRFxCRZEXM+r75Kpn7prqf46Yx+mgaxkH9DgwHKDZGgrha9l7lsTk4GQRz0N8vylGKOV-ACQLiiUELQkq0oqsOo5NOptFaQg2aGjOc7mpaRYSO6hoWho5YKBYHooasu4YelmWPJEAAKd6-AVJRVTCKZqTRmlZvqzV5vO7WQeIBIwdIqhVjWZLDHMNgpaNHKBuN9BTTNhQlLkpS5BcsqJOGRE1dqGkhU1uazvm23saouLolYxoWCuahKEoF3CVdGWHGAqX+gwX11atpgOpM1KnYdmIVqoO06LokwyGYh2SJo2KqJocOOQj40o5hTDiB+tUrSF1jYyoB02E4VaSITxOSD03Q0tTQFujo9NpYzSM4LAJDuXc9CTWk6qFLkCRFHKkTBMExWPWkMqBRzwW-rtFjosMQv0rSpqaDtVMAXauKYrO8HMDpsuo9dCtK+J5DhDJIhK+eyPkAAZucGAABTiVH4mwAAFgAlPy6Hwy2TOB2AwdBOjnOW4SpNC+6qICxuxMooBZgQ+aB2WO4qH4NwEBwMIPrURbdEIAAtDYkH9wagxj97tjDCuVZuKhPp4EQ2eQD3E59+oO3GmZli7dYnoMn0dNz1nDOBJeexRDEK8-b+-U9OXwyWVoLFE+x5Y4KaVjUkaihC7TvvNrsMI4QDhHBOGcS4Nx7hgCvvVDoBJIKDH2osKsVYKzgUGP-JyMDMYICsqTf6rUFwdQQCDGCcUpC6BNPIOyR8RpL2ct4bBIVySaA2gDLahZIKelLFSU0iE5hkkwQjbCuBJLSUvEwy2UhWF9EUMoLQbpJ4IJcJMICdpdrQ12kNNCdCT6iWPKeSRfddr80NBWPEQE5jYhsAoaKzgcDYkcKLZx28aHDSEnohhQkxFGIaiYtENkrSWF0FYqeO18QGjxBoQ69J6Solnu4hycsWwiJwOJMR7kJHLV7n46R795BJQUTIJwdiAICKAsaA+kh7RCJzkjXxHQWFsMIUDYyeI0RWmsALBCNZYa0I8ckzkTNLoBgaRIDQrCyQ2AZPpPhSga4ATUIMV0CgyT4nOv0pJftEZHEVsrMgdwxm2ngq7cxVp+aU3AiLNE9cG5Wmgm4nRAztm5xIEHEOWSgqrz8ScriZzbBVyuexHQrDyZWE9M4TQ+hD7uCAA */ - createMachine( - { - context: initialContext, - preserveActionOrder: true, - predictableActionArguments: true, - id: 'Query', - initial: 'uninitialized', - states: { - uninitialized: { - always: { - target: 'initializingFromTimeFilterService', - }, - }, - - initializingFromUrl: { - on: { - INITIALIZED_FROM_URL: { - target: 'initialized', - actions: ['storeQuery', 'storeFilters', 'updateTimeContextFromUrl'], - }, - }, - invoke: { - src: 'initializeFromUrl', - }, - }, - initializingFromTimeFilterService: { - on: { - INITIALIZED_FROM_TIME_FILTER_SERVICE: { - target: 'initializingFromUrl', - actions: ['updateTimeContextFromTimeFilterService'], - }, - }, - invoke: { - src: 'initializeFromTimeFilterService', - }, - }, - initialized: { - type: 'parallel', - states: { - query: { - entry: ['updateContextInUrl', 'updateQueryInSearchBar', 'updateFiltersInSearchBar'], - invoke: [ - { - src: 'subscribeToQuerySearchBarChanges', - }, - { - src: 'subscribeToFilterSearchBarChanges', - }, - ], - initial: 'validating', - states: { - validating: { - invoke: { - src: 'validateQuery', - }, - on: { - VALIDATION_SUCCEEDED: { - target: 'valid', - actions: 'storeParsedQuery', - }, - - VALIDATION_FAILED: { - target: 'invalid', - actions: [ - 'storeValidationError', - 'storeDefaultParsedQuery', - 'showValidationErrorToast', - ], - }, - }, - }, - valid: { - entry: 'notifyValidQueryChanged', - }, - invalid: { - entry: 'notifyInvalidQueryChanged', - }, - revalidating: { - invoke: { - src: 'validateQuery', - }, - on: { - VALIDATION_FAILED: { - target: 'invalid', - actions: ['storeValidationError', 'showValidationErrorToast'], - }, - VALIDATION_SUCCEEDED: { - target: 'valid', - actions: ['clearValidationError', 'storeParsedQuery'], - }, - }, - }, - }, - on: { - QUERY_FROM_SEARCH_BAR_CHANGED: { - target: '.revalidating', - actions: ['storeQuery', 'updateContextInUrl'], - }, - - FILTERS_FROM_SEARCH_BAR_CHANGED: { - target: '.revalidating', - actions: ['storeFilters', 'updateContextInUrl'], - }, - - DATA_VIEWS_CHANGED: { - target: '.revalidating', - actions: 'storeDataViews', - }, - }, - }, - time: { - initial: 'initialized', - entry: ['notifyTimeChanged', 'updateTimeInTimeFilterService'], - invoke: [ - { - src: 'subscribeToTimeFilterServiceChanges', - }, - ], - states: { - initialized: { - always: [{ target: 'streaming', cond: 'isStreaming' }, { target: 'static' }], - }, - static: { - on: { - PAGE_END_BUFFER_REACHED: { - actions: ['expandPageEnd'], - }, - }, - }, - streaming: { - after: { - refresh: { target: 'streaming', actions: ['refreshTime'] }, - }, - }, - }, - on: { - TIME_FROM_TIME_FILTER_SERVICE_CHANGED: { - target: '.initialized', - actions: [ - 'updateTimeContextFromTimeFilterService', - 'notifyTimeChanged', - 'updateContextInUrl', - ], - }, - - UPDATE_TIME_RANGE: { - target: '.initialized', - actions: [ - 'updateTimeContextFromTimeRangeUpdate', - 'notifyTimeChanged', - 'updateTimeInTimeFilterService', - 'updateContextInUrl', - ], - }, - - UPDATE_REFRESH_INTERVAL: { - target: '.initialized', - actions: [ - 'updateTimeContextFromRefreshIntervalUpdate', - 'notifyTimeChanged', - 'updateTimeInTimeFilterService', - 'updateContextInUrl', - ], - }, - }, - }, - }, - }, - }, - }, - { - actions: { - notifyInvalidQueryChanged: actions.pure(() => undefined), - notifyValidQueryChanged: actions.pure(() => undefined), - notifyTimeChanged: actions.pure(() => undefined), - storeQuery: actions.assign((_context, event) => { - return 'query' in event ? ({ query: event.query } as LogStreamQueryContextWithQuery) : {}; - }), - storeFilters: actions.assign((_context, event) => - 'filters' in event ? ({ filters: event.filters } as LogStreamQueryContextWithFilters) : {} - ), - storeTimeRange: actions.assign((_context, event) => - 'timeRange' in event - ? ({ timeRange: event.timeRange } as LogStreamQueryContextWithTimeRange) - : {} - ), - storeRefreshInterval: actions.assign((_context, event) => - 'refreshInterval' in event - ? ({ - refreshInterval: event.refreshInterval, - } as LogStreamQueryContextWithRefreshInterval) - : {} - ), - storeDataViews: actions.assign((_context, event) => - 'dataViews' in event - ? ({ dataViews: event.dataViews } as LogStreamQueryContextWithDataViews) - : {} - ), - storeValidationError: actions.assign((_context, event) => - 'error' in event - ? ({ - validationError: event.error, - } as LogStreamQueryContextWithQuery & LogStreamQueryContextWithValidationError) - : {} - ), - storeDefaultParsedQuery: actions.assign( - (_context, _event) => - ({ parsedQuery: safeDefaultParsedQuery } as LogStreamQueryContextWithParsedQuery) - ), - storeParsedQuery: actions.assign((_context, event) => - 'parsedQuery' in event - ? ({ parsedQuery: event.parsedQuery } as LogStreamQueryContextWithParsedQuery) - : {} - ), - clearValidationError: actions.assign( - (_context, _event) => - ({ validationError: undefined } as Omit< - LogStreamQueryContextWithValidationError, - 'validationError' - >) - ), - updateTimeContextFromTimeFilterService, - updateTimeContextFromTimeRangeUpdate, - updateTimeContextFromRefreshIntervalUpdate, - refreshTime: send({ type: 'UPDATE_TIME_RANGE', timeRange: DEFAULT_REFRESH_TIME_RANGE }), - expandPageEnd: send((context) => ({ - type: 'UPDATE_TIME_RANGE', - timeRange: { to: context.timeRange.to }, - })), - updateTimeContextFromUrl, - }, - guards: { - isStreaming: (context, event) => - 'refreshInterval' in context ? !context.refreshInterval.pause : false, - }, - delays: { - refresh: (context, event) => - 'refreshInterval' in context - ? context.refreshInterval.value - : DEFAULT_REFRESH_INTERVAL.value, - }, - } - ); - -export interface LogStreamQueryStateMachineDependencies { - kibanaQuerySettings: EsQueryConfig; - queryStringService: QueryStringContract; - filterManagerService: FilterManager; - urlStateStorage: IKbnUrlStateStorage; - toastsService: IToasts; - timeFilterService: TimefilterContract; -} - -export const createLogStreamQueryStateMachine = ( - initialContext: LogStreamQueryContextWithDataViews & LogStreamQueryContextWithTime, - { - kibanaQuerySettings, - queryStringService, - toastsService, - filterManagerService, - urlStateStorage, - timeFilterService, - }: LogStreamQueryStateMachineDependencies -) => - createPureLogStreamQueryStateMachine(initialContext).withConfig({ - actions: { - updateContextInUrl: updateContextInUrl({ toastsService, urlStateStorage }), - // Query - notifyInvalidQueryChanged: sendIfDefined(SpecialTargets.Parent)( - logStreamQueryNotificationEventSelectors.invalidQueryChanged - ), - notifyValidQueryChanged: sendIfDefined(SpecialTargets.Parent)( - logStreamQueryNotificationEventSelectors.validQueryChanged - ), - showValidationErrorToast: showValidationErrorToast({ toastsService }), - updateQueryInSearchBar: updateQueryInSearchBar({ queryStringService }), - updateFiltersInSearchBar: updateFiltersInSearchBar({ filterManagerService }), - // Time - updateTimeInTimeFilterService: updateTimeInTimeFilterService({ timeFilterService }), - notifyTimeChanged: sendIfDefined(SpecialTargets.Parent)( - logStreamQueryNotificationEventSelectors.timeChanged - ), - }, - services: { - initializeFromUrl: initializeFromUrl({ toastsService, urlStateStorage }), - initializeFromTimeFilterService: initializeFromTimeFilterService({ timeFilterService }), - validateQuery: validateQuery({ kibanaQuerySettings }), - subscribeToQuerySearchBarChanges: subscribeToQuerySearchBarChanges({ - queryStringService, - }), - subscribeToFilterSearchBarChanges: subscribeToFilterSearchBarChanges({ - filterManagerService, - }), - subscribeToTimeFilterServiceChanges: subscribeToTimeFilterServiceChanges({ - timeFilterService, - }), - }, - }); - -export type LogStreamQueryStateMachine = ReturnType; -export type LogStreamQueryActorRef = OmitDeprecatedState>; diff --git a/x-pack/solutions/observability/plugins/infra/public/observability_logs/log_stream_query_state/src/time_filter_state_service.ts b/x-pack/solutions/observability/plugins/infra/public/observability_logs/log_stream_query_state/src/time_filter_state_service.ts deleted file mode 100644 index 96a2aa6bbc4e6..0000000000000 --- a/x-pack/solutions/observability/plugins/infra/public/observability_logs/log_stream_query_state/src/time_filter_state_service.ts +++ /dev/null @@ -1,192 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { RefreshInterval, TimefilterContract } from '@kbn/data-plugin/public'; -import type { TimeRange } from '@kbn/es-query'; -import { map, merge } from 'rxjs'; -import type { InvokeCreator } from 'xstate'; -import { actions } from 'xstate'; -import { datemathToEpochMillis } from '../../../utils/datemath'; -import { DEFAULT_REFRESH_TIME_RANGE } from './defaults'; -import type { LogStreamQueryContext, LogStreamQueryEvent } from './types'; - -export interface TimefilterState { - timeRange: TimeRange; - refreshInterval: RefreshInterval; -} - -export const initializeFromTimeFilterService = - ({ - timeFilterService, - }: { - timeFilterService: TimefilterContract; - }): InvokeCreator => - (_context, _event) => - (send) => { - const timeRange = timeFilterService.getTime(); - const refreshInterval = timeFilterService.getRefreshInterval(); - - send({ - type: 'INITIALIZED_FROM_TIME_FILTER_SERVICE', - timeRange, - refreshInterval, - }); - }; - -export const updateTimeInTimeFilterService = - ({ timeFilterService }: { timeFilterService: TimefilterContract }) => - (context: LogStreamQueryContext, event: LogStreamQueryEvent) => { - if ('timeRange' in context) { - timeFilterService.setTime(context.timeRange); - } - - if ('refreshInterval' in context) { - timeFilterService.setRefreshInterval(context.refreshInterval); - } - }; - -export const subscribeToTimeFilterServiceChanges = - ({ - timeFilterService, - }: { - timeFilterService: TimefilterContract; - }): InvokeCreator => - (context) => - merge(timeFilterService.getTimeUpdate$(), timeFilterService.getRefreshIntervalUpdate$()).pipe( - map(() => getTimefilterState(timeFilterService)), - map((timeState): LogStreamQueryEvent => { - return { - type: 'TIME_FROM_TIME_FILTER_SERVICE_CHANGED', - ...timeState, - }; - }) - ); - -const getTimefilterState = (timeFilterService: TimefilterContract): TimefilterState => ({ - timeRange: timeFilterService.getTime(), - refreshInterval: timeFilterService.getRefreshInterval(), -}); - -export const updateTimeContextFromTimeFilterService = actions.assign( - (context: LogStreamQueryContext, event: LogStreamQueryEvent) => { - if ( - event.type === 'TIME_FROM_TIME_FILTER_SERVICE_CHANGED' || - event.type === 'INITIALIZED_FROM_TIME_FILTER_SERVICE' - ) { - return { - ...getTimeFromEvent(context, event), - refreshInterval: - event.type === 'TIME_FROM_TIME_FILTER_SERVICE_CHANGED' - ? event.refreshInterval - : { ...context.refreshInterval, pause: event.refreshInterval.pause }, - }; - } else { - return {}; - } - } -); - -export const updateTimeContextFromUrl = actions.assign( - (context: LogStreamQueryContext, event: LogStreamQueryEvent) => { - if (event.type === 'INITIALIZED_FROM_URL') { - return { - ...('timeRange' in event && event.timeRange ? { ...getTimeFromEvent(context, event) } : {}), - ...('refreshInterval' in event && event.refreshInterval - ? { refreshInterval: event.refreshInterval } - : {}), - }; - } else { - return {}; - } - } -); - -export const updateTimeContextFromTimeRangeUpdate = actions.assign( - (context: LogStreamQueryContext, event: LogStreamQueryEvent) => { - if ('timeRange' in event && event.type === 'UPDATE_TIME_RANGE') { - return getTimeFromEvent(context, event); - } else { - return {}; - } - } -); - -export const updateTimeContextFromRefreshIntervalUpdate = actions.assign( - (context: LogStreamQueryContext, event: LogStreamQueryEvent) => { - if ( - 'refreshInterval' in event && - 'refreshInterval' in context && - event.type === 'UPDATE_REFRESH_INTERVAL' - ) { - const pause = event.refreshInterval.pause ?? context.refreshInterval.pause; - const value = event.refreshInterval.value ?? context.refreshInterval.value; - - const nowTimestamp = Date.now(); - - const draftContext = { - refreshInterval: { - pause, - value, - }, - ...(!pause - ? { - timeRange: { - ...DEFAULT_REFRESH_TIME_RANGE, - lastChangedCompletely: nowTimestamp, - }, - } - : {}), - ...(!pause - ? { - timestamps: { - startTimestamp: datemathToEpochMillis(DEFAULT_REFRESH_TIME_RANGE.from, 'down') ?? 0, - endTimestamp: datemathToEpochMillis(DEFAULT_REFRESH_TIME_RANGE.to, 'up') ?? 0, - lastChangedTimestamp: nowTimestamp, - }, - } - : {}), - }; - - return draftContext; - } else { - return {}; - } - } -); - -const getTimeFromEvent = (context: LogStreamQueryContext, event: LogStreamQueryEvent) => { - if (!('timeRange' in event) || !('timeRange' in context) || !('timestamps' in context)) { - throw new Error('Missing keys to get time from event'); - } - - const nowTimestamp = Date.now(); - const from = event.timeRange?.from ?? context.timeRange.from; - const to = event.timeRange?.to ?? context.timeRange.to; - - const fromTimestamp = event.timeRange?.from - ? datemathToEpochMillis(from, 'down') - : context.timestamps.startTimestamp; - const toTimestamp = event.timeRange?.to - ? datemathToEpochMillis(to, 'up') - : context.timestamps.endTimestamp; - - return { - timeRange: { - from, - to, - lastChangedCompletely: - event.timeRange?.from && event.timeRange?.to - ? nowTimestamp - : context.timeRange.lastChangedCompletely, - }, - timestamps: { - startTimestamp: fromTimestamp ?? 0, - endTimestamp: toTimestamp ?? 0, - lastChangedTimestamp: nowTimestamp, - }, - }; -}; diff --git a/x-pack/solutions/observability/plugins/infra/public/observability_logs/log_stream_query_state/src/types.ts b/x-pack/solutions/observability/plugins/infra/public/observability_logs/log_stream_query_state/src/types.ts deleted file mode 100644 index 5e058626de9d1..0000000000000 --- a/x-pack/solutions/observability/plugins/infra/public/observability_logs/log_stream_query_state/src/types.ts +++ /dev/null @@ -1,160 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { RefreshInterval } from '@kbn/data-plugin/public'; -import type { - AggregateQuery, - BoolQuery, - DataViewBase, - Query, - Filter, - TimeRange, -} from '@kbn/es-query'; -import type { PageEndBufferReachedEvent } from '../../log_stream_position_state/src/notifications'; - -export type AnyQuery = Query | AggregateQuery; - -export interface ParsedQuery { - bool: BoolQuery; -} - -export interface LogStreamQueryContextWithDataViews { - dataViews: DataViewBase[]; -} - -export interface LogStreamQueryContextWithSavedQueryId { - savedQueryId: string; -} -export interface LogStreamQueryContextWithQuery { - query: AnyQuery; -} - -export interface LogStreamQueryContextWithParsedQuery { - parsedQuery: ParsedQuery; -} - -export interface LogStreamQueryContextWithFilters { - filters: Filter[]; -} - -export interface LogStreamQueryContextWithValidationError { - validationError: Error; -} - -export type ExtendedTimeRange = TimeRange & { lastChangedCompletely: number }; -export interface LogStreamQueryContextWithTimeRange { - timeRange: ExtendedTimeRange; -} - -export interface LogStreamQueryContextWithRefreshInterval { - refreshInterval: RefreshInterval; -} - -export interface Timestamps { - startTimestamp: number; - endTimestamp: number; - lastChangedTimestamp: number; -} - -export interface LogStreamQueryContextWithTimestamps { - timestamps: Timestamps; -} - -export type LogStreamQueryContextWithTime = LogStreamQueryContextWithTimeRange & - LogStreamQueryContextWithRefreshInterval & - LogStreamQueryContextWithTimestamps; - -export type LogStreamQueryTypestate = - | { - value: 'uninitialized'; - context: LogStreamQueryContextWithDataViews & LogStreamQueryContextWithTime; - } - | { - value: 'query' | { query: 'validating' }; - context: LogStreamQueryContextWithDataViews & - LogStreamQueryContextWithParsedQuery & - LogStreamQueryContextWithQuery & - LogStreamQueryContextWithFilters & - LogStreamQueryContextWithTime; - } - | { - value: { query: 'valid' }; - context: LogStreamQueryContextWithDataViews & - LogStreamQueryContextWithParsedQuery & - LogStreamQueryContextWithQuery & - LogStreamQueryContextWithFilters & - LogStreamQueryContextWithTime; - } - | { - value: { query: 'invalid' }; - context: LogStreamQueryContextWithDataViews & - LogStreamQueryContextWithParsedQuery & - LogStreamQueryContextWithQuery & - LogStreamQueryContextWithFilters & - LogStreamQueryContextWithTime & - LogStreamQueryContextWithValidationError; - } - | { - value: 'time' | { time: 'initialized' } | { time: 'streaming' } | { time: 'static' }; - context: LogStreamQueryContextWithDataViews & LogStreamQueryContextWithTime; - }; - -export type LogStreamQueryContext = LogStreamQueryTypestate['context']; - -export type LogStreamQueryStateValue = LogStreamQueryTypestate['value']; - -export interface UpdateTimeRangeEvent { - type: 'UPDATE_TIME_RANGE'; - timeRange: Partial; -} - -export interface UpdateRefreshIntervalEvent { - type: 'UPDATE_REFRESH_INTERVAL'; - refreshInterval: Partial; -} - -export type LogStreamQueryEvent = - | { - type: 'QUERY_FROM_SEARCH_BAR_CHANGED'; - query: AnyQuery; - } - | { - type: 'FILTERS_FROM_SEARCH_BAR_CHANGED'; - filters: Filter[]; - } - | { - type: 'DATA_VIEWS_CHANGED'; - dataViews: DataViewBase[]; - } - | { - type: 'VALIDATION_SUCCEEDED'; - parsedQuery: ParsedQuery; - } - | { - type: 'VALIDATION_FAILED'; - error: Error; - } - | { - type: 'INITIALIZED_FROM_URL'; - query: AnyQuery; - filters: Filter[]; - timeRange: TimeRange | null; - refreshInterval: RefreshInterval | null; - } - | { - type: 'INITIALIZED_FROM_TIME_FILTER_SERVICE'; - timeRange: TimeRange; - refreshInterval: RefreshInterval; - } - | { - type: 'TIME_FROM_TIME_FILTER_SERVICE_CHANGED'; - timeRange: TimeRange; - refreshInterval: RefreshInterval; - } - | UpdateTimeRangeEvent - | UpdateRefreshIntervalEvent - | PageEndBufferReachedEvent; diff --git a/x-pack/solutions/observability/plugins/infra/public/observability_logs/log_stream_query_state/src/url_state_storage_service.ts b/x-pack/solutions/observability/plugins/infra/public/observability_logs/log_stream_query_state/src/url_state_storage_service.ts deleted file mode 100644 index bfa7625a2beb1..0000000000000 --- a/x-pack/solutions/observability/plugins/infra/public/observability_logs/log_stream_query_state/src/url_state_storage_service.ts +++ /dev/null @@ -1,284 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { IToasts } from '@kbn/core-notifications-browser'; -import type { IKbnUrlStateStorage } from '@kbn/kibana-utils-plugin/public'; -import { withNotifyOnErrors } from '@kbn/kibana-utils-plugin/public'; -import * as Array from 'fp-ts/lib/Array'; -import * as Either from 'fp-ts/lib/Either'; -import { identity, pipe } from 'fp-ts/lib/function'; -import * as rt from 'io-ts'; -import type { InvokeCreator } from 'xstate'; -import { - defaultFilterStateKey, - defaultPositionStateKey, - DEFAULT_REFRESH_INTERVAL, -} from '@kbn/logs-shared-plugin/common'; -import moment from 'moment'; -import { createPlainError, formatErrors } from '@kbn/io-ts-utils'; -import { - getTimeRangeEndFromTime, - getTimeRangeStartFromTime, -} from '../../../../common/url_state_storage_service'; -import { minimalTimeKeyRT } from '../../../../common/time'; -import { datemathStringRT } from '../../../utils/datemath'; -import type { LogStreamQueryContext, LogStreamQueryEvent, ParsedQuery } from './types'; -import { DEFAULT_FILTERS, DEFAULT_QUERY, DEFAULT_TIMERANGE } from './defaults'; - -interface LogStreamQueryUrlStateDependencies { - filterStateKey?: string; - positionStateKey?: string; - savedQueryIdKey?: string; - toastsService: IToasts; - urlStateStorage: IKbnUrlStateStorage; -} - -type RequiredDefaults = Required>; -type OptionalDefaults = Pick; -type FullDefaults = Required; - -const requiredDefaultFilterStateValue: RequiredDefaults = { - query: DEFAULT_QUERY, - filters: DEFAULT_FILTERS, -}; - -const optionalDefaultFilterStateValue = { - timeRange: DEFAULT_TIMERANGE, - refreshInterval: DEFAULT_REFRESH_INTERVAL, -}; - -const defaultFilterStateValue: FullDefaults = { - ...requiredDefaultFilterStateValue, - ...optionalDefaultFilterStateValue, -}; - -export const safeDefaultParsedQuery: ParsedQuery = { - bool: { - must: [], - must_not: [], - should: [], - filter: [{ match_none: {} }], - }, -}; - -export const updateContextInUrl = - ({ - urlStateStorage, - filterStateKey = defaultFilterStateKey, - }: LogStreamQueryUrlStateDependencies) => - (context: LogStreamQueryContext, _event: LogStreamQueryEvent) => { - if ( - !('query' in context) || - !('filters' in context) || - !('timeRange' in context) || - !('refreshInterval' in context) - ) { - throw new Error('Missing keys from context needed to sync to the URL'); - } - - urlStateStorage.set( - filterStateKey, - filterStateInUrlRT.encode({ - query: context.query, - filters: context.filters, - timeRange: context.timeRange, - refreshInterval: context.refreshInterval, - }), - { replace: true } - ); - }; - -export const initializeFromUrl = - ({ - filterStateKey = defaultFilterStateKey, - positionStateKey = defaultPositionStateKey, - toastsService, - urlStateStorage, - }: LogStreamQueryUrlStateDependencies): InvokeCreator< - LogStreamQueryContext, - LogStreamQueryEvent - > => - (_context, _event) => - (send) => { - const filterQueryValueFromUrl = - urlStateStorage.get(filterStateKey) ?? requiredDefaultFilterStateValue; - const filterQueryE = decodeFilterQueryValueFromUrl(filterQueryValueFromUrl); - - // NOTE: Access logPosition for backwards compatibility with values previously stored under that key. - const positionQueryValueFromUrl = urlStateStorage.get(positionStateKey) ?? {}; - const positionQueryE = decodePositionQueryValueFromUrl(positionQueryValueFromUrl); - - if (Either.isLeft(filterQueryE) || Either.isLeft(positionQueryE)) { - withNotifyOnErrors(toastsService).onGetError( - createPlainError( - formatErrors([ - ...(Either.isLeft(filterQueryE) ? filterQueryE.left : []), - ...(Either.isLeft(positionQueryE) ? positionQueryE.left : []), - ]) - ) - ); - - send({ - type: 'INITIALIZED_FROM_URL', - query: defaultFilterStateValue.query, - filters: defaultFilterStateValue.filters, - timeRange: null, - refreshInterval: null, - }); - } else { - send({ - type: 'INITIALIZED_FROM_URL', - query: filterQueryE.right.query ?? defaultFilterStateValue.query, - filters: filterQueryE.right.filters ?? defaultFilterStateValue.filters, - timeRange: pipe( - // Via the logFilter key - pipe( - filterQueryE.right.timeRange, - Either.fromNullable(null), - Either.chain(({ from, to }) => - from && to ? Either.right({ from, to }) : Either.left(null) - ) - ), - // Via the legacy logPosition key, and start / end timeRange parameters - Either.alt(() => - pipe( - positionQueryE.right, - Either.fromNullable(null), - Either.chain(({ start, end }) => - start && end ? Either.right({ from: start, to: end }) : Either.left(null) - ) - ) - ), - // Via the legacy logPosition key, and deriving from / to from position.time - Either.alt(() => - pipe( - positionQueryE.right, - Either.fromNullable(null), - Either.chain(({ position }) => - position && position.time - ? Either.right({ - from: getTimeRangeStartFromTime(moment(position.time).valueOf()), - to: getTimeRangeEndFromTime(moment(position.time).valueOf()), - }) - : Either.left(null) - ) - ) - ), - Either.fold(identity, identity) - ), - refreshInterval: pipe( - // Via the logFilter key - pipe(filterQueryE.right.refreshInterval, Either.fromNullable(null)), - // Via the legacy logPosition key, and the boolean streamLive parameter - Either.alt(() => - pipe( - positionQueryE.right, - Either.fromNullable(null), - Either.chain(({ streamLive }) => - typeof streamLive === 'boolean' - ? Either.right({ - pause: !streamLive, - value: defaultFilterStateValue.refreshInterval.value, // NOTE: Was not previously synced to the URL, so falls straight to the default. - }) - : Either.left(null) - ) - ) - ), - Either.fold(identity, identity) - ), - }); - } - }; - -const legacyLegacyFilterStateWithExpressionInUrlRT = rt.type({ - kind: rt.literal('kuery'), - expression: rt.string, -}); - -export const legacyPositionStateInUrlRT = rt.partial({ - streamLive: rt.boolean, - start: datemathStringRT, - end: datemathStringRT, - position: rt.union([rt.partial(minimalTimeKeyRT.props), rt.null]), -}); - -const decodeFilterQueryValueFromUrl = (queryValueFromUrl: unknown) => - Either.getAltValidation(Array.getMonoid()).alt( - pipe( - pipe( - legacyLegacyFilterStateWithExpressionInUrlRT.decode(queryValueFromUrl), - Either.map(({ expression, kind }) => ({ query: { language: kind, query: expression } })) - ), - Either.alt(() => - pipe( - legacyFilterStateInUrlRT.decode(queryValueFromUrl), - Either.map((legacyQuery) => ({ query: legacyQuery })) - ) - ) - ), - () => filterStateInUrlRT.decode(queryValueFromUrl) - ); - -const decodePositionQueryValueFromUrl = (queryValueFromUrl: unknown) => { - return legacyPositionStateInUrlRT.decode(queryValueFromUrl); -}; - -export type FilterStateInUrl = rt.TypeOf; - -export const filterMeta = rt.partial({ - alias: rt.union([rt.string, rt.null]), - disabled: rt.boolean, - negate: rt.boolean, - controlledBy: rt.string, - group: rt.string, - index: rt.string, - isMultiIndex: rt.boolean, - type: rt.string, - key: rt.string, - params: rt.any, - value: rt.any, -}); - -export const filter = rt.intersection([ - rt.type({ - meta: filterMeta, - }), - rt.partial({ - query: rt.UnknownRecord, - }), -]); - -export const filterStateInUrlRT = rt.partial({ - query: rt.union([ - rt.strict({ - language: rt.string, - query: rt.union([rt.string, rt.record(rt.string, rt.unknown)]), - }), - rt.strict({ - esql: rt.string, - }), - ]), - filters: rt.array(filter), - timeRange: rt.strict({ - from: rt.string, - to: rt.string, - }), - refreshInterval: rt.strict({ - pause: rt.boolean, - value: rt.number, - }), -}); - -export const legacyFilterStateInUrlRT = rt.union([ - rt.strict({ - language: rt.string, - query: rt.union([rt.string, rt.record(rt.string, rt.unknown)]), - }), - rt.strict({ - esql: rt.string, - }), -]); diff --git a/x-pack/solutions/observability/plugins/infra/public/observability_logs/log_stream_query_state/src/validate_query_service.ts b/x-pack/solutions/observability/plugins/infra/public/observability_logs/log_stream_query_state/src/validate_query_service.ts deleted file mode 100644 index 5834b40f838c7..0000000000000 --- a/x-pack/solutions/observability/plugins/infra/public/observability_logs/log_stream_query_state/src/validate_query_service.ts +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { IToasts } from '@kbn/core-notifications-browser'; -import type { EsQueryConfig } from '@kbn/es-query'; -import { buildEsQuery, isOfQueryType } from '@kbn/es-query'; -import { i18n } from '@kbn/i18n'; -import type { InvokeCreator } from 'xstate'; -import { QueryParsingError, UnsupportedLanguageError } from './errors'; -import type { LogStreamQueryContext, LogStreamQueryEvent } from './types'; - -export const validateQuery = - ({ - kibanaQuerySettings, - }: { - kibanaQuerySettings: EsQueryConfig; - }): InvokeCreator => - (context) => - (send) => { - if (!('query' in context)) { - throw new Error('Failed to validate query: no query in context'); - } - - const { dataViews, query, filters } = context; - - if (!isOfQueryType(query)) { - send({ - type: 'VALIDATION_FAILED', - error: new UnsupportedLanguageError('Failed to validate query: unsupported language'), - }); - - return; - } - - try { - const parsedQuery = buildEsQuery(dataViews, query, filters, kibanaQuerySettings); - - send({ - type: 'VALIDATION_SUCCEEDED', - parsedQuery, - }); - } catch (error) { - send({ - type: 'VALIDATION_FAILED', - error: new QueryParsingError(`${error}`), - }); - } - }; - -export const showValidationErrorToast = - ({ toastsService }: { toastsService: IToasts }) => - (_context: LogStreamQueryContext, event: LogStreamQueryEvent) => { - if (event.type !== 'VALIDATION_FAILED') { - return; - } - - toastsService.addError(event.error, { - title: validationErrorToastTitle, - }); - }; - -const validationErrorToastTitle = i18n.translate( - 'xpack.infra.logsPage.toolbar.logFilterErrorToastTitle', - { - defaultMessage: 'Log filter error', - } -); diff --git a/x-pack/solutions/observability/plugins/infra/public/observability_logs/xstate_helpers/README.md b/x-pack/solutions/observability/plugins/infra/public/observability_logs/xstate_helpers/README.md deleted file mode 100644 index 337f65add4226..0000000000000 --- a/x-pack/solutions/observability/plugins/infra/public/observability_logs/xstate_helpers/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# @kbn/observability-logs-xstate-helpers - -Helpers to design well-typed state machines with XState diff --git a/x-pack/solutions/observability/plugins/infra/public/observability_logs/xstate_helpers/index.ts b/x-pack/solutions/observability/plugins/infra/public/observability_logs/xstate_helpers/index.ts deleted file mode 100644 index 3b2a320ae181f..0000000000000 --- a/x-pack/solutions/observability/plugins/infra/public/observability_logs/xstate_helpers/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export * from './src'; diff --git a/x-pack/solutions/observability/plugins/infra/public/observability_logs/xstate_helpers/src/index.ts b/x-pack/solutions/observability/plugins/infra/public/observability_logs/xstate_helpers/src/index.ts deleted file mode 100644 index 67b23e66b78e8..0000000000000 --- a/x-pack/solutions/observability/plugins/infra/public/observability_logs/xstate_helpers/src/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export * from './invalid_state_callout'; -export * from './state_machine_playground'; diff --git a/x-pack/solutions/observability/plugins/infra/public/observability_logs/xstate_helpers/src/invalid_state_callout.tsx b/x-pack/solutions/observability/plugins/infra/public/observability_logs/xstate_helpers/src/invalid_state_callout.tsx deleted file mode 100644 index d6827fba2f343..0000000000000 --- a/x-pack/solutions/observability/plugins/infra/public/observability_logs/xstate_helpers/src/invalid_state_callout.tsx +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EuiCallOut } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n-react'; -import stringify from 'json-stable-stringify'; -import React from 'react'; -import type { State } from 'xstate'; - -export const InvalidStateCallout: React.FC<{ state: State }> = ({ - state, -}) => ( - - - -); - -const invalidStateCalloutTitle = i18n.translate( - 'xpack.infra.logs.common.invalidStateCalloutTitle', - { defaultMessage: 'Invalid state encountered' } -); diff --git a/x-pack/solutions/observability/plugins/infra/public/observability_logs/xstate_helpers/src/state_machine_playground.tsx b/x-pack/solutions/observability/plugins/infra/public/observability_logs/xstate_helpers/src/state_machine_playground.tsx deleted file mode 100644 index 5a40bd5d32292..0000000000000 --- a/x-pack/solutions/observability/plugins/infra/public/observability_logs/xstate_helpers/src/state_machine_playground.tsx +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EuiButton } from '@elastic/eui'; -import React, { useCallback } from 'react'; -import { useLogViewContext } from '@kbn/logs-shared-plugin/public'; - -export const StateMachinePlayground = () => { - const { changeLogViewReference, revertToDefaultLogView, update, isLoading, logViewStateService } = - useLogViewContext(); - - const switchToInlineLogView = useCallback(() => { - changeLogViewReference({ - type: 'log-view-inline', - id: 'playground-log-view', - attributes: { - name: 'playground-log-view-name', - description: 'from the state machine playground', - logIndices: { type: 'index_name', indexName: 'logs-*' }, - logColumns: [ - { - fieldColumn: { - id: 'playground-field-column', - field: 'event.dataset', - }, - }, - ], - }, - }); - }, [changeLogViewReference]); - - const updateLogView = useCallback(() => { - update({ - name: 'Updated playground name', - }); - }, [update]); - - const persistInlineLogView = useCallback(() => { - logViewStateService.send({ - type: 'PERSIST_INLINE_LOG_VIEW', - }); - }, [logViewStateService]); - - return ( - <> - {isLoading && 'Is loading'} - switchToInlineLogView()} - > - {'Switch to inline Log View'} - - persistInlineLogView()} - > - {'Persist inline Log View'} - - revertToDefaultLogView()} - > - {'Revert to default (persisted) Log View'} - - updateLogView()} - > - {'Update log view'} - - - ); -}; diff --git a/x-pack/solutions/observability/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/category_example_message.tsx b/x-pack/solutions/observability/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/category_example_message.tsx index 59885c7ce3b1b..4fa36fc450a0c 100644 --- a/x-pack/solutions/observability/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/category_example_message.tsx +++ b/x-pack/solutions/observability/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/category_example_message.tsx @@ -115,8 +115,8 @@ export const CategoryExampleMessage: React.FunctionComponent<{ onClose={closeMenu} items={[ { - label: i18n.translate('xpack.infra.logs.categoryExample.viewInStreamText', { - defaultMessage: 'View in stream', + label: i18n.translate('xpack.infra.logs.categoryExample.viewInDiscoverText', { + defaultMessage: 'View in Discover', }), onClick: viewInStreamLinkProps.onClick!, href: viewInStreamLinkProps.href, diff --git a/x-pack/solutions/observability/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/log_entry_example.tsx b/x-pack/solutions/observability/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/log_entry_example.tsx index 61b1685cf36e7..1152b7c96ac17 100644 --- a/x-pack/solutions/observability/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/log_entry_example.tsx +++ b/x-pack/solutions/observability/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/log_entry_example.tsx @@ -54,10 +54,10 @@ const VIEW_DETAILS_LABEL = i18n.translate( } ); -const VIEW_IN_STREAM_LABEL = i18n.translate( - 'xpack.infra.logs.analysis.logEntryExamplesViewInStreamLabel', +const VIEW_IN_DISCOVER_LABEL = i18n.translate( + 'xpack.infra.logs.analysis.logEntryExamplesViewInDiscoverLabel', { - defaultMessage: 'View in stream', + defaultMessage: 'View in Discover', } ); @@ -160,7 +160,7 @@ export const LogEntryExampleMessage: React.FunctionComponent = ({ }, }, { - label: VIEW_IN_STREAM_LABEL, + label: VIEW_IN_DISCOVER_LABEL, onClick: viewInStreamLinkProps.onClick, href: viewInStreamLinkProps.href, }, diff --git a/x-pack/solutions/observability/plugins/infra/public/pages/logs/page_content.tsx b/x-pack/solutions/observability/plugins/infra/public/pages/logs/page_content.tsx index 3eb3c9fae53de..774eef04e6274 100644 --- a/x-pack/solutions/observability/plugins/infra/public/pages/logs/page_content.tsx +++ b/x-pack/solutions/observability/plugins/infra/public/pages/logs/page_content.tsx @@ -10,14 +10,13 @@ import { i18n } from '@kbn/i18n'; import React, { useContext } from 'react'; import { Routes, Route } from '@kbn/shared-ux-router'; import { useKibana } from '@kbn/kibana-react-plugin/public'; -import { HeaderMenuPortal, useLinkProps } from '@kbn/observability-shared-plugin/public'; +import { HeaderMenuPortal } from '@kbn/observability-shared-plugin/public'; import type { SharePublicStart } from '@kbn/share-plugin/public/plugin'; import { type ObservabilityOnboardingLocatorParams, OBSERVABILITY_ONBOARDING_LOCATOR, } from '@kbn/deeplinks-observability'; import { dynamic } from '@kbn/shared-ux-utility'; -import { isDevMode } from '@kbn/xstate-utils'; import { type LogsLocatorParams, LOGS_LOCATOR_ID } from '@kbn/logs-shared-plugin/common'; import { LazyAlertDropdownWrapper } from '../../alerting/log_threshold'; import { HelpCenterContent } from '../../components/help_center_content'; @@ -34,12 +33,6 @@ const LogEntryRatePage = dynamic(() => import('./log_entry_rate').then((mod) => ({ default: mod.LogEntryRatePage })) ); -const StateMachinePlayground = dynamic(() => - import('../../observability_logs/xstate_helpers').then((mod) => ({ - default: mod.StateMachinePlayground, - })) -); - export const LogsPageContent: React.FunctionComponent = () => { const { application, share } = useKibana<{ share: SharePublicStart }>().services; @@ -49,17 +42,10 @@ export const LogsPageContent: React.FunctionComponent = () => { ); const { setHeaderActionMenu, theme$ } = useContext(HeaderActionMenuContext); - const enableDeveloperRoutes = isDevMode(); - useReadOnlyBadge(!uiCapabilities?.logs?.save); const routes = getLogsAppRoutes(); - const settingsLinkProps = useLinkProps({ - app: 'logs', - pathname: 'settings', - }); - return ( <> @@ -69,9 +55,6 @@ export const LogsPageContent: React.FunctionComponent = () => { - - {settingsTabTitle} - { /> - {enableDeveloperRoutes && ( - - )} @@ -115,10 +95,6 @@ const pageTitle = i18n.translate('xpack.infra.header.logsTitle', { defaultMessage: 'Logs', }); -const settingsTabTitle = i18n.translate('xpack.infra.logs.index.settingsTabTitle', { - defaultMessage: 'Settings', -}); - const feedbackLinkUrl = 'https://discuss.elastic.co/c/logs'; const ADD_DATA_LABEL = i18n.translate('xpack.infra.logsHeaderAddDataButtonLabel', { diff --git a/x-pack/solutions/observability/plugins/infra/public/pages/logs/settings/add_log_column_popover.tsx b/x-pack/solutions/observability/plugins/infra/public/pages/logs/settings/add_log_column_popover.tsx deleted file mode 100644 index 1b0fe39429609..0000000000000 --- a/x-pack/solutions/observability/plugins/infra/public/pages/logs/settings/add_log_column_popover.tsx +++ /dev/null @@ -1,161 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { EuiSelectableOption } from '@elastic/eui'; -import { EuiBadge, EuiButton, EuiPopover, EuiPopoverTitle, EuiSelectable } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n-react'; -import React, { useCallback, useMemo } from 'react'; -import { v4 as uuidv4 } from 'uuid'; -import styled from '@emotion/styled'; -import type { LogColumnConfiguration } from '../../../utils/source_configuration'; -import { useVisibilityState } from '../../../hooks/use_visibility_state'; - -interface SelectableColumnOption { - optionProps: EuiSelectableOption; - columnConfiguration: LogColumnConfiguration; -} - -export const AddLogColumnButtonAndPopover: React.FunctionComponent<{ - addLogColumn: (logColumnConfiguration: LogColumnConfiguration) => void; - availableFields: string[]; - isDisabled?: boolean; -}> = ({ addLogColumn, availableFields, isDisabled }) => { - const { isVisible: isOpen, show: openPopover, hide: closePopover } = useVisibilityState(false); - - const availableColumnOptions = useMemo( - () => [ - { - optionProps: { - append: , - 'data-test-subj': 'addTimestampLogColumn', - // this key works around EuiSelectable using a lowercased label as - // key, which leads to conflicts with field names - key: 'timestamp', - label: 'Timestamp', - }, - columnConfiguration: { - timestampColumn: { - id: uuidv4(), - }, - }, - }, - { - optionProps: { - 'data-test-subj': 'addMessageLogColumn', - append: , - // this key works around EuiSelectable using a lowercased label as - // key, which leads to conflicts with field names - key: 'message', - label: 'Message', - }, - columnConfiguration: { - messageColumn: { - id: uuidv4(), - }, - }, - }, - ...availableFields.map((field) => ({ - optionProps: { - 'data-test-subj': `addFieldLogColumn addFieldLogColumn:${field}`, - // this key works around EuiSelectable using a lowercased label as - // key, which leads to conflicts with fields that only differ in the - // case (e.g. the metricbeat mongodb module) - key: `field-${field}`, - label: field, - }, - columnConfiguration: { - fieldColumn: { - id: uuidv4(), - field, - }, - }, - })), - ], - [availableFields] - ); - - const availableOptions = useMemo( - () => availableColumnOptions.map((availableColumnOption) => availableColumnOption.optionProps), - [availableColumnOptions] - ); - - const handleColumnSelection = useCallback( - (selectedOptions: EuiSelectableOption[]) => { - closePopover(); - - const selectedOptionIndex = selectedOptions.findIndex( - (selectedOption) => selectedOption.checked === 'on' - ); - const selectedOption = availableColumnOptions[selectedOptionIndex]; - - addLogColumn(selectedOption.columnConfiguration); - }, - [addLogColumn, availableColumnOptions, closePopover] - ); - - return ( - - - - } - closePopover={closePopover} - id="addLogColumn" - isOpen={isOpen} - ownFocus - panelPaddingSize="none" - > - - {(list, search) => ( - - {search} - {list} - - )} - - - ); -}; - -const searchProps = { - 'data-test-subj': 'fieldSearchInput', -}; - -const selectableListProps = { - showIcons: false, -}; - -const SystemColumnBadge: React.FunctionComponent = () => ( - - - -); - -const SelectableContent = styled.div` - width: 400px; -`; diff --git a/x-pack/solutions/observability/plugins/infra/public/pages/logs/settings/form_elements.tsx b/x-pack/solutions/observability/plugins/infra/public/pages/logs/settings/form_elements.tsx deleted file mode 100644 index 151a070a328b0..0000000000000 --- a/x-pack/solutions/observability/plugins/infra/public/pages/logs/settings/form_elements.tsx +++ /dev/null @@ -1,244 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import equal from 'fast-deep-equal'; -import { useCallback, useMemo, useState } from 'react'; -import useAsync from 'react-use/lib/useAsync'; -import type { ObjectEntries } from '../../../../common/utility_types'; -import type { ChildFormValidationError, GenericValidationError } from './validation_errors'; - -const unsetValue = Symbol('unset form value'); - -type ValueUpdater = (updater: (previousValue: Value) => Value) => void; - -export interface FormElement { - initialValue: Value; - isDirty: boolean; - resetValue: () => void; - updateValue: ValueUpdater; - validity: FormElementValidity; - value: Value; -} - -type FormElementMap = { - [formElementName in keyof FormValues]: FormElement; -}; - -export interface CompositeFormElement - extends FormElement { - childFormElements: FormElementMap; -} - -export type FormElementValidity = - | { validity: 'valid' } - | { validity: 'invalid'; reasons: InvalidReason[] } - | { validity: 'pending' }; - -export const useFormElement = ({ - initialValue, - validate, -}: { - initialValue: Value; - validate?: (value: Value) => Promise; -}): FormElement => { - const [changedValue, setChangedValue] = useState(unsetValue); - - const value = changedValue !== unsetValue ? changedValue : initialValue; - - const updateValue = useCallback>( - (updater) => - setChangedValue((previousValue) => - previousValue === unsetValue ? updater(initialValue) : updater(previousValue) - ), - [initialValue] - ); - - const resetValue = useCallback(() => setChangedValue(unsetValue), []); - - const isDirty = useMemo(() => !equal(value, initialValue), [value, initialValue]); - - const validity = useValidity(value, validate); - - return useMemo( - () => ({ - initialValue, - isDirty, - resetValue, - updateValue, - validity, - value, - }), - [initialValue, isDirty, resetValue, updateValue, validity, value] - ); -}; - -export const useCompositeFormElement = ({ - childFormElements, - validate, -}: { - childFormElements: FormElementMap; - validate?: (values: FormValues) => Promise; -}): CompositeFormElement => { - const childFormElementEntries = useMemo( - () => Object.entries(childFormElements) as ObjectEntries, - // eslint-disable-next-line react-hooks/exhaustive-deps - Object.entries(childFormElements).flat() - ); - - const value = useMemo( - () => - childFormElementEntries.reduce( - (accumulatedFormValues, [formElementName, formElement]) => ({ - ...accumulatedFormValues, - [formElementName]: formElement.value, - }), - {} as FormValues - ), - [childFormElementEntries] - ); - - const updateValue = useCallback( - (updater: (previousValues: FormValues) => FormValues) => { - const newValues = updater(value); - - childFormElementEntries.forEach(([formElementName, formElement]) => - formElement.updateValue(() => newValues[formElementName]) - ); - }, - [childFormElementEntries, value] - ); - - const isDirty = useMemo( - () => childFormElementEntries.some(([, formElement]) => formElement.isDirty), - [childFormElementEntries] - ); - - const formValidity = useValidity(value, validate); - const childFormElementsValidity = useMemo< - FormElementValidity - >(() => { - if ( - childFormElementEntries.some(([, formElement]) => formElement.validity.validity === 'invalid') - ) { - return { - validity: 'invalid', - reasons: [{ type: 'child' }], - }; - } else if ( - childFormElementEntries.some(([, formElement]) => formElement.validity.validity === 'pending') - ) { - return { - validity: 'pending', - }; - } else { - return { - validity: 'valid', - }; - } - }, [childFormElementEntries]); - - const validity = useMemo( - () => getCombinedValidity(formValidity, childFormElementsValidity), - [formValidity, childFormElementsValidity] - ); - - const resetValue = useCallback(() => { - childFormElementEntries.forEach(([, formElement]) => formElement.resetValue()); - }, [childFormElementEntries]); - - const initialValue = useMemo( - () => - childFormElementEntries.reduce( - (accumulatedFormValues, [formElementName, formElement]) => ({ - ...accumulatedFormValues, - [formElementName]: formElement.initialValue, - }), - {} as FormValues - ), - [childFormElementEntries] - ); - - return useMemo( - () => ({ - childFormElements, - initialValue, - isDirty, - resetValue, - updateValue, - validity, - value, - }), - [childFormElements, initialValue, isDirty, resetValue, updateValue, validity, value] - ); -}; - -const useValidity = ( - value: Value, - validate?: (value: Value) => Promise -) => { - const validationState = useAsync( - () => validate?.(value) ?? Promise.resolve([]), - [validate, value] - ); - - const validity = useMemo>(() => { - if (validationState.loading) { - return { validity: 'pending' as const }; - } else if (validationState.error != null) { - return { - validity: 'invalid' as const, - reasons: [ - { - type: 'generic' as const, - message: `${validationState.error}`, - }, - ], - }; - } else if (validationState.value && validationState.value.length > 0) { - return { - validity: 'invalid' as const, - reasons: validationState.value, - }; - } else { - return { - validity: 'valid' as const, - }; - } - }, [validationState.error, validationState.loading, validationState.value]); - - return validity; -}; - -export const getCombinedValidity = ( - first: FormElementValidity, - second: FormElementValidity -): FormElementValidity => { - if (first.validity === 'invalid' || second.validity === 'invalid') { - return { - validity: 'invalid', - reasons: [ - ...(first.validity === 'invalid' ? first.reasons : []), - ...(second.validity === 'invalid' ? second.reasons : []), - ], - }; - } else if (first.validity === 'pending' || second.validity === 'pending') { - return { - validity: 'pending', - }; - } else { - return { - validity: 'valid', - }; - } -}; - -export const isFormElementForType = - (isValue: (value: any) => value is Value) => - ( - formElement: FormElement - ): formElement is FormElement => - isValue(formElement.value); diff --git a/x-pack/solutions/observability/plugins/infra/public/pages/logs/settings/form_field_props.tsx b/x-pack/solutions/observability/plugins/infra/public/pages/logs/settings/form_field_props.tsx deleted file mode 100644 index 27f8c50db74f9..0000000000000 --- a/x-pack/solutions/observability/plugins/infra/public/pages/logs/settings/form_field_props.tsx +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import React from 'react'; -import type { FormElement } from './form_elements'; -import { LogSourceConfigurationFormError } from './source_configuration_form_errors'; -import type { FormValidationError } from './validation_errors'; - -export const getFormRowProps = (formElement: FormElement) => ({ - error: - formElement.validity.validity === 'invalid' - ? formElement.validity.reasons.map((error) => ( - - )) - : [], - isInvalid: formElement.validity.validity === 'invalid', -}); - -export const getInputFieldProps = - ( - decodeInputValue: (value: string) => Value, - encodeInputValue: (value: Value) => string - ) => - (formElement: FormElement) => ({ - isInvalid: formElement.validity.validity === 'invalid', - onChange: (evt: React.ChangeEvent) => { - const newValue = evt.currentTarget.value; - formElement.updateValue(() => decodeInputValue(newValue)); - }, - value: encodeInputValue(formElement.value), - }); - -export const getStringInputFieldProps = getInputFieldProps( - (value) => `${value}`, - (value) => value -); diff --git a/x-pack/solutions/observability/plugins/infra/public/pages/logs/settings/index.ts b/x-pack/solutions/observability/plugins/infra/public/pages/logs/settings/index.ts deleted file mode 100644 index fd8dc9a06753b..0000000000000 --- a/x-pack/solutions/observability/plugins/infra/public/pages/logs/settings/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export * from './source_configuration_settings'; diff --git a/x-pack/solutions/observability/plugins/infra/public/pages/logs/settings/index_names_configuration_panel.tsx b/x-pack/solutions/observability/plugins/infra/public/pages/logs/settings/index_names_configuration_panel.tsx deleted file mode 100644 index cf6629df42215..0000000000000 --- a/x-pack/solutions/observability/plugins/infra/public/pages/logs/settings/index_names_configuration_panel.tsx +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EuiCode, EuiDescribedFormGroup, EuiFieldText, EuiFormRow } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n-react'; -import React from 'react'; -import { useTrackPageview } from '@kbn/observability-shared-plugin/public'; -import type { LogIndexNameReference } from '@kbn/logs-shared-plugin/common'; -import type { FormElement } from './form_elements'; -import { getFormRowProps, getInputFieldProps } from './form_field_props'; -import type { FormValidationError } from './validation_errors'; - -export const IndexNamesConfigurationPanel: React.FC<{ - isLoading: boolean; - isReadOnly: boolean; - indexNamesFormElement: FormElement; -}> = ({ isLoading, isReadOnly, indexNamesFormElement }) => { - useTrackPageview({ app: 'infra_logs', path: 'log_source_configuration_index_name' }); - useTrackPageview({ - app: 'infra_logs', - path: 'log_source_configuration_index_name', - delay: 15000, - }); - - return ( - <> - - - - } - description={ - - } - > - logs-*,filebeat-*, - }} - /> - } - label={ - - } - {...getFormRowProps(indexNamesFormElement)} - > - - - - - ); -}; - -const getIndexNamesInputFieldProps = getInputFieldProps( - (value) => ({ - type: 'index_name', - indexName: value, - }), - ({ indexName }) => indexName -); diff --git a/x-pack/solutions/observability/plugins/infra/public/pages/logs/settings/index_pattern_configuration_panel.tsx b/x-pack/solutions/observability/plugins/infra/public/pages/logs/settings/index_pattern_configuration_panel.tsx deleted file mode 100644 index caca8bcf72594..0000000000000 --- a/x-pack/solutions/observability/plugins/infra/public/pages/logs/settings/index_pattern_configuration_panel.tsx +++ /dev/null @@ -1,115 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EuiDescribedFormGroup, EuiFormRow, EuiLink, EuiSpacer } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n-react'; -import React, { useCallback, useMemo } from 'react'; -import { useTrackPageview } from '@kbn/observability-shared-plugin/public'; -import { useLinkProps } from '@kbn/observability-shared-plugin/public'; -import type { LogDataViewReference } from '@kbn/logs-shared-plugin/common'; -import type { FormElement } from './form_elements'; -import { getFormRowProps } from './form_field_props'; -import { IndexPatternSelector } from './index_pattern_selector'; -import type { FormValidationError } from './validation_errors'; - -export const IndexPatternConfigurationPanel: React.FC<{ - isLoading: boolean; - isReadOnly: boolean; - indexPatternFormElement: FormElement; -}> = ({ isLoading, isReadOnly, indexPatternFormElement }) => { - useTrackPageview({ app: 'infra_logs', path: 'log_source_configuration_index_pattern' }); - useTrackPageview({ - app: 'infra_logs', - path: 'log_source_configuration_index_pattern', - delay: 15000, - }); - - const changeIndexPatternId = useCallback( - (dataViewId: string | undefined) => { - if (dataViewId != null) { - indexPatternFormElement.updateValue(() => ({ - type: 'data_view', - dataViewId, - })); - } else { - indexPatternFormElement.updateValue(() => undefined); - } - }, - [indexPatternFormElement] - ); - - return ( - <> - - - - - - } - description={ - - } - > - - } - {...useMemo( - () => (isLoading ? {} : getFormRowProps(indexPatternFormElement)), - [isLoading, indexPatternFormElement] - )} - > - - - - - ); -}; - -const DataViewsInlineHelpMessage = React.memo(() => { - const dataViewsManagementLinkProps = useLinkProps({ - app: 'management', - pathname: '/kibana/dataViews', - }); - - return ( - - - - ), - }} - /> - ); -}); diff --git a/x-pack/solutions/observability/plugins/infra/public/pages/logs/settings/index_pattern_selector.tsx b/x-pack/solutions/observability/plugins/infra/public/pages/logs/settings/index_pattern_selector.tsx deleted file mode 100644 index 7283cbeea47bb..0000000000000 --- a/x-pack/solutions/observability/plugins/infra/public/pages/logs/settings/index_pattern_selector.tsx +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { EuiComboBoxOptionOption } from '@elastic/eui'; -import { EuiComboBox } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import React, { useCallback, useEffect, useMemo } from 'react'; -import { useKibanaIndexPatternTitles } from '../../../hooks/use_kibana_index_patterns'; - -type IndexPatternOption = EuiComboBoxOptionOption; - -export const IndexPatternSelector: React.FC<{ - indexPatternId: string | undefined; - isLoading: boolean; - isReadOnly: boolean; - onChangeIndexPatternId: (indexPatternId: string | undefined) => void; -}> = ({ indexPatternId, isLoading, isReadOnly, onChangeIndexPatternId }) => { - const { - indexPatternTitles: availableIndexPatterns, - latestIndexPatternTitlesRequest, - fetchIndexPatternTitles, - } = useKibanaIndexPatternTitles(); - - useEffect(() => { - fetchIndexPatternTitles(); - }, [fetchIndexPatternTitles]); - - const availableOptions = useMemo(() => { - const options = [ - ...availableIndexPatterns.map(({ id, title }) => ({ - key: id, - label: title, - value: id, - })), - ...(indexPatternId == null || availableIndexPatterns.some(({ id }) => id === indexPatternId) - ? [] - : [ - { - key: indexPatternId, - label: i18n.translate('xpack.infra.logSourceConfiguration.missingDataViewsLabel', { - defaultMessage: `Missing data view {indexPatternId}`, - values: { - indexPatternId, - }, - }), - value: indexPatternId, - }, - ]), - ]; - return options; - }, [availableIndexPatterns, indexPatternId]); - - const selectedOptions = useMemo( - () => availableOptions.filter(({ key }) => key === indexPatternId), - [availableOptions, indexPatternId] - ); - - const changeSelectedIndexPatterns = useCallback( - ([newlySelectedOption]: IndexPatternOption[]) => { - if (typeof newlySelectedOption?.key === 'string') { - return onChangeIndexPatternId(newlySelectedOption.key); - } - - return onChangeIndexPatternId(undefined); - }, - [onChangeIndexPatternId] - ); - - return ( - - isLoading={isLoading || latestIndexPatternTitlesRequest.state === 'pending'} - isDisabled={isReadOnly} - options={availableOptions} - placeholder={indexPatternSelectorPlaceholder} - selectedOptions={selectedOptions} - singleSelection={{ asPlainText: true }} - onChange={changeSelectedIndexPatterns} - /> - ); -}; - -const indexPatternSelectorPlaceholder = i18n.translate( - 'xpack.infra.logSourceConfiguration.dataViewSelectorPlaceholder', - { defaultMessage: 'Choose a data view' } -); diff --git a/x-pack/solutions/observability/plugins/infra/public/pages/logs/settings/indices_configuration_form_state.ts b/x-pack/solutions/observability/plugins/infra/public/pages/logs/settings/indices_configuration_form_state.ts deleted file mode 100644 index 46f3980c95323..0000000000000 --- a/x-pack/solutions/observability/plugins/infra/public/pages/logs/settings/indices_configuration_form_state.ts +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { useMemo } from 'react'; -import { SavedObjectNotFound } from '@kbn/kibana-utils-plugin/common'; -import { useUiTracker } from '@kbn/observability-shared-plugin/public'; -import type { - LogDataViewReference, - LogIndexNameReference, - LogSourcesKibanaAdvancedSettingReference, -} from '@kbn/logs-shared-plugin/common'; -import { - logIndexNameReferenceRT, - logSourcesKibanaAdvancedSettingRT, -} from '@kbn/logs-shared-plugin/common'; -import { useKibanaIndexPatternService } from '../../../hooks/use_kibana_index_patterns'; -import { useFormElement } from './form_elements'; -import type { FormValidationError } from './validation_errors'; -import { - validateIndexPattern, - validateStringNotEmpty, - validateStringNoSpaces, -} from './validation_errors'; - -export type LogIndicesFormState = - | LogIndexNameReference - | LogDataViewReference - | LogSourcesKibanaAdvancedSettingReference - | undefined; - -export const useLogIndicesFormElement = (initialValue: LogIndicesFormState) => { - const indexPatternService = useKibanaIndexPatternService(); - - const trackIndexPatternValidationError = useUiTracker({ app: 'infra_logs' }); - - const logIndicesFormElement = useFormElement({ - initialValue, - validate: useMemo( - () => async (logIndices) => { - if (logIndices == null) { - return validateStringNotEmpty('log data view', ''); - } else if (logSourcesKibanaAdvancedSettingRT.is(logIndices)) { - return []; - } else if (logIndexNameReferenceRT.is(logIndices)) { - return [ - ...validateStringNotEmpty('log indices', logIndices.indexName), - ...validateStringNoSpaces('log indices', logIndices.indexName), - ]; - } else { - const emptyStringErrors = validateStringNotEmpty('log data view', logIndices.dataViewId); - - if (emptyStringErrors.length > 0) { - return emptyStringErrors; - } - - const indexPatternErrors = await indexPatternService - .get(logIndices.dataViewId) - .then(validateIndexPattern, (error): FormValidationError[] => { - if (error instanceof SavedObjectNotFound) { - return [ - { - type: 'missing_index_pattern' as const, - indexPatternId: logIndices.dataViewId, - }, - ]; - } else { - throw error; - } - }); - - if (indexPatternErrors.length > 0) { - trackIndexPatternValidationError({ - metric: 'configuration_index_pattern_validation_failed', - }); - } else { - trackIndexPatternValidationError({ - metric: 'configuration_index_pattern_validation_succeeded', - }); - } - - return indexPatternErrors; - } - }, - [indexPatternService, trackIndexPatternValidationError] - ), - }); - - return logIndicesFormElement; -}; diff --git a/x-pack/solutions/observability/plugins/infra/public/pages/logs/settings/indices_configuration_panel.stories.tsx b/x-pack/solutions/observability/plugins/infra/public/pages/logs/settings/indices_configuration_panel.stories.tsx deleted file mode 100644 index 864797635312e..0000000000000 --- a/x-pack/solutions/observability/plugins/infra/public/pages/logs/settings/indices_configuration_panel.stories.tsx +++ /dev/null @@ -1,148 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { PropsOf } from '@elastic/eui'; -import { EuiCodeBlock, EuiPage, EuiPageBody, EuiPanel } from '@elastic/eui'; -import { I18nProvider } from '@kbn/i18n-react'; -import type { Meta, Story } from '@storybook/react/types-6-0'; -import React from 'react'; -import { KBN_FIELD_TYPES } from '@kbn/data-plugin/public'; -import type { MockIndexPatternSpec } from '../../../hooks/use_kibana_index_patterns.mock'; -import { MockIndexPatternsKibanaContextProvider } from '../../../hooks/use_kibana_index_patterns.mock'; -import { decorateWithGlobalStorybookThemeProviders } from '../../../test_utils/use_global_storybook_theme'; -import type { LogIndicesFormState } from './indices_configuration_form_state'; -import { useLogIndicesFormElement } from './indices_configuration_form_state'; -import { IndicesConfigurationPanel } from './indices_configuration_panel'; - -export default { - title: 'infra/logsSettings/indicesConfiguration', - decorators: [ - (WrappedStory, { args }) => { - return ( - - - - - - - - - - - - ); - }, - decorateWithGlobalStorybookThemeProviders, - ], - argTypes: { - logIndices: { - control: { - type: 'object', - }, - }, - availableIndexPatterns: { - control: { - type: 'object', - }, - }, - }, -} as Meta; - -type IndicesConfigurationPanelProps = PropsOf; - -type IndicesConfigurationPanelStoryArgs = Pick< - IndicesConfigurationPanelProps, - 'isLoading' | 'isReadOnly' -> & { - availableIndexPatterns: MockIndexPatternSpec[]; - logIndices: LogIndicesFormState; -}; - -const IndicesConfigurationPanelTemplate: Story = ({ - isLoading, - isReadOnly, - logIndices, -}) => { - const logIndicesFormElement = useLogIndicesFormElement(logIndices); - - return ( - <> - - - // field states{'\n'} - {JSON.stringify( - { - logIndices: { - value: logIndicesFormElement.value, - validity: logIndicesFormElement.validity, - }, - }, - null, - 2 - )} - - - ); -}; - -const defaultArgs: IndicesConfigurationPanelStoryArgs = { - isLoading: false, - isReadOnly: false, - logIndices: { - type: 'index_name' as const, - indexName: 'logs-*', - }, - availableIndexPatterns: [ - { - id: 'INDEX_PATTERN_A', - title: 'pattern-a-*', - timeFieldName: '@timestamp', - type: undefined, - fields: [ - { - name: '@timestamp', - type: KBN_FIELD_TYPES.DATE, - searchable: true, - aggregatable: true, - }, - { - name: 'message', - type: KBN_FIELD_TYPES.STRING, - searchable: true, - aggregatable: true, - }, - ], - }, - { - id: 'INDEX_PATTERN_B', - title: 'pattern-b-*', - timeFieldName: '@timestamp', - type: undefined, - fields: [], - }, - ], -}; - -export const IndexNameWithDefaultFields = IndicesConfigurationPanelTemplate.bind({}); - -IndexNameWithDefaultFields.args = { - ...defaultArgs, -}; - -export const IndexPattern = IndicesConfigurationPanelTemplate.bind({}); - -IndexPattern.args = { - ...defaultArgs, - logIndices: undefined, -}; diff --git a/x-pack/solutions/observability/plugins/infra/public/pages/logs/settings/indices_configuration_panel.tsx b/x-pack/solutions/observability/plugins/infra/public/pages/logs/settings/indices_configuration_panel.tsx deleted file mode 100644 index 70c59259a2c52..0000000000000 --- a/x-pack/solutions/observability/plugins/infra/public/pages/logs/settings/indices_configuration_panel.tsx +++ /dev/null @@ -1,246 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EuiCheckableCard, EuiFormFieldset, EuiSpacer, EuiTitle } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n-react'; -import React, { useCallback, useEffect, useState } from 'react'; -import { useUiTracker } from '@kbn/observability-shared-plugin/public'; -import type { LogDataViewReference, LogIndexReference } from '@kbn/logs-shared-plugin/common'; -import { - logIndexNameReferenceRT, - logDataViewReferenceRT, - logSourcesKibanaAdvancedSettingRT, -} from '@kbn/logs-shared-plugin/common'; -import { EuiCallOut } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { loadRuleAggregations } from '@kbn/triggers-actions-ui-plugin/public'; -import { AlertConsumers, LOG_THRESHOLD_ALERT_TYPE_ID } from '@kbn/rule-data-utils'; - -import type { RulesParams } from '@kbn/observability-plugin/public'; -import { rulesLocatorID } from '@kbn/observability-plugin/public'; -import { EuiLink } from '@elastic/eui'; -import { useKibanaContextForPlugin } from '../../../hooks/use_kibana'; -import type { FormElement } from './form_elements'; -import { isFormElementForType } from './form_elements'; -import { IndexNamesConfigurationPanel } from './index_names_configuration_panel'; -import { IndexPatternConfigurationPanel } from './index_pattern_configuration_panel'; -import type { FormValidationError } from './validation_errors'; -import { KibanaAdvancedSettingConfigurationPanel } from './kibana_advanced_setting_configuration_panel'; - -export const IndicesConfigurationPanel = React.memo<{ - isLoading: boolean; - isReadOnly: boolean; - indicesFormElement: FormElement; -}>(({ isLoading, isReadOnly, indicesFormElement }) => { - const { - services: { - http, - share: { - url: { locators }, - }, - }, - } = useKibanaContextForPlugin(); - const [numberOfLogsRules, setNumberOfLogsRules] = useState(0); - - const rulesLocator = locators.get(rulesLocatorID); - const viewAffectedRulesLink = rulesLocator?.useUrl({ type: [LOG_THRESHOLD_ALERT_TYPE_ID] }); - - const trackChangeIndexSourceType = useUiTracker({ app: 'infra_logs' }); - - const changeToIndexPatternType = useCallback(() => { - if (logDataViewReferenceRT.is(indicesFormElement.initialValue)) { - indicesFormElement.updateValue(() => indicesFormElement.initialValue); - } else { - indicesFormElement.updateValue(() => undefined); - } - - trackChangeIndexSourceType({ - metric: 'configuration_switch_to_index_pattern_reference', - }); - }, [indicesFormElement, trackChangeIndexSourceType]); - - const changeToIndexNameType = useCallback(() => { - if (indicesFormElement.initialValue?.type === 'index_name') { - indicesFormElement.updateValue(() => indicesFormElement.initialValue); - } else { - indicesFormElement.updateValue(() => ({ - type: 'index_name', - indexName: '', - })); - } - - trackChangeIndexSourceType({ - metric: 'configuration_switch_to_index_names_reference', - }); - }, [indicesFormElement, trackChangeIndexSourceType]); - - const changeToKibanaAdvancedSettingType = useCallback(() => { - // This is always a readonly value, synced with the setting, we just reset back to the correct type. - indicesFormElement.updateValue(() => ({ - type: 'kibana_advanced_setting', - })); - - trackChangeIndexSourceType({ - metric: 'configuration_switch_to_kibana_advanced_setting_reference', - }); - }, [indicesFormElement, trackChangeIndexSourceType]); - - useEffect(() => { - const getNumberOfInfraRules = async () => { - if (http) { - const { ruleExecutionStatus } = await loadRuleAggregations({ - http, - ruleTypeIds: [LOG_THRESHOLD_ALERT_TYPE_ID], - consumers: [AlertConsumers.LOGS, AlertConsumers.ALERTS, AlertConsumers.OBSERVABILITY], - }); - const numberOfRules = Object.values(ruleExecutionStatus).reduce( - (acc, value) => acc + value, - 0 - ); - setNumberOfLogsRules(numberOfRules); - } - }; - getNumberOfInfraRules(); - }, [http]); - - return ( - -

- -

- - ), - }} - > - {' '} - -

- -

- - } - name="kibanaAdvancedSetting" - value="kibanaAdvancedSetting" - checked={isKibanaAdvancedSettingFormElement(indicesFormElement)} - onChange={changeToKibanaAdvancedSettingType} - disabled={isReadOnly} - > - {isKibanaAdvancedSettingFormElement(indicesFormElement) && ( - - )} -
- - -

- -

- - } - name="dataView" - value="dataView" - checked={isDataViewFormElement(indicesFormElement)} - onChange={changeToIndexPatternType} - disabled={isReadOnly} - > - {isDataViewFormElement(indicesFormElement) && ( - - )} -
- - -

- -

- - } - name="indexNames" - value="indexNames" - checked={isIndexNamesFormElement(indicesFormElement)} - onChange={changeToIndexNameType} - disabled={isReadOnly} - data-test-subj="logIndicesCheckableCard" - > - {isIndexNamesFormElement(indicesFormElement) && ( - - )} -
- {numberOfLogsRules > 0 && indicesFormElement.isDirty && ( - <> - - - - - - - - - - )} -
- ); -}); - -const isDataViewFormElement = isFormElementForType( - (value): value is LogDataViewReference | undefined => - value == null || logDataViewReferenceRT.is(value) -); - -const isIndexNamesFormElement = isFormElementForType(logIndexNameReferenceRT.is); - -const isKibanaAdvancedSettingFormElement = isFormElementForType( - logSourcesKibanaAdvancedSettingRT.is -); diff --git a/x-pack/solutions/observability/plugins/infra/public/pages/logs/settings/inline_log_view_callout.tsx b/x-pack/solutions/observability/plugins/infra/public/pages/logs/settings/inline_log_view_callout.tsx deleted file mode 100644 index d3db574528490..0000000000000 --- a/x-pack/solutions/observability/plugins/infra/public/pages/logs/settings/inline_log_view_callout.tsx +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EuiButton } from '@elastic/eui'; -import { EuiCallOut } from '@elastic/eui'; -import React from 'react'; -import { FormattedMessage } from '@kbn/i18n-react'; - -export const InlineLogViewCallout = ({ - revertToDefaultLogView, -}: { - revertToDefaultLogView: () => void; -}) => { - return ( - - } - > - <> -

- -

- - - - -
- ); -}; diff --git a/x-pack/solutions/observability/plugins/infra/public/pages/logs/settings/kibana_advanced_setting_configuration_panel.tsx b/x-pack/solutions/observability/plugins/infra/public/pages/logs/settings/kibana_advanced_setting_configuration_panel.tsx deleted file mode 100644 index a095b2825cd3e..0000000000000 --- a/x-pack/solutions/observability/plugins/infra/public/pages/logs/settings/kibana_advanced_setting_configuration_panel.tsx +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { useTrackPageview } from '@kbn/observability-shared-plugin/public'; -import { - LogSourcesSettingSynchronisationInfo, - useLogSourcesContext, -} from '@kbn/logs-data-access-plugin/public'; -import { useKibanaContextForPlugin } from '../../../hooks/use_kibana'; - -export const KibanaAdvancedSettingConfigurationPanel: React.FC = () => { - const { - services: { application }, - } = useKibanaContextForPlugin(); - - useTrackPageview({ app: 'infra_logs', path: 'log_source_configuration_kibana_advanced_setting' }); - useTrackPageview({ - app: 'infra_logs', - path: 'log_source_configuration_kibana_advanced_setting', - delay: 15000, - }); - - const { isLoadingLogSources, combinedIndices } = useLogSourcesContext(); - - return ( - - ); -}; diff --git a/x-pack/solutions/observability/plugins/infra/public/pages/logs/settings/log_columns_configuration_form_state.tsx b/x-pack/solutions/observability/plugins/infra/public/pages/logs/settings/log_columns_configuration_form_state.tsx deleted file mode 100644 index 79526ec853055..0000000000000 --- a/x-pack/solutions/observability/plugins/infra/public/pages/logs/settings/log_columns_configuration_form_state.tsx +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { useMemo } from 'react'; -import type { LogColumnConfiguration } from '../../../utils/source_configuration'; -import { useFormElement } from './form_elements'; -import type { FormValidationError } from './validation_errors'; -import { validateColumnListNotEmpty } from './validation_errors'; - -export const useLogColumnsFormElement = (initialValue: LogColumnConfiguration[]) => { - const logColumnsFormElement = useFormElement({ - initialValue, - validate: useMemo(() => async (logColumns) => validateColumnListNotEmpty(logColumns), []), - }); - - return logColumnsFormElement; -}; diff --git a/x-pack/solutions/observability/plugins/infra/public/pages/logs/settings/log_columns_configuration_panel.tsx b/x-pack/solutions/observability/plugins/infra/public/pages/logs/settings/log_columns_configuration_panel.tsx deleted file mode 100644 index 4e0139c9d78c7..0000000000000 --- a/x-pack/solutions/observability/plugins/infra/public/pages/logs/settings/log_columns_configuration_panel.tsx +++ /dev/null @@ -1,336 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { - EuiButtonIcon, - EuiDragDropContext, - EuiDraggable, - EuiDroppable, - EuiEmptyPrompt, - EuiFlexGroup, - EuiFlexItem, - EuiIcon, - EuiPanel, - EuiSpacer, - EuiText, - EuiTitle, -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n-react'; -import React, { useCallback } from 'react'; -import type { DropResult, DragHandleProps } from '../../../types'; -import type { - FieldLogColumnConfiguration, - LogColumnConfiguration, - MessageLogColumnConfiguration, - TimestampLogColumnConfiguration, -} from '../../../utils/source_configuration'; -import { - getLogColumnConfigurationId, - isMessageLogColumnConfiguration, - isTimestampLogColumnConfiguration, -} from '../../../utils/source_configuration'; -import { AddLogColumnButtonAndPopover } from './add_log_column_popover'; -import type { FormElement } from './form_elements'; -import { LogSourceConfigurationFormError } from './source_configuration_form_errors'; -import type { FormValidationError } from './validation_errors'; - -export const LogColumnsConfigurationPanel = React.memo<{ - availableFields: string[]; - isLoading: boolean; - logColumnsFormElement: FormElement; -}>(({ availableFields, isLoading, logColumnsFormElement }) => { - const addLogColumn = useCallback( - (logColumnConfiguration: LogColumnConfiguration) => - logColumnsFormElement.updateValue((logColumns) => [...logColumns, logColumnConfiguration]), - [logColumnsFormElement] - ); - - const removeLogColumn = useCallback( - (logColumn: LogColumnConfiguration) => - logColumnsFormElement.updateValue((logColumns) => - logColumns.filter((item) => item !== logColumn) - ), - [logColumnsFormElement] - ); - - const moveLogColumn = useCallback( - (sourceIndex: number, destinationIndex: number) => { - logColumnsFormElement.updateValue((logColumns) => { - if (destinationIndex >= 0 && sourceIndex <= logColumnsFormElement.value.length - 1) { - const newLogColumns = [...logColumnsFormElement.value]; - newLogColumns.splice(destinationIndex, 0, newLogColumns.splice(sourceIndex, 1)[0]); - return newLogColumns; - } else { - return logColumns; - } - }); - }, - [logColumnsFormElement] - ); - - const onDragEnd = useCallback( - ({ source, destination }: DropResult) => - destination && moveLogColumn(source.index, destination.index), - [moveLogColumn] - ); - - return ( - <> - - - -

- -

-
-
- - - -
- {logColumnsFormElement.value.length > 0 ? ( - - - {logColumnsFormElement.value.map((logColumnConfiguration, index) => { - const columnId = getLogColumnConfigurationId(logColumnConfiguration); - return ( - - {(provided) => ( - - )} - - ); - })} - - - ) : ( - - )} - {logColumnsFormElement.validity.validity === 'invalid' - ? logColumnsFormElement.validity.reasons.map((error) => ( - - - - )) - : null} - - ); -}); - -const LogColumnConfigurationPanel: React.FunctionComponent<{ - logColumnConfiguration: LogColumnConfiguration; - dragHandleProps: DragHandleProps; - onRemove: (logColumnConfiguration: LogColumnConfiguration) => void; -}> = ({ logColumnConfiguration, dragHandleProps, onRemove }) => { - const removeColumn = useCallback( - () => onRemove(logColumnConfiguration), - [logColumnConfiguration, onRemove] - ); - - return ( - <> - - {isTimestampLogColumnConfiguration(logColumnConfiguration) ? ( - - ) : isMessageLogColumnConfiguration(logColumnConfiguration) ? ( - - ) : ( - - )} - - ); -}; - -interface LogColumnConfigurationPanelProps { - logColumnConfiguration: LogColumnConfigurationType; - dragHandleProps: DragHandleProps; - onRemove: () => void; -} - -const TimestampLogColumnConfigurationPanel: React.FunctionComponent< - LogColumnConfigurationPanelProps -> = ({ dragHandleProps, onRemove }) => ( - timestamp, - }} - /> - } - onRemove={onRemove} - dragHandleProps={dragHandleProps} - /> -); - -const MessageLogColumnConfigurationPanel: React.FunctionComponent< - LogColumnConfigurationPanelProps -> = ({ dragHandleProps, onRemove }) => ( - - } - onRemove={onRemove} - dragHandleProps={dragHandleProps} - /> -); - -const FieldLogColumnConfigurationPanel: React.FunctionComponent< - LogColumnConfigurationPanelProps -> = ({ - dragHandleProps, - logColumnConfiguration: { - fieldColumn: { field }, - }, - onRemove, -}) => { - return ( - - - -
- -
-
- {fieldLogColumnTitle} - - {field} - - - - -
-
- ); -}; - -const ExplainedLogColumnConfigurationPanel: React.FunctionComponent<{ - fieldName: React.ReactNode; - helpText: React.ReactNode; - onRemove: () => void; - dragHandleProps: DragHandleProps; -}> = ({ fieldName, helpText, onRemove, dragHandleProps }) => ( - - - -
- -
-
- {fieldName} - - - {helpText} - - - - - -
-
-); - -const RemoveLogColumnButton: React.FunctionComponent<{ - onClick?: () => void; - columnDescription: string; -}> = ({ onClick, columnDescription }) => { - const removeColumnLabel = i18n.translate( - 'xpack.infra.sourceConfiguration.removeLogColumnButtonLabel', - { - defaultMessage: 'Remove {columnDescription} column', - values: { columnDescription }, - } - ); - - return ( - - ); -}; - -const LogColumnConfigurationEmptyPrompt: React.FunctionComponent = () => ( - - - - } - body={ -

- -

- } - /> -); - -const fieldLogColumnTitle = i18n.translate('xpack.infra.sourceConfiguration.fieldLogColumnTitle', { - defaultMessage: 'Field', -}); diff --git a/x-pack/solutions/observability/plugins/infra/public/pages/logs/settings/name_configuration_form_state.tsx b/x-pack/solutions/observability/plugins/infra/public/pages/logs/settings/name_configuration_form_state.tsx deleted file mode 100644 index ae653f8d06f6a..0000000000000 --- a/x-pack/solutions/observability/plugins/infra/public/pages/logs/settings/name_configuration_form_state.tsx +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { useMemo } from 'react'; -import { useFormElement } from './form_elements'; -import type { FormValidationError } from './validation_errors'; -import { validateStringNotEmpty } from './validation_errors'; - -export const useNameFormElement = (initialValue: string) => { - const nameFormElement = useFormElement({ - initialValue, - validate: useMemo(() => async (name) => validateStringNotEmpty('name', name), []), - }); - - return nameFormElement; -}; diff --git a/x-pack/solutions/observability/plugins/infra/public/pages/logs/settings/name_configuration_panel.tsx b/x-pack/solutions/observability/plugins/infra/public/pages/logs/settings/name_configuration_panel.tsx deleted file mode 100644 index d319753d49f49..0000000000000 --- a/x-pack/solutions/observability/plugins/infra/public/pages/logs/settings/name_configuration_panel.tsx +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { - EuiDescribedFormGroup, - EuiFieldText, - EuiForm, - EuiFormRow, - EuiSpacer, - EuiTitle, -} from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n-react'; -import React, { useMemo } from 'react'; -import type { FormElement } from './form_elements'; -import { getFormRowProps, getStringInputFieldProps } from './form_field_props'; -import type { FormValidationError } from './validation_errors'; - -export const NameConfigurationPanel = React.memo<{ - isLoading: boolean; - isReadOnly: boolean; - nameFormElement: FormElement; -}>(({ isLoading, isReadOnly, nameFormElement }) => ( - - -

- -

-
- - - - - } - description={ - - } - > - - } - {...useMemo(() => getFormRowProps(nameFormElement), [nameFormElement])} - > - getStringInputFieldProps(nameFormElement), [nameFormElement])} - /> - - -
-)); diff --git a/x-pack/solutions/observability/plugins/infra/public/pages/logs/settings/source_configuration_form_errors.tsx b/x-pack/solutions/observability/plugins/infra/public/pages/logs/settings/source_configuration_form_errors.tsx deleted file mode 100644 index 2ed1237ca786a..0000000000000 --- a/x-pack/solutions/observability/plugins/infra/public/pages/logs/settings/source_configuration_form_errors.tsx +++ /dev/null @@ -1,125 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EuiCallOut, EuiCode } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n-react'; -import React from 'react'; -import type { FormValidationError } from './validation_errors'; - -export const LogSourceConfigurationFormErrors: React.FC<{ errors: FormValidationError[] }> = ({ - errors, -}) => ( - -
    - {errors.map((error, errorIndex) => ( -
  • - -
  • - ))} -
-
-); - -export const LogSourceConfigurationFormError: React.FC<{ error: FormValidationError }> = ({ - error, -}) => { - if (error.type === 'generic') { - return <>{error.message}; - } else if (error.type === 'empty_field') { - return ( - - ); - } else if (error.type === 'includes_spaces') { - return ( - - ); - } else if (error.type === 'empty_column_list') { - return ( - - ); - } else if (error.type === 'child') { - return ( - - ); - } else if (error.type === 'missing_timestamp_field') { - return ( - - ); - } else if (error.type === 'missing_message_field') { - return ( - message, - }} - /> - ); - } else if (error.type === 'invalid_message_field_type') { - return ( - message, - }} - /> - ); - } else if (error.type === 'rollup_index_pattern') { - return ( - - ); - } else if (error.type === 'missing_index_pattern') { - return ( - {error.indexPatternId}, - }} - /> - ); - } else { - return null; - } -}; - -const logSourceConfigurationFormErrorsCalloutTitle = i18n.translate( - 'xpack.infra.logSourceConfiguration.logSourceConfigurationFormErrorsCalloutTitle', - { - defaultMessage: 'Inconsistent source configuration', - } -); diff --git a/x-pack/solutions/observability/plugins/infra/public/pages/logs/settings/source_configuration_form_state.tsx b/x-pack/solutions/observability/plugins/infra/public/pages/logs/settings/source_configuration_form_state.tsx deleted file mode 100644 index eee7dd8f516e7..0000000000000 --- a/x-pack/solutions/observability/plugins/infra/public/pages/logs/settings/source_configuration_form_state.tsx +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { useMemo } from 'react'; -import type { LogViewAttributes } from '@kbn/logs-shared-plugin/common'; -import { useCompositeFormElement } from './form_elements'; -import { useLogIndicesFormElement } from './indices_configuration_form_state'; -import { useLogColumnsFormElement } from './log_columns_configuration_form_state'; -import { useNameFormElement } from './name_configuration_form_state'; - -export const useLogSourceConfigurationFormState = (logViewAttributes?: LogViewAttributes) => { - const nameFormElement = useNameFormElement(logViewAttributes?.name ?? ''); - - const logIndicesFormElement = useLogIndicesFormElement( - useMemo( - () => - logViewAttributes?.logIndices ?? { - type: 'kibana_advanced_setting', - }, - [logViewAttributes] - ) - ); - - const logColumnsFormElement = useLogColumnsFormElement( - useMemo(() => logViewAttributes?.logColumns ?? [], [logViewAttributes]) - ); - - const sourceConfigurationFormElement = useCompositeFormElement( - useMemo( - () => ({ - childFormElements: { - name: nameFormElement, - logIndices: logIndicesFormElement, - logColumns: logColumnsFormElement, - }, - validate: async () => [], - }), - [nameFormElement, logIndicesFormElement, logColumnsFormElement] - ) - ); - - return { - formState: sourceConfigurationFormElement.value, - logIndicesFormElement, - logColumnsFormElement, - nameFormElement, - sourceConfigurationFormElement, - }; -}; diff --git a/x-pack/solutions/observability/plugins/infra/public/pages/logs/settings/source_configuration_settings.tsx b/x-pack/solutions/observability/plugins/infra/public/pages/logs/settings/source_configuration_settings.tsx deleted file mode 100644 index d1df2a5820dd3..0000000000000 --- a/x-pack/solutions/observability/plugins/infra/public/pages/logs/settings/source_configuration_settings.tsx +++ /dev/null @@ -1,214 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { - EuiButton, - EuiErrorBoundary, - EuiFlexGroup, - EuiFlexItem, - EuiPanel, - EuiSpacer, -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n-react'; -import React, { useCallback, useMemo } from 'react'; -import { useKibana } from '@kbn/kibana-react-plugin/public'; -import { Prompt } from '@kbn/observability-shared-plugin/public'; -import { useTrackPageview } from '@kbn/observability-shared-plugin/public'; -import { useLogViewContext } from '@kbn/logs-shared-plugin/public'; -import { LogsDeprecationCallout } from '../../../components/logs_deprecation_callout'; -import { SourceLoadingPage } from '../../../components/source_loading_page'; -import { useLogsBreadcrumbs } from '../../../hooks/use_logs_breadcrumbs'; -import { settingsTitle } from '../../../translations'; -import { LogsPageTemplate } from '../shared/page_template'; -import { IndicesConfigurationPanel } from './indices_configuration_panel'; -import { LogColumnsConfigurationPanel } from './log_columns_configuration_panel'; -import { NameConfigurationPanel } from './name_configuration_panel'; -import { LogSourceConfigurationFormErrors } from './source_configuration_form_errors'; -import { useLogSourceConfigurationFormState } from './source_configuration_form_state'; -import { InlineLogViewCallout } from './inline_log_view_callout'; - -export const LogsSettingsPage = () => { - const uiCapabilities = useKibana().services.application?.capabilities; - const shouldAllowEdit = uiCapabilities?.logs?.configureSource === true; - - useTrackPageview({ app: 'infra_logs', path: 'log_source_configuration' }); - useTrackPageview({ - app: 'infra_logs', - path: 'log_source_configuration', - delay: 15000, - }); - - useLogsBreadcrumbs([ - { - text: settingsTitle, - }, - ]); - - const { - logView, - hasFailedLoadingLogView, - isLoading, - isUninitialized, - update, - resolvedLogView, - isInlineLogView, - revertToDefaultLogView, - } = useLogViewContext(); - - const availableFields = useMemo( - () => resolvedLogView?.fields.map((field) => field.name) ?? [], - [resolvedLogView] - ); - - const { - sourceConfigurationFormElement, - formState, - logIndicesFormElement, - logColumnsFormElement, - nameFormElement, - } = useLogSourceConfigurationFormState(logView?.attributes); - - const persistUpdates = useCallback(async () => { - await update(formState); - sourceConfigurationFormElement.resetValue(); - }, [update, sourceConfigurationFormElement, formState]); - - const isWriteable = useMemo( - () => shouldAllowEdit && logView && logView.origin !== 'internal', - [shouldAllowEdit, logView] - ); - - if ((isLoading || isUninitialized) && !resolvedLogView) { - return ; - } - if (hasFailedLoadingLogView) { - return null; - } - - return ( - - - - - {isInlineLogView && ( - - - - - - - )} - - - - - - - - - - - - - {sourceConfigurationFormElement.validity.validity === 'invalid' ? ( - <> - - - - ) : null} - - {isWriteable && ( - - {isLoading ? ( - - - - {i18n.translate('xpack.infra.logsSettingsPage.loadingButtonLabel', { - defaultMessage: 'Loading', - })} - - - - ) : ( - - - { - sourceConfigurationFormElement.resetValue(); - }} - > - - - - - - - - - - )} - - )} - - - - ); -}; - -const unsavedFormPromptMessage = i18n.translate( - 'xpack.infra.logSourceConfiguration.unsavedFormPromptMessage', - { - defaultMessage: 'Are you sure you want to leave? Changes will be lost', - } -); diff --git a/x-pack/solutions/observability/plugins/infra/public/pages/logs/settings/validation_errors.ts b/x-pack/solutions/observability/plugins/infra/public/pages/logs/settings/validation_errors.ts deleted file mode 100644 index b2671601bde45..0000000000000 --- a/x-pack/solutions/observability/plugins/infra/public/pages/logs/settings/validation_errors.ts +++ /dev/null @@ -1,130 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { DataView } from '@kbn/data-views-plugin/public'; -import { KBN_FIELD_TYPES } from '@kbn/data-plugin/public'; - -export interface GenericValidationError { - type: 'generic'; - message: string; -} - -export interface ChildFormValidationError { - type: 'child'; -} - -export interface EmptyFieldValidationError { - type: 'empty_field'; - fieldName: string; -} - -export interface IncludesSpacesValidationError { - type: 'includes_spaces'; - fieldName: string; -} - -export interface EmptyColumnListValidationError { - type: 'empty_column_list'; -} - -export interface MissingTimestampFieldValidationError { - type: 'missing_timestamp_field'; - indexPatternTitle: string; -} - -export interface MissingMessageFieldValidationError { - type: 'missing_message_field'; - indexPatternTitle: string; -} - -export interface InvalidMessageFieldTypeValidationError { - type: 'invalid_message_field_type'; - indexPatternTitle: string; -} - -export interface RollupIndexPatternValidationError { - type: 'rollup_index_pattern'; - indexPatternTitle: string; -} - -export interface MissingIndexPatternValidationError { - type: 'missing_index_pattern'; - indexPatternId: string; -} - -export type FormValidationError = - | GenericValidationError - | ChildFormValidationError - | EmptyFieldValidationError - | IncludesSpacesValidationError - | EmptyColumnListValidationError - | MissingTimestampFieldValidationError - | MissingMessageFieldValidationError - | InvalidMessageFieldTypeValidationError - | RollupIndexPatternValidationError - | MissingIndexPatternValidationError; - -export const validateStringNotEmpty = (fieldName: string, value: string): FormValidationError[] => - value === '' ? [{ type: 'empty_field', fieldName }] : []; - -export const validateStringNoSpaces = (fieldName: string, value: string): FormValidationError[] => - value.includes(' ') ? [{ type: 'includes_spaces', fieldName }] : []; - -export const validateColumnListNotEmpty = (columns: unknown[]): FormValidationError[] => - columns.length <= 0 ? [{ type: 'empty_column_list' }] : []; - -export const validateIndexPattern = (indexPattern: DataView): FormValidationError[] => { - return [ - ...validateIndexPatternIsTimeBased(indexPattern), - ...validateIndexPatternHasStringMessageField(indexPattern), - ...validateIndexPatternIsntRollup(indexPattern), - ]; -}; - -export const validateIndexPatternIsTimeBased = (indexPattern: DataView): FormValidationError[] => - indexPattern.isTimeBased() - ? [] - : [ - { - type: 'missing_timestamp_field' as const, - indexPatternTitle: indexPattern.getIndexPattern(), - }, - ]; - -export const validateIndexPatternHasStringMessageField = ( - indexPattern: DataView -): FormValidationError[] => { - const messageField = indexPattern.getFieldByName('message'); - - if (messageField == null) { - return [ - { - type: 'missing_message_field' as const, - indexPatternTitle: indexPattern.getIndexPattern(), - }, - ]; - } else if (messageField.type !== KBN_FIELD_TYPES.STRING) { - return [ - { - type: 'invalid_message_field_type' as const, - indexPatternTitle: indexPattern.getIndexPattern(), - }, - ]; - } else { - return []; - } -}; - -export const validateIndexPatternIsntRollup = (indexPattern: DataView): FormValidationError[] => - indexPattern.type != null - ? [ - { - type: 'rollup_index_pattern' as const, - indexPatternTitle: indexPattern.getIndexPattern(), - }, - ] - : []; diff --git a/x-pack/solutions/observability/plugins/infra/public/pages/logs/shared/page_log_view_error.tsx b/x-pack/solutions/observability/plugins/infra/public/pages/logs/shared/page_log_view_error.tsx index 8914eb36f8c20..d4086e810b700 100644 --- a/x-pack/solutions/observability/plugins/infra/public/pages/logs/shared/page_log_view_error.tsx +++ b/x-pack/solutions/observability/plugins/infra/public/pages/logs/shared/page_log_view_error.tsx @@ -8,11 +8,9 @@ import { EuiButton, EuiButtonEmpty, EuiCallOut, EuiEmptyPrompt, EuiSpacer } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import type { FC, PropsWithChildren } from 'react'; -import React, { useCallback } from 'react'; +import React from 'react'; import { SavedObjectNotFound } from '@kbn/kibana-utils-plugin/common'; import { useLinkProps } from '@kbn/observability-shared-plugin/public'; -import { useSelector } from '@xstate/react'; -import { useLogViewContext } from '@kbn/logs-shared-plugin/public'; import { FetchLogViewStatusError, FetchLogViewError, @@ -89,26 +87,6 @@ export const LogViewErrorPage: React.FC<{ export const LogSourceErrorPage = LogViewErrorPage; -export const ConnectedLogViewErrorPage: React.FC = () => { - const { logViewStateService } = useLogViewContext(); - - const errors = useSelector(logViewStateService, (state) => { - return state.matches('loadingFailed') || - state.matches('resolutionFailed') || - state.matches('checkingStatusFailed') - ? [state.context.error] - : []; - }); - - const retry = useCallback(() => { - logViewStateService.send({ - type: 'RETRY', - }); - }, [logViewStateService]); - - return ; -}; - const LogSourceErrorMessage: React.FC<{ error: Error }> = ({ error }) => { if (error instanceof ResolveLogViewError) { return ( diff --git a/x-pack/solutions/observability/plugins/infra/public/pages/logs/stream/components/stream_live_button.tsx b/x-pack/solutions/observability/plugins/infra/public/pages/logs/stream/components/stream_live_button.tsx deleted file mode 100644 index 756849d185c4d..0000000000000 --- a/x-pack/solutions/observability/plugins/infra/public/pages/logs/stream/components/stream_live_button.tsx +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { EuiButtonEmpty } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n-react'; - -export const StreamLiveButton: React.FC<{ - isStreaming: boolean; - onStartStreaming: () => void; - onStopStreaming: () => void; -}> = ({ isStreaming, onStartStreaming, onStopStreaming }) => - isStreaming ? ( - - - - ) : ( - - - - ); diff --git a/x-pack/solutions/observability/plugins/infra/public/pages/logs/stream/components/stream_page_template.tsx b/x-pack/solutions/observability/plugins/infra/public/pages/logs/stream/components/stream_page_template.tsx deleted file mode 100644 index fbe7a34837031..0000000000000 --- a/x-pack/solutions/observability/plugins/infra/public/pages/logs/stream/components/stream_page_template.tsx +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { APP_WRAPPER_CLASS } from '@kbn/core/public'; -import { i18n } from '@kbn/i18n'; -import React from 'react'; -import { useLogViewContext } from '@kbn/logs-shared-plugin/public'; -import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiBadge, EuiToolTip } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n-react'; -import { fullHeightContentStyles } from '../../../../page_template.styles'; -import type { LogsPageTemplateProps } from '../../shared/page_template'; -import { LogsPageTemplate } from '../../shared/page_template'; - -export const LogStreamPageTemplate: React.FC = (props) => { - const { logView, isInlineLogView, revertToDefaultLogView } = useLogViewContext(); - return ( -
- - {logView.attributes.name} - - - } - > - - - - - - - ) : ( - streamTitle - ), - breadcrumbs: isInlineLogView - ? [ - { - text: ( - - - - - - - - - ), - color: 'primary', - 'aria-current': false, - 'data-test-subj': 'infraAssetDetailsReturnButton', - href: '#', - onClick: revertToDefaultLogView, - }, - ] - : undefined, - }} - pageSectionProps={{ - contentProps: { - css: fullHeightContentStyles, - }, - }} - {...props} - /> -
- ); -}; - -const streamTitle = i18n.translate('xpack.infra.logs.streamPageTitle', { - defaultMessage: 'Stream', -}); diff --git a/x-pack/solutions/observability/plugins/infra/public/pages/logs/stream/index.ts b/x-pack/solutions/observability/plugins/infra/public/pages/logs/stream/index.ts deleted file mode 100644 index 56fb7e79e7717..0000000000000 --- a/x-pack/solutions/observability/plugins/infra/public/pages/logs/stream/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export { StreamPage } from './page'; diff --git a/x-pack/solutions/observability/plugins/infra/public/pages/logs/stream/page.tsx b/x-pack/solutions/observability/plugins/infra/public/pages/logs/stream/page.tsx deleted file mode 100644 index 706fb7811fa78..0000000000000 --- a/x-pack/solutions/observability/plugins/infra/public/pages/logs/stream/page.tsx +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EuiErrorBoundary } from '@elastic/eui'; -import { useKibanaQuerySettings, useTrackPageview } from '@kbn/observability-shared-plugin/public'; -import React from 'react'; -import { useLogViewContext } from '@kbn/logs-shared-plugin/public'; -import { useKibanaContextForPlugin } from '../../../hooks/use_kibana'; -import { useLogsBreadcrumbs } from '../../../hooks/use_logs_breadcrumbs'; -import { LogStreamPageStateProvider } from '../../../observability_logs/log_stream_page/state'; -import { streamTitle } from '../../../translations'; -import { useKbnUrlStateStorageFromRouterContext } from '../../../containers/kbn_url_state_context'; -import { ConnectedStreamPageContent } from './page_content'; - -export const StreamPage = () => { - useTrackPageview({ app: 'infra_logs', path: 'stream' }); - useTrackPageview({ app: 'infra_logs', path: 'stream', delay: 15000 }); - - useLogsBreadcrumbs([ - { - text: streamTitle, - }, - ]); - - const { logViewStateNotifications } = useLogViewContext(); - const { - services: { - data: { - query: { - queryString: queryStringService, - filterManager: filterManagerService, - timefilter: { timefilter: timeFilterService }, - }, - }, - notifications: { toasts: toastsService }, - }, - } = useKibanaContextForPlugin(); - - const kibanaQuerySettings = useKibanaQuerySettings(); - const urlStateStorage = useKbnUrlStateStorageFromRouterContext(); - - return ( - - - - - - ); -}; - -const ConnectedStreamPageContentMemo = React.memo(ConnectedStreamPageContent); diff --git a/x-pack/solutions/observability/plugins/infra/public/pages/logs/stream/page_content.tsx b/x-pack/solutions/observability/plugins/infra/public/pages/logs/stream/page_content.tsx deleted file mode 100644 index 8ec0031f535c1..0000000000000 --- a/x-pack/solutions/observability/plugins/infra/public/pages/logs/stream/page_content.tsx +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { TimeRange } from '@kbn/es-query'; -import { useActor } from '@xstate/react'; -import React, { useMemo } from 'react'; -import type { VisiblePositions } from '../../../observability_logs/log_stream_position_state'; -import type { TimeKey } from '../../../../common/time'; -import { SourceLoadingPage } from '../../../components/source_loading_page'; -import type { - LogStreamPageCallbacks, - LogStreamPageState, -} from '../../../observability_logs/log_stream_page/state'; -import { useLogStreamPageStateContext } from '../../../observability_logs/log_stream_page/state'; -import { InvalidStateCallout } from '../../../observability_logs/xstate_helpers'; -import { ConnectedLogViewErrorPage } from '../shared/page_log_view_error'; -import { LogStreamPageTemplate } from './components/stream_page_template'; -import { StreamPageLogsContentForState } from './page_logs_content'; -import { StreamPageMissingIndicesContent } from './page_missing_indices_content'; -import { LogStreamPageContentProviders } from './page_providers'; - -export const ConnectedStreamPageContent: React.FC = () => { - const logStreamPageStateService = useLogStreamPageStateContext(); - const [logStreamPageState, logStreamPageSend] = useActor(logStreamPageStateService); - - const pageStateCallbacks = useMemo(() => { - return { - updateTimeRange: (timeRange: Partial) => { - logStreamPageSend({ - type: 'UPDATE_TIME_RANGE', - timeRange, - }); - }, - jumpToTargetPosition: (targetPosition: TimeKey | null) => { - logStreamPageSend({ type: 'JUMP_TO_TARGET_POSITION', targetPosition }); - }, - jumpToTargetPositionTime: (time: string) => { - logStreamPageSend({ type: 'JUMP_TO_TARGET_POSITION', targetPosition: { time } }); - }, - reportVisiblePositions: (visiblePositions: VisiblePositions) => { - logStreamPageSend({ - type: 'REPORT_VISIBLE_POSITIONS', - visiblePositions, - }); - }, - startLiveStreaming: () => { - logStreamPageSend({ type: 'UPDATE_REFRESH_INTERVAL', refreshInterval: { pause: false } }); - }, - stopLiveStreaming: () => { - logStreamPageSend({ type: 'UPDATE_REFRESH_INTERVAL', refreshInterval: { pause: true } }); - }, - }; - }, [logStreamPageSend]); - - return ( - - ); -}; - -export const StreamPageContentForState: React.FC<{ - logStreamPageState: LogStreamPageState; - logStreamPageCallbacks: LogStreamPageCallbacks; -}> = ({ logStreamPageState, logStreamPageCallbacks }) => { - if ( - logStreamPageState.matches('uninitialized') || - logStreamPageState.matches({ hasLogViewIndices: 'uninitialized' }) || - logStreamPageState.matches('loadingLogView') - ) { - return ; - } else if (logStreamPageState.matches('loadingLogViewFailed')) { - return ; - } else if (logStreamPageState.matches('missingLogViewIndices')) { - return ; - } else if (logStreamPageState.matches({ hasLogViewIndices: 'initialized' })) { - return ( - - - - - - ); - } else { - return ; - } -}; diff --git a/x-pack/solutions/observability/plugins/infra/public/pages/logs/stream/page_logs_content.tsx b/x-pack/solutions/observability/plugins/infra/public/pages/logs/stream/page_logs_content.tsx deleted file mode 100644 index ad12c1f719a21..0000000000000 --- a/x-pack/solutions/observability/plugins/infra/public/pages/logs/stream/page_logs_content.tsx +++ /dev/null @@ -1,363 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EuiSpacer } from '@elastic/eui'; -import type { Query } from '@kbn/es-query'; -import styled from '@emotion/styled'; -import type { LogEntry } from '@kbn/logs-shared-plugin/common'; -import { convertISODateToNanoPrecision } from '@kbn/logs-shared-plugin/common'; -import type { - LogEntryStreamItem, - UpdatedDateRange, - VisibleInterval, - WithSummaryProps, -} from '@kbn/logs-shared-plugin/public'; -import { - LogEntryFlyout, - ScrollableLogTextStreamView, - useLogHighlightsStateContext, - useLogPositionStateContext, - useLogStreamContext, - useLogViewContext, - WithSummary, -} from '@kbn/logs-shared-plugin/public'; -import { useSelector } from '@xstate/react'; -import stringify from 'json-stable-stringify'; -import React, { useCallback, useEffect, useMemo } from 'react'; -import usePrevious from 'react-use/lib/usePrevious'; -import type { MatchedStateFromActor } from '@kbn/xstate-utils'; -import { LogsDeprecationCallout } from '../../../components/logs_deprecation_callout'; -import type { TimeKey } from '../../../../common/time'; -import { AutoSizer } from '../../../components/auto_sizer'; -import { LogMinimap } from '../../../components/logging/log_minimap'; -import { PageContent } from '../../../components/page'; -import { - useLogEntryFlyoutContext, - WithFlyoutOptionsUrlState, -} from '../../../containers/logs/log_flyout'; -import { useLogViewConfigurationContext } from '../../../containers/logs/log_view_configuration'; -import { useViewLogInProviderContext } from '../../../containers/logs/view_log_in_context'; -import { WithLogTextviewUrlState } from '../../../containers/logs/with_log_textview'; -import { useKibanaContextForPlugin } from '../../../hooks/use_kibana'; -import type { - LogStreamPageActorRef, - LogStreamPageCallbacks, -} from '../../../observability_logs/log_stream_page/state'; -import { useLogStreamPageStateContext } from '../../../observability_logs/log_stream_page/state'; -import { type ParsedQuery } from '../../../observability_logs/log_stream_query_state'; -import { datemathToEpochMillis, isValidDatemath } from '../../../utils/datemath'; -import { LogsToolbar } from './page_toolbar'; -import { PageViewLogInContext } from './page_view_log_in_context'; - -const PAGE_THRESHOLD = 2; - -export const StreamPageLogsContent = React.memo<{ - filterQuery: ParsedQuery; - logStreamPageCallbacks: LogStreamPageCallbacks; -}>(({ filterQuery, logStreamPageCallbacks }) => { - const { - data: { - query: { queryString }, - }, - } = useKibanaContextForPlugin().services; - const { resolvedLogView, logView, logViewReference } = useLogViewContext(); - const { textScale, textWrap } = useLogViewConfigurationContext(); - const { - surroundingLogsId, - setSurroundingLogsId, - closeFlyout: closeLogEntryFlyout, - openFlyout: openLogEntryFlyout, - isFlyoutOpen, - logEntryId: flyoutLogEntryId, - } = useLogEntryFlyoutContext(); - - const { - startTimestamp, - endTimestamp, - isStreaming, - targetPosition, - visibleMidpointTime, - visibleTimeInterval, - reportVisiblePositions, - jumpToTargetPosition, - startLiveStreaming, - stopLiveStreaming, - startDateExpression, - endDateExpression, - updateDateRange, - lastCompleteDateRangeExpressionUpdate, - } = useLogPositionStateContext(); - - const { - isReloading, - entries, - topCursor, - bottomCursor, - hasMoreAfter: hasMoreAfterEnd, - hasMoreBefore: hasMoreBeforeStart, - isLoadingMore, - lastLoadedTime, - fetchEntries, - fetchPreviousEntries, - fetchNextEntries, - fetchNewestEntries, - } = useLogStreamContext(); - - const prevStartTimestamp = usePrevious(startTimestamp); - const prevEndTimestamp = usePrevious(endTimestamp); - const prevFilterQuery = usePrevious(filterQuery); - const prevLastCompleteDateRangeExpressionUpdate = usePrevious( - lastCompleteDateRangeExpressionUpdate - ); - - // Refetch entries if... - useEffect(() => { - const isFirstLoad = !prevStartTimestamp || !prevEndTimestamp; - - const completeDateRangeExpressionHasChanged = - lastCompleteDateRangeExpressionUpdate !== prevLastCompleteDateRangeExpressionUpdate; - - const isCenterPointOutsideLoadedRange = - targetPosition != null && - ((topCursor != null && - convertISODateToNanoPrecision(targetPosition.time) < - convertISODateToNanoPrecision(topCursor.time)) || - (bottomCursor != null && - convertISODateToNanoPrecision(targetPosition.time) > - convertISODateToNanoPrecision(bottomCursor.time))); - - const hasQueryChanged = filterQuery !== prevFilterQuery; - - if ( - isFirstLoad || - completeDateRangeExpressionHasChanged || - isCenterPointOutsideLoadedRange || - hasQueryChanged - ) { - if (isStreaming) { - fetchNewestEntries(); - } else { - fetchEntries(); - } - } - }, [ - fetchEntries, - fetchNewestEntries, - isStreaming, - prevStartTimestamp, - prevEndTimestamp, - startTimestamp, - endTimestamp, - targetPosition, - topCursor, - bottomCursor, - filterQuery, - prevFilterQuery, - lastCompleteDateRangeExpressionUpdate, - prevLastCompleteDateRangeExpressionUpdate, - ]); - - const { logSummaryHighlights, currentHighlightKey, logEntryHighlightsById } = - useLogHighlightsStateContext(); - - const items = useMemo( - () => - isReloading - ? [] - : entries.map((logEntry) => - createLogEntryStreamItem(logEntry, logEntryHighlightsById[logEntry.id] || []) - ), - - [entries, isReloading, logEntryHighlightsById] - ); - - const [, { setContextEntry }] = useViewLogInProviderContext(); - - const handleDateRangeExtension = useCallback( - (newDateRange: UpdatedDateRange) => { - updateDateRange(newDateRange); - - if ( - newDateRange.startDateExpression != null && - isValidDatemath(newDateRange.startDateExpression) - ) { - fetchPreviousEntries({ - force: true, - extendTo: datemathToEpochMillis(newDateRange.startDateExpression)!, - }); - } - if ( - newDateRange.endDateExpression != null && - isValidDatemath(newDateRange.endDateExpression) - ) { - fetchNextEntries({ - force: true, - extendTo: datemathToEpochMillis(newDateRange.endDateExpression)!, - }); - } - }, - [updateDateRange, fetchPreviousEntries, fetchNextEntries] - ); - - const handlePagination = useCallback( - (params: VisibleInterval) => { - reportVisiblePositions(params); - if (!params.fromScroll) { - return; - } - - if (isLoadingMore) { - return; - } - - if (params.pagesBeforeStart < PAGE_THRESHOLD) { - fetchPreviousEntries(); - } else if (params.pagesAfterEnd < PAGE_THRESHOLD) { - fetchNextEntries(); - } - }, - [reportVisiblePositions, isLoadingMore, fetchPreviousEntries, fetchNextEntries] - ); - - const setFilter = useCallback( - (filter: Query, flyoutItemId: string, timeKey: TimeKey | undefined | null) => { - queryString.setQuery(filter); - if (timeKey) { - jumpToTargetPosition(timeKey); - } - setSurroundingLogsId(flyoutItemId); - stopLiveStreaming(); - }, - [jumpToTargetPosition, queryString, setSurroundingLogsId, stopLiveStreaming] - ); - - return ( - <> - - - - - - - {isFlyoutOpen ? ( - - ) : null} - - - - - {({ measureRef, bounds: { height = 0 }, content: { width = 0 } }) => { - return ( - - - {({ buckets, start, end }) => ( - 0 ? logSummaryHighlights[0].buckets : [] - } - target={visibleMidpointTime} - /> - )} - - - ); - }} - - - - ); -}); - -const WithSummaryAndQuery = (props: Omit) => { - const serializedParsedQuery = useSelector(useLogStreamPageStateContext(), (logStreamPageState) => - logStreamPageState.matches({ hasLogViewIndices: 'initialized' }) - ? stringify(logStreamPageState.context.parsedQuery) - : null - ); - - return ; -}; - -type InitializedLogStreamPageState = MatchedStateFromActor< - LogStreamPageActorRef, - { hasLogViewIndices: 'initialized' } ->; - -export const StreamPageLogsContentForState = React.memo<{ - logStreamPageState: InitializedLogStreamPageState; - logStreamPageCallbacks: LogStreamPageCallbacks; -}>(({ logStreamPageState, logStreamPageCallbacks }) => { - const { - context: { parsedQuery }, - } = logStreamPageState; - - return ( - - ); -}); - -const LogPageMinimapColumn = styled.div` - flex: 1 0 0%; - overflow: hidden; - min-width: 100px; - max-width: 100px; - display: flex; - flex-direction: column; -`; - -const createLogEntryStreamItem = ( - logEntry: LogEntry, - highlights: LogEntry[] -): LogEntryStreamItem => ({ - kind: 'logEntry' as 'logEntry', - logEntry, - highlights, -}); diff --git a/x-pack/solutions/observability/plugins/infra/public/pages/logs/stream/page_missing_indices_content.tsx b/x-pack/solutions/observability/plugins/infra/public/pages/logs/stream/page_missing_indices_content.tsx deleted file mode 100644 index 6d5ecf8ffd26c..0000000000000 --- a/x-pack/solutions/observability/plugins/infra/public/pages/logs/stream/page_missing_indices_content.tsx +++ /dev/null @@ -1,13 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { LogStreamPageTemplate } from './components/stream_page_template'; - -export const StreamPageMissingIndicesContent = React.memo(() => ( - -)); diff --git a/x-pack/solutions/observability/plugins/infra/public/pages/logs/stream/page_providers.tsx b/x-pack/solutions/observability/plugins/infra/public/pages/logs/stream/page_providers.tsx deleted file mode 100644 index cbeb08e3c4f38..0000000000000 --- a/x-pack/solutions/observability/plugins/infra/public/pages/logs/stream/page_providers.tsx +++ /dev/null @@ -1,129 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import stringify from 'json-stable-stringify'; -import type { FC, PropsWithChildren } from 'react'; -import React, { useMemo } from 'react'; -import { - LogHighlightsStateProvider, - LogPositionStateProvider, - LogStreamProvider, - useLogPositionStateContext, - useLogStreamContext, - useLogViewContext, -} from '@kbn/logs-shared-plugin/public'; -import type { MatchedStateFromActor } from '@kbn/xstate-utils'; -import type { - LogStreamPageActorRef, - LogStreamPageCallbacks, -} from '../../../observability_logs/log_stream_page/state'; -import { LogEntryFlyoutProvider } from '../../../containers/logs/log_flyout'; -import { LogViewConfigurationProvider } from '../../../containers/logs/log_view_configuration'; -import { ViewLogInContextProvider } from '../../../containers/logs/view_log_in_context'; - -const ViewLogInContext: FC> = ({ children }) => { - const { startTimestamp, endTimestamp } = useLogPositionStateContext(); - const { logViewReference } = useLogViewContext(); - - if (!startTimestamp || !endTimestamp) { - return null; - } - - return ( - - {children} - - ); -}; - -const LogEntriesStateProvider: FC< - PropsWithChildren<{ - logStreamPageState: InitializedLogStreamPageState; - }> -> = ({ children, logStreamPageState }) => { - const { logViewReference } = useLogViewContext(); - const { startTimestamp, endTimestamp, targetPosition } = useLogPositionStateContext(); - const { - context: { parsedQuery }, - } = logStreamPageState; - - // Don't render anything if the date range is incorrect. - if (!startTimestamp || !endTimestamp) { - return null; - } - - return ( - - {children} - - ); -}; - -const LogHighlightsState: FC< - PropsWithChildren<{ - logStreamPageState: InitializedLogStreamPageState; - }> -> = ({ children, logStreamPageState }) => { - const { logViewReference, logView } = useLogViewContext(); - const { topCursor, bottomCursor, entries } = useLogStreamContext(); - const serializedParsedQuery = useMemo( - () => stringify(logStreamPageState.context.parsedQuery), - [logStreamPageState.context.parsedQuery] - ); - - const highlightsProps = { - logViewReference, - sourceVersion: logView?.version, - entriesStart: topCursor, - entriesEnd: bottomCursor, - centerCursor: entries.length > 0 ? entries[Math.floor(entries.length / 2)].cursor : null, - size: entries.length, - filterQuery: serializedParsedQuery, - }; - return {children}; -}; - -export const LogStreamPageContentProviders: FC< - PropsWithChildren<{ - logStreamPageState: InitializedLogStreamPageState; - logStreamPageCallbacks: LogStreamPageCallbacks; - }> -> = ({ children, logStreamPageState, logStreamPageCallbacks }) => { - return ( - - - - - - - {children} - - - - - - - ); -}; - -type InitializedLogStreamPageState = MatchedStateFromActor< - LogStreamPageActorRef, - { hasLogViewIndices: 'initialized' } ->; diff --git a/x-pack/solutions/observability/plugins/infra/public/pages/logs/stream/page_toolbar.tsx b/x-pack/solutions/observability/plugins/infra/public/pages/logs/stream/page_toolbar.tsx deleted file mode 100644 index 9ea8e60eef0b9..0000000000000 --- a/x-pack/solutions/observability/plugins/infra/public/pages/logs/stream/page_toolbar.tsx +++ /dev/null @@ -1,111 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import React, { useMemo } from 'react'; -import { - useLogHighlightsStateContext, - useLogPositionStateContext, - useLogViewContext, -} from '@kbn/logs-shared-plugin/public'; -import { LogCustomizationMenu } from '../../../components/logging/log_customization_menu'; -import { LogHighlightsMenu } from '../../../components/logging/log_highlights_menu'; -import { LogTextScaleControls } from '../../../components/logging/log_text_scale_controls'; -import { LogTextWrapControls } from '../../../components/logging/log_text_wrap_controls'; -import { useLogViewConfigurationContext } from '../../../containers/logs/log_view_configuration'; -import { useKibanaContextForPlugin } from '../../../hooks/use_kibana'; -import { StreamLiveButton } from './components/stream_live_button'; - -export const LogsToolbar = () => { - const { derivedDataView } = useLogViewContext(); - const { availableTextScales, setTextScale, setTextWrap, textScale, textWrap } = - useLogViewConfigurationContext(); - const { - unifiedSearch: { - ui: { SearchBar }, - }, - } = useKibanaContextForPlugin().services; - - const { - setHighlightTerms, - loadLogEntryHighlightsRequest, - highlightTerms, - hasPreviousHighlight, - hasNextHighlight, - goToPreviousHighlight, - goToNextHighlight, - } = useLogHighlightsStateContext(); - const { isStreaming, startLiveStreaming, stopLiveStreaming } = useLogPositionStateContext(); - - const dataViews = useMemo( - () => (derivedDataView != null ? [derivedDataView] : undefined), - [derivedDataView] - ); - - return ( - <> - - -
- - - - - - - - - highlightTerm.length > 0).length > 0 - } - goToPreviousHighlight={goToPreviousHighlight} - goToNextHighlight={goToNextHighlight} - hasPreviousHighlight={hasPreviousHighlight} - hasNextHighlight={hasNextHighlight} - /> - - - - - - -
- - ); -}; diff --git a/x-pack/solutions/observability/plugins/infra/public/translations.ts b/x-pack/solutions/observability/plugins/infra/public/translations.ts index 3377ae1dd1fa1..cb7bd53fe034a 100644 --- a/x-pack/solutions/observability/plugins/infra/public/translations.ts +++ b/x-pack/solutions/observability/plugins/infra/public/translations.ts @@ -15,10 +15,6 @@ export const logsTitle = i18n.translate('xpack.infra.header.logsTitle', { defaultMessage: 'Logs', }); -export const streamTitle = i18n.translate('xpack.infra.logs.index.streamTabTitle', { - defaultMessage: 'Stream', -}); - export const logsAnomaliesTitle = i18n.translate('xpack.infra.logs.index.anomaliesTabTitle', { defaultMessage: 'Logs Anomalies', }); diff --git a/x-pack/solutions/observability/plugins/infra/public/utils/theme_utils/with_attrs.tsx b/x-pack/solutions/observability/plugins/infra/public/utils/theme_utils/with_attrs.tsx deleted file mode 100644 index b56f4e52dc91a..0000000000000 --- a/x-pack/solutions/observability/plugins/infra/public/utils/theme_utils/with_attrs.tsx +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import { type Theme, useTheme } from '@emotion/react'; -import React from 'react'; - -import type { ComponentType } from 'react'; - -export const withAttrs = - (Component: ComponentType, fn: (args: { theme: Theme; props: any }) => any) => - (props: any) => { - const theme = useTheme(); - const attrs = fn({ theme, props }); - - return ; - }; diff --git a/x-pack/solutions/observability/plugins/infra/tsconfig.json b/x-pack/solutions/observability/plugins/infra/tsconfig.json index b623aeec7842b..41dbf66ffe78f 100644 --- a/x-pack/solutions/observability/plugins/infra/tsconfig.json +++ b/x-pack/solutions/observability/plugins/infra/tsconfig.json @@ -108,7 +108,6 @@ "@kbn/observability-alerting-rule-utils", "@kbn/core-application-browser", "@kbn/shared-ux-page-no-data-types", - "@kbn/xstate-utils", "@kbn/entityManager-plugin", "@kbn/zod", "@kbn/observability-utils-server", diff --git a/x-pack/test/api_integration/apis/logs_ui/index.ts b/x-pack/test/api_integration/apis/logs_ui/index.ts index 694698d41f3b6..cc386ce0bff0f 100644 --- a/x-pack/test/api_integration/apis/logs_ui/index.ts +++ b/x-pack/test/api_integration/apis/logs_ui/index.ts @@ -11,8 +11,6 @@ export default function ({ loadTestFile }: FtrProviderContext) { describe('Logs UI routes', () => { loadTestFile(require.resolve('./log_views')); loadTestFile(require.resolve('./log_threshold_alert')); - loadTestFile(require.resolve('./log_entry_highlights')); - loadTestFile(require.resolve('./log_summary')); loadTestFile(require.resolve('./log_analysis_validation_log_entry_datasets')); }); } diff --git a/x-pack/test/api_integration/apis/logs_ui/log_entry_highlights.ts b/x-pack/test/api_integration/apis/logs_ui/log_entry_highlights.ts deleted file mode 100644 index aa0796c584c77..0000000000000 --- a/x-pack/test/api_integration/apis/logs_ui/log_entry_highlights.ts +++ /dev/null @@ -1,213 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import expect from '@kbn/expect'; - -import semver from 'semver'; -import { pipe } from 'fp-ts/lib/pipeable'; -import { identity } from 'fp-ts/lib/function'; -import { fold } from 'fp-ts/lib/Either'; - -import { createPlainError, throwErrors } from '@kbn/io-ts-utils'; - -import { - LOG_ENTRIES_HIGHLIGHTS_PATH, - logEntriesHighlightsRequestRT, - logEntriesHighlightsResponseRT, -} from '@kbn/logs-shared-plugin/common'; - -import moment from 'moment'; -import { FtrProviderContext } from '../../ftr_provider_context'; - -const KEY_BEFORE_START = { - time: new Date('2000-01-01T00:00:00.000Z').valueOf(), - tiebreaker: -1, -}; -const KEY_AFTER_END = { - time: new Date('2000-01-01T00:00:09.001Z').valueOf(), - tiebreaker: 0, -}; - -const COMMON_HEADERS = { - 'kbn-xsrf': 'some-xsrf-token', - 'Elastic-Api-Version': '1', -}; - -export default function ({ getService }: FtrProviderContext) { - const es = getService('es'); - const esArchiver = getService('esArchiver'); - const supertest = getService('supertest'); - const kibanaServer = getService('kibanaServer'); - - describe('log highlight apis', () => { - before(() => esArchiver.load('x-pack/test/functional/es_archives/infra/simple_logs')); - after(() => esArchiver.unload('x-pack/test/functional/es_archives/infra/simple_logs')); - - describe('/log_entries/highlights', () => { - describe('with the default source', () => { - before(() => kibanaServer.savedObjects.cleanStandardList()); - after(() => kibanaServer.savedObjects.cleanStandardList()); - - it('Handles empty responses', async () => { - const { body } = await supertest - .post(LOG_ENTRIES_HIGHLIGHTS_PATH) - .set(COMMON_HEADERS) - .send( - logEntriesHighlightsRequestRT.encode({ - logView: { type: 'log-view-reference', logViewId: 'default' }, - startTimestamp: KEY_BEFORE_START.time, - endTimestamp: KEY_AFTER_END.time, - highlightTerms: ['some string that does not exist'], - }) - ) - .expect(200); - - const logEntriesHighlightsResponse = pipe( - logEntriesHighlightsResponseRT.decode(body), - fold(throwErrors(createPlainError), identity) - ); - - expect(logEntriesHighlightsResponse.data).to.have.length(1); - - const data = logEntriesHighlightsResponse.data[0]; - - expect(data.entries).to.have.length(0); - expect(data.topCursor).to.be(null); - expect(data.bottomCursor).to.be(null); - }); - - it('highlights built-in message column', async () => { - const esInfo = await es.info(); - const highlightTerms = 'message of document 0'; - const { body } = await supertest - .post(LOG_ENTRIES_HIGHLIGHTS_PATH) - .set(COMMON_HEADERS) - .send( - logEntriesHighlightsRequestRT.encode({ - logView: { type: 'log-view-reference', logViewId: 'default' }, - startTimestamp: KEY_BEFORE_START.time, - endTimestamp: KEY_AFTER_END.time, - highlightTerms: [highlightTerms], - }) - ) - .expect(200); - - const logEntriesHighlightsResponse = pipe( - logEntriesHighlightsResponseRT.decode(body), - fold(throwErrors(createPlainError), identity) - ); - - expect(logEntriesHighlightsResponse.data).to.have.length(1); - - const data = logEntriesHighlightsResponse.data[0]; - const entries = data.entries; - const firstEntry = entries[0]; - const lastEntry = entries[entries.length - 1]; - - // Finds expected entries - expect(entries).to.have.length(10); - - // Cursors are set correctly - expect(firstEntry.cursor).to.eql(data.topCursor); - expect(lastEntry.cursor).to.eql(data.bottomCursor); - - // Entries fall within range - // @kbn/expect doesn't have a `lessOrEqualThan` or `moreOrEqualThan` comparators - expect(firstEntry.cursor.time >= moment(KEY_BEFORE_START.time).toISOString()).to.be(true); - expect(lastEntry.cursor.time <= moment(KEY_AFTER_END.time).toISOString()).to.be(true); - - // All entries contain the highlights - entries.forEach((entry) => { - entry.columns.forEach((column) => { - if ('message' in column && 'highlights' in column.message[0]) { - const expectation = semver.gte(esInfo.version.number, '8.10.0') - ? [highlightTerms] - : highlightTerms.split(' '); - expect(column.message[0].highlights).to.eql(expectation); - } - }); - }); - }); - - it('highlights field columns', async () => { - const { body } = await supertest - .post(LOG_ENTRIES_HIGHLIGHTS_PATH) - .set(COMMON_HEADERS) - .send( - logEntriesHighlightsRequestRT.encode({ - logView: { type: 'log-view-reference', logViewId: 'default' }, - startTimestamp: KEY_BEFORE_START.time, - endTimestamp: KEY_AFTER_END.time, - highlightTerms: ['generate_test_data/simple_logs'], - }) - ) - .expect(200); - - const logEntriesHighlightsResponse = pipe( - logEntriesHighlightsResponseRT.decode(body), - fold(throwErrors(createPlainError), identity) - ); - - expect(logEntriesHighlightsResponse.data).to.have.length(1); - - const entries = logEntriesHighlightsResponse.data[0].entries; - - // Finds expected entries - expect(entries).to.have.length(50); - - // All entries contain the highlights - entries.forEach((entry) => { - entry.columns.forEach((column) => { - if ('field' in column && 'highlights' in column && column.highlights.length > 0) { - expect(column.highlights).to.eql(['generate_test_data/simple_logs']); - } - }); - }); - }); - - it('applies the query as well as the highlight', async () => { - const { body } = await supertest - .post(LOG_ENTRIES_HIGHLIGHTS_PATH) - .set(COMMON_HEADERS) - .send( - logEntriesHighlightsRequestRT.encode({ - logView: { type: 'log-view-reference', logViewId: 'default' }, - startTimestamp: KEY_BEFORE_START.time, - endTimestamp: KEY_AFTER_END.time, - query: JSON.stringify({ - multi_match: { query: 'host-a', type: 'phrase', lenient: true }, - }), - highlightTerms: ['message'], - }) - ) - .expect(200); - - const logEntriesHighlightsResponse = pipe( - logEntriesHighlightsResponseRT.decode(body), - fold(throwErrors(createPlainError), identity) - ); - - expect(logEntriesHighlightsResponse.data).to.have.length(1); - - const entries = logEntriesHighlightsResponse.data[0].entries; - - // Finds expected entries - expect(entries).to.have.length(25); - - // All entries contain the highlights - entries.forEach((entry) => { - entry.columns.forEach((column) => { - if ('message' in column && 'highlights' in column.message[0]) { - expect(column.message[0].highlights).to.eql(['message', 'message']); - } - }); - }); - }); - }); - }); - }); -} diff --git a/x-pack/test/api_integration/apis/logs_ui/log_summary.ts b/x-pack/test/api_integration/apis/logs_ui/log_summary.ts deleted file mode 100644 index 8a02dad1f1c5b..0000000000000 --- a/x-pack/test/api_integration/apis/logs_ui/log_summary.ts +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import expect from '@kbn/expect'; - -import { pairs } from 'd3-array'; - -import { pipe } from 'fp-ts/lib/pipeable'; -import { identity } from 'fp-ts/lib/function'; -import { fold } from 'fp-ts/lib/Either'; - -import { createPlainError, throwErrors } from '@kbn/io-ts-utils'; - -import { - LOG_ENTRIES_SUMMARY_PATH, - logEntriesSummaryRequestRT, - logEntriesSummaryResponseRT, -} from '@kbn/logs-shared-plugin/common'; - -import { FtrProviderContext } from '../../ftr_provider_context'; - -const EARLIEST_TIME_WITH_DATA = new Date('2018-10-17T19:42:22.000Z').valueOf(); -const LATEST_TIME_WITH_DATA = new Date('2018-10-17T19:57:21.611Z').valueOf(); - -const COMMON_HEADERS = { - 'kbn-xsrf': 'some-xsrf-token', - 'Elastic-Api-Version': '1', -}; - -export default function ({ getService }: FtrProviderContext) { - const esArchiver = getService('esArchiver'); - const supertest = getService('supertest'); - - describe('logSummaryBetween', () => { - before(() => esArchiver.load('x-pack/test/functional/es_archives/infra/metrics_and_logs')); - after(() => esArchiver.unload('x-pack/test/functional/es_archives/infra/metrics_and_logs')); - - it('should return empty and non-empty consecutive buckets', async () => { - const startTimestamp = EARLIEST_TIME_WITH_DATA; - const endTimestamp = - LATEST_TIME_WITH_DATA + (LATEST_TIME_WITH_DATA - EARLIEST_TIME_WITH_DATA); - const bucketSize = Math.ceil((endTimestamp - startTimestamp) / 10); - - const { body } = await supertest - .post(LOG_ENTRIES_SUMMARY_PATH) - .set(COMMON_HEADERS) - .send( - logEntriesSummaryRequestRT.encode({ - logView: { type: 'log-view-reference', logViewId: 'default' }, - startTimestamp, - endTimestamp, - bucketSize, - query: null, - }) - ) - .expect(200); - - const logSummaryResponse = pipe( - logEntriesSummaryResponseRT.decode(body), - fold(throwErrors(createPlainError), identity) - ); - - expect(logSummaryResponse.data.buckets).to.have.length(10); - expect( - logSummaryResponse.data.buckets.filter((bucket: any) => bucket.entriesCount > 0) - ).to.have.length(5); - expect( - pairs( - logSummaryResponse.data.buckets, - (first: any, second: any) => first.end === second.start - ).every((pair) => pair) - ).to.equal(true); - }); - }); -} diff --git a/x-pack/test/functional/apps/infra/index.ts b/x-pack/test/functional/apps/infra/index.ts index 999d7342f4639..89f4a40a74d45 100644 --- a/x-pack/test/functional/apps/infra/index.ts +++ b/x-pack/test/functional/apps/infra/index.ts @@ -26,10 +26,7 @@ export default ({ loadTestFile }: FtrProviderContext) => { describe('Logs UI', function () { loadTestFile(require.resolve('./logs/log_entry_categories_tab')); loadTestFile(require.resolve('./logs/log_entry_rate_tab')); - loadTestFile(require.resolve('./logs/logs_source_configuration')); - loadTestFile(require.resolve('./logs/log_stream_date_nano')); loadTestFile(require.resolve('./logs/link_to')); - loadTestFile(require.resolve('./logs/log_stream')); loadTestFile(require.resolve('./logs/ml_job_id_formats/tests')); }); }); diff --git a/x-pack/test/functional/apps/infra/logs/log_stream.ts b/x-pack/test/functional/apps/infra/logs/log_stream.ts deleted file mode 100644 index b8f926380e632..0000000000000 --- a/x-pack/test/functional/apps/infra/logs/log_stream.ts +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import expect from '@kbn/expect'; -import { URL } from 'url'; -import { FtrProviderContext } from '../../../ftr_provider_context'; - -const SERVICE_ID = '49a18510598271e924253ed2581d7ada'; - -export default ({ getPageObjects, getService }: FtrProviderContext) => { - const pageObjects = getPageObjects(['common']); - const retry = getService('retry'); - const browser = getService('browser'); - const esArchiver = getService('esArchiver'); - - describe.skip('Log stream', function () { - describe('Legacy URL handling', () => { - describe('Correctly handles legacy versions of logFilter', () => { - before(async () => { - await esArchiver.load('x-pack/test/functional/es_archives/infra/8.0.0/logs_and_metrics'); - }); - after(async () => { - await esArchiver.unload( - 'x-pack/test/functional/es_archives/infra/8.0.0/logs_and_metrics' - ); - }); - it('Expression and kind', async () => { - const location = { - hash: '', - pathname: '/stream', - search: `logFilter=(expression:'service.id:"${SERVICE_ID}"',kind:kuery)`, - state: undefined, - }; - - await pageObjects.common.navigateToUrlWithBrowserHistory( - 'infraLogs', - location.pathname, - location.search, - { - ensureCurrentUrl: false, - } - ); - - await retry.tryForTime(5000, async () => { - const currentUrl = await browser.getCurrentUrl(); - const parsedUrl = new URL(currentUrl); - - expect(parsedUrl.pathname).to.be('/app/logs/stream'); - expect(parsedUrl.searchParams.get('logFilter')).to.contain( - `(filters:!(),query:(language:kuery,query:\'service.id:"${SERVICE_ID}"\')` - ); - }); - }); - it('Top-level query and language', async () => { - const location = { - hash: '', - pathname: '/stream', - search: `logFilter=(query:'service.id:"${SERVICE_ID}"',language:kuery)`, - state: undefined, - }; - - await pageObjects.common.navigateToUrlWithBrowserHistory( - 'infraLogs', - location.pathname, - location.search, - { - ensureCurrentUrl: false, - } - ); - - await retry.tryForTime(5000, async () => { - const currentUrl = await browser.getCurrentUrl(); - const parsedUrl = new URL(currentUrl); - - expect(parsedUrl.pathname).to.be('/app/logs/stream'); - expect(parsedUrl.searchParams.get('logFilter')).to.contain( - `(filters:!(),query:(language:kuery,query:\'service.id:"${SERVICE_ID}"\')` - ); - }); - }); - }); - }); - }); -}; diff --git a/x-pack/test/functional/apps/infra/logs/log_stream_date_nano.ts b/x-pack/test/functional/apps/infra/logs/log_stream_date_nano.ts deleted file mode 100644 index eb0e23083ad6b..0000000000000 --- a/x-pack/test/functional/apps/infra/logs/log_stream_date_nano.ts +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../ftr_provider_context'; -import { DATES } from '../constants'; - -export default ({ getPageObjects, getService }: FtrProviderContext) => { - const retry = getService('retry'); - const esArchiver = getService('esArchiver'); - const logsUi = getService('logsUi'); - const find = getService('find'); - const logFilter = { - timeRange: { - from: DATES.metricsAndLogs.stream.startWithData, - to: DATES.metricsAndLogs.stream.endWithData, - }, - }; - - describe.skip('Log stream supports nano precision', function () { - before(async () => { - await esArchiver.load('x-pack/test/functional/es_archives/infra/logs_with_nano_date'); - }); - after(async () => { - await esArchiver.unload('x-pack/test/functional/es_archives/infra/logs_with_nano_date'); - }); - - it('should display logs entries containing date_nano timestamps properly ', async () => { - await logsUi.logStreamPage.navigateTo({ logFilter }); - - const logStreamEntries = await logsUi.logStreamPage.getStreamEntries(); - - expect(logStreamEntries.length).to.be(4); - }); - - it('should render timestamp column properly', async () => { - await logsUi.logStreamPage.navigateTo({ logFilter }); - - await retry.try(async () => { - const columnHeaderLabels = await logsUi.logStreamPage.getColumnHeaderLabels(); - expect(columnHeaderLabels[0]).to.eql('Oct 17, 2018'); - }); - }); - - it('should render timestamp column values properly', async () => { - await logsUi.logStreamPage.navigateTo({ logFilter }); - - const logStreamEntries = await logsUi.logStreamPage.getStreamEntries(); - - const firstLogStreamEntry = logStreamEntries[0]; - - const entryTimestamp = await logsUi.logStreamPage.getLogEntryColumnValueByName( - firstLogStreamEntry, - 'timestampLogColumn' - ); - - expect(entryTimestamp).to.be('19:43:22.111'); - }); - - it('should properly render timestamp in flyout with nano precision', async () => { - await logsUi.logStreamPage.navigateTo({ logFilter }); - - const logStreamEntries = await logsUi.logStreamPage.getStreamEntries(); - const firstLogStreamEntry = logStreamEntries[0]; - - await logsUi.logStreamPage.openLogEntryDetailsFlyout(firstLogStreamEntry); - - const cells = await find.allByCssSelector('.euiTableCellContent'); - - let isFound = false; - - for (const cell of cells) { - const cellText = await cell.getVisibleText(); - if (cellText === '2018-10-17T19:43:22.111111111Z') { - isFound = true; - return; - } - } - - expect(isFound).to.be(true); - }); - }); -}; diff --git a/x-pack/test/functional/apps/infra/logs/logs_source_configuration.ts b/x-pack/test/functional/apps/infra/logs/logs_source_configuration.ts deleted file mode 100644 index 27fa95ca16696..0000000000000 --- a/x-pack/test/functional/apps/infra/logs/logs_source_configuration.ts +++ /dev/null @@ -1,195 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import expect from '@kbn/expect'; -import { - ELASTIC_HTTP_VERSION_HEADER, - X_ELASTIC_INTERNAL_ORIGIN_REQUEST, -} from '@kbn/core-http-common'; -import { DATES } from '../constants'; - -import { FtrProviderContext } from '../../../ftr_provider_context'; - -const COMMON_REQUEST_HEADERS = { - 'kbn-xsrf': 'some-xsrf-token', -}; - -export default ({ getPageObjects, getService }: FtrProviderContext) => { - const esArchiver = getService('esArchiver'); - const browser = getService('browser'); - const logsUi = getService('logsUi'); - const infraSourceConfigurationForm = getService('infraSourceConfigurationForm'); - const pageObjects = getPageObjects(['common', 'header', 'infraLogs']); - const retry = getService('retry'); - const supertest = getService('supertest'); - const kibanaServer = getService('kibanaServer'); - - describe.skip('Logs Source Configuration', function () { - before(async () => { - await kibanaServer.savedObjects.cleanStandardList(); - }); - after(async () => { - await kibanaServer.savedObjects.cleanStandardList(); - }); - - describe('Allows indices configuration', () => { - const logFilter = { - timeRange: { - from: DATES.metricsAndLogs.stream.startWithData, - to: DATES.metricsAndLogs.stream.endWithData, - }, - }; - const formattedLocalStart = new Date(logFilter.timeRange.from).toLocaleDateString('en-US', { - month: 'short', - day: 'numeric', - year: 'numeric', - }); - - before(async () => { - await esArchiver.load('x-pack/test/functional/es_archives/infra/metrics_and_logs'); - }); - after(async () => { - await esArchiver.unload('x-pack/test/functional/es_archives/infra/metrics_and_logs'); - }); - - it('renders the correct page title', async () => { - await pageObjects.infraLogs.navigateToTab('settings'); - - await pageObjects.header.waitUntilLoadingHasFinished(); - - await retry.try(async () => { - const documentTitle = await browser.getTitle(); - expect(documentTitle).to.contain('Settings - Logs - Observability - Elastic'); - }); - }); - - it('can change the log indices to a pattern that matches nothing', async () => { - await pageObjects.infraLogs.navigateToTab('settings'); - - await retry.try(async () => { - await infraSourceConfigurationForm.getForm(); - }); - - await pageObjects.header.waitUntilLoadingHasFinished(); - - await infraSourceConfigurationForm.selectIndicesPanel(); - - const nameInput = await infraSourceConfigurationForm.getNameInput(); - await nameInput.clearValueWithKeyboard({ charByChar: true }); - await nameInput.type('Modified Source'); - - const logIndicesInput = await infraSourceConfigurationForm.getLogIndicesInput(); - await logIndicesInput.clearValueWithKeyboard({ charByChar: true }); - await logIndicesInput.type('does-not-exist-*'); - - await infraSourceConfigurationForm.saveConfiguration(); - }); - - it('renders the no indices screen when no indices match the pattern', async () => { - await logsUi.logStreamPage.navigateTo(); - - await retry.try(async () => { - await logsUi.logStreamPage.getNoDataPage(); - }); - }); - - it('can change the log indices back to a pattern that matches something', async () => { - await pageObjects.infraLogs.navigateToTab('settings'); - - await retry.try(async () => { - await infraSourceConfigurationForm.getForm(); - }); - - await pageObjects.header.waitUntilLoadingHasFinished(); - - const logIndicesInput = await infraSourceConfigurationForm.getLogIndicesInput(); - await logIndicesInput.clearValueWithKeyboard({ charByChar: true }); - await logIndicesInput.type('filebeat-*'); - - await infraSourceConfigurationForm.saveConfiguration(); - }); - - it('renders the default log columns with their headers', async () => { - await logsUi.logStreamPage.navigateTo({ logFilter }); - - await retry.try(async () => { - const columnHeaderLabels = await logsUi.logStreamPage.getColumnHeaderLabels(); - - expect(columnHeaderLabels).to.eql([formattedLocalStart, 'event.dataset', 'Message']); - }); - - const logStreamEntries = await logsUi.logStreamPage.getStreamEntries(); - expect(logStreamEntries.length).to.be.greaterThan(0); - - const firstLogStreamEntry = logStreamEntries[0]; - const logStreamEntryColumns = await logsUi.logStreamPage.getLogColumnsOfStreamEntry( - firstLogStreamEntry - ); - - expect(logStreamEntryColumns).to.have.length(3); - }); - - it('records telemetry for logs', async () => { - await logsUi.logStreamPage.navigateTo({ logFilter }); - - await logsUi.logStreamPage.getStreamEntries(); - - const [{ stats }] = await supertest - .post(`/internal/telemetry/clusters/_stats`) - .set(COMMON_REQUEST_HEADERS) - .set(ELASTIC_HTTP_VERSION_HEADER, '2') - .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') - .set('Accept', 'application/json') - .send({ - unencrypted: true, - refreshCache: true, - }) - .expect(200) - .then((res: any) => res.body); - - expect(stats.stack_stats.kibana.plugins.infraops.last_24_hours.hits.logs).to.be.greaterThan( - 0 - ); - }); - - it('can change the log columns', async () => { - await pageObjects.infraLogs.navigateToTab('settings'); - - await retry.try(async () => { - await infraSourceConfigurationForm.getForm(); - }); - - await infraSourceConfigurationForm.removeAllLogColumns(); - await infraSourceConfigurationForm.addTimestampLogColumn(); - await infraSourceConfigurationForm.addFieldLogColumn('host.name'); - - await infraSourceConfigurationForm.saveConfiguration(); - }); - - it('renders the changed log columns with their headers', async () => { - await logsUi.logStreamPage.navigateTo({ logFilter }); - - await retry.try(async () => { - const columnHeaderLabels = await logsUi.logStreamPage.getColumnHeaderLabels(); - - expect(columnHeaderLabels).to.eql([formattedLocalStart, 'host.name']); - }); - - const logStreamEntries = await logsUi.logStreamPage.getStreamEntries(); - - expect(logStreamEntries.length).to.be.greaterThan(0); - - const firstLogStreamEntry = logStreamEntries[0]; - const logStreamEntryColumns = await logsUi.logStreamPage.getLogColumnsOfStreamEntry( - firstLogStreamEntry - ); - - expect(logStreamEntryColumns).to.have.length(2); - }); - }); - }); -}; diff --git a/x-pack/test/functional/page_objects/infra_logs_page.ts b/x-pack/test/functional/page_objects/infra_logs_page.ts index 778acfc5fe480..21d54b844a050 100644 --- a/x-pack/test/functional/page_objects/infra_logs_page.ts +++ b/x-pack/test/functional/page_objects/infra_logs_page.ts @@ -5,24 +5,8 @@ * 2.0. */ -import { FlyoutOptionsUrlState } from '@kbn/infra-plugin/public/containers/logs/log_flyout'; -import querystring from 'querystring'; -import { encode } from '@kbn/rison'; -import type { PositionStateInUrl } from '@kbn/infra-plugin/public/observability_logs/log_stream_position_state/src/url_state_storage_service'; -import { FilterStateInUrl } from '@kbn/infra-plugin/public/observability_logs/log_stream_query_state'; import { FtrProviderContext } from '../ftr_provider_context'; -export interface TabsParams { - stream: { - logPosition?: Partial; - logFilter?: Partial; - flyoutOptions?: Partial; - }; - settings: never; - 'log-categories': any; - 'log-rate': any; -} - export function InfraLogsPageProvider({ getPageObjects, getService }: FtrProviderContext) { const testSubjects = getService('testSubjects'); const pageObjects = getPageObjects(['common']); @@ -32,24 +16,11 @@ export function InfraLogsPageProvider({ getPageObjects, getService }: FtrProvide await pageObjects.common.navigateToApp('infraLogs'); }, - async navigateToTab(logsUiTab: T, params?: TabsParams[T]) { - let qs = ''; - if (params) { - const parsedParams: Record = {}; - - for (const key in params) { - if (Object.hasOwn(params, key)) { - const value = params[key]; - parsedParams[key] = encode(value); - } - } - qs = '?' + querystring.stringify(parsedParams); - } - + async navigateToTab(logsUiTab: T) { await pageObjects.common.navigateToUrlWithBrowserHistory( 'infraLogs', `/${logsUiTab}`, - qs, + '', { ensureCurrentUrl: false } // Test runner struggles with `rison-node` escaped values ); }, @@ -60,4 +31,4 @@ export function InfraLogsPageProvider({ getPageObjects, getService }: FtrProvide }; } -type LogsUiTab = 'log-categories' | 'log-rate' | 'settings' | 'stream'; +type LogsUiTab = 'log-categories' | 'log-rate'; diff --git a/x-pack/test/functional/services/infra_source_configuration_form.ts b/x-pack/test/functional/services/infra_source_configuration_form.ts index c8d28e0e3656b..9589debf5d289 100644 --- a/x-pack/test/functional/services/infra_source_configuration_form.ts +++ b/x-pack/test/functional/services/infra_source_configuration_form.ts @@ -14,8 +14,6 @@ export function InfraSourceConfigurationFormProvider({ }: FtrProviderContext) { const retry = getService('retry'); const testSubjects = getService('testSubjects'); - const browser = getService('browser'); - const common = getPageObject('common'); return { /** @@ -24,92 +22,12 @@ export function InfraSourceConfigurationFormProvider({ async getNameInput(): Promise { return await testSubjects.findDescendant('~nameInput', await this.getForm()); }, - async getLogIndicesInput(): Promise { - return await testSubjects.findDescendant('~logIndicesInput', await this.getForm()); - }, async getMetricIndicesInput(): Promise { return await testSubjects.findDescendant('~metricIndicesInput', await this.getForm()); }, async selectIndicesPanel(): Promise { return await testSubjects.click('logIndicesCheckableCard'); }, - /** - * Logs - */ - async getAddLogColumnButton(): Promise { - return await testSubjects.findDescendant('~addLogColumnButton', await this.getForm()); - }, - async getAddLogColumnPopover(): Promise { - return await testSubjects.find('~addLogColumnPopover'); - }, - async addTimestampLogColumn() { - // try to open the popover - const popover = await retry.try(async () => { - await (await this.getAddLogColumnButton()).click(); - return this.getAddLogColumnPopover(); - }); - - // try to select the timestamp field - await retry.try(async () => { - await (await testSubjects.findDescendant('~addTimestampLogColumn', popover)).click(); - }); - - // wait for timestamp panel to show up - await testSubjects.findDescendant('~systemLogColumnPanel:Timestamp', await this.getForm()); - }, - async addFieldLogColumn(fieldName: string) { - // try to open the popover - const popover = await retry.try(async () => { - await (await this.getAddLogColumnButton()).click(); - return this.getAddLogColumnPopover(); - }); - - // try to select the given field - await retry.try(async () => { - await (await testSubjects.findDescendant('~fieldSearchInput', popover)).type(fieldName); - await ( - await testSubjects.findDescendant(`~addFieldLogColumn:${fieldName}`, popover) - ).click(); - }); - - // wait for field panel to show up - await testSubjects.findDescendant(`~fieldLogColumnPanel:${fieldName}`, await this.getForm()); - }, - async getLogColumnPanels(): Promise { - return await testSubjects.findAllDescendant('~logColumnPanel', await this.getForm()); - }, - async removeLogColumn(columnIndex: number) { - const logColumnPanel = (await this.getLogColumnPanels())[columnIndex]; - await (await testSubjects.findDescendant('~removeLogColumnButton', logColumnPanel)).click(); - await testSubjects.waitForDeleted(logColumnPanel); - }, - async removeAllLogColumns() { - for (const _ of await this.getLogColumnPanels()) { - await this.removeLogColumn(0); - } - }, - async moveLogColumn(sourceIndex: number, destinationIndex: number) { - const KEY_PRESS_DELAY_MS = 500; // This may need to be high for Jenkins; 100 works on desktop - - const logColumnPanel = (await this.getLogColumnPanels())[sourceIndex]; - const moveLogColumnHandle = await testSubjects.findDescendant( - '~moveLogColumnHandle', - logColumnPanel - ); - await moveLogColumnHandle.focus(); - const movementDifference = destinationIndex - sourceIndex; - await moveLogColumnHandle.pressKeys(browser.keys.SPACE); - for (let i = 0; i < Math.abs(movementDifference); i++) { - await common.sleep(KEY_PRESS_DELAY_MS); - if (movementDifference > 0) { - await moveLogColumnHandle.pressKeys(browser.keys.ARROW_DOWN); - } else { - await moveLogColumnHandle.pressKeys(browser.keys.ARROW_UP); - } - } - await moveLogColumnHandle.pressKeys(browser.keys.SPACE); - await common.sleep(KEY_PRESS_DELAY_MS); - }, /** * Infra Metrics bottom actions bar diff --git a/x-pack/test/functional/services/logs_ui/index.ts b/x-pack/test/functional/services/logs_ui/index.ts index c0690957c4148..7a7bf8050dbf7 100644 --- a/x-pack/test/functional/services/logs_ui/index.ts +++ b/x-pack/test/functional/services/logs_ui/index.ts @@ -8,13 +8,11 @@ import { FtrProviderContext } from '../../ftr_provider_context'; import { LogEntryCategoriesPageProvider } from './log_entry_categories'; import { LogEntryRatePageProvider } from './log_entry_rate'; -import { LogStreamPageProvider } from './log_stream'; export function LogsUiProvider(context: FtrProviderContext) { return { logEntryCategoriesPage: LogEntryCategoriesPageProvider(context), logEntryRatePage: LogEntryRatePageProvider(context), - logStreamPage: LogStreamPageProvider(context), cleanIndices: createCleanIndicesHandler(context), }; } diff --git a/x-pack/test/functional/services/logs_ui/log_stream.ts b/x-pack/test/functional/services/logs_ui/log_stream.ts deleted file mode 100644 index 160e949c84de4..0000000000000 --- a/x-pack/test/functional/services/logs_ui/log_stream.ts +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { WebElementWrapper } from '@kbn/ftr-common-functional-ui-services'; -import { FtrProviderContext } from '../../ftr_provider_context'; -import { TabsParams } from '../../page_objects/infra_logs_page'; - -export function LogStreamPageProvider({ getPageObjects, getService }: FtrProviderContext) { - const pageObjects = getPageObjects(['infraLogs']); - const retry = getService('retry'); - const find = getService('find'); - const testSubjects = getService('testSubjects'); - - return { - async navigateTo(params?: TabsParams['stream']) { - await pageObjects.infraLogs.navigateToTab('stream', params); - }, - - async getColumnHeaderLabels(): Promise { - const columnHeaderElements: WebElementWrapper[] = await testSubjects.findAll( - '~logColumnHeader' - ); - return await Promise.all(columnHeaderElements.map((element) => element.getVisibleText())); - }, - - async getStreamEntries(minimumItems = 1): Promise { - await retry.try(async () => { - const elements = await testSubjects.findAll('~streamEntry'); - if (!elements || elements.length < minimumItems) { - throw new Error(); - } - }); - - return await testSubjects.findAll('~streamEntry'); - }, - - async getLogColumnsOfStreamEntry( - entryElement: WebElementWrapper - ): Promise { - return await testSubjects.findAllDescendant('~logColumn', entryElement); - }, - - async getLogEntryColumnValueByName( - entryElement: WebElementWrapper, - column: string - ): Promise { - const columnElement = await testSubjects.findDescendant(`~${column}`, entryElement); - - const contentElement = await columnElement.findByCssSelector( - `[data-test-subj='LogEntryColumnContent']` - ); - - return await contentElement.getVisibleText(); - }, - - async openLogEntryDetailsFlyout(entryElement: WebElementWrapper) { - await entryElement.click(); - - const menuButton = await testSubjects.findDescendant( - `~infraLogEntryContextMenuButton`, - entryElement - ); - await menuButton.click(); - - await find.clickByButtonText('View details'); - }, - - async getNoLogsIndicesPrompt() { - return await testSubjects.find('noLogsIndicesPrompt'); - }, - - async getNoDataPage() { - return await testSubjects.find('noDataPage'); - }, - }; -} diff --git a/x-pack/test/upgrade/apps/logs/index.ts b/x-pack/test/upgrade/apps/logs/index.ts deleted file mode 100644 index ed4181f75d76d..0000000000000 --- a/x-pack/test/upgrade/apps/logs/index.ts +++ /dev/null @@ -1,14 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { FtrProviderContext } from '../../ftr_provider_context'; - -export default function ({ loadTestFile }: FtrProviderContext) { - describe('upgrade', function () { - loadTestFile(require.resolve('./logs_smoke_tests')); - }); -} diff --git a/x-pack/test/upgrade/apps/logs/logs_smoke_tests.ts b/x-pack/test/upgrade/apps/logs/logs_smoke_tests.ts deleted file mode 100644 index 2cde8de43b62a..0000000000000 --- a/x-pack/test/upgrade/apps/logs/logs_smoke_tests.ts +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; - -export default function ({ getPageObjects, getService }: FtrProviderContext) { - const PageObjects = getPageObjects(['common', 'header', 'home', 'timePicker']); - const logsUi = getService('logsUi'); - - describe('upgrade logs smoke tests', function describeIndexTests() { - const spaces = [ - { space: 'default', basePath: '' }, - { space: 'automation', basePath: 's/automation' }, - ]; - - spaces.forEach(({ space, basePath }) => { - describe('space: ' + space, () => { - before(async () => { - await PageObjects.common.navigateToActualUrl('home', '/tutorial_directory/sampleData', { - basePath, - }); - await PageObjects.header.waitUntilLoadingHasFinished(); - await PageObjects.home.launchSampleLogs('logs'); - await PageObjects.header.waitUntilLoadingHasFinished(); - await PageObjects.timePicker.setCommonlyUsedTime('Last_1 year'); - }); - - it('should show log streams', async () => { - const logStreamEntries = await logsUi.logStreamPage.getStreamEntries(); - expect(logStreamEntries.length).to.be.greaterThan(100); - }); - }); - }); - }); -} diff --git a/x-pack/test/upgrade/config.ts b/x-pack/test/upgrade/config.ts index d6458f087e393..732b9004e2abd 100644 --- a/x-pack/test/upgrade/config.ts +++ b/x-pack/test/upgrade/config.ts @@ -24,7 +24,6 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { require.resolve('./apps/dashboard'), require.resolve('./apps/discover'), require.resolve('./apps/graph'), - require.resolve('./apps/logs'), require.resolve('./apps/maps'), require.resolve('./apps/reporting'), require.resolve('./apps/rules'), From f612e71452b9f38e84ebe9ee275804624dd6e43e Mon Sep 17 00:00:00 2001 From: Artem Shelkovnikov Date: Fri, 10 Jan 2025 12:51:25 +0100 Subject: [PATCH 15/42] Add new native fields for SPO connector - certificate authenticate (#205337) ## Summary This PR reflects the changes done in https://github.com/elastic/connectors/pull/3064: update of Sharepoint Online native connector configurable fields. ### Checklist Check the PR satisfies following conditions. Reviewers should verify this PR satisfies this list as well. - [ ] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [ ] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [ ] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [ ] If a plugin configuration key changed, check if it needs to be allowlisted in the cloud and added to the [docker list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker) - [ ] This was checked for breaking HTTP API changes, and any breaking changes have been approved by the breaking-change committee. The `release_note:breaking` label should be applied in these situations. - [ ] [Flaky Test Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was used on any tests changed - [ ] The PR description includes the appropriate Release Notes section, and the correct `release_note:*` label is applied per the [guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) --- .../types/native_connectors.ts | 205 +++++++++++++----- 1 file changed, 147 insertions(+), 58 deletions(-) diff --git a/src/platform/packages/shared/kbn-search-connectors/types/native_connectors.ts b/src/platform/packages/shared/kbn-search-connectors/types/native_connectors.ts index 96af7ea581e6c..ba8aff69693d8 100644 --- a/src/platform/packages/shared/kbn-search-connectors/types/native_connectors.ts +++ b/src/platform/packages/shared/kbn-search-connectors/types/native_connectors.ts @@ -4242,6 +4242,45 @@ export const NATIVE_CONNECTOR_DEFINITIONS: Record Date: Fri, 10 Jan 2025 14:11:47 +0100 Subject: [PATCH 16/42] [Streams] Fix broken image usage (#206258) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 📓 Summary Fixes a missing referenced image after merging https://github.com/elastic/kibana/pull/204793 --- .../components/stream_detail_overview/index.tsx | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_overview/index.tsx b/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_overview/index.tsx index 09aaebc395410..1b9ed0a531927 100644 --- a/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_overview/index.tsx +++ b/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_overview/index.tsx @@ -8,7 +8,6 @@ import { EuiButton, EuiFlexGroup, EuiFlexItem, - EuiImage, EuiLoadingSpinner, EuiPanel, EuiTab, @@ -22,7 +21,6 @@ import React, { useMemo } from 'react'; import { css } from '@emotion/css'; import { ReadStreamDefinition, isWiredReadStream, isWiredStream } from '@kbn/streams-schema'; import { useDateRange } from '@kbn/observability-utils-browser/hooks/use_date_range'; -import illustration from '../assets/illustration.png'; import { useKibana } from '../../hooks/use_kibana'; import { useStreamsAppFetch } from '../../hooks/use_streams_app_fetch'; import { ControlledEsqlChart } from '../esql_chart/controlled_esql_chart'; @@ -30,6 +28,7 @@ import { StreamsAppSearchBar } from '../streams_app_search_bar'; import { getIndexPatterns } from '../../util/hierarchy_helpers'; import { StreamsList } from '../streams_list'; import { useStreamsAppRouter } from '../../hooks/use_streams_app_router'; +import { AssetImage } from '../asset_image'; const formatNumber = (val: number) => { return Number(val).toLocaleString('en', { @@ -303,13 +302,7 @@ function ChildStreamList({ stream }: { stream?: ReadStreamDefinition }) { `} > - + {i18n.translate('xpack.streams.entityDetailOverview.noChildStreams', { defaultMessage: 'Create streams for your logs', From 86e8a2fceeb24c83f52d421c18600811ac028ef4 Mon Sep 17 00:00:00 2001 From: Paulina Shakirova Date: Fri, 10 Jan 2025 14:38:00 +0100 Subject: [PATCH 17/42] [VisLibrary] AnnotGroup listing page papercuts (#205914) ## Summary This PR fixes [Annotation groups Listing Page Papercuts](https://github.com/elastic/kibana/issues/198731) and [Dashboard Listing Page Papercuts](https://github.com/elastic/kibana/issues/198728) issues. 1. Changed the name of the first column 2. Fixed edit icon being invisible while editing functionality is available. In the past the logic was different - hiding of icon was happening based on `isEditable(item)` property, and in the [[Managed content] readonly in library views](https://github.com/elastic/kibana/pull/176263/files#diff-e442682471f1021a9126ddcad7e00a0d334e57ac8db512c1c3268e14ecac0074L552) PR the logic was changed to depend on adding a key `{ edit: { enabled: false }` if there is a need to hide the Edit button. What happened is that the logic should be -> If you don't want to show the Edit icon, add `{ edit: { enabled: false }`, but in the current code, although there is no such key, the pencil stays invisible, because the `Boolean(tableItemsRowActions[item.id]?.edit?.enabled)` resolved to `false` when it is `undefined` (when the Edit functionality isn't disabled.) In this PR I propose an adjustment to this line of code. 3. Changed the View Details icon. 4. Show Reload page toast when a user changes preferred `savedObjects:perPage` in Advanced Settings. 5. Fix sorting algorithm that was sorting incorrectly if the preferred `savedObjects:perPage` was less than 10. Screenshot 2025-01-09 at 13 44 39 Screenshot 2025-01-09 at 13 43 30 --- .../src/table_list_view_table.tsx | 10 +++++----- .../shared/saved_objects_finder/server/ui_settings.ts | 1 + .../private/translations/translations/fr-FR.json | 1 - .../private/translations/translations/ja-JP.json | 1 - .../private/translations/translations/zh-CN.json | 1 - 5 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/platform/packages/shared/content-management/table_list_view_table/src/table_list_view_table.tsx b/src/platform/packages/shared/content-management/table_list_view_table/src/table_list_view_table.tsx index 7a5356f75eb2b..3ab48d58539f4 100644 --- a/src/platform/packages/shared/content-management/table_list_view_table/src/table_list_view_table.tsx +++ b/src/platform/packages/shared/content-management/table_list_view_table/src/table_list_view_table.tsx @@ -299,7 +299,7 @@ const urlStateSerializer = (updated: { const tableColumnMetadata = { title: { field: 'attributes.title', - name: 'Name, description, tags', + name: 'Name', }, updatedAt: { field: 'updatedAt', @@ -421,7 +421,7 @@ function TableListViewTableComp({ pageIndex: 0, totalItemCount: 0, pageSize: initialPageSize, - pageSizeOptions: uniq([10, 20, 50, initialPageSize]).sort(), + pageSizeOptions: uniq([10, 20, 50, initialPageSize]).sort((a, b) => Number(a) - Number(b)), }, tableSort: initialSort.tableSort, sortColumnChanged: !initialSort.isDefault, @@ -606,7 +606,7 @@ function TableListViewTableComp({ name: titleColumnName ?? i18n.translate('contentManagement.tableList.mainColumnName', { - defaultMessage: 'Name, description, tags', + defaultMessage: 'Name', }), sortable: true, render: (field: keyof T, record: T) => { @@ -696,7 +696,7 @@ function TableListViewTableComp({ ), icon: 'pencil', type: 'icon', - available: (item) => Boolean(tableItemsRowActions[item.id]?.edit?.enabled), + available: (item) => tableItemsRowActions[item.id]?.edit?.enabled !== false, enabled: (v) => !(v as unknown as { error: string })?.error, onClick: editItem, 'data-test-subj': `edit-action`, @@ -722,7 +722,7 @@ function TableListViewTableComp({ defaultMessage: 'View details', } ), - icon: 'iInCircle', + icon: 'controlsVertical', type: 'icon', onClick: inspectItem, 'data-test-subj': `inspect-action`, diff --git a/src/platform/plugins/shared/saved_objects_finder/server/ui_settings.ts b/src/platform/plugins/shared/saved_objects_finder/server/ui_settings.ts index caf064232abbb..f2ba0a4475014 100644 --- a/src/platform/plugins/shared/saved_objects_finder/server/ui_settings.ts +++ b/src/platform/plugins/shared/saved_objects_finder/server/ui_settings.ts @@ -23,6 +23,7 @@ export const uiSettings: Record = { description: i18n.translate('savedObjectsFinder.advancedSettings.perPageText', { defaultMessage: 'Number of objects to show per page in the load dialog', }), + requiresPageReload: true, schema: schema.number(), }, [LISTING_LIMIT_SETTING]: { diff --git a/x-pack/platform/plugins/private/translations/translations/fr-FR.json b/x-pack/platform/plugins/private/translations/translations/fr-FR.json index 9c894d9c411ed..42de200cec13b 100644 --- a/x-pack/platform/plugins/private/translations/translations/fr-FR.json +++ b/x-pack/platform/plugins/private/translations/translations/fr-FR.json @@ -593,7 +593,6 @@ "contentManagement.tableList.listing.userFilter.errorMessage": "Échec de chargement des utilisateurs", "contentManagement.tableList.listing.userFilter.filterLabel": "Créé par", "contentManagement.tableList.listing.userFilter.noCreators": "Aucun créateur", - "contentManagement.tableList.mainColumnName": "Nom, description, balises", "contentManagement.tableList.managedItemNoEdit": "Elastic gère cet objet. Clonez-le pour effectuer des modifications.", "contentManagement.tableList.tabsFilter.allTabLabel": "Tous", "contentManagement.tableList.tabsFilter.favoriteTabLabel": "Éléments avec étoiles", diff --git a/x-pack/platform/plugins/private/translations/translations/ja-JP.json b/x-pack/platform/plugins/private/translations/translations/ja-JP.json index 4a003c530cea9..2303c208517ba 100644 --- a/x-pack/platform/plugins/private/translations/translations/ja-JP.json +++ b/x-pack/platform/plugins/private/translations/translations/ja-JP.json @@ -595,7 +595,6 @@ "contentManagement.tableList.listing.userFilter.errorMessage": "ユーザーの読み込みに失敗しました", "contentManagement.tableList.listing.userFilter.filterLabel": "作成者", "contentManagement.tableList.listing.userFilter.noCreators": "作成担当なし", - "contentManagement.tableList.mainColumnName": "名前、説明、タグ", "contentManagement.tableList.managedItemNoEdit": "Elasticはこの項目を管理します。変更するには、複製してください。", "contentManagement.tableList.tabsFilter.allTabLabel": "すべて", "contentManagement.tableList.tabsFilter.favoriteTabLabel": "スター付き", diff --git a/x-pack/platform/plugins/private/translations/translations/zh-CN.json b/x-pack/platform/plugins/private/translations/translations/zh-CN.json index f98bbce64ce64..e56d56268807d 100644 --- a/x-pack/platform/plugins/private/translations/translations/zh-CN.json +++ b/x-pack/platform/plugins/private/translations/translations/zh-CN.json @@ -617,7 +617,6 @@ "contentManagement.tableList.listing.userFilter.errorMessage": "无法加载用户", "contentManagement.tableList.listing.userFilter.filterLabel": "创建者", "contentManagement.tableList.listing.userFilter.noCreators": "无创建者", - "contentManagement.tableList.mainColumnName": "名称、描述、标签", "contentManagement.tableList.managedItemNoEdit": "Elastic 将管理此项目。进行克隆以做出更改。", "contentManagement.tableList.tabsFilter.allTabLabel": "全部", "contentManagement.tableList.tabsFilter.favoriteTabLabel": "带星标", From 5be981535b5226467578bde6578b5fe6ac22a332 Mon Sep 17 00:00:00 2001 From: "elastic-renovate-prod[bot]" <174716857+elastic-renovate-prod[bot]@users.noreply.github.com> Date: Fri, 10 Jan 2025 14:06:12 +0000 Subject: [PATCH 18/42] Update dependency @elastic/ecs to ^8.11.5 (main) (#206159) Co-authored-by: elastic-renovate-prod[bot] <174716857+elastic-renovate-prod[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 3f965e6fd97a7..42b5b560d5a89 100644 --- a/package.json +++ b/package.json @@ -114,7 +114,7 @@ "@elastic/charts": "68.0.4", "@elastic/datemath": "5.0.3", "@elastic/ebt": "^1.1.1", - "@elastic/ecs": "^8.11.1", + "@elastic/ecs": "^8.11.5", "@elastic/elasticsearch": "^8.16.0", "@elastic/ems-client": "8.6.2", "@elastic/eui": "98.2.1-borealis.2", diff --git a/yarn.lock b/yarn.lock index c4a383bb41de2..551eb104c30b7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2300,10 +2300,10 @@ dependencies: "@elastic/ecs-helpers" "^2.1.1" -"@elastic/ecs@^8.11.1": - version "8.11.1" - resolved "https://registry.yarnpkg.com/@elastic/ecs/-/ecs-8.11.1.tgz#81d9a7592cdc2fb75fd484a2526f1a4f2f65d24e" - integrity sha512-f8P6XeFBQQ4Pxag131hjqUoGonYShzZl4cMSdbdstEtipjf9sqjzKKuEdnwbFhcGLiNOrq+K81P7476C4RC/aw== +"@elastic/ecs@^8.11.5": + version "8.11.5" + resolved "https://registry.yarnpkg.com/@elastic/ecs/-/ecs-8.11.5.tgz#70b9fed9b5946e8930ddbbbf5a2c8202d59e26b9" + integrity sha512-S7qkIaRrKHyiU6ww0r5qt8mOh8wK8w3LpNdMb0qxuj6wxQmn6TVpZ9DxxICGVKwaPbQIYuNTmBPSUocl+VhINw== "@elastic/elasticsearch-types@npm:@elastic/elasticsearch@^8.2.1": version "8.6.0" From 2cd882c50d01967a34d88463af630a8384fede9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20Haro?= Date: Fri, 10 Jan 2025 15:41:19 +0100 Subject: [PATCH 19/42] [ES `body` removal] `@elastic/kibana-core` (#204851) --- .../src/is_scripting_enabled.test.ts | 2 +- .../src/lib/apis/bulk_create.test.ts | 77 ++++++++------- .../src/lib/apis/bulk_create.ts | 2 +- .../src/lib/apis/bulk_delete.test.ts | 16 ++-- .../src/lib/apis/bulk_delete.ts | 2 +- .../src/lib/apis/bulk_get.isolated.test.ts | 2 +- .../src/lib/apis/bulk_get.test.ts | 16 ++-- .../src/lib/apis/bulk_get.ts | 6 +- .../src/lib/apis/bulk_update.test.ts | 28 +++--- .../src/lib/apis/bulk_update.ts | 4 +- .../src/lib/apis/check_conflicts.test.ts | 16 ++-- .../src/lib/apis/check_conflicts.ts | 6 +- .../src/lib/apis/create.test.ts | 40 ++++---- .../src/lib/apis/create.ts | 9 +- .../src/lib/apis/delete.test.ts | 2 +- .../src/lib/apis/delete_by_namespace.ts | 40 ++++---- .../src/lib/apis/find.isolated.test.ts | 2 +- .../src/lib/apis/find.test.ts | 38 ++++---- .../api-server-internal/src/lib/apis/find.ts | 47 ++++----- .../src/lib/apis/get.test.ts | 2 +- .../src/lib/apis/helpers/preflight_check.ts | 5 +- .../src/lib/apis/increment_counter.test.ts | 46 ++++----- ...collect_multi_namespace_references.test.ts | 2 +- .../collect_multi_namespace_references.ts | 2 +- .../delete_legacy_url_aliases.test.ts | 28 +++--- .../internals/delete_legacy_url_aliases.ts | 28 +++--- .../internals/increment_counter_internal.ts | 54 +++++------ .../internals/internal_bulk_resolve.test.ts | 12 +-- .../apis/internals/internal_bulk_resolve.ts | 6 +- .../preflight_check_for_create.test.ts | 4 +- .../internals/preflight_check_for_create.ts | 4 +- .../internals/update_objects_spaces.test.ts | 8 +- .../apis/internals/update_objects_spaces.ts | 6 +- .../src/lib/apis/open_point_in_time.test.ts | 4 +- .../src/lib/apis/remove_references_to.test.ts | 23 ++--- .../src/lib/apis/remove_references_to.ts | 44 ++++----- .../src/lib/apis/update.test.ts | 34 +++---- .../src/lib/apis/update.ts | 10 +- .../src/lib/apis/utils/es_responses.ts | 2 +- .../src/lib/apis/utils/internal_utils.ts | 2 +- .../src/lib/apis/utils/merge_for_update.ts | 2 +- .../src/lib/point_in_time_finder.ts | 2 +- .../repository.encryption_extension.test.ts | 16 ++-- .../lib/repository.spaces_extension.test.ts | 56 +++++------ .../api-server-internal/src/lib/repository.ts | 4 +- .../src/lib/repository_es_client.test.ts | 8 +- .../search/aggregations/validation.test.ts | 2 +- .../src/lib/search/aggregations/validation.ts | 2 +- .../src/lib/search/search_dsl/search_dsl.ts | 2 +- .../lib/search/search_dsl/sorting_params.ts | 2 +- .../test_helpers/repository.test.common.ts | 2 +- .../saved-objects/api-server/src/apis/find.ts | 2 +- .../src/utils/get_field_list.ts | 2 +- .../bulk_overwrite_transformed_documents.ts | 2 +- .../src/actions/create_index.ts | 2 +- .../src/actions/read_with_pit.ts | 2 +- .../server/src/mapping_definition.ts | 2 +- .../server/src/saved_objects_type.ts | 2 +- .../src/core_usage_data_service.ts | 2 +- .../group3/actions/es_errors.test.ts | 2 +- .../packages/shared/kbn-es-types/index.ts | 1 - .../packages/shared/kbn-es-types/src/index.ts | 14 ++- .../shared/kbn-es-types/src/search.ts | 15 +-- .../get_local_stats.test.ts | 2 +- .../telemetry_collection/get_local_stats.ts | 2 +- .../telemetry_collection/get_nodes_usage.ts | 2 +- .../telemetry_collection/get_license.ts | 2 +- .../get_stats_with_xpack.test.ts | 2 +- .../licensing/server/license_fetcher.test.ts | 2 +- .../licensing/server/license_fetcher.ts | 2 +- .../shared/licensing/server/plugin.test.ts | 2 +- .../application/services/forecast_service.ts | 96 ++++++++----------- .../create_entities_es_client.ts | 3 +- .../lib/helpers/get_apm_alerts_client.ts | 3 +- .../routes/alerts/alerting_es_client.ts | 3 +- .../get_container_id_from_signals.ts | 2 +- .../get_service_name_from_signals.ts | 2 +- .../create_apm_event_client/index.ts | 3 +- .../lib/helpers/get_infra_alerts_client.ts | 3 +- .../lib/helpers/get_infra_metrics_client.ts | 3 +- .../create_alerts_client.ts | 3 +- .../clients/create_entities_es_client.ts | 3 +- .../server/services/get_alerts_client.ts | 3 +- .../apps/group3/upgrade_assistant.ts | 38 ++++---- 84 files changed, 473 insertions(+), 535 deletions(-) diff --git a/src/core/packages/elasticsearch/server-internal/src/is_scripting_enabled.test.ts b/src/core/packages/elasticsearch/server-internal/src/is_scripting_enabled.test.ts index d77121cec13c0..e94981d88966b 100644 --- a/src/core/packages/elasticsearch/server-internal/src/is_scripting_enabled.test.ts +++ b/src/core/packages/elasticsearch/server-internal/src/is_scripting_enabled.test.ts @@ -8,7 +8,7 @@ */ import { isRetryableEsClientErrorMock } from './is_scripting_enabled.test.mocks'; -import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import * as estypes from '@elastic/elasticsearch/lib/api/types'; import { elasticsearchClientMock } from '@kbn/core-elasticsearch-client-server-mocks'; import { isInlineScriptingEnabled } from './is_scripting_enabled'; diff --git a/src/core/packages/saved-objects/api-server-internal/src/lib/apis/bulk_create.test.ts b/src/core/packages/saved-objects/api-server-internal/src/lib/apis/bulk_create.test.ts index 92dbeb4339a27..cd0ddf183da42 100644 --- a/src/core/packages/saved-objects/api-server-internal/src/lib/apis/bulk_create.test.ts +++ b/src/core/packages/saved-objects/api-server-internal/src/lib/apis/bulk_create.test.ts @@ -168,9 +168,9 @@ describe('#bulkCreate', () => { getId = () => expect.any(String), }: { method: string; _index?: string; getId?: (type: string, id?: string) => string } ) => { - const body = []; + const operations = []; for (const { type, id, if_primary_term: ifPrimaryTerm, if_seq_no: ifSeqNo } of objects) { - body.push({ + operations.push({ [method]: { _index, _id: getId(type, id), @@ -179,10 +179,10 @@ describe('#bulkCreate', () => { : {}), }, }); - body.push(expect.any(Object)); + operations.push(expect.any(Object)); } expect(client.bulk).toHaveBeenCalledWith( - expect.objectContaining({ body }), + expect.objectContaining({ operations }), expect.anything() ); }; @@ -290,9 +290,9 @@ describe('#bulkCreate', () => { it(`formats the ES request`, async () => { await bulkCreateSuccess(client, repository, [obj1, obj2]); - const body = [...expectObjArgs(obj1), ...expectObjArgs(obj2)]; + const operations = [...expectObjArgs(obj1), ...expectObjArgs(obj2)]; expect(client.bulk).toHaveBeenCalledWith( - expect.objectContaining({ body }), + expect.objectContaining({ operations }), expect.anything() ); }); @@ -301,9 +301,12 @@ describe('#bulkCreate', () => { const obj1WithManagedTrue = { ...obj1, managed: true }; const obj2WithManagedTrue = { ...obj2, managed: true }; await bulkCreateSuccess(client, repository, [obj1WithManagedTrue, obj2WithManagedTrue]); - const body = [...expectObjArgs(obj1WithManagedTrue), ...expectObjArgs(obj2WithManagedTrue)]; + const operations = [ + ...expectObjArgs(obj1WithManagedTrue), + ...expectObjArgs(obj2WithManagedTrue), + ]; expect(client.bulk).toHaveBeenCalledWith( - expect.objectContaining({ body }), + expect.objectContaining({ operations }), expect.anything() ); }); @@ -333,9 +336,9 @@ describe('#bulkCreate', () => { await bulkCreateSuccess(client, repository, objects); const expected = expect.not.objectContaining({ originId: expect.anything() }); - const body = [expect.any(Object), expected, expect.any(Object), expected]; + const operations = [expect.any(Object), expected, expect.any(Object), expected]; expect(client.bulk).toHaveBeenCalledWith( - expect.objectContaining({ body }), + expect.objectContaining({ operations }), expect.anything() ); }); @@ -360,9 +363,9 @@ describe('#bulkCreate', () => { ]; await bulkCreateSuccess(client, repository, objects); const expected = expect.objectContaining({ originId: 'some-originId' }); - const body = [expect.any(Object), expected, expect.any(Object), expected]; + const operations = [expect.any(Object), expected, expect.any(Object), expected]; expect(client.bulk).toHaveBeenCalledWith( - expect.objectContaining({ body }), + expect.objectContaining({ operations }), expect.anything() ); }); @@ -375,9 +378,9 @@ describe('#bulkCreate', () => { ]; await bulkCreateSuccess(client, repository, objects); const expected = expect.not.objectContaining({ originId: expect.anything() }); - const body = [expect.any(Object), expected, expect.any(Object), expected]; + const operations = [expect.any(Object), expected, expect.any(Object), expected]; expect(client.bulk).toHaveBeenCalledWith( - expect.objectContaining({ body }), + expect.objectContaining({ operations }), expect.anything() ); }); @@ -389,9 +392,9 @@ describe('#bulkCreate', () => { ]; await bulkCreateSuccess(client, repository, objects); const expected = expect.objectContaining({ originId: 'existing-originId' }); - const body = [expect.any(Object), expected, expect.any(Object), expected]; + const operations = [expect.any(Object), expected, expect.any(Object), expected]; expect(client.bulk).toHaveBeenCalledWith( - expect.objectContaining({ body }), + expect.objectContaining({ operations }), expect.anything() ); }); @@ -401,9 +404,9 @@ describe('#bulkCreate', () => { it(`adds namespace to request body for any types that are single-namespace`, async () => { await bulkCreateSuccess(client, repository, [obj1, obj2], { namespace }); const expected = expect.objectContaining({ namespace }); - const body = [expect.any(Object), expected, expect.any(Object), expected]; + const operations = [expect.any(Object), expected, expect.any(Object), expected]; expect(client.bulk).toHaveBeenCalledWith( - expect.objectContaining({ body }), + expect.objectContaining({ operations }), expect.anything() ); }); @@ -412,9 +415,9 @@ describe('#bulkCreate', () => { it(`adds managed=false to request body if declared for any types that are single-namespace`, async () => { await bulkCreateSuccess(client, repository, [obj1, obj2], { namespace, managed: false }); const expected = expect.objectContaining({ namespace, managed: false }); - const body = [expect.any(Object), expected, expect.any(Object), expected]; + const operations = [expect.any(Object), expected, expect.any(Object), expected]; expect(client.bulk).toHaveBeenCalledWith( - expect.objectContaining({ body }), + expect.objectContaining({ operations }), expect.anything() ); }); @@ -422,9 +425,9 @@ describe('#bulkCreate', () => { it(`adds managed=true to request body if declared for any types that are single-namespace`, async () => { await bulkCreateSuccess(client, repository, [obj1, obj2], { namespace, managed: true }); const expected = expect.objectContaining({ namespace, managed: true }); - const body = [expect.any(Object), expected, expect.any(Object), expected]; + const operations = [expect.any(Object), expected, expect.any(Object), expected]; expect(client.bulk).toHaveBeenCalledWith( - expect.objectContaining({ body }), + expect.objectContaining({ operations }), expect.anything() ); }); @@ -432,9 +435,9 @@ describe('#bulkCreate', () => { it(`normalizes options.namespace from 'default' to undefined`, async () => { await bulkCreateSuccess(client, repository, [obj1, obj2], { namespace: 'default' }); const expected = expect.not.objectContaining({ namespace: 'default' }); - const body = [expect.any(Object), expected, expect.any(Object), expected]; + const operations = [expect.any(Object), expected, expect.any(Object), expected]; expect(client.bulk).toHaveBeenCalledWith( - expect.objectContaining({ body }), + expect.objectContaining({ operations }), expect.anything() ); }); @@ -446,9 +449,9 @@ describe('#bulkCreate', () => { ]; await bulkCreateSuccess(client, repository, objects, { namespace }); const expected = expect.not.objectContaining({ namespace: expect.anything() }); - const body = [expect.any(Object), expected, expect.any(Object), expected]; + const operations = [expect.any(Object), expected, expect.any(Object), expected]; expect(client.bulk).toHaveBeenCalledWith( - expect.objectContaining({ body }), + expect.objectContaining({ operations }), expect.anything() ); }); @@ -468,9 +471,9 @@ describe('#bulkCreate', () => { await bulkCreateSuccess(client, repository, objects, { namespace, overwrite: true }); const expected1 = expect.objectContaining({ namespaces: [namespace ?? 'default'] }); const expected2 = expect.objectContaining({ namespaces: ['*'] }); - const body = [expect.any(Object), expected1, expect.any(Object), expected2]; + const operations = [expect.any(Object), expected1, expect.any(Object), expected2]; expect(client.bulk).toHaveBeenCalledWith( - expect.objectContaining({ body }), + expect.objectContaining({ operations }), expect.anything() ); client.bulk.mockClear(); @@ -503,7 +506,7 @@ describe('#bulkCreate', () => { }, ]); await bulkCreateSuccess(client, repository, objects, { namespace, overwrite: true }); - const body = [ + const operations = [ { index: expect.objectContaining({ _id: `${ns2}:dashboard:${o1.id}` }) }, expect.objectContaining({ namespace: ns2 }), { @@ -525,7 +528,7 @@ describe('#bulkCreate', () => { }) ); expect(client.bulk).toHaveBeenCalledWith( - expect.objectContaining({ body }), + expect.objectContaining({ operations }), expect.anything() ); client.bulk.mockClear(); @@ -539,12 +542,12 @@ describe('#bulkCreate', () => { const test = async (namespace?: string) => { const objects = [{ ...obj1, type: 'dashboard', initialNamespaces: ['default'] }]; await bulkCreateSuccess(client, repository, objects, { namespace, overwrite: true }); - const body = [ + const operations = [ { index: expect.objectContaining({ _id: `dashboard:${obj1.id}` }) }, expect.not.objectContaining({ namespace: 'default' }), ]; expect(client.bulk).toHaveBeenCalledWith( - expect.objectContaining({ body }), + expect.objectContaining({ operations }), expect.anything() ); client.bulk.mockClear(); @@ -558,9 +561,9 @@ describe('#bulkCreate', () => { const objects = [obj1, { ...obj2, type: NAMESPACE_AGNOSTIC_TYPE }]; await bulkCreateSuccess(client, repository, objects, { namespace, overwrite: true }); const expected = expect.not.objectContaining({ namespaces: expect.anything() }); - const body = [expect.any(Object), expected, expect.any(Object), expected]; + const operations = [expect.any(Object), expected, expect.any(Object), expected]; expect(client.bulk).toHaveBeenCalledWith( - expect.objectContaining({ body }), + expect.objectContaining({ operations }), expect.anything() ); client.bulk.mockClear(); @@ -652,9 +655,9 @@ describe('#bulkCreate', () => { const result = await repository.bulkCreate(objects); expect(client.bulk).toHaveBeenCalled(); const objCall = isBulkError ? expectObjArgs(obj) : []; - const body = [...expectObjArgs(obj1), ...objCall, ...expectObjArgs(obj2)]; + const operations = [...expectObjArgs(obj1), ...objCall, ...expectObjArgs(obj2)]; expect(client.bulk).toHaveBeenCalledWith( - expect.objectContaining({ body }), + expect.objectContaining({ operations }), expect.anything() ); expect(result).toEqual({ @@ -765,7 +768,7 @@ describe('#bulkCreate', () => { ); expect(client.bulk).toHaveBeenCalled(); expect(client.bulk).toHaveBeenCalledWith( - expect.objectContaining({ body: [...expectObjArgs(o1), ...expectObjArgs(o5)] }), + expect.objectContaining({ operations: [...expectObjArgs(o1), ...expectObjArgs(o5)] }), expect.anything() ); expect(result).toEqual({ diff --git a/src/core/packages/saved-objects/api-server-internal/src/lib/apis/bulk_create.ts b/src/core/packages/saved-objects/api-server-internal/src/lib/apis/bulk_create.ts index 95e9c9acb7953..1a9b8956a3084 100644 --- a/src/core/packages/saved-objects/api-server-internal/src/lib/apis/bulk_create.ts +++ b/src/core/packages/saved-objects/api-server-internal/src/lib/apis/bulk_create.ts @@ -282,7 +282,7 @@ export const performBulkCreate = async ( ? await client.bulk({ refresh, require_alias: true, - body: bulkCreateParams, + operations: bulkCreateParams, }) : undefined; diff --git a/src/core/packages/saved-objects/api-server-internal/src/lib/apis/bulk_delete.test.ts b/src/core/packages/saved-objects/api-server-internal/src/lib/apis/bulk_delete.test.ts index d0efc5bf6be4f..a4c6f365529f1 100644 --- a/src/core/packages/saved-objects/api-server-internal/src/lib/apis/bulk_delete.test.ts +++ b/src/core/packages/saved-objects/api-server-internal/src/lib/apis/bulk_delete.test.ts @@ -18,7 +18,7 @@ import { } from '../repository.test.mock'; import type { Payload } from '@hapi/boom'; -import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import * as estypes from '@elastic/elasticsearch/lib/api/types'; import type { SavedObjectsBulkDeleteObject, @@ -131,9 +131,9 @@ describe('#bulkDelete', () => { overrides?: Record; } ) => { - const body = []; + const operations = []; for (const { type, id } of objects) { - body.push({ + operations.push({ [method]: { _index, _id: getId(type, id), @@ -143,7 +143,7 @@ describe('#bulkDelete', () => { } expect(client.bulk).toHaveBeenCalledWith( - expect.objectContaining({ body }), + expect.objectContaining({ operations }), expect.anything() ); }; @@ -202,9 +202,9 @@ describe('#bulkDelete', () => { overrides?: Record; } ) => { - const body = []; + const operations = []; for (const { type, id } of objects) { - body.push({ + operations.push({ [method]: { _index, _id: getId(type, id), @@ -213,7 +213,7 @@ describe('#bulkDelete', () => { }); } expect(client.bulk).toHaveBeenCalledWith( - expect.objectContaining({ body }), + expect.objectContaining({ operations }), expect.anything() ); }; @@ -266,7 +266,7 @@ describe('#bulkDelete', () => { expect.objectContaining({ _id: `${MULTI_NAMESPACE_ISOLATED_TYPE}:${obj2.id}` }), ]; expect(client.mget).toHaveBeenCalledWith( - expect.objectContaining({ body: { docs } }), + expect.objectContaining({ docs }), expect.anything() ); }); diff --git a/src/core/packages/saved-objects/api-server-internal/src/lib/apis/bulk_delete.ts b/src/core/packages/saved-objects/api-server-internal/src/lib/apis/bulk_delete.ts index e3fcff2dd1003..6495272fccdb0 100644 --- a/src/core/packages/saved-objects/api-server-internal/src/lib/apis/bulk_delete.ts +++ b/src/core/packages/saved-objects/api-server-internal/src/lib/apis/bulk_delete.ts @@ -141,7 +141,7 @@ export const performBulkDelete = async ( const bulkDeleteResponse = bulkDeleteParams.length ? await client.bulk({ refresh, - body: bulkDeleteParams, + operations: bulkDeleteParams, require_alias: true, }) : undefined; diff --git a/src/core/packages/saved-objects/api-server-internal/src/lib/apis/bulk_get.isolated.test.ts b/src/core/packages/saved-objects/api-server-internal/src/lib/apis/bulk_get.isolated.test.ts index 792bc652b0f6a..9c24ab6d68d4e 100644 --- a/src/core/packages/saved-objects/api-server-internal/src/lib/apis/bulk_get.isolated.test.ts +++ b/src/core/packages/saved-objects/api-server-internal/src/lib/apis/bulk_get.isolated.test.ts @@ -11,7 +11,7 @@ import { getSavedObjectFromSourceMock, rawDocExistsInNamespaceMock, } from './bulk_get.isolated.test.mocks'; -import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import * as estypes from '@elastic/elasticsearch/lib/api/types'; import { SavedObject, CheckAuthorizationResult } from '@kbn/core-saved-objects-server'; import { apiContextMock, ApiExecutionContextMock } from '../../mocks'; import { performBulkGet } from './bulk_get'; diff --git a/src/core/packages/saved-objects/api-server-internal/src/lib/apis/bulk_get.test.ts b/src/core/packages/saved-objects/api-server-internal/src/lib/apis/bulk_get.test.ts index 3e5310952c116..eff12c97379c8 100644 --- a/src/core/packages/saved-objects/api-server-internal/src/lib/apis/bulk_get.test.ts +++ b/src/core/packages/saved-objects/api-server-internal/src/lib/apis/bulk_get.test.ts @@ -15,7 +15,7 @@ import { } from '../repository.test.mock'; import type { Payload } from '@hapi/boom'; -import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import * as estypes from '@elastic/elasticsearch/lib/api/types'; import type { SavedObjectsBulkGetObject } from '@kbn/core-saved-objects-api-server'; import { type SavedObjectsRawDocSource, type SavedObject } from '@kbn/core-saved-objects-server'; @@ -149,14 +149,12 @@ describe('#bulkGet', () => { ) => { expect(client.mget).toHaveBeenCalledWith( expect.objectContaining({ - body: { - docs: objects.map(({ type, id }) => - expect.objectContaining({ - _index, - _id: getId(type, id), - }) - ), - }, + docs: objects.map(({ type, id }) => + expect.objectContaining({ + _index, + _id: getId(type, id), + }) + ), }), expect.anything() ); diff --git a/src/core/packages/saved-objects/api-server-internal/src/lib/apis/bulk_get.ts b/src/core/packages/saved-objects/api-server-internal/src/lib/apis/bulk_get.ts index 3cc6511adefa2..858b4830a7270 100644 --- a/src/core/packages/saved-objects/api-server-internal/src/lib/apis/bulk_get.ts +++ b/src/core/packages/saved-objects/api-server-internal/src/lib/apis/bulk_get.ts @@ -134,11 +134,7 @@ export const performBulkGet = async ( })); const bulkGetResponse = bulkGetDocs.length ? await client.mget( - { - body: { - docs: bulkGetDocs, - }, - }, + { docs: bulkGetDocs }, { ignore: [404], meta: true } ) : undefined; diff --git a/src/core/packages/saved-objects/api-server-internal/src/lib/apis/bulk_update.test.ts b/src/core/packages/saved-objects/api-server-internal/src/lib/apis/bulk_update.test.ts index 183c33e6f4079..411caec4d319b 100644 --- a/src/core/packages/saved-objects/api-server-internal/src/lib/apis/bulk_update.test.ts +++ b/src/core/packages/saved-objects/api-server-internal/src/lib/apis/bulk_update.test.ts @@ -17,7 +17,7 @@ import { } from '../repository.test.mock'; import type { Payload } from '@hapi/boom'; -import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import * as estypes from '@elastic/elasticsearch/lib/api/types'; import type { SavedObjectsBulkUpdateObject, @@ -164,13 +164,13 @@ describe('#bulkUpdate', () => { overrides?: Record; } ) => { - const body = []; + const operations = []; for (const object of objects) { - body.push(getBulkIndexEntry(method, object, _index, getId, overrides)); - body.push(expect.any(Object)); + operations.push(getBulkIndexEntry(method, object, _index, getId, overrides)); + operations.push(expect.any(Object)); } expect(client.bulk).toHaveBeenCalledWith( - expect.objectContaining({ body }), + expect.objectContaining({ operations }), expect.anything() ); }; @@ -214,7 +214,7 @@ describe('#bulkUpdate', () => { expect.objectContaining({ _id: `${MULTI_NAMESPACE_ISOLATED_TYPE}:${obj2.id}` }), ]; expect(client.mget).toHaveBeenCalledWith( - expect.objectContaining({ body: { docs } }), + expect.objectContaining({ docs }), expect.anything() ); }); @@ -239,7 +239,7 @@ describe('#bulkUpdate', () => { expect(client.bulk).toHaveBeenCalledTimes(1); expect(client.bulk).toHaveBeenCalledWith( expect.objectContaining({ - body: [ + operations: [ getBulkIndexEntry('index', _obj1), expect.objectContaining({ [obj1.type]: { @@ -267,7 +267,7 @@ describe('#bulkUpdate', () => { expect(client.bulk).toHaveBeenCalledTimes(1); expect(client.bulk).toHaveBeenCalledWith( expect.objectContaining({ - body: [ + operations: [ getBulkIndexEntry('index', _obj1), expect.objectContaining({ [obj1.type]: { @@ -288,12 +288,12 @@ describe('#bulkUpdate', () => { it(`defaults to no references`, async () => { await bulkUpdateSuccess(client, repository, registry, [obj1, obj2]); - const body = [ + const operations = [ ...expectObjArgs({ ...obj1, references: [] }), ...expectObjArgs({ ...obj2, references: [] }), ]; expect(client.bulk).toHaveBeenCalledWith( - expect.objectContaining({ body }), + expect.objectContaining({ operations }), expect.anything() ); }); @@ -302,12 +302,12 @@ describe('#bulkUpdate', () => { const test = async (references: SavedObjectReference[]) => { const objects = [obj1, obj2].map((obj) => ({ ...obj, references })); await bulkUpdateSuccess(client, repository, registry, objects); - const body = [ + const operations = [ ...expectObjArgs({ ...obj1, references }), ...expectObjArgs({ ...obj2, references }), ]; expect(client.bulk).toHaveBeenCalledWith( - expect.objectContaining({ body }), + expect.objectContaining({ operations }), expect.anything() ); client.bulk.mockClear(); @@ -322,12 +322,12 @@ describe('#bulkUpdate', () => { const test = async (references: unknown) => { const objects = [obj1, obj2]; await bulkUpdateSuccess(client, repository, registry, objects); - const body = [ + const operations = [ ...expectObjArgs({ ...obj1, references: expect.not.arrayContaining([references]) }), ...expectObjArgs({ ...obj2, references: expect.not.arrayContaining([references]) }), ]; expect(client.bulk).toHaveBeenCalledWith( - expect.objectContaining({ body }), + expect.objectContaining({ operations }), expect.anything() ); client.bulk.mockClear(); diff --git a/src/core/packages/saved-objects/api-server-internal/src/lib/apis/bulk_update.ts b/src/core/packages/saved-objects/api-server-internal/src/lib/apis/bulk_update.ts index b8515ac1bf9e1..4ca0635066884 100644 --- a/src/core/packages/saved-objects/api-server-internal/src/lib/apis/bulk_update.ts +++ b/src/core/packages/saved-objects/api-server-internal/src/lib/apis/bulk_update.ts @@ -168,7 +168,7 @@ export const performBulkUpdate = async ( const bulkGetResponse = bulkGetDocs.length ? await client.mget( - { body: { docs: bulkGetDocs } }, + { docs: bulkGetDocs }, { ignore: [404], meta: true } ) : undefined; @@ -344,7 +344,7 @@ export const performBulkUpdate = async ( const bulkUpdateResponse = bulkUpdateParams.length ? await client.bulk({ refresh, - body: bulkUpdateParams, + operations: bulkUpdateParams, _source_includes: ['originId'], require_alias: true, }) diff --git a/src/core/packages/saved-objects/api-server-internal/src/lib/apis/check_conflicts.test.ts b/src/core/packages/saved-objects/api-server-internal/src/lib/apis/check_conflicts.test.ts index 2858ae5288819..e67c6b623aa6a 100644 --- a/src/core/packages/saved-objects/api-server-internal/src/lib/apis/check_conflicts.test.ts +++ b/src/core/packages/saved-objects/api-server-internal/src/lib/apis/check_conflicts.test.ts @@ -13,7 +13,7 @@ import { mockGetSearchDsl, } from '../repository.test.mock'; -import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import * as estypes from '@elastic/elasticsearch/lib/api/types'; import { ALL_NAMESPACES_STRING } from '@kbn/core-saved-objects-utils-server'; import { SavedObjectsRepository } from '../repository'; @@ -100,14 +100,12 @@ describe('#checkConflicts', () => { ) => { expect(client.mget).toHaveBeenCalledWith( expect.objectContaining({ - body: { - docs: objects.map(({ type, id }) => - expect.objectContaining({ - _index, - _id: getId(type, id), - }) - ), - }, + docs: objects.map(({ type, id }) => + expect.objectContaining({ + _index, + _id: getId(type, id), + }) + ), }), expect.anything() ); diff --git a/src/core/packages/saved-objects/api-server-internal/src/lib/apis/check_conflicts.ts b/src/core/packages/saved-objects/api-server-internal/src/lib/apis/check_conflicts.ts index c85564b1a0eac..40cf6e590ddcf 100644 --- a/src/core/packages/saved-objects/api-server-internal/src/lib/apis/check_conflicts.ts +++ b/src/core/packages/saved-objects/api-server-internal/src/lib/apis/check_conflicts.ts @@ -85,11 +85,7 @@ export const performCheckConflicts = async ( })); const bulkGetResponse = bulkGetDocs.length ? await client.mget( - { - body: { - docs: bulkGetDocs, - }, - }, + { docs: bulkGetDocs }, { ignore: [404], meta: true } ) : undefined; diff --git a/src/core/packages/saved-objects/api-server-internal/src/lib/apis/create.test.ts b/src/core/packages/saved-objects/api-server-internal/src/lib/apis/create.test.ts index b1a3c951613d2..6473af396945e 100644 --- a/src/core/packages/saved-objects/api-server-internal/src/lib/apis/create.test.ts +++ b/src/core/packages/saved-objects/api-server-internal/src/lib/apis/create.test.ts @@ -16,7 +16,7 @@ import { mockGetSearchDsl, } from '../repository.test.mock'; -import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import * as estypes from '@elastic/elasticsearch/lib/api/types'; import type { SavedObjectsCreateOptions } from '@kbn/core-saved-objects-api-server'; import { @@ -234,8 +234,8 @@ describe('#create', () => { it(`defaults to empty references array`, async () => { await createSuccess(type, attributes, { id }); expect( - (client.create.mock.calls[0][0] as estypes.CreateRequest).body! - .references + (client.create.mock.calls[0][0] as estypes.CreateRequest) + .document!.references ).toEqual([]); }); @@ -244,7 +244,7 @@ describe('#create', () => { await createSuccess(type, attributes, { id, references }); expect( (client.create.mock.calls[0][0] as estypes.CreateRequest) - .body!.references + .document!.references ).toEqual(references); client.create.mockClear(); }; @@ -259,7 +259,7 @@ describe('#create', () => { await createSuccess(type, attributes, { id, references }); expect( (client.create.mock.calls[0][0] as estypes.CreateRequest) - .body!.references + .document!.references ).not.toBeDefined(); client.create.mockClear(); }; @@ -286,9 +286,7 @@ describe('#create', () => { it(`${objType} defaults to no originId`, async () => { await createSuccess(objType, attributes, { id }); expect(client.create).toHaveBeenCalledWith( - expect.objectContaining({ - body: expect.not.objectContaining({ originId: expect.anything() }), - }), + expect.not.objectContaining({ originId: expect.anything() }), expect.anything() ); }); @@ -310,7 +308,7 @@ describe('#create', () => { await createSuccess(objType, attributes, { id, originId: 'some-originId' }); expect(client.create).toHaveBeenCalledWith( expect.objectContaining({ - body: expect.objectContaining({ originId: 'some-originId' }), + document: expect.objectContaining({ originId: 'some-originId' }), }), expect.anything() ); @@ -321,7 +319,7 @@ describe('#create', () => { await createSuccess(objType, attributes, { id, originId: undefined }); expect(client.create).toHaveBeenCalledWith( expect.objectContaining({ - body: expect.not.objectContaining({ originId: expect.anything() }), + document: expect.not.objectContaining({ originId: expect.anything() }), }), expect.anything() ); @@ -331,7 +329,7 @@ describe('#create', () => { await createSuccess(objType, attributes, { id }); expect(client.create).toHaveBeenCalledWith( expect.objectContaining({ - body: expect.objectContaining({ originId: 'existing-originId' }), + document: expect.objectContaining({ originId: 'existing-originId' }), }), expect.anything() ); @@ -388,7 +386,7 @@ describe('#create', () => { expect(client.create).toHaveBeenCalledWith( expect.objectContaining({ id: `${namespace}:${type}:${id}`, - body: expect.objectContaining({ namespace }), + document: expect.objectContaining({ namespace }), }), expect.anything() ); @@ -399,7 +397,7 @@ describe('#create', () => { expect(client.create).toHaveBeenCalledWith( expect.objectContaining({ id: `${type}:${id}`, - body: expect.not.objectContaining({ namespace: expect.anything() }), + document: expect.not.objectContaining({ namespace: expect.anything() }), }), expect.anything() ); @@ -410,7 +408,7 @@ describe('#create', () => { expect(client.create).toHaveBeenCalledWith( expect.objectContaining({ id: `${type}:${id}`, - body: expect.not.objectContaining({ namespace: expect.anything() }), + document: expect.not.objectContaining({ namespace: expect.anything() }), }), expect.anything() ); @@ -457,7 +455,7 @@ describe('#create', () => { expect(client.create).toHaveBeenCalledWith( expect.objectContaining({ id: `${MULTI_NAMESPACE_TYPE}:${id}`, - body: expect.objectContaining({ namespaces: [namespace] }), + document: expect.objectContaining({ namespaces: [namespace] }), }), expect.anything() ); @@ -465,7 +463,7 @@ describe('#create', () => { expect(client.index).toHaveBeenCalledWith( expect.objectContaining({ id: `${MULTI_NAMESPACE_ISOLATED_TYPE}:${id}`, - body: expect.objectContaining({ namespaces: ['*'] }), + document: expect.objectContaining({ namespaces: ['*'] }), }), expect.anything() ); @@ -524,7 +522,7 @@ describe('#create', () => { 1, expect.objectContaining({ id: `${ns2}:dashboard:${id}`, - body: expect.objectContaining({ namespace: ns2 }), + document: expect.objectContaining({ namespace: ns2 }), }), expect.anything() ); @@ -532,7 +530,7 @@ describe('#create', () => { 2, expect.objectContaining({ id: `${MULTI_NAMESPACE_TYPE}:${id}`, - body: expect.objectContaining({ namespaces: [ns2, ns3] }), + document: expect.objectContaining({ namespaces: [ns2, ns3] }), }), expect.anything() ); @@ -540,7 +538,7 @@ describe('#create', () => { expect(client.index).toHaveBeenCalledWith( expect.objectContaining({ id: `${MULTI_NAMESPACE_ISOLATED_TYPE}:${id}`, - body: expect.objectContaining({ namespaces: [ns2] }), + document: expect.objectContaining({ namespaces: [ns2] }), }), expect.anything() ); @@ -558,7 +556,7 @@ describe('#create', () => { 1, expect.objectContaining({ id: `dashboard:${id}`, - body: expect.not.objectContaining({ namespace: 'default' }), + document: expect.not.objectContaining({ namespace: 'default' }), }), expect.anything() ); @@ -569,7 +567,7 @@ describe('#create', () => { expect(client.create).toHaveBeenCalledWith( expect.objectContaining({ id: `${NAMESPACE_AGNOSTIC_TYPE}:${id}`, - body: expect.not.objectContaining({ + document: expect.not.objectContaining({ namespace: expect.anything(), namespaces: expect.anything(), }), diff --git a/src/core/packages/saved-objects/api-server-internal/src/lib/apis/create.ts b/src/core/packages/saved-objects/api-server-internal/src/lib/apis/create.ts index 5fb22be52febd..4c7e0a17842f7 100644 --- a/src/core/packages/saved-objects/api-server-internal/src/lib/apis/create.ts +++ b/src/core/packages/saved-objects/api-server-internal/src/lib/apis/create.ts @@ -16,6 +16,7 @@ import { import { SavedObjectsUtils } from '@kbn/core-saved-objects-utils-server'; import { decodeRequestVersion } from '@kbn/core-saved-objects-base-server-internal'; import { SavedObjectsCreateOptions } from '@kbn/core-saved-objects-api-server'; +import { CreateRequest, type IndexRequest } from '@elastic/elasticsearch/lib/api/types'; import { DEFAULT_REFRESH_SETTING } from '../constants'; import type { PreflightCheckForCreateResult } from './internals/preflight_check_for_create'; import { getSavedObjectNamespaces, getCurrentTime, normalizeNamespace, setManaged } from './utils'; @@ -152,19 +153,19 @@ export const performCreate = async ( const raw = serializer.savedObjectToRaw(migrated as SavedObjectSanitizedDoc); - const requestParams = { + const requestParams: IndexRequest | CreateRequest = { id: raw._id, index: commonHelper.getIndexForType(type), refresh, - body: raw._source, + document: raw._source, ...(overwrite && version ? decodeRequestVersion(version) : {}), require_alias: true, }; const { body, statusCode, headers } = id && overwrite - ? await client.index(requestParams, { meta: true }) - : await client.create(requestParams, { meta: true }); + ? await client.index(requestParams as IndexRequest, { meta: true }) + : await client.create(requestParams as CreateRequest, { meta: true }); // throw if we can't verify a 404 response is from Elasticsearch if (isNotFoundFromUnsupportedServer({ statusCode, headers })) { diff --git a/src/core/packages/saved-objects/api-server-internal/src/lib/apis/delete.test.ts b/src/core/packages/saved-objects/api-server-internal/src/lib/apis/delete.test.ts index 1c501b1fb5ec5..d788c5edff65e 100644 --- a/src/core/packages/saved-objects/api-server-internal/src/lib/apis/delete.test.ts +++ b/src/core/packages/saved-objects/api-server-internal/src/lib/apis/delete.test.ts @@ -16,7 +16,7 @@ import { mockGetSearchDsl, } from '../repository.test.mock'; -import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import * as estypes from '@elastic/elasticsearch/lib/api/types'; import type { SavedObjectsDeleteOptions } from '@kbn/core-saved-objects-api-server'; import { ALL_NAMESPACES_STRING } from '@kbn/core-saved-objects-utils-server'; diff --git a/src/core/packages/saved-objects/api-server-internal/src/lib/apis/delete_by_namespace.ts b/src/core/packages/saved-objects/api-server-internal/src/lib/apis/delete_by_namespace.ts index 14b9143fa6225..fc8bed73802c6 100644 --- a/src/core/packages/saved-objects/api-server-internal/src/lib/apis/delete_by_namespace.ts +++ b/src/core/packages/saved-objects/api-server-internal/src/lib/apis/delete_by_namespace.ts @@ -49,28 +49,26 @@ export const performDeleteByNamespace = async ( { index: commonHelper.getIndicesForTypes(typesToUpdate), refresh: options.refresh, - body: { - script: { - source: ` - if (!ctx._source.containsKey('namespaces')) { - ctx.op = "delete"; - } else { - ctx._source['namespaces'].removeAll(Collections.singleton(params['namespace'])); - if (ctx._source['namespaces'].empty) { - ctx.op = "delete"; - } - } - `, - lang: 'painless', - params: { namespace }, - }, - conflicts: 'proceed', - ...getSearchDsl(mappings, registry, { - namespaces: [namespace], - type: typesToUpdate, - kueryNode, - }), + script: { + source: ` + if (!ctx._source.containsKey('namespaces')) { + ctx.op = "delete"; + } else { + ctx._source['namespaces'].removeAll(Collections.singleton(params['namespace'])); + if (ctx._source['namespaces'].empty) { + ctx.op = "delete"; + } + } + `, + lang: 'painless', + params: { namespace }, }, + conflicts: 'proceed', + ...(getSearchDsl(mappings, registry, { + namespaces: [namespace], + type: typesToUpdate, + kueryNode, + }) as Omit, 'sort'>), // Sort types don't match and we're not sorting anyways }, { ignore: [404], meta: true } ); diff --git a/src/core/packages/saved-objects/api-server-internal/src/lib/apis/find.isolated.test.ts b/src/core/packages/saved-objects/api-server-internal/src/lib/apis/find.isolated.test.ts index f92e074ec4a26..a3d5778083574 100644 --- a/src/core/packages/saved-objects/api-server-internal/src/lib/apis/find.isolated.test.ts +++ b/src/core/packages/saved-objects/api-server-internal/src/lib/apis/find.isolated.test.ts @@ -8,7 +8,7 @@ */ import { isSupportedEsServerMock } from './find.isolated.test.mocks'; -import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import * as estypes from '@elastic/elasticsearch/lib/api/types'; import { SavedObject, AuthorizationTypeMap } from '@kbn/core-saved-objects-server'; import { apiContextMock, ApiExecutionContextMock } from '../../mocks'; import { performFind } from './find'; diff --git a/src/core/packages/saved-objects/api-server-internal/src/lib/apis/find.test.ts b/src/core/packages/saved-objects/api-server-internal/src/lib/apis/find.test.ts index 12b14bb6f1a32..6c9cca176c4d6 100644 --- a/src/core/packages/saved-objects/api-server-internal/src/lib/apis/find.test.ts +++ b/src/core/packages/saved-objects/api-server-internal/src/lib/apis/find.test.ts @@ -106,9 +106,7 @@ describe('find', () => { await findSuccess(client, repository, { type }); expect(client.search).toHaveBeenCalledWith( - expect.objectContaining({ - body: expect.objectContaining({ ...query }), - }), + expect.objectContaining({ ...query }), expect.anything() ); }); @@ -138,24 +136,22 @@ describe('find', () => { await findSuccess(client, repository, { type, fields: ['title'] }); expect(client.search).toHaveBeenCalledWith( expect.objectContaining({ - body: expect.objectContaining({ - _source: [ - `${type}.title`, - 'namespace', - 'namespaces', - 'type', - 'references', - 'migrationVersion', - 'coreMigrationVersion', - 'typeMigrationVersion', - 'managed', - 'updated_at', - 'updated_by', - 'created_at', - 'created_by', - 'originId', - ], - }), + _source: [ + `${type}.title`, + 'namespace', + 'namespaces', + 'type', + 'references', + 'migrationVersion', + 'coreMigrationVersion', + 'typeMigrationVersion', + 'managed', + 'updated_at', + 'updated_by', + 'created_at', + 'created_by', + 'originId', + ], }), expect.anything() ); diff --git a/src/core/packages/saved-objects/api-server-internal/src/lib/apis/find.ts b/src/core/packages/saved-objects/api-server-internal/src/lib/apis/find.ts index 40501a0d80a37..ee3f98eade3d5 100644 --- a/src/core/packages/saved-objects/api-server-internal/src/lib/apis/find.ts +++ b/src/core/packages/saved-objects/api-server-internal/src/lib/apis/find.ts @@ -8,7 +8,7 @@ */ import Boom from '@hapi/boom'; -import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import * as estypes from '@elastic/elasticsearch/lib/api/types'; import { isSupportedEsServer } from '@kbn/core-elasticsearch-server-internal'; import { SavedObjectsErrorHelpers, @@ -191,31 +191,26 @@ export const performFind = async ( preference, rest_total_hits_as_int: true, size: perPage, - body: { - size: perPage, - seq_no_primary_term: true, - from: perPage * (page - 1), - _source: includedFields(allowedTypes, fields), - ...(aggsObject ? { aggs: aggsObject } : {}), - ...getSearchDsl(mappings, registry, { - search, - defaultSearchOperator, - searchFields, - pit, - rootSearchFields, - type: allowedTypes, - searchAfter, - sortField, - sortOrder, - namespaces, - typeToNamespacesMap, // If defined, this takes precedence over the `type` and `namespaces` fields - hasReference, - hasReferenceOperator, - hasNoReference, - hasNoReferenceOperator, - kueryNode, - }), - }, + seq_no_primary_term: true, + ...(aggsObject ? { aggs: aggsObject } : {}), + ...getSearchDsl(mappings, registry, { + search, + defaultSearchOperator, + searchFields, + pit, + rootSearchFields, + type: allowedTypes, + searchAfter, + sortField, + sortOrder, + namespaces, + typeToNamespacesMap, // If defined, this takes precedence over the `type` and `namespaces` fields + hasReference, + hasReferenceOperator, + hasNoReference, + hasNoReferenceOperator, + kueryNode, + }), }; const { body, statusCode, headers } = await client.search(esOptions, { diff --git a/src/core/packages/saved-objects/api-server-internal/src/lib/apis/get.test.ts b/src/core/packages/saved-objects/api-server-internal/src/lib/apis/get.test.ts index b04f7266b85f4..501e3b8a587cf 100644 --- a/src/core/packages/saved-objects/api-server-internal/src/lib/apis/get.test.ts +++ b/src/core/packages/saved-objects/api-server-internal/src/lib/apis/get.test.ts @@ -15,7 +15,7 @@ import { mockGetSearchDsl, } from '../repository.test.mock'; -import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import * as estypes from '@elastic/elasticsearch/lib/api/types'; import type { SavedObjectsBaseOptions } from '@kbn/core-saved-objects-api-server'; import { ALL_NAMESPACES_STRING } from '@kbn/core-saved-objects-utils-server'; diff --git a/src/core/packages/saved-objects/api-server-internal/src/lib/apis/helpers/preflight_check.ts b/src/core/packages/saved-objects/api-server-internal/src/lib/apis/helpers/preflight_check.ts index 73428ebcc8529..ab05a435a445b 100644 --- a/src/core/packages/saved-objects/api-server-internal/src/lib/apis/helpers/preflight_check.ts +++ b/src/core/packages/saved-objects/api-server-internal/src/lib/apis/helpers/preflight_check.ts @@ -88,10 +88,7 @@ export class PreflightCheckHelper { })); const bulkGetMultiNamespaceDocsResponse = bulkGetMultiNamespaceDocs.length - ? await this.client.mget( - { body: { docs: bulkGetMultiNamespaceDocs } }, - { ignore: [404], meta: true } - ) + ? await this.client.mget({ docs: bulkGetMultiNamespaceDocs }, { ignore: [404], meta: true }) : undefined; // fail fast if we can't verify a 404 response is from Elasticsearch if ( diff --git a/src/core/packages/saved-objects/api-server-internal/src/lib/apis/increment_counter.test.ts b/src/core/packages/saved-objects/api-server-internal/src/lib/apis/increment_counter.test.ts index b4062ecbe7f2b..88ce6fb865f57 100644 --- a/src/core/packages/saved-objects/api-server-internal/src/lib/apis/increment_counter.test.ts +++ b/src/core/packages/saved-objects/api-server-internal/src/lib/apis/increment_counter.test.ts @@ -16,7 +16,7 @@ import { mockGetSearchDsl, } from '../repository.test.mock'; -import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import * as estypes from '@elastic/elasticsearch/lib/api/types'; import type { SavedObjectsIncrementCounterField, @@ -222,19 +222,17 @@ describe('#incrementCounter', () => { await incrementCounterSuccess(type, id, counterFields, { namespace, upsertAttributes }); expect(client.update).toHaveBeenCalledWith( expect.objectContaining({ - body: expect.objectContaining({ - upsert: expect.objectContaining({ - [type]: { - foo: 'bar', - hello: 'dolly', - ...counterFields.reduce((aggs, field) => { - return { - ...aggs, - [field]: 1, - }; - }, {}), - }, - }), + upsert: expect.objectContaining({ + [type]: { + foo: 'bar', + hello: 'dolly', + ...counterFields.reduce((aggs, field) => { + return { + ...aggs, + [field]: 1, + }; + }, {}), + }, }), }), expect.anything() @@ -495,12 +493,10 @@ describe('#incrementCounter', () => { expect(client.update).toBeCalledTimes(1); expect(client.update).toBeCalledWith( expect.objectContaining({ - body: expect.objectContaining({ - script: expect.objectContaining({ - params: expect.objectContaining({ - counterFieldNames: [counterFields[0]], - counts: [3], - }), + script: expect.objectContaining({ + params: expect.objectContaining({ + counterFieldNames: [counterFields[0]], + counts: [3], }), }), }), @@ -514,12 +510,10 @@ describe('#incrementCounter', () => { expect(client.update).toBeCalledTimes(1); expect(client.update).toBeCalledWith( expect.objectContaining({ - body: expect.objectContaining({ - script: expect.objectContaining({ - params: expect.objectContaining({ - counterFieldNames: [counterFields[0]], - counts: [0], - }), + script: expect.objectContaining({ + params: expect.objectContaining({ + counterFieldNames: [counterFields[0]], + counts: [0], }), }), }), diff --git a/src/core/packages/saved-objects/api-server-internal/src/lib/apis/internals/collect_multi_namespace_references.test.ts b/src/core/packages/saved-objects/api-server-internal/src/lib/apis/internals/collect_multi_namespace_references.test.ts index 3d0ef40a2a22a..e43eeb570620a 100644 --- a/src/core/packages/saved-objects/api-server-internal/src/lib/apis/internals/collect_multi_namespace_references.test.ts +++ b/src/core/packages/saved-objects/api-server-internal/src/lib/apis/internals/collect_multi_namespace_references.test.ts @@ -134,7 +134,7 @@ describe('collectMultiNamespaceReferences', () => { ...objects: SavedObjectsCollectMultiNamespaceReferencesObject[] ) { const docs = objects.map(({ type, id }) => expect.objectContaining({ _id: `${type}:${id}` })); - expect(client.mget).toHaveBeenNthCalledWith(n, { body: { docs } }, expect.anything()); + expect(client.mget).toHaveBeenNthCalledWith(n, { docs }, expect.anything()); } it('returns an empty array if no object args are passed in', async () => { diff --git a/src/core/packages/saved-objects/api-server-internal/src/lib/apis/internals/collect_multi_namespace_references.ts b/src/core/packages/saved-objects/api-server-internal/src/lib/apis/internals/collect_multi_namespace_references.ts index 4b35b45b89bb9..f802b8a97bcfd 100644 --- a/src/core/packages/saved-objects/api-server-internal/src/lib/apis/internals/collect_multi_namespace_references.ts +++ b/src/core/packages/saved-objects/api-server-internal/src/lib/apis/internals/collect_multi_namespace_references.ts @@ -185,7 +185,7 @@ async function getObjectsAndReferences({ ); } const bulkGetResponse = await client.mget( - { body: { docs: makeBulkGetDocs(bulkGetObjects) } }, + { docs: makeBulkGetDocs(bulkGetObjects) }, { ignore: [404], meta: true } ); // exit early if we can't verify a 404 response is from Elasticsearch diff --git a/src/core/packages/saved-objects/api-server-internal/src/lib/apis/internals/delete_legacy_url_aliases.test.ts b/src/core/packages/saved-objects/api-server-internal/src/lib/apis/internals/delete_legacy_url_aliases.test.ts index 2334a5a5a3eeb..f4bba00e26968 100644 --- a/src/core/packages/saved-objects/api-server-internal/src/lib/apis/internals/delete_legacy_url_aliases.test.ts +++ b/src/core/packages/saved-objects/api-server-internal/src/lib/apis/internals/delete_legacy_url_aliases.test.ts @@ -86,14 +86,12 @@ describe('deleteLegacyUrlAliases', () => { expect(params.client.updateByQuery).toHaveBeenCalledTimes(1); expect(params.client.updateByQuery).toHaveBeenCalledWith( expect.objectContaining({ - body: expect.objectContaining({ - script: expect.objectContaining({ - params: { - namespaces, - matchTargetNamespaceOp: 'delete', - notMatchTargetNamespaceOp: 'noop', - }, - }), + script: expect.objectContaining({ + params: { + namespaces, + matchTargetNamespaceOp: 'delete', + notMatchTargetNamespaceOp: 'noop', + }, }), }), expect.anything() @@ -111,14 +109,12 @@ describe('deleteLegacyUrlAliases', () => { expect(params.client.updateByQuery).toHaveBeenCalledTimes(1); expect(params.client.updateByQuery).toHaveBeenCalledWith( expect.objectContaining({ - body: expect.objectContaining({ - script: expect.objectContaining({ - params: { - namespaces, - matchTargetNamespaceOp: 'noop', - notMatchTargetNamespaceOp: 'delete', - }, - }), + script: expect.objectContaining({ + params: { + namespaces, + matchTargetNamespaceOp: 'noop', + notMatchTargetNamespaceOp: 'delete', + }, }), }), expect.anything() diff --git a/src/core/packages/saved-objects/api-server-internal/src/lib/apis/internals/delete_legacy_url_aliases.ts b/src/core/packages/saved-objects/api-server-internal/src/lib/apis/internals/delete_legacy_url_aliases.ts index b56eaddd35583..4486051898da5 100644 --- a/src/core/packages/saved-objects/api-server-internal/src/lib/apis/internals/delete_legacy_url_aliases.ts +++ b/src/core/packages/saved-objects/api-server-internal/src/lib/apis/internals/delete_legacy_url_aliases.ts @@ -69,29 +69,27 @@ export async function deleteLegacyUrlAliases(params: DeleteLegacyUrlAliasesParam { index: getIndexForType(LEGACY_URL_ALIAS_TYPE), refresh: false, // This could be called many times in succession, intentionally do not wait for a refresh - body: { - ...getSearchDsl(mappings, registry, { - type: LEGACY_URL_ALIAS_TYPE, - kueryNode: createKueryNode(type, id), - }), - script: { - // Intentionally use one script source with variable params to take advantage of ES script caching - source: ` + ...(getSearchDsl(mappings, registry, { + type: LEGACY_URL_ALIAS_TYPE, + kueryNode: createKueryNode(type, id), + }) as Omit, 'sort'>), // Omitting sort in the types in this operation because types expect only string[] and we're not really sorting + script: { + // Intentionally use one script source with variable params to take advantage of ES script caching + source: ` if (params['namespaces'].indexOf(ctx._source['${LEGACY_URL_ALIAS_TYPE}']['targetNamespace']) > -1) { ctx.op = params['matchTargetNamespaceOp']; } else { ctx.op = params['notMatchTargetNamespaceOp']; } `, - params: { - namespaces, - matchTargetNamespaceOp: deleteBehavior === 'inclusive' ? 'delete' : 'noop', - notMatchTargetNamespaceOp: deleteBehavior === 'inclusive' ? 'noop' : 'delete', - }, - lang: 'painless', + params: { + namespaces, + matchTargetNamespaceOp: deleteBehavior === 'inclusive' ? 'delete' : 'noop', + notMatchTargetNamespaceOp: deleteBehavior === 'inclusive' ? 'noop' : 'delete', }, - conflicts: 'proceed', + lang: 'painless', }, + conflicts: 'proceed', }, { ignore: [404] } ); diff --git a/src/core/packages/saved-objects/api-server-internal/src/lib/apis/internals/increment_counter_internal.ts b/src/core/packages/saved-objects/api-server-internal/src/lib/apis/internals/increment_counter_internal.ts index 00a588fb4d006..23a7091aefad6 100644 --- a/src/core/packages/saved-objects/api-server-internal/src/lib/apis/internals/increment_counter_internal.ts +++ b/src/core/packages/saved-objects/api-server-internal/src/lib/apis/internals/increment_counter_internal.ts @@ -125,36 +125,34 @@ export const incrementCounterInternal = async ( refresh, require_alias: true, _source: true, - body: { - script: { - source: ` - for (int i = 0; i < params.counterFieldNames.length; i++) { - def counterFieldName = params.counterFieldNames[i]; - def count = params.counts[i]; - - if (ctx._source[params.type][counterFieldName] == null) { - ctx._source[params.type][counterFieldName] = count; - } - else { - ctx._source[params.type][counterFieldName] += count; - } - } - ctx._source.updated_at = params.time; - `, - lang: 'painless', - params: { - counts: normalizedCounterFields.map( - (normalizedCounterField) => normalizedCounterField.incrementBy - ), - counterFieldNames: normalizedCounterFields.map( - (normalizedCounterField) => normalizedCounterField.fieldName - ), - time, - type, - }, + script: { + source: ` + for (int i = 0; i < params.counterFieldNames.length; i++) { + def counterFieldName = params.counterFieldNames[i]; + def count = params.counts[i]; + + if (ctx._source[params.type][counterFieldName] == null) { + ctx._source[params.type][counterFieldName] = count; + } + else { + ctx._source[params.type][counterFieldName] += count; + } + } + ctx._source.updated_at = params.time; + `, + lang: 'painless', + params: { + counts: normalizedCounterFields.map( + (normalizedCounterField) => normalizedCounterField.incrementBy + ), + counterFieldNames: normalizedCounterFields.map( + (normalizedCounterField) => normalizedCounterField.fieldName + ), + time, + type, }, - upsert: raw._source, }, + upsert: raw._source, }); const { originId } = body.get?._source ?? {}; diff --git a/src/core/packages/saved-objects/api-server-internal/src/lib/apis/internals/internal_bulk_resolve.test.ts b/src/core/packages/saved-objects/api-server-internal/src/lib/apis/internals/internal_bulk_resolve.test.ts index 55141d196709c..527ef0a855b28 100644 --- a/src/core/packages/saved-objects/api-server-internal/src/lib/apis/internals/internal_bulk_resolve.test.ts +++ b/src/core/packages/saved-objects/api-server-internal/src/lib/apis/internals/internal_bulk_resolve.test.ts @@ -147,7 +147,7 @@ describe('internalBulkResolve', () => { expect(client.bulk).toHaveBeenCalledTimes(1); expect(client.bulk).toHaveBeenCalledWith( expect.objectContaining({ - body: aliasIds + operations: aliasIds .map((id) => [ { update: { @@ -169,12 +169,10 @@ describe('internalBulkResolve', () => { expect(client.mget).toHaveBeenCalledTimes(1); expect(client.mget).toHaveBeenCalledWith( { - body: { - docs: objectIds.map((id) => ({ - _id: serializer.generateRawId(normalizedNamespace, OBJ_TYPE, id), - _index: `index-for-${OBJ_TYPE}`, - })), - }, + docs: objectIds.map((id) => ({ + _id: serializer.generateRawId(normalizedNamespace, OBJ_TYPE, id), + _index: `index-for-${OBJ_TYPE}`, + })), }, expect.anything() ); diff --git a/src/core/packages/saved-objects/api-server-internal/src/lib/apis/internals/internal_bulk_resolve.ts b/src/core/packages/saved-objects/api-server-internal/src/lib/apis/internals/internal_bulk_resolve.ts index 208056be6fc92..b76fe9402d20b 100644 --- a/src/core/packages/saved-objects/api-server-internal/src/lib/apis/internals/internal_bulk_resolve.ts +++ b/src/core/packages/saved-objects/api-server-internal/src/lib/apis/internals/internal_bulk_resolve.ts @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import type { MgetResponseItem } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import type { MgetResponseItem } from '@elastic/elasticsearch/lib/api/types'; import { isNotFoundFromUnsupportedServer } from '@kbn/core-elasticsearch-server-internal'; import type { @@ -141,7 +141,7 @@ export async function internalBulkResolve( const bulkGetResponse = docsToBulkGet.length ? await client.mget( - { body: { docs: docsToBulkGet } }, + { docs: docsToBulkGet }, { ignore: [404], meta: true } ) : undefined; @@ -330,7 +330,7 @@ async function fetchAndUpdateAliases( const bulkUpdateResponse = await client.bulk({ refresh: false, require_alias: true, - body: bulkUpdateDocs, + operations: bulkUpdateDocs, }); return bulkUpdateResponse.items.map((item) => { // Map the bulk update response to the `_source` fields that were returned for each document diff --git a/src/core/packages/saved-objects/api-server-internal/src/lib/apis/internals/preflight_check_for_create.test.ts b/src/core/packages/saved-objects/api-server-internal/src/lib/apis/internals/preflight_check_for_create.test.ts index ba355acd01c5f..e16ef3aacacf0 100644 --- a/src/core/packages/saved-objects/api-server-internal/src/lib/apis/internals/preflight_check_for_create.test.ts +++ b/src/core/packages/saved-objects/api-server-internal/src/lib/apis/internals/preflight_check_for_create.test.ts @@ -66,7 +66,7 @@ describe('preflightCheckForCreate', () => { return found ? { // @ts-expect-error - _id: params!.body!.docs![i]._id as string, // needed for mockRawDocExistsInNamespaces mock implementation and existingDocument assertions + _id: params!.docs![i]._id, // needed for mockRawDocExistsInNamespaces mock implementation and existingDocument assertions _index: 'doesnt-matter', _source: { ...(disabled !== undefined && { [LEGACY_URL_ALIAS_TYPE]: { disabled } }), @@ -86,7 +86,7 @@ describe('preflightCheckForCreate', () => { /** Asserts that mget is called for the given raw object IDs */ function expectMgetArgs(...rawObjectIds: string[]) { const docs = rawObjectIds.map((_id) => expect.objectContaining({ _id })); - expect(client.mget).toHaveBeenCalledWith({ body: { docs } }, expect.anything()); + expect(client.mget).toHaveBeenCalledWith({ docs }, expect.anything()); } /** Asserts that findLegacyUrlAliases is called for the given objects */ diff --git a/src/core/packages/saved-objects/api-server-internal/src/lib/apis/internals/preflight_check_for_create.ts b/src/core/packages/saved-objects/api-server-internal/src/lib/apis/internals/preflight_check_for_create.ts index 341c65e722dd5..4bae59efefdca 100644 --- a/src/core/packages/saved-objects/api-server-internal/src/lib/apis/internals/preflight_check_for_create.ts +++ b/src/core/packages/saved-objects/api-server-internal/src/lib/apis/internals/preflight_check_for_create.ts @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import * as estypes from '@elastic/elasticsearch/lib/api/types'; import { isNotFoundFromUnsupportedServer } from '@kbn/core-elasticsearch-server-internal'; import { type ISavedObjectTypeRegistry, @@ -278,7 +278,7 @@ async function bulkGetObjectsAndAliases( const bulkGetResponse = docsToBulkGet.length ? await client.mget( - { body: { docs: docsToBulkGet } }, + { docs: docsToBulkGet }, { ignore: [404], meta: true } ) : undefined; diff --git a/src/core/packages/saved-objects/api-server-internal/src/lib/apis/internals/update_objects_spaces.test.ts b/src/core/packages/saved-objects/api-server-internal/src/lib/apis/internals/update_objects_spaces.test.ts index 537bd2db5e81c..426db23b6c57a 100644 --- a/src/core/packages/saved-objects/api-server-internal/src/lib/apis/internals/update_objects_spaces.test.ts +++ b/src/core/packages/saved-objects/api-server-internal/src/lib/apis/internals/update_objects_spaces.test.ts @@ -134,7 +134,7 @@ describe('#updateObjectsSpaces', () => { /** Asserts that mget is called for the given objects */ function expectMgetArgs(...objects: SavedObjectsUpdateObjectsSpacesObject[]) { const docs = objects.map(({ type, id }) => expect.objectContaining({ _id: `${type}:${id}` })); - expect(client.mget).toHaveBeenCalledWith({ body: { docs } }, expect.anything()); + expect(client.mget).toHaveBeenCalledWith({ docs }, expect.anything()); } /** Mocks the saved objects client so it returns the expected results */ @@ -153,14 +153,14 @@ describe('#updateObjectsSpaces', () => { }); } - /** Asserts that mget is called for the given objects */ + /** Asserts that bulk is called for the given objects */ function expectBulkArgs( ...objectActions: Array<{ object: { type: string; id: string; namespaces?: string[] }; action: 'update' | 'delete'; }> ) { - const body = objectActions.flatMap( + const operations = objectActions.flatMap( ({ object: { type, id, namespaces = expect.any(Array) }, action }) => { const operation = { [action]: { @@ -174,7 +174,7 @@ describe('#updateObjectsSpaces', () => { : [operation]; // 'delete' only uses an operation } ); - expect(client.bulk).toHaveBeenCalledWith(expect.objectContaining({ body })); + expect(client.bulk).toHaveBeenCalledWith(expect.objectContaining({ operations })); } beforeEach(() => { diff --git a/src/core/packages/saved-objects/api-server-internal/src/lib/apis/internals/update_objects_spaces.ts b/src/core/packages/saved-objects/api-server-internal/src/lib/apis/internals/update_objects_spaces.ts index e3e7fcf42bb88..747db5976f004 100644 --- a/src/core/packages/saved-objects/api-server-internal/src/lib/apis/internals/update_objects_spaces.ts +++ b/src/core/packages/saved-objects/api-server-internal/src/lib/apis/internals/update_objects_spaces.ts @@ -8,7 +8,7 @@ */ import pMap from 'p-map'; -import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import * as estypes from '@elastic/elasticsearch/lib/api/types'; import intersection from 'lodash/intersection'; import type { Logger } from '@kbn/logging'; @@ -155,7 +155,7 @@ export async function updateObjectsSpaces({ })); const bulkGetResponse = bulkGetDocs.length ? await client.mget( - { body: { docs: bulkGetDocs } }, + { docs: bulkGetDocs }, { ignore: [404], meta: true } ) : undefined; @@ -261,7 +261,7 @@ export async function updateObjectsSpaces({ const { refresh = DEFAULT_REFRESH_SETTING } = options; const bulkOperationResponse = bulkOperationParams.length - ? await client.bulk({ refresh, body: bulkOperationParams, require_alias: true }) + ? await client.bulk({ refresh, operations: bulkOperationParams, require_alias: true }) : undefined; // Delete aliases if necessary, ensuring we don't have too many concurrent operations running. diff --git a/src/core/packages/saved-objects/api-server-internal/src/lib/apis/open_point_in_time.test.ts b/src/core/packages/saved-objects/api-server-internal/src/lib/apis/open_point_in_time.test.ts index 2efc33eddddfd..bda6952138c41 100644 --- a/src/core/packages/saved-objects/api-server-internal/src/lib/apis/open_point_in_time.test.ts +++ b/src/core/packages/saved-objects/api-server-internal/src/lib/apis/open_point_in_time.test.ts @@ -189,9 +189,7 @@ describe('SavedObjectsRepository', () => { await successResponse('abc123'); expect(client.closePointInTime).toHaveBeenCalledWith( expect.objectContaining({ - body: expect.objectContaining({ - id: 'abc123', - }), + id: 'abc123', }), expect.anything() ); diff --git a/src/core/packages/saved-objects/api-server-internal/src/lib/apis/remove_references_to.test.ts b/src/core/packages/saved-objects/api-server-internal/src/lib/apis/remove_references_to.test.ts index e8dc5a4fe5fb1..cbc864adf0e39 100644 --- a/src/core/packages/saved-objects/api-server-internal/src/lib/apis/remove_references_to.test.ts +++ b/src/core/packages/saved-objects/api-server-internal/src/lib/apis/remove_references_to.test.ts @@ -15,7 +15,7 @@ import { mockGetSearchDsl, } from '../repository.test.mock'; -import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import * as estypes from '@elastic/elasticsearch/lib/api/types'; import { SavedObjectsRepository } from '../repository'; import { loggerMock } from '@kbn/logging-mocks'; @@ -106,9 +106,7 @@ describe('SavedObjectsRepository', () => { await removeReferencesToSuccess(client, repository, type, id, { type }); expect(client.updateByQuery).toHaveBeenCalledWith( - expect.objectContaining({ - body: expect.objectContaining({ ...query }), - }), + expect.objectContaining({ ...query }), expect.anything() ); }); @@ -139,13 +137,11 @@ describe('SavedObjectsRepository', () => { await removeReferencesToSuccess(client, repository, type, id); expect(client.updateByQuery).toHaveBeenCalledWith( expect.objectContaining({ - body: expect.objectContaining({ - script: expect.objectContaining({ - params: { - type, - id, - }, - }), + script: expect.objectContaining({ + params: { + type, + id, + }, }), }), expect.anything() @@ -270,11 +266,10 @@ describe('SavedObjectsRepository', () => { const client = apiExecutionContext.client; expect(client.updateByQuery).toHaveBeenCalledTimes(1); expect(client.updateByQuery).toHaveBeenLastCalledWith( - { + expect.objectContaining({ refresh: false, index: indices, - body: expect.any(Object), - }, + }), { ignore: [404], meta: true } ); }); diff --git a/src/core/packages/saved-objects/api-server-internal/src/lib/apis/remove_references_to.ts b/src/core/packages/saved-objects/api-server-internal/src/lib/apis/remove_references_to.ts index ec34343d9818c..0deb2db4a9df4 100644 --- a/src/core/packages/saved-objects/api-server-internal/src/lib/apis/remove_references_to.ts +++ b/src/core/packages/saved-objects/api-server-internal/src/lib/apis/remove_references_to.ts @@ -43,32 +43,30 @@ export const performRemoveReferencesTo = async ( { index: targetIndices, refresh, - body: { - script: { - source: ` - if (ctx._source.containsKey('references')) { - def items_to_remove = []; - for (item in ctx._source.references) { - if ( (item['type'] == params['type']) && (item['id'] == params['id']) ) { - items_to_remove.add(item); - } - } - ctx._source.references.removeAll(items_to_remove); + script: { + source: ` + if (ctx._source.containsKey('references')) { + def items_to_remove = []; + for (item in ctx._source.references) { + if ( (item['type'] == params['type']) && (item['id'] == params['id']) ) { + items_to_remove.add(item); } - `, - params: { - type, - id, - }, - lang: 'painless', + } + ctx._source.references.removeAll(items_to_remove); + } + `, + params: { + type, + id, }, - conflicts: 'proceed', - ...getSearchDsl(mappings, registry, { - namespaces: namespace ? [namespace] : undefined, - type: allTypes, - hasReference: { type, id }, - }), + lang: 'painless', }, + conflicts: 'proceed', + ...(getSearchDsl(mappings, registry, { + namespaces: namespace ? [namespace] : undefined, + type: allTypes, + hasReference: { type, id }, + }) as Omit, 'sort'>), // TS is complaining and it's unlikely that we sort here }, { ignore: [404], meta: true } ); diff --git a/src/core/packages/saved-objects/api-server-internal/src/lib/apis/update.test.ts b/src/core/packages/saved-objects/api-server-internal/src/lib/apis/update.test.ts index 728f8c42ce71b..7881947f58cc1 100644 --- a/src/core/packages/saved-objects/api-server-internal/src/lib/apis/update.test.ts +++ b/src/core/packages/saved-objects/api-server-internal/src/lib/apis/update.test.ts @@ -11,7 +11,7 @@ import { mockGetCurrentTime, mockPreflightCheckForCreate } from '../repository.test.mock'; -import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import * as estypes from '@elastic/elasticsearch/lib/api/types'; import { type SavedObjectUnsanitizedDoc, type SavedObjectReference, @@ -190,7 +190,7 @@ describe('#update', () => { expect(client.index).toHaveBeenCalledTimes(1); expect(client.index).toHaveBeenCalledWith( expect.objectContaining({ - body: expect.objectContaining({ + document: expect.objectContaining({ globalType: { foo: 'bar', title: 'Testing', @@ -219,7 +219,7 @@ describe('#update', () => { expect(client.index).toHaveBeenCalledTimes(1); expect(client.index).toHaveBeenCalledWith( expect.objectContaining({ - body: expect.objectContaining({ + document: expect.objectContaining({ globalType: { foo: 'bar', }, @@ -250,8 +250,8 @@ describe('#update', () => { migrator.migrateDocument.mockImplementationOnce((doc) => ({ ...doc })); await updateSuccess(client, repository, registry, type, id, attributes); expect( - (client.index.mock.calls[0][0] as estypes.CreateRequest).body! - .references + (client.index.mock.calls[0][0] as estypes.CreateRequest) + .document!.references ).toEqual([]); // we're indexing a full new doc, serializer adds default if not defined }); @@ -262,8 +262,8 @@ describe('#update', () => { references, }); expect( - (client.index.mock.calls[0][0] as estypes.CreateRequest).body! - .references + (client.index.mock.calls[0][0] as estypes.CreateRequest) + .document!.references ).toEqual(references); client.index.mockClear(); }; @@ -277,8 +277,8 @@ describe('#update', () => { references, }); expect( - (client.index.mock.calls[0][0] as estypes.CreateRequest).body! - .references + (client.index.mock.calls[0][0] as estypes.CreateRequest) + .document!.references ).toEqual(references); client.index.mockClear(); }; @@ -292,8 +292,8 @@ describe('#update', () => { references, }); expect( - (client.index.mock.calls[0][0] as estypes.CreateRequest).body! - .references + (client.index.mock.calls[0][0] as estypes.CreateRequest) + .document!.references ).toEqual(references); client.index.mockClear(); }; @@ -324,7 +324,8 @@ describe('#update', () => { ...mockTimestampFieldsWithCreated, }; expect( - (client.create.mock.calls[0][0] as estypes.CreateRequest).body! + (client.create.mock.calls[0][0] as estypes.CreateRequest) + .document! ).toEqual(expected); }); @@ -357,7 +358,8 @@ describe('#update', () => { ...mockTimestampFieldsWithCreated, }; expect( - (client.create.mock.calls[0][0] as estypes.CreateRequest).body! + (client.create.mock.calls[0][0] as estypes.CreateRequest) + .document! ).toEqual(expectedType); }); @@ -383,7 +385,7 @@ describe('#update', () => { index: '.kibana-test_8.0.0-testing', refresh: 'wait_for', require_alias: true, - body: expect.objectContaining({ + document: expect.objectContaining({ multiNamespaceIsolatedType: { title: 'Testing' }, namespaces: ['default'], references: [], @@ -403,8 +405,8 @@ describe('#update', () => { references, }); expect( - (client.index.mock.calls[0][0] as estypes.CreateRequest).body! - .references + (client.index.mock.calls[0][0] as estypes.CreateRequest) + .document!.references ).toEqual([]); client.index.mockClear(); client.create.mockClear(); diff --git a/src/core/packages/saved-objects/api-server-internal/src/lib/apis/update.ts b/src/core/packages/saved-objects/api-server-internal/src/lib/apis/update.ts index 2fdad31c2936d..552885bfd51b9 100644 --- a/src/core/packages/saved-objects/api-server-internal/src/lib/apis/update.ts +++ b/src/core/packages/saved-objects/api-server-internal/src/lib/apis/update.ts @@ -22,6 +22,7 @@ import type { SavedObjectsUpdateResponse, } from '@kbn/core-saved-objects-api-server'; import { isNotFoundFromUnsupportedServer } from '@kbn/core-elasticsearch-server-internal'; +import type { CreateRequest, IndexRequest } from '@elastic/elasticsearch/lib/api/types'; import { DEFAULT_REFRESH_SETTING, DEFAULT_RETRY_COUNT } from '../constants'; import { isValidRequest } from '../utils'; import { getCurrentTime, getSavedObjectFromSource, mergeForUpdate } from './utils'; @@ -187,12 +188,13 @@ export const executeUpdate = async ( validationHelper.validateObjectForCreate(type, migratedUpsert); const rawUpsert = serializer.savedObjectToRaw(migratedUpsert); - const createRequestParams = { + const createRequestParams: CreateRequest = { id: rawUpsert._id, index: commonHelper.getIndexForType(type), refresh, - body: rawUpsert._source, + document: rawUpsert._source, ...(version ? decodeRequestVersion(version) : {}), + // @ts-expect-error require_alias: true, }; @@ -289,11 +291,11 @@ export const executeUpdate = async ( ); // implement creating the call params - const indexRequestParams = { + const indexRequestParams: IndexRequest = { id: docToSend._id, index: commonHelper.getIndexForType(type), refresh, - body: docToSend._source, + document: docToSend._source, // using version from the source doc if not provided as option to avoid erasing changes in case of concurrent calls ...decodeRequestVersion(version || migrated!.version), require_alias: true, diff --git a/src/core/packages/saved-objects/api-server-internal/src/lib/apis/utils/es_responses.ts b/src/core/packages/saved-objects/api-server-internal/src/lib/apis/utils/es_responses.ts index 70979081bc960..0079d276bbe29 100644 --- a/src/core/packages/saved-objects/api-server-internal/src/lib/apis/utils/es_responses.ts +++ b/src/core/packages/saved-objects/api-server-internal/src/lib/apis/utils/es_responses.ts @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import * as estypes from '@elastic/elasticsearch/lib/api/types'; /** * Type and type guard function for converting a possibly not existent doc to an existent doc. diff --git a/src/core/packages/saved-objects/api-server-internal/src/lib/apis/utils/internal_utils.ts b/src/core/packages/saved-objects/api-server-internal/src/lib/apis/utils/internal_utils.ts index fed582f5c43d9..411d452b1a82f 100644 --- a/src/core/packages/saved-objects/api-server-internal/src/lib/apis/utils/internal_utils.ts +++ b/src/core/packages/saved-objects/api-server-internal/src/lib/apis/utils/internal_utils.ts @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import * as estypes from '@elastic/elasticsearch/lib/api/types'; import type { Payload } from '@hapi/boom'; import { SavedObjectsErrorHelpers, diff --git a/src/core/packages/saved-objects/api-server-internal/src/lib/apis/utils/merge_for_update.ts b/src/core/packages/saved-objects/api-server-internal/src/lib/apis/utils/merge_for_update.ts index 909ac19a18d6c..a930b0841b3df 100644 --- a/src/core/packages/saved-objects/api-server-internal/src/lib/apis/utils/merge_for_update.ts +++ b/src/core/packages/saved-objects/api-server-internal/src/lib/apis/utils/merge_for_update.ts @@ -9,7 +9,7 @@ import { isPlainObject } from 'lodash'; import { set } from '@kbn/safer-lodash-set'; -import type { MappingProperty as EsMappingProperty } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import type { MappingProperty as EsMappingProperty } from '@elastic/elasticsearch/lib/api/types'; import type { SavedObjectsTypeMappingDefinition, SavedObjectsFieldMapping, diff --git a/src/core/packages/saved-objects/api-server-internal/src/lib/point_in_time_finder.ts b/src/core/packages/saved-objects/api-server-internal/src/lib/point_in_time_finder.ts index cce8ca83dc5d9..7342690a4024a 100644 --- a/src/core/packages/saved-objects/api-server-internal/src/lib/point_in_time_finder.ts +++ b/src/core/packages/saved-objects/api-server-internal/src/lib/point_in_time_finder.ts @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import * as estypes from '@elastic/elasticsearch/lib/api/types'; import type { Logger } from '@kbn/logging'; import type { SavedObjectsFindOptions, diff --git a/src/core/packages/saved-objects/api-server-internal/src/lib/repository.encryption_extension.test.ts b/src/core/packages/saved-objects/api-server-internal/src/lib/repository.encryption_extension.test.ts index cf66621565577..5c1a9ecfce2a6 100644 --- a/src/core/packages/saved-objects/api-server-internal/src/lib/repository.encryption_extension.test.ts +++ b/src/core/packages/saved-objects/api-server-internal/src/lib/repository.encryption_extension.test.ts @@ -14,7 +14,7 @@ import { mockGetSearchDsl, } from './repository.test.mock'; -import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import * as estypes from '@elastic/elasticsearch/lib/api/types'; import { SavedObjectsRepository } from './repository'; import { loggerMock } from '@kbn/logging-mocks'; @@ -440,14 +440,12 @@ describe('SavedObjectsRepository Encryption Extension', () => { ) => { expect(client.mget).toHaveBeenCalledWith( expect.objectContaining({ - body: { - docs: objects.map(({ type, id }) => - expect.objectContaining({ - _index, - _id: getId(type, id), - }) - ), - }, + docs: objects.map(({ type, id }) => + expect.objectContaining({ + _index, + _id: getId(type, id), + }) + ), }), expect.anything() ); diff --git a/src/core/packages/saved-objects/api-server-internal/src/lib/repository.spaces_extension.test.ts b/src/core/packages/saved-objects/api-server-internal/src/lib/repository.spaces_extension.test.ts index d91effdb90f19..c880baadb5eed 100644 --- a/src/core/packages/saved-objects/api-server-internal/src/lib/repository.spaces_extension.test.ts +++ b/src/core/packages/saved-objects/api-server-internal/src/lib/repository.spaces_extension.test.ts @@ -18,7 +18,7 @@ import { mockDeleteLegacyUrlAliases, } from './repository.test.mock'; -import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import * as estypes from '@elastic/elasticsearch/lib/api/types'; import { SavedObjectsRepository } from './repository'; import { loggerMock } from '@kbn/logging-mocks'; @@ -234,7 +234,7 @@ describe('SavedObjectsRepository Spaces Extension', () => { id: `${ currentSpace.expectedNamespace ? `${currentSpace.expectedNamespace}:` : '' }${type}:${id}`, - body: expect.objectContaining( + document: expect.objectContaining( currentSpace.expectedNamespace ? { namespace: currentSpace.expectedNamespace, @@ -274,7 +274,7 @@ describe('SavedObjectsRepository Spaces Extension', () => { expect(client.create).toHaveBeenCalledWith( expect.objectContaining({ id: expect.stringMatching(regex), - body: expect.objectContaining( + document: expect.objectContaining( currentSpace.expectedNamespace ? { namespace: currentSpace.expectedNamespace, @@ -379,18 +379,16 @@ describe('SavedObjectsRepository Spaces Extension', () => { expect(client.mget).toHaveBeenCalledTimes(1); expect(client.mget).toHaveBeenCalledWith( expect.objectContaining({ - body: expect.objectContaining({ - docs: expect.arrayContaining([ - expect.objectContaining({ - _id: `${ - currentSpace.expectedNamespace ? `${currentSpace.expectedNamespace}:` : '' - }${obj1.type}:${obj1.id}`, - }), - expect.objectContaining({ - _id: `${obj2.type}:${obj2.id}`, - }), - ]), - }), + docs: expect.arrayContaining([ + expect.objectContaining({ + _id: `${ + currentSpace.expectedNamespace ? `${currentSpace.expectedNamespace}:` : '' + }${obj1.type}:${obj1.id}`, + }), + expect.objectContaining({ + _id: `${obj2.type}:${obj2.id}`, + }), + ]), }), { ignore: [404], meta: true } ); @@ -570,18 +568,16 @@ describe('SavedObjectsRepository Spaces Extension', () => { expect(client.mget).toHaveBeenCalledTimes(1); expect(client.mget).toHaveBeenCalledWith( expect.objectContaining({ - body: expect.objectContaining({ - docs: expect.arrayContaining([ - expect.objectContaining({ - _id: `${ - currentSpace.expectedNamespace ? `${currentSpace.expectedNamespace}:` : '' - }${obj1.type}:${obj1.id}`, - }), - expect.objectContaining({ - _id: `${obj2.type}:${obj2.id}`, - }), - ]), - }), + docs: expect.arrayContaining([ + expect.objectContaining({ + _id: `${ + currentSpace.expectedNamespace ? `${currentSpace.expectedNamespace}:` : '' + }${obj1.type}:${obj1.id}`, + }), + expect.objectContaining({ + _id: `${obj2.type}:${obj2.id}`, + }), + ]), }), { ignore: [404], meta: true } ); @@ -638,7 +634,7 @@ describe('SavedObjectsRepository Spaces Extension', () => { expect(client.bulk).toHaveBeenCalledTimes(1); expect(client.bulk).toHaveBeenCalledWith( expect.objectContaining({ - body: expect.arrayContaining([ + operations: expect.arrayContaining([ expect.objectContaining({ create: expect.objectContaining({ _id: `${ @@ -696,7 +692,7 @@ describe('SavedObjectsRepository Spaces Extension', () => { expect(client.bulk).toHaveBeenCalledTimes(1); expect(client.bulk).toHaveBeenCalledWith( expect.objectContaining({ - body: expect.arrayContaining([ + operations: expect.arrayContaining([ expect.objectContaining({ index: expect.objectContaining({ _id: `${ @@ -855,7 +851,7 @@ describe('SavedObjectsRepository Spaces Extension', () => { expect(client.bulk).toHaveBeenCalledTimes(1); expect(client.bulk).toHaveBeenCalledWith( expect.objectContaining({ - body: expect.arrayContaining([ + operations: expect.arrayContaining([ expect.objectContaining({ delete: expect.objectContaining({ _id: `${ diff --git a/src/core/packages/saved-objects/api-server-internal/src/lib/repository.ts b/src/core/packages/saved-objects/api-server-internal/src/lib/repository.ts index b2b8de1b4192a..926a36a3bc207 100644 --- a/src/core/packages/saved-objects/api-server-internal/src/lib/repository.ts +++ b/src/core/packages/saved-objects/api-server-internal/src/lib/repository.ts @@ -537,9 +537,7 @@ export class SavedObjectsRepository implements ISavedObjectsRepository { this.extensions.securityExtension.auditClosePointInTime(); } - return await this.client.closePointInTime({ - body: { id }, - }); + return await this.client.closePointInTime({ id }); } /** diff --git a/src/core/packages/saved-objects/api-server-internal/src/lib/repository_es_client.test.ts b/src/core/packages/saved-objects/api-server-internal/src/lib/repository_es_client.test.ts index b8f2ee6e41e3d..d271777f52294 100644 --- a/src/core/packages/saved-objects/api-server-internal/src/lib/repository_es_client.test.ts +++ b/src/core/packages/saved-objects/api-server-internal/src/lib/repository_es_client.test.ts @@ -25,19 +25,19 @@ describe('RepositoryEsClient', () => { it('delegates call to ES client method', async () => { expect(repositoryClient.bulk).toStrictEqual(expect.any(Function)); - await repositoryClient.bulk({ body: [] }); + await repositoryClient.bulk({ operations: [] }); expect(client.bulk).toHaveBeenCalledTimes(1); }); it('wraps a method call in retryCallCluster', async () => { - await repositoryClient.bulk({ body: [] }); + await repositoryClient.bulk({ operations: [] }); expect(retryCallClusterMock).toHaveBeenCalledTimes(1); }); it('keeps call options unchanged', async () => { expect(repositoryClient.bulk).toStrictEqual(expect.any(Function)); const options = { maxRetries: 12 }; - await repositoryClient.bulk({ body: [] }, options); + await repositoryClient.bulk({ operations: [] }, options); expect(client.bulk).toHaveBeenCalledWith(expect.any(Object), options); }); @@ -45,7 +45,7 @@ describe('RepositoryEsClient', () => { expect.assertions(1); client.bulk.mockRejectedValue(new Error('reason')); try { - await repositoryClient.bulk({ body: [] }); + await repositoryClient.bulk({ operations: [] }); } catch (e) { expect(SavedObjectsErrorHelpers.isSavedObjectsClientError(e)).toBe(true); } diff --git a/src/core/packages/saved-objects/api-server-internal/src/lib/search/aggregations/validation.test.ts b/src/core/packages/saved-objects/api-server-internal/src/lib/search/aggregations/validation.test.ts index f7337ba2ca5dd..79cfd0c582458 100644 --- a/src/core/packages/saved-objects/api-server-internal/src/lib/search/aggregations/validation.test.ts +++ b/src/core/packages/saved-objects/api-server-internal/src/lib/search/aggregations/validation.test.ts @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import * as estypes from '@elastic/elasticsearch/lib/api/types'; import { validateAndConvertAggregations } from './validation'; type AggsMap = Record; diff --git a/src/core/packages/saved-objects/api-server-internal/src/lib/search/aggregations/validation.ts b/src/core/packages/saved-objects/api-server-internal/src/lib/search/aggregations/validation.ts index 2345261284519..3ee6d7077499a 100644 --- a/src/core/packages/saved-objects/api-server-internal/src/lib/search/aggregations/validation.ts +++ b/src/core/packages/saved-objects/api-server-internal/src/lib/search/aggregations/validation.ts @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import * as estypes from '@elastic/elasticsearch/lib/api/types'; import { ObjectType } from '@kbn/config-schema'; import { isPlainObject, isArray } from 'lodash'; diff --git a/src/core/packages/saved-objects/api-server-internal/src/lib/search/search_dsl/search_dsl.ts b/src/core/packages/saved-objects/api-server-internal/src/lib/search/search_dsl/search_dsl.ts index 639798aa27ec8..90bb8ae96fb8f 100644 --- a/src/core/packages/saved-objects/api-server-internal/src/lib/search/search_dsl/search_dsl.ts +++ b/src/core/packages/saved-objects/api-server-internal/src/lib/search/search_dsl/search_dsl.ts @@ -9,7 +9,7 @@ import Boom from '@hapi/boom'; -import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import * as estypes from '@elastic/elasticsearch/lib/api/types'; import type { SavedObjectsPitParams } from '@kbn/core-saved-objects-api-server'; import type { ISavedObjectTypeRegistry } from '@kbn/core-saved-objects-server'; import type { IndexMapping } from '@kbn/core-saved-objects-base-server-internal'; diff --git a/src/core/packages/saved-objects/api-server-internal/src/lib/search/search_dsl/sorting_params.ts b/src/core/packages/saved-objects/api-server-internal/src/lib/search/search_dsl/sorting_params.ts index d64c290da0569..c62f12dac8002 100644 --- a/src/core/packages/saved-objects/api-server-internal/src/lib/search/search_dsl/sorting_params.ts +++ b/src/core/packages/saved-objects/api-server-internal/src/lib/search/search_dsl/sorting_params.ts @@ -8,7 +8,7 @@ */ import Boom from '@hapi/boom'; -import type { SortOrder, SortCombinations } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import type { SortOrder, SortCombinations } from '@elastic/elasticsearch/lib/api/types'; import type { SavedObjectsPitParams } from '@kbn/core-saved-objects-api-server/src/apis'; import { getProperty, type IndexMapping } from '@kbn/core-saved-objects-base-server-internal'; diff --git a/src/core/packages/saved-objects/api-server-internal/src/test_helpers/repository.test.common.ts b/src/core/packages/saved-objects/api-server-internal/src/test_helpers/repository.test.common.ts index 5670062c69ef2..25be3695db92f 100644 --- a/src/core/packages/saved-objects/api-server-internal/src/test_helpers/repository.test.common.ts +++ b/src/core/packages/saved-objects/api-server-internal/src/test_helpers/repository.test.common.ts @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import * as estypes from '@elastic/elasticsearch/lib/api/types'; import { schema } from '@kbn/config-schema'; import { loggerMock } from '@kbn/logging-mocks'; import type { Payload } from 'elastic-apm-node'; diff --git a/src/core/packages/saved-objects/api-server/src/apis/find.ts b/src/core/packages/saved-objects/api-server/src/apis/find.ts index 83bd08ba8b9c7..6d70566c7a8ad 100644 --- a/src/core/packages/saved-objects/api-server/src/apis/find.ts +++ b/src/core/packages/saved-objects/api-server/src/apis/find.ts @@ -11,7 +11,7 @@ import type { SortOrder, AggregationsAggregationContainer, SortResults, -} from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +} from '@elastic/elasticsearch/lib/api/types'; import type { SavedObject } from '../..'; type KueryNode = any; diff --git a/src/core/packages/saved-objects/base-server-internal/src/utils/get_field_list.ts b/src/core/packages/saved-objects/base-server-internal/src/utils/get_field_list.ts index 1e3c29bb24a56..a0f07c300d271 100644 --- a/src/core/packages/saved-objects/base-server-internal/src/utils/get_field_list.ts +++ b/src/core/packages/saved-objects/base-server-internal/src/utils/get_field_list.ts @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { MappingProperty as EsMappingProperty } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { MappingProperty as EsMappingProperty } from '@elastic/elasticsearch/lib/api/types'; import type { SavedObjectsTypeMappingDefinition, SavedObjectsFieldMapping, diff --git a/src/core/packages/saved-objects/migration-server-internal/src/actions/bulk_overwrite_transformed_documents.ts b/src/core/packages/saved-objects/migration-server-internal/src/actions/bulk_overwrite_transformed_documents.ts index b2222c562f9ad..fa5ab8e09b3d6 100644 --- a/src/core/packages/saved-objects/migration-server-internal/src/actions/bulk_overwrite_transformed_documents.ts +++ b/src/core/packages/saved-objects/migration-server-internal/src/actions/bulk_overwrite_transformed_documents.ts @@ -9,7 +9,7 @@ import * as Either from 'fp-ts/lib/Either'; import * as TaskEither from 'fp-ts/lib/TaskEither'; -import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import type * as estypes from '@elastic/elasticsearch/lib/api/types'; import { errors as esErrors } from '@elastic/elasticsearch'; import type { ElasticsearchClient } from '@kbn/core-elasticsearch-server'; import { diff --git a/src/core/packages/saved-objects/migration-server-internal/src/actions/create_index.ts b/src/core/packages/saved-objects/migration-server-internal/src/actions/create_index.ts index 60d3eb2aba762..5895f6b1cca0a 100644 --- a/src/core/packages/saved-objects/migration-server-internal/src/actions/create_index.ts +++ b/src/core/packages/saved-objects/migration-server-internal/src/actions/create_index.ts @@ -10,7 +10,7 @@ import * as Either from 'fp-ts/lib/Either'; import * as TaskEither from 'fp-ts/lib/TaskEither'; import { pipe } from 'fp-ts/lib/function'; -import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import * as estypes from '@elastic/elasticsearch/lib/api/types'; import type { ElasticsearchClient, ElasticsearchCapabilities, diff --git a/src/core/packages/saved-objects/migration-server-internal/src/actions/read_with_pit.ts b/src/core/packages/saved-objects/migration-server-internal/src/actions/read_with_pit.ts index d22f05919aeb4..283786a4d90f5 100644 --- a/src/core/packages/saved-objects/migration-server-internal/src/actions/read_with_pit.ts +++ b/src/core/packages/saved-objects/migration-server-internal/src/actions/read_with_pit.ts @@ -9,7 +9,7 @@ import * as Either from 'fp-ts/lib/Either'; import * as TaskEither from 'fp-ts/lib/TaskEither'; -import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import * as estypes from '@elastic/elasticsearch/lib/api/types'; import { errors as EsErrors } from '@elastic/elasticsearch'; import type { ElasticsearchClient } from '@kbn/core-elasticsearch-server'; import type { SavedObjectsRawDoc } from '@kbn/core-saved-objects-server'; diff --git a/src/core/packages/saved-objects/server/src/mapping_definition.ts b/src/core/packages/saved-objects/server/src/mapping_definition.ts index 1eee67f537095..d238d2298fc3e 100644 --- a/src/core/packages/saved-objects/server/src/mapping_definition.ts +++ b/src/core/packages/saved-objects/server/src/mapping_definition.ts @@ -11,7 +11,7 @@ import type { PropertyName as EsPropertyName, MappingProperty as EsMappingProperty, MappingPropertyBase as EsMappingPropertyBase, -} from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +} from '@elastic/elasticsearch/lib/api/types'; /** * Describe a saved object type mapping. diff --git a/src/core/packages/saved-objects/server/src/saved_objects_type.ts b/src/core/packages/saved-objects/server/src/saved_objects_type.ts index cb2930f5d4676..dee93ada50cb3 100644 --- a/src/core/packages/saved-objects/server/src/saved_objects_type.ts +++ b/src/core/packages/saved-objects/server/src/saved_objects_type.ts @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; import type { MaybePromise } from '@kbn/utility-types'; import type { ElasticsearchClient } from '@kbn/core-elasticsearch-server'; import type { SavedObjectsNamespaceType } from '@kbn/core-saved-objects-common'; diff --git a/src/core/packages/usage-data/server-internal/src/core_usage_data_service.ts b/src/core/packages/usage-data/server-internal/src/core_usage_data_service.ts index 7ac7cbb7fbb57..c970bd4abfa6d 100644 --- a/src/core/packages/usage-data/server-internal/src/core_usage_data_service.ts +++ b/src/core/packages/usage-data/server-internal/src/core_usage_data_service.ts @@ -16,7 +16,7 @@ import type { AggregationsMultiBucketAggregateBase, AggregationsSingleBucketAggregateBase, SearchTotalHits, -} from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +} from '@elastic/elasticsearch/lib/api/types'; import type { CoreContext, CoreService } from '@kbn/core-base-server-internal'; import type { LoggingConfigType } from '@kbn/core-logging-server-internal'; import type { Logger } from '@kbn/logging'; diff --git a/src/core/server/integration_tests/saved_objects/migrations/group3/actions/es_errors.test.ts b/src/core/server/integration_tests/saved_objects/migrations/group3/actions/es_errors.test.ts index 108d92fc9a12a..a0d725324d9a1 100644 --- a/src/core/server/integration_tests/saved_objects/migrations/group3/actions/es_errors.test.ts +++ b/src/core/server/integration_tests/saved_objects/migrations/group3/actions/es_errors.test.ts @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import type * as estypes from '@elastic/elasticsearch/lib/api/types'; import { InternalCoreStart } from '@kbn/core-lifecycle-server-internal'; import { Root } from '@kbn/core-root-server-internal'; import { elasticsearchServiceMock } from '@kbn/core-elasticsearch-server-mocks'; diff --git a/src/platform/packages/shared/kbn-es-types/index.ts b/src/platform/packages/shared/kbn-es-types/index.ts index d4ba23840e2a6..683fddb541baf 100644 --- a/src/platform/packages/shared/kbn-es-types/index.ts +++ b/src/platform/packages/shared/kbn-es-types/index.ts @@ -13,7 +13,6 @@ export type { SearchHit, ESSearchResponse, ESSearchRequest, - ESSearchRequestWithoutBody, ESSourceOptions, InferSearchResponseOf, AggregationResultOf, diff --git a/src/platform/packages/shared/kbn-es-types/src/index.ts b/src/platform/packages/shared/kbn-es-types/src/index.ts index 77d02320b6f2d..354a26169c328 100644 --- a/src/platform/packages/shared/kbn-es-types/src/index.ts +++ b/src/platform/packages/shared/kbn-es-types/src/index.ts @@ -7,12 +7,10 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import * as estypesWithoutBody from '@elastic/elasticsearch/lib/api/types'; -import type { - Field, - QueryDslFieldAndFormat, -} from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import * as estypes from '@elastic/elasticsearch/lib/api/types'; +// TODO: Remove when all usages have been migrated to non-body +import { SearchRequest as SearchRequestWithBodyKey } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import type { Field, QueryDslFieldAndFormat } from '@elastic/elasticsearch/lib/api/types'; import { InferSearchResponseOf, AggregateOf as AggregationResultOf, @@ -26,8 +24,8 @@ import { } from './search'; export type ESFilter = estypes.QueryDslQueryContainer; -export type ESSearchRequest = estypes.SearchRequest; -export type ESSearchRequestWithoutBody = estypesWithoutBody.SearchRequest; +// For now, we also accept with body to unblock the migration to without body. +export type ESSearchRequest = estypes.SearchRequest | SearchRequestWithBodyKey; export type AggregationOptionsByType = Required; // Typings for Elasticsearch queries and aggregations. These are intended to be diff --git a/src/platform/packages/shared/kbn-es-types/src/search.ts b/src/platform/packages/shared/kbn-es-types/src/search.ts index 1c9a9e16fd4a7..e02b8a4a5e843 100644 --- a/src/platform/packages/shared/kbn-es-types/src/search.ts +++ b/src/platform/packages/shared/kbn-es-types/src/search.ts @@ -8,8 +8,9 @@ */ import type { ValuesType, UnionToIntersection } from 'utility-types'; -import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import * as estypesWithoutBodyKey from '@elastic/elasticsearch/lib/api/types'; +import * as estypes from '@elastic/elasticsearch/lib/api/types'; +// TODO: Remove when all usages have been migrated to non-body +import { SearchRequest as SearchRequestWithBodyKey } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; interface AggregationsAggregationContainer extends Record { aggs?: any; @@ -61,7 +62,7 @@ type ValueTypeOfField = T extends Record type MaybeArray = T | T[]; -type Fields = Required['body']>['fields']; +type Fields = Required['fields']; type DocValueFields = MaybeArray; export type ChangePointType = @@ -635,8 +636,8 @@ type WrapAggregationResponse = keyof UnionToIntersection extends never export type InferSearchResponseOf< TDocument = unknown, TSearchRequest extends - | estypes.SearchRequest - | (estypesWithoutBodyKey.SearchRequest & { body?: never }) = estypes.SearchRequest, + | (estypes.SearchRequest & { body?: never }) // the union is necessary for the check 4 lines below + | SearchRequestWithBodyKey = estypes.SearchRequest, TOptions extends { restTotalHitsAsInt?: boolean } = {} > = Omit, 'aggregations' | 'hits'> & (TSearchRequest['body'] extends TopLevelAggregationRequest @@ -656,7 +657,7 @@ export type InferSearchResponseOf< }; }) & { hits: HitsOf< - TSearchRequest extends estypes.SearchRequest ? TSearchRequest['body'] : TSearchRequest, + TSearchRequest extends SearchRequestWithBodyKey ? TSearchRequest['body'] : TSearchRequest, TDocument >; }; @@ -690,5 +691,5 @@ export interface ESQLSearchParams { locale?: string; include_ccs_metadata?: boolean; dropNullColumns?: boolean; - params?: estypesWithoutBodyKey.ScalarValue[] | Array>; + params?: estypes.ScalarValue[] | Array>; } diff --git a/src/platform/plugins/shared/telemetry/server/telemetry_collection/get_local_stats.test.ts b/src/platform/plugins/shared/telemetry/server/telemetry_collection/get_local_stats.test.ts index d07743c2fc1aa..17a984841bb38 100644 --- a/src/platform/plugins/shared/telemetry/server/telemetry_collection/get_local_stats.test.ts +++ b/src/platform/plugins/shared/telemetry/server/telemetry_collection/get_local_stats.test.ts @@ -8,7 +8,7 @@ */ import { merge, omit } from 'lodash'; -import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import type * as estypes from '@elastic/elasticsearch/lib/api/types'; import { getLocalStats, handleLocalStats } from './get_local_stats'; import { diff --git a/src/platform/plugins/shared/telemetry/server/telemetry_collection/get_local_stats.ts b/src/platform/plugins/shared/telemetry/server/telemetry_collection/get_local_stats.ts index a0d54d043d11c..df3fc3d154ede 100644 --- a/src/platform/plugins/shared/telemetry/server/telemetry_collection/get_local_stats.ts +++ b/src/platform/plugins/shared/telemetry/server/telemetry_collection/get_local_stats.ts @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import type * as estypes from '@elastic/elasticsearch/lib/api/types'; import { StatsGetter, StatsCollectionContext, diff --git a/src/platform/plugins/shared/telemetry/server/telemetry_collection/get_nodes_usage.ts b/src/platform/plugins/shared/telemetry/server/telemetry_collection/get_nodes_usage.ts index 4fc70b95532d3..aaa27dd1ee98a 100644 --- a/src/platform/plugins/shared/telemetry/server/telemetry_collection/get_nodes_usage.ts +++ b/src/platform/plugins/shared/telemetry/server/telemetry_collection/get_nodes_usage.ts @@ -8,7 +8,7 @@ */ import type { ElasticsearchClient } from '@kbn/core/server'; -import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import type * as estypes from '@elastic/elasticsearch/lib/api/types'; import { TIMEOUT } from './constants'; /** diff --git a/x-pack/platform/plugins/private/telemetry_collection_xpack/server/telemetry_collection/get_license.ts b/x-pack/platform/plugins/private/telemetry_collection_xpack/server/telemetry_collection/get_license.ts index 11f2b1359ab13..4a13dd72f8cef 100644 --- a/x-pack/platform/plugins/private/telemetry_collection_xpack/server/telemetry_collection/get_license.ts +++ b/x-pack/platform/plugins/private/telemetry_collection_xpack/server/telemetry_collection/get_license.ts @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import type * as estypes from '@elastic/elasticsearch/lib/api/types'; import { ElasticsearchClient } from '@kbn/core/server'; export type ESLicense = estypes.LicenseGetLicenseInformation; diff --git a/x-pack/platform/plugins/private/telemetry_collection_xpack/server/telemetry_collection/get_stats_with_xpack.test.ts b/x-pack/platform/plugins/private/telemetry_collection_xpack/server/telemetry_collection/get_stats_with_xpack.test.ts index f2adb257f9cb6..436acb643d8a4 100644 --- a/x-pack/platform/plugins/private/telemetry_collection_xpack/server/telemetry_collection/get_stats_with_xpack.test.ts +++ b/x-pack/platform/plugins/private/telemetry_collection_xpack/server/telemetry_collection/get_stats_with_xpack.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import type * as estypes from '@elastic/elasticsearch/lib/api/types'; import { coreMock, elasticsearchServiceMock } from '@kbn/core/server/mocks'; import { getStatsWithXpack } from './get_stats_with_xpack'; import { SavedObjectsClient } from '@kbn/core/server'; diff --git a/x-pack/platform/plugins/shared/licensing/server/license_fetcher.test.ts b/x-pack/platform/plugins/shared/licensing/server/license_fetcher.test.ts index 195c70c7c7c49..9c97489cf5c85 100644 --- a/x-pack/platform/plugins/shared/licensing/server/license_fetcher.test.ts +++ b/x-pack/platform/plugins/shared/licensing/server/license_fetcher.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import type * as estypes from '@elastic/elasticsearch/lib/api/types'; import { getLicenseFetcher } from './license_fetcher'; import { loggerMock, type MockedLogger } from '@kbn/logging-mocks'; import { elasticsearchServiceMock } from '@kbn/core/server/mocks'; diff --git a/x-pack/platform/plugins/shared/licensing/server/license_fetcher.ts b/x-pack/platform/plugins/shared/licensing/server/license_fetcher.ts index 278142e5c39b2..6357fb7170a4c 100644 --- a/x-pack/platform/plugins/shared/licensing/server/license_fetcher.ts +++ b/x-pack/platform/plugins/shared/licensing/server/license_fetcher.ts @@ -5,7 +5,7 @@ * 2.0. */ -import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import type * as estypes from '@elastic/elasticsearch/lib/api/types'; import { createHash } from 'crypto'; import pRetry from 'p-retry'; import stringify from 'json-stable-stringify'; diff --git a/x-pack/platform/plugins/shared/licensing/server/plugin.test.ts b/x-pack/platform/plugins/shared/licensing/server/plugin.test.ts index d46d9e675f6d3..87190d8450692 100644 --- a/x-pack/platform/plugins/shared/licensing/server/plugin.test.ts +++ b/x-pack/platform/plugins/shared/licensing/server/plugin.test.ts @@ -7,7 +7,7 @@ import moment from 'moment'; import { BehaviorSubject, firstValueFrom, take, toArray } from 'rxjs'; -import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import type * as estypes from '@elastic/elasticsearch/lib/api/types'; import { ClusterClientMock, coreMock, diff --git a/x-pack/platform/plugins/shared/ml/public/application/services/forecast_service.ts b/x-pack/platform/plugins/shared/ml/public/application/services/forecast_service.ts index 51ddc3ef9a926..ddce5e0b69f44 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/services/forecast_service.ts +++ b/x-pack/platform/plugins/shared/ml/public/application/services/forecast_service.ts @@ -61,16 +61,13 @@ export function forecastServiceFactory(mlApi: MlApi) { mlApi.results .anomalySearch( { - // @ts-expect-error SearchRequest type has not been updated to include size size: maxResults, - body: { - query: { - bool: { - filter: filterCriteria, - }, + query: { + bool: { + filter: filterCriteria, }, - sort: [{ forecast_create_timestamp: { order: 'desc' } }], }, + sort: [{ forecast_create_timestamp: { order: 'desc' } }], }, [job.job_id] ) @@ -124,24 +121,21 @@ export function forecastServiceFactory(mlApi: MlApi) { mlApi.results .anomalySearch( { - // @ts-expect-error SearchRequest type has not been updated to include size size: 0, - body: { - query: { - bool: { - filter: filterCriteria, - }, + query: { + bool: { + filter: filterCriteria, }, - aggs: { - earliest: { - min: { - field: 'timestamp', - }, + }, + aggs: { + earliest: { + min: { + field: 'timestamp', }, - latest: { - max: { - field: 'timestamp', - }, + }, + latest: { + max: { + field: 'timestamp', }, }, }, @@ -264,36 +258,33 @@ export function forecastServiceFactory(mlApi: MlApi) { return mlApi.results .anomalySearch$( { - // @ts-expect-error SearchRequest type has not been updated to include size size: 0, - body: { - query: { - bool: { - filter: filterCriteria, - }, + query: { + bool: { + filter: filterCriteria, }, - aggs: { - times: { - date_histogram: { - field: 'timestamp', - fixed_interval: `${intervalMs}ms`, - min_doc_count: 1, - }, - aggs: { - prediction: { - [forecastAggs.avg]: { - field: 'forecast_prediction', - }, + }, + aggs: { + times: { + date_histogram: { + field: 'timestamp', + fixed_interval: `${intervalMs}ms`, + min_doc_count: 1, + }, + aggs: { + prediction: { + [forecastAggs.avg]: { + field: 'forecast_prediction', }, - forecastUpper: { - [forecastAggs.max]: { - field: 'forecast_upper', - }, + }, + forecastUpper: { + [forecastAggs.max]: { + field: 'forecast_upper', }, - forecastLower: { - [forecastAggs.min]: { - field: 'forecast_lower', - }, + }, + forecastLower: { + [forecastAggs.min]: { + field: 'forecast_lower', }, }, }, @@ -368,13 +359,10 @@ export function forecastServiceFactory(mlApi: MlApi) { mlApi.results .anomalySearch( { - // @ts-expect-error SearchRequest type has not been updated to include size size: 1, - body: { - query: { - bool: { - filter: filterCriteria, - }, + query: { + bool: { + filter: filterCriteria, }, }, }, diff --git a/x-pack/solutions/observability/plugins/apm/server/lib/helpers/create_es_client/create_entities_es_client/create_entities_es_client.ts b/x-pack/solutions/observability/plugins/apm/server/lib/helpers/create_es_client/create_entities_es_client/create_entities_es_client.ts index 108e738b15dd6..2055ce0fced68 100644 --- a/x-pack/solutions/observability/plugins/apm/server/lib/helpers/create_es_client/create_entities_es_client/create_entities_es_client.ts +++ b/x-pack/solutions/observability/plugins/apm/server/lib/helpers/create_es_client/create_entities_es_client/create_entities_es_client.ts @@ -5,7 +5,8 @@ * 2.0. */ -import type { ESSearchRequest, InferSearchResponseOf } from '@kbn/es-types'; +import type { SearchRequest as ESSearchRequest } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import type { InferSearchResponseOf } from '@kbn/es-types'; import type { KibanaRequest } from '@kbn/core/server'; import type { ElasticsearchClient } from '@kbn/core/server'; import { entitiesAliasPattern, ENTITY_LATEST, ENTITY_HISTORY } from '@kbn/entities-schema'; diff --git a/x-pack/solutions/observability/plugins/apm/server/lib/helpers/get_apm_alerts_client.ts b/x-pack/solutions/observability/plugins/apm/server/lib/helpers/get_apm_alerts_client.ts index b8a2e21316723..f0084151dfce0 100644 --- a/x-pack/solutions/observability/plugins/apm/server/lib/helpers/get_apm_alerts_client.ts +++ b/x-pack/solutions/observability/plugins/apm/server/lib/helpers/get_apm_alerts_client.ts @@ -6,7 +6,8 @@ */ import { isEmpty } from 'lodash'; -import type { ESSearchRequest, InferSearchResponseOf } from '@kbn/es-types'; +import type { SearchRequest as ESSearchRequest } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import type { InferSearchResponseOf } from '@kbn/es-types'; import type { ParsedTechnicalFields } from '@kbn/rule-registry-plugin/common'; import type { DataTier } from '@kbn/observability-shared-plugin/common'; import { searchExcludedDataTiers } from '@kbn/observability-plugin/common/ui_settings_keys'; diff --git a/x-pack/solutions/observability/plugins/apm/server/routes/alerts/alerting_es_client.ts b/x-pack/solutions/observability/plugins/apm/server/routes/alerts/alerting_es_client.ts index 4c27879a8985e..d590f808149a3 100644 --- a/x-pack/solutions/observability/plugins/apm/server/routes/alerts/alerting_es_client.ts +++ b/x-pack/solutions/observability/plugins/apm/server/routes/alerts/alerting_es_client.ts @@ -5,7 +5,8 @@ * 2.0. */ -import type { ESSearchRequest, ESSearchResponse } from '@kbn/es-types'; +import type { SearchRequest as ESSearchRequest } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import type { ESSearchResponse } from '@kbn/es-types'; import type { RuleExecutorServices } from '@kbn/alerting-plugin/server'; import type { IUiSettingsClient } from '@kbn/core/server'; import type { DataTier } from '@kbn/observability-shared-plugin/common'; diff --git a/x-pack/solutions/observability/plugins/apm/server/routes/assistant_functions/get_observability_alert_details_context/get_container_id_from_signals.ts b/x-pack/solutions/observability/plugins/apm/server/routes/assistant_functions/get_observability_alert_details_context/get_container_id_from_signals.ts index 24af8e677a01a..c7de1d41a863f 100644 --- a/x-pack/solutions/observability/plugins/apm/server/routes/assistant_functions/get_observability_alert_details_context/get_container_id_from_signals.ts +++ b/x-pack/solutions/observability/plugins/apm/server/routes/assistant_functions/get_observability_alert_details_context/get_container_id_from_signals.ts @@ -5,12 +5,12 @@ * 2.0. */ +import type { SearchRequest as ESSearchRequest } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import type { ElasticsearchClient } from '@kbn/core-elasticsearch-server'; import type { CoreRequestHandlerContext } from '@kbn/core-http-request-handler-context-server'; import { rangeQuery, typedSearch } from '@kbn/observability-plugin/server/utils/queries'; import type * as t from 'io-ts'; import moment from 'moment'; -import type { ESSearchRequest } from '@kbn/es-types'; import type { alertDetailsContextRt } from '@kbn/observability-plugin/server/services'; import type { LogSourcesService } from '@kbn/logs-data-access-plugin/common/types'; import { CONTAINER_ID } from '@kbn/apm-types'; diff --git a/x-pack/solutions/observability/plugins/apm/server/routes/assistant_functions/get_observability_alert_details_context/get_service_name_from_signals.ts b/x-pack/solutions/observability/plugins/apm/server/routes/assistant_functions/get_observability_alert_details_context/get_service_name_from_signals.ts index 220394df3b142..936c40c49fbc0 100644 --- a/x-pack/solutions/observability/plugins/apm/server/routes/assistant_functions/get_observability_alert_details_context/get_service_name_from_signals.ts +++ b/x-pack/solutions/observability/plugins/apm/server/routes/assistant_functions/get_observability_alert_details_context/get_service_name_from_signals.ts @@ -5,11 +5,11 @@ * 2.0. */ +import type { SearchRequest as ESSearchRequest } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import type { ElasticsearchClient } from '@kbn/core-elasticsearch-server'; import { rangeQuery, termQuery, typedSearch } from '@kbn/observability-plugin/server/utils/queries'; import type * as t from 'io-ts'; import moment from 'moment'; -import type { ESSearchRequest } from '@kbn/es-types'; import type { alertDetailsContextRt } from '@kbn/observability-plugin/server/services'; import type { LogSourcesService } from '@kbn/logs-data-access-plugin/common/types'; import { unflattenKnownApmEventFields } from '@kbn/apm-data-access-plugin/server/utils'; diff --git a/x-pack/solutions/observability/plugins/apm_data_access/server/lib/helpers/create_es_client/create_apm_event_client/index.ts b/x-pack/solutions/observability/plugins/apm_data_access/server/lib/helpers/create_es_client/create_apm_event_client/index.ts index 0d671447a1ce3..4132b9e277d46 100644 --- a/x-pack/solutions/observability/plugins/apm_data_access/server/lib/helpers/create_es_client/create_apm_event_client/index.ts +++ b/x-pack/solutions/observability/plugins/apm_data_access/server/lib/helpers/create_es_client/create_apm_event_client/index.ts @@ -14,8 +14,9 @@ import type { TermsEnumRequest, TermsEnumResponse, } from '@elastic/elasticsearch/lib/api/types'; +import type { SearchRequest as ESSearchRequest } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import type { ElasticsearchClient, KibanaRequest } from '@kbn/core/server'; -import type { ESSearchRequest, InferSearchResponseOf } from '@kbn/es-types'; +import type { InferSearchResponseOf } from '@kbn/es-types'; import { ProcessorEvent } from '@kbn/observability-plugin/common'; import { unwrapEsResponse } from '@kbn/observability-plugin/server'; import { compact, omit } from 'lodash'; diff --git a/x-pack/solutions/observability/plugins/infra/server/lib/helpers/get_infra_alerts_client.ts b/x-pack/solutions/observability/plugins/infra/server/lib/helpers/get_infra_alerts_client.ts index 9597bfe49326f..581ef994ea1a6 100644 --- a/x-pack/solutions/observability/plugins/infra/server/lib/helpers/get_infra_alerts_client.ts +++ b/x-pack/solutions/observability/plugins/infra/server/lib/helpers/get_infra_alerts_client.ts @@ -5,7 +5,8 @@ * 2.0. */ import { isEmpty } from 'lodash'; -import type { ESSearchRequest, InferSearchResponseOf } from '@kbn/es-types'; +import type { SearchRequest as ESSearchRequest } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import type { InferSearchResponseOf } from '@kbn/es-types'; import type { ParsedTechnicalFields } from '@kbn/rule-registry-plugin/common'; import type { KibanaRequest } from '@kbn/core/server'; import { OBSERVABILITY_RULE_TYPE_IDS } from '@kbn/rule-data-utils'; diff --git a/x-pack/solutions/observability/plugins/infra/server/lib/helpers/get_infra_metrics_client.ts b/x-pack/solutions/observability/plugins/infra/server/lib/helpers/get_infra_metrics_client.ts index 96ae89b902285..faba18b6d745d 100644 --- a/x-pack/solutions/observability/plugins/infra/server/lib/helpers/get_infra_metrics_client.ts +++ b/x-pack/solutions/observability/plugins/infra/server/lib/helpers/get_infra_metrics_client.ts @@ -4,7 +4,8 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import type { ESSearchRequest, InferSearchResponseOf } from '@kbn/es-types'; +import type { SearchRequest as ESSearchRequest } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import type { InferSearchResponseOf } from '@kbn/es-types'; import type { KibanaRequest } from '@kbn/core/server'; import { searchExcludedDataTiers } from '@kbn/observability-plugin/common/ui_settings_keys'; import type { DataTier } from '@kbn/observability-shared-plugin/common'; diff --git a/x-pack/solutions/observability/plugins/inventory/server/lib/create_alerts_client/create_alerts_client.ts b/x-pack/solutions/observability/plugins/inventory/server/lib/create_alerts_client/create_alerts_client.ts index 0b33606e22798..06e43526e58f1 100644 --- a/x-pack/solutions/observability/plugins/inventory/server/lib/create_alerts_client/create_alerts_client.ts +++ b/x-pack/solutions/observability/plugins/inventory/server/lib/create_alerts_client/create_alerts_client.ts @@ -6,7 +6,8 @@ */ import { isEmpty } from 'lodash'; -import type { ESSearchRequest, InferSearchResponseOf } from '@kbn/es-types'; +import type { SearchRequest as ESSearchRequest } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import type { InferSearchResponseOf } from '@kbn/es-types'; import type { ParsedTechnicalFields } from '@kbn/rule-registry-plugin/common'; import { OBSERVABILITY_RULE_TYPE_IDS } from '@kbn/rule-data-utils'; import type { InventoryRouteHandlerResources } from '../../routes/types'; diff --git a/x-pack/solutions/observability/plugins/investigate_app/server/clients/create_entities_es_client.ts b/x-pack/solutions/observability/plugins/investigate_app/server/clients/create_entities_es_client.ts index c11cd3eb9bc02..6eb1beaf0ede6 100644 --- a/x-pack/solutions/observability/plugins/investigate_app/server/clients/create_entities_es_client.ts +++ b/x-pack/solutions/observability/plugins/investigate_app/server/clients/create_entities_es_client.ts @@ -5,7 +5,8 @@ * 2.0. */ -import { ESSearchRequest, InferSearchResponseOf } from '@kbn/es-types'; +import { SearchRequest as ESSearchRequest } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { InferSearchResponseOf } from '@kbn/es-types'; import type { KibanaRequest } from '@kbn/core/server'; import { ElasticsearchClient } from '@kbn/core/server'; import { entitiesAliasPattern, ENTITY_LATEST } from '@kbn/entities-schema'; diff --git a/x-pack/solutions/observability/plugins/investigate_app/server/services/get_alerts_client.ts b/x-pack/solutions/observability/plugins/investigate_app/server/services/get_alerts_client.ts index c5eaf3f4c3d8e..eae37e54c5648 100644 --- a/x-pack/solutions/observability/plugins/investigate_app/server/services/get_alerts_client.ts +++ b/x-pack/solutions/observability/plugins/investigate_app/server/services/get_alerts_client.ts @@ -6,7 +6,8 @@ */ import { isEmpty } from 'lodash'; -import { ESSearchRequest, InferSearchResponseOf } from '@kbn/es-types'; +import { SearchRequest as ESSearchRequest } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { InferSearchResponseOf } from '@kbn/es-types'; import { ParsedTechnicalFields } from '@kbn/rule-registry-plugin/common'; import { OBSERVABILITY_RULE_TYPE_IDS } from '@kbn/rule-data-utils'; import { InvestigateAppRouteHandlerResources } from '../routes/types'; diff --git a/x-pack/test/accessibility/apps/group3/upgrade_assistant.ts b/x-pack/test/accessibility/apps/group3/upgrade_assistant.ts index 02f823ef6a674..63f1f468a47b6 100644 --- a/x-pack/test/accessibility/apps/group3/upgrade_assistant.ts +++ b/x-pack/test/accessibility/apps/group3/upgrade_assistant.ts @@ -10,36 +10,32 @@ * valid deprecations */ -import type { IndicesCreateRequest } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import type { IndicesCreateRequest } from '@elastic/elasticsearch/lib/api/types'; import { FtrProviderContext } from '../../ftr_provider_context'; const translogSettingsIndexDeprecation: IndicesCreateRequest = { index: 'deprecated_settings', - body: { - settings: { - 'translog.retention.size': '1b', - 'translog.retention.age': '5m', - 'index.soft_deletes.enabled': true, - }, + settings: { + 'translog.retention.size': '1b', + 'translog.retention.age': '5m', + 'index.soft_deletes.enabled': true, }, }; const multiFieldsIndexDeprecation: IndicesCreateRequest = { index: 'nested_multi_fields', - body: { - mappings: { - properties: { - text: { - type: 'text', - fields: { - english: { - type: 'text', - analyzer: 'english', - fields: { - english: { - type: 'text', - analyzer: 'english', - }, + mappings: { + properties: { + text: { + type: 'text', + fields: { + english: { + type: 'text', + analyzer: 'english', + fields: { + english: { + type: 'text', + analyzer: 'english', }, }, }, From 07ab93fc6b654f463b0be7d1c8ea7011de80c3b4 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Sat, 11 Jan 2025 01:45:57 +1100 Subject: [PATCH 20/42] Unauthorized route migration for routes owned by appex-sharedux (#198328) ### Authz API migration for unauthorized routes This PR migrates unauthorized routes owned by your team to a new security configuration. Please refer to the documentation for more information: [Authorization API](https://docs.elastic.dev/kibana-dev-docs/key-concepts/security-api-authorization) ### **Before migration:** ```ts router.get({ path: '/api/path', ... }, handler); ``` ### **After migration:** ```ts router.get({ path: '/api/path', security: { authz: { enabled: false, reason: 'This route is opted out from authorization because ...', }, }, ... }, handler); ``` ### What to do next? 1. Review the changes in this PR. 2. Elaborate on the reasoning to opt-out of authorization. 3. Routes without a compelling reason to opt-out of authorization should plan to introduce them as soon as possible. 2. You might need to update your tests to reflect the new security configuration: - If you have snapshot tests that include the route definition. ## Any questions? If you have any questions or need help with API authorization, please reach out to the `@elastic/kibana-security` team. --------- Co-authored-by: Eyo Okon Eyo --- .../server/rpc/routes/routes.ts | 7 ++ .../server/routes/config_routes.ts | 7 ++ .../server/routes/guide_state_routes.ts | 7 ++ .../server/routes/plugin_state_routes.ts | 14 +++ .../services/sample_data/routes/install.ts | 7 ++ .../services/sample_data/routes/list.ts | 87 +++++++++++-------- .../services/sample_data/routes/uninstall.ts | 7 ++ .../services/tutorials/tutorials_registry.ts | 12 ++- .../http/short_urls/register_create_route.ts | 7 ++ .../http/short_urls/register_delete_route.ts | 7 ++ .../http/short_urls/register_get_route.ts | 7 ++ .../http/short_urls/register_resolve_route.ts | 7 ++ .../private/banners/server/routes/info.ts | 6 ++ .../custom_branding/server/routes/info.ts | 7 ++ .../internal/deprecations/deprecations.ts | 12 +++ .../routes/internal/diagnostic/browser.ts | 6 ++ .../generate/generate_from_jobparams.ts | 12 ++- .../server/routes/internal/management/jobs.ts | 30 +++++++ .../routes/public/generate_from_jobparams.ts | 18 +++- .../reporting/server/routes/public/jobs.ts | 12 +++ .../global_search/server/routes/find.ts | 6 ++ .../server/routes/get_searchable_types.ts | 6 ++ .../assignments/find_assignable_objects.ts | 7 ++ .../assignments/get_assignable_types.ts | 7 ++ .../assignments/update_tags_assignments.ts | 7 ++ .../server/routes/internal/bulk_delete.ts | 7 ++ .../server/routes/internal/find_tags.ts | 7 ++ .../server/routes/tags/create_tag.ts | 7 ++ .../server/routes/tags/delete_tag.ts | 7 ++ .../server/routes/tags/get_all_tags.ts | 7 ++ .../server/routes/tags/get_tag.ts | 7 ++ .../server/routes/tags/update_tag.ts | 7 ++ .../shared/serverless/server/plugin.ts | 7 ++ 33 files changed, 326 insertions(+), 42 deletions(-) diff --git a/src/platform/plugins/shared/content_management/server/rpc/routes/routes.ts b/src/platform/plugins/shared/content_management/server/rpc/routes/routes.ts index 36bfb42f7cccc..021fd224c7fe6 100644 --- a/src/platform/plugins/shared/content_management/server/rpc/routes/routes.ts +++ b/src/platform/plugins/shared/content_management/server/rpc/routes/routes.ts @@ -42,6 +42,13 @@ export function initRpcRoutes( router.post( { path: '/api/content_management/rpc/{name}', + security: { + authz: { + enabled: false, + reason: + "This route is opted out from authorization, because it's a wrapper around Saved Object client", + }, + }, validate: { params: schema.object({ // @ts-ignore We validate above that procedureNames has at least one item diff --git a/src/platform/plugins/shared/guided_onboarding/server/routes/config_routes.ts b/src/platform/plugins/shared/guided_onboarding/server/routes/config_routes.ts index 36194c4677786..5880aecb6679f 100644 --- a/src/platform/plugins/shared/guided_onboarding/server/routes/config_routes.ts +++ b/src/platform/plugins/shared/guided_onboarding/server/routes/config_routes.ts @@ -18,6 +18,13 @@ export const registerGetConfigRoute = (router: IRouter, guidesConfig: GuidesConf router.get( { path: `${API_BASE_PATH}/configs/{guideId}`, + security: { + authz: { + enabled: false, + reason: + 'This route is opted out from authorization, as it returns config for guided onboarding, the guide onboarding is not security sensitive', + }, + }, validate: { params: schema.object({ guideId: schema.string(), diff --git a/src/platform/plugins/shared/guided_onboarding/server/routes/guide_state_routes.ts b/src/platform/plugins/shared/guided_onboarding/server/routes/guide_state_routes.ts index 1601094afdf6a..b380203bfeda8 100644 --- a/src/platform/plugins/shared/guided_onboarding/server/routes/guide_state_routes.ts +++ b/src/platform/plugins/shared/guided_onboarding/server/routes/guide_state_routes.ts @@ -17,6 +17,13 @@ export const registerGetGuideStateRoute = (router: IRouter) => { router.get( { path: `${API_BASE_PATH}/guides`, + security: { + authz: { + enabled: false, + reason: + "This route is opted out from authorization, because it's a wrapper around the Saved Object client", + }, + }, validate: false, }, async (context, request, response) => { diff --git a/src/platform/plugins/shared/guided_onboarding/server/routes/plugin_state_routes.ts b/src/platform/plugins/shared/guided_onboarding/server/routes/plugin_state_routes.ts index 0db93943f821c..ff30b0a421aa6 100644 --- a/src/platform/plugins/shared/guided_onboarding/server/routes/plugin_state_routes.ts +++ b/src/platform/plugins/shared/guided_onboarding/server/routes/plugin_state_routes.ts @@ -19,6 +19,13 @@ export const registerGetPluginStateRoute = (router: IRouter) => { router.get( { path: `${API_BASE_PATH}/state`, + security: { + authz: { + enabled: false, + reason: + "This route is opted out from authorization because it's a wrapper around the Saved Object client", + }, + }, validate: false, }, async (context, request, response) => { @@ -40,6 +47,13 @@ export const registerPutPluginStateRoute = (router: IRouter) => { router.put( { path: `${API_BASE_PATH}/state`, + security: { + authz: { + enabled: false, + reason: + "This route is opted out from authorization because it's a wrapper around the Saved Object client", + }, + }, validate: { body: schema.object({ status: schema.maybe(schema.string()), diff --git a/src/platform/plugins/shared/home/server/services/sample_data/routes/install.ts b/src/platform/plugins/shared/home/server/services/sample_data/routes/install.ts index 9c90fa4e843b9..e4ea014a48a1b 100644 --- a/src/platform/plugins/shared/home/server/services/sample_data/routes/install.ts +++ b/src/platform/plugins/shared/home/server/services/sample_data/routes/install.ts @@ -26,6 +26,13 @@ export function createInstallRoute( router.post( { path: '/api/sample_data/{id}', + security: { + authz: { + enabled: false, + reason: + 'This route is opted out from authorization because the sample data installer is a wrapper around the Saved Object client', + }, + }, validate: { params: schema.object({ id: schema.string() }), // TODO validate now as date diff --git a/src/platform/plugins/shared/home/server/services/sample_data/routes/list.ts b/src/platform/plugins/shared/home/server/services/sample_data/routes/list.ts index 4fe2710786d1b..4e406342cf7d5 100644 --- a/src/platform/plugins/shared/home/server/services/sample_data/routes/list.ts +++ b/src/platform/plugins/shared/home/server/services/sample_data/routes/list.ts @@ -26,47 +26,60 @@ export const createListRoute = ( appLinksMap: Map, logger: Logger ) => { - router.get({ path: '/api/sample_data', validate: false }, async (context, _req, res) => { - const allExistingObjects = await findExistingSampleObjects(context, logger, sampleDatasets); + router.get( + { + path: '/api/sample_data', + security: { + authz: { + enabled: false, + reason: + 'This route is opted out from authorization because the data retrieved is derived and fetched from the saved objects client', + }, + }, + validate: false, + }, + async (context, _req, res) => { + const allExistingObjects = await findExistingSampleObjects(context, logger, sampleDatasets); - const registeredSampleDatasets = await Promise.all( - sampleDatasets.map(async (sampleDataset) => { - const existingObjects = allExistingObjects.get(sampleDataset.id)!; - const findObjectId = (type: string, id: string) => - existingObjects.find((object) => object.type === type && object.id === id) - ?.foundObjectId ?? id; + const registeredSampleDatasets = await Promise.all( + sampleDatasets.map(async (sampleDataset) => { + const existingObjects = allExistingObjects.get(sampleDataset.id)!; + const findObjectId = (type: string, id: string) => + existingObjects.find((object) => object.type === type && object.id === id) + ?.foundObjectId ?? id; - const appLinks = (appLinksMap.get(sampleDataset.id) ?? []).map((data) => { - const { sampleObject, getPath, label, icon, order } = data; - if (sampleObject === null) { - return { path: getPath(''), label, icon, order }; - } - const objectId = findObjectId(sampleObject.type, sampleObject.id); - return { path: getPath(objectId), label, icon, order }; - }); - const sampleDataStatus = await getSampleDatasetStatus( - context, - allExistingObjects, - sampleDataset - ); + const appLinks = (appLinksMap.get(sampleDataset.id) ?? []).map((data) => { + const { sampleObject, getPath, label, icon, order } = data; + if (sampleObject === null) { + return { path: getPath(''), label, icon, order }; + } + const objectId = findObjectId(sampleObject.type, sampleObject.id); + return { path: getPath(objectId), label, icon, order }; + }); + const sampleDataStatus = await getSampleDatasetStatus( + context, + allExistingObjects, + sampleDataset + ); - return { - id: sampleDataset.id, - name: sampleDataset.name, - description: sampleDataset.description, - previewImagePath: sampleDataset.previewImagePath, - darkPreviewImagePath: sampleDataset.darkPreviewImagePath, - overviewDashboard: findObjectId('dashboard', sampleDataset.overviewDashboard), - appLinks: sortBy(appLinks, 'order'), - defaultIndex: findObjectId('index-pattern', sampleDataset.defaultIndex), - dataIndices: sampleDataset.dataIndices.map(({ id }) => ({ id })), - ...sampleDataStatus, - }; - }) - ); + return { + id: sampleDataset.id, + name: sampleDataset.name, + description: sampleDataset.description, + previewImagePath: sampleDataset.previewImagePath, + darkPreviewImagePath: sampleDataset.darkPreviewImagePath, + overviewDashboard: findObjectId('dashboard', sampleDataset.overviewDashboard), + appLinks: sortBy(appLinks, 'order'), + defaultIndex: findObjectId('index-pattern', sampleDataset.defaultIndex), + dataIndices: sampleDataset.dataIndices.map(({ id }) => ({ id })), + ...sampleDataStatus, + }; + }) + ); - return res.ok({ body: registeredSampleDatasets }); - }); + return res.ok({ body: registeredSampleDatasets }); + } + ); }; type ExistingSampleObjects = Map; diff --git a/src/platform/plugins/shared/home/server/services/sample_data/routes/uninstall.ts b/src/platform/plugins/shared/home/server/services/sample_data/routes/uninstall.ts index 3f2fffebb3926..c17580d1d49ce 100644 --- a/src/platform/plugins/shared/home/server/services/sample_data/routes/uninstall.ts +++ b/src/platform/plugins/shared/home/server/services/sample_data/routes/uninstall.ts @@ -26,6 +26,13 @@ export function createUninstallRoute( router.delete( { path: '/api/sample_data/{id}', + security: { + authz: { + enabled: false, + reason: + 'This route is opted out from authorization because the uninstaller route is a wrapper around the Saved Object client', + }, + }, validate: { params: schema.object({ id: schema.string() }), }, diff --git a/src/platform/plugins/shared/home/server/services/tutorials/tutorials_registry.ts b/src/platform/plugins/shared/home/server/services/tutorials/tutorials_registry.ts index 1b7a27a766e8b..7f2a800aa6b59 100644 --- a/src/platform/plugins/shared/home/server/services/tutorials/tutorials_registry.ts +++ b/src/platform/plugins/shared/home/server/services/tutorials/tutorials_registry.ts @@ -81,7 +81,17 @@ export class TutorialsRegistry { const router = core.http.createRouter(); router.get( - { path: '/api/kibana/home/tutorials', validate: false }, + { + path: '/api/kibana/home/tutorials', + security: { + authz: { + enabled: false, + reason: + 'This route is opted out from authorization, because tutorials are public and ideally should be available to all users.', + }, + }, + validate: false, + }, async (context, req, res) => { const initialContext = this.baseTutorialContext; const scopedContext = this.scopedTutorialContextFactories.reduce( diff --git a/src/platform/plugins/shared/share/server/url_service/http/short_urls/register_create_route.ts b/src/platform/plugins/shared/share/server/url_service/http/short_urls/register_create_route.ts index 1ecdb7ad65ce7..92b6519a95ea5 100644 --- a/src/platform/plugins/shared/share/server/url_service/http/short_urls/register_create_route.ts +++ b/src/platform/plugins/shared/share/server/url_service/http/short_urls/register_create_route.ts @@ -16,6 +16,13 @@ export const registerCreateRoute = (router: IRouter, url: ServerUrlService) => { router.post( { path: '/api/short_url', + security: { + authz: { + enabled: false, + reason: + 'This route is opted out from authorization, because the url service is a wrapper around the Saved Object client', + }, + }, options: { access: 'public', summary: `Create a short URL`, diff --git a/src/platform/plugins/shared/share/server/url_service/http/short_urls/register_delete_route.ts b/src/platform/plugins/shared/share/server/url_service/http/short_urls/register_delete_route.ts index 64a3dfaa098a5..7f148962e1d29 100644 --- a/src/platform/plugins/shared/share/server/url_service/http/short_urls/register_delete_route.ts +++ b/src/platform/plugins/shared/share/server/url_service/http/short_urls/register_delete_route.ts @@ -15,6 +15,13 @@ export const registerDeleteRoute = (router: IRouter, url: ServerUrlService) => { router.delete( { path: '/api/short_url/{id}', + security: { + authz: { + enabled: false, + reason: + 'This route is opted out from authorization, because the url service is a wrapper around the Saved Object client', + }, + }, options: { access: 'public', summary: `Delete a short URL`, diff --git a/src/platform/plugins/shared/share/server/url_service/http/short_urls/register_get_route.ts b/src/platform/plugins/shared/share/server/url_service/http/short_urls/register_get_route.ts index 92e8fcbdf414b..840886213786d 100644 --- a/src/platform/plugins/shared/share/server/url_service/http/short_urls/register_get_route.ts +++ b/src/platform/plugins/shared/share/server/url_service/http/short_urls/register_get_route.ts @@ -15,6 +15,13 @@ export const registerGetRoute = (router: IRouter, url: ServerUrlService) => { router.get( { path: '/api/short_url/{id}', + security: { + authz: { + enabled: false, + reason: + 'This route is opted out from authorization, because the url service is a wrapper around the Saved Object client', + }, + }, options: { access: 'public', summary: `Get a short URL`, diff --git a/src/platform/plugins/shared/share/server/url_service/http/short_urls/register_resolve_route.ts b/src/platform/plugins/shared/share/server/url_service/http/short_urls/register_resolve_route.ts index 0fa129a6aada9..4201d21b47596 100644 --- a/src/platform/plugins/shared/share/server/url_service/http/short_urls/register_resolve_route.ts +++ b/src/platform/plugins/shared/share/server/url_service/http/short_urls/register_resolve_route.ts @@ -16,6 +16,13 @@ export const registerResolveRoute = (router: IRouter, url: ServerUrlService) => router.get( { path: '/api/short_url/_slug/{slug}', + security: { + authz: { + enabled: false, + reason: + 'This route is opted out from authorization, because the url service is a wrapper around the Saved Object client', + }, + }, options: { access: 'public', summary: `Resolve a short URL`, diff --git a/x-pack/platform/plugins/private/banners/server/routes/info.ts b/x-pack/platform/plugins/private/banners/server/routes/info.ts index 17d60cdbc532f..110ddf9b53872 100644 --- a/x-pack/platform/plugins/private/banners/server/routes/info.ts +++ b/x-pack/platform/plugins/private/banners/server/routes/info.ts @@ -15,6 +15,12 @@ export const registerInfoRoute = (router: BannersRouter, config: BannersConfigTy router.get( { path: '/api/banners/info', + security: { + authz: { + enabled: false, + reason: 'This route is opted out from authorization', + }, + }, validate: false, options: { authRequired: 'optional', diff --git a/x-pack/platform/plugins/private/custom_branding/server/routes/info.ts b/x-pack/platform/plugins/private/custom_branding/server/routes/info.ts index 83921db247782..d856f7959bb2f 100644 --- a/x-pack/platform/plugins/private/custom_branding/server/routes/info.ts +++ b/x-pack/platform/plugins/private/custom_branding/server/routes/info.ts @@ -13,6 +13,13 @@ export const registerInfoRoute = (router: CustomBrandingRouter) => { router.get( { path: '/api/custom_branding/info', + security: { + authz: { + enabled: false, + reason: + 'This route is opted out from authorization, because this route is rather a status check route than a data fetching route', + }, + }, validate: false, options: { authRequired: 'optional', diff --git a/x-pack/platform/plugins/private/reporting/server/routes/internal/deprecations/deprecations.ts b/x-pack/platform/plugins/private/reporting/server/routes/internal/deprecations/deprecations.ts index 407d126940ae4..daf56d7233b40 100644 --- a/x-pack/platform/plugins/private/reporting/server/routes/internal/deprecations/deprecations.ts +++ b/x-pack/platform/plugins/private/reporting/server/routes/internal/deprecations/deprecations.ts @@ -63,6 +63,12 @@ export const registerDeprecationsRoutes = (reporting: ReportingCore, logger: Log router.get( { path: getStatusPath, + security: { + authz: { + enabled: false, + reason: 'This route is opted out from authorization', + }, + }, validate: false, options: { access: 'internal' }, }, @@ -100,6 +106,12 @@ export const registerDeprecationsRoutes = (reporting: ReportingCore, logger: Log router.put( { path: migrateApiPath, + security: { + authz: { + enabled: false, + reason: 'This route is opted out from authorization', + }, + }, validate: false, options: { access: 'internal' }, }, diff --git a/x-pack/platform/plugins/private/reporting/server/routes/internal/diagnostic/browser.ts b/x-pack/platform/plugins/private/reporting/server/routes/internal/diagnostic/browser.ts index 71d8e1e15e74f..f90cca1125515 100644 --- a/x-pack/platform/plugins/private/reporting/server/routes/internal/diagnostic/browser.ts +++ b/x-pack/platform/plugins/private/reporting/server/routes/internal/diagnostic/browser.ts @@ -51,6 +51,12 @@ export const registerDiagnoseBrowser = (reporting: ReportingCore, logger: Logger router.get( { path, + security: { + authz: { + enabled: false, + reason: 'This route is opted out from authorization', + }, + }, validate: false, options: { access: 'internal' }, }, diff --git a/x-pack/platform/plugins/private/reporting/server/routes/internal/generate/generate_from_jobparams.ts b/x-pack/platform/plugins/private/reporting/server/routes/internal/generate/generate_from_jobparams.ts index f8ee55cc12fb5..bd26c88bf6a0a 100644 --- a/x-pack/platform/plugins/private/reporting/server/routes/internal/generate/generate_from_jobparams.ts +++ b/x-pack/platform/plugins/private/reporting/server/routes/internal/generate/generate_from_jobparams.ts @@ -18,15 +18,23 @@ export function registerGenerationRoutesInternal(reporting: ReportingCore, logge const setupDeps = reporting.getPluginSetupDeps(); const { router } = setupDeps; - const kibanaAccessControlTags = ['access:generateReport']; + const kibanaAccessControlTags = ['generateReport']; const registerInternalPostGenerationEndpoint = () => { const path = `${GENERATE_PREFIX}/{exportType}`; router.post( { path, + security: { + authz: { + requiredPrivileges: kibanaAccessControlTags, + }, + }, validate: RequestHandler.getValidation(), - options: { tags: kibanaAccessControlTags, access: 'internal' }, + options: { + tags: kibanaAccessControlTags.map((accessControlTag) => `access:${accessControlTag}`), + access: 'internal', + }, }, authorizedUserPreRouting(reporting, async (user, context, req, res) => { try { diff --git a/x-pack/platform/plugins/private/reporting/server/routes/internal/management/jobs.ts b/x-pack/platform/plugins/private/reporting/server/routes/internal/management/jobs.ts index c501f73d43698..492d579727c51 100644 --- a/x-pack/platform/plugins/private/reporting/server/routes/internal/management/jobs.ts +++ b/x-pack/platform/plugins/private/reporting/server/routes/internal/management/jobs.ts @@ -30,6 +30,12 @@ export function registerJobInfoRoutesInternal(reporting: ReportingCore) { router.get( { path, + security: { + authz: { + enabled: false, + reason: 'This route is opted out from authorization', + }, + }, validate: { query: schema.object({ page: schema.string({ defaultValue: '0' }), @@ -72,6 +78,12 @@ export function registerJobInfoRoutesInternal(reporting: ReportingCore) { router.get( { path, + security: { + authz: { + enabled: false, + reason: 'This route is opted out from authorization', + }, + }, validate: false, options: { access: 'internal' }, }, @@ -107,6 +119,12 @@ export function registerJobInfoRoutesInternal(reporting: ReportingCore) { router.get( { path, + security: { + authz: { + enabled: false, + reason: 'This route is opted out from authorization', + }, + }, validate: jobHandlers.validate, options: { access: 'internal' }, }, @@ -145,6 +163,12 @@ export function registerJobInfoRoutesInternal(reporting: ReportingCore) { router.get( { path, + security: { + authz: { + enabled: false, + reason: 'This route is opted out from authorization', + }, + }, validate: jobHandlers.validate, options: { tags: [ROUTE_TAG_CAN_REDIRECT], access: 'internal' }, }, @@ -161,6 +185,12 @@ export function registerJobInfoRoutesInternal(reporting: ReportingCore) { router.delete( { path, + security: { + authz: { + enabled: false, + reason: 'This route is opted out from authorization', + }, + }, validate: jobHandlers.validate, options: { access: 'internal' }, }, diff --git a/x-pack/platform/plugins/private/reporting/server/routes/public/generate_from_jobparams.ts b/x-pack/platform/plugins/private/reporting/server/routes/public/generate_from_jobparams.ts index 1f901634f3f00..f547faa9cab52 100644 --- a/x-pack/platform/plugins/private/reporting/server/routes/public/generate_from_jobparams.ts +++ b/x-pack/platform/plugins/private/reporting/server/routes/public/generate_from_jobparams.ts @@ -16,15 +16,23 @@ export function registerGenerationRoutesPublic(reporting: ReportingCore, logger: const setupDeps = reporting.getPluginSetupDeps(); const { router } = setupDeps; - const kibanaAccessControlTags = ['access:generateReport']; + const kibanaAccessControlTags = ['generateReport']; const registerPublicPostGenerationEndpoint = () => { const path = `${PUBLIC_ROUTES.GENERATE_PREFIX}/{exportType}`; router.post( { path, + security: { + authz: { + requiredPrivileges: kibanaAccessControlTags, + }, + }, validate: RequestHandler.getValidation(), - options: { tags: kibanaAccessControlTags, access: 'public' }, + options: { + tags: kibanaAccessControlTags.map((controlAccessTag) => `access:${controlAccessTag}`), + access: 'public', + }, }, authorizedUserPreRouting(reporting, async (user, context, req, res) => { try { @@ -56,6 +64,12 @@ export function registerGenerationRoutesPublic(reporting: ReportingCore, logger: router.get( { path: `${PUBLIC_ROUTES.GENERATE_PREFIX}/{p*}`, + security: { + authz: { + enabled: false, + reason: 'This route is opted out from authorization', + }, + }, validate: false, options: { access: 'public' }, }, diff --git a/x-pack/platform/plugins/private/reporting/server/routes/public/jobs.ts b/x-pack/platform/plugins/private/reporting/server/routes/public/jobs.ts index 04d417c4eb89f..cf2b13fafffd8 100644 --- a/x-pack/platform/plugins/private/reporting/server/routes/public/jobs.ts +++ b/x-pack/platform/plugins/private/reporting/server/routes/public/jobs.ts @@ -24,6 +24,12 @@ export function registerJobInfoRoutesPublic(reporting: ReportingCore) { router.get( { path, + security: { + authz: { + enabled: false, + reason: 'This route is opted out from authorization', + }, + }, validate: jobHandlers.validate, options: { tags: [ROUTE_TAG_CAN_REDIRECT], access: 'public' }, }, @@ -39,6 +45,12 @@ export function registerJobInfoRoutesPublic(reporting: ReportingCore) { router.delete( { path, + security: { + authz: { + enabled: false, + reason: 'This route is opted out from authorization', + }, + }, validate: jobHandlers.validate, options: { access: 'public' }, }, diff --git a/x-pack/platform/plugins/shared/global_search/server/routes/find.ts b/x-pack/platform/plugins/shared/global_search/server/routes/find.ts index ee151dc1c4856..9772c849b6f22 100644 --- a/x-pack/platform/plugins/shared/global_search/server/routes/find.ts +++ b/x-pack/platform/plugins/shared/global_search/server/routes/find.ts @@ -14,6 +14,12 @@ export const registerInternalFindRoute = (router: GlobalSearchRouter) => { router.post( { path: '/internal/global_search/find', + security: { + authz: { + enabled: false, + reason: 'This route is opted out from authorization', + }, + }, validate: { body: schema.object({ params: schema.object({ diff --git a/x-pack/platform/plugins/shared/global_search/server/routes/get_searchable_types.ts b/x-pack/platform/plugins/shared/global_search/server/routes/get_searchable_types.ts index f456c0e665f19..4b4c44973dd46 100644 --- a/x-pack/platform/plugins/shared/global_search/server/routes/get_searchable_types.ts +++ b/x-pack/platform/plugins/shared/global_search/server/routes/get_searchable_types.ts @@ -11,6 +11,12 @@ export const registerInternalSearchableTypesRoute = (router: GlobalSearchRouter) router.get( { path: '/internal/global_search/searchable_types', + security: { + authz: { + enabled: false, + reason: 'This route is opted out from authorization', + }, + }, validate: false, }, async (ctx, req, res) => { diff --git a/x-pack/platform/plugins/shared/saved_objects_tagging/server/routes/assignments/find_assignable_objects.ts b/x-pack/platform/plugins/shared/saved_objects_tagging/server/routes/assignments/find_assignable_objects.ts index 2fd6dfe56b33e..2835a69784198 100644 --- a/x-pack/platform/plugins/shared/saved_objects_tagging/server/routes/assignments/find_assignable_objects.ts +++ b/x-pack/platform/plugins/shared/saved_objects_tagging/server/routes/assignments/find_assignable_objects.ts @@ -13,6 +13,13 @@ export const registerFindAssignableObjectsRoute = (router: TagsPluginRouter) => router.get( { path: '/internal/saved_objects_tagging/assignments/_find_assignable_objects', + security: { + authz: { + enabled: false, + reason: + 'This route is opted out from authorization as there is a separate authorization check within the assignment service.', + }, + }, validate: { query: schema.object({ search: schema.maybe(schema.string()), diff --git a/x-pack/platform/plugins/shared/saved_objects_tagging/server/routes/assignments/get_assignable_types.ts b/x-pack/platform/plugins/shared/saved_objects_tagging/server/routes/assignments/get_assignable_types.ts index bd82e7d31bc7f..aaa0d93137ba3 100644 --- a/x-pack/platform/plugins/shared/saved_objects_tagging/server/routes/assignments/get_assignable_types.ts +++ b/x-pack/platform/plugins/shared/saved_objects_tagging/server/routes/assignments/get_assignable_types.ts @@ -12,6 +12,13 @@ export const registerGetAssignableTypesRoute = (router: TagsPluginRouter) => { router.get( { path: '/internal/saved_objects_tagging/assignments/_assignable_types', + security: { + authz: { + enabled: false, + reason: + 'This route is opted out from authorization as there is a separate authorization check within the assignment service.', + }, + }, validate: {}, }, router.handleLegacyErrors(async (ctx, req, res) => { diff --git a/x-pack/platform/plugins/shared/saved_objects_tagging/server/routes/assignments/update_tags_assignments.ts b/x-pack/platform/plugins/shared/saved_objects_tagging/server/routes/assignments/update_tags_assignments.ts index 5bc1f844c4d36..6c53c68b81a02 100644 --- a/x-pack/platform/plugins/shared/saved_objects_tagging/server/routes/assignments/update_tags_assignments.ts +++ b/x-pack/platform/plugins/shared/saved_objects_tagging/server/routes/assignments/update_tags_assignments.ts @@ -18,6 +18,13 @@ export const registerUpdateTagsAssignmentsRoute = (router: TagsPluginRouter) => router.post( { path: '/api/saved_objects_tagging/assignments/update_by_tags', + security: { + authz: { + enabled: false, + reason: + 'This route is opted out from authorization as there is a separate authorization check within the assignment service.', + }, + }, validate: { body: schema.object( { diff --git a/x-pack/platform/plugins/shared/saved_objects_tagging/server/routes/internal/bulk_delete.ts b/x-pack/platform/plugins/shared/saved_objects_tagging/server/routes/internal/bulk_delete.ts index ea4471f7d1243..9fa33d7004f71 100644 --- a/x-pack/platform/plugins/shared/saved_objects_tagging/server/routes/internal/bulk_delete.ts +++ b/x-pack/platform/plugins/shared/saved_objects_tagging/server/routes/internal/bulk_delete.ts @@ -12,6 +12,13 @@ export const registerInternalBulkDeleteRoute = (router: TagsPluginRouter) => { router.post( { path: '/internal/saved_objects_tagging/tags/_bulk_delete', + security: { + authz: { + enabled: false, + reason: + 'This route is opted out from authorization because the tags client internals leverages the SO client', + }, + }, validate: { body: schema.object({ ids: schema.arrayOf(schema.string()), diff --git a/x-pack/platform/plugins/shared/saved_objects_tagging/server/routes/internal/find_tags.ts b/x-pack/platform/plugins/shared/saved_objects_tagging/server/routes/internal/find_tags.ts index 78286b160d218..ab253d29798f4 100644 --- a/x-pack/platform/plugins/shared/saved_objects_tagging/server/routes/internal/find_tags.ts +++ b/x-pack/platform/plugins/shared/saved_objects_tagging/server/routes/internal/find_tags.ts @@ -16,6 +16,13 @@ export const registerInternalFindTagsRoute = (router: TagsPluginRouter) => { router.get( { path: '/internal/saved_objects_tagging/tags/_find', + security: { + authz: { + enabled: false, + reason: + 'This route is opted out from authorization because this route leverages the SO client', + }, + }, validate: { query: schema.object({ perPage: schema.number({ min: 0, defaultValue: 20 }), diff --git a/x-pack/platform/plugins/shared/saved_objects_tagging/server/routes/tags/create_tag.ts b/x-pack/platform/plugins/shared/saved_objects_tagging/server/routes/tags/create_tag.ts index 0c48168eed281..979198c256d2b 100644 --- a/x-pack/platform/plugins/shared/saved_objects_tagging/server/routes/tags/create_tag.ts +++ b/x-pack/platform/plugins/shared/saved_objects_tagging/server/routes/tags/create_tag.ts @@ -13,6 +13,13 @@ export const registerCreateTagRoute = (router: TagsPluginRouter) => { router.post( { path: '/api/saved_objects_tagging/tags/create', + security: { + authz: { + enabled: false, + reason: + 'This route is opted out from authorization because the tags client internals leverages the SO client', + }, + }, validate: { body: schema.object({ name: schema.string(), diff --git a/x-pack/platform/plugins/shared/saved_objects_tagging/server/routes/tags/delete_tag.ts b/x-pack/platform/plugins/shared/saved_objects_tagging/server/routes/tags/delete_tag.ts index 505ecfd4974a0..9d6e29c33b57e 100644 --- a/x-pack/platform/plugins/shared/saved_objects_tagging/server/routes/tags/delete_tag.ts +++ b/x-pack/platform/plugins/shared/saved_objects_tagging/server/routes/tags/delete_tag.ts @@ -12,6 +12,13 @@ export const registerDeleteTagRoute = (router: TagsPluginRouter) => { router.delete( { path: '/api/saved_objects_tagging/tags/{id}', + security: { + authz: { + enabled: false, + reason: + 'This route is opted out from authorization because the tags client internals leverages the SO client', + }, + }, validate: { params: schema.object({ id: schema.string(), diff --git a/x-pack/platform/plugins/shared/saved_objects_tagging/server/routes/tags/get_all_tags.ts b/x-pack/platform/plugins/shared/saved_objects_tagging/server/routes/tags/get_all_tags.ts index 011d764983faf..662f5b7a7baf1 100644 --- a/x-pack/platform/plugins/shared/saved_objects_tagging/server/routes/tags/get_all_tags.ts +++ b/x-pack/platform/plugins/shared/saved_objects_tagging/server/routes/tags/get_all_tags.ts @@ -11,6 +11,13 @@ export const registerGetAllTagsRoute = (router: TagsPluginRouter) => { router.get( { path: '/api/saved_objects_tagging/tags', + security: { + authz: { + enabled: false, + reason: + 'This route is opted out from authorization because the tags client internals leverages the SO client', + }, + }, validate: {}, }, router.handleLegacyErrors(async (ctx, req, res) => { diff --git a/x-pack/platform/plugins/shared/saved_objects_tagging/server/routes/tags/get_tag.ts b/x-pack/platform/plugins/shared/saved_objects_tagging/server/routes/tags/get_tag.ts index 4488d4dae6e2b..5a2e172330697 100644 --- a/x-pack/platform/plugins/shared/saved_objects_tagging/server/routes/tags/get_tag.ts +++ b/x-pack/platform/plugins/shared/saved_objects_tagging/server/routes/tags/get_tag.ts @@ -12,6 +12,13 @@ export const registerGetTagRoute = (router: TagsPluginRouter) => { router.get( { path: '/api/saved_objects_tagging/tags/{id}', + security: { + authz: { + enabled: false, + reason: + 'This route is opted out from authorization because the tags client internals leverages the SO client', + }, + }, validate: { params: schema.object({ id: schema.string(), diff --git a/x-pack/platform/plugins/shared/saved_objects_tagging/server/routes/tags/update_tag.ts b/x-pack/platform/plugins/shared/saved_objects_tagging/server/routes/tags/update_tag.ts index 62f8c73dddc78..0cdfe038af507 100644 --- a/x-pack/platform/plugins/shared/saved_objects_tagging/server/routes/tags/update_tag.ts +++ b/x-pack/platform/plugins/shared/saved_objects_tagging/server/routes/tags/update_tag.ts @@ -13,6 +13,13 @@ export const registerUpdateTagRoute = (router: TagsPluginRouter) => { router.post( { path: '/api/saved_objects_tagging/tags/{id}', + security: { + authz: { + enabled: false, + reason: + 'This route is opted out from authorization because the tags client internals leverages the SO client', + }, + }, validate: { params: schema.object({ id: schema.string(), diff --git a/x-pack/platform/plugins/shared/serverless/server/plugin.ts b/x-pack/platform/plugins/shared/serverless/server/plugin.ts index 03a4ace34253a..31a815a4327ed 100644 --- a/x-pack/platform/plugins/shared/serverless/server/plugin.ts +++ b/x-pack/platform/plugins/shared/serverless/server/plugin.ts @@ -71,6 +71,13 @@ export class ServerlessPlugin router.post( { path: API_SWITCH_PROJECT, + security: { + authz: { + enabled: false, + reason: + 'This route is opted out from authorization, because it is only used in development', + }, + }, validate: { body: switchBodySchema, }, From 2642aff1dd32fded1717178c1ab2557a4e2bafcb Mon Sep 17 00:00:00 2001 From: Shahzad Date: Fri, 10 Jan 2025 15:49:57 +0100 Subject: [PATCH 21/42] [Synthetics] E2e tests update flakiness !! (#206257) ## Summary E2e tests update flakiness !! --- .../e2e/synthetics/journeys/add_monitor.journey.ts | 3 ++- .../e2e/synthetics/page_objects/synthetics_app.tsx | 10 +++++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/x-pack/solutions/observability/plugins/synthetics/e2e/synthetics/journeys/add_monitor.journey.ts b/x-pack/solutions/observability/plugins/synthetics/e2e/synthetics/journeys/add_monitor.journey.ts index c7dcc6742d7d6..ced8701d72b7f 100644 --- a/x-pack/solutions/observability/plugins/synthetics/e2e/synthetics/journeys/add_monitor.journey.ts +++ b/x-pack/solutions/observability/plugins/synthetics/e2e/synthetics/journeys/add_monitor.journey.ts @@ -192,7 +192,8 @@ const createMonitorJourney = ({ monitorType ); expect(hasFailure).toBeFalsy(); - await page.click('text=Update monitor'); + await page.waitForTimeout(1000); + await page.getByTestId('syntheticsMonitorConfigSubmitButton').click(); await page.waitForSelector('text=Monitor updated successfully.'); }); diff --git a/x-pack/solutions/observability/plugins/synthetics/e2e/synthetics/page_objects/synthetics_app.tsx b/x-pack/solutions/observability/plugins/synthetics/e2e/synthetics/page_objects/synthetics_app.tsx index 2f3d0099192f4..133832249651b 100644 --- a/x-pack/solutions/observability/plugins/synthetics/e2e/synthetics/page_objects/synthetics_app.tsx +++ b/x-pack/solutions/observability/plugins/synthetics/e2e/synthetics/page_objects/synthetics_app.tsx @@ -110,9 +110,13 @@ export function syntheticsAppPageProvider({ }, async navigateToAddMonitor() { - await page.goto(addMonitor, { - waitUntil: 'networkidle', - }); + if (await page.isVisible('[data-test-subj="syntheticsAddMonitorBtn"]')) { + await page.click('[data-test-subj="syntheticsAddMonitorBtn"]'); + } else { + await page.goto(addMonitor, { + waitUntil: 'networkidle', + }); + } }, async ensureIsOnMonitorConfigPage() { From d0abdbdb7e6800dd01845f776f813122b53ed6dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20Haro?= Date: Fri, 10 Jan 2025 15:54:23 +0100 Subject: [PATCH 22/42] [ES `body` removal] `@elastic/search-kibana` (#204876) --- .../src/components/api_key_flyout_wrapper.tsx | 2 +- .../pipelines/simulate_existing_ml_inference_pipeline.ts | 2 +- .../simulate_ml_inference_pipeline_processors.ts | 2 +- .../pipelines/ml_inference/test_pipeline_logic.ts | 2 +- .../enterprise_search/server/lib/indices/fetch_indices.ts | 2 +- .../ml_inference/get_ml_inference_pipeline_history.ts | 2 +- .../lib/indices/utils/extract_always_show_indices.test.ts | 2 +- .../lib/indices/utils/extract_always_show_indices.ts | 2 +- .../server/lib/ml/get_ml_model_deployment_status.ts | 2 +- .../server/lib/ml/start_ml_model_deployment.ts | 2 +- .../server/lib/ml/start_ml_model_download.ts | 8 +++----- .../public/components/quick_stats/mappings_convertor.ts | 5 +---- .../public/hooks/api/use_delete_document.ts | 2 +- .../search/plugins/search_indices/public/types.ts | 5 +---- .../plugins/search_indices/server/utils/index_utils.ts | 2 +- .../search_playground/public/hooks/use_query_indices.ts | 2 +- .../search/plugins/search_playground/public/types.ts | 2 +- .../search_playground/public/utils/create_query.ts | 2 +- .../server/lib/elasticsearch_retriever.ts | 2 +- .../serverless_search/server/lib/indices/fetch_indices.ts | 2 +- .../plugins/serverless_search/server/utils/index_utils.ts | 2 +- .../functional/test_suites/search/inference_management.ts | 2 +- 22 files changed, 24 insertions(+), 32 deletions(-) diff --git a/x-pack/solutions/search/packages/kbn-search-api-keys-components/src/components/api_key_flyout_wrapper.tsx b/x-pack/solutions/search/packages/kbn-search-api-keys-components/src/components/api_key_flyout_wrapper.tsx index bf69875a42647..809ae8441ee93 100644 --- a/x-pack/solutions/search/packages/kbn-search-api-keys-components/src/components/api_key_flyout_wrapper.tsx +++ b/x-pack/solutions/search/packages/kbn-search-api-keys-components/src/components/api_key_flyout_wrapper.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { ApiKeyFlyout, ApiKeyFlyoutProps } from '@kbn/security-api-key-management'; -import type { SecurityCreateApiKeyResponse } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import type { SecurityCreateApiKeyResponse } from '@elastic/elasticsearch/lib/api/types'; const API_KEY_NAME = 'Unrestricted API Key'; diff --git a/x-pack/solutions/search/plugins/enterprise_search/public/applications/enterprise_search_content/api/pipelines/simulate_existing_ml_inference_pipeline.ts b/x-pack/solutions/search/plugins/enterprise_search/public/applications/enterprise_search_content/api/pipelines/simulate_existing_ml_inference_pipeline.ts index b822017cc0726..df5c80b1139eb 100644 --- a/x-pack/solutions/search/plugins/enterprise_search/public/applications/enterprise_search_content/api/pipelines/simulate_existing_ml_inference_pipeline.ts +++ b/x-pack/solutions/search/plugins/enterprise_search/public/applications/enterprise_search_content/api/pipelines/simulate_existing_ml_inference_pipeline.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { IngestSimulateResponse } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { IngestSimulateResponse } from '@elastic/elasticsearch/lib/api/types'; import { createApiLogic } from '../../../shared/api_logic/create_api_logic'; diff --git a/x-pack/solutions/search/plugins/enterprise_search/public/applications/enterprise_search_content/api/pipelines/simulate_ml_inference_pipeline_processors.ts b/x-pack/solutions/search/plugins/enterprise_search/public/applications/enterprise_search_content/api/pipelines/simulate_ml_inference_pipeline_processors.ts index 18c90fbd7e6c1..7981b8b45e339 100644 --- a/x-pack/solutions/search/plugins/enterprise_search/public/applications/enterprise_search_content/api/pipelines/simulate_ml_inference_pipeline_processors.ts +++ b/x-pack/solutions/search/plugins/enterprise_search/public/applications/enterprise_search_content/api/pipelines/simulate_ml_inference_pipeline_processors.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { IngestSimulateResponse } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { IngestSimulateResponse } from '@elastic/elasticsearch/lib/api/types'; import type { MlInferencePipeline } from '../../../../../common/types/pipelines'; diff --git a/x-pack/solutions/search/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/test_pipeline_logic.ts b/x-pack/solutions/search/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/test_pipeline_logic.ts index deed56ab72986..be802834c86dd 100644 --- a/x-pack/solutions/search/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/test_pipeline_logic.ts +++ b/x-pack/solutions/search/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/test_pipeline_logic.ts @@ -7,7 +7,7 @@ import { kea, MakeLogicType } from 'kea'; -import { IngestSimulateResponse } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { IngestSimulateResponse } from '@elastic/elasticsearch/lib/api/types'; import { Status, HttpError } from '../../../../../../../common/types/api'; import { MlInferencePipeline } from '../../../../../../../common/types/pipelines'; diff --git a/x-pack/solutions/search/plugins/enterprise_search/server/lib/indices/fetch_indices.ts b/x-pack/solutions/search/plugins/enterprise_search/server/lib/indices/fetch_indices.ts index 94e65344f5a65..51ad2b61ee15e 100644 --- a/x-pack/solutions/search/plugins/enterprise_search/server/lib/indices/fetch_indices.ts +++ b/x-pack/solutions/search/plugins/enterprise_search/server/lib/indices/fetch_indices.ts @@ -10,7 +10,7 @@ import { IndicesGetResponse, SecurityHasPrivilegesPrivileges, IndicesStatsIndicesStats, -} from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +} from '@elastic/elasticsearch/lib/api/types'; import { IScopedClusterClient } from '@kbn/core/server'; import { AlwaysShowPattern, ElasticsearchIndexWithPrivileges } from '../../../common/types/indices'; diff --git a/x-pack/solutions/search/plugins/enterprise_search/server/lib/indices/pipelines/ml_inference/get_ml_inference_pipeline_history.ts b/x-pack/solutions/search/plugins/enterprise_search/server/lib/indices/pipelines/ml_inference/get_ml_inference_pipeline_history.ts index f6a01ec2b3e87..9201e3184c2a4 100644 --- a/x-pack/solutions/search/plugins/enterprise_search/server/lib/indices/pipelines/ml_inference/get_ml_inference_pipeline_history.ts +++ b/x-pack/solutions/search/plugins/enterprise_search/server/lib/indices/pipelines/ml_inference/get_ml_inference_pipeline_history.ts @@ -8,7 +8,7 @@ import { AggregationsMultiBucketAggregateBase, AggregationsStringRareTermsBucketKeys, -} from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +} from '@elastic/elasticsearch/lib/api/types'; import { ElasticsearchClient } from '@kbn/core/server'; diff --git a/x-pack/solutions/search/plugins/enterprise_search/server/lib/indices/utils/extract_always_show_indices.test.ts b/x-pack/solutions/search/plugins/enterprise_search/server/lib/indices/utils/extract_always_show_indices.test.ts index d67d4b4addc8b..40ffc1a04f713 100644 --- a/x-pack/solutions/search/plugins/enterprise_search/server/lib/indices/utils/extract_always_show_indices.test.ts +++ b/x-pack/solutions/search/plugins/enterprise_search/server/lib/indices/utils/extract_always_show_indices.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { SecurityHasPrivilegesPrivileges } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { SecurityHasPrivilegesPrivileges } from '@elastic/elasticsearch/lib/api/types'; import { ElasticsearchIndex } from '@kbn/search-connectors'; import { expandAliases, getAlwaysShowAliases } from './extract_always_show_indices'; diff --git a/x-pack/solutions/search/plugins/enterprise_search/server/lib/indices/utils/extract_always_show_indices.ts b/x-pack/solutions/search/plugins/enterprise_search/server/lib/indices/utils/extract_always_show_indices.ts index 4312bcebd6f83..18cd54927908b 100644 --- a/x-pack/solutions/search/plugins/enterprise_search/server/lib/indices/utils/extract_always_show_indices.ts +++ b/x-pack/solutions/search/plugins/enterprise_search/server/lib/indices/utils/extract_always_show_indices.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { SecurityHasPrivilegesPrivileges } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { SecurityHasPrivilegesPrivileges } from '@elastic/elasticsearch/lib/api/types'; import { ElasticsearchIndex } from '@kbn/search-connectors'; import { AlwaysShowPattern } from '../../../../common/types/indices'; diff --git a/x-pack/solutions/search/plugins/enterprise_search/server/lib/ml/get_ml_model_deployment_status.ts b/x-pack/solutions/search/plugins/enterprise_search/server/lib/ml/get_ml_model_deployment_status.ts index 2d65d516dd5bf..494cb925bf77f 100644 --- a/x-pack/solutions/search/plugins/enterprise_search/server/lib/ml/get_ml_model_deployment_status.ts +++ b/x-pack/solutions/search/plugins/enterprise_search/server/lib/ml/get_ml_model_deployment_status.ts @@ -8,7 +8,7 @@ import { MlGetTrainedModelsStatsRequest, MlGetTrainedModelsRequest, -} from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +} from '@elastic/elasticsearch/lib/api/types'; import { MlTrainedModels } from '@kbn/ml-plugin/server'; import { MlModelDeploymentStatus, MlModelDeploymentState } from '../../../common/types/ml'; diff --git a/x-pack/solutions/search/plugins/enterprise_search/server/lib/ml/start_ml_model_deployment.ts b/x-pack/solutions/search/plugins/enterprise_search/server/lib/ml/start_ml_model_deployment.ts index becd34a6c3c95..acf4a9d47edb0 100644 --- a/x-pack/solutions/search/plugins/enterprise_search/server/lib/ml/start_ml_model_deployment.ts +++ b/x-pack/solutions/search/plugins/enterprise_search/server/lib/ml/start_ml_model_deployment.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { MlStartTrainedModelDeploymentRequest } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { MlStartTrainedModelDeploymentRequest } from '@elastic/elasticsearch/lib/api/types'; import { MlTrainedModels } from '@kbn/ml-plugin/server'; diff --git a/x-pack/solutions/search/plugins/enterprise_search/server/lib/ml/start_ml_model_download.ts b/x-pack/solutions/search/plugins/enterprise_search/server/lib/ml/start_ml_model_download.ts index ffa51acc5bd32..7684baaeae161 100644 --- a/x-pack/solutions/search/plugins/enterprise_search/server/lib/ml/start_ml_model_download.ts +++ b/x-pack/solutions/search/plugins/enterprise_search/server/lib/ml/start_ml_model_download.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { MlPutTrainedModelRequest } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { MlPutTrainedModelRequest } from '@elastic/elasticsearch/lib/api/types'; import { MlTrainedModels } from '@kbn/ml-plugin/server'; import { MlModelDeploymentState, MlModelDeploymentStatus } from '../../../common/types/ml'; @@ -43,10 +43,8 @@ export const startMlModelDownload = async ( // we're not downloaded yet - let's initiate that... const putRequest: MlPutTrainedModelRequest = { - body: { - input: { - field_names: ['text_field'], - }, + input: { + field_names: ['text_field'], }, model_id: modelName, }; diff --git a/x-pack/solutions/search/plugins/search_indices/public/components/quick_stats/mappings_convertor.ts b/x-pack/solutions/search/plugins/search_indices/public/components/quick_stats/mappings_convertor.ts index 749fe05de1f54..b6063c7c3694c 100644 --- a/x-pack/solutions/search/plugins/search_indices/public/components/quick_stats/mappings_convertor.ts +++ b/x-pack/solutions/search/plugins/search_indices/public/components/quick_stats/mappings_convertor.ts @@ -5,10 +5,7 @@ * 2.0. */ -import type { - MappingProperty, - MappingPropertyBase, -} from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import type { MappingProperty, MappingPropertyBase } from '@elastic/elasticsearch/lib/api/types'; import type { Mappings } from '../../types'; interface VectorFieldTypes { diff --git a/x-pack/solutions/search/plugins/search_indices/public/hooks/api/use_delete_document.ts b/x-pack/solutions/search/plugins/search_indices/public/hooks/api/use_delete_document.ts index 4c5a64b270f91..d2190d46b32b5 100644 --- a/x-pack/solutions/search/plugins/search_indices/public/hooks/api/use_delete_document.ts +++ b/x-pack/solutions/search/plugins/search_indices/public/hooks/api/use_delete_document.ts @@ -7,7 +7,7 @@ import { AcknowledgedResponseBase } from '@elastic/elasticsearch/lib/api/types'; import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { SearchHit } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { SearchHit } from '@elastic/elasticsearch/lib/api/types'; import { MutationKeys, QueryKeys } from '../../constants'; import { useKibana } from '../use_kibana'; import { INDEX_SEARCH_POLLING, IndexDocuments } from './use_document_search'; diff --git a/x-pack/solutions/search/plugins/search_indices/public/types.ts b/x-pack/solutions/search/plugins/search_indices/public/types.ts index ccf4d56e13a67..abf749b67e374 100644 --- a/x-pack/solutions/search/plugins/search_indices/public/types.ts +++ b/x-pack/solutions/search/plugins/search_indices/public/types.ts @@ -13,10 +13,7 @@ import type { UsageCollectionSetup, UsageCollectionStart, } from '@kbn/usage-collection-plugin/public'; -import type { - MappingProperty, - MappingPropertyBase, -} from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import type { MappingProperty, MappingPropertyBase } from '@elastic/elasticsearch/lib/api/types'; import type { IndexManagementPluginSetup, IndexManagementPluginStart, diff --git a/x-pack/solutions/search/plugins/search_indices/server/utils/index_utils.ts b/x-pack/solutions/search/plugins/search_indices/server/utils/index_utils.ts index d0b47b303679e..31ccb562f91bb 100644 --- a/x-pack/solutions/search/plugins/search_indices/server/utils/index_utils.ts +++ b/x-pack/solutions/search/plugins/search_indices/server/utils/index_utils.ts @@ -5,7 +5,7 @@ * 2.0. */ -import type { IndicesIndexState } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import type { IndicesIndexState } from '@elastic/elasticsearch/lib/api/types'; export function isHidden(index: IndicesIndexState): boolean { return index.settings?.index?.hidden === true || index.settings?.index?.hidden === 'true'; diff --git a/x-pack/solutions/search/plugins/search_playground/public/hooks/use_query_indices.ts b/x-pack/solutions/search/plugins/search_playground/public/hooks/use_query_indices.ts index cc5812306097c..f106d7d5c5a48 100644 --- a/x-pack/solutions/search/plugins/search_playground/public/hooks/use_query_indices.ts +++ b/x-pack/solutions/search/plugins/search_playground/public/hooks/use_query_indices.ts @@ -6,7 +6,7 @@ */ import { useQuery } from '@tanstack/react-query'; -import { IndexName } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { IndexName } from '@elastic/elasticsearch/lib/api/types'; import { useKibana } from './use_kibana'; import { APIRoutes } from '../types'; diff --git a/x-pack/solutions/search/plugins/search_playground/public/types.ts b/x-pack/solutions/search/plugins/search_playground/public/types.ts index dfd415d5b9781..4ec69553e1c72 100644 --- a/x-pack/solutions/search/plugins/search_playground/public/types.ts +++ b/x-pack/solutions/search/plugins/search_playground/public/types.ts @@ -10,7 +10,7 @@ import { IndexName, IndicesStatsIndexMetadataState, Uuid, -} from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +} from '@elastic/elasticsearch/lib/api/types'; import type { NavigationPublicPluginStart } from '@kbn/navigation-plugin/public'; import React, { ComponentType } from 'react'; import type { SharePluginSetup, SharePluginStart } from '@kbn/share-plugin/public'; diff --git a/x-pack/solutions/search/plugins/search_playground/public/utils/create_query.ts b/x-pack/solutions/search/plugins/search_playground/public/utils/create_query.ts index 5dcb85eb4f50e..63cdcdf76bb65 100644 --- a/x-pack/solutions/search/plugins/search_playground/public/utils/create_query.ts +++ b/x-pack/solutions/search/plugins/search_playground/public/utils/create_query.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { RetrieverContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { RetrieverContainer } from '@elastic/elasticsearch/lib/api/types'; import { IndicesQuerySourceFields, QuerySourceFields } from '../types'; export type IndexFields = Record; diff --git a/x-pack/solutions/search/plugins/search_playground/server/lib/elasticsearch_retriever.ts b/x-pack/solutions/search/plugins/search_playground/server/lib/elasticsearch_retriever.ts index 8504651bb6398..57967ba773569 100644 --- a/x-pack/solutions/search/plugins/search_playground/server/lib/elasticsearch_retriever.ts +++ b/x-pack/solutions/search/plugins/search_playground/server/lib/elasticsearch_retriever.ts @@ -12,7 +12,7 @@ import { AggregationsAggregate, SearchHit, SearchResponse, -} from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +} from '@elastic/elasticsearch/lib/api/types'; import { getValueForSelectedField } from '../utils/get_value_for_selected_field'; export interface ElasticsearchRetrieverInput extends BaseRetrieverInput { diff --git a/x-pack/solutions/search/plugins/serverless_search/server/lib/indices/fetch_indices.ts b/x-pack/solutions/search/plugins/serverless_search/server/lib/indices/fetch_indices.ts index 9560bbe7f9bb3..6027fb438af6b 100644 --- a/x-pack/solutions/search/plugins/serverless_search/server/lib/indices/fetch_indices.ts +++ b/x-pack/solutions/search/plugins/serverless_search/server/lib/indices/fetch_indices.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { IndicesStatsIndicesStats } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { IndicesStatsIndicesStats } from '@elastic/elasticsearch/lib/api/types'; import { ElasticsearchClient } from '@kbn/core-elasticsearch-server'; import { isNotNullish } from '../../../common/utils/is_not_nullish'; import { isHidden, isClosed } from '../../utils/index_utils'; diff --git a/x-pack/solutions/search/plugins/serverless_search/server/utils/index_utils.ts b/x-pack/solutions/search/plugins/serverless_search/server/utils/index_utils.ts index 043bb80d7f8d0..5f86f9ae9d962 100644 --- a/x-pack/solutions/search/plugins/serverless_search/server/utils/index_utils.ts +++ b/x-pack/solutions/search/plugins/serverless_search/server/utils/index_utils.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { IndicesIndexState } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { IndicesIndexState } from '@elastic/elasticsearch/lib/api/types'; export function isHidden(index: IndicesIndexState): boolean { return index.settings?.index?.hidden === true || index.settings?.index?.hidden === 'true'; diff --git a/x-pack/test_serverless/functional/test_suites/search/inference_management.ts b/x-pack/test_serverless/functional/test_suites/search/inference_management.ts index 939deb7445213..389e46b45ca10 100644 --- a/x-pack/test_serverless/functional/test_suites/search/inference_management.ts +++ b/x-pack/test_serverless/functional/test_suites/search/inference_management.ts @@ -5,7 +5,7 @@ * 2.0. */ -import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import type * as estypes from '@elastic/elasticsearch/lib/api/types'; import { testHasEmbeddedConsole } from './embedded_console'; From f6fb68ce163e045eb108a8e133895cf478fef103 Mon Sep 17 00:00:00 2001 From: Paulina Shakirova Date: Fri, 10 Jan 2025 15:59:58 +0100 Subject: [PATCH 23/42] Consistent help dropdown UI (#206280) #### This is a second PR, I migrated relevant code changes here after closing the previous one. You may read the comments history [here](https://github.com/elastic/kibana/pull/205781). ## Summary This PR fixes [Make Help dropdown consistent across all environments](https://github.com/elastic/kibana/issues/199465) issue. Since we need to make dropdown consistent also in the cloud, I will firstly merge this PR, and then deal with the Cloud part by either opening PR in that repo, or reach out to the responsible team showcasing changes in Kibana and ask them to implement the changes. This is how the dropdown is displayed currently (1st screenshot) and how it will be looking with my changes (2nd screenshot). After discussing the design with @ek-so and trying out different variants, this seems to be the most suitable and universal. The changes include refactoring usage or relevant Eui components and removing the displaying of icons in the general menu dropdown (while keeping this functionality of adding icons to the solutions additional menu dropdown items). Screenshot 2025-01-07 at 13 07 49 Screenshot 2025-01-09 at 15 22 36 --- .../src/ui/header/header_help_menu.tsx | 87 ++++++++++--------- .../chrome/browser/src/nav_controls.ts | 1 - 2 files changed, 45 insertions(+), 43 deletions(-) diff --git a/src/core/packages/chrome/browser-internal/src/ui/header/header_help_menu.tsx b/src/core/packages/chrome/browser-internal/src/ui/header/header_help_menu.tsx index b6789a096f7da..84c93a2ed0222 100644 --- a/src/core/packages/chrome/browser-internal/src/ui/header/header_help_menu.tsx +++ b/src/core/packages/chrome/browser-internal/src/ui/header/header_help_menu.tsx @@ -21,8 +21,9 @@ import { EuiPopover, EuiPopoverTitle, EuiSpacer, - EuiTitle, - EuiHorizontalRule, + EuiPopoverFooter, + withEuiTheme, + WithEuiThemeProps, } from '@elastic/eui'; import type { InternalApplicationStart } from '@kbn/core-application-browser-internal'; @@ -33,6 +34,7 @@ import type { import type { ChromeHelpMenuLink } from '@kbn/core-chrome-browser/src'; import type { DocLinksStart } from '@kbn/core-doc-links-browser'; +import { css } from '@emotion/react'; import { HeaderExtension } from './header_extension'; import { isModifiedOrPrevented } from './nav_link'; @@ -68,7 +70,6 @@ const buildDefaultContentLinks = ({ defaultMessage: 'Open an issue in GitHub', }), href: docLinks.links.kibana.createGithubIssue, - iconType: 'logoGithub', }, ]; @@ -92,10 +93,10 @@ interface State { defaultContentLinks: ChromeHelpMenuLink[]; } -export class HeaderHelpMenu extends Component { +class HelpMenu extends Component { private subscription?: Subscription; - constructor(props: Props) { + constructor(props: Props & WithEuiThemeProps) { super(props); this.state = { @@ -136,12 +137,16 @@ export class HeaderHelpMenu extends Component { } public render() { - const { kibanaVersion } = this.props; + const { kibanaVersion, theme } = this.props; const defaultContent = this.renderDefaultContent(); const globalCustomContent = this.renderGlobalCustomContent(); const customContent = this.renderCustomContent(); + const euiThemePadding = css` + padding: ${theme.euiTheme.size.s}; + `; + const button = ( { {defaultContent} {customContent && ( <> - + {customContent} )} @@ -210,40 +215,37 @@ export class HeaderHelpMenu extends Component { return ( - {defaultContentLinks.map( - ({ href, title, iconType, onClick: _onClick, dataTestSubj }, i) => { - const isLast = i === defaultContentLinks.length - 1; - - if (href && _onClick) { - throw new Error( - 'Only one of `href` and `onClick` should be provided for the help menu link.' - ); - } - - const hrefProps = href ? { href, target: '_blank' } : {}; - const onClick = () => { - if (!_onClick) return; - _onClick(); - this.closeMenu(); - }; - - return ( - - - {title} - - {!isLast && } - + {defaultContentLinks.map(({ href, title, onClick: _onClick, dataTestSubj }, i) => { + const isLast = i === defaultContentLinks.length - 1; + + if (href && _onClick) { + throw new Error( + 'Only one of `href` and `onClick` should be provided for the help menu link.' ); } - )} + + const hrefProps = href ? { href, target: '_blank' } : {}; + const onClick = () => { + if (!_onClick) return; + _onClick(); + this.closeMenu(); + }; + + return ( + + + {title} + + {!isLast && } + + ); + })} ); } @@ -333,10 +335,9 @@ export class HeaderHelpMenu extends Component { return ( <> - +

{appName}

-
- + {customLinks} {content && ( <> @@ -402,3 +403,5 @@ const createCustomLink = ( ); }; + +export const HeaderHelpMenu = withEuiTheme(HelpMenu); diff --git a/src/core/packages/chrome/browser/src/nav_controls.ts b/src/core/packages/chrome/browser/src/nav_controls.ts index 9efa4c457e8b4..7bf1d96ce35dc 100644 --- a/src/core/packages/chrome/browser/src/nav_controls.ts +++ b/src/core/packages/chrome/browser/src/nav_controls.ts @@ -20,7 +20,6 @@ export interface ChromeNavControl { export interface ChromeHelpMenuLink { title: string; href?: string; - iconType?: string; onClick?: () => void; dataTestSubj?: string; } From 44c2504240eacff6f5de9773fbc17e68312f4a6d Mon Sep 17 00:00:00 2001 From: "elastic-renovate-prod[bot]" <174716857+elastic-renovate-prod[bot]@users.noreply.github.com> Date: Fri, 10 Jan 2025 16:04:11 +0100 Subject: [PATCH 24/42] Update dependency io-ts to ^2.2.22 (main) (#206174) Co-authored-by: elastic-renovate-prod[bot] <174716857+elastic-renovate-prod[bot]@users.noreply.github.com> Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 42b5b560d5a89..6131d545082de 100644 --- a/package.json +++ b/package.json @@ -1160,7 +1160,7 @@ "icalendar": "0.7.1", "immer": "^9.0.21", "inquirer": "^7.3.3", - "io-ts": "^2.0.5", + "io-ts": "^2.2.22", "ipaddr.js": "2.0.0", "isbinaryfile": "4.0.2", "joi": "^17.13.3", diff --git a/yarn.lock b/yarn.lock index 551eb104c30b7..0720d461e34cf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -21297,10 +21297,10 @@ invert-kv@^1.0.0: resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6" integrity sha1-EEqOSqym09jNFXqO+L+rLXo//bY= -io-ts@^2.0.5: - version "2.0.5" - resolved "https://registry.yarnpkg.com/io-ts/-/io-ts-2.0.5.tgz#e6e3db9df8b047f9cbd6b69e7d2ad3e6437a0b13" - integrity sha512-pL7uUptryanI5Glv+GUv7xh+aLBjxGEDmLwmEYNSx0yOD3djK0Nw5Bt0N6BAkv9LadOUU7QKpRsLcqnTh3UlLA== +io-ts@^2.0.5, io-ts@^2.2.22: + version "2.2.22" + resolved "https://registry.yarnpkg.com/io-ts/-/io-ts-2.2.22.tgz#5ab0d3636fe8494a275f0266461ab019da4b8d0b" + integrity sha512-FHCCztTkHoV9mdBsHpocLpdTAfh956ZQcIkWQxxS0U5HT53vtrcuYdQneEJKH6xILaLNzXVl2Cvwtoy8XNN0AA== ip-address@^9.0.5: version "9.0.5" From b6176b232ba4cc33e47d49f34409492252c39a14 Mon Sep 17 00:00:00 2001 From: Rodney Norris Date: Fri, 10 Jan 2025 09:18:52 -0600 Subject: [PATCH 25/42] [Search] remove appSearch & workplaceSearch endpoints (#206107) ## Summary Removing the registration of App Search & Workplace Search routes from `enterprise_search` --- .../enterprise_search/server/plugin.ts | 4 - .../app_search/adaptive_relevance.test.ts | 143 -- .../routes/app_search/adaptive_relevance.ts | 101 -- .../routes/app_search/analytics.test.ts | 115 -- .../server/routes/app_search/analytics.ts | 56 - .../server/routes/app_search/api_logs.test.ts | 54 - .../server/routes/app_search/api_logs.ts | 35 - .../app_search/app_search_gated_form.test.ts | 60 - .../app_search/app_search_gated_form.ts | 34 - .../server/routes/app_search/crawler.test.ts | 715 --------- .../server/routes/app_search/crawler.ts | 296 ---- .../app_search/crawler_crawl_rules.test.ts | 137 -- .../routes/app_search/crawler_crawl_rules.ts | 82 - .../app_search/crawler_entry_points.test.ts | 132 -- .../routes/app_search/crawler_entry_points.ts | 77 - .../app_search/crawler_sitemaps.test.ts | 132 -- .../routes/app_search/crawler_sitemaps.ts | 77 - .../routes/app_search/credentials.test.ts | 322 ---- .../server/routes/app_search/credentials.ts | 103 -- .../routes/app_search/curations.test.ts | 232 --- .../server/routes/app_search/curations.ts | 124 -- .../routes/app_search/documents.test.ts | 83 - .../server/routes/app_search/documents.ts | 65 - .../server/routes/app_search/engines.test.ts | 324 ---- .../server/routes/app_search/engines.ts | 117 -- .../server/routes/app_search/index.test.ts | 17 - .../server/routes/app_search/index.ts | 59 - .../routes/app_search/onboarding.test.ts | 42 - .../server/routes/app_search/onboarding.ts | 30 - .../routes/app_search/result_settings.test.ts | 75 - .../routes/app_search/result_settings.ts | 45 - .../routes/app_search/role_mappings.test.ts | 211 --- .../server/routes/app_search/role_mappings.ts | 125 -- .../server/routes/app_search/schema.test.ts | 81 - .../server/routes/app_search/schema.ts | 59 - .../server/routes/app_search/search.test.ts | 35 - .../server/routes/app_search/search.ts | 58 - .../routes/app_search/search_settings.test.ts | 135 -- .../routes/app_search/search_settings.ts | 59 - .../routes/app_search/search_ui.test.ts | 40 - .../server/routes/app_search/search_ui.ts | 29 - .../server/routes/app_search/settings.test.ts | 87 -- .../server/routes/app_search/settings.ts | 58 - .../routes/app_search/source_engines.test.ts | 153 -- .../routes/app_search/source_engines.ts | 67 - .../server/routes/app_search/synonyms.test.ts | 121 -- .../server/routes/app_search/synonyms.ts | 78 - .../routes/workplace_search/api_keys.test.ts | 92 -- .../routes/workplace_search/api_keys.ts | 57 - .../workplace_search/gated_form.test.ts | 60 - .../routes/workplace_search/gated_form.ts | 34 - .../routes/workplace_search/groups.test.ts | 303 ---- .../server/routes/workplace_search/groups.ts | 185 --- .../routes/workplace_search/index.test.ts | 17 - .../server/routes/workplace_search/index.ts | 30 - .../routes/workplace_search/oauth.test.ts | 143 -- .../server/routes/workplace_search/oauth.ts | 89 -- .../routes/workplace_search/overview.test.ts | 54 - .../routes/workplace_search/overview.ts | 24 - .../workplace_search/role_mappings.test.ts | 180 --- .../routes/workplace_search/role_mappings.ts | 128 -- .../routes/workplace_search/security.test.ts | 107 -- .../routes/workplace_search/security.ts | 79 - .../routes/workplace_search/settings.test.ts | 138 -- .../routes/workplace_search/settings.ts | 101 -- .../routes/workplace_search/sources.test.ts | 1355 ----------------- .../server/routes/workplace_search/sources.ts | 1025 ------------- 67 files changed, 9485 deletions(-) delete mode 100644 x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/adaptive_relevance.test.ts delete mode 100644 x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/adaptive_relevance.ts delete mode 100644 x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/analytics.test.ts delete mode 100644 x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/analytics.ts delete mode 100644 x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/api_logs.test.ts delete mode 100644 x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/api_logs.ts delete mode 100644 x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/app_search_gated_form.test.ts delete mode 100644 x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/app_search_gated_form.ts delete mode 100644 x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/crawler.test.ts delete mode 100644 x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/crawler.ts delete mode 100644 x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/crawler_crawl_rules.test.ts delete mode 100644 x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/crawler_crawl_rules.ts delete mode 100644 x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/crawler_entry_points.test.ts delete mode 100644 x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/crawler_entry_points.ts delete mode 100644 x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/crawler_sitemaps.test.ts delete mode 100644 x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/crawler_sitemaps.ts delete mode 100644 x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/credentials.test.ts delete mode 100644 x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/credentials.ts delete mode 100644 x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/curations.test.ts delete mode 100644 x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/curations.ts delete mode 100644 x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/documents.test.ts delete mode 100644 x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/documents.ts delete mode 100644 x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/engines.test.ts delete mode 100644 x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/engines.ts delete mode 100644 x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/index.test.ts delete mode 100644 x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/index.ts delete mode 100644 x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/onboarding.test.ts delete mode 100644 x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/onboarding.ts delete mode 100644 x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/result_settings.test.ts delete mode 100644 x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/result_settings.ts delete mode 100644 x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/role_mappings.test.ts delete mode 100644 x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/role_mappings.ts delete mode 100644 x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/schema.test.ts delete mode 100644 x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/schema.ts delete mode 100644 x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/search.test.ts delete mode 100644 x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/search.ts delete mode 100644 x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/search_settings.test.ts delete mode 100644 x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/search_settings.ts delete mode 100644 x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/search_ui.test.ts delete mode 100644 x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/search_ui.ts delete mode 100644 x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/settings.test.ts delete mode 100644 x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/settings.ts delete mode 100644 x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/source_engines.test.ts delete mode 100644 x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/source_engines.ts delete mode 100644 x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/synonyms.test.ts delete mode 100644 x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/synonyms.ts delete mode 100644 x-pack/solutions/search/plugins/enterprise_search/server/routes/workplace_search/api_keys.test.ts delete mode 100644 x-pack/solutions/search/plugins/enterprise_search/server/routes/workplace_search/api_keys.ts delete mode 100644 x-pack/solutions/search/plugins/enterprise_search/server/routes/workplace_search/gated_form.test.ts delete mode 100644 x-pack/solutions/search/plugins/enterprise_search/server/routes/workplace_search/gated_form.ts delete mode 100644 x-pack/solutions/search/plugins/enterprise_search/server/routes/workplace_search/groups.test.ts delete mode 100644 x-pack/solutions/search/plugins/enterprise_search/server/routes/workplace_search/groups.ts delete mode 100644 x-pack/solutions/search/plugins/enterprise_search/server/routes/workplace_search/index.test.ts delete mode 100644 x-pack/solutions/search/plugins/enterprise_search/server/routes/workplace_search/index.ts delete mode 100644 x-pack/solutions/search/plugins/enterprise_search/server/routes/workplace_search/oauth.test.ts delete mode 100644 x-pack/solutions/search/plugins/enterprise_search/server/routes/workplace_search/oauth.ts delete mode 100644 x-pack/solutions/search/plugins/enterprise_search/server/routes/workplace_search/overview.test.ts delete mode 100644 x-pack/solutions/search/plugins/enterprise_search/server/routes/workplace_search/overview.ts delete mode 100644 x-pack/solutions/search/plugins/enterprise_search/server/routes/workplace_search/role_mappings.test.ts delete mode 100644 x-pack/solutions/search/plugins/enterprise_search/server/routes/workplace_search/role_mappings.ts delete mode 100644 x-pack/solutions/search/plugins/enterprise_search/server/routes/workplace_search/security.test.ts delete mode 100644 x-pack/solutions/search/plugins/enterprise_search/server/routes/workplace_search/security.ts delete mode 100644 x-pack/solutions/search/plugins/enterprise_search/server/routes/workplace_search/settings.test.ts delete mode 100644 x-pack/solutions/search/plugins/enterprise_search/server/routes/workplace_search/settings.ts delete mode 100644 x-pack/solutions/search/plugins/enterprise_search/server/routes/workplace_search/sources.test.ts delete mode 100644 x-pack/solutions/search/plugins/enterprise_search/server/routes/workplace_search/sources.ts diff --git a/x-pack/solutions/search/plugins/enterprise_search/server/plugin.ts b/x-pack/solutions/search/plugins/enterprise_search/server/plugin.ts index 2791e4d43a52d..1f8911d5e3b55 100644 --- a/x-pack/solutions/search/plugins/enterprise_search/server/plugin.ts +++ b/x-pack/solutions/search/plugins/enterprise_search/server/plugin.ts @@ -71,7 +71,6 @@ import { IEnterpriseSearchRequestHandler, } from './lib/enterprise_search_request_handler'; -import { registerAppSearchRoutes } from './routes/app_search'; import { registerEnterpriseSearchRoutes } from './routes/enterprise_search'; import { registerAnalyticsRoutes } from './routes/enterprise_search/analytics'; import { registerApiKeysRoutes } from './routes/enterprise_search/api_keys'; @@ -80,7 +79,6 @@ import { registerConnectorRoutes } from './routes/enterprise_search/connectors'; import { registerCrawlerRoutes } from './routes/enterprise_search/crawler/crawler'; import { registerStatsRoutes } from './routes/enterprise_search/stats'; import { registerTelemetryRoute } from './routes/enterprise_search/telemetry'; -import { registerWorkplaceSearchRoutes } from './routes/workplace_search'; import { appSearchTelemetryType } from './saved_objects/app_search/telemetry'; import { enterpriseSearchTelemetryType } from './saved_objects/enterprise_search/telemetry'; @@ -291,9 +289,7 @@ export class EnterpriseSearchPlugin implements Plugin { - beforeEach(() => { - jest.clearAllMocks(); - }); - - describe('POST /internal/app_search/engines/{name}/adaptive_relevance/suggestions', () => { - const mockRouter = new MockRouter({ - method: 'post', - path: '/internal/app_search/engines/{engineName}/adaptive_relevance/suggestions', - }); - - beforeEach(() => { - registerSearchRelevanceSuggestionsRoutes({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request to enterprise search', async () => { - await mockRouter.callRoute({ - params: { engineName: 'some-engine' }, - }); - - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/api/as/v0/engines/:engineName/adaptive_relevance/suggestions', - }); - }); - }); - - describe('PUT /internal/app_search/engines/{name}/adaptive_relevance/suggestions', () => { - const mockRouter = new MockRouter({ - method: 'put', - path: '/internal/app_search/engines/{engineName}/adaptive_relevance/suggestions', - }); - - beforeEach(() => { - registerSearchRelevanceSuggestionsRoutes({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request to enterprise search', async () => { - await mockRouter.callRoute({ - params: { engineName: 'some-engine' }, - body: { - query: 'some query', - type: 'curation', - status: 'applied', - }, - }); - - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/api/as/v0/engines/:engineName/adaptive_relevance/suggestions', - }); - }); - }); - - describe('GET /internal/app_search/engines/{name}/adaptive_relevance/settings', () => { - const mockRouter = new MockRouter({ - method: 'get', - path: '/internal/app_search/engines/{engineName}/adaptive_relevance/settings', - }); - - beforeEach(() => { - registerSearchRelevanceSuggestionsRoutes({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request to enterprise search', async () => { - await mockRouter.callRoute({ - params: { engineName: 'some-engine' }, - }); - - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/api/as/v0/engines/:engineName/adaptive_relevance/settings', - }); - }); - }); - - describe('PUT /internal/app_search/engines/{name}/adaptive_relevance/settings', () => { - const mockRouter = new MockRouter({ - method: 'put', - path: '/internal/app_search/engines/{engineName}/adaptive_relevance/settings', - }); - - beforeEach(() => { - registerSearchRelevanceSuggestionsRoutes({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request to enterprise search', async () => { - await mockRouter.callRoute({ - params: { engineName: 'some-engine' }, - body: { curation: { enabled: true } }, - }); - - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/api/as/v0/engines/:engineName/adaptive_relevance/settings', - }); - }); - }); - - describe('GET /internal/app_search/engines/{engineName}/adaptive_relevance/suggestions/{query}', () => { - const mockRouter = new MockRouter({ - method: 'get', - path: '/internal/app_search/engines/{engineName}/adaptive_relevance/suggestions/{query}', - }); - - beforeEach(() => { - registerSearchRelevanceSuggestionsRoutes({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request to enterprise search', async () => { - await mockRouter.callRoute({ - params: { engineName: 'some-engine', query: 'foo' }, - query: { type: 'curation' }, - }); - - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/as/engines/:engineName/adaptive_relevance/suggestions/:query', - }); - }); - }); -}); diff --git a/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/adaptive_relevance.ts b/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/adaptive_relevance.ts deleted file mode 100644 index 02260d19186da..0000000000000 --- a/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/adaptive_relevance.ts +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { schema } from '@kbn/config-schema'; - -import { skipBodyValidation } from '../../lib/route_config_helpers'; - -import { RouteDependencies } from '../../plugin'; - -export function registerSearchRelevanceSuggestionsRoutes({ - router, - enterpriseSearchRequestHandler, -}: RouteDependencies) { - router.post( - { - path: '/internal/app_search/engines/{engineName}/adaptive_relevance/suggestions', - validate: { - params: schema.object({ - engineName: schema.string(), - }), - body: schema.object({ - page: schema.object({ - current: schema.number(), - size: schema.number(), - }), - filters: schema.object({ - status: schema.arrayOf(schema.string()), - type: schema.string(), - }), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/api/as/v0/engines/:engineName/adaptive_relevance/suggestions', - }) - ); - - router.put( - skipBodyValidation({ - path: '/internal/app_search/engines/{engineName}/adaptive_relevance/suggestions', - validate: { - params: schema.object({ - engineName: schema.string(), - }), - }, - }), - enterpriseSearchRequestHandler.createRequest({ - path: '/api/as/v0/engines/:engineName/adaptive_relevance/suggestions', - }) - ); - - router.get( - { - path: '/internal/app_search/engines/{engineName}/adaptive_relevance/settings', - validate: { - params: schema.object({ - engineName: schema.string(), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/api/as/v0/engines/:engineName/adaptive_relevance/settings', - }) - ); - - router.put( - skipBodyValidation({ - path: '/internal/app_search/engines/{engineName}/adaptive_relevance/settings', - validate: { - params: schema.object({ - engineName: schema.string(), - }), - }, - }), - enterpriseSearchRequestHandler.createRequest({ - path: '/api/as/v0/engines/:engineName/adaptive_relevance/settings', - }) - ); - - router.get( - { - path: '/internal/app_search/engines/{engineName}/adaptive_relevance/suggestions/{query}', - validate: { - params: schema.object({ - engineName: schema.string(), - query: schema.string(), - }), - query: schema.object({ - type: schema.string(), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/as/engines/:engineName/adaptive_relevance/suggestions/:query', - }) - ); -} diff --git a/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/analytics.test.ts b/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/analytics.test.ts deleted file mode 100644 index c0313876f4007..0000000000000 --- a/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/analytics.test.ts +++ /dev/null @@ -1,115 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { MockRouter, mockRequestHandler, mockDependencies } from '../../__mocks__'; - -import { registerAnalyticsRoutes } from './analytics'; - -describe('analytics routes', () => { - describe('GET /internal/app_search/engines/{engineName}/analytics/queries', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'get', - path: '/internal/app_search/engines/{engineName}/analytics/queries', - }); - - registerAnalyticsRoutes({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request handler', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/as/engines/:engineName/analytics/queries', - }); - }); - - describe('validates', () => { - it('correctly without optional query params', () => { - const request = { query: {} }; - mockRouter.shouldValidate(request); - }); - - it('correctly with all optional query params', () => { - const request = { - query: { - size: 20, - start: '1970-01-01', - end: '1970-01-02', - tag: 'some-tag', - }, - }; - mockRouter.shouldValidate(request); - }); - - it('incorrect types', () => { - const request = { - query: { - start: 100, - size: '100', - }, - }; - mockRouter.shouldThrow(request); - }); - }); - }); - - describe('GET /internal/app_search/engines/{engineName}/analytics/queries/{query}', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'get', - path: '/internal/app_search/engines/{engineName}/analytics/queries/{query}', - }); - - registerAnalyticsRoutes({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request handler', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/as/engines/:engineName/analytics/query/:query', - }); - }); - - describe('validates', () => { - it('correctly without optional query params', () => { - const request = { query: {} }; - mockRouter.shouldValidate(request); - }); - - it('correctly with all optional query params', () => { - const request = { - query: { - start: '1970-01-01', - end: '1970-01-02', - tag: 'some-tag', - }, - }; - mockRouter.shouldValidate(request); - }); - - it('incorrect types', () => { - const request = { - query: { - start: 100, - tag: false, - }, - }; - mockRouter.shouldThrow(request); - }); - }); - }); -}); diff --git a/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/analytics.ts b/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/analytics.ts deleted file mode 100644 index f6a03ad5674eb..0000000000000 --- a/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/analytics.ts +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { schema } from '@kbn/config-schema'; - -import { RouteDependencies } from '../../plugin'; - -const querySchema = { - start: schema.maybe(schema.string()), // Date string, expected format 'YYYY-MM-DD' - end: schema.maybe(schema.string()), // Date string, expected format 'YYYY-MM-DD' - tag: schema.maybe(schema.string()), -}; -const queriesSchema = { - ...querySchema, - size: schema.maybe(schema.number()), -}; - -export function registerAnalyticsRoutes({ - router, - enterpriseSearchRequestHandler, -}: RouteDependencies) { - router.get( - { - path: '/internal/app_search/engines/{engineName}/analytics/queries', - validate: { - params: schema.object({ - engineName: schema.string(), - }), - query: schema.object(queriesSchema), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/as/engines/:engineName/analytics/queries', - }) - ); - - router.get( - { - path: '/internal/app_search/engines/{engineName}/analytics/queries/{query}', - validate: { - params: schema.object({ - engineName: schema.string(), - query: schema.string(), - }), - query: schema.object(querySchema), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/as/engines/:engineName/analytics/query/:query', - }) - ); -} diff --git a/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/api_logs.test.ts b/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/api_logs.test.ts deleted file mode 100644 index 9d2fb6d292e31..0000000000000 --- a/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/api_logs.test.ts +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { MockRouter, mockRequestHandler, mockDependencies } from '../../__mocks__'; - -import { registerApiLogsRoutes } from './api_logs'; - -describe('API logs routes', () => { - describe('GET /internal/app_search/engines/{engineName}/api_logs', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'get', - path: '/internal/app_search/engines/{engineName}/api_logs', - }); - - registerApiLogsRoutes({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request handler', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/as/engines/:engineName/api_logs/collection', - }); - }); - - describe('validates', () => { - it('with required query params', () => { - const request = { - query: { - 'filters[date][from]': '1970-01-01T12:00:00.000Z', - 'filters[date][to]': '1970-01-02T12:00:00.000Z', - 'page[current]': 1, - sort_direction: 'desc', - }, - }; - mockRouter.shouldValidate(request); - }); - - it('missing params', () => { - const request = { query: {} }; - mockRouter.shouldThrow(request); - }); - }); - }); -}); diff --git a/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/api_logs.ts b/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/api_logs.ts deleted file mode 100644 index e0803ade141d3..0000000000000 --- a/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/api_logs.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { schema } from '@kbn/config-schema'; - -import { RouteDependencies } from '../../plugin'; - -export function registerApiLogsRoutes({ - router, - enterpriseSearchRequestHandler, -}: RouteDependencies) { - router.get( - { - path: '/internal/app_search/engines/{engineName}/api_logs', - validate: { - params: schema.object({ - engineName: schema.string(), - }), - query: schema.object({ - 'filters[date][from]': schema.string(), // Date string, expected format: ISO string - 'filters[date][to]': schema.string(), // Date string, expected format: ISO string - 'page[current]': schema.number(), - sort_direction: schema.string(), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/as/engines/:engineName/api_logs/collection', - }) - ); -} diff --git a/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/app_search_gated_form.test.ts b/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/app_search_gated_form.test.ts deleted file mode 100644 index ef440cada1d7a..0000000000000 --- a/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/app_search_gated_form.test.ts +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { MockRouter, mockRequestHandler, mockDependencies } from '../../__mocks__'; - -import { registerAppSearchGatedFormRoute } from './app_search_gated_form'; - -describe('Overview route with kibana_uis_enabled ', () => { - describe('POST /internal/app_search/as_gate', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'post', - path: '/internal/app_search/as_gate', - }); - - registerAppSearchGatedFormRoute({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request handler', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/api/ent/v2/internal/as_gate', - }); - }); - - describe('validates', () => { - it('correctly', () => { - const request = { - body: { - as_gate_data: { - additional_feedback: '', - feature: 'Selected feature', - features_other: '', - participate_in_ux_labs: true, - }, - }, - }; - mockRouter.shouldValidate(request); - }); - - it('throws error unexpected values in body', () => { - const request = { - body: { - foo: 'bar', - }, - }; - mockRouter.shouldThrow(request); - }); - }); - }); -}); diff --git a/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/app_search_gated_form.ts b/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/app_search_gated_form.ts deleted file mode 100644 index 47bb3e0c93259..0000000000000 --- a/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/app_search_gated_form.ts +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { schema } from '@kbn/config-schema'; - -import { RouteDependencies } from '../../plugin'; - -export function registerAppSearchGatedFormRoute({ - router, - enterpriseSearchRequestHandler, -}: RouteDependencies) { - router.post( - { - path: '/internal/app_search/as_gate', - validate: { - body: schema.object({ - as_gate_data: schema.object({ - additional_feedback: schema.maybe(schema.string()), - feature: schema.string(), - features_other: schema.maybe(schema.string()), - participate_in_ux_labs: schema.maybe(schema.boolean()), - }), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/api/ent/v2/internal/as_gate', - }) - ); -} diff --git a/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/crawler.test.ts b/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/crawler.test.ts deleted file mode 100644 index 2684f9d9732a4..0000000000000 --- a/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/crawler.test.ts +++ /dev/null @@ -1,715 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { mockDependencies, mockRequestHandler, MockRouter } from '../../__mocks__'; - -import { registerCrawlerRoutes } from './crawler'; - -describe('crawler routes', () => { - describe('GET /internal/app_search/engines/{name}/crawler', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'get', - path: '/internal/app_search/engines/{name}/crawler', - }); - - registerCrawlerRoutes({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request to enterprise search', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/api/as/v1/engines/:name/crawler', - }); - }); - - it('validates correctly with name', () => { - const request = { params: { name: 'some-engine' } }; - mockRouter.shouldValidate(request); - }); - - it('fails validation without name', () => { - const request = { params: {} }; - mockRouter.shouldThrow(request); - }); - }); - - describe('GET /internal/app_search/engines/{name}/crawler/crawl_requests', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'get', - path: '/internal/app_search/engines/{name}/crawler/crawl_requests', - }); - - registerCrawlerRoutes({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request to enterprise search', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/api/as/v1/engines/:name/crawler/crawl_requests', - }); - }); - - it('validates correctly with name', () => { - const request = { params: { name: 'some-engine' } }; - mockRouter.shouldValidate(request); - }); - - it('fails validation without name', () => { - const request = { params: {} }; - mockRouter.shouldThrow(request); - }); - }); - - describe('GET /internal/app_search/engines/{name}/crawler/crawl_requests/{id}', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'get', - path: '/internal/app_search/engines/{name}/crawler/crawl_requests/{id}', - }); - - registerCrawlerRoutes({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request to enterprise search', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/api/as/v1/engines/:name/crawler/crawl_requests/:id', - }); - }); - - it('validates correctly with name and id', () => { - const request = { params: { name: 'some-engine', id: '12345' } }; - mockRouter.shouldValidate(request); - }); - - it('fails validation without name', () => { - const request = { params: { id: '12345' } }; - mockRouter.shouldThrow(request); - }); - - it('fails validation without id', () => { - const request = { params: { name: 'some-engine' } }; - mockRouter.shouldThrow(request); - }); - }); - - describe('POST /internal/app_search/engines/{name}/crawler/crawl_requests', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'post', - path: '/internal/app_search/engines/{name}/crawler/crawl_requests', - }); - - registerCrawlerRoutes({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request to enterprise search', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/api/as/v1/engines/:name/crawler/crawl_requests', - }); - }); - - it('validates correctly with name', () => { - const request = { params: { name: 'some-engine' } }; - mockRouter.shouldValidate(request); - }); - - it('validates correctly with domain urls', () => { - const request = { - params: { name: 'some-engine' }, - body: { overrides: { domain_allowlist: ['https://www.elastic.co'] } }, - }; - mockRouter.shouldValidate(request); - }); - - it('validates correctly with max crawl depth', () => { - const request = { - params: { name: 'some-engine' }, - body: { overrides: { max_crawl_depth: 10 } }, - }; - mockRouter.shouldValidate(request); - }); - - it('validates correctly with seed urls', () => { - const request = { - params: { name: 'some-engine' }, - body: { overrides: { seed_urls: ['https://www.elastic.co/guide'] } }, - }; - mockRouter.shouldValidate(request); - }); - - it('validates correctly with sitemap urls', () => { - const request = { - params: { name: 'some-engine' }, - body: { overrides: { sitemap_urls: ['https://www.elastic.co/sitemap1.xml'] } }, - }; - mockRouter.shouldValidate(request); - }); - - it('validates correctly when we set sitemap discovery', () => { - const request = { - params: { name: 'some-engine' }, - body: { overrides: { sitemap_discovery_disabled: true } }, - }; - mockRouter.shouldValidate(request); - }); - - it('validates correctly with empty overrides', () => { - const request = { params: { name: 'some-engine' }, body: { overrides: {} } }; - mockRouter.shouldValidate(request); - }); - - it('fails validation without name', () => { - const request = { params: {} }; - mockRouter.shouldThrow(request); - }); - }); - - describe('GET /internal/app_search/engines/{name}/crawler/domains', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'get', - path: '/internal/app_search/engines/{name}/crawler/domains', - }); - - registerCrawlerRoutes({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request to enterprise search', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/api/as/v1/engines/:name/crawler/domains', - }); - }); - - it('validates correctly', () => { - const request = { - params: { name: 'some-engine' }, - query: { - 'page[current]': 5, - 'page[size]': 10, - }, - }; - mockRouter.shouldValidate(request); - }); - - it('fails validation without required params', () => { - const request = { params: {} }; - mockRouter.shouldThrow(request); - }); - }); - - describe('POST /internal/app_search/engines/{name}/crawler/crawl_requests/cancel', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'post', - path: '/internal/app_search/engines/{name}/crawler/crawl_requests/cancel', - }); - - registerCrawlerRoutes({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request to enterprise search', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/api/as/v1/engines/:name/crawler/crawl_requests/active/cancel', - }); - }); - - it('validates correctly with name', () => { - const request = { params: { name: 'some-engine' } }; - mockRouter.shouldValidate(request); - }); - - it('fails validation without name', () => { - const request = { params: {} }; - mockRouter.shouldThrow(request); - }); - }); - - describe('POST /internal/app_search/engines/{name}/crawler/domains', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'post', - path: '/internal/app_search/engines/{name}/crawler/domains', - }); - - registerCrawlerRoutes({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request to enterprise search', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/api/as/v1/engines/:name/crawler/domains', - }); - }); - - it('validates correctly with params and body', () => { - const request = { - params: { name: 'some-engine' }, - body: { name: 'https://elastic.co/guide', entry_points: [{ value: '/guide' }] }, - }; - mockRouter.shouldValidate(request); - }); - - it('accepts a query param', () => { - const request = { - params: { name: 'some-engine' }, - body: { name: 'https://elastic.co/guide', entry_points: [{ value: '/guide' }] }, - query: { respond_with: 'crawler_details' }, - }; - mockRouter.shouldValidate(request); - }); - - it('fails validation without a name param', () => { - const request = { - params: {}, - body: { name: 'https://elastic.co/guide', entry_points: [{ value: '/guide' }] }, - }; - mockRouter.shouldThrow(request); - }); - - it('fails validation without a body', () => { - const request = { - params: { name: 'some-engine' }, - body: {}, - }; - mockRouter.shouldThrow(request); - }); - }); - - describe('DELETE /internal/app_search/engines/{name}/crawler/domains/{id}', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'delete', - path: '/internal/app_search/engines/{name}/crawler/domains/{id}', - }); - - registerCrawlerRoutes({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request to enterprise search', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/api/as/v1/engines/:name/crawler/domains/:id', - }); - }); - - it('validates correctly with name and id', () => { - const request = { params: { name: 'some-engine', id: '1234' } }; - mockRouter.shouldValidate(request); - }); - - it('fails validation without name', () => { - const request = { params: { id: '1234' } }; - mockRouter.shouldThrow(request); - }); - - it('fails validation without id', () => { - const request = { params: { name: 'test-engine' } }; - mockRouter.shouldThrow(request); - }); - - it('accepts a query param', () => { - const request = { - params: { name: 'test-engine', id: '1234' }, - query: { respond_with: 'crawler_details' }, - }; - mockRouter.shouldValidate(request); - }); - }); - - describe('PUT /internal/app_search/engines/{name}/crawler/domains/{id}', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'put', - path: '/internal/app_search/engines/{name}/crawler/domains/{id}', - }); - - registerCrawlerRoutes({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request to enterprise search', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/api/as/v1/engines/:name/crawler/domains/:id', - }); - }); - - it('validates correctly with crawl rules', () => { - const request = { - params: { name: 'some-engine', id: '1234' }, - body: { - crawl_rules: [ - { - order: 1, - id: '5678', - }, - ], - }, - }; - mockRouter.shouldValidate(request); - }); - - it('validates correctly with deduplication enabled', () => { - const request = { - params: { name: 'some-engine', id: '1234' }, - body: { - deduplication_enabled: true, - }, - }; - mockRouter.shouldValidate(request); - }); - - it('validates correctly with deduplication fields', () => { - const request = { - params: { name: 'some-engine', id: '1234' }, - body: { - deduplication_fields: ['title', 'description'], - }, - }; - mockRouter.shouldValidate(request); - }); - }); - - describe('GET /internal/app_search/engines/{name}/crawler/domains/{id}', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'get', - path: '/internal/app_search/engines/{name}/crawler/domains/{id}', - }); - - registerCrawlerRoutes({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request to enterprise search', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/api/as/v1/engines/:name/crawler/domains/:id', - }); - }); - - it('validates correctly with name and id', () => { - const request = { params: { name: 'some-engine', id: '1234' } }; - mockRouter.shouldValidate(request); - }); - - it('fails validation without name', () => { - const request = { params: { id: '1234' } }; - mockRouter.shouldThrow(request); - }); - - it('fails validation without id', () => { - const request = { params: { name: 'test-engine' } }; - mockRouter.shouldThrow(request); - }); - }); - - describe('POST /internal/app_search/crawler/validate_url', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'post', - path: '/internal/app_search/crawler/validate_url', - }); - - registerCrawlerRoutes({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request to enterprise search', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/api/as/v1/crawler/validate_url', - }); - }); - - it('validates correctly with body', () => { - const request = { - body: { url: 'elastic.co', checks: ['tcp', 'url_request'] }, - }; - mockRouter.shouldValidate(request); - }); - - it('fails validation without a body', () => { - const request = { - body: {}, - }; - mockRouter.shouldThrow(request); - }); - }); - - describe('POST /internal/app_search/engines/{name}/crawler/process_crawls', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'post', - path: '/internal/app_search/engines/{name}/crawler/process_crawls', - }); - - registerCrawlerRoutes({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request to enterprise search', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/api/as/v1/engines/:name/crawler/process_crawls', - }); - }); - - it('validates correctly', () => { - const request = { - params: { name: 'some-engine' }, - body: { domains: ['https://elastic.co', 'https://swiftype.com'] }, - }; - mockRouter.shouldValidate(request); - }); - - it('validates correctly without body', () => { - const request = { - params: { name: 'some-engine' }, - body: {}, - }; - mockRouter.shouldValidate(request); - }); - - it('fails validation without a name param', () => { - const request = { - params: {}, - body: { domains: ['https://elastic.co', 'https://swiftype.com'] }, - }; - mockRouter.shouldThrow(request); - }); - }); - - describe('GET /internal/app_search/engines/{name}/crawler/crawl_schedule', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'get', - path: '/internal/app_search/engines/{name}/crawler/crawl_schedule', - }); - - registerCrawlerRoutes({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request to enterprise search', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/api/as/v1/engines/:name/crawler/crawl_schedule', - }); - }); - - it('validates correctly', () => { - const request = { - params: { name: 'some-engine' }, - }; - mockRouter.shouldValidate(request); - }); - - it('fails validation without a name param', () => { - const request = { - params: {}, - }; - mockRouter.shouldThrow(request); - }); - }); - - describe('PUT /internal/app_search/engines/{name}/crawler/crawl_schedule', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'put', - path: '/internal/app_search/engines/{name}/crawler/crawl_schedule', - }); - - registerCrawlerRoutes({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request to enterprise search', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/api/as/v1/engines/:name/crawler/crawl_schedule', - }); - }); - - it('validates correctly', () => { - const request = { - params: { name: 'some-engine' }, - body: { unit: 'day', frequency: 7 }, - }; - mockRouter.shouldValidate(request); - }); - - it('fails validation without a name param', () => { - const request = { - params: {}, - body: { unit: 'day', frequency: 7 }, - }; - mockRouter.shouldThrow(request); - }); - - it('fails validation without a unit property in body', () => { - const request = { - params: { name: 'some-engine' }, - body: { frequency: 7 }, - }; - mockRouter.shouldThrow(request); - }); - - it('fails validation without a frequency property in body', () => { - const request = { - params: { name: 'some-engine' }, - body: { unit: 'day' }, - }; - mockRouter.shouldThrow(request); - }); - }); - - describe('DELETE /internal/app_search/engines/{name}/crawler/crawl_schedule', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'delete', - path: '/internal/app_search/engines/{name}/crawler/crawl_schedule', - }); - - registerCrawlerRoutes({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request to enterprise search', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/api/as/v1/engines/:name/crawler/crawl_schedule', - }); - }); - - it('validates correctly', () => { - const request = { - params: { name: 'some-engine' }, - }; - mockRouter.shouldValidate(request); - }); - - it('fails validation without a name param', () => { - const request = { - params: {}, - }; - mockRouter.shouldThrow(request); - }); - }); - - describe('GET /internal/app_search/engines/{name}/crawler/domain_configs', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'get', - path: '/internal/app_search/engines/{name}/crawler/domain_configs', - }); - - registerCrawlerRoutes({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request to enterprise search', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/api/as/v1/engines/:name/crawler/domain_configs', - }); - }); - - it('validates correctly with name', () => { - const request = { params: { name: 'some-engine' }, query: { 'page[current]': 4 } }; - mockRouter.shouldValidate(request); - }); - - it('validates correctly with page[current]', () => { - const request = { params: { name: 'some-engine' }, query: { 'page[size]': 100 } }; - mockRouter.shouldValidate(request); - }); - - it('fails validation without page[size]', () => { - const request = { params: {} }; - mockRouter.shouldThrow(request); - }); - }); -}); diff --git a/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/crawler.ts b/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/crawler.ts deleted file mode 100644 index 4e8f6ab527e85..0000000000000 --- a/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/crawler.ts +++ /dev/null @@ -1,296 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { schema } from '@kbn/config-schema'; - -import { RouteDependencies } from '../../plugin'; - -export function registerCrawlerRoutes({ - router, - enterpriseSearchRequestHandler, -}: RouteDependencies) { - router.get( - { - path: '/internal/app_search/engines/{name}/crawler', - validate: { - params: schema.object({ - name: schema.string(), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/api/as/v1/engines/:name/crawler', - }) - ); - - router.get( - { - path: '/internal/app_search/engines/{name}/crawler/crawl_requests', - validate: { - params: schema.object({ - name: schema.string(), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/api/as/v1/engines/:name/crawler/crawl_requests', - }) - ); - - router.get( - { - path: '/internal/app_search/engines/{name}/crawler/crawl_requests/{id}', - validate: { - params: schema.object({ - name: schema.string(), - id: schema.string(), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/api/as/v1/engines/:name/crawler/crawl_requests/:id', - }) - ); - - router.post( - { - path: '/internal/app_search/engines/{name}/crawler/crawl_requests', - validate: { - params: schema.object({ - name: schema.string(), - }), - body: schema.object({ - overrides: schema.maybe( - schema.object({ - domain_allowlist: schema.maybe(schema.arrayOf(schema.string())), - max_crawl_depth: schema.maybe(schema.number()), - seed_urls: schema.maybe(schema.arrayOf(schema.string())), - sitemap_urls: schema.maybe(schema.arrayOf(schema.string())), - sitemap_discovery_disabled: schema.maybe(schema.boolean()), - }) - ), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/api/as/v1/engines/:name/crawler/crawl_requests', - }) - ); - - router.post( - { - path: '/internal/app_search/engines/{name}/crawler/crawl_requests/cancel', - validate: { - params: schema.object({ - name: schema.string(), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/api/as/v1/engines/:name/crawler/crawl_requests/active/cancel', - }) - ); - - router.get( - { - path: '/internal/app_search/engines/{name}/crawler/domains', - validate: { - params: schema.object({ - name: schema.string(), - }), - query: schema.object({ - 'page[current]': schema.number(), - 'page[size]': schema.number(), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/api/as/v1/engines/:name/crawler/domains', - }) - ); - - router.post( - { - path: '/internal/app_search/engines/{name}/crawler/domains', - validate: { - params: schema.object({ - name: schema.string(), - }), - body: schema.object({ - name: schema.string(), - entry_points: schema.arrayOf( - schema.object({ - value: schema.string(), - }) - ), - }), - query: schema.object({ - respond_with: schema.maybe(schema.string()), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/api/as/v1/engines/:name/crawler/domains', - }) - ); - - router.get( - { - path: '/internal/app_search/engines/{name}/crawler/domains/{id}', - validate: { - params: schema.object({ - name: schema.string(), - id: schema.string(), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/api/as/v1/engines/:name/crawler/domains/:id', - }) - ); - - router.delete( - { - path: '/internal/app_search/engines/{name}/crawler/domains/{id}', - validate: { - params: schema.object({ - name: schema.string(), - id: schema.string(), - }), - query: schema.object({ - respond_with: schema.maybe(schema.string()), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/api/as/v1/engines/:name/crawler/domains/:id', - }) - ); - - router.put( - { - path: '/internal/app_search/engines/{name}/crawler/domains/{id}', - validate: { - params: schema.object({ - name: schema.string(), - id: schema.string(), - }), - body: schema.object({ - crawl_rules: schema.maybe( - schema.arrayOf( - schema.object({ - order: schema.number(), - id: schema.string(), - }) - ) - ), - deduplication_enabled: schema.maybe(schema.boolean()), - deduplication_fields: schema.maybe(schema.arrayOf(schema.string())), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/api/as/v1/engines/:name/crawler/domains/:id', - }) - ); - - router.post( - { - path: '/internal/app_search/crawler/validate_url', - validate: { - body: schema.object({ - url: schema.string(), - checks: schema.arrayOf(schema.string()), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/api/as/v1/crawler/validate_url', - }) - ); - - router.post( - { - path: '/internal/app_search/engines/{name}/crawler/process_crawls', - validate: { - params: schema.object({ - name: schema.string(), - }), - body: schema.object({ - domains: schema.maybe(schema.arrayOf(schema.string())), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/api/as/v1/engines/:name/crawler/process_crawls', - }) - ); - - router.get( - { - path: '/internal/app_search/engines/{name}/crawler/crawl_schedule', - validate: { - params: schema.object({ - name: schema.string(), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/api/as/v1/engines/:name/crawler/crawl_schedule', - }) - ); - - router.put( - { - path: '/internal/app_search/engines/{name}/crawler/crawl_schedule', - validate: { - params: schema.object({ - name: schema.string(), - }), - body: schema.object({ - unit: schema.string(), - frequency: schema.number(), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/api/as/v1/engines/:name/crawler/crawl_schedule', - }) - ); - - router.delete( - { - path: '/internal/app_search/engines/{name}/crawler/crawl_schedule', - validate: { - params: schema.object({ - name: schema.string(), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/api/as/v1/engines/:name/crawler/crawl_schedule', - }) - ); - - router.get( - { - path: '/internal/app_search/engines/{name}/crawler/domain_configs', - validate: { - params: schema.object({ - name: schema.string(), - }), - query: schema.object({ - 'page[current]': schema.maybe(schema.number()), - 'page[size]': schema.maybe(schema.number()), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/api/as/v1/engines/:name/crawler/domain_configs', - }) - ); -} diff --git a/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/crawler_crawl_rules.test.ts b/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/crawler_crawl_rules.test.ts deleted file mode 100644 index c3d1468687ec4..0000000000000 --- a/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/crawler_crawl_rules.test.ts +++ /dev/null @@ -1,137 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { mockDependencies, mockRequestHandler, MockRouter } from '../../__mocks__'; - -import { registerCrawlerCrawlRulesRoutes } from './crawler_crawl_rules'; - -describe('crawler crawl rules routes', () => { - describe('POST /internal/app_search/engines/{engineName}/crawler/domains/{domainId}/crawl_rules', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'post', - path: '/internal/app_search/engines/{engineName}/crawler/domains/{domainId}/crawl_rules', - }); - - registerCrawlerCrawlRulesRoutes({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request to enterprise search', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/api/as/v1/engines/:engineName/crawler/domains/:domainId/crawl_rules', - params: { - respond_with: 'index', - }, - }); - }); - - it('validates correctly with required params', () => { - const request = { - params: { engineName: 'some-engine', domainId: '1234' }, - body: { - pattern: '*', - policy: 'allow', - rule: 'begins', - }, - }; - mockRouter.shouldValidate(request); - }); - - it('fails otherwise', () => { - const request = { params: {}, body: {} }; - mockRouter.shouldThrow(request); - }); - }); - - describe('PUT /internal/app_search/engines/{engineName}/crawler/domains/{domainId}/crawl_rules/{crawlRuleId}', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'put', - path: '/internal/app_search/engines/{engineName}/crawler/domains/{domainId}/crawl_rules/{crawlRuleId}', - }); - - registerCrawlerCrawlRulesRoutes({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request to enterprise search', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/api/as/v1/engines/:engineName/crawler/domains/:domainId/crawl_rules/:crawlRuleId', - params: { - respond_with: 'index', - }, - }); - }); - - it('validates correctly with required params', () => { - const request = { - params: { engineName: 'some-engine', domainId: '1234', crawlRuleId: '5678' }, - body: { - order: 1, - pattern: '*', - policy: 'allow', - rule: 'begins', - }, - }; - mockRouter.shouldValidate(request); - }); - - it('fails otherwise', () => { - const request = { params: {}, body: {} }; - mockRouter.shouldThrow(request); - }); - }); - - describe('DELETE /internal/app_search/engines/{engineName}/crawler/domains/{domainId}/crawl_rules/{crawlRuleId}', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'delete', - path: '/internal/app_search/engines/{engineName}/crawler/domains/{domainId}/crawl_rules/{crawlRuleId}', - }); - - registerCrawlerCrawlRulesRoutes({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request to enterprise search', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/api/as/v1/engines/:engineName/crawler/domains/:domainId/crawl_rules/:crawlRuleId', - params: { - respond_with: 'index', - }, - }); - }); - - it('validates correctly with required params', () => { - const request = { - params: { engineName: 'some-engine', domainId: '1234', crawlRuleId: '5678' }, - }; - mockRouter.shouldValidate(request); - }); - - it('fails otherwise', () => { - const request = { params: {} }; - mockRouter.shouldThrow(request); - }); - }); -}); diff --git a/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/crawler_crawl_rules.ts b/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/crawler_crawl_rules.ts deleted file mode 100644 index 26637623f0885..0000000000000 --- a/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/crawler_crawl_rules.ts +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { schema } from '@kbn/config-schema'; - -import { RouteDependencies } from '../../plugin'; - -export function registerCrawlerCrawlRulesRoutes({ - router, - enterpriseSearchRequestHandler, -}: RouteDependencies) { - router.post( - { - path: '/internal/app_search/engines/{engineName}/crawler/domains/{domainId}/crawl_rules', - validate: { - params: schema.object({ - engineName: schema.string(), - domainId: schema.string(), - }), - body: schema.object({ - pattern: schema.string(), - policy: schema.string(), - rule: schema.string(), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/api/as/v1/engines/:engineName/crawler/domains/:domainId/crawl_rules', - params: { - respond_with: 'index', - }, - }) - ); - - router.put( - { - path: '/internal/app_search/engines/{engineName}/crawler/domains/{domainId}/crawl_rules/{crawlRuleId}', - validate: { - params: schema.object({ - engineName: schema.string(), - domainId: schema.string(), - crawlRuleId: schema.string(), - }), - body: schema.object({ - order: schema.number(), - pattern: schema.string(), - policy: schema.string(), - rule: schema.string(), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/api/as/v1/engines/:engineName/crawler/domains/:domainId/crawl_rules/:crawlRuleId', - params: { - respond_with: 'index', - }, - }) - ); - - router.delete( - { - path: '/internal/app_search/engines/{engineName}/crawler/domains/{domainId}/crawl_rules/{crawlRuleId}', - validate: { - params: schema.object({ - engineName: schema.string(), - domainId: schema.string(), - crawlRuleId: schema.string(), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/api/as/v1/engines/:engineName/crawler/domains/:domainId/crawl_rules/:crawlRuleId', - params: { - respond_with: 'index', - }, - }) - ); -} diff --git a/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/crawler_entry_points.test.ts b/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/crawler_entry_points.test.ts deleted file mode 100644 index dc7ad493a5149..0000000000000 --- a/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/crawler_entry_points.test.ts +++ /dev/null @@ -1,132 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { mockDependencies, mockRequestHandler, MockRouter } from '../../__mocks__'; - -import { registerCrawlerEntryPointRoutes } from './crawler_entry_points'; - -describe('crawler entry point routes', () => { - describe('POST /internal/app_search/engines/{engineName}/crawler/domains/{domainId}/entry_points', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'post', - path: '/internal/app_search/engines/{engineName}/crawler/domains/{domainId}/entry_points', - }); - - registerCrawlerEntryPointRoutes({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request to enterprise search', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/api/as/v1/engines/:engineName/crawler/domains/:domainId/entry_points', - params: { - respond_with: 'index', - }, - }); - }); - - it('validates correctly with required params', () => { - const request = { - params: { engineName: 'some-engine', domainId: '1234' }, - body: { - value: 'test', - }, - }; - mockRouter.shouldValidate(request); - }); - - it('fails otherwise', () => { - const request = { params: {}, body: {} }; - mockRouter.shouldThrow(request); - }); - }); - - describe('PUT /internal/app_search/engines/{engineName}/crawler/domains/{domainId}/entry_points/{entryPointId}', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'put', - path: '/internal/app_search/engines/{engineName}/crawler/domains/{domainId}/entry_points/{entryPointId}', - }); - - registerCrawlerEntryPointRoutes({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request to enterprise search', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/api/as/v1/engines/:engineName/crawler/domains/:domainId/entry_points/:entryPointId', - params: { - respond_with: 'index', - }, - }); - }); - - it('validates correctly with required params', () => { - const request = { - params: { engineName: 'some-engine', domainId: '1234', entryPointId: '5678' }, - body: { - value: 'test', - }, - }; - mockRouter.shouldValidate(request); - }); - - it('fails otherwise', () => { - const request = { params: {}, body: {} }; - mockRouter.shouldThrow(request); - }); - }); - - describe('DELETE /internal/app_search/engines/{engineName}/crawler/domains/{domainId}/entry_points/{entryPointId}', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'delete', - path: '/internal/app_search/engines/{engineName}/crawler/domains/{domainId}/entry_points/{entryPointId}', - }); - - registerCrawlerEntryPointRoutes({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request to enterprise search', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/api/as/v1/engines/:engineName/crawler/domains/:domainId/entry_points/:entryPointId', - params: { - respond_with: 'index', - }, - }); - }); - - it('validates correctly with required params', () => { - const request = { - params: { engineName: 'some-engine', domainId: '1234', entryPointId: '5678' }, - }; - mockRouter.shouldValidate(request); - }); - - it('fails otherwise', () => { - const request = { params: {} }; - mockRouter.shouldThrow(request); - }); - }); -}); diff --git a/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/crawler_entry_points.ts b/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/crawler_entry_points.ts deleted file mode 100644 index fd81475c860ad..0000000000000 --- a/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/crawler_entry_points.ts +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { schema } from '@kbn/config-schema'; - -import { RouteDependencies } from '../../plugin'; - -export function registerCrawlerEntryPointRoutes({ - router, - enterpriseSearchRequestHandler, -}: RouteDependencies) { - router.post( - { - path: '/internal/app_search/engines/{engineName}/crawler/domains/{domainId}/entry_points', - validate: { - params: schema.object({ - engineName: schema.string(), - domainId: schema.string(), - }), - body: schema.object({ - value: schema.string(), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/api/as/v1/engines/:engineName/crawler/domains/:domainId/entry_points', - params: { - respond_with: 'index', - }, - }) - ); - - router.put( - { - path: '/internal/app_search/engines/{engineName}/crawler/domains/{domainId}/entry_points/{entryPointId}', - validate: { - params: schema.object({ - engineName: schema.string(), - domainId: schema.string(), - entryPointId: schema.string(), - }), - body: schema.object({ - value: schema.string(), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/api/as/v1/engines/:engineName/crawler/domains/:domainId/entry_points/:entryPointId', - params: { - respond_with: 'index', - }, - }) - ); - - router.delete( - { - path: '/internal/app_search/engines/{engineName}/crawler/domains/{domainId}/entry_points/{entryPointId}', - validate: { - params: schema.object({ - engineName: schema.string(), - domainId: schema.string(), - entryPointId: schema.string(), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/api/as/v1/engines/:engineName/crawler/domains/:domainId/entry_points/:entryPointId', - params: { - respond_with: 'index', - }, - }) - ); -} diff --git a/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/crawler_sitemaps.test.ts b/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/crawler_sitemaps.test.ts deleted file mode 100644 index 3d6eb86bcba26..0000000000000 --- a/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/crawler_sitemaps.test.ts +++ /dev/null @@ -1,132 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { mockDependencies, mockRequestHandler, MockRouter } from '../../__mocks__'; - -import { registerCrawlerSitemapRoutes } from './crawler_sitemaps'; - -describe('crawler sitemap routes', () => { - describe('POST /internal/app_search/engines/{engineName}/crawler/domains/{domainId}/sitemaps', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'post', - path: '/internal/app_search/engines/{engineName}/crawler/domains/{domainId}/sitemaps', - }); - - registerCrawlerSitemapRoutes({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request to enterprise search', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/api/as/v1/engines/:engineName/crawler/domains/:domainId/sitemaps', - params: { - respond_with: 'index', - }, - }); - }); - - it('validates correctly with required params', () => { - const request = { - params: { engineName: 'some-engine', domainId: '1234' }, - body: { - url: 'http://www.example.com/sitemaps.xml', - }, - }; - mockRouter.shouldValidate(request); - }); - - it('fails otherwise', () => { - const request = { params: {}, body: {} }; - mockRouter.shouldThrow(request); - }); - }); - - describe('PUT /internal/app_search/engines/{engineName}/crawler/domains/{domainId}/sitemaps/{sitemapId}', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'put', - path: '/internal/app_search/engines/{engineName}/crawler/domains/{domainId}/sitemaps/{sitemapId}', - }); - - registerCrawlerSitemapRoutes({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request to enterprise search', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/api/as/v1/engines/:engineName/crawler/domains/:domainId/sitemaps/:sitemapId', - params: { - respond_with: 'index', - }, - }); - }); - - it('validates correctly with required params', () => { - const request = { - params: { engineName: 'some-engine', domainId: '1234', sitemapId: '5678' }, - body: { - url: 'http://www.example.com/sitemaps.xml', - }, - }; - mockRouter.shouldValidate(request); - }); - - it('fails otherwise', () => { - const request = { params: {}, body: {} }; - mockRouter.shouldThrow(request); - }); - }); - - describe('DELETE /internal/app_search/engines/{engineName}/crawler/domains/{domainId}/sitemaps/{sitemapId}', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'delete', - path: '/internal/app_search/engines/{engineName}/crawler/domains/{domainId}/sitemaps/{sitemapId}', - }); - - registerCrawlerSitemapRoutes({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request to enterprise search', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/api/as/v1/engines/:engineName/crawler/domains/:domainId/sitemaps/:sitemapId', - params: { - respond_with: 'index', - }, - }); - }); - - it('validates correctly with required params', () => { - const request = { - params: { engineName: 'some-engine', domainId: '1234', sitemapId: '5678' }, - }; - mockRouter.shouldValidate(request); - }); - - it('fails otherwise', () => { - const request = { params: {} }; - mockRouter.shouldThrow(request); - }); - }); -}); diff --git a/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/crawler_sitemaps.ts b/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/crawler_sitemaps.ts deleted file mode 100644 index 0965acd967306..0000000000000 --- a/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/crawler_sitemaps.ts +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { schema } from '@kbn/config-schema'; - -import { RouteDependencies } from '../../plugin'; - -export function registerCrawlerSitemapRoutes({ - router, - enterpriseSearchRequestHandler, -}: RouteDependencies) { - router.post( - { - path: '/internal/app_search/engines/{engineName}/crawler/domains/{domainId}/sitemaps', - validate: { - params: schema.object({ - engineName: schema.string(), - domainId: schema.string(), - }), - body: schema.object({ - url: schema.string(), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/api/as/v1/engines/:engineName/crawler/domains/:domainId/sitemaps', - params: { - respond_with: 'index', - }, - }) - ); - - router.put( - { - path: '/internal/app_search/engines/{engineName}/crawler/domains/{domainId}/sitemaps/{sitemapId}', - validate: { - params: schema.object({ - engineName: schema.string(), - domainId: schema.string(), - sitemapId: schema.string(), - }), - body: schema.object({ - url: schema.string(), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/api/as/v1/engines/:engineName/crawler/domains/:domainId/sitemaps/:sitemapId', - params: { - respond_with: 'index', - }, - }) - ); - - router.delete( - { - path: '/internal/app_search/engines/{engineName}/crawler/domains/{domainId}/sitemaps/{sitemapId}', - validate: { - params: schema.object({ - engineName: schema.string(), - domainId: schema.string(), - sitemapId: schema.string(), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/api/as/v1/engines/:engineName/crawler/domains/:domainId/sitemaps/:sitemapId', - params: { - respond_with: 'index', - }, - }) - ); -} diff --git a/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/credentials.test.ts b/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/credentials.test.ts deleted file mode 100644 index 292d9200f2a4e..0000000000000 --- a/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/credentials.test.ts +++ /dev/null @@ -1,322 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { MockRouter, mockRequestHandler, mockDependencies } from '../../__mocks__'; - -import { registerCredentialsRoutes } from './credentials'; - -describe('credentials routes', () => { - describe('GET /internal/app_search/credentials', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'get', - path: '/internal/app_search/credentials', - }); - - registerCredentialsRoutes({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request handler', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/as/credentials/collection', - }); - }); - - describe('validates', () => { - it('correctly', () => { - const request = { - query: { - 'page[current]': 1, - 'page[size]': 10, - }, - }; - mockRouter.shouldValidate(request); - }); - - it('missing page query params', () => { - const request = { query: {} }; - mockRouter.shouldThrow(request); - }); - }); - }); - - describe('POST /internal/app_search/credentials', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'post', - path: '/internal/app_search/credentials', - }); - - registerCredentialsRoutes({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request handler', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/as/credentials/collection', - }); - }); - - describe('validates', () => { - describe('admin keys', () => { - it('correctly', () => { - const request = { - body: { - name: 'admin-key', - type: 'admin', - }, - }; - mockRouter.shouldValidate(request); - }); - - it('throws on unnecessary properties', () => { - const request = { - body: { - name: 'admin-key', - type: 'admin', - read: true, - access_all_engines: true, - }, - }; - mockRouter.shouldThrow(request); - }); - }); - - describe('private keys', () => { - it('correctly', () => { - const request = { - body: { - name: 'private-key', - type: 'private', - read: true, - write: false, - access_all_engines: false, - engines: ['engine1', 'engine2'], - }, - }; - mockRouter.shouldValidate(request); - }); - - it('throws on missing keys', () => { - const request = { - body: { - name: 'private-key', - type: 'private', - }, - }; - mockRouter.shouldThrow(request); - }); - }); - - describe('search keys', () => { - it('correctly', () => { - const request = { - body: { - name: 'search-key', - type: 'search', - access_all_engines: true, - }, - }; - mockRouter.shouldValidate(request); - }); - - it('throws on missing keys', () => { - const request = { - body: { - name: 'search-key', - type: 'search', - }, - }; - mockRouter.shouldThrow(request); - }); - - it('throws on extra keys', () => { - const request = { - body: { - name: 'search-key', - type: 'search', - read: true, - write: false, - access_all_engines: false, - engines: ['engine1', 'engine2'], - }, - }; - mockRouter.shouldThrow(request); - }); - }); - }); - }); - - describe('GET /internal/app_search/credentials/details', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'get', - path: '/internal/app_search/credentials/details', - }); - - registerCredentialsRoutes({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request handler', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/as/credentials/details', - }); - }); - }); - - describe('PUT /internal/app_search/credentials/{name}', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'put', - path: '/internal/app_search/credentials/{name}', - }); - - registerCredentialsRoutes({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request to enterprise search', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/as/credentials/:name', - }); - }); - - describe('validates', () => { - describe('admin keys', () => { - it('correctly', () => { - const request = { - body: { - name: 'admin-key', - type: 'admin', - }, - }; - mockRouter.shouldValidate(request); - }); - - it('throws on unnecessary properties', () => { - const request = { - body: { - name: 'admin-key', - type: 'admin', - read: true, - access_all_engines: true, - }, - }; - mockRouter.shouldThrow(request); - }); - }); - - describe('private keys', () => { - it('correctly', () => { - const request = { - body: { - name: 'private-key', - type: 'private', - read: true, - write: false, - access_all_engines: false, - engines: ['engine1', 'engine2'], - }, - }; - mockRouter.shouldValidate(request); - }); - - it('throws on missing keys', () => { - const request = { - body: { - name: 'private-key', - type: 'private', - }, - }; - mockRouter.shouldThrow(request); - }); - }); - - describe('search keys', () => { - it('correctly', () => { - const request = { - body: { - name: 'search-key', - type: 'search', - access_all_engines: true, - }, - }; - mockRouter.shouldValidate(request); - }); - - it('throws on missing keys', () => { - const request = { - body: { - name: 'search-key', - type: 'search', - }, - }; - mockRouter.shouldThrow(request); - }); - - it('throws on extra keys', () => { - const request = { - body: { - name: 'search-key', - type: 'search', - read: true, - write: false, - access_all_engines: false, - engines: ['engine1', 'engine2'], - }, - }; - mockRouter.shouldThrow(request); - }); - }); - }); - }); - - describe('DELETE /internal/app_search/credentials/{name}', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'delete', - path: '/internal/app_search/credentials/{name}', - }); - - registerCredentialsRoutes({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request to enterprise search', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/as/credentials/:name', - }); - }); - }); -}); diff --git a/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/credentials.ts b/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/credentials.ts deleted file mode 100644 index b1916479a39ac..0000000000000 --- a/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/credentials.ts +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { schema } from '@kbn/config-schema'; - -import { RouteDependencies } from '../../plugin'; - -const tokenSchema = schema.oneOf([ - schema.object({ - name: schema.string(), - type: schema.literal('admin'), - }), - schema.object({ - name: schema.string(), - type: schema.literal('private'), - read: schema.boolean(), - write: schema.boolean(), - access_all_engines: schema.boolean(), - engines: schema.maybe(schema.arrayOf(schema.string())), - }), - schema.object({ - name: schema.string(), - type: schema.literal('search'), - access_all_engines: schema.boolean(), - engines: schema.maybe(schema.arrayOf(schema.string())), - }), -]); - -export function registerCredentialsRoutes({ - router, - enterpriseSearchRequestHandler, -}: RouteDependencies) { - // Credentials API - router.get( - { - path: '/internal/app_search/credentials', - validate: { - query: schema.object({ - 'page[current]': schema.number(), - 'page[size]': schema.number(), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/as/credentials/collection', - }) - ); - router.post( - { - path: '/internal/app_search/credentials', - validate: { - body: tokenSchema, - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/as/credentials/collection', - }) - ); - - // TODO: It would be great to remove this someday - router.get( - { - path: '/internal/app_search/credentials/details', - validate: false, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/as/credentials/details', - }) - ); - - // Single credential API - router.put( - { - path: '/internal/app_search/credentials/{name}', - validate: { - params: schema.object({ - name: schema.string(), - }), - body: tokenSchema, - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/as/credentials/:name', - }) - ); - router.delete( - { - path: '/internal/app_search/credentials/{name}', - validate: { - params: schema.object({ - name: schema.string(), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/as/credentials/:name', - }) - ); -} diff --git a/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/curations.test.ts b/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/curations.test.ts deleted file mode 100644 index 8e2221b8d8f32..0000000000000 --- a/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/curations.test.ts +++ /dev/null @@ -1,232 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { MockRouter, mockRequestHandler, mockDependencies } from '../../__mocks__'; - -import { registerCurationsRoutes } from './curations'; - -describe('curations routes', () => { - describe('GET /internal/app_search/engines/{engineName}/curations', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'get', - path: '/internal/app_search/engines/{engineName}/curations', - }); - - registerCurationsRoutes({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request handler', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/as/engines/:engineName/curations/collection', - }); - }); - - describe('validates', () => { - it('with pagination query params', () => { - const request = { - query: { - 'page[current]': 1, - 'page[size]': 10, - }, - }; - mockRouter.shouldValidate(request); - }); - - it('missing query params', () => { - const request = { query: {} }; - mockRouter.shouldThrow(request); - }); - }); - }); - - describe('POST /internal/app_search/engines/{engineName}/curations', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'post', - path: '/internal/app_search/engines/{engineName}/curations', - }); - - registerCurationsRoutes({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request handler', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/as/engines/:engineName/curations/collection', - }); - }); - - describe('validates', () => { - it('with curation queries', () => { - const request = { - body: { - queries: ['a', 'b', 'c'], - }, - }; - mockRouter.shouldValidate(request); - }); - - it('empty queries array', () => { - const request = { - body: { - queries: [], - }, - }; - mockRouter.shouldThrow(request); - }); - - it('empty query strings', () => { - const request = { - body: { - queries: ['', '', ''], - }, - }; - mockRouter.shouldThrow(request); - }); - - it('missing queries', () => { - const request = { body: {} }; - mockRouter.shouldThrow(request); - }); - }); - }); - - describe('DELETE /internal/app_search/engines/{engineName}/curations/{curationId}', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'delete', - path: '/internal/app_search/engines/{engineName}/curations/{curationId}', - }); - - registerCurationsRoutes({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request handler', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/as/engines/:engineName/curations/:curationId', - }); - }); - }); - - describe('GET /internal/app_search/engines/{engineName}/curations/{curationId}', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'get', - path: '/internal/app_search/engines/{engineName}/curations/{curationId}', - }); - - registerCurationsRoutes({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request handler', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/as/engines/:engineName/curations/:curationId', - }); - }); - }); - - describe('PUT /internal/app_search/engines/{engineName}/curations/{curationId}', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'put', - path: '/internal/app_search/engines/{engineName}/curations/{curationId}', - }); - - registerCurationsRoutes({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request handler', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/as/engines/:engineName/curations/:curationId', - }); - }); - - describe('validates', () => { - it('required body', () => { - const request = { - body: { - query: 'hello', - queries: ['hello', 'world'], - promoted: ['some-doc-id'], - hidden: ['another-doc-id'], - }, - }; - mockRouter.shouldValidate(request); - }); - - it('missing body', () => { - const request = { body: {} }; - mockRouter.shouldThrow(request); - }); - }); - }); - - describe('POST /internal/app_search/engines/{engineName}/curations/find_or_create', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'post', - path: '/internal/app_search/engines/{engineName}/curations/find_or_create', - }); - - registerCurationsRoutes({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request handler', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/as/engines/:engineName/curations/find_or_create', - }); - }); - - describe('validates', () => { - it('required query param', () => { - const request = { body: { query: 'some query' } }; - mockRouter.shouldValidate(request); - }); - - it('missing query', () => { - const request = { body: {} }; - mockRouter.shouldThrow(request); - }); - }); - }); -}); diff --git a/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/curations.ts b/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/curations.ts deleted file mode 100644 index 27927f2c36913..0000000000000 --- a/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/curations.ts +++ /dev/null @@ -1,124 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { schema } from '@kbn/config-schema'; - -import { RouteDependencies } from '../../plugin'; - -export function registerCurationsRoutes({ - router, - enterpriseSearchRequestHandler, -}: RouteDependencies) { - router.get( - { - path: '/internal/app_search/engines/{engineName}/curations', - validate: { - params: schema.object({ - engineName: schema.string(), - }), - query: schema.object({ - 'page[current]': schema.number(), - 'page[size]': schema.number(), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/as/engines/:engineName/curations/collection', - }) - ); - - router.post( - { - path: '/internal/app_search/engines/{engineName}/curations', - validate: { - params: schema.object({ - engineName: schema.string(), - }), - body: schema.object({ - queries: schema.arrayOf(schema.string({ minLength: 1 }), { minSize: 1 }), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/as/engines/:engineName/curations/collection', - }) - ); - - router.delete( - { - path: '/internal/app_search/engines/{engineName}/curations/{curationId}', - validate: { - params: schema.object({ - engineName: schema.string(), - curationId: schema.string(), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/as/engines/:engineName/curations/:curationId', - }) - ); - - router.get( - { - path: '/internal/app_search/engines/{engineName}/curations/{curationId}', - validate: { - query: schema.object({ - skip_record_analytics: schema.string(), - }), - params: schema.object({ - engineName: schema.string(), - curationId: schema.string(), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/as/engines/:engineName/curations/:curationId', - }) - ); - - router.put( - { - path: '/internal/app_search/engines/{engineName}/curations/{curationId}', - validate: { - query: schema.object({ - skip_record_analytics: schema.string(), - }), - params: schema.object({ - engineName: schema.string(), - curationId: schema.string(), - }), - body: schema.object({ - query: schema.string(), - queries: schema.arrayOf(schema.string()), - promoted: schema.arrayOf(schema.string()), - hidden: schema.arrayOf(schema.string()), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/as/engines/:engineName/curations/:curationId', - }) - ); - - router.post( - { - path: '/internal/app_search/engines/{engineName}/curations/find_or_create', - validate: { - params: schema.object({ - engineName: schema.string(), - }), - body: schema.object({ - query: schema.string(), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/as/engines/:engineName/curations/find_or_create', - }) - ); -} diff --git a/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/documents.test.ts b/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/documents.test.ts deleted file mode 100644 index efccb5d3f6e3c..0000000000000 --- a/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/documents.test.ts +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { MockRouter, mockRequestHandler, mockDependencies } from '../../__mocks__'; - -import { registerDocumentsRoutes, registerDocumentRoutes } from './documents'; - -describe('documents routes', () => { - describe('POST /internal/app_search/engines/{engineName}/documents', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'post', - path: '/internal/app_search/engines/{engineName}/documents', - }); - - registerDocumentsRoutes({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request to enterprise search', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/as/engines/:engineName/documents/new', - }); - }); - }); -}); - -describe('document routes', () => { - describe('GET /internal/app_search/engines/{engineName}/documents/{documentId}', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'get', - path: '/internal/app_search/engines/{engineName}/documents/{documentId}', - }); - - registerDocumentRoutes({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request to enterprise search', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/as/engines/:engineName/documents/:documentId', - }); - }); - }); - - describe('DELETE /internal/app_search/engines/{engineName}/documents/{documentId}', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'delete', - path: '/internal/app_search/engines/{engineName}/documents/{documentId}', - }); - - registerDocumentRoutes({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request to enterprise search', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/as/engines/:engineName/documents/:documentId', - }); - }); - }); -}); diff --git a/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/documents.ts b/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/documents.ts deleted file mode 100644 index f0af2fb351813..0000000000000 --- a/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/documents.ts +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { schema } from '@kbn/config-schema'; - -import { skipBodyValidation } from '../../lib/route_config_helpers'; - -import { RouteDependencies } from '../../plugin'; - -export function registerDocumentsRoutes({ - router, - enterpriseSearchRequestHandler, -}: RouteDependencies) { - router.post( - skipBodyValidation({ - path: '/internal/app_search/engines/{engineName}/documents', - validate: { - params: schema.object({ - engineName: schema.string(), - }), - }, - }), - enterpriseSearchRequestHandler.createRequest({ - path: '/as/engines/:engineName/documents/new', - }) - ); -} - -export function registerDocumentRoutes({ - router, - enterpriseSearchRequestHandler, -}: RouteDependencies) { - router.get( - { - path: '/internal/app_search/engines/{engineName}/documents/{documentId}', - validate: { - params: schema.object({ - engineName: schema.string(), - documentId: schema.string(), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/as/engines/:engineName/documents/:documentId', - }) - ); - router.delete( - { - path: '/internal/app_search/engines/{engineName}/documents/{documentId}', - validate: { - params: schema.object({ - engineName: schema.string(), - documentId: schema.string(), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/as/engines/:engineName/documents/:documentId', - }) - ); -} diff --git a/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/engines.test.ts b/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/engines.test.ts deleted file mode 100644 index f86001832a3c6..0000000000000 --- a/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/engines.test.ts +++ /dev/null @@ -1,324 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { MockRouter, mockRequestHandler, mockDependencies } from '../../__mocks__'; - -import { registerEnginesRoutes } from './engines'; - -describe('engine routes', () => { - describe('GET /internal/app_search/engines', () => { - const mockRequest = { - query: { - type: 'indexed', - 'page[current]': 1, - 'page[size]': 10, - }, - }; - - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'get', - path: '/internal/app_search/engines', - }); - - registerEnginesRoutes({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request handler', async () => { - await mockRouter.callRoute(mockRequest); - - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/as/engines/collection', - hasValidData: expect.any(Function), - }); - }); - - describe('hasValidData', () => { - it('should correctly validate that the response has data', async () => { - mockRequestHandler.createRequest.mockClear(); - const response = { - meta: { - page: { - total_results: 1, - }, - }, - results: [], - }; - - await mockRouter.callRoute(mockRequest); - expect(mockRequestHandler.hasValidData(response)).toBe(true); - }); - - it('should correctly validate that a response does not have data', async () => { - mockRequestHandler.createRequest.mockClear(); - const response = {}; - - await mockRouter.callRoute(mockRequest); - expect(mockRequestHandler.hasValidData(response)).toBe(false); - }); - }); - - describe('validates', () => { - it('correctly', () => { - const request = { - query: { - type: 'meta', - 'page[current]': 5, - 'page[size]': 10, - }, - }; - mockRouter.shouldValidate(request); - }); - - it('wrong type string', () => { - const request = { - query: { - type: 'invalid', - 'page[current]': 5, - 'page[size]': 10, - }, - }; - mockRouter.shouldThrow(request); - }); - - it('missing query params', () => { - const request = { query: {} }; - mockRouter.shouldThrow(request); - }); - }); - }); - - describe('POST /internal/app_search/engines', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'post', - path: '/internal/app_search/engines', - }); - - registerEnginesRoutes({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request handler', async () => { - await mockRouter.callRoute({ body: { name: 'some-engine', language: 'en' } }); - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/as/engines/collection', - }); - }); - - describe('validates', () => { - describe('indexed engines', () => { - it('correctly', () => { - const request = { body: { name: 'some-engine', language: 'en' } }; - mockRouter.shouldValidate(request); - }); - - it('missing name', () => { - const request = { body: { language: 'en' } }; - mockRouter.shouldThrow(request); - }); - - it('optional language', () => { - const request = { body: { name: 'some-engine' } }; - mockRouter.shouldValidate(request); - }); - }); - - describe('meta engines', () => { - it('all properties', () => { - const request = { - body: { name: 'some-meta-engine', type: 'any', language: 'en', source_engines: [] }, - }; - mockRouter.shouldValidate(request); - }); - - it('missing name', () => { - const request = { - body: { type: 'any', language: 'en', source_engines: [] }, - }; - mockRouter.shouldThrow(request); - }); - - it('optional language', () => { - const request = { - body: { name: 'some-meta-engine', type: 'any', source_engines: [] }, - }; - mockRouter.shouldValidate(request); - }); - - it('optional source_engines', () => { - const request = { - body: { name: 'some-meta-engine', type: 'any', language: 'en' }, - }; - mockRouter.shouldValidate(request); - }); - - it('optional type', () => { - const request = { body: { name: 'some-engine' } }; - mockRouter.shouldValidate(request); - }); - }); - }); - }); - - describe('POST /internal/app_search/elasticsearch/engines', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'post', - path: '/internal/app_search/elasticsearch/engines', - }); - - registerEnginesRoutes({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request handler', async () => { - await mockRouter.callRoute({ - body: { - name: 'some-elasticindexed-engine', - search_index: { type: 'elasticsearch', index_name: 'search-elastic-index' }, - }, - }); - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/api/as/v0/engines', - }); - }); - - describe('validates', () => { - describe('indexed engines', () => { - it('correctly', () => { - const request = { - body: { - name: 'some-engine', - search_index: { type: 'elasticsearch', index_name: 'search-elastic-index' }, - }, - }; - mockRouter.shouldValidate(request); - }); - - it('missing name', () => { - const request = { - body: { - search_index: { type: 'elasticsearch', index_name: 'search-elastic-index' }, - }, - }; - mockRouter.shouldThrow(request); - }); - - it('missing index_name', () => { - const request = { - name: 'some-engine', - body: { - search_index: { type: 'elasticsearch' }, - }, - }; - mockRouter.shouldThrow(request); - }); - }); - }); - }); - - describe('GET /internal/app_search/engines/{name}', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'get', - path: '/internal/app_search/engines/{name}', - }); - - registerEnginesRoutes({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request to enterprise search', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/as/engines/:name/details', - }); - }); - }); - - describe('DELETE /internal/app_search/engines/{name}', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'delete', - path: '/internal/app_search/engines/{name}', - }); - - registerEnginesRoutes({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request to enterprise search', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/as/engines/:name', - }); - }); - - it('validates correctly with name', () => { - const request = { params: { name: 'test-engine' } }; - mockRouter.shouldValidate(request); - }); - - it('fails validation without name', () => { - const request = { params: {} }; - mockRouter.shouldThrow(request); - }); - - it('fails validation with a non-string name', () => { - const request = { params: { name: 1 } }; - mockRouter.shouldThrow(request); - }); - }); - - describe('GET /internal/app_search/engines/{name}/overview', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'get', - path: '/internal/app_search/engines/{name}/overview', - }); - - registerEnginesRoutes({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request to enterprise search', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/as/engines/:name/overview_metrics', - }); - }); - }); -}); diff --git a/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/engines.ts b/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/engines.ts deleted file mode 100644 index 57e5122abb6fe..0000000000000 --- a/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/engines.ts +++ /dev/null @@ -1,117 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { schema } from '@kbn/config-schema'; - -import { RouteDependencies } from '../../plugin'; - -interface EnginesResponse { - results: object[]; - meta: { page: { total_results: number } }; -} - -export function registerEnginesRoutes({ - router, - enterpriseSearchRequestHandler, -}: RouteDependencies) { - router.get( - { - path: '/internal/app_search/engines', - validate: { - query: schema.object({ - type: schema.oneOf([schema.literal('indexed'), schema.literal('meta')]), - 'page[current]': schema.number(), - 'page[size]': schema.number(), - }), - }, - }, - async (context, request, response) => { - return enterpriseSearchRequestHandler.createRequest({ - path: '/as/engines/collection', - hasValidData: (body?: EnginesResponse) => - Array.isArray(body?.results) && typeof body?.meta?.page?.total_results === 'number', - })(context, request, response); - } - ); - - router.post( - { - path: '/internal/app_search/engines', - validate: { - body: schema.object({ - name: schema.string(), - language: schema.maybe(schema.string()), - source_engines: schema.maybe(schema.arrayOf(schema.string())), - type: schema.maybe(schema.string()), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/as/engines/collection', - }) - ); - - router.post( - { - path: '/internal/app_search/elasticsearch/engines', - validate: { - body: schema.object({ - name: schema.string(), - search_index: schema.object({ - type: schema.string(), - index_name: schema.string(), - alias_name: schema.maybe(schema.string()), - }), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/api/as/v0/engines', - }) - ); - - // Single engine endpoints - router.get( - { - path: '/internal/app_search/engines/{name}', - validate: { - params: schema.object({ - name: schema.string(), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/as/engines/:name/details', - }) - ); - router.delete( - { - path: '/internal/app_search/engines/{name}', - validate: { - params: schema.object({ - name: schema.string(), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/as/engines/:name', - }) - ); - router.get( - { - path: '/internal/app_search/engines/{name}/overview', - validate: { - params: schema.object({ - name: schema.string(), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/as/engines/:name/overview_metrics', - }) - ); -} diff --git a/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/index.test.ts b/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/index.test.ts deleted file mode 100644 index 61249f79ff44b..0000000000000 --- a/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/index.test.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { mockDependencies, MockRouter } from '../../__mocks__'; - -import { registerAppSearchRoutes } from '.'; - -describe('registerAppSearchRoutes', () => { - it('runs without errors', () => { - const mockRouter = new MockRouter({} as any); - registerAppSearchRoutes({ ...mockDependencies, router: mockRouter.router }); - }); -}); diff --git a/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/index.ts b/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/index.ts deleted file mode 100644 index bc98625bfd514..0000000000000 --- a/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/index.ts +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { RouteDependencies } from '../../plugin'; -import { registerCrawlerExtractionRulesRoutes } from '../enterprise_search/crawler/crawler_extraction_rules'; - -import { registerSearchRelevanceSuggestionsRoutes } from './adaptive_relevance'; -import { registerAnalyticsRoutes } from './analytics'; -import { registerApiLogsRoutes } from './api_logs'; -import { registerAppSearchGatedFormRoute } from './app_search_gated_form'; -import { registerCrawlerRoutes } from './crawler'; -import { registerCrawlerCrawlRulesRoutes } from './crawler_crawl_rules'; -import { registerCrawlerEntryPointRoutes } from './crawler_entry_points'; -import { registerCrawlerSitemapRoutes } from './crawler_sitemaps'; -import { registerCredentialsRoutes } from './credentials'; -import { registerCurationsRoutes } from './curations'; -import { registerDocumentsRoutes, registerDocumentRoutes } from './documents'; -import { registerEnginesRoutes } from './engines'; -import { registerOnboardingRoutes } from './onboarding'; -import { registerResultSettingsRoutes } from './result_settings'; -import { registerRoleMappingsRoutes } from './role_mappings'; -import { registerSchemaRoutes } from './schema'; -import { registerSearchRoutes } from './search'; -import { registerSearchSettingsRoutes } from './search_settings'; -import { registerSearchUIRoutes } from './search_ui'; -import { registerSettingsRoutes } from './settings'; -import { registerSourceEnginesRoutes } from './source_engines'; -import { registerSynonymsRoutes } from './synonyms'; - -export const registerAppSearchRoutes = (dependencies: RouteDependencies) => { - registerEnginesRoutes(dependencies); - registerCredentialsRoutes(dependencies); - registerSettingsRoutes(dependencies); - registerAnalyticsRoutes(dependencies); - registerDocumentsRoutes(dependencies); - registerDocumentRoutes(dependencies); - registerSchemaRoutes(dependencies); - registerSearchRoutes(dependencies); - registerSourceEnginesRoutes(dependencies); - registerCurationsRoutes(dependencies); - registerSynonymsRoutes(dependencies); - registerSearchSettingsRoutes(dependencies); - registerRoleMappingsRoutes(dependencies); - registerSearchUIRoutes(dependencies); - registerResultSettingsRoutes(dependencies); - registerApiLogsRoutes(dependencies); - registerOnboardingRoutes(dependencies); - registerCrawlerRoutes(dependencies); - registerCrawlerEntryPointRoutes(dependencies); - registerCrawlerCrawlRulesRoutes(dependencies); - registerCrawlerExtractionRulesRoutes(dependencies); - registerCrawlerSitemapRoutes(dependencies); - registerSearchRelevanceSuggestionsRoutes(dependencies); - registerAppSearchGatedFormRoute(dependencies); -}; diff --git a/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/onboarding.test.ts b/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/onboarding.test.ts deleted file mode 100644 index bb281983d5382..0000000000000 --- a/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/onboarding.test.ts +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { MockRouter, mockRequestHandler, mockDependencies } from '../../__mocks__'; - -import { registerOnboardingRoutes } from './onboarding'; - -describe('engine routes', () => { - describe('POST /internal/app_search/onboarding_complete', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'post', - path: '/internal/app_search/onboarding_complete', - }); - - registerOnboardingRoutes({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request handler', async () => { - await mockRouter.callRoute({ body: {} }); - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/as/onboarding/complete', - hasJsonResponse: false, - }); - }); - - it('validates seed_sample_engine ', () => { - const request = { body: { seed_sample_engine: true } }; - mockRouter.shouldValidate(request); - }); - }); -}); diff --git a/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/onboarding.ts b/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/onboarding.ts deleted file mode 100644 index b61c49c06b905..0000000000000 --- a/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/onboarding.ts +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { schema } from '@kbn/config-schema'; - -import { RouteDependencies } from '../../plugin'; - -export function registerOnboardingRoutes({ - router, - enterpriseSearchRequestHandler, -}: RouteDependencies) { - router.post( - { - path: '/internal/app_search/onboarding_complete', - validate: { - body: schema.object({ - seed_sample_engine: schema.maybe(schema.boolean()), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/as/onboarding/complete', - hasJsonResponse: false, - }) - ); -} diff --git a/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/result_settings.test.ts b/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/result_settings.test.ts deleted file mode 100644 index fcbf10c080b30..0000000000000 --- a/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/result_settings.test.ts +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { mockDependencies, mockRequestHandler, MockRouter } from '../../__mocks__'; - -import { registerResultSettingsRoutes } from './result_settings'; - -const resultFields = { - id: { - raw: {}, - }, - hp: { - raw: {}, - }, - name: { - raw: {}, - }, -}; - -describe('result settings routes', () => { - describe('GET /internal/app_search/engines/{name}/result_settings/details', () => { - const mockRouter = new MockRouter({ - method: 'get', - path: '/internal/app_search/engines/{engineName}/result_settings/details', - }); - - beforeEach(() => { - registerResultSettingsRoutes({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request to enterprise search', async () => { - await mockRouter.callRoute({ - params: { engineName: 'some-engine' }, - }); - - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/as/engines/:engineName/result_settings/details', - }); - }); - }); - - describe('PUT /internal/app_search/engines/{name}/result_settings', () => { - const mockRouter = new MockRouter({ - method: 'put', - path: '/internal/app_search/engines/{engineName}/result_settings', - }); - - beforeEach(() => { - registerResultSettingsRoutes({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request to enterprise search', async () => { - await mockRouter.callRoute({ - params: { engineName: 'some-engine' }, - body: { - result_settings: resultFields, - }, - }); - - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/as/engines/:engineName/result_settings', - }); - }); - }); -}); diff --git a/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/result_settings.ts b/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/result_settings.ts deleted file mode 100644 index 0a5bd5af8edf6..0000000000000 --- a/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/result_settings.ts +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { schema } from '@kbn/config-schema'; - -import { skipBodyValidation } from '../../lib/route_config_helpers'; - -import { RouteDependencies } from '../../plugin'; - -export function registerResultSettingsRoutes({ - router, - enterpriseSearchRequestHandler, -}: RouteDependencies) { - router.get( - { - path: '/internal/app_search/engines/{engineName}/result_settings/details', - validate: { - params: schema.object({ - engineName: schema.string(), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/as/engines/:engineName/result_settings/details', - }) - ); - - router.put( - skipBodyValidation({ - path: '/internal/app_search/engines/{engineName}/result_settings', - validate: { - params: schema.object({ - engineName: schema.string(), - }), - }, - }), - enterpriseSearchRequestHandler.createRequest({ - path: '/as/engines/:engineName/result_settings', - }) - ); -} diff --git a/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/role_mappings.test.ts b/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/role_mappings.test.ts deleted file mode 100644 index 0c06ff67b233c..0000000000000 --- a/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/role_mappings.test.ts +++ /dev/null @@ -1,211 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { MockRouter, mockRequestHandler, mockDependencies } from '../../__mocks__'; - -import { - registerEnableRoleMappingsRoute, - registerRoleMappingsRoute, - registerRoleMappingRoute, - registerUserRoute, -} from './role_mappings'; - -const roleMappingBaseSchema = { - rules: { username: 'user' }, - roleType: 'owner', - engines: ['e1', 'e2'], - accessAllEngines: false, -}; - -describe('role mappings routes', () => { - describe('POST /internal/app_search/role_mappings/enable_role_based_access', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'post', - path: '/internal/app_search/role_mappings/enable_role_based_access', - }); - - registerEnableRoleMappingsRoute({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request handler', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/as/role_mappings/enable_role_based_access', - }); - }); - }); - - describe('GET /internal/app_search/role_mappings', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'get', - path: '/internal/app_search/role_mappings', - }); - - registerRoleMappingsRoute({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request handler', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/as/role_mappings', - }); - }); - }); - - describe('POST /internal/app_search/role_mappings', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'post', - path: '/internal/app_search/role_mappings', - }); - - registerRoleMappingsRoute({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request handler', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/as/role_mappings', - }); - }); - - describe('validates', () => { - it('correctly', () => { - const request = { body: roleMappingBaseSchema }; - mockRouter.shouldValidate(request); - }); - - it('missing required fields', () => { - const request = { body: {} }; - mockRouter.shouldThrow(request); - }); - }); - }); - - describe('PUT /internal/app_search/role_mappings/{id}', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'put', - path: '/internal/app_search/role_mappings/{id}', - }); - - registerRoleMappingRoute({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request handler', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/as/role_mappings/:id', - }); - }); - - describe('validates', () => { - it('correctly', () => { - const request = { body: roleMappingBaseSchema }; - mockRouter.shouldValidate(request); - }); - - it('missing required fields', () => { - const request = { body: {} }; - mockRouter.shouldThrow(request); - }); - }); - }); - - describe('DELETE /internal/app_search/role_mappings/{id}', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'delete', - path: '/internal/app_search/role_mappings/{id}', - }); - - registerRoleMappingRoute({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request handler', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/as/role_mappings/:id', - }); - }); - }); - - describe('POST /internal/app_search/single_user_role_mapping', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'post', - path: '/internal/app_search/single_user_role_mapping', - }); - - registerUserRoute({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - describe('validates', () => { - it('correctly', () => { - const request = { - body: { - roleMapping: { - engines: ['foo', 'bar'], - roleType: 'admin', - accessAllEngines: true, - id: '123asf', - }, - elasticsearchUser: { - username: 'user2@elastic.co', - email: 'user2', - }, - }, - }; - mockRouter.shouldValidate(request); - }); - - it('missing required fields', () => { - const request = { body: {} }; - mockRouter.shouldThrow(request); - }); - }); - - it('creates a request handler', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/as/role_mappings/upsert_single_user_role_mapping', - }); - }); - }); -}); diff --git a/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/role_mappings.ts b/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/role_mappings.ts deleted file mode 100644 index e7f736ea42a8b..0000000000000 --- a/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/role_mappings.ts +++ /dev/null @@ -1,125 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { schema } from '@kbn/config-schema'; - -import { RouteDependencies } from '../../plugin'; - -const roleMappingBaseSchema = { - rules: schema.recordOf(schema.string(), schema.string()), - roleType: schema.string(), - engines: schema.arrayOf(schema.string()), - accessAllEngines: schema.boolean(), -}; - -export function registerEnableRoleMappingsRoute({ - router, - enterpriseSearchRequestHandler, -}: RouteDependencies) { - router.post( - { - path: '/internal/app_search/role_mappings/enable_role_based_access', - validate: false, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/as/role_mappings/enable_role_based_access', - }) - ); -} - -export function registerRoleMappingsRoute({ - router, - enterpriseSearchRequestHandler, -}: RouteDependencies) { - router.get( - { - path: '/internal/app_search/role_mappings', - validate: false, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/as/role_mappings', - }) - ); - - router.post( - { - path: '/internal/app_search/role_mappings', - validate: { - body: schema.object(roleMappingBaseSchema), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/as/role_mappings', - }) - ); -} - -export function registerRoleMappingRoute({ - router, - enterpriseSearchRequestHandler, -}: RouteDependencies) { - router.put( - { - path: '/internal/app_search/role_mappings/{id}', - validate: { - body: schema.object(roleMappingBaseSchema), - params: schema.object({ - id: schema.string(), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/as/role_mappings/:id', - }) - ); - - router.delete( - { - path: '/internal/app_search/role_mappings/{id}', - validate: { - params: schema.object({ - id: schema.string(), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/as/role_mappings/:id', - }) - ); -} - -export function registerUserRoute({ router, enterpriseSearchRequestHandler }: RouteDependencies) { - router.post( - { - path: '/internal/app_search/single_user_role_mapping', - validate: { - body: schema.object({ - roleMapping: schema.object({ - engines: schema.arrayOf(schema.string()), - roleType: schema.string(), - accessAllEngines: schema.boolean(), - id: schema.maybe(schema.string()), - }), - elasticsearchUser: schema.object({ - username: schema.string(), - email: schema.string(), - }), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/as/role_mappings/upsert_single_user_role_mapping', - }) - ); -} - -export const registerRoleMappingsRoutes = (dependencies: RouteDependencies) => { - registerEnableRoleMappingsRoute(dependencies); - registerRoleMappingsRoute(dependencies); - registerRoleMappingRoute(dependencies); - registerUserRoute(dependencies); -}; diff --git a/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/schema.test.ts b/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/schema.test.ts deleted file mode 100644 index 79d445aace90e..0000000000000 --- a/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/schema.test.ts +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { MockRouter, mockRequestHandler, mockDependencies } from '../../__mocks__'; - -import { registerSchemaRoutes } from './schema'; - -describe('schema routes', () => { - describe('GET /internal/app_search/engines/{engineName}/schema', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'get', - path: '/internal/app_search/engines/{engineName}/schema', - }); - - registerSchemaRoutes({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request handler', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/as/engines/:engineName/schema', - }); - }); - }); - - describe('POST /internal/app_search/engines/{engineName}/schema', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'post', - path: '/internal/app_search/engines/{engineName}/schema', - }); - - registerSchemaRoutes({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request handler', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/as/engines/:engineName/schema', - }); - }); - }); - - describe('GET /internal/app_search/engines/{engineName}/reindex_job/{reindexJobId}', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'get', - path: '/internal/app_search/engines/{engineName}/reindex_job/{reindexJobId}', - }); - - registerSchemaRoutes({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request handler', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/as/engines/:engineName/reindex_job/:reindexJobId', - }); - }); - }); -}); diff --git a/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/schema.ts b/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/schema.ts deleted file mode 100644 index 98f13e4564cc5..0000000000000 --- a/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/schema.ts +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { schema } from '@kbn/config-schema'; - -import { skipBodyValidation } from '../../lib/route_config_helpers'; -import { RouteDependencies } from '../../plugin'; - -export function registerSchemaRoutes({ - router, - enterpriseSearchRequestHandler, -}: RouteDependencies) { - router.get( - { - path: '/internal/app_search/engines/{engineName}/schema', - validate: { - params: schema.object({ - engineName: schema.string(), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/as/engines/:engineName/schema', - }) - ); - - router.post( - skipBodyValidation({ - path: '/internal/app_search/engines/{engineName}/schema', - validate: { - params: schema.object({ - engineName: schema.string(), - }), - }, - }), - enterpriseSearchRequestHandler.createRequest({ - path: '/as/engines/:engineName/schema', - }) - ); - - router.get( - { - path: '/internal/app_search/engines/{engineName}/reindex_job/{reindexJobId}', - validate: { - params: schema.object({ - engineName: schema.string(), - reindexJobId: schema.string(), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/as/engines/:engineName/reindex_job/:reindexJobId', - }) - ); -} diff --git a/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/search.test.ts b/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/search.test.ts deleted file mode 100644 index 972bcaa8891be..0000000000000 --- a/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/search.test.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { MockRouter, mockRequestHandler, mockDependencies } from '../../__mocks__'; - -import { registerSearchRoutes } from './search'; - -describe('search routes', () => { - describe('GET /internal/app_search/engines/{engineName}/search', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'get', - path: '/internal/app_search/engines/{engineName}/schema', - }); - - registerSearchRoutes({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request handler', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/as/engines/:engineName/dashboard_search', - }); - }); - }); -}); diff --git a/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/search.ts b/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/search.ts deleted file mode 100644 index 1150866bc1aa4..0000000000000 --- a/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/search.ts +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { schema } from '@kbn/config-schema'; - -import { skipBodyValidation } from '../../lib/route_config_helpers'; - -import { RouteDependencies } from '../../plugin'; - -export function registerSearchRoutes({ - router, - enterpriseSearchRequestHandler, -}: RouteDependencies) { - router.post( - skipBodyValidation({ - path: '/internal/app_search/engines/{engineName}/search', - validate: { - params: schema.object({ - engineName: schema.string(), - }), - query: schema.object({ - query: schema.string(), - }), - }, - }), - enterpriseSearchRequestHandler.createRequest({ - path: '/as/engines/:engineName/dashboard_search', - }) - ); - - // Search UI always posts it's requests to {some_configured_base_url}/api/as/v1/engines/{engineName}/search.json - // For that reason, we have to create a proxy url with that same suffix below, so that we can proxy Search UI - // requests through Kibana's server. - router.post( - skipBodyValidation({ - path: '/internal/app_search/search-ui/api/as/v1/engines/{engineName}/search.json', - validate: { - params: schema.object({ - engineName: schema.string(), - }), - }, - }), - enterpriseSearchRequestHandler.createRequest({ - path: '/as/engines/:engineName/dashboard_search', - }) - ); -} diff --git a/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/search_settings.test.ts b/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/search_settings.test.ts deleted file mode 100644 index 5b6395912766f..0000000000000 --- a/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/search_settings.test.ts +++ /dev/null @@ -1,135 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { MockRouter, mockRequestHandler, mockDependencies } from '../../__mocks__'; - -import { registerSearchSettingsRoutes } from './search_settings'; - -describe('search settings routes', () => { - const boosts = { - types: [ - { - type: 'value', - factor: 6.2, - value: ['1313'], - }, - ], - hp: [ - { - function: 'exponential', - type: 'functional', - factor: 1, - operation: 'add', - }, - ], - }; - const resultFields = { - id: { - raw: {}, - }, - hp: { - raw: {}, - }, - name: { - raw: {}, - }, - }; - const searchFields = { - hp: { - weight: 1, - }, - name: { - weight: 1, - }, - id: { - weight: 1, - }, - }; - const searchSettings = { - boosts, - result_fields: resultFields, - search_fields: searchFields, - precision: 2, - }; - - beforeEach(() => { - jest.clearAllMocks(); - }); - - describe('GET /internal/app_search/engines/{name}/search_settings/details', () => { - const mockRouter = new MockRouter({ - method: 'get', - path: '/internal/app_search/engines/{engineName}/search_settings/details', - }); - - beforeEach(() => { - registerSearchSettingsRoutes({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request to enterprise search', async () => { - await mockRouter.callRoute({ - params: { engineName: 'some-engine' }, - }); - - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/as/engines/:engineName/search_settings/details', - }); - }); - }); - - describe('PUT /internal/app_search/engines/{name}/search_settings', () => { - const mockRouter = new MockRouter({ - method: 'put', - path: '/internal/app_search/engines/{engineName}/search_settings', - }); - - beforeEach(() => { - registerSearchSettingsRoutes({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request to enterprise search', async () => { - await mockRouter.callRoute({ - params: { engineName: 'some-engine' }, - body: searchSettings, - }); - - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/as/engines/:engineName/search_settings', - }); - }); - }); - - describe('POST /internal/app_search/engines/{name}/search_settings/reset', () => { - const mockRouter = new MockRouter({ - method: 'post', - path: '/internal/app_search/engines/{engineName}/search_settings/reset', - }); - - beforeEach(() => { - registerSearchSettingsRoutes({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request to enterprise search', async () => { - await mockRouter.callRoute({ - params: { engineName: 'some-engine' }, - }); - - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/as/engines/:engineName/search_settings/reset', - }); - }); - }); -}); diff --git a/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/search_settings.ts b/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/search_settings.ts deleted file mode 100644 index 2dd097a17bf64..0000000000000 --- a/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/search_settings.ts +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { schema } from '@kbn/config-schema'; - -import { skipBodyValidation } from '../../lib/route_config_helpers'; - -import { RouteDependencies } from '../../plugin'; - -export function registerSearchSettingsRoutes({ - router, - enterpriseSearchRequestHandler, -}: RouteDependencies) { - router.get( - { - path: '/internal/app_search/engines/{engineName}/search_settings/details', - validate: { - params: schema.object({ - engineName: schema.string(), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/as/engines/:engineName/search_settings/details', - }) - ); - - router.post( - { - path: '/internal/app_search/engines/{engineName}/search_settings/reset', - validate: { - params: schema.object({ - engineName: schema.string(), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/as/engines/:engineName/search_settings/reset', - }) - ); - - router.put( - skipBodyValidation({ - path: '/internal/app_search/engines/{engineName}/search_settings', - validate: { - params: schema.object({ - engineName: schema.string(), - }), - }, - }), - enterpriseSearchRequestHandler.createRequest({ - path: '/as/engines/:engineName/search_settings', - }) - ); -} diff --git a/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/search_ui.test.ts b/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/search_ui.test.ts deleted file mode 100644 index 1cfb52640c09b..0000000000000 --- a/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/search_ui.test.ts +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { mockDependencies, mockRequestHandler, MockRouter } from '../../__mocks__'; - -import { registerSearchUIRoutes } from './search_ui'; - -describe('reference application routes', () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - describe('GET /internal/app_search/engines/{engineName}/search_settings/details', () => { - const mockRouter = new MockRouter({ - method: 'get', - path: '/internal/app_search/engines/{engineName}/search_ui/field_config', - }); - - beforeEach(() => { - registerSearchUIRoutes({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request to enterprise search', async () => { - await mockRouter.callRoute({ - params: { engineName: 'some-engine' }, - }); - - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/as/engines/:engineName/search_experience/field_config', - }); - }); - }); -}); diff --git a/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/search_ui.ts b/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/search_ui.ts deleted file mode 100644 index 3d9a671b6e81a..0000000000000 --- a/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/search_ui.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { schema } from '@kbn/config-schema'; - -import { RouteDependencies } from '../../plugin'; - -export function registerSearchUIRoutes({ - router, - enterpriseSearchRequestHandler, -}: RouteDependencies) { - router.get( - { - path: '/internal/app_search/engines/{engineName}/search_ui/field_config', - validate: { - params: schema.object({ - engineName: schema.string(), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/as/engines/:engineName/search_experience/field_config', - }) - ); -} diff --git a/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/settings.test.ts b/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/settings.test.ts deleted file mode 100644 index d8ff7f1815a62..0000000000000 --- a/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/settings.test.ts +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { MockRouter, mockRequestHandler, mockDependencies } from '../../__mocks__'; - -import { registerSettingsRoutes } from './settings'; - -describe('log settings routes', () => { - describe('GET /internal/app_search/log_settings', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'get', - path: '/internal/app_search/log_settings', - }); - - registerSettingsRoutes({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request to enterprise search', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/as/log_settings', - }); - }); - }); - - describe('PUT /internal/app_search/log_settings', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'put', - path: '/internal/app_search/log_settings', - }); - - registerSettingsRoutes({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request to enterprise search', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/as/log_settings', - }); - }); - - describe('validates', () => { - it('validates good data', () => { - mockRouter.shouldValidate({ - body: { - analytics: { enabled: true }, - }, - }); - mockRouter.shouldValidate({ - body: { - api: { enabled: true }, - }, - }); - mockRouter.shouldValidate({ - body: { - crawler: { enabled: true }, - }, - }); - }); - - it('rejects bad data', () => { - const request = { - body: { - foo: 'bar', - }, - }; - mockRouter.shouldThrow(request); - }); - }); - }); -}); diff --git a/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/settings.ts b/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/settings.ts deleted file mode 100644 index e77002c6c3ed3..0000000000000 --- a/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/settings.ts +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { schema } from '@kbn/config-schema'; - -import { RouteDependencies } from '../../plugin'; - -export function registerSettingsRoutes({ - router, - enterpriseSearchRequestHandler, -}: RouteDependencies) { - router.get( - { - path: '/internal/app_search/log_settings', - validate: false, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/as/log_settings', - }) - ); - - router.put( - { - path: '/internal/app_search/log_settings', - validate: { - body: schema.object({ - api: schema.maybe( - schema.object({ - enabled: schema.boolean(), - }) - ), - analytics: schema.maybe( - schema.object({ - enabled: schema.boolean(), - }) - ), - audit: schema.maybe( - schema.object({ - enabled: schema.boolean(), - }) - ), - crawler: schema.maybe( - schema.object({ - enabled: schema.boolean(), - }) - ), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/as/log_settings', - }) - ); -} diff --git a/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/source_engines.test.ts b/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/source_engines.test.ts deleted file mode 100644 index 4374bbe88e183..0000000000000 --- a/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/source_engines.test.ts +++ /dev/null @@ -1,153 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { MockRouter, mockRequestHandler, mockDependencies } from '../../__mocks__'; - -import { registerSourceEnginesRoutes } from './source_engines'; - -describe('source engine routes', () => { - describe('GET /internal/app_search/engines/{name}/source_engines', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'get', - path: '/internal/app_search/engines/{name}/source_engines', - }); - - registerSourceEnginesRoutes({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('validates correctly with name', () => { - const request = { params: { name: 'test-engine' } }; - mockRouter.shouldValidate(request); - }); - - it('fails validation without name', () => { - const request = { params: {} }; - mockRouter.shouldThrow(request); - }); - - it('fails validation with a non-string name', () => { - const request = { params: { name: 1 } }; - mockRouter.shouldThrow(request); - }); - - it('fails validation with missing query params', () => { - const request = { query: {} }; - mockRouter.shouldThrow(request); - }); - - it('creates a request to enterprise search', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/as/engines/:name/source_engines', - }); - }); - }); - - describe('POST /internal/app_search/engines/{name}/source_engines/bulk_create', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'post', - path: '/internal/app_search/engines/{name}/source_engines/bulk_create', - }); - - registerSourceEnginesRoutes({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('validates correctly with name', () => { - const request = { params: { name: 'test-engine' }, body: { source_engine_slugs: [] } }; - mockRouter.shouldValidate(request); - }); - - it('fails validation without name', () => { - const request = { params: {}, body: { source_engine_slugs: [] } }; - mockRouter.shouldThrow(request); - }); - - it('fails validation with a non-string name', () => { - const request = { params: { name: 1 }, body: { source_engine_slugs: [] } }; - mockRouter.shouldThrow(request); - }); - - it('fails validation with missing query params', () => { - const request = { params: { name: 'test-engine' }, body: {} }; - mockRouter.shouldThrow(request); - }); - - it('creates a request to enterprise search', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/as/engines/:name/source_engines/bulk_create', - hasJsonResponse: false, - }); - }); - }); - - describe('DELETE /internal/app_search/engines/{name}/source_engines/{source_engine_name}', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'delete', - path: '/internal/app_search/engines/{name}/source_engines/{source_engine_name}', - }); - - registerSourceEnginesRoutes({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('validates correctly with name and source_engine_name', () => { - const request = { params: { name: 'test-engine', source_engine_name: 'source-engine' } }; - mockRouter.shouldValidate(request); - }); - - it('fails validation without name', () => { - const request = { params: { source_engine_name: 'source-engine' } }; - mockRouter.shouldThrow(request); - }); - - it('fails validation with a non-string name', () => { - const request = { params: { name: 1, source_engine_name: 'source-engine' } }; - mockRouter.shouldThrow(request); - }); - - it('fails validation without source_engine_name', () => { - const request = { params: { name: 'test-engine' } }; - mockRouter.shouldThrow(request); - }); - - it('fails validation with a non-string source_engine_name', () => { - const request = { params: { name: 'test-engine', source_engine_name: 1 } }; - mockRouter.shouldThrow(request); - }); - - it('fails validation with missing query params', () => { - const request = { query: {} }; - mockRouter.shouldThrow(request); - }); - - it('creates a request to enterprise search', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/as/engines/:name/source_engines/:source_engine_name', - hasJsonResponse: false, - }); - }); - }); -}); diff --git a/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/source_engines.ts b/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/source_engines.ts deleted file mode 100644 index 79be0c9c29322..0000000000000 --- a/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/source_engines.ts +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { schema } from '@kbn/config-schema'; - -import { RouteDependencies } from '../../plugin'; - -export function registerSourceEnginesRoutes({ - router, - enterpriseSearchRequestHandler, -}: RouteDependencies) { - router.get( - { - path: '/internal/app_search/engines/{name}/source_engines', - validate: { - params: schema.object({ - name: schema.string(), - }), - query: schema.object({ - 'page[current]': schema.number(), - 'page[size]': schema.number(), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/as/engines/:name/source_engines', - }) - ); - - router.post( - { - path: '/internal/app_search/engines/{name}/source_engines/bulk_create', - validate: { - params: schema.object({ - name: schema.string(), - }), - body: schema.object({ - source_engine_slugs: schema.arrayOf(schema.string()), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/as/engines/:name/source_engines/bulk_create', - hasJsonResponse: false, - }) - ); - - router.delete( - { - path: '/internal/app_search/engines/{name}/source_engines/{source_engine_name}', - validate: { - params: schema.object({ - name: schema.string(), - source_engine_name: schema.string(), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/as/engines/:name/source_engines/:source_engine_name', - hasJsonResponse: false, - }) - ); -} diff --git a/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/synonyms.test.ts b/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/synonyms.test.ts deleted file mode 100644 index 0007b3f27cff0..0000000000000 --- a/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/synonyms.test.ts +++ /dev/null @@ -1,121 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { MockRouter, mockRequestHandler, mockDependencies } from '../../__mocks__'; - -import { registerSynonymsRoutes } from './synonyms'; - -describe('synonyms routes', () => { - describe('GET /internal/app_search/engines/{engineName}/synonyms', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'get', - path: '/internal/app_search/engines/{engineName}/synonyms', - }); - - registerSynonymsRoutes({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request handler', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/as/engines/:engineName/synonyms/collection', - }); - }); - - describe('validates', () => { - it('with pagination query params', () => { - const request = { - query: { - 'page[current]': 1, - 'page[size]': 10, - }, - }; - mockRouter.shouldValidate(request); - }); - - it('missing query params', () => { - const request = { query: {} }; - mockRouter.shouldThrow(request); - }); - }); - }); - - describe('POST /internal/app_search/engines/{engineName}/synonyms', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'post', - path: '/internal/app_search/engines/{engineName}/synonyms', - }); - - registerSynonymsRoutes({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request handler', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/as/engines/:engineName/synonyms/collection', - }); - }); - }); - - describe('PUT /internal/app_search/engines/{engineName}/synonyms/{synonymId}', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'put', - path: '/internal/app_search/engines/{engineName}/synonyms/{synonymId}', - }); - - registerSynonymsRoutes({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request handler', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/as/engines/:engineName/synonyms/:synonymId', - }); - }); - }); - - describe('DELETE /internal/app_search/engines/{engineName}/synonyms/{synonymId}', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'delete', - path: '/internal/app_search/engines/{engineName}/synonyms/{synonymId}', - }); - - registerSynonymsRoutes({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request handler', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/as/engines/:engineName/synonyms/:synonymId', - }); - }); - }); -}); diff --git a/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/synonyms.ts b/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/synonyms.ts deleted file mode 100644 index 0c9682bb983d4..0000000000000 --- a/x-pack/solutions/search/plugins/enterprise_search/server/routes/app_search/synonyms.ts +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { schema } from '@kbn/config-schema'; - -import { skipBodyValidation } from '../../lib/route_config_helpers'; -import { RouteDependencies } from '../../plugin'; - -export function registerSynonymsRoutes({ - router, - enterpriseSearchRequestHandler, -}: RouteDependencies) { - router.get( - { - path: '/internal/app_search/engines/{engineName}/synonyms', - validate: { - params: schema.object({ - engineName: schema.string(), - }), - query: schema.object({ - 'page[current]': schema.number(), - 'page[size]': schema.number(), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/as/engines/:engineName/synonyms/collection', - }) - ); - - router.post( - skipBodyValidation({ - path: '/internal/app_search/engines/{engineName}/synonyms', - validate: { - params: schema.object({ - engineName: schema.string(), - }), - }, - }), - enterpriseSearchRequestHandler.createRequest({ - path: '/as/engines/:engineName/synonyms/collection', - }) - ); - - router.put( - skipBodyValidation({ - path: '/internal/app_search/engines/{engineName}/synonyms/{synonymId}', - validate: { - params: schema.object({ - engineName: schema.string(), - synonymId: schema.string(), - }), - }, - }), - enterpriseSearchRequestHandler.createRequest({ - path: '/as/engines/:engineName/synonyms/:synonymId', - }) - ); - - router.delete( - { - path: '/internal/app_search/engines/{engineName}/synonyms/{synonymId}', - validate: { - params: schema.object({ - engineName: schema.string(), - synonymId: schema.string(), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/as/engines/:engineName/synonyms/:synonymId', - }) - ); -} diff --git a/x-pack/solutions/search/plugins/enterprise_search/server/routes/workplace_search/api_keys.test.ts b/x-pack/solutions/search/plugins/enterprise_search/server/routes/workplace_search/api_keys.test.ts deleted file mode 100644 index 4855716cfc2fd..0000000000000 --- a/x-pack/solutions/search/plugins/enterprise_search/server/routes/workplace_search/api_keys.test.ts +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { MockRouter, mockRequestHandler, mockDependencies } from '../../__mocks__'; - -import { registerApiKeysRoute } from './api_keys'; - -describe('api keys routes', () => { - describe('GET /internal/workplace_search/api_keys', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'get', - path: '/internal/workplace_search/api_keys', - }); - - registerApiKeysRoute({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request handler', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/ws/org/api_tokens', - }); - }); - }); - - describe('POST /internal/workplace_search/api_keys', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'post', - path: '/internal/workplace_search/api_keys', - }); - - registerApiKeysRoute({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request handler', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/ws/org/api_tokens', - }); - }); - - describe('validates', () => { - it('correctly', () => { - const request = { - body: { - name: 'my-api-key', - }, - }; - mockRouter.shouldValidate(request); - }); - }); - }); - - describe('DELETE /internal/workplace_search/api_keys/{tokenName}', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'delete', - path: '/internal/workplace_search/api_keys/{tokenName}', - }); - - registerApiKeysRoute({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request handler', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/ws/org/api_tokens/:tokenName', - }); - }); - }); -}); diff --git a/x-pack/solutions/search/plugins/enterprise_search/server/routes/workplace_search/api_keys.ts b/x-pack/solutions/search/plugins/enterprise_search/server/routes/workplace_search/api_keys.ts deleted file mode 100644 index ff63c7b146750..0000000000000 --- a/x-pack/solutions/search/plugins/enterprise_search/server/routes/workplace_search/api_keys.ts +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { schema } from '@kbn/config-schema'; - -import { RouteDependencies } from '../../plugin'; - -export function registerApiKeysRoute({ - router, - enterpriseSearchRequestHandler, -}: RouteDependencies) { - router.get( - { - path: '/internal/workplace_search/api_keys', - validate: false, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/ws/org/api_tokens', - }) - ); - - router.post( - { - path: '/internal/workplace_search/api_keys', - validate: { - body: schema.object({ - name: schema.string(), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/ws/org/api_tokens', - }) - ); - - router.delete( - { - path: '/internal/workplace_search/api_keys/{tokenName}', - validate: { - params: schema.object({ - tokenName: schema.string(), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/ws/org/api_tokens/:tokenName', - }) - ); -} - -export const registerApiKeysRoutes = (dependencies: RouteDependencies) => { - registerApiKeysRoute(dependencies); -}; diff --git a/x-pack/solutions/search/plugins/enterprise_search/server/routes/workplace_search/gated_form.test.ts b/x-pack/solutions/search/plugins/enterprise_search/server/routes/workplace_search/gated_form.test.ts deleted file mode 100644 index a47512ea6e154..0000000000000 --- a/x-pack/solutions/search/plugins/enterprise_search/server/routes/workplace_search/gated_form.test.ts +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { MockRouter, mockRequestHandler, mockDependencies } from '../../__mocks__'; - -import { registerGatedFormRoute } from './gated_form'; - -describe('Overview route with kibana_uis_enabled ', () => { - describe('POST /internal/workplace_search/ws_gate', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'post', - path: '/internal/workplace_search/ws_gate', - }); - - registerGatedFormRoute({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request handler', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/api/ent/v2/internal/ws_gate', - }); - }); - - describe('validates', () => { - it('correctly', () => { - const request = { - body: { - ws_gate_data: { - additional_feedback: '', - feature: 'Selected feature', - features_other: '', - participate_in_ux_labs: true, - }, - }, - }; - mockRouter.shouldValidate(request); - }); - - it('throws error unexpected values in body', () => { - const request = { - body: { - foo: 'bar', - }, - }; - mockRouter.shouldThrow(request); - }); - }); - }); -}); diff --git a/x-pack/solutions/search/plugins/enterprise_search/server/routes/workplace_search/gated_form.ts b/x-pack/solutions/search/plugins/enterprise_search/server/routes/workplace_search/gated_form.ts deleted file mode 100644 index 70181b44cf89d..0000000000000 --- a/x-pack/solutions/search/plugins/enterprise_search/server/routes/workplace_search/gated_form.ts +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { schema } from '@kbn/config-schema'; - -import { RouteDependencies } from '../../plugin'; - -export function registerGatedFormRoute({ - router, - enterpriseSearchRequestHandler, -}: RouteDependencies) { - router.post( - { - path: '/internal/workplace_search/ws_gate', - validate: { - body: schema.object({ - ws_gate_data: schema.object({ - additional_feedback: schema.maybe(schema.string()), - feature: schema.string(), - features_other: schema.maybe(schema.string()), - participate_in_ux_labs: schema.maybe(schema.boolean()), - }), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/api/ent/v2/internal/ws_gate', - }) - ); -} diff --git a/x-pack/solutions/search/plugins/enterprise_search/server/routes/workplace_search/groups.test.ts b/x-pack/solutions/search/plugins/enterprise_search/server/routes/workplace_search/groups.test.ts deleted file mode 100644 index dc1308a4140d3..0000000000000 --- a/x-pack/solutions/search/plugins/enterprise_search/server/routes/workplace_search/groups.test.ts +++ /dev/null @@ -1,303 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { MockRouter, mockRequestHandler, mockDependencies } from '../../__mocks__'; - -import { - registerGroupsRoute, - registerSearchGroupsRoute, - registerGroupRoute, - registerGroupUsersRoute, - registerShareGroupRoute, - registerBoostsGroupRoute, -} from './groups'; - -describe('groups routes', () => { - describe('GET /internal/workplace_search/groups', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'get', - path: '/internal/workplace_search/groups', - }); - - registerGroupsRoute({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request handler', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/ws/org/groups', - }); - }); - }); - - describe('POST /internal/workplace_search/groups', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'post', - path: '/internal/workplace_search/groups', - }); - - registerGroupsRoute({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request handler', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/ws/org/groups', - }); - }); - - describe('validates', () => { - it('correctly', () => { - const request = { - body: { - group_name: 'group', - }, - }; - mockRouter.shouldValidate(request); - }); - }); - }); - - describe('POST /internal/workplace_search/groups/search', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'post', - path: '/internal/workplace_search/groups/search', - }); - - registerSearchGroupsRoute({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request handler', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/ws/org/groups/search', - }); - }); - - describe('validates', () => { - it('correctly', () => { - const request = { - body: { - page: { - current: 1, - size: 1, - }, - search: { - query: 'foo', - content_source_ids: ['123', '234'], - }, - }, - }; - mockRouter.shouldValidate(request); - }); - - it('throws on unnecessary properties', () => { - const request = { - body: { - page: null, - search: { - kites: 'bar', - }, - }, - }; - mockRouter.shouldThrow(request); - }); - }); - }); - - describe('GET /internal/workplace_search/groups/{id}', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'get', - path: '/internal/workplace_search/groups/{id}', - }); - - registerGroupRoute({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request handler', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/ws/org/groups/:id', - }); - }); - }); - - describe('PUT /internal/workplace_search/groups/{id}', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'put', - path: '/internal/workplace_search/groups/{id}', - }); - - registerGroupRoute({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request handler', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/ws/org/groups/:id', - }); - }); - - describe('validates', () => { - it('correctly', () => { - const request = { - body: { - group: { - name: 'group', - }, - }, - }; - mockRouter.shouldValidate(request); - }); - }); - }); - - describe('DELETE /internal/workplace_search/groups/{id}', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'delete', - path: '/internal/workplace_search/groups/{id}', - }); - - registerGroupRoute({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request handler', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/ws/org/groups/:id', - }); - }); - }); - - describe('GET /internal/workplace_search/groups/{id}/group_users', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'get', - path: '/internal/workplace_search/groups/{id}/group_users', - }); - - registerGroupUsersRoute({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request handler', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/ws/org/groups/:id/group_users', - }); - }); - }); - - describe('POST /internal/workplace_search/groups/{id}/share', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'post', - path: '/internal/workplace_search/groups/{id}/share', - }); - - registerShareGroupRoute({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request handler', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/ws/org/groups/:id/share', - }); - }); - - describe('validates', () => { - it('correctly', () => { - const request = { - params: { id: '123' }, - body: { - content_source_ids: ['123', '234'], - }, - }; - mockRouter.shouldValidate(request); - }); - }); - }); - - describe('PUT /internal/workplace_search/groups/{id}/boosts', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'put', - path: '/internal/workplace_search/groups/{id}/boosts', - }); - - registerBoostsGroupRoute({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request handler', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/ws/org/groups/:id/update_source_boosts', - }); - }); - - describe('validates', () => { - it('correctly', () => { - const request = { - body: { - content_source_boosts: [['boost'], ['boost2', 'boost3']], - }, - }; - mockRouter.shouldValidate(request); - }); - }); - }); -}); diff --git a/x-pack/solutions/search/plugins/enterprise_search/server/routes/workplace_search/groups.ts b/x-pack/solutions/search/plugins/enterprise_search/server/routes/workplace_search/groups.ts deleted file mode 100644 index 8dc153e7a2923..0000000000000 --- a/x-pack/solutions/search/plugins/enterprise_search/server/routes/workplace_search/groups.ts +++ /dev/null @@ -1,185 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { schema } from '@kbn/config-schema'; - -import { RouteDependencies } from '../../plugin'; - -export function registerGroupsRoute({ router, enterpriseSearchRequestHandler }: RouteDependencies) { - router.get( - { - path: '/internal/workplace_search/groups', - validate: false, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/ws/org/groups', - }) - ); - - router.post( - { - path: '/internal/workplace_search/groups', - validate: { - body: schema.object({ - group_name: schema.string(), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/ws/org/groups', - }) - ); -} - -export function registerSearchGroupsRoute({ - router, - enterpriseSearchRequestHandler, -}: RouteDependencies) { - router.post( - { - path: '/internal/workplace_search/groups/search', - validate: { - body: schema.object({ - page: schema.object({ - current: schema.number(), - size: schema.number(), - }), - search: schema.object({ - query: schema.string(), - content_source_ids: schema.arrayOf(schema.string()), - }), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/ws/org/groups/search', - }) - ); -} - -export function registerGroupRoute({ router, enterpriseSearchRequestHandler }: RouteDependencies) { - router.get( - { - path: '/internal/workplace_search/groups/{id}', - validate: { - params: schema.object({ - id: schema.string(), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/ws/org/groups/:id', - }) - ); - - router.put( - { - path: '/internal/workplace_search/groups/{id}', - validate: { - params: schema.object({ - id: schema.string(), - }), - body: schema.object({ - group: schema.object({ - name: schema.string(), - }), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/ws/org/groups/:id', - }) - ); - - router.delete( - { - path: '/internal/workplace_search/groups/{id}', - validate: { - params: schema.object({ - id: schema.string(), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/ws/org/groups/:id', - }) - ); -} - -export function registerGroupUsersRoute({ - router, - enterpriseSearchRequestHandler, -}: RouteDependencies) { - router.get( - { - path: '/internal/workplace_search/groups/{id}/group_users', - validate: { - params: schema.object({ - id: schema.string(), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/ws/org/groups/:id/group_users', - }) - ); -} - -export function registerShareGroupRoute({ - router, - enterpriseSearchRequestHandler, -}: RouteDependencies) { - router.post( - { - path: '/internal/workplace_search/groups/{id}/share', - validate: { - params: schema.object({ - id: schema.string(), - }), - body: schema.object({ - content_source_ids: schema.arrayOf(schema.string()), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/ws/org/groups/:id/share', - }) - ); -} - -export function registerBoostsGroupRoute({ - router, - enterpriseSearchRequestHandler, -}: RouteDependencies) { - router.put( - { - path: '/internal/workplace_search/groups/{id}/boosts', - validate: { - params: schema.object({ - id: schema.string(), - }), - body: schema.object({ - content_source_boosts: schema.arrayOf( - schema.arrayOf(schema.oneOf([schema.string(), schema.number()])) - ), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/ws/org/groups/:id/update_source_boosts', - }) - ); -} - -export const registerGroupsRoutes = (dependencies: RouteDependencies) => { - registerGroupsRoute(dependencies); - registerSearchGroupsRoute(dependencies); - registerGroupRoute(dependencies); - registerGroupUsersRoute(dependencies); - registerShareGroupRoute(dependencies); - registerBoostsGroupRoute(dependencies); -}; diff --git a/x-pack/solutions/search/plugins/enterprise_search/server/routes/workplace_search/index.test.ts b/x-pack/solutions/search/plugins/enterprise_search/server/routes/workplace_search/index.test.ts deleted file mode 100644 index 65cacd69b29a7..0000000000000 --- a/x-pack/solutions/search/plugins/enterprise_search/server/routes/workplace_search/index.test.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { mockDependencies, MockRouter } from '../../__mocks__'; - -import { registerWorkplaceSearchRoutes } from '.'; - -describe('registerWorkplaceSearchRoutes', () => { - it('runs without errors', () => { - const mockRouter = new MockRouter({} as any); - registerWorkplaceSearchRoutes({ ...mockDependencies, router: mockRouter.router }); - }); -}); diff --git a/x-pack/solutions/search/plugins/enterprise_search/server/routes/workplace_search/index.ts b/x-pack/solutions/search/plugins/enterprise_search/server/routes/workplace_search/index.ts deleted file mode 100644 index b3837cd62353c..0000000000000 --- a/x-pack/solutions/search/plugins/enterprise_search/server/routes/workplace_search/index.ts +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { RouteDependencies } from '../../plugin'; - -import { registerApiKeysRoutes } from './api_keys'; -import { registerGatedFormRoute } from './gated_form'; -import { registerGroupsRoutes } from './groups'; -import { registerOAuthRoutes } from './oauth'; -import { registerOverviewRoute } from './overview'; -import { registerRoleMappingsRoutes } from './role_mappings'; -import { registerSecurityRoutes } from './security'; -import { registerSettingsRoutes } from './settings'; -import { registerSourcesRoutes } from './sources'; - -export const registerWorkplaceSearchRoutes = (dependencies: RouteDependencies) => { - registerApiKeysRoutes(dependencies); - registerOverviewRoute(dependencies); - registerOAuthRoutes(dependencies); - registerGroupsRoutes(dependencies); - registerRoleMappingsRoutes(dependencies); - registerSourcesRoutes(dependencies); - registerSettingsRoutes(dependencies); - registerSecurityRoutes(dependencies); - registerGatedFormRoute(dependencies); -}; diff --git a/x-pack/solutions/search/plugins/enterprise_search/server/routes/workplace_search/oauth.test.ts b/x-pack/solutions/search/plugins/enterprise_search/server/routes/workplace_search/oauth.test.ts deleted file mode 100644 index af703c1d0ceee..0000000000000 --- a/x-pack/solutions/search/plugins/enterprise_search/server/routes/workplace_search/oauth.test.ts +++ /dev/null @@ -1,143 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { MockRouter, mockRequestHandler, mockDependencies } from '../../__mocks__'; - -import { - registerOAuthAuthorizeRoute, - registerOAuthAuthorizeAcceptRoute, - registerOAuthAuthorizeDenyRoute, -} from './oauth'; - -describe('oauth routes', () => { - describe('GET /internal/workplace_search/oauth/authorize', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - - mockRouter = new MockRouter({ - method: 'get', - path: '/internal/workplace_search/oauth/authorize', - }); - - registerOAuthAuthorizeRoute({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request handler', async () => { - await mockRouter.callRoute({}); - - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/ws/oauth/authorize', - }); - }); - - describe('validates', () => { - it('correctly', () => { - const request = { - query: { - access_type: 'offline', - client_id: 'SomeClientUID', - code_challenge: 'SomeRandomString', - code_challenge_method: 'plain', - response_type: 'code', - response_mode: 'query', - redirect_uri: 'https://my.domain/callback', - scope: 'search', - state: 'someRandomString', - }, - }; - mockRouter.shouldValidate(request); - }); - }); - }); - - describe('POST /internal/workplace_search/oauth/authorize', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - - mockRouter = new MockRouter({ - method: 'post', - path: '/internal/workplace_search/oauth/authorize', - }); - - registerOAuthAuthorizeAcceptRoute({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request handler', async () => { - await mockRouter.callRoute({}); - - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/ws/oauth/authorize', - }); - }); - - describe('validates', () => { - it('correctly', () => { - const request = { - body: { - client_id: 'SomeClientUID', - response_type: 'code', - redirect_uri: 'https://my.domain/callback', - scope: 'search', - state: 'someRandomString', - }, - }; - mockRouter.shouldValidate(request); - }); - }); - }); - - describe('DELETE /internal/workplace_search/oauth/authorize', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - - mockRouter = new MockRouter({ - method: 'delete', - path: '/internal/workplace_search/oauth/authorize', - }); - - registerOAuthAuthorizeDenyRoute({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request handler', async () => { - await mockRouter.callRoute({}); - - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/ws/oauth/authorize', - }); - }); - - describe('validates', () => { - it('correctly', () => { - const request = { - body: { - client_id: 'SomeClientUID', - response_type: 'code', - redirect_uri: 'https://my.domain/callback', - scope: 'search', - state: 'someRandomString', - }, - }; - mockRouter.shouldValidate(request); - }); - }); - }); -}); diff --git a/x-pack/solutions/search/plugins/enterprise_search/server/routes/workplace_search/oauth.ts b/x-pack/solutions/search/plugins/enterprise_search/server/routes/workplace_search/oauth.ts deleted file mode 100644 index a87d22b6b047a..0000000000000 --- a/x-pack/solutions/search/plugins/enterprise_search/server/routes/workplace_search/oauth.ts +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { schema } from '@kbn/config-schema'; - -import { RouteDependencies } from '../../plugin'; - -export function registerOAuthAuthorizeRoute({ - router, - enterpriseSearchRequestHandler, -}: RouteDependencies) { - router.get( - { - path: '/internal/workplace_search/oauth/authorize', - validate: { - query: schema.object({ - access_type: schema.maybe(schema.string()), - client_id: schema.string(), - code_challenge: schema.maybe(schema.string()), - code_challenge_method: schema.maybe(schema.string()), - response_type: schema.string(), - response_mode: schema.maybe(schema.string()), - redirect_uri: schema.maybe(schema.string()), - scope: schema.maybe(schema.string()), - state: schema.nullable(schema.string()), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/ws/oauth/authorize', - }) - ); -} - -export function registerOAuthAuthorizeAcceptRoute({ - router, - enterpriseSearchRequestHandler, -}: RouteDependencies) { - router.post( - { - path: '/internal/workplace_search/oauth/authorize', - validate: { - body: schema.object({ - client_id: schema.string(), - response_type: schema.string(), - redirect_uri: schema.maybe(schema.string()), - scope: schema.maybe(schema.string()), - state: schema.nullable(schema.string()), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/ws/oauth/authorize', - }) - ); -} - -export function registerOAuthAuthorizeDenyRoute({ - router, - enterpriseSearchRequestHandler, -}: RouteDependencies) { - router.delete( - { - path: '/internal/workplace_search/oauth/authorize', - validate: { - body: schema.object({ - client_id: schema.string(), - response_type: schema.string(), - redirect_uri: schema.maybe(schema.string()), - scope: schema.maybe(schema.string()), - state: schema.nullable(schema.string()), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/ws/oauth/authorize', - }) - ); -} - -export const registerOAuthRoutes = (dependencies: RouteDependencies) => { - registerOAuthAuthorizeRoute(dependencies); - registerOAuthAuthorizeAcceptRoute(dependencies); - registerOAuthAuthorizeDenyRoute(dependencies); -}; diff --git a/x-pack/solutions/search/plugins/enterprise_search/server/routes/workplace_search/overview.test.ts b/x-pack/solutions/search/plugins/enterprise_search/server/routes/workplace_search/overview.test.ts deleted file mode 100644 index 0b4010bd1a09f..0000000000000 --- a/x-pack/solutions/search/plugins/enterprise_search/server/routes/workplace_search/overview.test.ts +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { MockRouter, mockRequestHandler, mockDependencies } from '../../__mocks__'; - -import { registerOverviewRoute } from './overview'; - -describe('Overview route', () => { - describe('GET /internal/workplace_search/overview', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'get', - path: '/internal/workplace_search/overview', - }); - - registerOverviewRoute({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request handler', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/ws/org', - hasValidData: expect.any(Function), - }); - }); - - describe('hasValidData', () => { - it('should correctly validate that the response has data', () => { - const response = { - accountsCount: 1, - }; - - expect(mockRequestHandler.hasValidData(response)).toBe(true); - }); - - it('should correctly validate that a response does not have data', () => { - const response = { - foo: 'bar', - }; - - expect(mockRequestHandler.hasValidData(response)).toBe(false); - }); - }); - }); -}); diff --git a/x-pack/solutions/search/plugins/enterprise_search/server/routes/workplace_search/overview.ts b/x-pack/solutions/search/plugins/enterprise_search/server/routes/workplace_search/overview.ts deleted file mode 100644 index 74185439c2f37..0000000000000 --- a/x-pack/solutions/search/plugins/enterprise_search/server/routes/workplace_search/overview.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { RouteDependencies } from '../../plugin'; - -export function registerOverviewRoute({ - router, - enterpriseSearchRequestHandler, -}: RouteDependencies) { - router.get( - { - path: '/internal/workplace_search/overview', - validate: false, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/ws/org', - hasValidData: (body: { accountsCount: number }) => typeof body?.accountsCount === 'number', - }) - ); -} diff --git a/x-pack/solutions/search/plugins/enterprise_search/server/routes/workplace_search/role_mappings.test.ts b/x-pack/solutions/search/plugins/enterprise_search/server/routes/workplace_search/role_mappings.test.ts deleted file mode 100644 index 4130a6ba12097..0000000000000 --- a/x-pack/solutions/search/plugins/enterprise_search/server/routes/workplace_search/role_mappings.test.ts +++ /dev/null @@ -1,180 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { MockRouter, mockRequestHandler, mockDependencies } from '../../__mocks__'; - -import { - registerOrgEnableRoleMappingsRoute, - registerOrgRoleMappingsRoute, - registerOrgRoleMappingRoute, - registerOrgUserRoute, -} from './role_mappings'; - -describe('role mappings routes', () => { - describe('POST /internal/workplace_search/org/role_mappings/enable_role_based_access', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'post', - path: '/internal/workplace_search/org/role_mappings/enable_role_based_access', - }); - - registerOrgEnableRoleMappingsRoute({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request handler', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/ws/org/role_mappings/enable_role_based_access', - }); - }); - }); - - describe('GET /internal/workplace_search/org/role_mappings', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'get', - path: '/internal/workplace_search/org/role_mappings', - }); - - registerOrgRoleMappingsRoute({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request handler', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/ws/org/role_mappings/collection', - }); - }); - }); - - describe('POST /internal/workplace_search/org/role_mappings', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'post', - path: '/internal/workplace_search/org/role_mappings', - }); - - registerOrgRoleMappingsRoute({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request handler', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/ws/org/role_mappings/collection', - }); - }); - }); - - describe('PUT /internal/workplace_search/org/role_mappings/{id}', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'put', - path: '/internal/workplace_search/org/role_mappings/{id}', - }); - - registerOrgRoleMappingRoute({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request handler', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/ws/org/role_mappings/:id', - }); - }); - }); - - describe('DELETE /internal/workplace_search/org/role_mappings/{id}', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'delete', - path: '/internal/workplace_search/org/role_mappings/{id}', - }); - - registerOrgRoleMappingRoute({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request handler', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/ws/org/role_mappings/:id', - }); - }); - }); - - describe('POST /internal/workplace_search/org/single_user_role_mapping', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'post', - path: '/internal/workplace_search/org/single_user_role_mapping', - }); - - registerOrgUserRoute({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - describe('validates', () => { - it('correctly', () => { - const request = { - body: { - roleMapping: { - groups: ['foo', 'bar'], - roleType: 'admin', - allGroups: true, - id: '123asf', - }, - elasticsearchUser: { - username: 'user2@elastic.co', - email: 'user2', - }, - }, - }; - mockRouter.shouldValidate(request); - }); - - it('missing required fields', () => { - const request = { body: {} }; - mockRouter.shouldThrow(request); - }); - }); - - it('creates a request handler', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/ws/org/role_mappings/upsert_single_user_role_mapping', - }); - }); - }); -}); diff --git a/x-pack/solutions/search/plugins/enterprise_search/server/routes/workplace_search/role_mappings.ts b/x-pack/solutions/search/plugins/enterprise_search/server/routes/workplace_search/role_mappings.ts deleted file mode 100644 index 094ad6c2a238b..0000000000000 --- a/x-pack/solutions/search/plugins/enterprise_search/server/routes/workplace_search/role_mappings.ts +++ /dev/null @@ -1,128 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { schema } from '@kbn/config-schema'; - -import { RouteDependencies } from '../../plugin'; - -const roleMappingBaseSchema = { - rules: schema.recordOf(schema.string(), schema.string()), - roleType: schema.string(), - groups: schema.arrayOf(schema.string()), - allGroups: schema.boolean(), -}; - -export function registerOrgEnableRoleMappingsRoute({ - router, - enterpriseSearchRequestHandler, -}: RouteDependencies) { - router.post( - { - path: '/internal/workplace_search/org/role_mappings/enable_role_based_access', - validate: false, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/ws/org/role_mappings/enable_role_based_access', - }) - ); -} - -export function registerOrgRoleMappingsRoute({ - router, - enterpriseSearchRequestHandler, -}: RouteDependencies) { - router.get( - { - path: '/internal/workplace_search/org/role_mappings', - validate: false, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/ws/org/role_mappings/collection', - }) - ); - - router.post( - { - path: '/internal/workplace_search/org/role_mappings', - validate: { - body: schema.object(roleMappingBaseSchema), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/ws/org/role_mappings/collection', - }) - ); -} - -export function registerOrgRoleMappingRoute({ - router, - enterpriseSearchRequestHandler, -}: RouteDependencies) { - router.put( - { - path: '/internal/workplace_search/org/role_mappings/{id}', - validate: { - body: schema.object(roleMappingBaseSchema), - params: schema.object({ - id: schema.string(), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/ws/org/role_mappings/:id', - }) - ); - - router.delete( - { - path: '/internal/workplace_search/org/role_mappings/{id}', - validate: { - params: schema.object({ - id: schema.string(), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/ws/org/role_mappings/:id', - }) - ); -} - -export function registerOrgUserRoute({ - router, - enterpriseSearchRequestHandler, -}: RouteDependencies) { - router.post( - { - path: '/internal/workplace_search/org/single_user_role_mapping', - validate: { - body: schema.object({ - roleMapping: schema.object({ - groups: schema.arrayOf(schema.string()), - roleType: schema.string(), - allGroups: schema.boolean(), - id: schema.maybe(schema.string()), - }), - elasticsearchUser: schema.object({ - username: schema.string(), - email: schema.string(), - }), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/ws/org/role_mappings/upsert_single_user_role_mapping', - }) - ); -} - -export const registerRoleMappingsRoutes = (dependencies: RouteDependencies) => { - registerOrgEnableRoleMappingsRoute(dependencies); - registerOrgRoleMappingsRoute(dependencies); - registerOrgRoleMappingRoute(dependencies); - registerOrgUserRoute(dependencies); -}; diff --git a/x-pack/solutions/search/plugins/enterprise_search/server/routes/workplace_search/security.test.ts b/x-pack/solutions/search/plugins/enterprise_search/server/routes/workplace_search/security.test.ts deleted file mode 100644 index ebcbba2767be3..0000000000000 --- a/x-pack/solutions/search/plugins/enterprise_search/server/routes/workplace_search/security.test.ts +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { MockRouter, mockRequestHandler, mockDependencies } from '../../__mocks__'; - -import { registerSecurityRoute, registerSecuritySourceRestrictionsRoute } from './security'; - -describe('security routes', () => { - describe('GET /internal/workplace_search/org/security', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - - mockRouter = new MockRouter({ - method: 'get', - path: '/internal/workplace_search/org/security', - }); - - registerSecurityRoute({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request handler', async () => { - await mockRouter.callRoute({}); - - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/ws/org/security', - }); - }); - }); - - describe('GET /internal/workplace_search/org/security/source_restrictions', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - - mockRouter = new MockRouter({ - method: 'get', - path: '/internal/workplace_search/org/security/source_restrictions', - }); - - registerSecuritySourceRestrictionsRoute({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request handler', async () => { - await mockRouter.callRoute({}); - - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/ws/org/security/source_restrictions', - }); - }); - }); - - describe('PATCH /internal/workplace_search/org/security/source_restrictions', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - - mockRouter = new MockRouter({ - method: 'patch', - path: '/internal/workplace_search/org/security/source_restrictions', - }); - - registerSecuritySourceRestrictionsRoute({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request handler', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/ws/org/security/source_restrictions', - }); - }); - - describe('validates', () => { - it('correctly', () => { - const request = { - body: { - isEnabled: true, - remote: { - isEnabled: true, - contentSources: [{ id: 'gmail', name: 'Gmail', isEnabled: true }], - }, - standard: { - isEnabled: false, - contentSources: [{ id: 'dropbox', name: 'Dropbox', isEnabled: false }], - }, - }, - }; - mockRouter.shouldValidate(request); - }); - }); - }); -}); diff --git a/x-pack/solutions/search/plugins/enterprise_search/server/routes/workplace_search/security.ts b/x-pack/solutions/search/plugins/enterprise_search/server/routes/workplace_search/security.ts deleted file mode 100644 index 7c9bf7f462a06..0000000000000 --- a/x-pack/solutions/search/plugins/enterprise_search/server/routes/workplace_search/security.ts +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { schema } from '@kbn/config-schema'; - -import { RouteDependencies } from '../../plugin'; - -export function registerSecurityRoute({ - router, - enterpriseSearchRequestHandler, -}: RouteDependencies) { - router.get( - { - path: '/internal/workplace_search/org/security', - validate: false, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/ws/org/security', - }) - ); -} - -export function registerSecuritySourceRestrictionsRoute({ - router, - enterpriseSearchRequestHandler, -}: RouteDependencies) { - router.get( - { - path: '/internal/workplace_search/org/security/source_restrictions', - validate: false, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/ws/org/security/source_restrictions', - }) - ); - - router.patch( - { - path: '/internal/workplace_search/org/security/source_restrictions', - validate: { - body: schema.object({ - isEnabled: schema.boolean(), - remote: schema.object({ - isEnabled: schema.boolean(), - contentSources: schema.arrayOf( - schema.object({ - isEnabled: schema.boolean(), - id: schema.string(), - name: schema.string(), - }) - ), - }), - standard: schema.object({ - isEnabled: schema.boolean(), - contentSources: schema.arrayOf( - schema.object({ - isEnabled: schema.boolean(), - id: schema.string(), - name: schema.string(), - }) - ), - }), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/ws/org/security/source_restrictions', - }) - ); -} - -export const registerSecurityRoutes = (dependencies: RouteDependencies) => { - registerSecurityRoute(dependencies); - registerSecuritySourceRestrictionsRoute(dependencies); -}; diff --git a/x-pack/solutions/search/plugins/enterprise_search/server/routes/workplace_search/settings.test.ts b/x-pack/solutions/search/plugins/enterprise_search/server/routes/workplace_search/settings.test.ts deleted file mode 100644 index 205b7b244ab45..0000000000000 --- a/x-pack/solutions/search/plugins/enterprise_search/server/routes/workplace_search/settings.test.ts +++ /dev/null @@ -1,138 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { MockRouter, mockRequestHandler, mockDependencies } from '../../__mocks__'; - -import { - registerOrgSettingsRoute, - registerOrgSettingsCustomizeRoute, - registerOrgSettingsUploadImagesRoute, - registerOrgSettingsOauthApplicationRoute, -} from './settings'; - -describe('settings routes', () => { - describe('GET /internal/workplace_search/org/settings', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'get', - path: '/internal/workplace_search/org/settings', - }); - - registerOrgSettingsRoute({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request handler', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/ws/org/settings', - }); - }); - }); - - describe('PUT /internal/workplace_search/org/settings/customize', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'put', - path: '/internal/workplace_search/org/settings/customize', - }); - - registerOrgSettingsCustomizeRoute({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request handler', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/ws/org/settings/customize', - }); - }); - - describe('validates', () => { - it('correctly', () => { - const request = { body: { name: 'foo' } }; - mockRouter.shouldValidate(request); - }); - }); - }); - - describe('PUT /internal/workplace_search/org/settings/upload_images', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'put', - path: '/internal/workplace_search/org/settings/upload_images', - }); - - registerOrgSettingsUploadImagesRoute({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request handler', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/ws/org/settings/upload_images', - }); - }); - - describe('validates', () => { - it('correctly', () => { - const request = { body: { logo: 'foo', icon: null } }; - mockRouter.shouldValidate(request); - }); - }); - }); - - describe('PUT /internal/workplace_search/org/settings/oauth_application', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'put', - path: '/internal/workplace_search/org/settings/oauth_application', - }); - - registerOrgSettingsOauthApplicationRoute({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request handler', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/ws/org/settings/oauth_application', - }); - }); - - describe('validates', () => { - it('correctly', () => { - const request = { - body: { - oauth_application: { - name: 'foo', - confidential: true, - redirect_uri: 'http://foo.bar', - }, - }, - }; - mockRouter.shouldValidate(request); - }); - }); - }); -}); diff --git a/x-pack/solutions/search/plugins/enterprise_search/server/routes/workplace_search/settings.ts b/x-pack/solutions/search/plugins/enterprise_search/server/routes/workplace_search/settings.ts deleted file mode 100644 index 5a2a5309ee35d..0000000000000 --- a/x-pack/solutions/search/plugins/enterprise_search/server/routes/workplace_search/settings.ts +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { schema } from '@kbn/config-schema'; - -import { RouteDependencies } from '../../plugin'; - -const MAX_IMAGE_BYTES = 2000000; - -export function registerOrgSettingsRoute({ - router, - enterpriseSearchRequestHandler, -}: RouteDependencies) { - router.get( - { - path: '/internal/workplace_search/org/settings', - validate: false, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/ws/org/settings', - }) - ); -} - -export function registerOrgSettingsCustomizeRoute({ - router, - enterpriseSearchRequestHandler, -}: RouteDependencies) { - router.put( - { - path: '/internal/workplace_search/org/settings/customize', - validate: { - body: schema.object({ - name: schema.string(), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/ws/org/settings/customize', - }) - ); -} - -export function registerOrgSettingsUploadImagesRoute({ - router, - enterpriseSearchRequestHandler, -}: RouteDependencies) { - router.put( - { - path: '/internal/workplace_search/org/settings/upload_images', - validate: { - body: schema.object({ - logo: schema.maybe(schema.nullable(schema.string())), - icon: schema.maybe(schema.nullable(schema.string())), - }), - }, - options: { - body: { - maxBytes: MAX_IMAGE_BYTES, - }, - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/ws/org/settings/upload_images', - }) - ); -} - -export function registerOrgSettingsOauthApplicationRoute({ - router, - enterpriseSearchRequestHandler, -}: RouteDependencies) { - router.put( - { - path: '/internal/workplace_search/org/settings/oauth_application', - validate: { - body: schema.object({ - oauth_application: schema.object({ - name: schema.string(), - confidential: schema.boolean(), - redirect_uri: schema.string(), - }), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/ws/org/settings/oauth_application', - }) - ); -} - -export const registerSettingsRoutes = (dependencies: RouteDependencies) => { - registerOrgSettingsRoute(dependencies); - registerOrgSettingsCustomizeRoute(dependencies); - registerOrgSettingsUploadImagesRoute(dependencies); - registerOrgSettingsOauthApplicationRoute(dependencies); -}; diff --git a/x-pack/solutions/search/plugins/enterprise_search/server/routes/workplace_search/sources.test.ts b/x-pack/solutions/search/plugins/enterprise_search/server/routes/workplace_search/sources.test.ts deleted file mode 100644 index 06b6fce3baee5..0000000000000 --- a/x-pack/solutions/search/plugins/enterprise_search/server/routes/workplace_search/sources.test.ts +++ /dev/null @@ -1,1355 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { MockRouter, mockRequestHandler, mockDependencies } from '../../__mocks__'; - -import { ENTERPRISE_SEARCH_KIBANA_COOKIE } from '../../../common/constants'; - -import { - registerAccountSourcesRoute, - registerAccountSourcesStatusRoute, - registerAccountSourceRoute, - registerAccountCreateSourceRoute, - registerAccountSourceDocumentsRoute, - registerAccountSourceFederatedSummaryRoute, - registerAccountSourceReauthPrepareRoute, - registerAccountSourceSettingsRoute, - registerAccountPreSourceRoute, - registerAccountPrepareSourcesRoute, - registerAccountSourceSearchableRoute, - registerAccountSourceDisplaySettingsConfig, - registerAccountSourceSchemasRoute, - registerAccountSourceReindexJobRoute, - registerAccountSourceDownloadDiagnosticsRoute, - registerOrgSourcesRoute, - registerOrgSourcesStatusRoute, - registerOrgSourceRoute, - registerOrgCreateSourceRoute, - registerOrgSourceDocumentsRoute, - registerOrgSourceFederatedSummaryRoute, - registerOrgSourceReauthPrepareRoute, - registerOrgSourceSettingsRoute, - registerOrgPreSourceRoute, - registerOrgPrepareSourcesRoute, - registerOrgSourceSearchableRoute, - registerOrgSourceDisplaySettingsConfig, - registerOrgSourceSchemasRoute, - registerOrgSourceReindexJobRoute, - registerOrgSourceDownloadDiagnosticsRoute, - registerOrgSourceOauthConfigurationsRoute, - registerOrgSourceOauthConfigurationRoute, - registerOrgSourceSynchronizeRoute, - registerOauthConnectorParamsRoute, - registerAccountSourceValidateIndexingRulesRoute, - registerOrgSourceValidateIndexingRulesRoute, -} from './sources'; - -describe('sources routes', () => { - describe('GET /internal/workplace_search/account/sources', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'get', - path: '/internal/workplace_search/account/sources', - }); - - registerAccountSourcesRoute({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request handler', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/ws/sources', - }); - }); - }); - - describe('GET /internal/workplace_search/account/sources/status', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'get', - path: '/internal/workplace_search/account/sources/status', - }); - - registerAccountSourcesStatusRoute({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request handler', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/ws/sources/status', - }); - }); - }); - - describe('GET /internal/workplace_search/account/sources/{id}', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'get', - path: '/internal/workplace_search/account/sources/{id}', - }); - - registerAccountSourceRoute({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request handler', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/ws/sources/:id', - }); - }); - }); - - describe('DELETE /internal/workplace_search/account/sources/{id}', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'delete', - path: '/internal/workplace_search/account/sources/{id}', - }); - - registerAccountSourceRoute({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request handler', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/ws/sources/:id', - }); - }); - }); - - describe('POST /internal/workplace_search/account/create_source', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'post', - path: '/internal/workplace_search/account/create_source', - }); - - registerAccountCreateSourceRoute({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request handler', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/ws/sources/form_create', - }); - }); - - describe('validates', () => { - it('correctly', () => { - const request = { - body: { - service_type: 'google', - name: 'Google', - login: 'user', - password: 'changeme', - organizations: ['swiftype'], - index_permissions: true, - }, - }; - mockRouter.shouldValidate(request); - }); - }); - }); - - describe('POST /internal/workplace_search/account/sources/{id}/documents', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'post', - path: '/internal/workplace_search/account/sources/{id}/documents', - }); - - registerAccountSourceDocumentsRoute({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request handler', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/ws/sources/:id/documents', - }); - }); - - describe('validates', () => { - it('correctly', () => { - const request = { - body: { - query: 'foo', - page: { - current: 1, - size: 10, - total_pages: 1, - total_results: 10, - }, - }, - }; - mockRouter.shouldValidate(request); - }); - }); - }); - - describe('GET /internal/workplace_search/account/sources/{id}/federated_summary', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'get', - path: '/internal/workplace_search/account/sources/{id}/federated_summary', - }); - - registerAccountSourceFederatedSummaryRoute({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request handler', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/ws/sources/:id/federated_summary', - }); - }); - }); - - describe('GET /internal/workplace_search/account/sources/{id}/reauth_prepare', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'get', - path: '/internal/workplace_search/account/sources/{id}/reauth_prepare', - }); - - registerAccountSourceReauthPrepareRoute({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request handler', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/ws/sources/:id/reauth_prepare', - }); - }); - }); - - describe('PATCH /internal/workplace_search/account/sources/{id}/settings', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'patch', - path: '/internal/workplace_search/account/sources/{id}/settings', - }); - - registerAccountSourceSettingsRoute({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request handler', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/ws/sources/:id/settings', - }); - }); - - describe('validates', () => { - it('correctly', () => { - const request = { - body: { - content_source: { - name: 'foo', - }, - }, - }; - mockRouter.shouldValidate(request); - }); - }); - }); - - describe('POST /internal/workplace_search/account/sources/{id}/indexing_rules/validate', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'post', - path: '/internal/workplace_search/account/sources/{id}/indexing_rules/validate', - }); - - registerAccountSourceValidateIndexingRulesRoute({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request handler', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/ws/sources/:id/indexing_rules/validate', - }); - }); - - describe('validates', () => { - it('correctly', () => { - const request = { - body: { - rules: [ - { - filter_type: 'path_template', - exclude: '', - }, - ], - }, - }; - mockRouter.shouldValidate(request); - }); - }); - }); - - describe('GET /internal/workplace_search/account/pre_sources/{id}', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'get', - path: '/internal/workplace_search/account/pre_sources/{id}', - }); - - registerAccountPreSourceRoute({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request handler', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/ws/pre_content_sources/:id', - }); - }); - }); - - describe('GET /internal/workplace_search/account/sources/{serviceType}/prepare', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'get', - path: '/internal/workplace_search/account/sources/{serviceType}/prepare', - }); - - registerAccountPrepareSourcesRoute({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request handler', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/ws/sources/:serviceType/prepare', - }); - }); - }); - - describe('PUT /internal/workplace_search/account/sources/{id}/searchable', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'put', - path: '/internal/workplace_search/account/sources/{id}/searchable', - }); - - registerAccountSourceSearchableRoute({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request handler', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/ws/sources/:id/searchable', - }); - }); - - describe('validates', () => { - it('correctly', () => { - const request = { - body: { - searchable: true, - }, - }; - mockRouter.shouldValidate(request); - }); - }); - }); - - describe('GET /internal/workplace_search/account/sources/{id}/display_settings/config', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'get', - path: '/internal/workplace_search/account/sources/{id}/display_settings/config', - }); - - registerAccountSourceDisplaySettingsConfig({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request handler', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/ws/sources/:id/display_settings/config', - }); - }); - }); - - describe('POST /internal/workplace_search/account/sources/{id}/display_settings/config', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'post', - path: '/internal/workplace_search/account/sources/{id}/display_settings/config', - }); - - registerAccountSourceDisplaySettingsConfig({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request handler', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/ws/sources/:id/display_settings/config', - }); - }); - - describe('validates', () => { - it('correctly', () => { - const request = { - body: { - titleField: 'foo', - subtitleField: 'bar', - descriptionField: 'this is a thing', - urlField: 'http://youknowfor.search', - urlFieldIsLinkable: true, - color: '#aaa', - detailFields: { - fieldName: 'myField', - label: 'My Field', - }, - }, - }; - mockRouter.shouldValidate(request); - }); - }); - }); - - describe('GET /internal/workplace_search/account/sources/{id}/schemas', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'get', - path: '/internal/workplace_search/account/sources/{id}/schemas', - }); - - registerAccountSourceSchemasRoute({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request handler', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/ws/sources/:id/schemas', - }); - }); - }); - - describe('POST /internal/workplace_search/account/sources/{id}/schemas', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'post', - path: '/internal/workplace_search/account/sources/{id}/schemas', - }); - - registerAccountSourceSchemasRoute({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request handler', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/ws/sources/:id/schemas', - }); - }); - - describe('validates', () => { - it('correctly', () => { - const request = { body: { someSchemaKey: 'text' } }; - mockRouter.shouldValidate(request); - }); - }); - }); - - describe('GET /internal/workplace_search/account/sources/{sourceId}/reindex_job/{jobId}', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'get', - path: '/internal/workplace_search/account/sources/{sourceId}/reindex_job/{jobId}', - }); - - registerAccountSourceReindexJobRoute({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request handler', async () => { - const mockRequest = { - params: { - sourceId: '123', - jobId: '345', - }, - }; - - await mockRouter.callRoute(mockRequest); - - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/ws/sources/:sourceId/reindex_job/:jobId', - }); - }); - }); - - describe('GET /internal/workplace_search/account/sources/{sourceId}/download_diagnostics', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'get', - path: '/internal/workplace_search/account/sources/{sourceId}/download_diagnostics', - }); - - registerAccountSourceDownloadDiagnosticsRoute({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request handler', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/ws/sources/:sourceId/download_diagnostics', - hasJsonResponse: false, - }); - }); - }); - - describe('GET /internal/workplace_search/org/sources', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'get', - path: '/internal/workplace_search/org/sources', - }); - - registerOrgSourcesRoute({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request handler', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/ws/org/sources', - }); - }); - }); - - describe('GET /internal/workplace_search/org/sources/status', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'get', - path: '/internal/workplace_search/org/sources/status', - }); - - registerOrgSourcesStatusRoute({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request handler', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/ws/org/sources/status', - }); - }); - }); - - describe('GET /internal/workplace_search/org/sources/{id}', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'get', - path: '/internal/workplace_search/org/sources/{id}', - }); - - registerOrgSourceRoute({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request handler', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/ws/org/sources/:id', - }); - }); - }); - - describe('DELETE /internal/workplace_search/org/sources/{id}', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'delete', - path: '/internal/workplace_search/org/sources/{id}', - }); - - registerOrgSourceRoute({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request handler', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/ws/org/sources/:id', - }); - }); - }); - - describe('POST /internal/workplace_search/org/create_source', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'post', - path: '/internal/workplace_search/org/create_source', - }); - - registerOrgCreateSourceRoute({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request handler', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/ws/org/sources/form_create', - }); - }); - - describe('validates', () => { - it('correctly', () => { - const request = { - body: { - service_type: 'google', - name: 'Google', - login: 'user', - password: 'changeme', - organizations: ['swiftype'], - index_permissions: true, - }, - }; - mockRouter.shouldValidate(request); - }); - }); - }); - - describe('POST /internal/workplace_search/org/sources/{id}/documents', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'post', - path: '/internal/workplace_search/org/sources/{id}/documents', - }); - - registerOrgSourceDocumentsRoute({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request handler', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/ws/org/sources/:id/documents', - }); - }); - - describe('validates', () => { - it('correctly', () => { - const request = { - body: { - query: 'foo', - page: { - current: 1, - size: 10, - total_pages: 1, - total_results: 10, - }, - }, - }; - mockRouter.shouldValidate(request); - }); - }); - }); - - describe('GET /internal/workplace_search/org/sources/{id}/federated_summary', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'get', - path: '/internal/workplace_search/org/sources/{id}/federated_summary', - }); - - registerOrgSourceFederatedSummaryRoute({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request handler', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/ws/org/sources/:id/federated_summary', - }); - }); - }); - - describe('GET /internal/workplace_search/org/sources/{id}/reauth_prepare', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'get', - path: '/internal/workplace_search/org/sources/{id}/reauth_prepare', - }); - - registerOrgSourceReauthPrepareRoute({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request handler', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/ws/org/sources/:id/reauth_prepare', - }); - }); - }); - - describe('PATCH /internal/workplace_search/org/sources/{id}/settings', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'patch', - path: '/internal/workplace_search/org/sources/{id}/settings', - }); - - registerOrgSourceSettingsRoute({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request handler', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/ws/org/sources/:id/settings', - }); - }); - - describe('validates', () => { - it('correctly', () => { - const request = { - body: { - content_source: { - name: 'foo', - }, - }, - }; - mockRouter.shouldValidate(request); - }); - }); - }); - - describe('POST /internal/workplace_search/org/sources/{id}/indexing_rules/validate', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'post', - path: '/internal/workplace_search/org/sources/{id}/indexing_rules/validate', - }); - - registerOrgSourceValidateIndexingRulesRoute({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request handler', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/ws/org/sources/:id/indexing_rules/validate', - }); - }); - - describe('validates', () => { - it('correctly', () => { - const request = { - body: { - rules: [ - { - filter_type: 'path_template', - exclude: '', - }, - ], - }, - }; - mockRouter.shouldValidate(request); - }); - }); - }); - - describe('GET /internal/workplace_search/org/pre_sources/{id}', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'get', - path: '/internal/workplace_search/org/pre_sources/{id}', - }); - - registerOrgPreSourceRoute({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request handler', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/ws/org/pre_content_sources/:id', - }); - }); - }); - - describe('GET /internal/workplace_search/org/sources/{serviceType}/prepare', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'get', - path: '/internal/workplace_search/org/sources/{serviceType}/prepare', - }); - - registerOrgPrepareSourcesRoute({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request handler', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/ws/org/sources/:serviceType/prepare', - }); - }); - }); - - describe('PUT /internal/workplace_search/org/sources/{id}/searchable', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'put', - path: '/internal/workplace_search/org/sources/{id}/searchable', - }); - - registerOrgSourceSearchableRoute({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request handler', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/ws/org/sources/:id/searchable', - }); - }); - - describe('validates', () => { - it('correctly', () => { - const request = { - body: { - searchable: true, - }, - }; - mockRouter.shouldValidate(request); - }); - }); - }); - - describe('GET /internal/workplace_search/org/sources/{id}/display_settings/config', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'get', - path: '/internal/workplace_search/org/sources/{id}/display_settings/config', - }); - - registerOrgSourceDisplaySettingsConfig({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request handler', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/ws/org/sources/:id/display_settings/config', - }); - }); - }); - - describe('POST /internal/workplace_search/org/sources/{id}/display_settings/config', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'post', - path: '/internal/workplace_search/org/sources/{id}/display_settings/config', - }); - - registerOrgSourceDisplaySettingsConfig({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request handler', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/ws/org/sources/:id/display_settings/config', - }); - }); - - describe('validates', () => { - it('correctly', () => { - const request = { - body: { - titleField: 'foo', - subtitleField: 'bar', - descriptionField: 'this is a thing', - urlField: 'http://youknowfor.search', - urlFieldIsLinkable: true, - color: '#aaa', - detailFields: { - fieldName: 'myField', - label: 'My Field', - }, - }, - }; - mockRouter.shouldValidate(request); - }); - }); - }); - - describe('GET /internal/workplace_search/org/sources/{id}/schemas', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'get', - path: '/internal/workplace_search/org/sources/{id}/schemas', - }); - - registerOrgSourceSchemasRoute({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request handler', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/ws/org/sources/:id/schemas', - }); - }); - }); - - describe('POST /internal/workplace_search/org/sources/{id}/schemas', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'post', - path: '/internal/workplace_search/org/sources/{id}/schemas', - }); - - registerOrgSourceSchemasRoute({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request handler', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/ws/org/sources/:id/schemas', - }); - }); - - describe('validates', () => { - it('correctly', () => { - const request = { body: { someSchemaKey: 'number' } }; - mockRouter.shouldValidate(request); - }); - }); - }); - - describe('GET /internal/workplace_search/org/sources/{sourceId}/reindex_job/{jobId}', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'get', - path: '/internal/workplace_search/org/sources/{sourceId}/reindex_job/{jobId}', - }); - - registerOrgSourceReindexJobRoute({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request handler', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/ws/org/sources/:sourceId/reindex_job/:jobId', - }); - }); - }); - - describe('GET /internal/workplace_search/org/sources/{sourceId}/download_diagnostics', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'get', - path: '/internal/workplace_search/org/sources/{sourceId}/download_diagnostics', - }); - - registerOrgSourceDownloadDiagnosticsRoute({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request handler', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/ws/org/sources/:sourceId/download_diagnostics', - hasJsonResponse: false, - }); - }); - }); - - describe('GET /internal/workplace_search/org/settings/connectors', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'get', - path: '/internal/workplace_search/org/settings/connectors', - }); - - registerOrgSourceOauthConfigurationsRoute({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request handler', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/ws/org/settings/connectors', - }); - }); - }); - - describe('POST /internal/workplace_search/org/settings/connectors', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'post', - path: '/internal/workplace_search/org/settings/connectors', - }); - - registerOrgSourceOauthConfigurationsRoute({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request handler', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/ws/org/settings/connectors', - }); - }); - }); - - describe('PUT /internal/workplace_search/org/settings/connectors', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'put', - path: '/internal/workplace_search/org/settings/connectors', - }); - - registerOrgSourceOauthConfigurationsRoute({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request handler', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/ws/org/settings/connectors', - }); - }); - }); - - describe('GET /internal/workplace_search/org/settings/connectors/{serviceType}', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'get', - path: '/internal/workplace_search/org/settings/connectors/{serviceType}', - }); - - registerOrgSourceOauthConfigurationRoute({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request handler', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/ws/org/settings/connectors/:serviceType', - }); - }); - }); - - describe('POST /internal/workplace_search/org/settings/connectors/{serviceType}', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'post', - path: '/internal/workplace_search/org/settings/connectors/{serviceType}', - }); - - registerOrgSourceOauthConfigurationRoute({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request handler', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/ws/org/settings/connectors/:serviceType', - }); - }); - }); - - describe('PUT /internal/workplace_search/org/settings/connectors/{serviceType}', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'put', - path: '/internal/workplace_search/org/settings/connectors/{serviceType}', - }); - - registerOrgSourceOauthConfigurationRoute({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request handler', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/ws/org/settings/connectors/:serviceType', - }); - }); - }); - - describe('DELETE /internal/workplace_search/org/settings/connectors/{serviceType}', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'delete', - path: '/internal/workplace_search/org/settings/connectors/{serviceType}', - }); - - registerOrgSourceOauthConfigurationRoute({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request handler', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/ws/org/settings/connectors/:serviceType', - }); - }); - }); - - describe('POST /internal/workplace_search/org/sources/{id}/sync', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'post', - path: '/internal/workplace_search/org/sources/{id}/sync', - }); - - registerOrgSourceSynchronizeRoute({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request handler', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/ws/org/sources/:id/sync', - }); - }); - }); - - describe('GET /internal/workplace_search/sources/create', () => { - const tokenPackage = 'some_encrypted_secrets'; - - const mockRequest = { - headers: { - authorization: 'BASIC 123', - cookie: `${ENTERPRISE_SEARCH_KIBANA_COOKIE}=${tokenPackage}`, - }, - }; - - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'get', - path: '/internal/workplace_search/sources/create', - }); - - registerOauthConnectorParamsRoute({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('creates a request handler', async () => { - await mockRouter.callRoute(mockRequest as any); - - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/ws/sources/create', - params: { token_package: tokenPackage }, - }); - }); - }); -}); diff --git a/x-pack/solutions/search/plugins/enterprise_search/server/routes/workplace_search/sources.ts b/x-pack/solutions/search/plugins/enterprise_search/server/routes/workplace_search/sources.ts deleted file mode 100644 index 46b9848022b28..0000000000000 --- a/x-pack/solutions/search/plugins/enterprise_search/server/routes/workplace_search/sources.ts +++ /dev/null @@ -1,1025 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { schema } from '@kbn/config-schema'; - -import { getOAuthTokenPackageParams } from '../../lib/get_oauth_token_package_params'; - -import { skipBodyValidation } from '../../lib/route_config_helpers'; -import { RouteDependencies } from '../../plugin'; - -const schemaValuesSchema = schema.recordOf( - schema.string(), - schema.oneOf([ - schema.literal('text'), - schema.literal('number'), - schema.literal('geolocation'), - schema.literal('date'), - ]) -); - -const pageSchema = schema.object({ - current: schema.nullable(schema.number()), - size: schema.nullable(schema.number()), - total_pages: schema.nullable(schema.number()), - total_results: schema.nullable(schema.number()), -}); - -const displayFieldSchema = schema.object({ - fieldName: schema.string(), - label: schema.string(), -}); - -const displaySettingsSchema = schema.object({ - titleField: schema.maybe(schema.string()), - subtitleField: schema.nullable(schema.string()), - descriptionField: schema.nullable(schema.string()), - urlField: schema.maybe(schema.string()), - typeField: schema.nullable(schema.string()), - mediaTypeField: schema.nullable(schema.string()), - createdByField: schema.nullable(schema.string()), - updatedByField: schema.nullable(schema.string()), - color: schema.string(), - urlFieldIsLinkable: schema.boolean(), - detailFields: schema.oneOf([schema.arrayOf(displayFieldSchema), displayFieldSchema]), -}); - -const sourceSettingsSchema = schema.object({ - content_source: schema.object({ - name: schema.maybe(schema.string()), - private_key: schema.maybe(schema.nullable(schema.string())), - indexing: schema.maybe( - schema.object({ - enabled: schema.maybe(schema.boolean()), - features: schema.maybe( - schema.object({ - thumbnails: schema.maybe( - schema.object({ - enabled: schema.boolean(), - }) - ), - content_extraction: schema.maybe( - schema.object({ - enabled: schema.boolean(), - }) - ), - }) - ), - schedule: schema.maybe( - schema.object({ - full: schema.maybe(schema.string()), - incremental: schema.maybe(schema.string()), - delete: schema.maybe(schema.string()), - permissions: schema.maybe(schema.string()), - blocked_windows: schema.maybe( - schema.arrayOf( - schema.object({ - job_type: schema.string(), - day: schema.string(), - start: schema.string(), - end: schema.string(), - }) - ) - ), - }) - ), - rules: schema.maybe( - schema.arrayOf( - schema.object({ - filter_type: schema.string(), - exclude: schema.maybe(schema.string()), - include: schema.maybe(schema.string()), - }) - ) - ), - }) - ), - }), -}); - -const validateRulesSchema = schema.object({ - rules: schema.maybe( - schema.arrayOf( - schema.object({ - filter_type: schema.string(), - exclude: schema.maybe(schema.string()), - include: schema.maybe(schema.string()), - }) - ) - ), -}); - -// Account routes -export function registerAccountSourcesRoute({ - router, - enterpriseSearchRequestHandler, -}: RouteDependencies) { - router.get( - { - path: '/internal/workplace_search/account/sources', - validate: false, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/ws/sources', - }) - ); -} - -export function registerAccountSourcesStatusRoute({ - router, - enterpriseSearchRequestHandler, -}: RouteDependencies) { - router.get( - { - path: '/internal/workplace_search/account/sources/status', - validate: false, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/ws/sources/status', - }) - ); -} - -export function registerAccountSourceRoute({ - router, - enterpriseSearchRequestHandler, -}: RouteDependencies) { - router.get( - { - path: '/internal/workplace_search/account/sources/{id}', - validate: { - params: schema.object({ - id: schema.string(), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/ws/sources/:id', - }) - ); - - router.delete( - { - path: '/internal/workplace_search/account/sources/{id}', - validate: { - params: schema.object({ - id: schema.string(), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/ws/sources/:id', - }) - ); -} - -export function registerAccountCreateSourceRoute({ - router, - enterpriseSearchRequestHandler, -}: RouteDependencies) { - router.post( - { - path: '/internal/workplace_search/account/create_source', - validate: { - body: schema.object({ - service_type: schema.string(), - base_service_type: schema.maybe(schema.string()), - name: schema.maybe(schema.string()), - login: schema.maybe(schema.string()), - password: schema.maybe(schema.string()), - organizations: schema.maybe(schema.arrayOf(schema.string())), - index_permissions: schema.maybe(schema.boolean()), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/ws/sources/form_create', - }) - ); -} - -export function registerAccountSourceDocumentsRoute({ - router, - enterpriseSearchRequestHandler, -}: RouteDependencies) { - router.post( - { - path: '/internal/workplace_search/account/sources/{id}/documents', - validate: { - body: schema.object({ - query: schema.string(), - page: pageSchema, - }), - params: schema.object({ - id: schema.string(), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/ws/sources/:id/documents', - }) - ); -} - -export function registerAccountSourceFederatedSummaryRoute({ - router, - enterpriseSearchRequestHandler, -}: RouteDependencies) { - router.get( - { - path: '/internal/workplace_search/account/sources/{id}/federated_summary', - validate: { - params: schema.object({ - id: schema.string(), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/ws/sources/:id/federated_summary', - }) - ); -} - -export function registerAccountSourceReauthPrepareRoute({ - router, - enterpriseSearchRequestHandler, -}: RouteDependencies) { - router.get( - { - path: '/internal/workplace_search/account/sources/{id}/reauth_prepare', - validate: { - params: schema.object({ - id: schema.string(), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/ws/sources/:id/reauth_prepare', - }) - ); -} - -export function registerAccountSourceSettingsRoute({ - router, - enterpriseSearchRequestHandler, -}: RouteDependencies) { - router.patch( - { - path: '/internal/workplace_search/account/sources/{id}/settings', - validate: { - body: sourceSettingsSchema, - params: schema.object({ - id: schema.string(), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/ws/sources/:id/settings', - }) - ); -} - -export function registerAccountSourceValidateIndexingRulesRoute({ - router, - enterpriseSearchRequestHandler, -}: RouteDependencies) { - router.post( - { - path: '/internal/workplace_search/account/sources/{id}/indexing_rules/validate', - validate: { - body: validateRulesSchema, - params: schema.object({ - id: schema.string(), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/ws/sources/:id/indexing_rules/validate', - }) - ); -} - -export function registerAccountPreSourceRoute({ - router, - enterpriseSearchRequestHandler, -}: RouteDependencies) { - router.get( - { - path: '/internal/workplace_search/account/pre_sources/{id}', - validate: { - params: schema.object({ - id: schema.string(), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/ws/pre_content_sources/:id', - }) - ); -} - -export function registerAccountPrepareSourcesRoute({ - router, - enterpriseSearchRequestHandler, -}: RouteDependencies) { - router.get( - { - path: '/internal/workplace_search/account/sources/{serviceType}/prepare', - validate: { - params: schema.object({ - serviceType: schema.string(), - }), - query: schema.object({ - subdomain: schema.maybe(schema.string()), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/ws/sources/:serviceType/prepare', - }) - ); -} - -export function registerAccountSourceSearchableRoute({ - router, - enterpriseSearchRequestHandler, -}: RouteDependencies) { - router.put( - { - path: '/internal/workplace_search/account/sources/{id}/searchable', - validate: { - body: schema.object({ - searchable: schema.boolean(), - }), - params: schema.object({ - id: schema.string(), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/ws/sources/:id/searchable', - }) - ); -} - -export function registerAccountSourceDisplaySettingsConfig({ - router, - enterpriseSearchRequestHandler, -}: RouteDependencies) { - router.get( - { - path: '/internal/workplace_search/account/sources/{id}/display_settings/config', - validate: { - params: schema.object({ - id: schema.string(), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/ws/sources/:id/display_settings/config', - }) - ); - - router.post( - { - path: '/internal/workplace_search/account/sources/{id}/display_settings/config', - validate: { - body: displaySettingsSchema, - params: schema.object({ - id: schema.string(), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/ws/sources/:id/display_settings/config', - }) - ); -} - -export function registerAccountSourceSchemasRoute({ - router, - enterpriseSearchRequestHandler, -}: RouteDependencies) { - router.get( - { - path: '/internal/workplace_search/account/sources/{id}/schemas', - validate: { - params: schema.object({ - id: schema.string(), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/ws/sources/:id/schemas', - }) - ); - - router.post( - { - path: '/internal/workplace_search/account/sources/{id}/schemas', - validate: { - body: schemaValuesSchema, - params: schema.object({ - id: schema.string(), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/ws/sources/:id/schemas', - }) - ); -} - -export function registerAccountSourceReindexJobRoute({ - router, - enterpriseSearchRequestHandler, -}: RouteDependencies) { - router.get( - { - path: '/internal/workplace_search/account/sources/{sourceId}/reindex_job/{jobId}', - validate: { - params: schema.object({ - sourceId: schema.string(), - jobId: schema.string(), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/ws/sources/:sourceId/reindex_job/:jobId', - }) - ); -} - -export function registerAccountSourceDownloadDiagnosticsRoute({ - router, - enterpriseSearchRequestHandler, -}: RouteDependencies) { - router.get( - { - path: '/internal/workplace_search/account/sources/{sourceId}/download_diagnostics', - validate: { - params: schema.object({ - sourceId: schema.string(), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/ws/sources/:sourceId/download_diagnostics', - hasJsonResponse: false, - }) - ); -} - -// Org routes -export function registerOrgSourcesRoute({ - router, - enterpriseSearchRequestHandler, -}: RouteDependencies) { - router.get( - { - path: '/internal/workplace_search/org/sources', - validate: false, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/ws/org/sources', - }) - ); -} - -export function registerOrgSourcesStatusRoute({ - router, - enterpriseSearchRequestHandler, -}: RouteDependencies) { - router.get( - { - path: '/internal/workplace_search/org/sources/status', - validate: false, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/ws/org/sources/status', - }) - ); -} - -export function registerOrgSourceRoute({ - router, - enterpriseSearchRequestHandler, -}: RouteDependencies) { - router.get( - { - path: '/internal/workplace_search/org/sources/{id}', - validate: { - params: schema.object({ - id: schema.string(), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/ws/org/sources/:id', - }) - ); - - router.delete( - { - path: '/internal/workplace_search/org/sources/{id}', - validate: { - params: schema.object({ - id: schema.string(), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/ws/org/sources/:id', - }) - ); -} - -export function registerOrgCreateSourceRoute({ - router, - enterpriseSearchRequestHandler, -}: RouteDependencies) { - router.post( - { - path: '/internal/workplace_search/org/create_source', - validate: { - body: schema.object({ - service_type: schema.string(), - base_service_type: schema.maybe(schema.string()), - name: schema.maybe(schema.string()), - login: schema.maybe(schema.string()), - password: schema.maybe(schema.string()), - organizations: schema.maybe(schema.arrayOf(schema.string())), - index_permissions: schema.maybe(schema.boolean()), - app_id: schema.maybe(schema.string()), - base_url: schema.maybe(schema.string()), - private_key: schema.nullable(schema.maybe(schema.string())), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/ws/org/sources/form_create', - }) - ); -} - -export function registerOrgSourceDocumentsRoute({ - router, - enterpriseSearchRequestHandler, -}: RouteDependencies) { - router.post( - { - path: '/internal/workplace_search/org/sources/{id}/documents', - validate: { - body: schema.object({ - query: schema.string(), - page: pageSchema, - }), - params: schema.object({ - id: schema.string(), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/ws/org/sources/:id/documents', - }) - ); -} - -export function registerOrgSourceFederatedSummaryRoute({ - router, - enterpriseSearchRequestHandler, -}: RouteDependencies) { - router.get( - { - path: '/internal/workplace_search/org/sources/{id}/federated_summary', - validate: { - params: schema.object({ - id: schema.string(), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/ws/org/sources/:id/federated_summary', - }) - ); -} - -export function registerOrgSourceReauthPrepareRoute({ - router, - enterpriseSearchRequestHandler, -}: RouteDependencies) { - router.get( - { - path: '/internal/workplace_search/org/sources/{id}/reauth_prepare', - validate: { - params: schema.object({ - id: schema.string(), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/ws/org/sources/:id/reauth_prepare', - }) - ); -} - -export function registerOrgSourceSettingsRoute({ - router, - enterpriseSearchRequestHandler, -}: RouteDependencies) { - router.patch( - { - path: '/internal/workplace_search/org/sources/{id}/settings', - validate: { - body: sourceSettingsSchema, - params: schema.object({ - id: schema.string(), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/ws/org/sources/:id/settings', - }) - ); -} - -export function registerOrgSourceValidateIndexingRulesRoute({ - router, - enterpriseSearchRequestHandler, -}: RouteDependencies) { - router.post( - { - path: '/internal/workplace_search/org/sources/{id}/indexing_rules/validate', - validate: { - body: validateRulesSchema, - params: schema.object({ - id: schema.string(), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/ws/org/sources/:id/indexing_rules/validate', - }) - ); -} - -export function registerOrgPreSourceRoute({ - router, - enterpriseSearchRequestHandler, -}: RouteDependencies) { - router.get( - { - path: '/internal/workplace_search/org/pre_sources/{id}', - validate: { - params: schema.object({ - id: schema.string(), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/ws/org/pre_content_sources/:id', - }) - ); -} - -export function registerOrgPrepareSourcesRoute({ - router, - enterpriseSearchRequestHandler, -}: RouteDependencies) { - router.get( - { - path: '/internal/workplace_search/org/sources/{serviceType}/prepare', - validate: { - params: schema.object({ - serviceType: schema.string(), - }), - query: schema.object({ - index_permissions: schema.boolean(), - subdomain: schema.maybe(schema.string()), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/ws/org/sources/:serviceType/prepare', - }) - ); -} - -export function registerOrgSourceSearchableRoute({ - router, - enterpriseSearchRequestHandler, -}: RouteDependencies) { - router.put( - { - path: '/internal/workplace_search/org/sources/{id}/searchable', - validate: { - body: schema.object({ - searchable: schema.boolean(), - }), - params: schema.object({ - id: schema.string(), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/ws/org/sources/:id/searchable', - }) - ); -} - -export function registerOrgSourceDisplaySettingsConfig({ - router, - enterpriseSearchRequestHandler, -}: RouteDependencies) { - router.get( - { - path: '/internal/workplace_search/org/sources/{id}/display_settings/config', - validate: { - params: schema.object({ - id: schema.string(), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/ws/org/sources/:id/display_settings/config', - }) - ); - - router.post( - { - path: '/internal/workplace_search/org/sources/{id}/display_settings/config', - validate: { - body: displaySettingsSchema, - params: schema.object({ - id: schema.string(), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/ws/org/sources/:id/display_settings/config', - }) - ); -} - -export function registerOrgSourceSchemasRoute({ - router, - enterpriseSearchRequestHandler, -}: RouteDependencies) { - router.get( - { - path: '/internal/workplace_search/org/sources/{id}/schemas', - validate: { - params: schema.object({ - id: schema.string(), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/ws/org/sources/:id/schemas', - }) - ); - - router.post( - { - path: '/internal/workplace_search/org/sources/{id}/schemas', - validate: { - body: schemaValuesSchema, - params: schema.object({ - id: schema.string(), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/ws/org/sources/:id/schemas', - }) - ); -} - -export function registerOrgSourceReindexJobRoute({ - router, - enterpriseSearchRequestHandler, -}: RouteDependencies) { - router.get( - { - path: '/internal/workplace_search/org/sources/{sourceId}/reindex_job/{jobId}', - validate: { - params: schema.object({ - sourceId: schema.string(), - jobId: schema.string(), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/ws/org/sources/:sourceId/reindex_job/:jobId', - }) - ); -} - -export function registerOrgSourceDownloadDiagnosticsRoute({ - router, - enterpriseSearchRequestHandler, -}: RouteDependencies) { - router.get( - { - path: '/internal/workplace_search/org/sources/{sourceId}/download_diagnostics', - validate: { - params: schema.object({ - sourceId: schema.string(), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/ws/org/sources/:sourceId/download_diagnostics', - hasJsonResponse: false, - }) - ); -} - -export function registerOrgSourceOauthConfigurationsRoute({ - router, - enterpriseSearchRequestHandler, -}: RouteDependencies) { - router.get( - { - path: '/internal/workplace_search/org/settings/connectors', - validate: false, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/ws/org/settings/connectors', - }) - ); - - router.post( - skipBodyValidation({ - path: '/internal/workplace_search/org/settings/connectors', - validate: {}, - }), - enterpriseSearchRequestHandler.createRequest({ - path: '/ws/org/settings/connectors', - }) - ); - - router.put( - skipBodyValidation({ - path: '/internal/workplace_search/org/settings/connectors', - validate: {}, - }), - enterpriseSearchRequestHandler.createRequest({ - path: '/ws/org/settings/connectors', - }) - ); -} - -export function registerOrgSourceOauthConfigurationRoute({ - router, - enterpriseSearchRequestHandler, -}: RouteDependencies) { - router.get( - { - path: '/internal/workplace_search/org/settings/connectors/{serviceType}', - validate: { - params: schema.object({ - serviceType: schema.string(), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/ws/org/settings/connectors/:serviceType', - }) - ); - - router.post( - skipBodyValidation({ - path: '/internal/workplace_search/org/settings/connectors/{serviceType}', - validate: { - params: schema.object({ - serviceType: schema.string(), - }), - }, - }), - enterpriseSearchRequestHandler.createRequest({ - path: '/ws/org/settings/connectors/:serviceType', - }) - ); - - router.put( - skipBodyValidation({ - path: '/internal/workplace_search/org/settings/connectors/{serviceType}', - validate: { - params: schema.object({ - serviceType: schema.string(), - }), - }, - }), - enterpriseSearchRequestHandler.createRequest({ - path: '/ws/org/settings/connectors/:serviceType', - }) - ); - - router.delete( - { - path: '/internal/workplace_search/org/settings/connectors/{serviceType}', - validate: { - params: schema.object({ - serviceType: schema.string(), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/ws/org/settings/connectors/:serviceType', - }) - ); -} - -export function registerOrgSourceSynchronizeRoute({ - router, - enterpriseSearchRequestHandler, -}: RouteDependencies) { - router.post( - { - path: '/internal/workplace_search/org/sources/{id}/sync', - validate: { - params: schema.object({ - id: schema.string(), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/ws/org/sources/:id/sync', - }) - ); -} - -// Same route is used for org and account. `state` passes the context. -export function registerOauthConnectorParamsRoute({ - router, - enterpriseSearchRequestHandler, -}: RouteDependencies) { - router.get( - { - path: '/internal/workplace_search/sources/create', - validate: { - query: schema.object({ - code: schema.maybe(schema.string()), - session_state: schema.maybe(schema.string()), - authuser: schema.maybe(schema.string()), - prompt: schema.maybe(schema.string()), - hd: schema.maybe(schema.string()), - scope: schema.maybe(schema.string()), - state: schema.string(), - oauth_token: schema.maybe(schema.string()), - oauth_verifier: schema.maybe(schema.string()), - }), - }, - }, - async (context, request, response) => { - return enterpriseSearchRequestHandler.createRequest({ - path: '/ws/sources/create', - params: getOAuthTokenPackageParams(request.headers.cookie), - })(context, request, response); - } - ); -} - -export const registerSourcesRoutes = (dependencies: RouteDependencies) => { - registerAccountSourcesRoute(dependencies); - registerAccountSourcesStatusRoute(dependencies); - registerAccountSourceRoute(dependencies); - registerAccountCreateSourceRoute(dependencies); - registerAccountSourceDocumentsRoute(dependencies); - registerAccountSourceFederatedSummaryRoute(dependencies); - registerAccountSourceReauthPrepareRoute(dependencies); - registerAccountSourceSettingsRoute(dependencies); - registerAccountSourceValidateIndexingRulesRoute(dependencies); - registerAccountPreSourceRoute(dependencies); - registerAccountPrepareSourcesRoute(dependencies); - registerAccountSourceSearchableRoute(dependencies); - registerAccountSourceDisplaySettingsConfig(dependencies); - registerAccountSourceSchemasRoute(dependencies); - registerAccountSourceReindexJobRoute(dependencies); - registerAccountSourceDownloadDiagnosticsRoute(dependencies); - registerOrgSourcesRoute(dependencies); - registerOrgSourcesStatusRoute(dependencies); - registerOrgSourceRoute(dependencies); - registerOrgCreateSourceRoute(dependencies); - registerOrgSourceDocumentsRoute(dependencies); - registerOrgSourceFederatedSummaryRoute(dependencies); - registerOrgSourceReauthPrepareRoute(dependencies); - registerOrgSourceSettingsRoute(dependencies); - registerOrgSourceValidateIndexingRulesRoute(dependencies); - registerOrgPreSourceRoute(dependencies); - registerOrgPrepareSourcesRoute(dependencies); - registerOrgSourceSearchableRoute(dependencies); - registerOrgSourceDisplaySettingsConfig(dependencies); - registerOrgSourceSchemasRoute(dependencies); - registerOrgSourceReindexJobRoute(dependencies); - registerOrgSourceDownloadDiagnosticsRoute(dependencies); - registerOrgSourceOauthConfigurationsRoute(dependencies); - registerOrgSourceOauthConfigurationRoute(dependencies); - registerOrgSourceSynchronizeRoute(dependencies); - registerOauthConnectorParamsRoute(dependencies); -}; From 54436e3c1c1330e4c3dfa4ab976d706ef5891608 Mon Sep 17 00:00:00 2001 From: Saarika Bhasi <55930906+saarikabhasi@users.noreply.github.com> Date: Fri, 10 Jan 2025 10:30:38 -0500 Subject: [PATCH 26/42] [Onboarding] Introduce search index details page locator and update all reference (#205005) ## Summary * Introducing new locator for onboarding `search_indices` plugin index details page - `SEARCH_INDEX_DETAILS_LOCATOR_ID`. * In stack, Updated view index details usage(connector table, connector details page, search application & web crawler) to use this locator to navigate to onboarding index details page ONLY when its `search - Elasticsearch solution nav` * Index management view index details would use extensionService with active solution id check in search_indices plugin * verified locally existing FTR & unit tests * Added FTR for index management in functional_search tests for search solution nav and classic nav https://github.com/user-attachments/assets/8f0fea00-3dce-449e-805a-b3cf317f4066 ### Checklist Check the PR satisfies following conditions. Reviewers should verify this PR satisfies this list as well. - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [x] [Flaky Test Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was used on any tests changed --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Elastic Machine --- .../search_application_indices.tsx | 47 +- .../search_application_view_index_link.tsx | 36 ++ .../search_application_indices_flyout.tsx | 22 +- .../components/generated_config_fields.tsx | 14 +- .../connectors/connectors_table.tsx | 19 +- .../connector_view_search_indices_details.tsx | 36 ++ .../plugins/search_indices/common/routes.ts | 1 + .../public/hooks/api/use_document_search.ts | 2 +- .../create_index_locator.ts} | 11 +- .../search_indices/public/locators/index.ts | 15 + .../public/locators/search_indices_locator.ts | 35 ++ .../plugins/search_indices/public/plugin.ts | 32 +- .../search_indices/server/routes/indices.ts | 51 ++- x-pack/test/functional/page_objects/index.ts | 6 + .../page_objects/search_api_keys.ts | 128 ++++++ .../page_objects/search_index_details_page.ts | 308 +++++++++++++ .../page_objects/search_navigation.ts | 53 +++ x-pack/test/functional_search/config.ts | 5 +- x-pack/test/functional_search/index.ts | 1 + .../tests/embedded_console.ts | 20 + .../tests/search_index_details.ts | 405 ++++++++++++++++++ 21 files changed, 1170 insertions(+), 77 deletions(-) create mode 100644 x-pack/solutions/search/plugins/enterprise_search/public/applications/applications/components/search_application/search_application_view_index_link.tsx create mode 100644 x-pack/solutions/search/plugins/enterprise_search/public/applications/enterprise_search_content/components/shared/connector_view_search_indices_details/connector_view_search_indices_details.tsx rename x-pack/solutions/search/plugins/search_indices/public/{locators.ts => locators/create_index_locator.ts} (59%) create mode 100644 x-pack/solutions/search/plugins/search_indices/public/locators/index.ts create mode 100644 x-pack/solutions/search/plugins/search_indices/public/locators/search_indices_locator.ts create mode 100644 x-pack/test/functional/page_objects/search_api_keys.ts create mode 100644 x-pack/test/functional/page_objects/search_index_details_page.ts create mode 100644 x-pack/test/functional/page_objects/search_navigation.ts create mode 100644 x-pack/test/functional_search/tests/embedded_console.ts create mode 100644 x-pack/test/functional_search/tests/search_index_details.ts diff --git a/x-pack/solutions/search/plugins/enterprise_search/public/applications/applications/components/search_application/search_application_indices.tsx b/x-pack/solutions/search/plugins/enterprise_search/public/applications/applications/components/search_application/search_application_indices.tsx index 7cd9d3bae3d3e..e59f95cc19d99 100644 --- a/x-pack/solutions/search/plugins/enterprise_search/public/applications/applications/components/search_application/search_application_indices.tsx +++ b/x-pack/solutions/search/plugins/enterprise_search/public/applications/applications/components/search_application/search_application_indices.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useState } from 'react'; +import React, { useMemo, useState } from 'react'; import { useActions, useValues } from 'kea'; @@ -22,26 +22,27 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { ENTERPRISE_SEARCH_CONTENT_PLUGIN } from '../../../../../common/constants'; import { EnterpriseSearchApplicationIndex } from '../../../../../common/types/search_applications'; -import { SEARCH_INDEX_PATH } from '../../../enterprise_search_content/routes'; import { CANCEL_BUTTON_LABEL } from '../../../shared/constants'; import { indexHealthToHealthColor } from '../../../shared/constants/health_colors'; -import { generateEncodedPath } from '../../../shared/encode_path_params'; import { KibanaLogic } from '../../../shared/kibana'; -import { EuiLinkTo } from '../../../shared/react_router_helpers'; import { TelemetryLogic } from '../../../shared/telemetry/telemetry_logic'; import { SearchApplicationIndicesLogic } from './search_application_indices_logic'; +import { SearchApplicationViewIndexLink } from './search_application_view_index_link'; export const SearchApplicationIndices: React.FC = () => { const subduedBackground = useEuiBackgroundColor('subdued'); const { sendEnterpriseSearchTelemetry } = useActions(TelemetryLogic); const { searchApplicationData } = useValues(SearchApplicationIndicesLogic); const { removeIndexFromSearchApplication } = useActions(SearchApplicationIndicesLogic); - const { navigateToUrl } = useValues(KibanaLogic); + const { navigateToUrl, share } = useValues(KibanaLogic); const [removeIndexConfirm, setConfirmRemoveIndex] = useState(null); + const searchIndicesLocator = useMemo( + () => share?.url.locators.get('SEARCH_INDEX_DETAILS_LOCATOR_ID'), + [share] + ); if (!searchApplicationData) return null; const { indices } = searchApplicationData; @@ -94,15 +95,10 @@ export const SearchApplicationIndices: React.FC = () => { health === 'unknown' ? ( name ) : ( - - {name} - + ), sortable: ({ name }: EnterpriseSearchApplicationIndex) => name, truncateText: true, @@ -148,6 +144,7 @@ export const SearchApplicationIndices: React.FC = () => { { actions: [ { + enabled: () => searchIndicesLocator !== undefined, available: (index) => index.health !== 'unknown', 'data-test-subj': 'search-application-view-index-btn', description: i18n.translate( @@ -168,15 +165,19 @@ export const SearchApplicationIndices: React.FC = () => { }, } ), - onClick: (index) => - navigateToUrl( - `${ENTERPRISE_SEARCH_CONTENT_PLUGIN.URL}/${generateEncodedPath(SEARCH_INDEX_PATH, { - indexName: index.name, - })}`, - { + + onClick: async (index) => { + if (searchIndicesLocator) { + const url = await searchIndicesLocator.getUrl({ indexName: index.name }); + navigateToUrl(url, { shouldNotCreateHref: true, - } - ), + shouldNotPrepend: true, + }); + } else { + return undefined; + } + }, + type: 'icon', }, ...(indices.length > 1 ? [removeIndexAction] : []), diff --git a/x-pack/solutions/search/plugins/enterprise_search/public/applications/applications/components/search_application/search_application_view_index_link.tsx b/x-pack/solutions/search/plugins/enterprise_search/public/applications/applications/components/search_application/search_application_view_index_link.tsx new file mode 100644 index 0000000000000..47b454c43cc24 --- /dev/null +++ b/x-pack/solutions/search/plugins/enterprise_search/public/applications/applications/components/search_application/search_application_view_index_link.tsx @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React from 'react'; + +import { useValues } from 'kea'; + +import { EuiLink } from '@elastic/eui'; + +import { KibanaLogic } from '../../../shared/kibana'; + +export const SearchApplicationViewIndexLink: React.FC<{ + indexName: string; + dataTestSubj?: string; + dataTelemetryId?: string; +}> = ({ indexName, dataTestSubj, dataTelemetryId }) => { + const { share } = useValues(KibanaLogic); + + const searchIndexDetailsUrl: string = + share?.url.locators.get('SEARCH_INDEX_DETAILS_LOCATOR_ID')?.useUrl({ indexName }) ?? ''; + + return searchIndexDetailsUrl ? ( + + {indexName} + + ) : ( + <>{indexName} + ); +}; diff --git a/x-pack/solutions/search/plugins/enterprise_search/public/applications/applications/components/search_applications/search_application_indices_flyout.tsx b/x-pack/solutions/search/plugins/enterprise_search/public/applications/applications/components/search_applications/search_application_indices_flyout.tsx index 78f20bc122ca8..dc06b8f69c396 100644 --- a/x-pack/solutions/search/plugins/enterprise_search/public/applications/applications/components/search_applications/search_application_indices_flyout.tsx +++ b/x-pack/solutions/search/plugins/enterprise_search/public/applications/applications/components/search_applications/search_application_indices_flyout.tsx @@ -25,14 +25,11 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; -import { ENTERPRISE_SEARCH_CONTENT_PLUGIN } from '../../../../../common/constants'; - import { EnterpriseSearchApplicationIndex } from '../../../../../common/types/search_applications'; -import { SEARCH_INDEX_PATH } from '../../../enterprise_search_content/routes'; import { healthColorsMap } from '../../../shared/constants/health_colors'; -import { generateEncodedPath } from '../../../shared/encode_path_params'; -import { EuiLinkTo } from '../../../shared/react_router_helpers'; + +import { SearchApplicationViewIndexLink } from '../search_application/search_application_view_index_link'; import { SearchApplicationIndicesFlyoutLogic } from './search_application_indices_flyout_logic'; @@ -58,16 +55,11 @@ export const SearchApplicationIndicesFlyout: React.FC = () => { } ), render: (indexName: string) => ( - - {indexName} - + ), sortable: true, truncateText: true, diff --git a/x-pack/solutions/search/plugins/enterprise_search/public/applications/enterprise_search_content/components/connector_detail/components/generated_config_fields.tsx b/x-pack/solutions/search/plugins/enterprise_search/public/applications/enterprise_search_content/components/connector_detail/components/generated_config_fields.tsx index a905242c90fec..09c0a8224238a 100644 --- a/x-pack/solutions/search/plugins/enterprise_search/public/applications/enterprise_search_content/components/connector_detail/components/generated_config_fields.tsx +++ b/x-pack/solutions/search/plugins/enterprise_search/public/applications/enterprise_search_content/components/connector_detail/components/generated_config_fields.tsx @@ -31,7 +31,8 @@ import { generateEncodedPath } from '../../../../shared/encode_path_params'; import { EuiLinkTo } from '../../../../shared/react_router_helpers'; import { ApiKey } from '../../../api/connector/generate_connector_api_key_api_logic'; -import { CONNECTOR_DETAIL_PATH, SEARCH_INDEX_PATH } from '../../../routes'; +import { CONNECTOR_DETAIL_PATH } from '../../../routes'; +import { ConnectorViewIndexLink } from '../../shared/connector_view_search_indices_details/connector_view_search_indices_details'; export interface GeneratedConfigFieldsProps { apiKey?: ApiKey; @@ -84,7 +85,6 @@ export const GeneratedConfigFields: React.FC = ({ isGenerateLoading, }) => { const [isModalVisible, setIsModalVisible] = useState(false); - const refreshButtonClick = () => { setIsModalVisible(true); }; @@ -184,15 +184,7 @@ export const GeneratedConfigFields: React.FC = ({
{connector.index_name && ( - - {connector.index_name} - + )} diff --git a/x-pack/solutions/search/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/connectors_table.tsx b/x-pack/solutions/search/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/connectors_table.tsx index 4f7937a0f1c76..d44eebff78cf1 100644 --- a/x-pack/solutions/search/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/connectors_table.tsx +++ b/x-pack/solutions/search/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/connectors_table.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React from 'react'; +import React, { useMemo } from 'react'; import { useValues } from 'kea'; @@ -32,6 +32,8 @@ import { connectorStatusToText, } from '../../utils/connector_status_helpers'; +import { ConnectorViewIndexLink } from '../shared/connector_view_search_indices_details/connector_view_search_indices_details'; + import { ConnectorType } from './connector_type'; import { ConnectorViewItem } from './connectors_logic'; @@ -57,7 +59,12 @@ export const ConnectorsTable: React.FC = ({ isLoading, onDelete, }) => { - const { navigateToUrl } = useValues(KibanaLogic); + const { navigateToUrl, share } = useValues(KibanaLogic); + const searchIndicesLocator = useMemo( + () => share?.url.locators.get('SEARCH_INDEX_DETAILS_LOCATOR_ID'), + [share] + ); + const columns: Array> = [ ...(!isCrawler ? [ @@ -88,12 +95,8 @@ export const ConnectorsTable: React.FC = ({ ), render: (connector: ConnectorViewItem) => connector.index_name ? ( - connector.indexExists ? ( - - {connector.index_name} - + connector.indexExists && searchIndicesLocator ? ( + ) : ( connector.index_name ) diff --git a/x-pack/solutions/search/plugins/enterprise_search/public/applications/enterprise_search_content/components/shared/connector_view_search_indices_details/connector_view_search_indices_details.tsx b/x-pack/solutions/search/plugins/enterprise_search/public/applications/enterprise_search_content/components/shared/connector_view_search_indices_details/connector_view_search_indices_details.tsx new file mode 100644 index 0000000000000..a21d8a8408811 --- /dev/null +++ b/x-pack/solutions/search/plugins/enterprise_search/public/applications/enterprise_search_content/components/shared/connector_view_search_indices_details/connector_view_search_indices_details.tsx @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React from 'react'; + +import { useValues } from 'kea'; + +import { EuiLink } from '@elastic/eui'; + +import { KibanaLogic } from '../../../../shared/kibana'; + +export const ConnectorViewIndexLink: React.FC<{ + indexName: string; + target?: boolean; +}> = ({ indexName, target }) => { + const { share } = useValues(KibanaLogic); + + const searchIndexDetailsUrl = share?.url.locators + .get('SEARCH_INDEX_DETAILS_LOCATOR_ID') + ?.useUrl({ indexName }); + + return searchIndexDetailsUrl ? ( + + {indexName} + + ) : ( + <>{indexName} + ); +}; diff --git a/x-pack/solutions/search/plugins/search_indices/common/routes.ts b/x-pack/solutions/search/plugins/search_indices/common/routes.ts index f527fa676e2a0..b6f27dc89d391 100644 --- a/x-pack/solutions/search/plugins/search_indices/common/routes.ts +++ b/x-pack/solutions/search/plugins/search_indices/common/routes.ts @@ -11,3 +11,4 @@ export const GET_USER_PRIVILEGES_ROUTE = '/internal/search_indices/start_privile export const POST_CREATE_INDEX_ROUTE = '/internal/search_indices/indices/create'; export const INDEX_DOCUMENT_ROUTE = '/internal/search_indices/{indexName}/documents/{id}'; +export const SEARCH_DOCUMENTS_ROUTE = '/internal/search_indices/{indexName}/documents/search'; diff --git a/x-pack/solutions/search/plugins/search_indices/public/hooks/api/use_document_search.ts b/x-pack/solutions/search/plugins/search_indices/public/hooks/api/use_document_search.ts index d566b90916892..a8afd69385623 100644 --- a/x-pack/solutions/search/plugins/search_indices/public/hooks/api/use_document_search.ts +++ b/x-pack/solutions/search/plugins/search_indices/public/hooks/api/use_document_search.ts @@ -33,7 +33,7 @@ export const useIndexDocumentSearch = (indexName: string) => { refetchIntervalInBackground: true, refetchOnWindowFocus: 'always', queryFn: async ({ signal }) => - http.post(`/internal/serverless_search/indices/${indexName}/search`, { + http.post(`/internal/search_indices/${indexName}/documents/search`, { body: JSON.stringify({ searchQuery: '', trackTotalHits: true, diff --git a/x-pack/solutions/search/plugins/search_indices/public/locators.ts b/x-pack/solutions/search/plugins/search_indices/public/locators/create_index_locator.ts similarity index 59% rename from x-pack/solutions/search/plugins/search_indices/public/locators.ts rename to x-pack/solutions/search/plugins/search_indices/public/locators/create_index_locator.ts index 587ce51f2c82d..7b16d06102dbe 100644 --- a/x-pack/solutions/search/plugins/search_indices/public/locators.ts +++ b/x-pack/solutions/search/plugins/search_indices/public/locators/create_index_locator.ts @@ -6,17 +6,12 @@ */ import type { LocatorDefinition } from '@kbn/share-plugin/common'; -import type { SharePluginSetup } from '@kbn/share-plugin/public'; import type { SerializableRecord } from '@kbn/utility-types'; -import { INDICES_APP_ID } from '../common'; -import { CREATE_INDEX_PATH } from './routes'; +import { INDICES_APP_ID } from '../../common'; +import { CREATE_INDEX_PATH } from '../routes'; -export function registerLocators(share: SharePluginSetup) { - share.url.locators.create(new CreateIndexLocatorDefinition()); -} - -class CreateIndexLocatorDefinition implements LocatorDefinition { +export class CreateIndexLocatorDefinition implements LocatorDefinition { public readonly getLocation = async () => { return { app: INDICES_APP_ID, diff --git a/x-pack/solutions/search/plugins/search_indices/public/locators/index.ts b/x-pack/solutions/search/plugins/search_indices/public/locators/index.ts new file mode 100644 index 0000000000000..d0959eec9255d --- /dev/null +++ b/x-pack/solutions/search/plugins/search_indices/public/locators/index.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { SharePluginSetup } from '@kbn/share-plugin/public'; +import { SerializableRecord } from '@kbn/utility-types'; +import { CreateIndexLocatorDefinition } from './create_index_locator'; +import { SearchIndicesLocatorDefinition } from './search_indices_locator'; + +export function registerLocators(share: SharePluginSetup) { + share.url.locators.create(new CreateIndexLocatorDefinition()); + share.url.locators.create(new SearchIndicesLocatorDefinition()); +} diff --git a/x-pack/solutions/search/plugins/search_indices/public/locators/search_indices_locator.ts b/x-pack/solutions/search/plugins/search_indices/public/locators/search_indices_locator.ts new file mode 100644 index 0000000000000..f0439fe26385d --- /dev/null +++ b/x-pack/solutions/search/plugins/search_indices/public/locators/search_indices_locator.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { LocatorDefinition } from '@kbn/share-plugin/common'; +import type { SerializableRecord } from '@kbn/utility-types'; + +import { INDICES_APP_ID } from '../../common'; +import { SearchIndexDetailsTabValues } from '../routes'; + +export interface SearchIndicesLocatorParams extends SerializableRecord { + indexName: string; + detailsTabId: string; +} + +export class SearchIndicesLocatorDefinition + implements LocatorDefinition +{ + public readonly getLocation = async (params: SearchIndicesLocatorParams) => { + const path = `/index_details/${params.indexName}`; + + return { + app: INDICES_APP_ID, + path: SearchIndexDetailsTabValues.includes(params.detailsTabId) + ? `${path}/${params.detailsTabId}` + : path, + state: {}, + }; + }; + + public readonly id = 'SEARCH_INDEX_DETAILS_LOCATOR_ID'; +} diff --git a/x-pack/solutions/search/plugins/search_indices/public/plugin.ts b/x-pack/solutions/search/plugins/search_indices/public/plugin.ts index b92fbaa5e7f45..3e835d0cc2d00 100644 --- a/x-pack/solutions/search/plugins/search_indices/public/plugin.ts +++ b/x-pack/solutions/search/plugins/search_indices/public/plugin.ts @@ -9,6 +9,7 @@ import type { CoreSetup, CoreStart, Plugin } from '@kbn/core/public'; import { SEARCH_INDICES_CREATE_INDEX } from '@kbn/deeplinks-search/constants'; import { i18n } from '@kbn/i18n'; +import { Subscription } from 'rxjs'; import { docLinks } from '../common/doc_links'; import type { AppPluginSetupDependencies, @@ -31,6 +32,7 @@ export class SearchIndicesPlugin implements Plugin { private pluginEnabled: boolean = false; + private activeSolutionIdSubscription: Subscription | undefined; public setup( core: CoreSetup, @@ -99,16 +101,23 @@ export class SearchIndicesPlugin ): SearchIndicesPluginStart { const { indexManagement } = deps; docLinks.setDocLinks(core.docLinks.links); + if (this.pluginEnabled) { - indexManagement?.extensionsService.setIndexDetailsPageRoute({ - renderRoute: (indexName, detailsTabId) => { - const route = `/app/elasticsearch/indices/index_details/${indexName}`; - if (detailsTabId && SearchIndexDetailsTabValues.includes(detailsTabId)) { - return `${route}/${detailsTabId}`; + this.activeSolutionIdSubscription = core.chrome + .getActiveSolutionNavId$() + .subscribe((activeSolutionId) => { + if (activeSolutionId === 'es') { + indexManagement?.extensionsService.setIndexDetailsPageRoute({ + renderRoute: (indexName, detailsTabId) => { + const route = `/app/elasticsearch/indices/index_details/${indexName}`; + if (detailsTabId && SearchIndexDetailsTabValues.includes(detailsTabId)) { + return `${route}/${detailsTabId}`; + } + return route; + }, + }); } - return route; - }, - }); + }); } return { enabled: this.pluginEnabled, @@ -117,5 +126,10 @@ export class SearchIndicesPlugin }; } - public stop() {} + public stop() { + if (this.activeSolutionIdSubscription) { + this.activeSolutionIdSubscription.unsubscribe(); + this.activeSolutionIdSubscription = undefined; + } + } } diff --git a/x-pack/solutions/search/plugins/search_indices/server/routes/indices.ts b/x-pack/solutions/search/plugins/search_indices/server/routes/indices.ts index cd42470d3c3fc..c726f2fc62975 100644 --- a/x-pack/solutions/search/plugins/search_indices/server/routes/indices.ts +++ b/x-pack/solutions/search/plugins/search_indices/server/routes/indices.ts @@ -10,7 +10,9 @@ import { i18n } from '@kbn/i18n'; import type { IRouter } from '@kbn/core/server'; import type { Logger } from '@kbn/logging'; -import { POST_CREATE_INDEX_ROUTE } from '../../common/routes'; +import { fetchSearchResults } from '@kbn/search-index-documents/lib'; +import { DEFAULT_DOCS_PER_PAGE } from '@kbn/search-index-documents/types'; +import { POST_CREATE_INDEX_ROUTE, SEARCH_DOCUMENTS_ROUTE } from '../../common/routes'; import { CreateIndexRequest } from '../../common/types'; import { createIndex } from '../lib/indices'; @@ -62,4 +64,51 @@ export function registerIndicesRoutes(router: IRouter, logger: Logger) { } } ); + router.post( + { + path: SEARCH_DOCUMENTS_ROUTE, + validate: { + body: schema.object({ + searchQuery: schema.string({ + defaultValue: '', + }), + trackTotalHits: schema.boolean({ defaultValue: false }), + }), + params: schema.object({ + indexName: schema.string(), + }), + query: schema.object({ + page: schema.number({ defaultValue: 0, min: 0 }), + size: schema.number({ + defaultValue: DEFAULT_DOCS_PER_PAGE, + min: 0, + }), + }), + }, + }, + async (context, request, response) => { + const client = (await context.core).elasticsearch.client.asCurrentUser; + const indexName = decodeURIComponent(request.params.indexName); + const searchQuery = request.body.searchQuery; + const { page = 0, size = DEFAULT_DOCS_PER_PAGE } = request.query; + const from = page * size; + const trackTotalHits = request.body.trackTotalHits; + + const searchResults = await fetchSearchResults( + client, + indexName, + searchQuery, + from, + size, + trackTotalHits + ); + + return response.ok({ + body: { + results: searchResults, + }, + headers: { 'content-type': 'application/json' }, + }); + } + ); } diff --git a/x-pack/test/functional/page_objects/index.ts b/x-pack/test/functional/page_objects/index.ts index 0d270661a05df..8cbc0d6d9f425 100644 --- a/x-pack/test/functional/page_objects/index.ts +++ b/x-pack/test/functional/page_objects/index.ts @@ -55,6 +55,9 @@ import { WatcherPageObject } from './watcher_page'; import { SearchProfilerPageProvider } from './search_profiler_page'; import { SearchPlaygroundPageProvider } from './search_playground_page'; import { SearchClassicNavigationProvider } from './search_classic_navigation'; +import { SearchApiKeysProvider } from './search_api_keys'; +import { SearchIndexDetailPageProvider } from './search_index_details_page'; +import { SearchNavigationProvider } from './search_navigation'; // just like services, PageObjects are defined as a map of // names to Providers. Merge in Kibana's or pick specific ones @@ -94,7 +97,10 @@ export const pageObjects = { reporting: ReportingPageObject, roleMappings: RoleMappingsPageProvider, rollup: RollupPageObject, + searchApiKeys: SearchApiKeysProvider, searchClassicNavigation: SearchClassicNavigationProvider, + searchIndexDetailsPage: SearchIndexDetailPageProvider, + searchNavigation: SearchNavigationProvider, searchProfiler: SearchProfilerPageProvider, searchPlayground: SearchPlaygroundPageProvider, searchSessionsManagement: SearchSessionsPageProvider, diff --git a/x-pack/test/functional/page_objects/search_api_keys.ts b/x-pack/test/functional/page_objects/search_api_keys.ts new file mode 100644 index 0000000000000..0ed836b9ab3d2 --- /dev/null +++ b/x-pack/test/functional/page_objects/search_api_keys.ts @@ -0,0 +1,128 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { SecurityApiKey } from '@elastic/elasticsearch/lib/api/types'; +import { FtrProviderContext } from '../ftr_provider_context'; + +const APIKEY_MASK = '•'.repeat(60); + +export function SearchApiKeysProvider({ getService, getPageObjects }: FtrProviderContext) { + const testSubjects = getService('testSubjects'); + const browser = getService('browser'); + const pageObjects = getPageObjects(['common', 'apiKeys']); + const retry = getService('retry'); + const es = getService('es'); + + const getAPIKeyFromSessionStorage = async () => { + const sessionStorageKey = await browser.getSessionStorageItem('searchApiKey'); + return sessionStorageKey && JSON.parse(sessionStorageKey); + }; + + return { + async clearAPIKeySessionStorage() { + await browser.clearSessionStorage(); + }, + + async expectAPIKeyExists() { + await testSubjects.existOrFail('apiKeyFormAPIKey', { timeout: 1000 }); + }, + + async expectAPIKeyAvailable() { + await testSubjects.existOrFail('apiKeyFormAPIKey'); + await retry.try(async () => { + expect(await testSubjects.getVisibleText('apiKeyFormAPIKey')).to.be(APIKEY_MASK); + }); + await testSubjects.click('showAPIKeyButton'); + let apiKey; + await retry.try(async () => { + apiKey = await testSubjects.getVisibleText('apiKeyFormAPIKey'); + expect(apiKey).to.be.a('string'); + expect(apiKey.length).to.be(60); + expect(apiKey).to.not.be(APIKEY_MASK); + }); + const sessionStorageKey = await getAPIKeyFromSessionStorage(); + expect(sessionStorageKey.encoded).to.eql(apiKey); + }, + + async expectAPIKeyNoPrivileges() { + await testSubjects.existOrFail('apiKeyFormNoUserPrivileges'); + }, + + async getAPIKeyFromSessionStorage() { + return getAPIKeyFromSessionStorage(); + }, + + async getAPIKeyFromUI() { + let apiKey = ''; + await retry.try(async () => { + apiKey = await testSubjects.getVisibleText('apiKeyFormAPIKey'); + expect(apiKey).to.not.be(APIKEY_MASK); + }); + expect(apiKey).to.be.a('string'); + return apiKey; + }, + + async invalidateAPIKey(apiKeyId: string) { + await es.security.invalidateApiKey({ ids: [apiKeyId] }); + }, + + async createAPIKey() { + await es.security.createApiKey({ + name: 'test-api-key', + role_descriptors: {}, + }); + }, + + async expectAPIKeyCreate() { + await testSubjects.existOrFail('apiKeyFormAPIKey'); + await retry.try(async () => { + expect(await testSubjects.getVisibleText('apiKeyFormAPIKey')).to.be(APIKEY_MASK); + }); + await testSubjects.click('showAPIKeyButton'); + await retry.try(async () => { + const apiKey = await testSubjects.getVisibleText('apiKeyFormAPIKey'); + expect(apiKey).to.be.a('string'); + expect(apiKey.length).to.be(60); + expect(apiKey).to.not.be(APIKEY_MASK); + }); + }, + + async deleteAPIKeys() { + const filterInvalid = (key: SecurityApiKey) => !key.invalidated; + + const { api_keys: apiKeys } = await es.security.getApiKey(); + + const validKeys = apiKeys.filter(filterInvalid); + + if (validKeys.length === 0) { + return; + } + + await es.security.invalidateApiKey({ + ids: validKeys.map((key) => key.id), + }); + }, + + async expectCreateApiKeyAction() { + await testSubjects.existOrFail('createAPIKeyButton'); + }, + + async createApiKeyFromFlyout() { + const apiKeyName = 'Happy API Key'; + await testSubjects.click('createAPIKeyButton'); + expect(await pageObjects.apiKeys.getFlyoutTitleText()).to.be('Create API key'); + + await pageObjects.apiKeys.setApiKeyName(apiKeyName); + await pageObjects.apiKeys.clickSubmitButtonOnApiKeyFlyout(); + }, + + async expectAPIKeyNotAvailable() { + await testSubjects.missingOrFail('apiKeyFormAPIKey'); + }, + }; +} diff --git a/x-pack/test/functional/page_objects/search_index_details_page.ts b/x-pack/test/functional/page_objects/search_index_details_page.ts new file mode 100644 index 0000000000000..45d7175b6e445 --- /dev/null +++ b/x-pack/test/functional/page_objects/search_index_details_page.ts @@ -0,0 +1,308 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../ftr_provider_context'; + +export function SearchIndexDetailPageProvider({ getService }: FtrProviderContext) { + const testSubjects = getService('testSubjects'); + const browser = getService('browser'); + const retry = getService('retry'); + + return { + async expectIndexDetailPageHeader() { + await testSubjects.existOrFail('searchIndexDetailsHeader', { timeout: 2000 }); + }, + async expectAPIReferenceDocLinkExists() { + await testSubjects.existOrFail('ApiReferenceDoc', { timeout: 2000 }); + }, + async expectActionItemReplacedWhenHasDocs() { + await testSubjects.missingOrFail('ApiReferenceDoc', { timeout: 2000 }); + await testSubjects.existOrFail('useInPlaygroundLink', { timeout: 5000 }); + await testSubjects.existOrFail('viewInDiscoverLink', { timeout: 5000 }); + }, + async expectConnectionDetails() { + await testSubjects.existOrFail('connectionDetailsEndpoint', { timeout: 2000 }); + expect(await (await testSubjects.find('connectionDetailsEndpoint')).getVisibleText()).match( + /^https?\:\/\/.*(\:\d+)?/ + ); + }, + async expectQuickStats() { + await testSubjects.existOrFail('quickStats', { timeout: 2000 }); + const quickStatsElem = await testSubjects.find('quickStats'); + const quickStatsDocumentElem = await quickStatsElem.findByTestSubject( + 'QuickStatsDocumentCount' + ); + expect(await quickStatsDocumentElem.getVisibleText()).to.contain('Document count\n0'); + expect(await quickStatsDocumentElem.getVisibleText()).not.to.contain('Index Size\n0b'); + await quickStatsDocumentElem.click(); + expect(await quickStatsDocumentElem.getVisibleText()).to.contain('Index Size\n227b'); + }, + + async expectQuickStatsToHaveDocumentCount(count: number) { + const quickStatsElem = await testSubjects.find('quickStats'); + const quickStatsDocumentElem = await quickStatsElem.findByTestSubject( + 'QuickStatsDocumentCount' + ); + expect(await quickStatsDocumentElem.getVisibleText()).to.contain(`Document count\n${count}`); + }, + + async expectQuickStatsAIMappings() { + await testSubjects.existOrFail('quickStats', { timeout: 2000 }); + const quickStatsElem = await testSubjects.find('quickStats'); + const quickStatsAIMappingsElem = await quickStatsElem.findByTestSubject( + 'QuickStatsAIMappings' + ); + await quickStatsAIMappingsElem.click(); + await testSubjects.existOrFail('setupAISearchButton', { timeout: 2000 }); + }, + + async expectQuickStatsAIMappingsToHaveVectorFields() { + const quickStatsDocumentElem = await testSubjects.find('QuickStatsAIMappings'); + await quickStatsDocumentElem.click(); + expect(await quickStatsDocumentElem.getVisibleText()).to.contain('AI Search\n1 Field'); + await testSubjects.missingOrFail('setupAISearchButton', { timeout: 2000 }); + }, + + async expectAddDocumentCodeExamples() { + await testSubjects.existOrFail('SearchIndicesAddDocumentsCode', { timeout: 2000 }); + }, + + async expectHasIndexDocuments() { + await retry.try(async () => { + await testSubjects.existOrFail('search-index-documents-result', { timeout: 2000 }); + }); + }, + + async expectMoreOptionsActionButtonExists() { + await testSubjects.existOrFail('moreOptionsActionButton'); + }, + async clickMoreOptionsActionsButton() { + await testSubjects.click('moreOptionsActionButton'); + }, + async expectMoreOptionsOverviewMenuIsShown() { + await testSubjects.existOrFail('moreOptionsContextMenu'); + }, + async expectToNavigateToPlayground(indexName: string) { + await testSubjects.click('moreOptionsPlayground'); + expect(await browser.getCurrentUrl()).contain( + `/search_playground/chat?default-index=${indexName}` + ); + await testSubjects.existOrFail('chatPage'); + }, + async expectAPIReferenceDocLinkExistsInMoreOptions() { + await testSubjects.existOrFail('moreOptionsApiReference', { timeout: 2000 }); + }, + async expectAPIReferenceDocLinkMissingInMoreOptions() { + await testSubjects.missingOrFail('moreOptionsApiReference', { timeout: 2000 }); + }, + async expectDeleteIndexButtonToBeDisabled() { + await testSubjects.existOrFail('moreOptionsDeleteIndex'); + const deleteIndexButton = await testSubjects.isEnabled('moreOptionsDeleteIndex'); + expect(deleteIndexButton).to.be(false); + await testSubjects.moveMouseTo('moreOptionsDeleteIndex'); + await testSubjects.existOrFail('moreOptionsDeleteIndexTooltip'); + }, + async expectDeleteIndexButtonToBeEnabled() { + await testSubjects.existOrFail('moreOptionsDeleteIndex'); + const deleteIndexButton = await testSubjects.isEnabled('moreOptionsDeleteIndex'); + expect(deleteIndexButton).to.be(true); + }, + async expectDeleteIndexButtonExistsInMoreOptions() { + await testSubjects.existOrFail('moreOptionsDeleteIndex'); + }, + async clickDeleteIndexButton() { + await testSubjects.click('moreOptionsDeleteIndex'); + }, + async expectDeleteIndexModalExists() { + await testSubjects.existOrFail('deleteIndexActionModal'); + }, + async clickConfirmingDeleteIndex() { + await testSubjects.existOrFail('confirmModalConfirmButton'); + await testSubjects.click('confirmModalConfirmButton'); + }, + async expectPageLoadErrorExists() { + await retry.tryForTime(60 * 1000, async () => { + await testSubjects.existOrFail('pageLoadError'); + }); + + await testSubjects.existOrFail('loadingErrorBackToIndicesButton'); + await testSubjects.existOrFail('reloadButton'); + }, + async expectIndexNotFoundErrorExists() { + const pageLoadErrorElement = await ( + await testSubjects.find('pageLoadError') + ).findByClassName('euiTitle'); + expect(await pageLoadErrorElement.getVisibleText()).to.contain('Not Found'); + }, + async clickPageReload() { + await retry.tryForTime(60 * 1000, async () => { + await testSubjects.click('reloadButton', 2000); + }); + }, + async expectTabsExists() { + await testSubjects.existOrFail('mappingsTab', { timeout: 2000 }); + await testSubjects.existOrFail('dataTab', { timeout: 2000 }); + }, + async changeTab(tab: 'dataTab' | 'mappingsTab' | 'settingsTab') { + await testSubjects.click(tab); + }, + async expectUrlShouldChangeTo(tab: 'data' | 'mappings' | 'settings') { + expect(await browser.getCurrentUrl()).contain(`/${tab}`); + }, + async expectMappingsComponentIsVisible() { + await testSubjects.existOrFail('indexDetailsMappingsToggleViewButton', { timeout: 2000 }); + }, + async expectSettingsComponentIsVisible() { + await testSubjects.existOrFail('indexDetailsSettingsEditModeSwitch', { timeout: 2000 }); + }, + async expectEditSettingsIsDisabled() { + await testSubjects.existOrFail('indexDetailsSettingsEditModeSwitch', { timeout: 2000 }); + const isEditSettingsButtonDisabled = await testSubjects.isEnabled( + 'indexDetailsSettingsEditModeSwitch' + ); + expect(isEditSettingsButtonDisabled).to.be(false); + await testSubjects.moveMouseTo('indexDetailsSettingsEditModeSwitch'); + await testSubjects.existOrFail('indexDetailsSettingsEditModeSwitchToolTip'); + }, + async expectEditSettingsToBeEnabled() { + await testSubjects.existOrFail('indexDetailsSettingsEditModeSwitch', { timeout: 2000 }); + const isEditSettingsButtonDisabled = await testSubjects.isEnabled( + 'indexDetailsSettingsEditModeSwitch' + ); + expect(isEditSettingsButtonDisabled).to.be(true); + }, + async expectSelectedLanguage(language: string) { + await testSubjects.existOrFail('codeExampleLanguageSelect'); + expect( + (await testSubjects.getVisibleText('codeExampleLanguageSelect')).toLowerCase() + ).contain(language); + }, + async selectCodingLanguage(language: string) { + await testSubjects.existOrFail('codeExampleLanguageSelect'); + await testSubjects.click('codeExampleLanguageSelect'); + await testSubjects.existOrFail(`lang-option-${language}`); + await testSubjects.click(`lang-option-${language}`); + expect( + (await testSubjects.getVisibleText('codeExampleLanguageSelect')).toLowerCase() + ).contain(language); + }, + async codeSampleContainsValue(subject: string, value: string) { + const tstSubjId = `${subject}-code-block`; + await testSubjects.existOrFail(tstSubjId); + expect(await testSubjects.getVisibleText(tstSubjId)).contain(value); + }, + async openConsoleCodeExample() { + await testSubjects.existOrFail('tryInConsoleButton'); + await testSubjects.click('tryInConsoleButton'); + }, + + async expectAPIKeyToBeVisibleInCodeBlock(apiKey: string) { + await testSubjects.existOrFail('ingestDataCodeExample-code-block'); + expect(await testSubjects.getVisibleText('ingestDataCodeExample-code-block')).to.contain( + apiKey + ); + }, + + async expectHasSampleDocuments() { + await testSubjects.existOrFail('ingestDataCodeExample-code-block'); + expect(await testSubjects.getVisibleText('ingestDataCodeExample-code-block')).to.contain( + 'Yellowstone National Park' + ); + expect(await testSubjects.getVisibleText('ingestDataCodeExample-code-block')).to.contain( + 'Yosemite National Park' + ); + expect(await testSubjects.getVisibleText('ingestDataCodeExample-code-block')).to.contain( + 'Rocky Mountain National Park' + ); + }, + + async expectSampleDocumentsWithCustomMappings() { + await browser.refresh(); + await testSubjects.existOrFail('ingestDataCodeExample-code-block'); + expect(await testSubjects.getVisibleText('ingestDataCodeExample-code-block')).to.contain( + 'Example text 1' + ); + expect(await testSubjects.getVisibleText('ingestDataCodeExample-code-block')).to.contain( + 'Example text 2' + ); + expect(await testSubjects.getVisibleText('ingestDataCodeExample-code-block')).to.contain( + 'Example text 3' + ); + }, + + async clickFirstDocumentDeleteAction() { + await testSubjects.existOrFail('documentMetadataButton'); + await testSubjects.click('documentMetadataButton'); + await testSubjects.existOrFail('deleteDocumentButton'); + await testSubjects.click('deleteDocumentButton'); + }, + async expectDeleteDocumentActionNotVisible() { + await testSubjects.existOrFail('documentMetadataButton'); + await testSubjects.click('documentMetadataButton'); + await testSubjects.missingOrFail('deleteDocumentButton'); + }, + async expectDeleteDocumentActionIsDisabled() { + await testSubjects.existOrFail('documentMetadataButton'); + await testSubjects.click('documentMetadataButton'); + await testSubjects.existOrFail('deleteDocumentButton'); + const isDeleteDocumentEnabled = await testSubjects.isEnabled('deleteDocumentButton'); + expect(isDeleteDocumentEnabled).to.be(false); + await testSubjects.moveMouseTo('deleteDocumentButton'); + await testSubjects.existOrFail('deleteDocumentButtonToolTip'); + }, + async expectDeleteDocumentActionToBeEnabled() { + await testSubjects.existOrFail('documentMetadataButton'); + await testSubjects.click('documentMetadataButton'); + await testSubjects.existOrFail('deleteDocumentButton'); + const isDeleteDocumentEnabled = await testSubjects.isEnabled('deleteDocumentButton'); + expect(isDeleteDocumentEnabled).to.be(true); + }, + + async openIndicesDetailFromIndexManagementIndicesListTable(indexOfRow: number) { + const indexList = await testSubjects.findAll('indexTableIndexNameLink'); + await indexList[indexOfRow].click(); + await retry.waitFor('index details page title to show up', async () => { + return (await testSubjects.isDisplayed('searchIndexDetailsHeader')) === true; + }); + }, + async expectSearchIndexDetailsTabsExists() { + await testSubjects.existOrFail('dataTab'); + await testSubjects.existOrFail('mappingsTab'); + await testSubjects.existOrFail('settingsTab'); + }, + + async expectBreadcrumbNavigationWithIndexName(indexName: string) { + await testSubjects.existOrFail('euiBreadcrumb'); + expect(await testSubjects.getVisibleText('breadcrumb last')).to.contain(indexName); + }, + + async clickOnIndexManagementBreadcrumb() { + const breadcrumbs = await testSubjects.findAll('breadcrumb'); + for (const breadcrumb of breadcrumbs) { + if ((await breadcrumb.getVisibleText()) === 'Index Management') { + await breadcrumb.click(); + return; + } + } + }, + + async expectAddFieldToBeDisabled() { + await testSubjects.existOrFail('indexDetailsMappingsAddField'); + const isMappingsFieldEnabled = await testSubjects.isEnabled('indexDetailsMappingsAddField'); + expect(isMappingsFieldEnabled).to.be(false); + await testSubjects.moveMouseTo('indexDetailsMappingsAddField'); + await testSubjects.existOrFail('indexDetailsMappingsAddFieldTooltip'); + }, + + async expectAddFieldToBeEnabled() { + await testSubjects.existOrFail('indexDetailsMappingsAddField'); + const isMappingsFieldEnabled = await testSubjects.isEnabled('indexDetailsMappingsAddField'); + expect(isMappingsFieldEnabled).to.be(true); + }, + }; +} diff --git a/x-pack/test/functional/page_objects/search_navigation.ts b/x-pack/test/functional/page_objects/search_navigation.ts new file mode 100644 index 0000000000000..3eb59c85507bd --- /dev/null +++ b/x-pack/test/functional/page_objects/search_navigation.ts @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../ftr_provider_context'; + +export function SearchNavigationProvider({ getService, getPageObjects }: FtrProviderContext) { + const retry = getService('retry'); + const PageObjects = getPageObjects(['common']); + const testSubjects = getService('testSubjects'); + + return { + async navigateToLandingPage() { + await retry.tryForTime(60 * 1000, async () => { + await PageObjects.common.navigateToApp('landingPage'); + // Wait for the side nav, since the landing page will sometimes redirect to index management now + await testSubjects.existOrFail('svlSearchSideNav', { timeout: 2000 }); + }); + }, + async navigateToGettingStartedPage() { + await retry.tryForTime(60 * 1000, async () => { + await PageObjects.common.navigateToApp('serverlessElasticsearch'); + await testSubjects.existOrFail('svlSearchOverviewPage', { timeout: 2000 }); + }); + }, + async navigateToElasticsearchStartPage(expectRedirect: boolean = false) { + await retry.tryForTime(60 * 1000, async () => { + await PageObjects.common.navigateToApp('elasticsearchStart', { + shouldLoginIfPrompted: false, + }); + if (!expectRedirect) { + await testSubjects.existOrFail('elasticsearchStartPage', { timeout: 2000 }); + } + }); + }, + async navigateToIndexDetailPage(indexName: string) { + await retry.tryForTime(60 * 1000, async () => { + await PageObjects.common.navigateToApp(`elasticsearch/indices/index_details/${indexName}`, { + shouldLoginIfPrompted: false, + }); + }); + await testSubjects.existOrFail('searchIndicesDetailsPage', { timeout: 2000 }); + }, + async navigateToInferenceManagementPage(expectRedirect: boolean = false) { + await PageObjects.common.navigateToApp('searchInferenceEndpoints', { + shouldLoginIfPrompted: false, + }); + }, + }; +} diff --git a/x-pack/test/functional_search/config.ts b/x-pack/test/functional_search/config.ts index 808edc73d97a6..cc703ef56da87 100644 --- a/x-pack/test/functional_search/config.ts +++ b/x-pack/test/functional_search/config.ts @@ -30,7 +30,10 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { }, kbnTestServer: { ...functionalConfig.get('kbnTestServer'), - serverArgs: [...functionalConfig.get('kbnTestServer.serverArgs')], + serverArgs: [ + ...functionalConfig.get('kbnTestServer.serverArgs'), + '--xpack.searchIndices.enabled=true', + ], }, }; } diff --git a/x-pack/test/functional_search/index.ts b/x-pack/test/functional_search/index.ts index d48bd1d695d16..799f2a65b7338 100644 --- a/x-pack/test/functional_search/index.ts +++ b/x-pack/test/functional_search/index.ts @@ -12,5 +12,6 @@ export default ({ loadTestFile }: FtrProviderContext): void => { describe('Search solution tests', function () { loadTestFile(require.resolve('./tests/classic_navigation')); loadTestFile(require.resolve('./tests/solution_navigation')); + loadTestFile(require.resolve('./tests/search_index_details')); }); }; diff --git a/x-pack/test/functional_search/tests/embedded_console.ts b/x-pack/test/functional_search/tests/embedded_console.ts new file mode 100644 index 0000000000000..04153d4a39ee6 --- /dev/null +++ b/x-pack/test/functional_search/tests/embedded_console.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../ftr_provider_context'; + +type PageObjects = Pick, 'embeddedConsole'>; + +export async function testHasEmbeddedConsole(pageObjects: PageObjects) { + await pageObjects.embeddedConsole.expectEmbeddedConsoleControlBarExists(); + await pageObjects.embeddedConsole.expectEmbeddedConsoleToBeClosed(); + await pageObjects.embeddedConsole.clickEmbeddedConsoleControlBar(); + await pageObjects.embeddedConsole.expectEmbeddedConsoleHaveFullscreenToggle(); + await pageObjects.embeddedConsole.expectEmbeddedConsoleToBeOpen(); + await pageObjects.embeddedConsole.clickEmbeddedConsoleControlBar(); + await pageObjects.embeddedConsole.expectEmbeddedConsoleToBeClosed(); +} diff --git a/x-pack/test/functional_search/tests/search_index_details.ts b/x-pack/test/functional_search/tests/search_index_details.ts new file mode 100644 index 0000000000000..4198869ebb4e0 --- /dev/null +++ b/x-pack/test/functional_search/tests/search_index_details.ts @@ -0,0 +1,405 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { FtrProviderContext } from '../ftr_provider_context'; +import { testHasEmbeddedConsole } from './embedded_console'; + +export default function ({ getPageObjects, getService }: FtrProviderContext) { + const pageObjects = getPageObjects([ + 'embeddedConsole', + 'searchIndexDetailsPage', + 'searchApiKeys', + 'header', + 'common', + 'indexManagement', + 'searchNavigation', + ]); + const es = getService('es'); + const security = getService('security'); + const browser = getService('browser'); + const retry = getService('retry'); + const spaces = getService('spaces'); + const esDeleteAllIndices = getService('esDeleteAllIndices'); + + const indexName = 'test-my-index'; + + describe('Search index details page', function () { + describe('Solution Nav - Search', function () { + let cleanUp: () => Promise; + let spaceCreated: { id: string } = { id: '' }; + + before(async () => { + // Navigate to the spaces management page which will log us in Kibana + await pageObjects.common.navigateToUrl('management', 'kibana/spaces', { + shouldUseHashForSubUrl: false, + }); + + // Create a space with the search solution and navigate to its home page + ({ cleanUp, space: spaceCreated } = await spaces.create({ + name: 'search-ftr', + solution: 'es', + })); + await pageObjects.searchApiKeys.deleteAPIKeys(); + }); + + after(async () => { + // Clean up space created + await cleanUp(); + await esDeleteAllIndices(indexName); + }); + describe('search index details page', () => { + before(async () => { + // Navigate to the spaces management page which will log us in Kibana + await pageObjects.searchApiKeys.deleteAPIKeys(); + await browser.navigateTo(spaces.getRootUrl(spaceCreated.id)); + await es.indices.create({ index: indexName }); + await pageObjects.searchNavigation.navigateToIndexDetailPage(indexName); + }); + after(async () => { + await esDeleteAllIndices(indexName); + }); + it('can load index detail page', async () => { + await pageObjects.searchNavigation.navigateToIndexDetailPage(indexName); + await pageObjects.searchIndexDetailsPage.expectIndexDetailPageHeader(); + await pageObjects.searchIndexDetailsPage.expectSearchIndexDetailsTabsExists(); + await pageObjects.searchIndexDetailsPage.expectAPIReferenceDocLinkExists(); + await pageObjects.searchIndexDetailsPage.expectAPIReferenceDocLinkMissingInMoreOptions(); + }); + it('should have embedded dev console', async () => { + await testHasEmbeddedConsole(pageObjects); + }); + it('should have connection details', async () => { + await pageObjects.searchIndexDetailsPage.expectConnectionDetails(); + }); + + describe('check code example texts', () => { + const indexNameCodeExample = 'test-my-index2'; + before(async () => { + await es.indices.create({ index: indexNameCodeExample }); + await pageObjects.searchNavigation.navigateToIndexDetailPage(indexNameCodeExample); + }); + + after(async () => { + await esDeleteAllIndices(indexNameCodeExample); + }); + + it('should have basic example texts', async () => { + await pageObjects.searchIndexDetailsPage.expectHasSampleDocuments(); + }); + + it('should have other example texts when mapping changed', async () => { + await es.indices.putMapping({ + index: indexNameCodeExample, + properties: { + text: { type: 'text' }, + number: { type: 'integer' }, + }, + }); + await pageObjects.searchIndexDetailsPage.expectSampleDocumentsWithCustomMappings(); + }); + }); + + describe('API key details', () => { + it('should show api key', async () => { + await pageObjects.searchApiKeys.deleteAPIKeys(); + await pageObjects.searchNavigation.navigateToIndexDetailPage(indexName); + // sometimes the API key exists in the cluster and its lost in sessionStorage + // if fails we retry to delete the API key and refresh the browser + await retry.try( + async () => { + await pageObjects.searchApiKeys.expectAPIKeyExists(); + }, + async () => { + await pageObjects.searchApiKeys.deleteAPIKeys(); + await browser.refresh(); + } + ); + await pageObjects.searchApiKeys.expectAPIKeyAvailable(); + const apiKey = await pageObjects.searchApiKeys.getAPIKeyFromUI(); + await pageObjects.searchIndexDetailsPage.expectAPIKeyToBeVisibleInCodeBlock(apiKey); + }); + }); + + it('should have quick stats', async () => { + await pageObjects.searchIndexDetailsPage.expectQuickStats(); + await pageObjects.searchIndexDetailsPage.expectQuickStatsAIMappings(); + await es.indices.putMapping({ + index: indexName, + body: { + properties: { + my_field: { + type: 'dense_vector', + dims: 3, + }, + }, + }, + }); + await pageObjects.searchNavigation.navigateToIndexDetailPage(indexName); + await pageObjects.searchIndexDetailsPage.expectQuickStatsAIMappingsToHaveVectorFields(); + }); + + it('should show code examples for adding documents', async () => { + await pageObjects.searchIndexDetailsPage.expectAddDocumentCodeExamples(); + await pageObjects.searchIndexDetailsPage.expectSelectedLanguage('python'); + await pageObjects.searchIndexDetailsPage.codeSampleContainsValue( + 'installCodeExample', + 'pip install' + ); + await pageObjects.searchIndexDetailsPage.selectCodingLanguage('javascript'); + await pageObjects.searchIndexDetailsPage.codeSampleContainsValue( + 'installCodeExample', + 'npm install' + ); + await pageObjects.searchIndexDetailsPage.selectCodingLanguage('curl'); + await pageObjects.searchIndexDetailsPage.openConsoleCodeExample(); + await pageObjects.embeddedConsole.expectEmbeddedConsoleToBeOpen(); + await pageObjects.embeddedConsole.clickEmbeddedConsoleControlBar(); + }); + + describe('With data', () => { + before(async () => { + await es.index({ + index: indexName, + refresh: true, + body: { + my_field: [1, 0, 1], + }, + }); + await pageObjects.searchNavigation.navigateToIndexDetailPage(indexName); + }); + it('should have index documents', async () => { + await pageObjects.searchIndexDetailsPage.expectHasIndexDocuments(); + }); + it('menu action item should be replaced with playground', async () => { + await pageObjects.searchIndexDetailsPage.expectActionItemReplacedWhenHasDocs(); + }); + it('should have link to API reference doc link in options menu', async () => { + await pageObjects.searchIndexDetailsPage.clickMoreOptionsActionsButton(); + await pageObjects.searchIndexDetailsPage.expectAPIReferenceDocLinkExistsInMoreOptions(); + }); + it('should have one document in quick stats', async () => { + await pageObjects.searchIndexDetailsPage.expectQuickStatsToHaveDocumentCount(1); + }); + it('should have with data tabs', async () => { + await pageObjects.searchIndexDetailsPage.expectTabsExists(); + await pageObjects.searchIndexDetailsPage.expectUrlShouldChangeTo('data'); + }); + it('should be able to change tabs to mappings and mappings is shown', async () => { + await pageObjects.searchIndexDetailsPage.changeTab('mappingsTab'); + await pageObjects.searchIndexDetailsPage.expectUrlShouldChangeTo('mappings'); + await pageObjects.searchIndexDetailsPage.expectMappingsComponentIsVisible(); + }); + it('should be able to change tabs to settings and settings is shown', async () => { + await pageObjects.searchIndexDetailsPage.changeTab('settingsTab'); + await pageObjects.searchIndexDetailsPage.expectUrlShouldChangeTo('settings'); + await pageObjects.searchIndexDetailsPage.expectSettingsComponentIsVisible(); + }); + it('should be able to delete document', async () => { + await pageObjects.searchIndexDetailsPage.changeTab('dataTab'); + await pageObjects.searchIndexDetailsPage.clickFirstDocumentDeleteAction(); + await pageObjects.searchIndexDetailsPage.expectAddDocumentCodeExamples(); + await pageObjects.searchIndexDetailsPage.expectQuickStatsToHaveDocumentCount(0); + }); + }); + describe('has index actions enabled', () => { + before(async () => { + await es.index({ + index: indexName, + body: { + my_field: [1, 0, 1], + }, + }); + await pageObjects.searchNavigation.navigateToIndexDetailPage(indexName); + }); + + beforeEach(async () => { + await pageObjects.searchNavigation.navigateToIndexDetailPage(indexName); + }); + + it('delete document button is enabled', async () => { + await pageObjects.searchIndexDetailsPage.expectDeleteDocumentActionToBeEnabled(); + }); + it('add field button is enabled', async () => { + await pageObjects.searchIndexDetailsPage.changeTab('mappingsTab'); + await pageObjects.searchIndexDetailsPage.expectAddFieldToBeEnabled(); + }); + it('edit settings button is enabled', async () => { + await pageObjects.searchIndexDetailsPage.changeTab('settingsTab'); + await pageObjects.searchIndexDetailsPage.expectEditSettingsToBeEnabled(); + }); + it('delete index button is enabled', async () => { + await pageObjects.searchIndexDetailsPage.expectMoreOptionsActionButtonExists(); + await pageObjects.searchIndexDetailsPage.clickMoreOptionsActionsButton(); + await pageObjects.searchIndexDetailsPage.expectMoreOptionsOverviewMenuIsShown(); + await pageObjects.searchIndexDetailsPage.expectDeleteIndexButtonExistsInMoreOptions(); + await pageObjects.searchIndexDetailsPage.expectDeleteIndexButtonToBeEnabled(); + }); + }); + + describe('page loading error', () => { + before(async () => { + await pageObjects.searchNavigation.navigateToIndexDetailPage(indexName); + await esDeleteAllIndices(indexName); + }); + it('has page load error section', async () => { + await pageObjects.searchIndexDetailsPage.expectPageLoadErrorExists(); + await pageObjects.searchIndexDetailsPage.expectIndexNotFoundErrorExists(); + }); + it('reload button shows details page again', async () => { + await es.indices.create({ index: indexName }); + await pageObjects.searchIndexDetailsPage.clickPageReload(); + await pageObjects.searchIndexDetailsPage.expectIndexDetailPageHeader(); + }); + }); + describe('Index more options menu', () => { + before(async () => { + await pageObjects.searchNavigation.navigateToIndexDetailPage(indexName); + }); + it('shows action menu in actions popover', async () => { + await pageObjects.searchIndexDetailsPage.expectMoreOptionsActionButtonExists(); + await pageObjects.searchIndexDetailsPage.clickMoreOptionsActionsButton(); + await pageObjects.searchIndexDetailsPage.expectMoreOptionsOverviewMenuIsShown(); + }); + it('should delete index', async () => { + await pageObjects.searchIndexDetailsPage.expectDeleteIndexButtonExistsInMoreOptions(); + await pageObjects.searchIndexDetailsPage.clickDeleteIndexButton(); + await pageObjects.searchIndexDetailsPage.clickConfirmingDeleteIndex(); + }); + }); + }); + + describe('index management index list page', () => { + before(async () => { + await esDeleteAllIndices(indexName); + await es.indices.create({ index: indexName }); + await security.testUser.setRoles(['index_management_user']); + }); + beforeEach(async () => { + // Navigate to search solution space + await browser.navigateTo(spaces.getRootUrl(spaceCreated.id)); + // Navigate to index management app + await pageObjects.common.navigateToApp('indexManagement', { + basePath: `s/${spaceCreated.id}`, + }); + // Navigate to the indices tab + await pageObjects.indexManagement.changeTabs('indicesTab'); + await pageObjects.header.waitUntilLoadingHasFinished(); + }); + after(async () => { + await esDeleteAllIndices(indexName); + }); + describe('manage index action', () => { + beforeEach(async () => { + await pageObjects.indexManagement.manageIndex(indexName); + await pageObjects.indexManagement.manageIndexContextMenuExists(); + }); + it('navigates to overview tab', async () => { + await pageObjects.indexManagement.changeManageIndexTab('showOverviewIndexMenuButton'); + await pageObjects.searchIndexDetailsPage.expectIndexDetailPageHeader(); + await pageObjects.searchIndexDetailsPage.expectUrlShouldChangeTo('data'); + }); + + it('navigates to settings tab', async () => { + await pageObjects.indexManagement.changeManageIndexTab('showSettingsIndexMenuButton'); + await pageObjects.searchIndexDetailsPage.expectIndexDetailPageHeader(); + await pageObjects.searchIndexDetailsPage.expectUrlShouldChangeTo('settings'); + }); + it('navigates to mappings tab', async () => { + await pageObjects.indexManagement.changeManageIndexTab('showMappingsIndexMenuButton'); + await pageObjects.searchIndexDetailsPage.expectIndexDetailPageHeader(); + await pageObjects.searchIndexDetailsPage.expectUrlShouldChangeTo('mappings'); + }); + }); + describe('can view search index details', function () { + it('renders search index details with no documents', async () => { + await pageObjects.searchIndexDetailsPage.openIndicesDetailFromIndexManagementIndicesListTable( + 0 + ); + await pageObjects.searchIndexDetailsPage.expectIndexDetailPageHeader(); + await pageObjects.searchIndexDetailsPage.expectSearchIndexDetailsTabsExists(); + await pageObjects.searchIndexDetailsPage.expectAPIReferenceDocLinkExists(); + }); + }); + }); + }); + describe('Classic Nav', function () { + let cleanUp: () => Promise; + let spaceCreated: { id: string } = { id: '' }; + + before(async () => { + // Navigate to the spaces management page which will log us in Kibana + await pageObjects.common.navigateToUrl('management', 'kibana/spaces', { + shouldUseHashForSubUrl: false, + }); + + // Create a space with the search solution and navigate to its home page + ({ cleanUp, space: spaceCreated } = await spaces.create({ + name: 'classic-nav', + solution: 'classic', + })); + await pageObjects.searchApiKeys.deleteAPIKeys(); + }); + + after(async () => { + // Clean up space created + await cleanUp(); + await esDeleteAllIndices(indexName); + }); + describe('index management index list page', () => { + before(async () => { + await esDeleteAllIndices(indexName); + await es.indices.create({ index: indexName }); + await security.testUser.setRoles(['index_management_user']); + }); + beforeEach(async () => { + // Navigate to search solution space + await browser.navigateTo(spaces.getRootUrl(spaceCreated.id)); + // Navigate to index management app + await pageObjects.common.navigateToApp('indexManagement', { + basePath: `s/${spaceCreated.id}`, + }); + // Navigate to the indices tab + await pageObjects.indexManagement.changeTabs('indicesTab'); + await pageObjects.header.waitUntilLoadingHasFinished(); + }); + after(async () => { + await esDeleteAllIndices(indexName); + }); + describe('manage index action', () => { + beforeEach(async () => { + await pageObjects.indexManagement.manageIndex(indexName); + await pageObjects.indexManagement.manageIndexContextMenuExists(); + }); + it('navigates to overview tab', async () => { + await pageObjects.indexManagement.changeManageIndexTab('showOverviewIndexMenuButton'); + await pageObjects.indexManagement.indexDetailsPage.expectIndexDetailsPageIsLoaded(); + await pageObjects.indexManagement.indexDetailsPage.expectUrlShouldChangeTo('overview'); + }); + + it('navigates to settings tab', async () => { + await pageObjects.indexManagement.changeManageIndexTab('showSettingsIndexMenuButton'); + await pageObjects.indexManagement.indexDetailsPage.expectIndexDetailsPageIsLoaded(); + await pageObjects.indexManagement.indexDetailsPage.expectUrlShouldChangeTo('settings'); + }); + it('navigates to mappings tab', async () => { + await pageObjects.indexManagement.changeManageIndexTab('showMappingsIndexMenuButton'); + await pageObjects.indexManagement.indexDetailsPage.expectIndexDetailsPageIsLoaded(); + await pageObjects.indexManagement.indexDetailsPage.expectUrlShouldChangeTo('mappings'); + }); + }); + describe('can view index management index details page', function () { + it('navigates to the index management index details page from the home page', async () => { + // display hidden indices to have some rows in the indices table + await pageObjects.indexManagement.toggleHiddenIndices(); + // click the first index in the table and wait for the index details page + await pageObjects.indexManagement.indexDetailsPage.openIndexDetailsPage(0); + await pageObjects.indexManagement.indexDetailsPage.expectIndexDetailsPageIsLoaded(); + }); + }); + }); + }); + }); +} From ac4577159e5ebb578a38860df5366b029e4a44b0 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Fri, 10 Jan 2025 08:32:01 -0700 Subject: [PATCH 27/42] [Security solution] Reinstall product documentation callout (#205975) --- .../impl/assistant/api/product_docs/const.ts | 11 +++ .../use_get_product_doc_status.test.ts | 62 ++++++++++++ .../use_get_product_doc_status.ts | 32 +++++++ .../use_install_product_doc.test.ts | 67 +++++++++++++ .../product_docs/use_install_product_doc.ts | 48 ++++++++++ .../product_documentation/index.test.tsx | 87 +++++++++++++++++ .../settings/product_documentation/index.tsx | 94 +++++++++++++++++++ .../product_documentation/translations.ts | 29 ++++++ .../impl/assistant_context/index.tsx | 6 ++ .../index.tsx | 2 + .../mock/test_providers/test_providers.tsx | 3 + .../kbn-elastic-assistant/tsconfig.json | 1 + .../mock/test_providers/test_providers.tsx | 3 + .../plugins/security_solution/kibana.jsonc | 3 +- .../public/assistant/provider.tsx | 2 + .../common/mock/mock_assistant_provider.tsx | 3 + .../rule_status_failed_callout.test.tsx | 39 +------- .../public/plugin_services.ts | 1 + .../plugins/security_solution/public/types.ts | 3 + .../plugins/security_solution/tsconfig.json | 4 +- 20 files changed, 460 insertions(+), 40 deletions(-) create mode 100644 x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/api/product_docs/const.ts create mode 100644 x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/api/product_docs/use_get_product_doc_status.test.ts create mode 100644 x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/api/product_docs/use_get_product_doc_status.ts create mode 100644 x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/api/product_docs/use_install_product_doc.test.ts create mode 100644 x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/api/product_docs/use_install_product_doc.ts create mode 100644 x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/settings/product_documentation/index.test.tsx create mode 100644 x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/settings/product_documentation/index.tsx create mode 100644 x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/settings/product_documentation/translations.ts diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/api/product_docs/const.ts b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/api/product_docs/const.ts new file mode 100644 index 0000000000000..89ccf285229da --- /dev/null +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/api/product_docs/const.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const REACT_QUERY_KEYS = { + GET_PRODUCT_DOC_STATUS: 'get_product_doc_status', + INSTALL_PRODUCT_DOC: 'install_product_doc', +}; diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/api/product_docs/use_get_product_doc_status.test.ts b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/api/product_docs/use_get_product_doc_status.test.ts new file mode 100644 index 0000000000000..1a6dc829c767a --- /dev/null +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/api/product_docs/use_get_product_doc_status.test.ts @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { waitFor, renderHook } from '@testing-library/react'; +import { useGetProductDocStatus } from './use_get_product_doc_status'; +import { useAssistantContext } from '../../../..'; +import { TestProviders } from '../../../mock/test_providers/test_providers'; + +jest.mock('../../../..', () => ({ + useAssistantContext: jest.fn(), +})); + +describe('useGetProductDocStatus', () => { + const mockGetStatus = jest.fn(); + + beforeEach(() => { + (useAssistantContext as jest.Mock).mockReturnValue({ + productDocBase: { + installation: { + getStatus: mockGetStatus, + }, + }, + }); + }); + + it('returns loading state initially', async () => { + mockGetStatus.mockResolvedValueOnce('status'); + const { result } = renderHook(() => useGetProductDocStatus(), { + wrapper: TestProviders, + }); + + expect(result.current.isLoading).toBe(true); + await waitFor(() => result.current.isSuccess); + }); + + it('returns success state with data', async () => { + mockGetStatus.mockResolvedValueOnce('status'); + const { result } = renderHook(() => useGetProductDocStatus(), { + wrapper: TestProviders, + }); + + await waitFor(() => { + expect(result.current.status).toBe('status'); + expect(result.current.isSuccess).toBe(true); + }); + }); + + it('returns error state when query fails', async () => { + mockGetStatus.mockRejectedValueOnce(new Error('error')); + const { result } = renderHook(() => useGetProductDocStatus(), { + wrapper: TestProviders, + }); + + await waitFor(() => { + expect(result.current.isError).toBe(true); + }); + }); +}); diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/api/product_docs/use_get_product_doc_status.ts b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/api/product_docs/use_get_product_doc_status.ts new file mode 100644 index 0000000000000..5e516a4207cfa --- /dev/null +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/api/product_docs/use_get_product_doc_status.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useQuery } from '@tanstack/react-query'; +import { REACT_QUERY_KEYS } from './const'; +import { useAssistantContext } from '../../../..'; + +export function useGetProductDocStatus() { + const { productDocBase } = useAssistantContext(); + + const { isLoading, isError, isSuccess, isRefetching, data, refetch } = useQuery({ + queryKey: [REACT_QUERY_KEYS.GET_PRODUCT_DOC_STATUS], + queryFn: async () => { + return productDocBase.installation.getStatus(); + }, + keepPreviousData: false, + refetchOnWindowFocus: false, + }); + + return { + status: data, + refetch, + isLoading, + isRefetching, + isSuccess, + isError, + }; +} diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/api/product_docs/use_install_product_doc.test.ts b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/api/product_docs/use_install_product_doc.test.ts new file mode 100644 index 0000000000000..3b3c12d6b9dc8 --- /dev/null +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/api/product_docs/use_install_product_doc.test.ts @@ -0,0 +1,67 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { waitFor, renderHook } from '@testing-library/react'; +import { useInstallProductDoc } from './use_install_product_doc'; +import { useAssistantContext } from '../../../..'; +import { TestProviders } from '../../../mock/test_providers/test_providers'; + +jest.mock('../../../..', () => ({ + useAssistantContext: jest.fn(), +})); + +describe('useInstallProductDoc', () => { + const mockInstall = jest.fn(); + const mockAddSuccess = jest.fn(); + const mockAddError = jest.fn(); + + beforeEach(() => { + (useAssistantContext as jest.Mock).mockReturnValue({ + productDocBase: { + installation: { + install: mockInstall, + }, + }, + toasts: { + addSuccess: mockAddSuccess, + addError: mockAddError, + }, + }); + }); + + it('returns success state and shows success toast on successful installation', async () => { + mockInstall.mockResolvedValueOnce({}); + const { result } = renderHook(() => useInstallProductDoc(), { + wrapper: TestProviders, + }); + + result.current.mutate(); + await waitFor(() => result.current.isSuccess); + + expect(mockAddSuccess).toHaveBeenCalledWith( + 'The Elastic documentation was successfully installed' + ); + }); + + it('returns error state and shows error toast on failed installation', async () => { + const error = new Error('error message'); + mockInstall.mockRejectedValueOnce(error); + const { result } = renderHook(() => useInstallProductDoc(), { + wrapper: TestProviders, + }); + + result.current.mutate(); + await waitFor(() => result.current.isError); + + expect(mockAddError).toHaveBeenCalledWith( + expect.objectContaining({ + message: 'error message', + }), + { title: 'Something went wrong while installing the Elastic documentation' } + ); + }); +}); diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/api/product_docs/use_install_product_doc.ts b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/api/product_docs/use_install_product_doc.ts new file mode 100644 index 0000000000000..b17dab7826c48 --- /dev/null +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/api/product_docs/use_install_product_doc.ts @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { i18n } from '@kbn/i18n'; +import type { IHttpFetchError, ResponseErrorBody } from '@kbn/core/public'; +import type { PerformInstallResponse } from '@kbn/product-doc-base-plugin/common/http_api/installation'; +import { REACT_QUERY_KEYS } from './const'; +import { useAssistantContext } from '../../../..'; + +type ServerError = IHttpFetchError; + +export function useInstallProductDoc() { + const { productDocBase, toasts } = useAssistantContext(); + const queryClient = useQueryClient(); + + return useMutation( + [REACT_QUERY_KEYS.INSTALL_PRODUCT_DOC], + () => { + return productDocBase.installation.install(); + }, + { + onSuccess: () => { + toasts?.addSuccess( + i18n.translate('xpack.elasticAssistant.kb.installProductDoc.successNotification', { + defaultMessage: 'The Elastic documentation was successfully installed', + }) + ); + + queryClient.invalidateQueries({ + queryKey: [REACT_QUERY_KEYS.GET_PRODUCT_DOC_STATUS], + refetchType: 'all', + }); + }, + onError: (error) => { + toasts?.addError(new Error(error.body?.message ?? error.message), { + title: i18n.translate('xpack.elasticAssistant.kb.installProductDoc.errorNotification', { + defaultMessage: 'Something went wrong while installing the Elastic documentation', + }), + }); + }, + } + ); +} diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/settings/product_documentation/index.test.tsx b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/settings/product_documentation/index.test.tsx new file mode 100644 index 0000000000000..c8700f995862f --- /dev/null +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/settings/product_documentation/index.test.tsx @@ -0,0 +1,87 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React from 'react'; +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import { ProductDocumentationManagement } from '.'; +import * as i18n from './translations'; +import { useInstallProductDoc } from '../../api/product_docs/use_install_product_doc'; +import { useGetProductDocStatus } from '../../api/product_docs/use_get_product_doc_status'; + +jest.mock('../../api/product_docs/use_install_product_doc'); +jest.mock('../../api/product_docs/use_get_product_doc_status'); + +describe('ProductDocumentationManagement', () => { + const mockInstallProductDoc = jest.fn().mockResolvedValue({}); + + beforeEach(() => { + (useInstallProductDoc as jest.Mock).mockReturnValue({ mutateAsync: mockInstallProductDoc }); + (useGetProductDocStatus as jest.Mock).mockReturnValue({ status: null, isLoading: false }); + jest.clearAllMocks(); + }); + + it('renders loading spinner when status is loading', async () => { + (useGetProductDocStatus as jest.Mock).mockReturnValue({ + status: { overall: 'not_installed' }, + isLoading: true, + }); + render(); + expect(screen.getByTestId('statusLoading')).toBeInTheDocument(); + }); + + it('renders install button when not installed', () => { + (useGetProductDocStatus as jest.Mock).mockReturnValue({ + status: { overall: 'not_installed' }, + isLoading: false, + }); + render(); + expect(screen.getByText(i18n.INSTALL)).toBeInTheDocument(); + }); + + it('does not render anything when already installed', () => { + (useGetProductDocStatus as jest.Mock).mockReturnValue({ + status: { overall: 'installed' }, + isLoading: false, + }); + const { container } = render(); + expect(container).toBeEmptyDOMElement(); + }); + + it('shows installing spinner and text when installing', async () => { + (useGetProductDocStatus as jest.Mock).mockReturnValue({ + status: { overall: 'not_installed' }, + isLoading: false, + }); + render(); + fireEvent.click(screen.getByText(i18n.INSTALL)); + await waitFor(() => { + expect(screen.getByTestId('installing')).toBeInTheDocument(); + expect(screen.getByText(i18n.INSTALLING)).toBeInTheDocument(); + }); + }); + + it('sets installed state to true after successful installation', async () => { + (useGetProductDocStatus as jest.Mock).mockReturnValue({ + status: { overall: 'not_installed' }, + isLoading: false, + }); + mockInstallProductDoc.mockResolvedValueOnce({}); + render(); + fireEvent.click(screen.getByText(i18n.INSTALL)); + await waitFor(() => expect(screen.queryByText(i18n.INSTALL)).not.toBeInTheDocument()); + }); + + it('sets installed state to false after failed installation', async () => { + (useGetProductDocStatus as jest.Mock).mockReturnValue({ + status: { overall: 'not_installed' }, + isLoading: false, + }); + mockInstallProductDoc.mockRejectedValueOnce(new Error('Installation failed')); + render(); + fireEvent.click(screen.getByText(i18n.INSTALL)); + await waitFor(() => expect(screen.getByText(i18n.INSTALL)).toBeInTheDocument()); + }); +}); diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/settings/product_documentation/index.tsx b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/settings/product_documentation/index.tsx new file mode 100644 index 0000000000000..45dc67c59784f --- /dev/null +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/settings/product_documentation/index.tsx @@ -0,0 +1,94 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + EuiButton, + EuiCallOut, + EuiFlexGroup, + EuiFlexItem, + EuiLoadingSpinner, + EuiSpacer, + EuiText, +} from '@elastic/eui'; +import React, { useEffect, useState, useCallback, useMemo } from 'react'; +import { useInstallProductDoc } from '../../api/product_docs/use_install_product_doc'; +import { useGetProductDocStatus } from '../../api/product_docs/use_get_product_doc_status'; +import * as i18n from './translations'; + +export const ProductDocumentationManagement: React.FC = React.memo(() => { + const [{ isInstalled, isInstalling }, setState] = useState({ + isInstalled: true, + isInstalling: false, + }); + + const { mutateAsync: installProductDoc } = useInstallProductDoc(); + const { status, isLoading: isStatusLoading } = useGetProductDocStatus(); + + useEffect(() => { + if (status) { + setState((prevState) => ({ + ...prevState, + isInstalled: status.overall === 'installed', + })); + } + }, [status]); + + const onClickInstall = useCallback(async () => { + setState((prevState) => ({ ...prevState, isInstalling: true })); + try { + await installProductDoc(); + setState({ isInstalled: true, isInstalling: false }); + } catch { + setState({ isInstalled: false, isInstalling: false }); + } + }, [installProductDoc]); + + const content = useMemo(() => { + if (isStatusLoading) { + return ; + } + if (isInstalling) { + return ( + + + {i18n.INSTALLING} + + ); + } + return ( + + + + {i18n.INSTALL} + + + + ); + }, [isInstalling, isStatusLoading, onClickInstall]); + + if (isInstalled) { + return null; + } + return ( + <> + + + {i18n.DESCRIPTION} + + + {content} + + + + ); +}); + +ProductDocumentationManagement.displayName = 'ProductDocumentationManagement'; diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/settings/product_documentation/translations.ts b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/settings/product_documentation/translations.ts new file mode 100644 index 0000000000000..196eef04a2fdf --- /dev/null +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/settings/product_documentation/translations.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const LABEL = i18n.translate('xpack.elasticAssistant.assistant.settings.productDocLabel', { + defaultMessage: 'Elastic documentation is not installed', +}); +export const DESCRIPTION = i18n.translate( + 'xpack.elasticAssistant.assistant.settings.productDocDescription', + { + defaultMessage: + 'The Elastic Documentation has been uninstalled. Please reinstall to ensure the most accurate results from the AI Assistant.', + } +); + +export const INSTALL = i18n.translate( + 'xpack.elasticAssistant.assistant.settings.installProductDocButtonLabel', + { defaultMessage: 'Install' } +); + +export const INSTALLING = i18n.translate( + 'xpack.elasticAssistant.assistant.settings.installingText', + { defaultMessage: 'Installing...' } +); diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant_context/index.tsx b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant_context/index.tsx index b6fa6a4859f41..ebf85e0f86a90 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant_context/index.tsx +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant_context/index.tsx @@ -15,6 +15,7 @@ import useSessionStorage from 'react-use/lib/useSessionStorage'; import type { DocLinksStart } from '@kbn/core-doc-links-browser'; import { AssistantFeatures, defaultAssistantFeatures } from '@kbn/elastic-assistant-common'; import { ChromeStart, NavigateToAppOptions, UserProfileService } from '@kbn/core/public'; +import type { ProductDocBasePluginStart } from '@kbn/product-doc-base-plugin/public'; import { useQuery } from '@tanstack/react-query'; import { updatePromptContexts } from './helpers'; import type { @@ -78,6 +79,7 @@ export interface AssistantProviderProps { title?: string; toasts?: IToasts; currentAppId: string; + productDocBase: ProductDocBasePluginStart; userProfileService: UserProfileService; chrome: ChromeStart; } @@ -131,6 +133,7 @@ export interface UseAssistantContext { unRegisterPromptContext: UnRegisterPromptContext; currentAppId: string; codeBlockRef: React.MutableRefObject<(codeBlock: string) => void>; + productDocBase: ProductDocBasePluginStart; userProfileService: UserProfileService; chrome: ChromeStart; } @@ -153,6 +156,7 @@ export const AssistantProvider: React.FC = ({ baseConversations, navigateToApp, nameSpace = DEFAULT_ASSISTANT_NAMESPACE, + productDocBase, title = DEFAULT_ASSISTANT_TITLE, toasts, currentAppId, @@ -291,6 +295,7 @@ export const AssistantProvider: React.FC = ({ promptContexts, navigateToApp, nameSpace, + productDocBase, registerPromptContext, selectedSettingsTab, // can be undefined from localStorage, if not defined, default to true @@ -331,6 +336,7 @@ export const AssistantProvider: React.FC = ({ promptContexts, navigateToApp, nameSpace, + productDocBase, registerPromptContext, selectedSettingsTab, localStorageStreaming, diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/knowledge_base/knowledge_base_settings_management/index.tsx b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/knowledge_base/knowledge_base_settings_management/index.tsx index 183e74a18247a..b47c7649dcefd 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/knowledge_base/knowledge_base_settings_management/index.tsx +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/knowledge_base/knowledge_base_settings_management/index.tsx @@ -31,6 +31,7 @@ import { import { css } from '@emotion/react'; import { DataViewsContract } from '@kbn/data-views-plugin/public'; import useAsync from 'react-use/lib/useAsync'; +import { ProductDocumentationManagement } from '../../assistant/settings/product_documentation'; import { KnowledgeBaseTour } from '../../tour/knowledge_base'; import { AlertsSettingsManagement } from '../../assistant/settings/alerts_settings/alerts_settings_management'; import { useKnowledgeBaseEntries } from '../../assistant/api/knowledge_base/entries/use_knowledge_base_entries'; @@ -332,6 +333,7 @@ export const KnowledgeBaseSettingsManagement: React.FC = React.memo(({ d return ( <> + = ({ navigateToApp={mockNavigateToApp} {...providerContext} currentAppId={'test'} + productDocBase={{ + installation: { getStatus: jest.fn(), install: jest.fn(), uninstall: jest.fn() }, + }} userProfileService={jest.fn() as unknown as UserProfileService} chrome={chrome} > diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/tsconfig.json b/x-pack/platform/packages/shared/kbn-elastic-assistant/tsconfig.json index c3513b4537f68..95c51c0d85119 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant/tsconfig.json +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/tsconfig.json @@ -37,5 +37,6 @@ "@kbn/core-chrome-browser-mocks", "@kbn/core-chrome-browser", "@kbn/ai-assistant-icon", + "@kbn/product-doc-base-plugin", ] } diff --git a/x-pack/solutions/security/packages/ecs_data_quality_dashboard/impl/data_quality_panel/mock/test_providers/test_providers.tsx b/x-pack/solutions/security/packages/ecs_data_quality_dashboard/impl/data_quality_panel/mock/test_providers/test_providers.tsx index 7165e699d059e..8aaf7012ebf17 100644 --- a/x-pack/solutions/security/packages/ecs_data_quality_dashboard/impl/data_quality_panel/mock/test_providers/test_providers.tsx +++ b/x-pack/solutions/security/packages/ecs_data_quality_dashboard/impl/data_quality_panel/mock/test_providers/test_providers.tsx @@ -88,6 +88,9 @@ const TestExternalProvidersComponent: React.FC = ({ http={mockHttp} baseConversations={{}} navigateToApp={mockNavigateToApp} + productDocBase={{ + installation: { getStatus: jest.fn(), install: jest.fn(), uninstall: jest.fn() }, + }} currentAppId={'securitySolutionUI'} userProfileService={jest.fn() as unknown as UserProfileService} chrome={chrome} diff --git a/x-pack/solutions/security/plugins/security_solution/kibana.jsonc b/x-pack/solutions/security/plugins/security_solution/kibana.jsonc index f672378c88df8..0763dafd23948 100644 --- a/x-pack/solutions/security/plugins/security_solution/kibana.jsonc +++ b/x-pack/solutions/security/plugins/security_solution/kibana.jsonc @@ -59,7 +59,8 @@ "charts", "entityManager", "inference", - "discoverShared" + "discoverShared", + "productDocBase" ], "optionalPlugins": [ "encryptedSavedObjects", diff --git a/x-pack/solutions/security/plugins/security_solution/public/assistant/provider.tsx b/x-pack/solutions/security/plugins/security_solution/public/assistant/provider.tsx index 910cd9c32515e..374ec85c4db65 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/assistant/provider.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/assistant/provider.tsx @@ -144,6 +144,7 @@ export const AssistantProvider: FC> = ({ children }) docLinks: { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION }, userProfile, chrome, + productDocBase, } = useKibana().services; let inferenceEnabled = false; @@ -235,6 +236,7 @@ export const AssistantProvider: FC> = ({ children }) http={http} inferenceEnabled={inferenceEnabled} navigateToApp={navigateToApp} + productDocBase={productDocBase} title={ASSISTANT_TITLE} toasts={toasts} currentAppId={currentAppId ?? 'securitySolutionUI'} diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/mock/mock_assistant_provider.tsx b/x-pack/solutions/security/plugins/security_solution/public/common/mock/mock_assistant_provider.tsx index 7bfc76bfb4880..c793f7722780a 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/mock/mock_assistant_provider.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/common/mock/mock_assistant_provider.tsx @@ -61,6 +61,9 @@ export const MockAssistantProviderComponent: React.FC = ({ navigateToApp={mockNavigateToApp} baseConversations={BASE_SECURITY_CONVERSATIONS} currentAppId={'test'} + productDocBase={{ + installation: { getStatus: jest.fn(), install: jest.fn(), uninstall: jest.fn() }, + }} userProfileService={mockUserProfileService} chrome={chrome} > diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/rules/rule_execution_status/rule_status_failed_callout.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/components/rules/rule_execution_status/rule_status_failed_callout.test.tsx index 036d7499aab72..0fa6ecacea9d3 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detections/components/rules/rule_execution_status/rule_status_failed_callout.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/rules/rule_execution_status/rule_status_failed_callout.test.tsx @@ -12,15 +12,10 @@ import { render } from '@testing-library/react'; import type { RuleExecutionStatus } from '../../../../../common/api/detection_engine/rule_monitoring'; import { RuleExecutionStatusEnum } from '../../../../../common/api/detection_engine/rule_monitoring'; import { RuleStatusFailedCallOut } from './rule_status_failed_callout'; -import { AssistantProvider } from '@kbn/elastic-assistant'; -import type { AssistantAvailability } from '@kbn/elastic-assistant'; -import { httpServiceMock } from '@kbn/core-http-browser-mocks'; -import { actionTypeRegistryMock } from '@kbn/triggers-actions-ui-plugin/public/application/action_type_registry.mock'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { BASE_SECURITY_CONVERSATIONS } from '../../../../assistant/content/conversations'; -import type { UserProfileService } from '@kbn/core-user-profile-browser'; import { chromeServiceMock } from '@kbn/core/public/mocks'; import { of } from 'rxjs'; +import { MockAssistantProviderComponent } from '../../../../common/mock/mock_assistant_provider'; jest.mock('../../../../common/lib/kibana'); @@ -28,18 +23,6 @@ const TEST_ID = 'ruleStatusFailedCallOut'; const DATE = '2022-01-27T15:03:31.176Z'; const MESSAGE = 'This rule is attempting to query data but...'; -const actionTypeRegistry = actionTypeRegistryMock.create(); -const mockGetComments = jest.fn(() => []); -const mockHttp = httpServiceMock.createStartContract({ basePath: '/test' }); -const mockNavigationToApp = jest.fn(); -const mockAssistantAvailability: AssistantAvailability = { - hasAssistantPrivilege: false, - hasConnectorsAllPrivilege: true, - hasConnectorsReadPrivilege: true, - hasUpdateAIAssistantAnonymization: true, - hasManageGlobalKnowledgeBase: true, - isAssistantEnabled: true, -}; const queryClient = new QueryClient({ defaultOptions: { queries: { @@ -58,25 +41,7 @@ const ContextWrapper: FC> = ({ children }) => { chrome.getChromeStyle$.mockReturnValue(of('classic')); return ( - - {children} - + {children} ); }; diff --git a/x-pack/solutions/security/plugins/security_solution/public/plugin_services.ts b/x-pack/solutions/security/plugins/security_solution/public/plugin_services.ts index cd066da31f549..24140efe59599 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/plugin_services.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/plugin_services.ts @@ -153,6 +153,7 @@ export class PluginServices { customDataService, timelineDataService, topValuesPopover: new TopValuesPopoverService(), + productDocBase: startPlugins.productDocBase, siemMigrations: await createSiemMigrationsService(coreStart, startPlugins), ...(params && { onAppLeave: params.onAppLeave, diff --git a/x-pack/solutions/security/plugins/security_solution/public/types.ts b/x-pack/solutions/security/plugins/security_solution/public/types.ts index b17ecf596f78f..896f928b4d4ac 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/types.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/types.ts @@ -21,6 +21,7 @@ import type { Storage } from '@kbn/kibana-utils-plugin/public'; import type { FleetStart } from '@kbn/fleet-plugin/public'; import type { PluginStart as ListsPluginStart } from '@kbn/lists-plugin/public'; import type { SpacesPluginStart } from '@kbn/spaces-plugin/public'; +import type { ProductDocBasePluginStart } from '@kbn/product-doc-base-plugin/public'; import type { TriggersAndActionsUIPublicPluginSetup as TriggersActionsSetup, TriggersAndActionsUIPublicPluginStart as TriggersActionsStart, @@ -161,6 +162,7 @@ export interface StartPlugins { core: CoreStart; integrationAssistant?: IntegrationAssistantPluginStart; serverless?: ServerlessPluginStart; + productDocBase: ProductDocBasePluginStart; } export interface StartPluginsDependencies extends StartPlugins { @@ -198,6 +200,7 @@ export type StartServices = CoreStart & topValuesPopover: TopValuesPopoverService; timelineDataService: DataPublicPluginStart; siemMigrations: SiemMigrationsService; + productDocBase: ProductDocBasePluginStart; }; export type StartRenderServices = Pick< diff --git a/x-pack/solutions/security/plugins/security_solution/tsconfig.json b/x-pack/solutions/security/plugins/security_solution/tsconfig.json index 81d4f62e39651..ba59f383ea6f6 100644 --- a/x-pack/solutions/security/plugins/security_solution/tsconfig.json +++ b/x-pack/solutions/security/plugins/security_solution/tsconfig.json @@ -225,7 +225,6 @@ "@kbn/core-saved-objects-server-mocks", "@kbn/core-security-server-mocks", "@kbn/serverless", - "@kbn/core-user-profile-browser", "@kbn/data-stream-adapter", "@kbn/core-lifecycle-server", "@kbn/core-user-profile-common", @@ -237,6 +236,7 @@ "@kbn/core-chrome-browser-mocks", "@kbn/ai-assistant-icon", "@kbn/llm-tasks-plugin", - "@kbn/charts-theme" + "@kbn/charts-theme", + "@kbn/product-doc-base-plugin" ] } From 8bb2da19ea4e22d279df355f620790080548da00 Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Fri, 10 Jan 2025 10:44:06 -0500 Subject: [PATCH 28/42] [Fleet] Split jest config public|common|server (#205798) --- .../shared/fleet/{ => common}/jest.config.js | 11 +++++------ .../plugins/shared/fleet/jest.config.dev.js | 17 +++++++++++++++++ .../shared/fleet/public/jest.config.js | 19 +++++++++++++++++++ .../shared/fleet/scripts/jest.config.js | 19 +++++++++++++++++++ .../shared/fleet/server/jest.config.js | 16 ++++++++++++++++ 5 files changed, 76 insertions(+), 6 deletions(-) rename x-pack/platform/plugins/shared/fleet/{ => common}/jest.config.js (58%) create mode 100644 x-pack/platform/plugins/shared/fleet/jest.config.dev.js create mode 100644 x-pack/platform/plugins/shared/fleet/public/jest.config.js create mode 100644 x-pack/platform/plugins/shared/fleet/scripts/jest.config.js create mode 100644 x-pack/platform/plugins/shared/fleet/server/jest.config.js diff --git a/x-pack/platform/plugins/shared/fleet/jest.config.js b/x-pack/platform/plugins/shared/fleet/common/jest.config.js similarity index 58% rename from x-pack/platform/plugins/shared/fleet/jest.config.js rename to x-pack/platform/plugins/shared/fleet/common/jest.config.js index dbdcca9bb0570..0711e6f7f317e 100644 --- a/x-pack/platform/plugins/shared/fleet/jest.config.js +++ b/x-pack/platform/plugins/shared/fleet/common/jest.config.js @@ -7,14 +7,13 @@ module.exports = { preset: '@kbn/test', - rootDir: '../../../../..', - roots: ['/x-pack/platform/plugins/shared/fleet'], + rootDir: '../../../../../..', + roots: ['/x-pack/platform/plugins/shared/fleet/common'], transform: { '^.+\\.stories\\.tsx?$': '@storybook/addon-storyshots/injectFileName', }, - coverageDirectory: '/target/kibana-coverage/jest/x-pack/platform/plugins/shared/fleet', + coverageDirectory: + '/target/kibana-coverage/jest/x-pack/platform/plugins/shared/fleet/common', coverageReporters: ['text', 'html'], - collectCoverageFrom: [ - '/x-pack/platform/plugins/shared/fleet/{common,public,server}/**/*.{ts,tsx}', - ], + collectCoverageFrom: ['/x-pack/platform/plugins/shared/fleet/common/**/*.{ts,tsx}'], }; diff --git a/x-pack/platform/plugins/shared/fleet/jest.config.dev.js b/x-pack/platform/plugins/shared/fleet/jest.config.dev.js new file mode 100644 index 0000000000000..ff985be3a5d6e --- /dev/null +++ b/x-pack/platform/plugins/shared/fleet/jest.config.dev.js @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +module.exports = { + preset: '@kbn/test/jest', + rootDir: '../../../../..', + roots: ['/x-pack/platform/plugins/shared/fleet'], + projects: [ + '/x-pack/platform/plugins/shared/fleet/common/*/jest.config.js', + '/x-pack/platform/plugins/shared/fleet/server/*/jest.config.js', + '/x-pack/platform/plugins/shared/fleet/public/*/jest.config.js', + ], +}; diff --git a/x-pack/platform/plugins/shared/fleet/public/jest.config.js b/x-pack/platform/plugins/shared/fleet/public/jest.config.js new file mode 100644 index 0000000000000..bb555a56943b8 --- /dev/null +++ b/x-pack/platform/plugins/shared/fleet/public/jest.config.js @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../../../../..', + roots: ['/x-pack/platform/plugins/shared/fleet/public'], + transform: { + '^.+\\.stories\\.tsx?$': '@storybook/addon-storyshots/injectFileName', + }, + coverageDirectory: + '/target/kibana-coverage/jest/x-pack/platform/plugins/shared/fleet/public', + coverageReporters: ['text', 'html'], + collectCoverageFrom: ['/x-pack/platform/plugins/shared/fleet/public/**/*.{ts,tsx}'], +}; diff --git a/x-pack/platform/plugins/shared/fleet/scripts/jest.config.js b/x-pack/platform/plugins/shared/fleet/scripts/jest.config.js new file mode 100644 index 0000000000000..e68dcd69cc3e3 --- /dev/null +++ b/x-pack/platform/plugins/shared/fleet/scripts/jest.config.js @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../../../../..', + roots: ['/x-pack/platform/plugins/shared/fleet/scripts'], + transform: { + '^.+\\.stories\\.tsx?$': '@storybook/addon-storyshots/injectFileName', + }, + coverageDirectory: + '/target/kibana-coverage/jest/x-pack/platform/plugins/shared/fleet/scripts', + coverageReporters: ['text', 'html'], + collectCoverageFrom: ['/x-pack/platform/plugins/shared/fleet/scripts/**/*.{ts,tsx}'], +}; diff --git a/x-pack/platform/plugins/shared/fleet/server/jest.config.js b/x-pack/platform/plugins/shared/fleet/server/jest.config.js new file mode 100644 index 0000000000000..648eb2d326e44 --- /dev/null +++ b/x-pack/platform/plugins/shared/fleet/server/jest.config.js @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../../../../..', + roots: ['/x-pack/platform/plugins/shared/fleet/server'], + coverageDirectory: + '/target/kibana-coverage/jest/x-pack/platform/plugins/shared/fleet/server', + coverageReporters: ['text', 'html'], + collectCoverageFrom: ['/x-pack/platform/plugins/shared/fleet/{server}/**/*.{ts,tsx}'], +}; From eddbbb1895920c36e7687854a10d598f913e206c Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Fri, 10 Jan 2025 10:44:33 -0500 Subject: [PATCH 29/42] [Fleet] Retry on conflict when updating agent space (#205964) --- .../server/services/spaces/agent_policy.ts | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/x-pack/platform/plugins/shared/fleet/server/services/spaces/agent_policy.ts b/x-pack/platform/plugins/shared/fleet/server/services/spaces/agent_policy.ts index dce40f4cc3ff4..ed3bfa2619134 100644 --- a/x-pack/platform/plugins/shared/fleet/server/services/spaces/agent_policy.ts +++ b/x-pack/platform/plugins/shared/fleet/server/services/spaces/agent_policy.ts @@ -154,21 +154,6 @@ export async function updateAgentPolicySpaces({ ignore_unavailable: true, refresh: true, }); - await esClient.updateByQuery({ - index: AGENTS_INDEX, - query: { - bool: { - must: { - terms: { - policy_id: [agentPolicyId], - }, - }, - }, - }, - script: `ctx._source.namespaces = [${newSpaceIds.map((spaceId) => `"${spaceId}"`).join(',')}]`, - ignore_unavailable: true, - refresh: true, - }); const agentIndexExists = await esClient.indices.exists({ index: AGENTS_INDEX, @@ -195,6 +180,21 @@ export async function updateAgentPolicySpaces({ break; } + const agentBulkRes = await esClient.bulk({ + operations: agents.flatMap(({ id }) => [ + { update: { _id: id, _index: AGENTS_INDEX, retry_on_conflict: 5 } }, + { doc: { namespaces: newSpaceIds } }, + ]), + refresh: 'wait_for', + index: AGENTS_INDEX, + }); + + for (const item of agentBulkRes.items) { + if (item.update?.error) { + throw item.update?.error; + } + } + const lastAgent = agents[agents.length - 1]; searchAfter = lastAgent.sort; From 03e299f202aeacb7b872fac44a7330f4708b9797 Mon Sep 17 00:00:00 2001 From: Nick Partridge Date: Fri, 10 Jan 2025 09:47:36 -0600 Subject: [PATCH 30/42] [Infra] Fix types in alerting `Threshold` (#206133) ## Summary This fixes bad typings for `chartProps` from https://github.com/elastic/kibana/pull/202405. At some point we started passing the eui theme to the chart props in `x-pack/solutions/observability/plugins/infra/public/alerting/log_threshold/components/alert_details_app_section/index.tsx`. The `chartProps.theme` is meant only to be the chart `PartialTheme` which can override settings from the `baseTheme`. --- .../infra/public/alerting/common/components/threshold.tsx | 6 +++--- .../components/alert_details_app_section/index.tsx | 6 ++---- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/x-pack/solutions/observability/plugins/infra/public/alerting/common/components/threshold.tsx b/x-pack/solutions/observability/plugins/infra/public/alerting/common/components/threshold.tsx index fd92be789f47d..91b1d249cfbf3 100644 --- a/x-pack/solutions/observability/plugins/infra/public/alerting/common/components/threshold.tsx +++ b/x-pack/solutions/observability/plugins/infra/public/alerting/common/components/threshold.tsx @@ -7,13 +7,13 @@ import React from 'react'; import { Chart, Metric, Settings } from '@elastic/charts'; -import { EuiIcon, EuiPanel, type UseEuiTheme, useEuiTheme } from '@elastic/eui'; +import { EuiIcon, EuiPanel, useEuiTheme } from '@elastic/eui'; import type { PartialTheme, Theme } from '@elastic/charts'; import { i18n } from '@kbn/i18n'; import type { COMPARATORS } from '@kbn/alerting-comparators'; export interface ChartProps { - theme?: UseEuiTheme<{}>; + theme?: PartialTheme; baseTheme: Theme; } @@ -57,7 +57,7 @@ export const Threshold = ({ data-test-subj={`threshold-${thresholds.join('-')}-${value}`} > - + String(threshold); const AlertDetailsAppSection = ({ rule, alert }: AlertDetailsAppSectionProps) => { const { logsShared } = useKibanaContextForPlugin().services; - const theme = useTheme(); const baseTheme = useElasticChartsTheme(); const timeRange = getPaddedAlertTimeRange(alert.fields[ALERT_START]!, alert.fields[ALERT_END]); const alertEnd = alert.fields[ALERT_END] ? moment(alert.fields[ALERT_END]).valueOf() : undefined; @@ -94,7 +92,7 @@ const AlertDetailsAppSection = ({ rule, alert }: AlertDetailsAppSectionProps) => Date: Fri, 10 Jan 2025 16:48:13 +0100 Subject: [PATCH 31/42] =?UTF-8?q?=F0=9F=8C=8A=20Fix=20ascendants=20check?= =?UTF-8?q?=20(#206080)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The id->name refactoring didn't catch the check for ascendants which was still looking for id. This PR fixes this --- .../streams/server/lib/streams/stream_crud.ts | 36 +++++++++---------- .../streams/server/routes/streams/read.ts | 2 +- .../routes/streams/schema/unmapped_fields.ts | 2 +- 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/x-pack/solutions/observability/plugins/streams/server/lib/streams/stream_crud.ts b/x-pack/solutions/observability/plugins/streams/server/lib/streams/stream_crud.ts index 16b07250d4546..cf42eedd31fd2 100644 --- a/x-pack/solutions/observability/plugins/streams/server/lib/streams/stream_crud.ts +++ b/x-pack/solutions/observability/plugins/streams/server/lib/streams/stream_crud.ts @@ -402,7 +402,7 @@ async function getUnmanagedElasticsearchAssets({ } interface ReadAncestorsParams extends BaseParams { - id: string; + name: string; } export interface ReadAncestorsResponse { @@ -410,10 +410,10 @@ export interface ReadAncestorsResponse { } export async function readAncestors({ - id, + name, scopedClusterClient, }: ReadAncestorsParams): Promise<{ ancestors: WiredStreamDefinition[] }> { - const ancestorIds = getAncestors(id); + const ancestorIds = getAncestors(name); return { ancestors: await Promise.all( @@ -430,10 +430,10 @@ export async function readAncestors({ } interface ReadDescendantsParams extends BaseParams { - id: string; + name: string; } -export async function readDescendants({ id, scopedClusterClient }: ReadDescendantsParams) { +export async function readDescendants({ name, scopedClusterClient }: ReadDescendantsParams) { const response = await scopedClusterClient.asInternalUser.search({ index: STREAMS_INDEX, size: 10000, @@ -442,12 +442,12 @@ export async function readDescendants({ id, scopedClusterClient }: ReadDescendan bool: { filter: { prefix: { - id, + name, }, }, must_not: { term: { - id, + name, }, }, }, @@ -459,25 +459,25 @@ export async function readDescendants({ id, scopedClusterClient }: ReadDescendan export async function validateAncestorFields( scopedClusterClient: IScopedClusterClient, - id: string, + name: string, fields: FieldDefinition ) { const { ancestors } = await readAncestors({ - id, + name, scopedClusterClient, }); for (const ancestor of ancestors) { - for (const name in fields) { + for (const fieldName in fields) { if ( - Object.hasOwn(fields, name) && + Object.hasOwn(fields, fieldName) && isWiredReadStream(ancestor) && Object.entries(ancestor.stream.ingest.wired.fields).some( ([ancestorFieldName, attr]) => - attr.type !== fields[name].type && ancestorFieldName === name + attr.type !== fields[fieldName].type && ancestorFieldName === fieldName ) ) { throw new MalformedFields( - `Field ${name} is already defined with incompatible type in the parent stream ${ancestor.name}` + `Field ${fieldName} is already defined with incompatible type in the parent stream ${ancestor.name}` ); } } @@ -486,20 +486,20 @@ export async function validateAncestorFields( export async function validateDescendantFields( scopedClusterClient: IScopedClusterClient, - id: string, + name: string, fields: FieldDefinition ) { const descendants = await readDescendants({ - id, + name, scopedClusterClient, }); for (const descendant of descendants) { - for (const name in fields) { + for (const fieldName in fields) { if ( - Object.hasOwn(fields, name) && + Object.hasOwn(fields, fieldName) && Object.entries(descendant.stream.ingest.wired.fields).some( ([descendantFieldName, attr]) => - attr.type !== fields[name].type && descendantFieldName === name + attr.type !== fields[fieldName].type && descendantFieldName === fieldName ) ) { throw new MalformedFields( diff --git a/x-pack/solutions/observability/plugins/streams/server/routes/streams/read.ts b/x-pack/solutions/observability/plugins/streams/server/routes/streams/read.ts index cadaaed2a6bdc..c08cfbf9cec4a 100644 --- a/x-pack/solutions/observability/plugins/streams/server/routes/streams/read.ts +++ b/x-pack/solutions/observability/plugins/streams/server/routes/streams/read.ts @@ -54,7 +54,7 @@ export const readStreamRoute = createServerRoute({ } const { ancestors } = await readAncestors({ - id: streamEntity.name, + name: streamEntity.name, scopedClusterClient, }); diff --git a/x-pack/solutions/observability/plugins/streams/server/routes/streams/schema/unmapped_fields.ts b/x-pack/solutions/observability/plugins/streams/server/routes/streams/schema/unmapped_fields.ts index e069245003304..725175b6af01c 100644 --- a/x-pack/solutions/observability/plugins/streams/server/routes/streams/schema/unmapped_fields.ts +++ b/x-pack/solutions/observability/plugins/streams/server/routes/streams/schema/unmapped_fields.ts @@ -78,7 +78,7 @@ export const unmappedFieldsRoute = createServerRoute({ } const { ancestors } = await readAncestors({ - id: params.path.id, + name: params.path.id, scopedClusterClient, }); From 55390001adf8ea1eb1f50d46a4a8bb925a8a33d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patryk=20Kopyci=C5=84ski?= Date: Fri, 10 Jan 2025 16:51:38 +0100 Subject: [PATCH 32/42] [Security Assistant] Migrate semantic_text to use highlighter instead of inner_hits (#204962) ## Summary Switch to use https://github.com/elastic/elasticsearch/pull/118064 when retrieving Knowledge base Index entry docs Followed testing instructions from https://github.com/elastic/kibana/pull/198020 Results: Zrzut ekranu 2024-12-19 o 16 32 28 Zrzut ekranu 2024-12-19 o 16 32 38 Zrzut ekranu 2024-12-19 o 16 32 43 Zrzut ekranu 2024-12-19 o 16 32 47 Zrzut ekranu 2024-12-19 o 16 32 50 --- .../knowledge_base/helpers.test.tsx | 14 +----- .../knowledge_base/helpers.ts | 48 +++++++------------ .../knowledge_base/index.ts | 2 - 3 files changed, 19 insertions(+), 45 deletions(-) diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/helpers.test.tsx b/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/helpers.test.tsx index 69b142bdac6be..275e2be56e39b 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/helpers.test.tsx +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/helpers.test.tsx @@ -159,7 +159,6 @@ describe('getStructuredToolForIndexEntry', () => { indexEntry: mockIndexEntry, esClient: mockEsClient, logger: mockLogger, - elserId: 'elser123', }); expect(tool).toBeInstanceOf(DynamicStructuredTool); @@ -181,15 +180,8 @@ describe('getStructuredToolForIndexEntry', () => { field1: 'value1', field2: 2, }, - inner_hits: { - 'test.test': { - hits: { - hits: [ - { _source: { text: 'Inner text 1' } }, - { _source: { text: 'Inner text 2' } }, - ], - }, - }, + highlight: { + test: ['Inner text 1', 'Inner text 2'], }, }, ], @@ -202,7 +194,6 @@ describe('getStructuredToolForIndexEntry', () => { indexEntry: mockIndexEntry, esClient: mockEsClient, logger: mockLogger, - elserId: 'elser123', }); const input = { query: 'testQuery', field1: 'value1', field2: 2 }; @@ -220,7 +211,6 @@ describe('getStructuredToolForIndexEntry', () => { indexEntry: mockIndexEntry, esClient: mockEsClient, logger: mockLogger, - elserId: 'elser123', }); const input = { query: 'testQuery', field1: 'value1', field2: 2 }; diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/helpers.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/helpers.ts index a0d3afb355b4b..a7c30690fdba7 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/helpers.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/helpers.ts @@ -6,7 +6,6 @@ */ import { z } from '@kbn/zod'; -import { get } from 'lodash'; import { DynamicStructuredTool } from '@langchain/core/tools'; import { errors } from '@elastic/elasticsearch'; import { QueryDslQueryContainer, SearchRequest } from '@elastic/elasticsearch/lib/api/types'; @@ -140,12 +139,10 @@ export const getStructuredToolForIndexEntry = ({ indexEntry, esClient, logger, - elserId, }: { indexEntry: IndexEntry; esClient: ElasticsearchClient; logger: Logger; - elserId: string; }): DynamicStructuredTool => { const inputSchema = indexEntry.inputSchema?.reduce((prev, input) => { const fieldType = @@ -182,28 +179,27 @@ export const getStructuredToolForIndexEntry = ({ const params: SearchRequest = { index: indexEntry.index, size: 10, - retriever: { - standard: { - query: { - nested: { - path: `${indexEntry.field}.inference.chunks`, - query: { - sparse_vector: { - inference_id: elserId, - field: `${indexEntry.field}.inference.chunks.embeddings`, - query: input.query, - }, - }, - inner_hits: { - size: 2, - name: `${indexEntry.name}.${indexEntry.field}`, - _source: [`${indexEntry.field}.inference.chunks.text`], + query: { + bool: { + must: [ + { + semantic: { + field: indexEntry.field, + query: input.query, }, }, - }, + ], filter, }, }, + highlight: { + fields: { + [indexEntry.field]: { + type: 'semantic', + number_of_fragments: 2, + }, + }, + }, }; try { @@ -217,18 +213,8 @@ export const getStructuredToolForIndexEntry = ({ }, {}); } - // We want to send relevant inner hits (chunks) to the LLM as a context - const innerHitPath = `${indexEntry.name}.${indexEntry.field}`; - if (hit.inner_hits?.[innerHitPath]) { - return { - text: hit.inner_hits[innerHitPath].hits.hits - .map((innerHit) => innerHit._source.text) - .join('\n --- \n'), - }; - } - return { - text: get(hit._source, `${indexEntry.field}.inference.chunks[0].text`), + text: hit.highlight?.[indexEntry.field].join('\n --- \n'), }; }); diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/index.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/index.ts index 0065066455475..89a2b834bf906 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/index.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/index.ts @@ -652,7 +652,6 @@ export class AIAssistantKnowledgeBaseDataClient extends AIAssistantDataClient { } try { - const elserId = ASSISTANT_ELSER_INFERENCE_ID; const userFilter = getKBUserFilter(user); const results = await this.findDocuments({ // Note: This is a magic number to set some upward bound as to not blow the context with too @@ -682,7 +681,6 @@ export class AIAssistantKnowledgeBaseDataClient extends AIAssistantDataClient { indexEntry, esClient, logger: this.options.logger, - elserId, }); }) ); From 473eb721bcd12503ea7a6db5a365a3594bac013c Mon Sep 17 00:00:00 2001 From: Lola Date: Fri, 10 Jan 2025 10:55:06 -0500 Subject: [PATCH 33/42] [Cloud Security] Feature Flag Support for Cloud Security Posture Plugin (#205438) ## Summary Summarize your PR. If it involves visual changes include a screenshot or gif. ## Changes * Adds `enableExperimental` to server `configSchema` * Makes feature flags configurable via `xpack.cloudSecurityPosture.enableExperimental` in `kibana.dev.yml` * Implements `ExperimentFeatureService.get()` for accessing feature flags * Add passing `initliaterContext` to plugin in order to access our plugin config ## Benefits * Avoids circular dependency with Security Solution `useIsExperimentalFeatureEnabled` and prop drilling feature flags from Fleet plugin `PackagePolicyReplaceDefineStepExtensionComponentProps` * Provides server-side configuration support * Enables pre-release feature testing * Creates centralized feature flag management This allows controlled testing of new features before release through configuration rather than code changes. --------- Co-authored-by: Elastic Machine --- packages/kbn-optimizer/limits.yml | 2 +- .../test_suites/core_plugins/rendering.ts | 1 + .../common/experimental_features.ts | 60 +++++++++++++++++++ .../common/experimental_features_service.ts | 30 ++++++++++ .../cloud_security_posture/public/index.ts | 4 +- .../cloud_security_posture/public/plugin.tsx | 20 ++++++- .../cloud_security_posture/server/config.ts | 18 ++++++ .../create_indices/create_indices.test.ts | 6 +- 8 files changed, 134 insertions(+), 7 deletions(-) create mode 100644 x-pack/solutions/security/plugins/cloud_security_posture/common/experimental_features.ts create mode 100644 x-pack/solutions/security/plugins/cloud_security_posture/public/common/experimental_features_service.ts diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index cadfe863b1ab3..43cefb1f22e0b 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -15,7 +15,7 @@ pageLoadAssetSize: cloudExperiments: 109746 cloudFullStory: 18493 cloudLinks: 55984 - cloudSecurityPosture: 19109 + cloudSecurityPosture: 19270 console: 46091 contentManagement: 16254 controls: 60000 diff --git a/test/plugin_functional/test_suites/core_plugins/rendering.ts b/test/plugin_functional/test_suites/core_plugins/rendering.ts index 1bda105ba07a6..8e6921407a7bf 100644 --- a/test/plugin_functional/test_suites/core_plugins/rendering.ts +++ b/test/plugin_functional/test_suites/core_plugins/rendering.ts @@ -250,6 +250,7 @@ export default function ({ getService }: PluginFunctionalProviderContext) { 'xpack.cloud.performance_url (string?)', 'xpack.cloud.users_and_roles_url (string?)', 'xpack.cloud.projects_url (string?|never)', + 'xpack.cloudSecurityPosture.enableExperimental (array?)', // can't be used to infer urls or customer id from the outside 'xpack.cloud.serverless.project_id (string?)', 'xpack.cloud.serverless.project_name (string?)', diff --git a/x-pack/solutions/security/plugins/cloud_security_posture/common/experimental_features.ts b/x-pack/solutions/security/plugins/cloud_security_posture/common/experimental_features.ts new file mode 100644 index 0000000000000..f9cc3753ff7de --- /dev/null +++ b/x-pack/solutions/security/plugins/cloud_security_posture/common/experimental_features.ts @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export interface CSPUIConfigType { + enableExperimental: string[]; +} + +export type ExperimentalFeatures = { [K in keyof typeof allowedExperimentalValues]: boolean }; + +/** + * A list of allowed values that can be used in `xpack.cloud_security_posture.enableExperimental`. + * This object is then used to validate and parse the value entered. + */ +export const allowedExperimentalValues = Object.freeze({ + /** + * Enables cloud Connectors for Cloud Security Posture + */ + cloudConnectorsEnabled: false, +}); + +type ExperimentalConfigKeys = Array; +type Mutable = { -readonly [P in keyof T]: T[P] }; + +const allowedKeys = Object.keys(allowedExperimentalValues) as Readonly; + +/** + * Parses the string value used in `xpack.cloud_security_posture.enableExperimental` kibana configuration, + * which should be a string of values delimited by a comma (`,`) + * + * @param configValue + * @throws SecuritySolutionInvalidExperimentalValue + */ +export const parseExperimentalConfigValue = ( + configValue: string[] +): { features: ExperimentalFeatures; invalid: string[] } => { + const enabledFeatures: Mutable> = {}; + const invalidKeys: string[] = []; + + for (const value of configValue) { + if (!allowedKeys.includes(value as keyof ExperimentalFeatures)) { + invalidKeys.push(value); + } else { + enabledFeatures[value as keyof ExperimentalFeatures] = true; + } + } + + return { + features: { + ...allowedExperimentalValues, + ...enabledFeatures, + }, + invalid: invalidKeys, + }; +}; + +export const getExperimentalAllowedValues = (): string[] => [...allowedKeys]; diff --git a/x-pack/solutions/security/plugins/cloud_security_posture/public/common/experimental_features_service.ts b/x-pack/solutions/security/plugins/cloud_security_posture/public/common/experimental_features_service.ts new file mode 100644 index 0000000000000..3112f7ab78b67 --- /dev/null +++ b/x-pack/solutions/security/plugins/cloud_security_posture/public/common/experimental_features_service.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { ExperimentalFeatures } from '../../common/experimental_features'; + +export class ExperimentalFeaturesService { + private static experimentalFeatures?: ExperimentalFeatures; + + public static init({ experimentalFeatures }: { experimentalFeatures: ExperimentalFeatures }) { + this.experimentalFeatures = experimentalFeatures; + } + + public static get(): ExperimentalFeatures { + if (!this.experimentalFeatures) { + this.throwUninitializedError(); + } + + return this.experimentalFeatures; + } + + private static throwUninitializedError(): never { + throw new Error( + 'Technical preview features services not initialized - are you trying to import this module from outside of the Security Solution app?' + ); + } +} diff --git a/x-pack/solutions/security/plugins/cloud_security_posture/public/index.ts b/x-pack/solutions/security/plugins/cloud_security_posture/public/index.ts index b7b9e2ba48fed..9355c51b64662 100755 --- a/x-pack/solutions/security/plugins/cloud_security_posture/public/index.ts +++ b/x-pack/solutions/security/plugins/cloud_security_posture/public/index.ts @@ -5,6 +5,7 @@ * 2.0. */ +import type { PluginInitializerContext } from '@kbn/core/public'; import { CspPlugin } from './plugin'; export type { CspSecuritySolutionContext } from './types'; export type { CloudSecurityPosturePageId } from './common/navigation/types'; @@ -12,4 +13,5 @@ export { getSecuritySolutionLink } from './common/navigation/security_solution_l export type { CspClientPluginSetup, CspClientPluginStart } from './types'; -export const plugin = () => new CspPlugin(); +export const plugin = (initializerContext: PluginInitializerContext) => + new CspPlugin(initializerContext); diff --git a/x-pack/solutions/security/plugins/cloud_security_posture/public/plugin.tsx b/x-pack/solutions/security/plugins/cloud_security_posture/public/plugin.tsx index b66e7a2b62f2e..e04bdbaef3397 100755 --- a/x-pack/solutions/security/plugins/cloud_security_posture/public/plugin.tsx +++ b/x-pack/solutions/security/plugins/cloud_security_posture/public/plugin.tsx @@ -5,7 +5,7 @@ * 2.0. */ import React, { lazy, Suspense } from 'react'; -import type { CoreSetup, CoreStart, Plugin } from '@kbn/core/public'; +import type { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from '@kbn/core/public'; import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import { Storage } from '@kbn/kibana-utils-plugin/public'; import { RedirectAppLinks } from '@kbn/shared-ux-link-redirect-app'; @@ -16,6 +16,12 @@ import type { CspRouterProps } from './application/csp_router'; import type { CspClientPluginSetup, CspClientPluginStart, CspClientPluginSetupDeps } from './types'; import { CLOUD_SECURITY_POSTURE_PACKAGE_NAME } from '../common/constants'; import { SetupContext } from './application/setup_context'; +import { + type CSPUIConfigType, + type ExperimentalFeatures, + parseExperimentalConfigValue, +} from '../common/experimental_features'; +import { ExperimentalFeaturesService } from './common/experimental_features_service'; const LazyCspPolicyTemplateForm = lazy( () => import('./components/fleet_extensions/policy_template_form') @@ -42,13 +48,22 @@ export class CspPlugin > { private isCloudEnabled?: boolean; + private config: CSPUIConfigType; + private experimentalFeatures: ExperimentalFeatures; + + constructor(private readonly initializerContext: PluginInitializerContext) { + this.config = this.initializerContext.config.get(); + + this.experimentalFeatures = parseExperimentalConfigValue( + this.config.enableExperimental || [] + )?.features; + } public setup( _core: CoreSetup, plugins: CspClientPluginSetupDeps ): CspClientPluginSetup { this.isCloudEnabled = plugins.cloud.isCloudEnabled; - if (plugins.usageCollection) uiMetricService.setup(plugins.usageCollection); // Return methods that should be available to other plugins @@ -56,6 +71,7 @@ export class CspPlugin } public start(core: CoreStart, plugins: CspClientPluginStartDeps): CspClientPluginStart { + ExperimentalFeaturesService.init({ experimentalFeatures: this.experimentalFeatures }); plugins.fleet.registerExtension({ package: CLOUD_SECURITY_POSTURE_PACKAGE_NAME, view: 'package-policy-replace-define-step', diff --git a/x-pack/solutions/security/plugins/cloud_security_posture/server/config.ts b/x-pack/solutions/security/plugins/cloud_security_posture/server/config.ts index 735cdb3cbe00f..898c581874887 100644 --- a/x-pack/solutions/security/plugins/cloud_security_posture/server/config.ts +++ b/x-pack/solutions/security/plugins/cloud_security_posture/server/config.ts @@ -18,10 +18,28 @@ const configSchema = schema.object({ options: { defaultValue: schema.contextRef('serverless') }, }), }), + /** + * For internal use. A list of string values (comma delimited) that will enable experimental + * type of functionality that is not yet released. Valid values for this settings need to + * be defined in: + * `x-pack/solutions/security/plugins/cloud_security_posture/common/experimental_features.ts` + * under the `allowedExperimentalValues` object + * + * @example + * xpack.cloudSecurityPosture.enableExperimental: + * - newFeatureA + * - newFeatureB + */ + enableExperimental: schema.arrayOf(schema.string(), { + defaultValue: () => [], + }), }); export type CloudSecurityPostureConfig = TypeOf; export const config: PluginConfigDescriptor = { schema: configSchema, + exposeToBrowser: { + enableExperimental: true, + }, }; diff --git a/x-pack/solutions/security/plugins/cloud_security_posture/server/create_indices/create_indices.test.ts b/x-pack/solutions/security/plugins/cloud_security_posture/server/create_indices/create_indices.test.ts index c7f500d891c1b..e9101880fb80a 100644 --- a/x-pack/solutions/security/plugins/cloud_security_posture/server/create_indices/create_indices.test.ts +++ b/x-pack/solutions/security/plugins/cloud_security_posture/server/create_indices/create_indices.test.ts @@ -63,7 +63,7 @@ describe('createBenchmarkScoreIndex', () => { it('should create index template the correct index patter, index name and default ingest pipeline but without lifecycle in serverless', async () => { await createBenchmarkScoreIndex( mockEsClient, - { serverless: { enabled: true }, enabled: true }, + { serverless: { enabled: true }, enabled: true, enableExperimental: [] }, logger ); expect(mockEsClient.indices.putIndexTemplate).toHaveBeenCalledTimes(1); @@ -87,7 +87,7 @@ describe('createBenchmarkScoreIndex', () => { await createBenchmarkScoreIndex( mockEsClient, - { serverless: { enabled: true }, enabled: true }, + { serverless: { enabled: true }, enabled: true, enableExperimental: [] }, logger ); expect(mockEsClient.indices.create).toHaveBeenCalledTimes(1); @@ -102,7 +102,7 @@ describe('createBenchmarkScoreIndex', () => { await createBenchmarkScoreIndex( mockEsClient, - { serverless: { enabled: true }, enabled: true }, + { serverless: { enabled: true }, enabled: true, enableExperimental: [] }, logger ); expect(mockEsClient.indices.create).toHaveBeenCalledTimes(0); From b37ec3ce208e7e8d07688abb3389d181bbdbe113 Mon Sep 17 00:00:00 2001 From: Tomasz Ciecierski Date: Fri, 10 Jan 2025 16:56:49 +0100 Subject: [PATCH 34/42] [EDR Workflows] Hardcode virtual box url (#206235) --- .../pull_request/security_solution/defend_workflows.yml | 2 +- .../scripts/endpoint/common/vagrant/Vagrantfile | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.buildkite/pipelines/pull_request/security_solution/defend_workflows.yml b/.buildkite/pipelines/pull_request/security_solution/defend_workflows.yml index 7cffb9017b527..a8ac3e742c23c 100644 --- a/.buildkite/pipelines/pull_request/security_solution/defend_workflows.yml +++ b/.buildkite/pipelines/pull_request/security_solution/defend_workflows.yml @@ -37,4 +37,4 @@ steps: retry: automatic: - exit_status: '-1' - limit: 1 \ No newline at end of file + limit: 1 diff --git a/x-pack/solutions/security/plugins/security_solution/scripts/endpoint/common/vagrant/Vagrantfile b/x-pack/solutions/security/plugins/security_solution/scripts/endpoint/common/vagrant/Vagrantfile index 6f08c6265ecd7..b9d34d6ab3a2f 100644 --- a/x-pack/solutions/security/plugins/security_solution/scripts/endpoint/common/vagrant/Vagrantfile +++ b/x-pack/solutions/security/plugins/security_solution/scripts/endpoint/common/vagrant/Vagrantfile @@ -21,6 +21,7 @@ Vagrant.configure("2") do |config| config.vm.provider :vmware_desktop do |v, override| override.vm.box = "starboard/ubuntu-arm64-20.04.5" override.vm.box_version = "20221120.20.40.0" + config.vm.box_url = "https://cloud-images.ubuntu.com/jammy/current/jammy-server-cloudimg-amd64-vagrant.box" override.vm.box_download_insecure = true override.vm.network "private_network", type: "dhcp" From df495ce532e4b478984830d533dac238bfd15444 Mon Sep 17 00:00:00 2001 From: Matthias Wilhelm Date: Fri, 10 Jan 2025 17:06:45 +0100 Subject: [PATCH 35/42] [Discover][ES]QL] Ensure the same time range is being used for documents and histogram (#204694) Fixing the case a relative time range is set (for example "Last 15 minutes") and the time range goes out of sync for table and histogram requests on Discover in ES|QL mode. Fixes a redundant ES|QL request for histogram data, with different timeranges, when the timerange is changed. --- .../layout/discover_documents.test.tsx | 11 ++++ .../components/layout/discover_documents.tsx | 10 +--- .../layout/discover_histogram_layout.test.tsx | 28 +++++----- .../layout/discover_layout.test.tsx | 4 ++ .../layout/use_discover_histogram.test.tsx | 17 ++++++ .../layout/use_discover_histogram.ts | 9 +--- .../main/data_fetching/fetch_all.test.ts | 3 ++ .../main/data_fetching/fetch_all.ts | 2 + .../main/data_fetching/fetch_esql.test.ts | 43 +++++++++++---- .../main/data_fetching/fetch_esql.ts | 52 ++++++++++++++----- .../data_fetching/update_search_source.ts | 10 ++-- .../discover_data_state_container.ts | 5 ++ .../discover_internal_state_container.ts | 17 +++++- .../public/embeddable/initialize_fetch.ts | 2 +- .../apps/discover/group3/_request_counts.ts | 16 +++--- 15 files changed, 160 insertions(+), 69 deletions(-) diff --git a/src/platform/plugins/shared/discover/public/application/main/components/layout/discover_documents.test.tsx b/src/platform/plugins/shared/discover/public/application/main/components/layout/discover_documents.test.tsx index 5ebe81da80a79..27bdcd46c3eb0 100644 --- a/src/platform/plugins/shared/discover/public/application/main/components/layout/discover_documents.test.tsx +++ b/src/platform/plugins/shared/discover/public/application/main/components/layout/discover_documents.test.tsx @@ -46,6 +46,17 @@ async function mountComponent(fetchStatus: FetchStatus, hits: EsHitRecord[]) { stateContainer.appState.update({ dataSource: createDataViewDataSource({ dataViewId: dataViewMock.id! }), }); + stateContainer.internalState.transitions.setDataRequestParams({ + timeRangeRelative: { + from: '2020-05-14T11:05:13.590', + to: '2020-05-14T11:20:13.590', + }, + timeRangeAbsolute: { + from: '2020-05-14T11:05:13.590', + to: '2020-05-14T11:20:13.590', + }, + }); + stateContainer.dataState.data$.documents$ = documents$; const props = { diff --git a/src/platform/plugins/shared/discover/public/application/main/components/layout/discover_documents.tsx b/src/platform/plugins/shared/discover/public/application/main/components/layout/discover_documents.tsx index 18996a7cdf9ca..ee00abbe5659d 100644 --- a/src/platform/plugins/shared/discover/public/application/main/components/layout/discover_documents.tsx +++ b/src/platform/plugins/shared/discover/public/application/main/components/layout/discover_documents.tsx @@ -46,7 +46,6 @@ import useObservable from 'react-use/lib/useObservable'; import type { DocViewFilterFn } from '@kbn/unified-doc-viewer/types'; import { DiscoverGridSettings } from '@kbn/saved-search-plugin/common'; import { useQuerySubscriber } from '@kbn/unified-field-list'; -import { map } from 'rxjs'; import { DiscoverGrid } from '../../../../components/discover_grid'; import { getDefaultRowsPerPage } from '../../../../../common/constants'; import { useInternalStateSelector } from '../../state_management/discover_internal_state_container'; @@ -112,6 +111,7 @@ function DiscoverDocumentsComponent({ const documents$ = stateContainer.dataState.data$.documents$; const savedSearch = useSavedSearchInitial(); const { dataViews, capabilities, uiSettings, uiActions, ebtManager, fieldsMetadata } = services; + const requestParams = useInternalStateSelector((state) => state.dataRequestParams); const [ dataSource, query, @@ -269,20 +269,14 @@ function DiscoverDocumentsComponent({ : undefined, [documentState.esqlQueryColumns] ); - const { filters } = useQuerySubscriber({ data: services.data }); - const timeRange = useObservable( - services.timefilter.getTimeUpdate$().pipe(map(() => services.timefilter.getTime())), - services.timefilter.getTime() - ); - const cellActionsMetadata = useAdditionalCellActions({ dataSource, dataView, query, filters, - timeRange, + timeRange: requestParams.timeRangeAbsolute, }); const renderDocumentView = useCallback( diff --git a/src/platform/plugins/shared/discover/public/application/main/components/layout/discover_histogram_layout.test.tsx b/src/platform/plugins/shared/discover/public/application/main/components/layout/discover_histogram_layout.test.tsx index 9aec3589c753e..121954410a90b 100644 --- a/src/platform/plugins/shared/discover/public/application/main/components/layout/discover_histogram_layout.test.tsx +++ b/src/platform/plugins/shared/discover/public/application/main/components/layout/discover_histogram_layout.test.tsx @@ -38,14 +38,25 @@ import { createDataViewDataSource } from '../../../../../common/data_sources'; function getStateContainer(savedSearch?: SavedSearch) { const stateContainer = getDiscoverStateMock({ isTimeBased: true, savedSearch }); const dataView = savedSearch?.searchSource?.getField('index') as DataView; - - stateContainer.appState.update({ + const appState = { dataSource: createDataViewDataSource({ dataViewId: dataView?.id! }), interval: 'auto', hideChart: false, - }); + }; + + stateContainer.appState.update(appState); stateContainer.internalState.transitions.setDataView(dataView); + stateContainer.internalState.transitions.setDataRequestParams({ + timeRangeAbsolute: { + from: '2020-05-14T11:05:13.590', + to: '2020-05-14T11:20:13.590', + }, + timeRangeRelative: { + from: '2020-05-14T11:05:13.590', + to: '2020-05-14T11:20:13.590', + }, + }); return stateContainer; } @@ -65,16 +76,7 @@ const mountComponent = async ({ const dataView = savedSearch?.searchSource?.getField('index') as DataView; let services = discoverServiceMock; - services.data.query.timefilter.timefilter.getAbsoluteTime = () => { - return { from: '2020-05-14T11:05:13.590', to: '2020-05-14T11:20:13.590' }; - }; - services.data.query.timefilter.timefilter.getTime = () => { - return { from: '2020-05-14T11:05:13.590', to: '2020-05-14T11:20:13.590' }; - }; - (services.data.query.queryString.getDefaultQuery as jest.Mock).mockReturnValue({ - language: 'kuery', - query: '', - }); + (searchSourceInstanceMock.fetch$ as jest.Mock).mockImplementation( jest.fn().mockReturnValue(of({ rawResponse: { hits: { total: 2 } } })) ); diff --git a/src/platform/plugins/shared/discover/public/application/main/components/layout/discover_layout.test.tsx b/src/platform/plugins/shared/discover/public/application/main/components/layout/discover_layout.test.tsx index c0b94760b61d3..c08b5539dd1c0 100644 --- a/src/platform/plugins/shared/discover/public/application/main/components/layout/discover_layout.test.tsx +++ b/src/platform/plugins/shared/discover/public/application/main/components/layout/discover_layout.test.tsx @@ -105,6 +105,10 @@ async function mountComponent( query, }); stateContainer.internalState.transitions.setDataView(dataView); + stateContainer.internalState.transitions.setDataRequestParams({ + timeRangeAbsolute: time, + timeRangeRelative: time, + }); const props = { dataView, diff --git a/src/platform/plugins/shared/discover/public/application/main/components/layout/use_discover_histogram.test.tsx b/src/platform/plugins/shared/discover/public/application/main/components/layout/use_discover_histogram.test.tsx index c8f829d442444..863a162c63a93 100644 --- a/src/platform/plugins/shared/discover/public/application/main/components/layout/use_discover_histogram.test.tsx +++ b/src/platform/plugins/shared/discover/public/application/main/components/layout/use_discover_histogram.test.tsx @@ -379,6 +379,23 @@ describe('useDiscoverHistogram', () => { }); expect(hook.result.current.isChartLoading).toBe(true); }); + + it('should use timerange + timeRangeRelative + query given by the internalState container', async () => { + const fetch$ = new Subject(); + const stateContainer = getStateContainer(); + const timeRangeAbs = { from: '2021-05-01T20:00:00Z', to: '2021-05-02T20:00:00Z' }; + const timeRangeRel = { from: 'now-15m', to: 'now' }; + stateContainer.internalState.transitions.setDataRequestParams({ + timeRangeAbsolute: timeRangeAbs, + timeRangeRelative: timeRangeRel, + }); + const { hook } = await renderUseDiscoverHistogram({ stateContainer }); + act(() => { + fetch$.next(); + }); + expect(hook.result.current.timeRange).toBe(timeRangeAbs); + expect(hook.result.current.relativeTimeRange).toBe(timeRangeRel); + }); }); describe('refetching', () => { diff --git a/src/platform/plugins/shared/discover/public/application/main/components/layout/use_discover_histogram.ts b/src/platform/plugins/shared/discover/public/application/main/components/layout/use_discover_histogram.ts index 3f2acf0ce933b..8f0a0fdf33dc1 100644 --- a/src/platform/plugins/shared/discover/public/application/main/components/layout/use_discover_histogram.ts +++ b/src/platform/plugins/shared/discover/public/application/main/components/layout/use_discover_histogram.ts @@ -217,14 +217,9 @@ export const useDiscoverHistogram = ({ * Request params */ const { query, filters } = useQuerySubscriber({ data: services.data }); + const requestParams = useInternalStateSelector((state) => state.dataRequestParams); const customFilters = useInternalStateSelector((state) => state.customFilters); - const timefilter = services.data.query.timefilter.timefilter; - const timeRange = timefilter.getAbsoluteTime(); - const relativeTimeRange = useObservable( - timefilter.getTimeUpdate$().pipe(map(() => timefilter.getTime())), - timefilter.getTime() - ); - + const { timeRangeRelative: relativeTimeRange, timeRangeAbsolute: timeRange } = requestParams; // When in ES|QL mode, update the data view, query, and // columns only when documents are done fetching so the Lens suggestions // don't frequently change, such as when the user modifies the table diff --git a/src/platform/plugins/shared/discover/public/application/main/data_fetching/fetch_all.test.ts b/src/platform/plugins/shared/discover/public/application/main/data_fetching/fetch_all.test.ts index ce2edba5d231e..0f620dab03654 100644 --- a/src/platform/plugins/shared/discover/public/application/main/data_fetching/fetch_all.test.ts +++ b/src/platform/plugins/shared/discover/public/application/main/data_fetching/fetch_all.test.ts @@ -81,6 +81,7 @@ describe('test fetchAll', () => { rowHeight: false, breakdownField: false, }, + dataRequestParams: {}, }), searchSessionId: '123', initialFetchStatus: FetchStatus.UNINITIALIZED, @@ -273,6 +274,7 @@ describe('test fetchAll', () => { rowHeight: false, breakdownField: false, }, + dataRequestParams: {}, }), }; fetchAll(subjects, false, deps); @@ -396,6 +398,7 @@ describe('test fetchAll', () => { rowHeight: false, breakdownField: false, }, + dataRequestParams: {}, }), }; fetchAll(subjects, false, deps); diff --git a/src/platform/plugins/shared/discover/public/application/main/data_fetching/fetch_all.ts b/src/platform/plugins/shared/discover/public/application/main/data_fetching/fetch_all.ts index 0e5a4e050f2fd..4f4af2f289d05 100644 --- a/src/platform/plugins/shared/discover/public/application/main/data_fetching/fetch_all.ts +++ b/src/platform/plugins/shared/discover/public/application/main/data_fetching/fetch_all.ts @@ -97,6 +97,7 @@ export function fetchAll( services, sort: getAppState().sort as SortOrder[], customFilters: getInternalState().customFilters, + inputTimeRange: getInternalState().dataRequestParams.timeRangeAbsolute, }); } @@ -117,6 +118,7 @@ export function fetchAll( data, expressions, profilesManager, + timeRange: getInternalState().dataRequestParams.timeRangeAbsolute, }) : fetchDocuments(searchSource, fetchDeps); const fetchType = isEsqlQuery ? 'fetchTextBased' : 'fetchDocuments'; diff --git a/src/platform/plugins/shared/discover/public/application/main/data_fetching/fetch_esql.test.ts b/src/platform/plugins/shared/discover/public/application/main/data_fetching/fetch_esql.test.ts index f4f2d6e4fa4af..ab96d22f24189 100644 --- a/src/platform/plugins/shared/discover/public/application/main/data_fetching/fetch_esql.test.ts +++ b/src/platform/plugins/shared/discover/public/application/main/data_fetching/fetch_esql.test.ts @@ -13,13 +13,23 @@ import { RequestAdapter } from '@kbn/inspector-plugin/common'; import { of } from 'rxjs'; import { dataViewWithTimefieldMock } from '../../../__mocks__/data_view_with_timefield'; import { discoverServiceMock } from '../../../__mocks__/services'; -import { fetchEsql } from './fetch_esql'; +import { fetchEsql, getTextBasedQueryStateToAstProps } from './fetch_esql'; +import { TimeRange } from '@kbn/es-query'; describe('fetchEsql', () => { beforeEach(() => { jest.clearAllMocks(); }); + const fetchEsqlMockProps = { + query: { esql: 'from *' }, + dataView: dataViewWithTimefieldMock, + inspectorAdapters: { requests: new RequestAdapter() }, + data: discoverServiceMock.data, + expressions: discoverServiceMock.expressions, + profilesManager: discoverServiceMock.profilesManager, + }; + it('resolves with returned records', async () => { const hits = [ { _id: '1', foo: 'bar' }, @@ -46,16 +56,7 @@ describe('fetchEsql', () => { discoverServiceMock.profilesManager, 'resolveDocumentProfile' ); - expect( - await fetchEsql({ - query: { esql: 'from *' }, - dataView: dataViewWithTimefieldMock, - inspectorAdapters: { requests: new RequestAdapter() }, - data: discoverServiceMock.data, - expressions: discoverServiceMock.expressions, - profilesManager: discoverServiceMock.profilesManager, - }) - ).toEqual({ + expect(await fetchEsql(fetchEsqlMockProps)).toEqual({ records, esqlQueryColumns: ['_id', 'foo'], esqlHeaderWarning: undefined, @@ -64,4 +65,24 @@ describe('fetchEsql', () => { expect(resolveDocumentProfileSpy).toHaveBeenCalledWith({ record: records[0] }); expect(resolveDocumentProfileSpy).toHaveBeenCalledWith({ record: records[1] }); }); + + it('should use inputTimeRange if provided', () => { + const timeRange: TimeRange = { from: 'now-15m', to: 'now' }; + const result = getTextBasedQueryStateToAstProps({ ...fetchEsqlMockProps, timeRange }); + expect(result.time).toEqual(timeRange); + }); + + it('should use absolute time from data if inputTimeRange is not provided', () => { + const absoluteTimeRange: TimeRange = { + from: '2021-08-31T22:00:00.000Z', + to: '2021-09-01T22:00:00.000Z', + }; + jest + .spyOn(discoverServiceMock.data.query.timefilter.timefilter, 'getAbsoluteTime') + .mockReturnValue(absoluteTimeRange); + + const result = getTextBasedQueryStateToAstProps(fetchEsqlMockProps); + + expect(result.time).toEqual(absoluteTimeRange); + }); }); diff --git a/src/platform/plugins/shared/discover/public/application/main/data_fetching/fetch_esql.ts b/src/platform/plugins/shared/discover/public/application/main/data_fetching/fetch_esql.ts index d7cad10b177f6..9c5540443f86d 100644 --- a/src/platform/plugins/shared/discover/public/application/main/data_fetching/fetch_esql.ts +++ b/src/platform/plugins/shared/discover/public/application/main/data_fetching/fetch_esql.ts @@ -32,7 +32,7 @@ export function fetchEsql({ query, inputQuery, filters, - inputTimeRange, + timeRange, dataView, abortSignal, inspectorAdapters, @@ -43,7 +43,7 @@ export function fetchEsql({ query: Query | AggregateQuery; inputQuery?: Query; filters?: Filter[]; - inputTimeRange?: TimeRange; + timeRange?: TimeRange; dataView: DataView; abortSignal?: AbortSignal; inspectorAdapters: Adapters; @@ -51,20 +51,15 @@ export function fetchEsql({ expressions: ExpressionsStart; profilesManager: ProfilesManager; }): Promise { - const timeRange = inputTimeRange ?? data.query.timefilter.timefilter.getTime(); - return textBasedQueryStateToAstWithValidation({ - filters, + const props = getTextBasedQueryStateToAstProps({ query, - time: timeRange, - timeFieldName: dataView.timeFieldName, inputQuery, - titleForInspector: i18n.translate('discover.inspectorEsqlRequestTitle', { - defaultMessage: 'Table', - }), - descriptionForInspector: i18n.translate('discover.inspectorEsqlRequestDescription', { - defaultMessage: 'This request queries Elasticsearch to fetch results for the table.', - }), - }) + filters, + timeRange, + dataView, + data, + }); + return textBasedQueryStateToAstWithValidation(props) .then((ast) => { if (ast) { const contract = expressions.execute(ast, null, { @@ -118,3 +113,32 @@ export function fetchEsql({ throw new Error(err.message); }); } +export function getTextBasedQueryStateToAstProps({ + query, + inputQuery, + filters, + timeRange, + dataView, + data, +}: { + query: Query | AggregateQuery; + inputQuery?: Query; + filters?: Filter[]; + timeRange?: TimeRange; + dataView: DataView; + data: DataPublicPluginStart; +}) { + return { + filters, + query, + time: timeRange ?? data.query.timefilter.timefilter.getAbsoluteTime(), + timeFieldName: dataView.timeFieldName, + inputQuery, + titleForInspector: i18n.translate('discover.inspectorEsqlRequestTitle', { + defaultMessage: 'Table', + }), + descriptionForInspector: i18n.translate('discover.inspectorEsqlRequestDescription', { + defaultMessage: 'This request queries Elasticsearch to fetch results for the table.', + }), + }; +} diff --git a/src/platform/plugins/shared/discover/public/application/main/data_fetching/update_search_source.ts b/src/platform/plugins/shared/discover/public/application/main/data_fetching/update_search_source.ts index ad79e93ec37e4..05ba512d0e716 100644 --- a/src/platform/plugins/shared/discover/public/application/main/data_fetching/update_search_source.ts +++ b/src/platform/plugins/shared/discover/public/application/main/data_fetching/update_search_source.ts @@ -7,9 +7,9 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { ISearchSource } from '@kbn/data-plugin/public'; -import { DataViewType, DataView } from '@kbn/data-views-plugin/public'; -import { Filter } from '@kbn/es-query'; +import type { ISearchSource } from '@kbn/data-plugin/public'; +import { DataViewType, type DataView } from '@kbn/data-views-plugin/public'; +import type { Filter, TimeRange } from '@kbn/es-query'; import type { SortOrder } from '@kbn/saved-search-plugin/public'; import { SORT_DEFAULT_ORDER_SETTING } from '@kbn/discover-utils'; import { DiscoverServices } from '../../../build_services'; @@ -25,11 +25,13 @@ export function updateVolatileSearchSource( services, sort, customFilters, + inputTimeRange, }: { dataView: DataView; services: DiscoverServices; sort?: SortOrder[]; customFilters: Filter[]; + inputTimeRange?: TimeRange; } ) { const { uiSettings, data } = services; @@ -48,7 +50,7 @@ export function updateVolatileSearchSource( if (dataView.type !== DataViewType.ROLLUP) { // Set the date range filter fields from timeFilter using the absolute format. Search sessions requires that it be converted from a relative range - const timeFilter = data.query.timefilter.timefilter.createFilter(dataView); + const timeFilter = data.query.timefilter.timefilter.createFilter(dataView, inputTimeRange); filters = timeFilter ? [...filters, timeFilter] : filters; } diff --git a/src/platform/plugins/shared/discover/public/application/main/state_management/discover_data_state_container.ts b/src/platform/plugins/shared/discover/public/application/main/state_management/discover_data_state_container.ts index 59220d7def3c1..029cbf1b229d4 100644 --- a/src/platform/plugins/shared/discover/public/application/main/state_management/discover_data_state_container.ts +++ b/src/platform/plugins/shared/discover/public/application/main/state_management/discover_data_state_container.ts @@ -254,6 +254,11 @@ export function getDataStateContainer({ return; } + internalStateContainer.transitions.setDataRequestParams({ + timeRangeAbsolute: timefilter.getAbsoluteTime(), + timeRangeRelative: timefilter.getTime(), + }); + await profilesManager.resolveDataSourceProfile({ dataSource: appStateContainer.getState().dataSource, dataView: getSavedSearch().searchSource.getField('index'), diff --git a/src/platform/plugins/shared/discover/public/application/main/state_management/discover_internal_state_container.ts b/src/platform/plugins/shared/discover/public/application/main/state_management/discover_internal_state_container.ts index 8aad8eb91738b..74815ad816daf 100644 --- a/src/platform/plugins/shared/discover/public/application/main/state_management/discover_internal_state_container.ts +++ b/src/platform/plugins/shared/discover/public/application/main/state_management/discover_internal_state_container.ts @@ -14,10 +14,15 @@ import { ReduxLikeStateContainer, } from '@kbn/kibana-utils-plugin/common'; import type { DataView, DataViewListItem } from '@kbn/data-views-plugin/common'; -import type { Filter } from '@kbn/es-query'; +import type { Filter, TimeRange } from '@kbn/es-query'; import type { DataTableRecord } from '@kbn/discover-utils/types'; import type { UnifiedHistogramVisContext } from '@kbn/unified-histogram-plugin/public'; +interface InternalStateDataRequestParams { + timeRangeAbsolute?: TimeRange; + timeRangeRelative?: TimeRange; +} + export interface InternalState { dataView: DataView | undefined; isDataViewLoading: boolean; @@ -33,6 +38,7 @@ export interface InternalState { rowHeight: boolean; breakdownField: boolean; }; + dataRequestParams: InternalStateDataRequestParams; } export interface InternalStateTransitions { @@ -65,6 +71,9 @@ export interface InternalStateTransitions { ) => ( resetDefaultProfileState: Omit ) => InternalState; + setDataRequestParams: ( + state: InternalState + ) => (params: InternalStateDataRequestParams) => InternalState; } export type DiscoverInternalStateContainer = ReduxLikeStateContainer< @@ -91,6 +100,7 @@ export function getInternalStateContainer() { rowHeight: false, breakdownField: false, }, + dataRequestParams: {}, }, { setDataView: (prevState: InternalState) => (nextDataView: DataView) => ({ @@ -162,6 +172,11 @@ export function getInternalStateContainer() { overriddenVisContextAfterInvalidation: undefined, expandedDoc: undefined, }), + setDataRequestParams: + (prevState: InternalState) => (params: InternalStateDataRequestParams) => ({ + ...prevState, + dataRequestParams: params, + }), setResetDefaultProfileState: (prevState: InternalState) => (resetDefaultProfileState: Omit) => ({ diff --git a/src/platform/plugins/shared/discover/public/embeddable/initialize_fetch.ts b/src/platform/plugins/shared/discover/public/embeddable/initialize_fetch.ts index 445b7da7dd984..261966931e320 100644 --- a/src/platform/plugins/shared/discover/public/embeddable/initialize_fetch.ts +++ b/src/platform/plugins/shared/discover/public/embeddable/initialize_fetch.ts @@ -148,7 +148,7 @@ export function initializeFetch({ // Request ES|QL data const result = await fetchEsql({ query: searchSourceQuery, - inputTimeRange: getTimeRangeFromFetchContext(fetchContext), + timeRange: getTimeRangeFromFetchContext(fetchContext), inputQuery: fetchContext.query, filters: fetchContext.filters, dataView, diff --git a/test/functional/apps/discover/group3/_request_counts.ts b/test/functional/apps/discover/group3/_request_counts.ts index 4be360f5a7e8c..03fdab88e2f23 100644 --- a/test/functional/apps/discover/group3/_request_counts.ts +++ b/test/functional/apps/discover/group3/_request_counts.ts @@ -138,16 +138,12 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it(`should send no more than ${expectedRequests} requests (documents + chart) when changing the time range`, async () => { - await expectSearches( - type, - type === 'esql' ? expectedRequests + 1 : expectedRequests, - async () => { - await timePicker.setAbsoluteRange( - 'Sep 21, 2015 @ 06:31:44.000', - 'Sep 23, 2015 @ 00:00:00.000' - ); - } - ); + await expectSearches(type, expectedRequests, async () => { + await timePicker.setAbsoluteRange( + 'Sep 21, 2015 @ 06:31:44.000', + 'Sep 23, 2015 @ 00:00:00.000' + ); + }); }); it(`should send ${savedSearchesRequests} requests for saved search changes`, async () => { From de3bd71d2cb34526e742a645f85b9f1f700a289d Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Fri, 10 Jan 2025 12:07:15 -0400 Subject: [PATCH 36/42] [Discover] Fix document comparison table padding (#205984) ## Summary This PR fixes the table padding on the Discover document comparison table which was causing the diff decorations to be cut off. This likely started after #188495 when some of the standard table styles the comparison table inherits from changed. Fixes #205463. ### Checklist - [ ] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/src/platform/packages/shared/kbn-i18n/README.md) - [ ] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [ ] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [ ] If a plugin configuration key changed, check if it needs to be allowlisted in the cloud and added to the [docker list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker) - [ ] This was checked for breaking HTTP API changes, and any breaking changes have been approved by the breaking-change committee. The `release_note:breaking` label should be applied in these situations. - [ ] [Flaky Test Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was used on any tests changed - [x] The PR description includes the appropriate Release Notes section, and the correct `release_note:*` label is applied per the [guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) --- .../components/compare_documents/compare_documents.test.tsx | 2 +- .../src/components/compare_documents/compare_documents.tsx | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/platform/packages/shared/kbn-unified-data-table/src/components/compare_documents/compare_documents.test.tsx b/src/platform/packages/shared/kbn-unified-data-table/src/components/compare_documents/compare_documents.test.tsx index 52df4974218fb..683b5a29a0657 100644 --- a/src/platform/packages/shared/kbn-unified-data-table/src/components/compare_documents/compare_documents.test.tsx +++ b/src/platform/packages/shared/kbn-unified-data-table/src/components/compare_documents/compare_documents.test.tsx @@ -109,7 +109,7 @@ describe('CompareDocuments', () => { "data-test-subj": "unifiedDataTableCompareDocuments", "gridStyle": Object { "border": "horizontal", - "cellPadding": "s", + "cellPadding": "l", "fontSize": "s", "header": "underline", "rowHover": "highlight", diff --git a/src/platform/packages/shared/kbn-unified-data-table/src/components/compare_documents/compare_documents.tsx b/src/platform/packages/shared/kbn-unified-data-table/src/components/compare_documents/compare_documents.tsx index fc79c0efadf91..3e29e3903e717 100644 --- a/src/platform/packages/shared/kbn-unified-data-table/src/components/compare_documents/compare_documents.tsx +++ b/src/platform/packages/shared/kbn-unified-data-table/src/components/compare_documents/compare_documents.tsx @@ -56,7 +56,11 @@ export interface CompareDocumentsProps { const COMPARISON_ROW_HEIGHT: EuiDataGridRowHeightsOptions = { defaultHeight: 'auto' }; const COMPARISON_IN_MEMORY: EuiDataGridInMemory = { level: 'sorting' }; -const COMPARISON_GRID_STYLE: EuiDataGridStyle = { ...DATA_GRID_STYLE_DEFAULT, stripes: undefined }; +const COMPARISON_GRID_STYLE: EuiDataGridStyle = { + ...DATA_GRID_STYLE_DEFAULT, + cellPadding: 'l', + stripes: undefined, +}; const getStorageKey = (consumer: string, key: string) => `${consumer}:dataGridComparison${key}`; From 9cda1a83a6e5b48b88a326a67a185bc8a6a703fa Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Fri, 10 Jan 2025 11:31:24 -0500 Subject: [PATCH 37/42] [Fleet] Fix output id when using default output as integration output (#206286) --- .../full_agent_policy.test.ts.snap | 154 ++++++++++++++++++ .../agent_policies/full_agent_policy.test.ts | 120 ++++++++++++++ .../agent_policies/full_agent_policy.ts | 24 ++- 3 files changed, 291 insertions(+), 7 deletions(-) diff --git a/x-pack/platform/plugins/shared/fleet/server/services/agent_policies/__snapshots__/full_agent_policy.test.ts.snap b/x-pack/platform/plugins/shared/fleet/server/services/agent_policies/__snapshots__/full_agent_policy.test.ts.snap index d5ae12c00a9ff..9d93fd5df3c81 100644 --- a/x-pack/platform/plugins/shared/fleet/server/services/agent_policies/__snapshots__/full_agent_policy.test.ts.snap +++ b/x-pack/platform/plugins/shared/fleet/server/services/agent_policies/__snapshots__/full_agent_policy.test.ts.snap @@ -1,5 +1,159 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`getFullAgentPolicy should return the right outputs and permissions when package policies use their own outputs (with default output) 1`] = ` +Object { + "agent": Object { + "download": Object { + "sourceURI": "http://default-registry.co", + }, + "features": Object {}, + "monitoring": Object { + "enabled": false, + "logs": false, + "metrics": false, + "traces": false, + }, + "protection": Object { + "enabled": false, + "signing_key": "", + "uninstall_token_hash": "", + }, + }, + "fleet": Object { + "hosts": Array [ + "http://fleetserver:8220", + ], + }, + "id": "integration-output-policy", + "inputs": Array [ + Object { + "data_stream": Object { + "namespace": "policyspace", + }, + "id": "test-logs-package-policy-using-output", + "meta": Object { + "package": Object { + "name": "test_package", + "version": "0.0.0", + }, + }, + "name": "test-policy-1", + "package_policy_id": "package-policy-using-output", + "revision": 1, + "streams": Array [ + Object { + "data_stream": Object { + "dataset": "some-logs", + "type": "logs", + }, + "id": "test-logs", + }, + ], + "type": "test-logs", + "use_output": "default", + }, + Object { + "data_stream": Object { + "namespace": "defaultspace", + }, + "id": "test-logs-package-policy-no-output", + "meta": Object { + "package": Object { + "name": "system", + "version": "1.0.0", + }, + }, + "name": "test-policy-2", + "package_policy_id": "package-policy-no-output", + "revision": 1, + "streams": Array [ + Object { + "data_stream": Object { + "dataset": "some-logs", + "type": "logs", + }, + "id": "test-logs", + }, + ], + "type": "test-logs", + "use_output": "data-output-id", + }, + ], + "output_permissions": Object { + "data-output-id": Object { + "_elastic_agent_checks": Object { + "cluster": Array [ + "monitor", + ], + }, + "package-policy-no-output": Object { + "indices": Array [ + Object { + "names": Array [ + "logs-some-logs-defaultspace", + ], + "privileges": Array [ + "auto_configure", + "create_doc", + ], + }, + ], + }, + }, + "default": Object { + "_elastic_agent_checks": Object { + "cluster": Array [ + "monitor", + ], + }, + "_elastic_agent_monitoring": Object { + "indices": Array [ + Object { + "names": Array [], + "privileges": Array [], + }, + ], + }, + "package-policy-using-output": Object { + "indices": Array [ + Object { + "names": Array [ + "logs-some-logs-policyspace", + ], + "privileges": Array [ + "auto_configure", + "create_doc", + ], + }, + ], + }, + }, + }, + "outputs": Object { + "data-output-id": Object { + "hosts": Array [ + "http://es-data.co:9201", + ], + "preset": "balanced", + "type": "elasticsearch", + }, + "default": Object { + "hosts": Array [ + "http://127.0.0.1:9201", + ], + "preset": "balanced", + "type": "elasticsearch", + }, + }, + "revision": 1, + "secret_references": Array [], + "signed": Object { + "data": "", + "signature": "", + }, +} +`; + exports[`getFullAgentPolicy should return the right outputs and permissions when package policies use their own outputs 1`] = ` Object { "agent": Object { diff --git a/x-pack/platform/plugins/shared/fleet/server/services/agent_policies/full_agent_policy.test.ts b/x-pack/platform/plugins/shared/fleet/server/services/agent_policies/full_agent_policy.test.ts index 96fd29b9534c6..d96393f56b7ef 100644 --- a/x-pack/platform/plugins/shared/fleet/server/services/agent_policies/full_agent_policy.test.ts +++ b/x-pack/platform/plugins/shared/fleet/server/services/agent_policies/full_agent_policy.test.ts @@ -564,6 +564,126 @@ describe('getFullAgentPolicy', () => { expect(agentPolicy).toMatchSnapshot(); }); + it('should return the right outputs and permissions when package policies use their own outputs (with default output)', async () => { + mockedGetPackageInfo.mockResolvedValue({ + data_streams: [ + { + type: 'logs', + dataset: 'elastic_agent.metricbeat', + }, + { + type: 'metrics', + dataset: 'elastic_agent.metricbeat', + }, + { + type: 'logs', + dataset: 'elastic_agent.filebeat', + }, + { + type: 'metrics', + dataset: 'elastic_agent.filebeat', + }, + ], + } as PackageInfo); + mockAgentPolicy({ + id: 'integration-output-policy', + status: 'active', + package_policies: [ + { + id: 'package-policy-using-output', + name: 'test-policy-1', + namespace: 'policyspace', + enabled: true, + package: { name: 'test_package', version: '0.0.0', title: 'Test Package' }, + output_id: 'test-id', + inputs: [ + { + type: 'test-logs', + enabled: true, + streams: [ + { + id: 'test-logs', + enabled: true, + data_stream: { type: 'logs', dataset: 'some-logs' }, + }, + ], + }, + { + type: 'test-metrics', + enabled: false, + streams: [ + { + id: 'test-logs', + enabled: false, + data_stream: { type: 'metrics', dataset: 'some-metrics' }, + }, + ], + }, + ], + created_at: '', + updated_at: '', + created_by: '', + updated_by: '', + revision: 1, + policy_id: '', + policy_ids: [''], + }, + { + id: 'package-policy-no-output', + name: 'test-policy-2', + namespace: '', + enabled: true, + package: { name: 'system', version: '1.0.0', title: 'System' }, + inputs: [ + { + type: 'test-logs', + enabled: true, + streams: [ + { + id: 'test-logs', + enabled: true, + data_stream: { type: 'logs', dataset: 'some-logs' }, + }, + ], + }, + { + type: 'test-metrics', + enabled: false, + streams: [ + { + id: 'test-logs', + enabled: false, + data_stream: { type: 'metrics', dataset: 'some-metrics' }, + }, + ], + }, + ], + created_at: '', + updated_at: '', + created_by: '', + updated_by: '', + revision: 1, + policy_id: '', + policy_ids: [''], + }, + ], + is_managed: false, + namespace: 'defaultspace', + revision: 1, + name: 'Policy', + updated_at: '2020-01-01', + updated_by: 'qwerty', + is_protected: false, + data_output_id: 'data-output-id', + }); + + const agentPolicy = await getFullAgentPolicy( + savedObjectsClientMock.create(), + 'integration-output-policy' + ); + expect(agentPolicy).toMatchSnapshot(); + }); + it('should return the sourceURI from the agent policy', async () => { mockAgentPolicy({ namespace: 'default', diff --git a/x-pack/platform/plugins/shared/fleet/server/services/agent_policies/full_agent_policy.ts b/x-pack/platform/plugins/shared/fleet/server/services/agent_policies/full_agent_policy.ts index 8116856265157..ea14167bb324a 100644 --- a/x-pack/platform/plugins/shared/fleet/server/services/agent_policies/full_agent_policy.ts +++ b/x-pack/platform/plugins/shared/fleet/server/services/agent_policies/full_agent_policy.ts @@ -120,13 +120,23 @@ export async function getFullAgentPolicy( }) ); - const inputs = await storedPackagePoliciesToAgentInputs( - agentPolicy.package_policies as PackagePolicy[], - packageInfoCache, - getOutputIdForAgentPolicy(dataOutput), - agentPolicy.namespace, - agentPolicy.global_data_tags - ); + const inputs = ( + await storedPackagePoliciesToAgentInputs( + agentPolicy.package_policies as PackagePolicy[], + packageInfoCache, + getOutputIdForAgentPolicy(dataOutput), + agentPolicy.namespace, + agentPolicy.global_data_tags + ) + ).map((input) => { + // fix output id for default output + const output = outputs.find(({ id: outputId }) => input.use_output === outputId); + if (output) { + input.use_output = getOutputIdForAgentPolicy(output); + } + + return input; + }); const features = (agentPolicy.agent_features || []).reduce((acc, { name, ...featureConfig }) => { acc[name] = featureConfig; return acc; From 0c106d331b3c1b946c4c706cf6afafee0c8377c4 Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Fri, 10 Jan 2025 11:35:16 -0500 Subject: [PATCH 38/42] [Fleet] Fix creating a policy in a different space than the current (#205925) --- .../agent_policy_advanced_fields/index.tsx | 2 +- .../server/routes/agent_policy/handlers.ts | 8 +++-- .../apis/space_awareness/agent_policies.ts | 30 +++++++++++++++++-- .../apis/space_awareness/api_helper.ts | 15 +++++++--- .../apis/space_awareness/space_settings.ts | 19 +++++------- 5 files changed, 54 insertions(+), 20 deletions(-) diff --git a/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_advanced_fields/index.tsx b/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_advanced_fields/index.tsx index b0889f825727f..1ac7186be665e 100644 --- a/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_advanced_fields/index.tsx +++ b/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_advanced_fields/index.tsx @@ -129,7 +129,7 @@ export const AgentPolicyAdvancedOptionsContent: React.FunctionComponent = agentPolicy.is_managed === true || agentPolicy?.supports_agentless === true; const userHasAccessToAllPolicySpaces = useMemo( - () => 'space_ids' in agentPolicy && !agentPolicy.space_ids?.includes(UNKNOWN_SPACE), + () => ('space_ids' in agentPolicy ? !agentPolicy.space_ids?.includes(UNKNOWN_SPACE) : true), [agentPolicy] ); diff --git a/x-pack/platform/plugins/shared/fleet/server/routes/agent_policy/handlers.ts b/x-pack/platform/plugins/shared/fleet/server/routes/agent_policy/handlers.ts index 2233d9fc9fa3c..218920d029c6b 100644 --- a/x-pack/platform/plugins/shared/fleet/server/routes/agent_policy/handlers.ts +++ b/x-pack/platform/plugins/shared/fleet/server/routes/agent_policy/handlers.ts @@ -327,8 +327,12 @@ export const createAgentPolicyHandler: FleetRequestHandler< const body: CreateAgentPolicyResponse = { item: agentPolicy, }; - - if (spaceIds && spaceIds.length > 1 && authorizedSpaces) { + // Update spaces if there is more than one space ID assigned to that policy or if there the space that policy is created is different than the current space + if ( + spaceIds && + authorizedSpaces && + (spaceIds.length > 1 || (spaceIds.length === 0 && spaceIds[0]) !== spaceId) + ) { await updateAgentPolicySpaces({ agentPolicyId: agentPolicy.id, currentSpaceId: spaceId, diff --git a/x-pack/test/fleet_api_integration/apis/space_awareness/agent_policies.ts b/x-pack/test/fleet_api_integration/apis/space_awareness/agent_policies.ts index acf4ce7c71e3d..539355912af7a 100644 --- a/x-pack/test/fleet_api_integration/apis/space_awareness/agent_policies.ts +++ b/x-pack/test/fleet_api_integration/apis/space_awareness/agent_policies.ts @@ -10,7 +10,7 @@ import { CreateAgentPolicyResponse } from '@kbn/fleet-plugin/common'; import { FtrProviderContext } from '../../../api_integration/ftr_provider_context'; import { skipIfNoDockerRegistry } from '../../helpers'; import { SpaceTestApiClient } from './api_helper'; -import { cleanFleetIndices, expectToRejectWithNotFound } from './helpers'; +import { cleanFleetIndices, expectToRejectWithError, expectToRejectWithNotFound } from './helpers'; import { setupTestUsers, testUsers } from '../test_users'; export default function (providerContext: FtrProviderContext) { @@ -31,13 +31,17 @@ export default function (providerContext: FtrProviderContext) { username: testUsers.fleet_read_only.username, password: testUsers.fleet_read_only.password, }); + const apiClientDefaultSpaceOnly = new SpaceTestApiClient(supertestWithoutAuth, { + username: testUsers.fleet_all_int_all_default_space_only.username, + password: testUsers.fleet_all_int_all_default_space_only.password, + }); let defaultSpacePolicy1: CreateAgentPolicyResponse; let spaceTest1Policy1: CreateAgentPolicyResponse; let spaceTest1Policy2: CreateAgentPolicyResponse; before(async () => { - await setupTestUsers(getService('security')); + await setupTestUsers(getService('security'), true); TEST_SPACE_1 = spaces.getDefaultTestSpace(); await kibanaServer.savedObjects.cleanStandardList(); await kibanaServer.savedObjects.cleanStandardList({ @@ -116,6 +120,28 @@ export default function (providerContext: FtrProviderContext) { }); expect(res.item.id).to.eql(`${TEST_SPACE_1}-fleet-server-policy`); }); + + it('should allow to create a policy in another space user has permissions from default space', async () => { + const res = await apiClient.createAgentPolicy('default', { + space_ids: [TEST_SPACE_1], + }); + + const policyId = res.item.id; + await expectToRejectWithNotFound(() => apiClient.getAgentPolicy(spaceTest1Policy1.item.id)); + + const policyFound = await apiClient.getAgentPolicy(policyId, TEST_SPACE_1); + expect(policyFound.item.id).to.eql(policyId); + }); + + it('should not allow to create a policy in another space when user do not have permissions from default space', async () => { + await expectToRejectWithError( + () => + apiClientDefaultSpaceOnly.createAgentPolicy('default', { + space_ids: [TEST_SPACE_1], + }), + /No enough permissions to create policies in space test1/ + ); + }); }); describe('GET /agent_policies_spaces', () => { diff --git a/x-pack/test/fleet_api_integration/apis/space_awareness/api_helper.ts b/x-pack/test/fleet_api_integration/apis/space_awareness/api_helper.ts index c26fa0a515f01..1fdeee8e87a8a 100644 --- a/x-pack/test/fleet_api_integration/apis/space_awareness/api_helper.ts +++ b/x-pack/test/fleet_api_integration/apis/space_awareness/api_helper.ts @@ -70,7 +70,7 @@ export class SpaceTestApiClient { spaceId?: string, data: Partial = {} ): Promise { - const { body: res } = await this.supertest + const { body: res, statusCode } = await this.supertest .post(`${this.getBaseUrl(spaceId)}/api/fleet/agent_policies`) .auth(this.auth.username, this.auth.password) .set('kbn-xsrf', 'xxxx') @@ -80,10 +80,17 @@ export class SpaceTestApiClient { namespace: 'default', inactivity_timeout: 24 * 1000, ...data, - }) - .expect(200); + }); - return res; + if (statusCode === 200) { + return res; + } + + if (statusCode === 404) { + throw new Error('404 "Not Found"'); + } else { + throw new Error(`${statusCode} ${res?.error} ${res.message}`); + } } async createPackagePolicy( spaceId?: string, diff --git a/x-pack/test/fleet_api_integration/apis/space_awareness/space_settings.ts b/x-pack/test/fleet_api_integration/apis/space_awareness/space_settings.ts index 11f6ef1e5cb9f..a81d806729ae4 100644 --- a/x-pack/test/fleet_api_integration/apis/space_awareness/space_settings.ts +++ b/x-pack/test/fleet_api_integration/apis/space_awareness/space_settings.ts @@ -8,6 +8,7 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../api_integration/ftr_provider_context'; import { SpaceTestApiClient } from './api_helper'; +import { expectToRejectWithError } from './helpers'; export default function (providerContext: FtrProviderContext) { const { getService } = providerContext; @@ -96,17 +97,13 @@ export default function (providerContext: FtrProviderContext) { ); }); it('should restrict non authorized agent policy namespace', async () => { - let err: Error | undefined; - try { - await apiClient.createAgentPolicy(TEST_SPACE_1, { - namespace: 'default', - }); - } catch (_err) { - err = _err; - } - - expect(err).to.be.an(Error); - expect(err?.message).to.match(/400 "Bad Request"/); + await expectToRejectWithError( + () => + apiClient.createAgentPolicy(TEST_SPACE_1, { + namespace: 'default', + }), + /400 Bad Request Invalid namespace, supported namespace prefixes: test/ + ); }); it('should allow authorized agent policy namespace', async () => { await apiClient.createAgentPolicy(TEST_SPACE_1, { From ecf181860864891d37f86eb8909a8bcd92e85e98 Mon Sep 17 00:00:00 2001 From: Dzmitry Lemechko Date: Fri, 10 Jan 2025 17:44:06 +0100 Subject: [PATCH 39/42] [kbn-test] export fleet package registry image (#206234) ## Summary Should fix TS check error `Project references may not form a circular graph` by removing `@kbn/test-suites-xpack` from `kbn-scout` dependency list. Since dockerImage for Fleet package registry is just a constant, that is used across different FTR and Scout configurations, it makes sense to export it from `kbn-test` --- .../src/config/serverless/serverless.base.config.ts | 10 ++++++---- .../kbn-scout/src/config/stateful/base.config.ts | 7 ++----- packages/kbn-scout/tsconfig.json | 1 - packages/kbn-test/index.ts | 7 +++++++ .../default_configs/serverless.config.base.ts | 10 +++++++--- .../default_configs/stateful.config.base.ts | 4 ++-- .../security_solution/endpoint_registry_helpers.ts | 6 ++++-- .../common/config.ts | 9 ++++++--- x-pack/test/fleet_api_integration/config.base.ts | 13 +++++-------- x-pack/test/functional/config.base.js | 8 -------- x-pack/test_serverless/shared/config.base.ts | 5 ++--- 11 files changed, 41 insertions(+), 39 deletions(-) diff --git a/packages/kbn-scout/src/config/serverless/serverless.base.config.ts b/packages/kbn-scout/src/config/serverless/serverless.base.config.ts index 0df42d354a1e1..c7fb06da954ef 100644 --- a/packages/kbn-scout/src/config/serverless/serverless.base.config.ts +++ b/packages/kbn-scout/src/config/serverless/serverless.base.config.ts @@ -12,10 +12,12 @@ import { format as formatUrl } from 'url'; import Fs from 'fs'; import { CA_CERT_PATH, kibanaDevServiceAccount } from '@kbn/dev-utils'; -import { defineDockerServersConfig, getDockerFileMountPath } from '@kbn/test'; +import { + fleetPackageRegistryDockerImage, + defineDockerServersConfig, + getDockerFileMountPath, +} from '@kbn/test'; import { MOCK_IDP_REALM_NAME } from '@kbn/mock-idp-utils'; - -import { dockerImage } from '@kbn/test-suites-xpack/fleet_api_integration/config.base'; import { REPO_ROOT } from '@kbn/repo-info'; import { ScoutServerConfig } from '../../types'; import { SAML_IDP_PLUGIN_PATH, SERVERLESS_IDP_METADATA_PATH, JWKS_PATH } from '../constants'; @@ -55,7 +57,7 @@ export const defaultConfig: ScoutServerConfig = { dockerServers: defineDockerServersConfig({ registry: { enabled: !!dockerRegistryPort, - image: dockerImage, + image: fleetPackageRegistryDockerImage, portInContainer: 8080, port: dockerRegistryPort, args: dockerArgs, diff --git a/packages/kbn-scout/src/config/stateful/base.config.ts b/packages/kbn-scout/src/config/stateful/base.config.ts index 2bac0024ad4e3..043b34723a21c 100644 --- a/packages/kbn-scout/src/config/stateful/base.config.ts +++ b/packages/kbn-scout/src/config/stateful/base.config.ts @@ -17,12 +17,9 @@ import { MOCK_IDP_ATTRIBUTE_EMAIL, MOCK_IDP_ATTRIBUTE_NAME, } from '@kbn/mock-idp-utils'; -import { defineDockerServersConfig } from '@kbn/test'; +import { fleetPackageRegistryDockerImage, defineDockerServersConfig } from '@kbn/test'; import path from 'path'; - import { MOCK_IDP_REALM_NAME } from '@kbn/mock-idp-utils'; - -import { dockerImage } from '@kbn/test-suites-xpack/fleet_api_integration/config.base'; import { REPO_ROOT } from '@kbn/repo-info'; import { STATEFUL_ROLES_ROOT_PATH } from '@kbn/es'; import type { ScoutServerConfig } from '../../types'; @@ -66,7 +63,7 @@ export const defaultConfig: ScoutServerConfig = { dockerServers: defineDockerServersConfig({ registry: { enabled: !!dockerRegistryPort, - image: dockerImage, + image: fleetPackageRegistryDockerImage, portInContainer: 8080, port: dockerRegistryPort, args: dockerArgs, diff --git a/packages/kbn-scout/tsconfig.json b/packages/kbn-scout/tsconfig.json index 4be38ce4c80fd..9a8fa16477ec2 100644 --- a/packages/kbn-scout/tsconfig.json +++ b/packages/kbn-scout/tsconfig.json @@ -25,7 +25,6 @@ "@kbn/es-archiver", "@kbn/dev-utils", "@kbn/mock-idp-utils", - "@kbn/test-suites-xpack", "@kbn/test-subj-selector", "@kbn/scout-info", "@kbn/scout-reporting" diff --git a/packages/kbn-test/index.ts b/packages/kbn-test/index.ts index cc97a4afa7906..725893b6c6a39 100644 --- a/packages/kbn-test/index.ts +++ b/packages/kbn-test/index.ts @@ -78,3 +78,10 @@ export * from './src/kbn_client'; export * from './src/find_test_plugin_paths'; export { getDockerFileMountPath } from '@kbn/es'; + +// Docker image to use for Fleet API integration tests. +// This image comes from the latest successful build of https://buildkite.com/elastic/kibana-package-registry-promote +// which is promoted after acceptance tests succeed against docker.elastic.co/package-registry/distribution:lite +export const fleetPackageRegistryDockerImage = + process.env.FLEET_PACKAGE_REGISTRY_DOCKER_IMAGE || + 'docker.elastic.co/kibana-ci/package-registry-distribution:lite'; diff --git a/x-pack/test/api_integration/deployment_agnostic/default_configs/serverless.config.base.ts b/x-pack/test/api_integration/deployment_agnostic/default_configs/serverless.config.base.ts index 8d3432f2e504e..2229c2774a066 100644 --- a/x-pack/test/api_integration/deployment_agnostic/default_configs/serverless.config.base.ts +++ b/x-pack/test/api_integration/deployment_agnostic/default_configs/serverless.config.base.ts @@ -4,11 +4,15 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { FtrConfigProviderContext, Config, defineDockerServersConfig } from '@kbn/test'; +import { + fleetPackageRegistryDockerImage, + FtrConfigProviderContext, + Config, + defineDockerServersConfig, +} from '@kbn/test'; import { ServerlessProjectType } from '@kbn/es'; import path from 'path'; -import { dockerImage } from '../../../fleet_api_integration/config.base'; import { DeploymentAgnosticCommonServices, services } from '../services'; interface CreateTestConfigOptions { @@ -88,7 +92,7 @@ export function createServerlessTestConfig { @@ -88,7 +88,7 @@ export function createStatefulTestConfig path.join(path.dirname(__filename), relativePath); -// Docker image to use for Fleet API integration tests. -// This image comes from the latest successful build of https://buildkite.com/elastic/kibana-package-registry-promote -// which is promoted after acceptance tests succeed against docker.elastic.co/package-registry/distribution:lite -export const dockerImage = - process.env.FLEET_PACKAGE_REGISTRY_DOCKER_IMAGE || - 'docker.elastic.co/kibana-ci/package-registry-distribution:lite'; export const BUNDLED_PACKAGE_DIR = '/tmp/fleet_bundled_packages'; @@ -47,7 +42,7 @@ export default async function ({ readConfigFile, log }: FtrConfigProviderContext ? defineDockerServersConfig({ registry: { enabled: !!registryPort, - image: dockerImage, + image: fleetPackageRegistryDockerImage, portInContainer: 8080, port: registryPort, args: dockerArgs, @@ -58,7 +53,9 @@ export default async function ({ readConfigFile, log }: FtrConfigProviderContext : undefined; if (skipRunningDockerRegistry) { - const cmd = `docker run ${dockerArgs.join(' ')} -p ${registryPort}:8080 ${dockerImage}`; + const cmd = `docker run ${dockerArgs.join( + ' ' + )} -p ${registryPort}:8080 ${fleetPackageRegistryDockerImage}`; log.warning(`Not running docker registry, you can run it with the following command: ${cmd}`); } diff --git a/x-pack/test/functional/config.base.js b/x-pack/test/functional/config.base.js index 24a4a63d53dda..3946623bfbd99 100644 --- a/x-pack/test/functional/config.base.js +++ b/x-pack/test/functional/config.base.js @@ -6,17 +6,9 @@ */ import { resolve } from 'path'; - import { services } from './services'; import { pageObjects } from './page_objects'; -// Docker image to use for Fleet API integration tests. -// This image comes from the latest successful build of https://buildkite.com/elastic/kibana-package-registry-promote -// which is promoted after acceptance tests succeed against docker.elastic.co/package-registry/distribution:lite -export const dockerImage = - process.env.FLEET_PACKAGE_REGISTRY_DOCKER_IMAGE || - 'docker.elastic.co/kibana-ci/package-registry-distribution:lite'; - // the default export of config files must be a config provider // that returns an object with the projects config values export default async function ({ readConfigFile }) { diff --git a/x-pack/test_serverless/shared/config.base.ts b/x-pack/test_serverless/shared/config.base.ts index 35fd64ee0b542..191e1a967f7b7 100644 --- a/x-pack/test_serverless/shared/config.base.ts +++ b/x-pack/test_serverless/shared/config.base.ts @@ -20,8 +20,7 @@ import { CA_CERT_PATH, kibanaDevServiceAccount } from '@kbn/dev-utils'; import { commonFunctionalServices } from '@kbn/ftr-common-functional-services'; import { MOCK_IDP_REALM_NAME } from '@kbn/mock-idp-utils'; import path from 'path'; -import { defineDockerServersConfig } from '@kbn/test'; -import { dockerImage } from '@kbn/test-suites-xpack/fleet_api_integration/config.base'; +import { fleetPackageRegistryDockerImage, defineDockerServersConfig } from '@kbn/test'; import { services } from './services'; export default async () => { @@ -66,7 +65,7 @@ export default async () => { dockerServers: defineDockerServersConfig({ registry: { enabled: !!dockerRegistryPort, - image: dockerImage, + image: fleetPackageRegistryDockerImage, portInContainer: 8080, port: dockerRegistryPort, args: dockerArgs, From a3d3c82900e0256ac3b4b1cea47bdbb10a416c24 Mon Sep 17 00:00:00 2001 From: "elastic-renovate-prod[bot]" <174716857+elastic-renovate-prod[bot]@users.noreply.github.com> Date: Fri, 10 Jan 2025 17:51:58 +0100 Subject: [PATCH 40/42] Update dependency @redocly/cli to ^1.27.0 (main) (#205158) Co-authored-by: elastic-renovate-prod[bot] <174716857+elastic-renovate-prod[bot]@users.noreply.github.com> Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- oas_docs/package-lock.json | 20 ++++++++++---------- oas_docs/package.json | 2 +- package.json | 2 +- yarn.lock | 19 +++++++++---------- 4 files changed, 21 insertions(+), 22 deletions(-) diff --git a/oas_docs/package-lock.json b/oas_docs/package-lock.json index 637641180323c..c6d3bb5e31641 100644 --- a/oas_docs/package-lock.json +++ b/oas_docs/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "license": "ISC", "dependencies": { - "@redocly/cli": "^1.26.0", + "@redocly/cli": "^1.27.0", "bump-cli": "^2.8.4" } }, @@ -515,12 +515,12 @@ } }, "node_modules/@redocly/cli": { - "version": "1.26.0", - "resolved": "https://registry.npmjs.org/@redocly/cli/-/cli-1.26.0.tgz", - "integrity": "sha512-yUsrTmEPHaBDQf16jSFGi2n+AJabHacLpr7La92Dseo0tRkgLKsUWiaVEEyqm79POBytD87mVTi3THitqnSyZQ==", + "version": "1.27.1", + "resolved": "https://registry.npmjs.org/@redocly/cli/-/cli-1.27.1.tgz", + "integrity": "sha512-IgFzIKgWDaGY8jOlWYVp7VyIwElIjqrVLvZWzdDS/vdQlJ0DdISQ1nRy/YSh0Rqw69hJOpgn5cXMH/SS/qO33Q==", "license": "MIT", "dependencies": { - "@redocly/openapi-core": "1.26.0", + "@redocly/openapi-core": "1.27.1", "abort-controller": "^3.0.0", "chokidar": "^3.5.1", "colorette": "^1.2.0", @@ -556,9 +556,9 @@ "license": "MIT" }, "node_modules/@redocly/openapi-core": { - "version": "1.26.0", - "resolved": "https://registry.npmjs.org/@redocly/openapi-core/-/openapi-core-1.26.0.tgz", - "integrity": "sha512-8Ofu6WpBp7eoLmf1qQ4+T0W4LRr8es+4Drw/RJG+acPXmaT2TmHk2B2v+3+1R9GqSIj6kx3N7JmQkxAPCnvDLw==", + "version": "1.27.1", + "resolved": "https://registry.npmjs.org/@redocly/openapi-core/-/openapi-core-1.27.1.tgz", + "integrity": "sha512-zQ47/A+Drk2Y75/af69MD3Oad4H9LxkUDzcm7XBkyLNDKIWQrDKDnS5476oDq77+zciymNxgMVtxxVXlnGS8kw==", "license": "MIT", "dependencies": { "@redocly/ajv": "^8.11.2", @@ -567,7 +567,6 @@ "https-proxy-agent": "^7.0.4", "js-levenshtein": "^1.1.6", "js-yaml": "^4.1.0", - "lodash.isequal": "^4.5.0", "minimatch": "^5.0.1", "node-fetch": "^2.6.1", "pluralize": "^8.0.0", @@ -2740,7 +2739,8 @@ "node_modules/lodash.isequal": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", - "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==" + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", + "peer": true }, "node_modules/loose-envify": { "version": "1.4.0", diff --git a/oas_docs/package.json b/oas_docs/package.json index 81ce46597c50d..ef981c7d891ee 100644 --- a/oas_docs/package.json +++ b/oas_docs/package.json @@ -8,7 +8,7 @@ }, "dependencies": { "bump-cli": "^2.8.4", - "@redocly/cli": "^1.26.0" + "@redocly/cli": "^1.27.0" }, "scripts": { "test": "echo \"Error: no test specified\" && exit 1" diff --git a/package.json b/package.json index 6131d545082de..018267b0388e5 100644 --- a/package.json +++ b/package.json @@ -1527,7 +1527,7 @@ "@octokit/rest": "^17.11.2", "@parcel/watcher": "^2.1.0", "@playwright/test": "1.49.0", - "@redocly/cli": "^1.26.0", + "@redocly/cli": "^1.27.0", "@statoscope/webpack-plugin": "^5.28.2", "@storybook/addon-a11y": "^6.5.16", "@storybook/addon-actions": "^6.5.16", diff --git a/yarn.lock b/yarn.lock index 0720d461e34cf..8a9761c70c8c5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9216,12 +9216,12 @@ require-from-string "^2.0.2" uri-js-replace "^1.0.1" -"@redocly/cli@^1.26.0": - version "1.26.0" - resolved "https://registry.yarnpkg.com/@redocly/cli/-/cli-1.26.0.tgz#0f5707f55d833993ffc9644fed7d85fcafe37877" - integrity sha512-yUsrTmEPHaBDQf16jSFGi2n+AJabHacLpr7La92Dseo0tRkgLKsUWiaVEEyqm79POBytD87mVTi3THitqnSyZQ== +"@redocly/cli@^1.27.0": + version "1.27.1" + resolved "https://registry.yarnpkg.com/@redocly/cli/-/cli-1.27.1.tgz#7ae671615c2f6e6d049fce1affe3aa19f308b5a6" + integrity sha512-IgFzIKgWDaGY8jOlWYVp7VyIwElIjqrVLvZWzdDS/vdQlJ0DdISQ1nRy/YSh0Rqw69hJOpgn5cXMH/SS/qO33Q== dependencies: - "@redocly/openapi-core" "1.26.0" + "@redocly/openapi-core" "1.27.1" abort-controller "^3.0.0" chokidar "^3.5.1" colorette "^1.2.0" @@ -9246,10 +9246,10 @@ resolved "https://registry.yarnpkg.com/@redocly/config/-/config-0.17.1.tgz#2def04cecf440dd78c0f102f53f3444fac050768" integrity sha512-CEmvaJuG7pm2ylQg53emPmtgm4nW2nxBgwXzbVEHpGas/lGnMyN8Zlkgiz6rPw0unASg6VW3wlz27SOL5XFHYQ== -"@redocly/openapi-core@1.26.0", "@redocly/openapi-core@^1.4.0": - version "1.26.0" - resolved "https://registry.yarnpkg.com/@redocly/openapi-core/-/openapi-core-1.26.0.tgz#ffd6c3d9f7e98c4b61584d5c0511cf953d1cfade" - integrity sha512-8Ofu6WpBp7eoLmf1qQ4+T0W4LRr8es+4Drw/RJG+acPXmaT2TmHk2B2v+3+1R9GqSIj6kx3N7JmQkxAPCnvDLw== +"@redocly/openapi-core@1.27.1", "@redocly/openapi-core@^1.4.0": + version "1.27.1" + resolved "https://registry.yarnpkg.com/@redocly/openapi-core/-/openapi-core-1.27.1.tgz#53b6b6be0ffecf696a1da5aee84fe989cdf6d764" + integrity sha512-zQ47/A+Drk2Y75/af69MD3Oad4H9LxkUDzcm7XBkyLNDKIWQrDKDnS5476oDq77+zciymNxgMVtxxVXlnGS8kw== dependencies: "@redocly/ajv" "^8.11.2" "@redocly/config" "^0.17.0" @@ -9257,7 +9257,6 @@ https-proxy-agent "^7.0.4" js-levenshtein "^1.1.6" js-yaml "^4.1.0" - lodash.isequal "^4.5.0" minimatch "^5.0.1" node-fetch "^2.6.1" pluralize "^8.0.0" From baf79bcd3556b150d1c9e80038d54403ff362181 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20Haro?= Date: Fri, 10 Jan 2025 17:58:31 +0100 Subject: [PATCH 41/42] [ES `body` removal] `@elastic/security-detections-response` (#204879) --- .../detections_response/alerts/get_alerts_by_id.ts | 2 +- .../detections_response/alerts/get_alerts_by_ids.ts | 2 +- .../legacy_actions/get_legacy_action_notifications_so_by_id.ts | 2 +- .../utils/actions/legacy_actions/get_legacy_action_so.ts | 2 +- .../utils/actions/legacy_actions/get_legacy_actions_so_by_id.ts | 2 +- .../utils/data_generator/data_generator_factory.ts | 2 +- .../detections_response/utils/data_generator/index_documents.ts | 2 +- .../detections_response/utils/rules/get_rule_so_by_id.ts | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/x-pack/test/common/utils/security_solution/detections_response/alerts/get_alerts_by_id.ts b/x-pack/test/common/utils/security_solution/detections_response/alerts/get_alerts_by_id.ts index 92791edd5ea7e..c13759c935d4c 100644 --- a/x-pack/test/common/utils/security_solution/detections_response/alerts/get_alerts_by_id.ts +++ b/x-pack/test/common/utils/security_solution/detections_response/alerts/get_alerts_by_id.ts @@ -7,7 +7,7 @@ import type { ToolingLog } from '@kbn/tooling-log'; import type SuperTest from 'supertest'; -import { SearchResponse } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { SearchResponse } from '@elastic/elasticsearch/lib/api/types'; import type { DetectionAlert } from '@kbn/security-solution-plugin/common/api/detection_engine'; import { DETECTION_ENGINE_QUERY_SIGNALS_URL as DETECTION_ENGINE_QUERY_ALERTS_URL } from '@kbn/security-solution-plugin/common/constants'; diff --git a/x-pack/test/common/utils/security_solution/detections_response/alerts/get_alerts_by_ids.ts b/x-pack/test/common/utils/security_solution/detections_response/alerts/get_alerts_by_ids.ts index d090a39ff3779..9cee570b8e804 100644 --- a/x-pack/test/common/utils/security_solution/detections_response/alerts/get_alerts_by_ids.ts +++ b/x-pack/test/common/utils/security_solution/detections_response/alerts/get_alerts_by_ids.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { SearchResponse } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { SearchResponse } from '@elastic/elasticsearch/lib/api/types'; import type { ToolingLog } from '@kbn/tooling-log'; import type SuperTest from 'supertest'; import type { DetectionAlert } from '@kbn/security-solution-plugin/common/api/detection_engine'; diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/actions/legacy_actions/get_legacy_action_notifications_so_by_id.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/actions/legacy_actions/get_legacy_action_notifications_so_by_id.ts index bed1197fc91ee..36811a1608385 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/actions/legacy_actions/get_legacy_action_notifications_so_by_id.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/actions/legacy_actions/get_legacy_action_notifications_so_by_id.ts @@ -6,7 +6,7 @@ */ import type { Client } from '@elastic/elasticsearch'; -import type { SearchResponse } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import type { SearchResponse } from '@elastic/elasticsearch/lib/api/types'; import { ALERTING_CASES_SAVED_OBJECT_INDEX } from '@kbn/core-saved-objects-server'; import { SavedObjectReference } from '@kbn/core/server'; import { LegacyRuleNotificationRuleTypeParams } from '@kbn/security-solution-plugin/server/lib/detection_engine/rule_actions_legacy'; diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/actions/legacy_actions/get_legacy_action_so.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/actions/legacy_actions/get_legacy_action_so.ts index e714dfcec28cc..e1736374f31ce 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/actions/legacy_actions/get_legacy_action_so.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/actions/legacy_actions/get_legacy_action_so.ts @@ -5,7 +5,7 @@ * 2.0. */ import type { Client } from '@elastic/elasticsearch'; -import type { SearchResponse } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import type { SearchResponse } from '@elastic/elasticsearch/lib/api/types'; import { SECURITY_SOLUTION_SAVED_OBJECT_INDEX } from '@kbn/core-saved-objects-server'; import type { SavedObjectReference } from '@kbn/core/server'; import type { LegacyRuleActions } from '@kbn/security-solution-plugin/server/lib/detection_engine/rule_actions_legacy'; diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/actions/legacy_actions/get_legacy_actions_so_by_id.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/actions/legacy_actions/get_legacy_actions_so_by_id.ts index 9e6b6a31e9786..da72be85b6c2e 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/actions/legacy_actions/get_legacy_actions_so_by_id.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/actions/legacy_actions/get_legacy_actions_so_by_id.ts @@ -6,7 +6,7 @@ */ import type { Client } from '@elastic/elasticsearch'; -import type { SearchResponse } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import type { SearchResponse } from '@elastic/elasticsearch/lib/api/types'; import { SECURITY_SOLUTION_SAVED_OBJECT_INDEX } from '@kbn/core-saved-objects-server'; import { SavedObjectReference } from '@kbn/core/server'; import type { LegacyRuleActions } from '@kbn/security-solution-plugin/server/lib/detection_engine/rule_actions_legacy'; diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/data_generator/data_generator_factory.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/data_generator/data_generator_factory.ts index 7842d105e40b0..d6d4d9d4751b8 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/data_generator/data_generator_factory.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/data_generator/data_generator_factory.ts @@ -7,7 +7,7 @@ import type { Client } from '@elastic/elasticsearch'; import { ToolingLog } from '@kbn/tooling-log'; -import type { BulkResponse } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import type { BulkResponse } from '@elastic/elasticsearch/lib/api/types'; import { indexDocuments } from './index_documents'; import { generateDocuments } from './generate_documents'; import { enhanceDocuments, EnhanceDocumentsOptions } from './enhance_documents'; diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/data_generator/index_documents.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/data_generator/index_documents.ts index 5408e11b25015..bb76e723f9563 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/data_generator/index_documents.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/data_generator/index_documents.ts @@ -6,7 +6,7 @@ */ import type { Client } from '@elastic/elasticsearch'; -import type { BulkResponse } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import type { BulkResponse } from '@elastic/elasticsearch/lib/api/types'; import { ToolingLog } from '@kbn/tooling-log'; interface IndexDocumentsParams { diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/rules/get_rule_so_by_id.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/rules/get_rule_so_by_id.ts index 584b49e379ee7..694a1cf516cb4 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/rules/get_rule_so_by_id.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/rules/get_rule_so_by_id.ts @@ -8,7 +8,7 @@ import type { Client } from '@elastic/elasticsearch'; import { SavedObjectReference } from '@kbn/core/server'; import { ALERTING_CASES_SAVED_OBJECT_INDEX } from '@kbn/core-saved-objects-server'; -import type { SearchResponse } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import type { SearchResponse } from '@elastic/elasticsearch/lib/api/types'; import { Rule } from '@kbn/alerting-plugin/common'; import { BaseRuleParams } from '@kbn/security-solution-plugin/server/lib/detection_engine/rule_schema'; From 05857120125f43bb1df71ba73f1fb8582b4528ee Mon Sep 17 00:00:00 2001 From: Kylie Meli Date: Fri, 10 Jan 2025 12:09:39 -0500 Subject: [PATCH 42/42] [Automatic Import] Migrating to UX design and adding support for generating auth (#202587) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary This PR adds authentication to the generated CEL programs, and overhauls the UI flow to be closer to the forthcoming UX design. ## Details This PR provides the following updates related to CEL generation: 1. Adds support for generating auth - basic, oauth2, digest and api tokens 2. Adds new tooling for working with the OpenAPI specs and support for reducing the spec to minimally required information to the LLM (new Kibana dep on [oas](https://www.npmjs.com/package/oas)) 3. Addresses various feedback around the generated CEL program (error handling, cursor, trimming the state.url, etc) 4. Migrates the CEL flow to be closer to the forthcoming design specified by UX, now within a flyout on the datastream step. 5. Removes the dependency on the CEL generation feature flag ## Current screenshots
Click me the datastream setup page: Screenshot 2024-12-13 at 4 33 28 PM the user selects the cel input and the button to configure shows up: Screenshot 2024-12-13 at 4 33 49 PM upon clicking the button, the flyout opens: Screenshot 2024-12-13 at 4 34 02 PM the user can upload the spec file (a json or yaml openapi file): Screenshot 2024-12-13 at 4 34 30 PM the llm will suggest paths to use, or the user can select to enter manually and view all the GETs Screenshot 2024-12-13 at 4 35 26 PM we will also suggest an auth method based on the spec, but allow the user to select otherwise if they want: Screenshot 2024-12-13 at 4 35 37 PM if they choose an auth the spec doesn't specify, we will warn but not block: Screenshot 2024-12-16 at 9 07 52 AM once path and auth are selected, they can save and continue (generate the cel config): Screenshot 2024-12-13 at 4 35 50 PM generating: Screenshot 2024-12-13 at 4 36 18 PM all configured: Screenshot 2024-12-13 at 4 36 35 PM
## Sample results > **_Note:_** All these sample integrations are built with the teleport log samples. ### API key [eset.json](https://github.com/user-attachments/files/18151638/eset.json) [eset___api_key-1.0.0.zip](https://github.com/user-attachments/files/18151622/eset___api_key-1.0.0.zip) ### OAuth2 [bitwarden.json](https://github.com/user-attachments/files/18151635/bitwarden.json) [bitwarden___oauth-1.0.0.zip](https://github.com/user-attachments/files/18151618/bitwarden___oauth-1.0.0.zip) ### Basic [sumlogic-api.yaml.zip](https://github.com/user-attachments/files/18151650/sumlogic-api.yaml.zip) [sumologic___basic-1.0.0.zip](https://github.com/user-attachments/files/18151630/sumologic___basic-1.0.0.zip) Relates: - https://github.com/elastic/kibana/issues/197651 - https://github.com/elastic/kibana/issues/197653 --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Ilya Nikokoshev --- package.json | 2 + .../translations/translations/fr-FR.json | 18 - .../translations/translations/ja-JP.json | 18 - .../translations/translations/zh-CN.json | 18 - .../__jest__/fixtures/api_analysis.ts | 22 + .../__jest__/fixtures/cel.ts | 38 +- .../__jest__/fixtures/index.ts | 15 +- .../api/analyze_api/analyze_api_route.gen.ts | 33 + .../analyze_api/analyze_api_route.schema.yaml | 39 + .../api/analyze_api/analyze_api_route.test.ts | 20 + .../common/api/cel/cel_input_route.gen.ts | 8 +- .../api/cel/cel_input_route.schema.yaml | 12 +- .../common/api/model/api_test.mock.ts | 19 +- .../api/model/cel_input_attributes.gen.ts | 47 +- .../model/cel_input_attributes.schema.yaml | 81 +- .../common/api/model/response_schemas.gen.ts | 11 +- .../api/model/response_schemas.schema.yaml | 17 +- .../integration_assistant/common/constants.ts | 1 + .../common/experimental_features.ts | 1 + .../integration_assistant/common/index.ts | 4 +- .../docs/imgs/analyze_api_graph.png | Bin 0 -> 12498 bytes .../docs/imgs/cel_graph.png | Bin 23312 -> 75906 bytes .../public/common/lib/api.ts | 18 +- .../create_integration_assistant.test.tsx | 328 +---- .../create_integration_assistant.tsx | 71 +- .../cel_configuration/create_cel_config.tsx | 109 ++ .../flyout/cel_configuration/footer.tsx | 93 ++ .../cel_configuration/index.ts} | 5 +- .../auth_selection.tsx | 74 ++ .../cel_confirm_step.tsx | 154 +++ .../endpoint_selection.tsx | 116 ++ .../generation_modal.tsx | 255 ++++ .../steps/cel_confirm_settings_step}/index.ts | 3 +- .../is_step_ready.ts | 11 + .../cel_confirm_settings_step/translations.ts | 88 ++ .../cel_input_step/api_definition_input.tsx | 173 +++ .../steps/cel_input_step/cel_input_step.tsx | 103 ++ .../steps/cel_input_step/generation_modal.tsx | 67 +- .../steps/cel_input_step/index.ts | 0 .../steps/cel_input_step/is_step_ready.ts | 11 + .../steps/cel_input_step/translations.ts | 101 ++ .../flyout/cel_configuration/translations.ts | 37 + .../footer/footer.tsx | 20 +- .../footer/translations.ts | 4 - .../mocks/state.ts | 15 +- .../create_integration_assistant/state.ts | 18 +- .../cel_input_step/api_definition_input.tsx | 118 -- .../steps/cel_input_step/cel_input_step.tsx | 71 -- .../steps/cel_input_step/is_step_ready.ts | 17 - .../steps/cel_input_step/translations.ts | 87 -- .../data_stream_step/data_stream_name.tsx | 37 + .../data_stream_step.test.tsx | 41 + .../data_stream_step/data_stream_step.tsx | 71 +- .../steps/data_stream_step/translations.ts | 51 + .../review_cel_step/cel_config_results.tsx | 65 - .../steps/review_cel_step/review_cel_step.tsx | 43 - .../steps/review_cel_step/translations.ts | 30 - .../create_integration_assistant/types.ts | 18 +- .../public/util/oas.test.ts | 1094 +++++++++++++++++ .../integration_assistant/public/util/oas.ts | 181 +++ .../scripts/draw_graphs_script.ts | 2 + .../server/graphs/api_analysis/constants.ts | 8 + .../server/graphs/api_analysis/graph.test.ts | 61 + .../server/graphs/api_analysis/graph.ts | 68 + .../server/graphs/api_analysis/index.ts | 8 + .../server/graphs/api_analysis/paths.test.ts | 28 + .../server/graphs/api_analysis/paths.ts | 30 + .../server/graphs/api_analysis/prompts.ts | 43 + .../server/graphs/api_analysis/types.ts | 20 + .../server/graphs/cel/analyze_headers.test.ts | 28 + .../server/graphs/cel/analyze_headers.ts | 28 + .../server/graphs/cel/auth_basic.test.ts | 28 + .../server/graphs/cel/auth_basic.ts | 31 + .../server/graphs/cel/auth_digest.test.ts | 28 + .../server/graphs/cel/auth_digest.ts | 30 + .../server/graphs/cel/auth_header.ts | 29 + .../server/graphs/cel/auth_oauth2.test.ts | 28 + .../server/graphs/cel/auth_oauth2.ts | 31 + .../server/graphs/cel/build_program.ts | 3 +- .../server/graphs/cel/constants.ts | 210 +++- .../server/graphs/cel/graph.test.ts | 170 ++- .../server/graphs/cel/graph.ts | 119 +- .../server/graphs/cel/prompts.ts | 201 ++- .../graphs/cel/retrieve_state_details.test.ts | 1 + .../graphs/cel/retrieve_state_details.ts | 10 +- .../server/graphs/cel/summarize_query.ts | 3 +- .../server/graphs/cel/types.ts | 5 +- .../server/graphs/cel/util.test.ts | 12 +- .../server/graphs/cel/util.ts | 17 + .../server/integration_builder/agent.test.ts | 40 +- .../server/integration_builder/agent.ts | 36 +- .../build_integration.test.ts | 12 +- .../integration_builder/build_integration.ts | 2 +- .../server/integration_builder/constants.ts | 11 + .../integration_builder/data_stream.test.ts | 17 +- .../server/integration_builder/data_stream.ts | 55 +- .../integration_assistant/server/plugin.ts | 7 +- .../server/routes/analyze_api_route.test.ts | 69 ++ .../server/routes/analyze_api_route.ts | 110 ++ .../server/routes/cel_route.test.ts | 22 +- .../server/routes/cel_routes.ts | 12 +- .../server/routes/register_routes.ts | 12 +- .../templates/agent/cel_generated.yml.hbs.njk | 141 +++ .../templates/manifest/cel_manifest.yml.njk | 41 +- .../integration_assistant/server/types.ts | 19 +- yarn.lock | 224 +++- 106 files changed, 5238 insertions(+), 1094 deletions(-) create mode 100644 x-pack/platform/plugins/shared/integration_assistant/__jest__/fixtures/api_analysis.ts create mode 100644 x-pack/platform/plugins/shared/integration_assistant/common/api/analyze_api/analyze_api_route.gen.ts create mode 100644 x-pack/platform/plugins/shared/integration_assistant/common/api/analyze_api/analyze_api_route.schema.yaml create mode 100644 x-pack/platform/plugins/shared/integration_assistant/common/api/analyze_api/analyze_api_route.test.ts create mode 100644 x-pack/platform/plugins/shared/integration_assistant/docs/imgs/analyze_api_graph.png create mode 100644 x-pack/platform/plugins/shared/integration_assistant/public/components/create_integration/create_integration_assistant/flyout/cel_configuration/create_cel_config.tsx create mode 100644 x-pack/platform/plugins/shared/integration_assistant/public/components/create_integration/create_integration_assistant/flyout/cel_configuration/footer.tsx rename x-pack/platform/plugins/shared/integration_assistant/public/components/create_integration/create_integration_assistant/{steps/review_cel_step/is_step_ready.ts => flyout/cel_configuration/index.ts} (57%) create mode 100644 x-pack/platform/plugins/shared/integration_assistant/public/components/create_integration/create_integration_assistant/flyout/cel_configuration/steps/cel_confirm_settings_step/auth_selection.tsx create mode 100644 x-pack/platform/plugins/shared/integration_assistant/public/components/create_integration/create_integration_assistant/flyout/cel_configuration/steps/cel_confirm_settings_step/cel_confirm_step.tsx create mode 100644 x-pack/platform/plugins/shared/integration_assistant/public/components/create_integration/create_integration_assistant/flyout/cel_configuration/steps/cel_confirm_settings_step/endpoint_selection.tsx create mode 100644 x-pack/platform/plugins/shared/integration_assistant/public/components/create_integration/create_integration_assistant/flyout/cel_configuration/steps/cel_confirm_settings_step/generation_modal.tsx rename x-pack/platform/plugins/shared/integration_assistant/public/components/create_integration/create_integration_assistant/{steps/review_cel_step => flyout/cel_configuration/steps/cel_confirm_settings_step}/index.ts (84%) create mode 100644 x-pack/platform/plugins/shared/integration_assistant/public/components/create_integration/create_integration_assistant/flyout/cel_configuration/steps/cel_confirm_settings_step/is_step_ready.ts create mode 100644 x-pack/platform/plugins/shared/integration_assistant/public/components/create_integration/create_integration_assistant/flyout/cel_configuration/steps/cel_confirm_settings_step/translations.ts create mode 100644 x-pack/platform/plugins/shared/integration_assistant/public/components/create_integration/create_integration_assistant/flyout/cel_configuration/steps/cel_input_step/api_definition_input.tsx create mode 100644 x-pack/platform/plugins/shared/integration_assistant/public/components/create_integration/create_integration_assistant/flyout/cel_configuration/steps/cel_input_step/cel_input_step.tsx rename x-pack/platform/plugins/shared/integration_assistant/public/components/create_integration/create_integration_assistant/{ => flyout/cel_configuration}/steps/cel_input_step/generation_modal.tsx (77%) rename x-pack/platform/plugins/shared/integration_assistant/public/components/create_integration/create_integration_assistant/{ => flyout/cel_configuration}/steps/cel_input_step/index.ts (100%) create mode 100644 x-pack/platform/plugins/shared/integration_assistant/public/components/create_integration/create_integration_assistant/flyout/cel_configuration/steps/cel_input_step/is_step_ready.ts create mode 100644 x-pack/platform/plugins/shared/integration_assistant/public/components/create_integration/create_integration_assistant/flyout/cel_configuration/steps/cel_input_step/translations.ts create mode 100644 x-pack/platform/plugins/shared/integration_assistant/public/components/create_integration/create_integration_assistant/flyout/cel_configuration/translations.ts delete mode 100644 x-pack/platform/plugins/shared/integration_assistant/public/components/create_integration/create_integration_assistant/steps/cel_input_step/api_definition_input.tsx delete mode 100644 x-pack/platform/plugins/shared/integration_assistant/public/components/create_integration/create_integration_assistant/steps/cel_input_step/cel_input_step.tsx delete mode 100644 x-pack/platform/plugins/shared/integration_assistant/public/components/create_integration/create_integration_assistant/steps/cel_input_step/is_step_ready.ts delete mode 100644 x-pack/platform/plugins/shared/integration_assistant/public/components/create_integration/create_integration_assistant/steps/cel_input_step/translations.ts create mode 100644 x-pack/platform/plugins/shared/integration_assistant/public/components/create_integration/create_integration_assistant/steps/data_stream_step/data_stream_name.tsx delete mode 100644 x-pack/platform/plugins/shared/integration_assistant/public/components/create_integration/create_integration_assistant/steps/review_cel_step/cel_config_results.tsx delete mode 100644 x-pack/platform/plugins/shared/integration_assistant/public/components/create_integration/create_integration_assistant/steps/review_cel_step/review_cel_step.tsx delete mode 100644 x-pack/platform/plugins/shared/integration_assistant/public/components/create_integration/create_integration_assistant/steps/review_cel_step/translations.ts create mode 100644 x-pack/platform/plugins/shared/integration_assistant/public/util/oas.test.ts create mode 100644 x-pack/platform/plugins/shared/integration_assistant/public/util/oas.ts create mode 100644 x-pack/platform/plugins/shared/integration_assistant/server/graphs/api_analysis/constants.ts create mode 100644 x-pack/platform/plugins/shared/integration_assistant/server/graphs/api_analysis/graph.test.ts create mode 100644 x-pack/platform/plugins/shared/integration_assistant/server/graphs/api_analysis/graph.ts create mode 100644 x-pack/platform/plugins/shared/integration_assistant/server/graphs/api_analysis/index.ts create mode 100644 x-pack/platform/plugins/shared/integration_assistant/server/graphs/api_analysis/paths.test.ts create mode 100644 x-pack/platform/plugins/shared/integration_assistant/server/graphs/api_analysis/paths.ts create mode 100644 x-pack/platform/plugins/shared/integration_assistant/server/graphs/api_analysis/prompts.ts create mode 100644 x-pack/platform/plugins/shared/integration_assistant/server/graphs/api_analysis/types.ts create mode 100644 x-pack/platform/plugins/shared/integration_assistant/server/graphs/cel/analyze_headers.test.ts create mode 100644 x-pack/platform/plugins/shared/integration_assistant/server/graphs/cel/analyze_headers.ts create mode 100644 x-pack/platform/plugins/shared/integration_assistant/server/graphs/cel/auth_basic.test.ts create mode 100644 x-pack/platform/plugins/shared/integration_assistant/server/graphs/cel/auth_basic.ts create mode 100644 x-pack/platform/plugins/shared/integration_assistant/server/graphs/cel/auth_digest.test.ts create mode 100644 x-pack/platform/plugins/shared/integration_assistant/server/graphs/cel/auth_digest.ts create mode 100644 x-pack/platform/plugins/shared/integration_assistant/server/graphs/cel/auth_header.ts create mode 100644 x-pack/platform/plugins/shared/integration_assistant/server/graphs/cel/auth_oauth2.test.ts create mode 100644 x-pack/platform/plugins/shared/integration_assistant/server/graphs/cel/auth_oauth2.ts create mode 100644 x-pack/platform/plugins/shared/integration_assistant/server/routes/analyze_api_route.test.ts create mode 100644 x-pack/platform/plugins/shared/integration_assistant/server/routes/analyze_api_route.ts create mode 100644 x-pack/platform/plugins/shared/integration_assistant/server/templates/agent/cel_generated.yml.hbs.njk diff --git a/package.json b/package.json index 018267b0388e5..510a94f8a2dc1 100644 --- a/package.json +++ b/package.json @@ -1173,6 +1173,7 @@ "json-stable-stringify": "^1.0.1", "json-stringify-pretty-compact": "1.2.0", "json-stringify-safe": "5.0.1", + "jsonpath-plus": "^10.2.0", "jsonwebtoken": "^9.0.2", "jsts": "^1.6.2", "kea": "^2.6.0", @@ -1204,6 +1205,7 @@ "nodemailer": "^6.9.15", "normalize-path": "^3.0.0", "nunjucks": "^3.2.4", + "oas": "^25.2.0", "object-hash": "^1.3.1", "object-path-immutable": "^3.1.1", "openai": "^4.72.0", diff --git a/x-pack/platform/plugins/private/translations/translations/fr-FR.json b/x-pack/platform/plugins/private/translations/translations/fr-FR.json index 42de200cec13b..4f2e66adc349a 100644 --- a/x-pack/platform/plugins/private/translations/translations/fr-FR.json +++ b/x-pack/platform/plugins/private/translations/translations/fr-FR.json @@ -23845,7 +23845,6 @@ "xpack.ingestPipelines.testPipelineFlyout.successNotificationText": "Pipeline exécuté", "xpack.ingestPipelines.testPipelineFlyout.title": "Pipeline de test", "xpack.integrationAssistant.bottomBar.addToElastic": "Ajouter à Elastic", - "xpack.integrationAssistant.bottomBar.analyzeCel": "Générer la configuration d'entrée CEL", "xpack.integrationAssistant.bottomBar.analyzeLogs": "Analyser les logs", "xpack.integrationAssistant.bottomBar.close": "Fermer", "xpack.integrationAssistant.bottomBar.loading": "Chargement", @@ -23901,17 +23900,6 @@ "xpack.integrationAssistant.missingPrivileges.title": "Privilèges manquants", "xpack.integrationAssistant.pages.header.avatarTitle": "Alimenté par l’IA générative", "xpack.integrationAssistant.pages.header.title": "Nouvelle intégration", - "xpack.integrationAssistant.step.celInput.apiDefinition.description": "Glissez et déposez un fichier ou parcourez les fichiers.", - "xpack.integrationAssistant.step.celInput.apiDefinition.description2": "Spécifications relatives à OpenAPI", - "xpack.integrationAssistant.step.celInput.apiDefinition.label": "Spéc. relatives à OpenAPI", - "xpack.integrationAssistant.step.celInput.celInputDescription": "Chargez un fichier de spécifications OpenAPI pour générer une configuration pour l'entrée CEL", - "xpack.integrationAssistant.step.celInput.celInputTitle": "Générer la configuration d'entrée CEL", - "xpack.integrationAssistant.step.celInput.generationError": "Une erreur s'est produite durant : Génération d'entrée CEL", - "xpack.integrationAssistant.step.celInput.openapiSpec.errorCanNotRead": "Impossible de lire le fichier de logs exemple", - "xpack.integrationAssistant.step.celInput.openapiSpec.errorCanNotReadWithReason": "Une erreur s'est produite lors de la lecture du fichier de spécifications : {reason}", - "xpack.integrationAssistant.step.celInput.openapiSpec.errorTooLargeToParse": "Ce fichier de spécifications est trop volumineux pour être analysé", - "xpack.integrationAssistant.step.celInput.progress.relatedGraph": "Génération de la configuration d'entrée CEL", - "xpack.integrationAssistant.step.celInput.retryButtonLabel": "Réessayer", "xpack.integrationAssistant.step.connector": "Connecteur", "xpack.integrationAssistant.step.dataStream": "Flux de données", "xpack.integrationAssistant.step.dataStream.analyzing": "Analyse", @@ -23963,12 +23951,6 @@ "xpack.integrationAssistant.step.review.ingestPipelineTitle": "Pipeline d'ingestion", "xpack.integrationAssistant.step.review.save": "Enregistrer", "xpack.integrationAssistant.step.review.title": "Examiner les résultats", - "xpack.integrationAssistant.step.reviewCel.description": "Vérifiez les paramètres de configuration d'entrée CEL générés pour votre intégration. Ces paramètres seront automatiquement renseignés dans la configuration d'intégration où la modification sera possible.", - "xpack.integrationAssistant.step.reviewCel.program": "Le programme CEL à exécuter pour chaque sondage", - "xpack.integrationAssistant.step.reviewCel.redact": "Champs à adapter", - "xpack.integrationAssistant.step.reviewCel.save": "Enregistrer", - "xpack.integrationAssistant.step.reviewCel.state": "État initial de l'évaluation du CEL", - "xpack.integrationAssistant.step.reviewCel.title": "Examiner les résultats", "xpack.integrationAssistant.steps.connector.createConnectorLabel": "Créer un nouveau connecteur", "xpack.integrationAssistant.steps.connector.description": "Sélectionnez un connecteur d’IA pour vous aider à créer votre intégration personnalisée", "xpack.integrationAssistant.steps.connector.supportedModelsInfo": "Pour une expérience optimale, nous recommandons actuellement d'utiliser un fournisseur prenant en charge les nouveaux modèles Claude. Nous travaillons actuellement à l'ajout d'une meilleure prise en charge de GPT-4 et de modèles similaires", diff --git a/x-pack/platform/plugins/private/translations/translations/ja-JP.json b/x-pack/platform/plugins/private/translations/translations/ja-JP.json index 2303c208517ba..dd966042483a8 100644 --- a/x-pack/platform/plugins/private/translations/translations/ja-JP.json +++ b/x-pack/platform/plugins/private/translations/translations/ja-JP.json @@ -23706,7 +23706,6 @@ "xpack.ingestPipelines.testPipelineFlyout.successNotificationText": "パイプラインが実行されました", "xpack.ingestPipelines.testPipelineFlyout.title": "パイプラインをテスト", "xpack.integrationAssistant.bottomBar.addToElastic": "Elasticに追加", - "xpack.integrationAssistant.bottomBar.analyzeCel": "CEL入力構成を生成", "xpack.integrationAssistant.bottomBar.analyzeLogs": "ログを分析", "xpack.integrationAssistant.bottomBar.close": "閉じる", "xpack.integrationAssistant.bottomBar.loading": "読み込み中", @@ -23762,17 +23761,6 @@ "xpack.integrationAssistant.missingPrivileges.title": "不足している権限", "xpack.integrationAssistant.pages.header.avatarTitle": "生成AIを活用", "xpack.integrationAssistant.pages.header.title": "新しい統合", - "xpack.integrationAssistant.step.celInput.apiDefinition.description": "ファイルをドラッグアンドドロップするか、ファイルを参照します。", - "xpack.integrationAssistant.step.celInput.apiDefinition.description2": "OpenAPI仕様", - "xpack.integrationAssistant.step.celInput.apiDefinition.label": "OpenAPI仕様", - "xpack.integrationAssistant.step.celInput.celInputDescription": "OpenAPI仕様ファイルをアップロードして、CEL入力の構成を生成", - "xpack.integrationAssistant.step.celInput.celInputTitle": "CEL入力構成を生成", - "xpack.integrationAssistant.step.celInput.generationError": "エラーが発生しました:CEL入力生成", - "xpack.integrationAssistant.step.celInput.openapiSpec.errorCanNotRead": "ログサンプルファイルを読み取れませんでした", - "xpack.integrationAssistant.step.celInput.openapiSpec.errorCanNotReadWithReason": "仕様ファイルの読み取り中にエラーが発生しました:{reason}", - "xpack.integrationAssistant.step.celInput.openapiSpec.errorTooLargeToParse": "この仕様ファイルは大きすぎて解析できません", - "xpack.integrationAssistant.step.celInput.progress.relatedGraph": "CEL入力構成を生成中", - "xpack.integrationAssistant.step.celInput.retryButtonLabel": "再試行", "xpack.integrationAssistant.step.connector": "コネクター", "xpack.integrationAssistant.step.dataStream": "データストリーム", "xpack.integrationAssistant.step.dataStream.analyzing": "分析中", @@ -23824,12 +23812,6 @@ "xpack.integrationAssistant.step.review.ingestPipelineTitle": "パイプラインを投入", "xpack.integrationAssistant.step.review.save": "保存", "xpack.integrationAssistant.step.review.title": "結果を確認", - "xpack.integrationAssistant.step.reviewCel.description": "統合の生成されたCEL入力構成設定を確認してください。これらの設定は、編集が可能な場合、統合構成に自動的に入力されます。", - "xpack.integrationAssistant.step.reviewCel.program": "各ポーリングで実行されるCELプログラム", - "xpack.integrationAssistant.step.reviewCel.redact": "改訂されたフィールド", - "xpack.integrationAssistant.step.reviewCel.save": "保存", - "xpack.integrationAssistant.step.reviewCel.state": "初期CEL評価状態", - "xpack.integrationAssistant.step.reviewCel.title": "結果を確認", "xpack.integrationAssistant.steps.connector.createConnectorLabel": "新しいコネクターを作成", "xpack.integrationAssistant.steps.connector.description": "カスタム統合の作成を支援するAIコネクターを選択", "xpack.integrationAssistant.steps.connector.supportedModelsInfo": "現在、最高のエクスペリエンスが得られるように、新しいClaudeモデルをサポートするプロバイダーを利用することをお勧めします。現在、GPT-4や類似モデルへの改善されたサポートの追加に取り組んでいます。", diff --git a/x-pack/platform/plugins/private/translations/translations/zh-CN.json b/x-pack/platform/plugins/private/translations/translations/zh-CN.json index e56d56268807d..c722363c6b6c9 100644 --- a/x-pack/platform/plugins/private/translations/translations/zh-CN.json +++ b/x-pack/platform/plugins/private/translations/translations/zh-CN.json @@ -23297,7 +23297,6 @@ "xpack.ingestPipelines.testPipelineFlyout.successNotificationText": "管道已执行", "xpack.ingestPipelines.testPipelineFlyout.title": "测试管道", "xpack.integrationAssistant.bottomBar.addToElastic": "添加到 Elastic", - "xpack.integrationAssistant.bottomBar.analyzeCel": "生成 CEL 输入配置", "xpack.integrationAssistant.bottomBar.analyzeLogs": "分析日志", "xpack.integrationAssistant.bottomBar.close": "关闭", "xpack.integrationAssistant.bottomBar.loading": "正在加载", @@ -23353,17 +23352,6 @@ "xpack.integrationAssistant.missingPrivileges.title": "缺少权限", "xpack.integrationAssistant.pages.header.avatarTitle": "由生成式 AI 提供支持", "xpack.integrationAssistant.pages.header.title": "新集成", - "xpack.integrationAssistant.step.celInput.apiDefinition.description": "拖放文件或浏览文件。", - "xpack.integrationAssistant.step.celInput.apiDefinition.description2": "OpenAPI 规范", - "xpack.integrationAssistant.step.celInput.apiDefinition.label": "OpenAPI 规范", - "xpack.integrationAssistant.step.celInput.celInputDescription": "上传 OpenAPI 规范文件以为 CEL 输入生成配置", - "xpack.integrationAssistant.step.celInput.celInputTitle": "生成 CEL 输入配置", - "xpack.integrationAssistant.step.celInput.generationError": "以下期间发生错误:CEL 输入生成", - "xpack.integrationAssistant.step.celInput.openapiSpec.errorCanNotRead": "无法读取日志样例文件", - "xpack.integrationAssistant.step.celInput.openapiSpec.errorCanNotReadWithReason": "读取规范文件时发生错误:{reason}", - "xpack.integrationAssistant.step.celInput.openapiSpec.errorTooLargeToParse": "此规范文件太大,无法解析", - "xpack.integrationAssistant.step.celInput.progress.relatedGraph": "正在生成 CEL 输入配置", - "xpack.integrationAssistant.step.celInput.retryButtonLabel": "重试", "xpack.integrationAssistant.step.connector": "连接器", "xpack.integrationAssistant.step.dataStream": "数据流", "xpack.integrationAssistant.step.dataStream.analyzing": "正在分析", @@ -23415,12 +23403,6 @@ "xpack.integrationAssistant.step.review.ingestPipelineTitle": "采集管道", "xpack.integrationAssistant.step.review.save": "保存", "xpack.integrationAssistant.step.review.title": "复查结果", - "xpack.integrationAssistant.step.reviewCel.description": "查看为您的集成生成的 CEL 输入配置设置。在可以进行编辑的情况下,会将这些设置自动填充到集成配置中。", - "xpack.integrationAssistant.step.reviewCel.program": "要为每次轮询运行的 CEL 程序", - "xpack.integrationAssistant.step.reviewCel.redact": "已编辑字段", - "xpack.integrationAssistant.step.reviewCel.save": "保存", - "xpack.integrationAssistant.step.reviewCel.state": "初始 CEL 评估状态", - "xpack.integrationAssistant.step.reviewCel.title": "复查结果", "xpack.integrationAssistant.steps.connector.createConnectorLabel": "创建新连接器", "xpack.integrationAssistant.steps.connector.description": "选择 AI 连接器以帮助您创建定制集成", "xpack.integrationAssistant.steps.connector.supportedModelsInfo": "当前,我们建议使用支持更新 Claude 模型的提供商,以获得最佳体验。目前,我们正努力为 GPT-4 和类似模型添加更全面的支持", diff --git a/x-pack/platform/plugins/shared/integration_assistant/__jest__/fixtures/api_analysis.ts b/x-pack/platform/plugins/shared/integration_assistant/__jest__/fixtures/api_analysis.ts new file mode 100644 index 0000000000000..e181160624af1 --- /dev/null +++ b/x-pack/platform/plugins/shared/integration_assistant/__jest__/fixtures/api_analysis.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ApiAnalysisState } from '../../server/types'; + +export const apiAnalysisTestState: ApiAnalysisState = { + dataStreamName: 'testDataStream', + lastExecutedChain: 'testchain', + results: { test: 'testResults' }, + pathOptions: { '/path1': 'path1 description', '/path2': 'path2 description' }, + suggestedPaths: ['/path1'], +}; + +export const apiAnalysisPathSuggestionsMockedResponse = ['/path1', '/path2', '/path3', '/path4']; + +export const apiAnalysisExpectedResults = { + suggestedPaths: ['/path1', '/path2', '/path3', '/path4'], +}; diff --git a/x-pack/platform/plugins/shared/integration_assistant/__jest__/fixtures/cel.ts b/x-pack/platform/plugins/shared/integration_assistant/__jest__/fixtures/cel.ts index e13c4216fbcdd..ff29e575baf6c 100644 --- a/x-pack/platform/plugins/shared/integration_assistant/__jest__/fixtures/cel.ts +++ b/x-pack/platform/plugins/shared/integration_assistant/__jest__/fixtures/cel.ts @@ -5,18 +5,25 @@ * 2.0. */ -export const celTestState = { +import { CelInputState } from '../../server/types'; + +export const celTestState: CelInputState = { dataStreamName: 'testDataStream', - apiDefinition: 'apiDefinition', lastExecutedChain: 'testchain', finalized: false, apiQuerySummary: 'testQuerySummary', - exampleCelPrograms: [], currentProgram: 'testProgram', stateVarNames: ['testVar'], stateSettings: { test: 'testDetails' }, + configFields: { test: { config1: 'config1testDetails' } }, redactVars: ['testRedact'], results: { test: 'testResults' }, + path: './testPath', + authType: 'basic', + openApiPathDetails: {}, + openApiSchemas: {}, + openApiAuthSchema: {}, + hasProgramHeaders: false, }; export const celQuerySummaryMockedResponse = `To cover all events in a chronological manner for the device_tasks endpoint, you should use the /v1/device_tasks GET route with pagination parameters. Specifically, use the pageSize and pageToken query parameters. Start with a large pageSize and use the nextPageToken from each response to fetch subsequent pages until all events are retrieved. @@ -83,16 +90,25 @@ export const celStateDetailsMockedResponse = [ name: 'config1', default: 50, redact: false, + configurable: true, + description: 'config1 description', + type: 'number', }, { name: 'config2', default: '', redact: true, + configurable: false, + description: 'config2 description', + type: 'string', }, { name: 'config3', default: 'event', redact: false, + configurable: false, + description: 'config3 description', + type: 'string', }, ]; @@ -102,6 +118,14 @@ export const celStateSettings = { config3: 'event', }; +export const celConfigFields = { + config1: { + default: 50, + type: 'number', + description: 'config1 description', + }, +}; + export const celRedact = ['config2']; export const celExpectedResults = { @@ -111,5 +135,13 @@ export const celExpectedResults = { config2: '', config3: 'event', }, + configFields: { + config1: { + default: 50, + type: 'number', + description: 'config1 description', + }, + }, + needsAuthConfigBlock: false, redactVars: ['config2'], }; diff --git a/x-pack/platform/plugins/shared/integration_assistant/__jest__/fixtures/index.ts b/x-pack/platform/plugins/shared/integration_assistant/__jest__/fixtures/index.ts index a25e3d6cf43a7..6bfb32a61d3d6 100644 --- a/x-pack/platform/plugins/shared/integration_assistant/__jest__/fixtures/index.ts +++ b/x-pack/platform/plugins/shared/integration_assistant/__jest__/fixtures/index.ts @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import type { Pipeline } from '../../common'; +import type { CelAuthType, Pipeline } from '../../common'; const currentPipelineMock: Pipeline = { description: 'Pipeline to process mysql_enterprise audit logs', processors: [ @@ -53,7 +53,16 @@ export const mockedRequestWithPipeline = { currentPipeline: currentPipelineMock, }; -export const mockedRequestWithApiDefinition = { - apiDefinition: '{ "openapi": "3.0.0" }', +export const mockedRequestWithCelDetails = { + dataStreamTitle: 'audit', + path: '/events', + authType: 'basic' as CelAuthType, + openApiDetails: {}, + openApiSchema: {}, + openApiAuthSchema: {}, +}; + +export const mockedApiAnalysisRequest = { dataStreamName: 'audit', + pathOptions: { '/path1': 'path1 description' }, }; diff --git a/x-pack/platform/plugins/shared/integration_assistant/common/api/analyze_api/analyze_api_route.gen.ts b/x-pack/platform/plugins/shared/integration_assistant/common/api/analyze_api/analyze_api_route.gen.ts new file mode 100644 index 0000000000000..1db062322e7b1 --- /dev/null +++ b/x-pack/platform/plugins/shared/integration_assistant/common/api/analyze_api/analyze_api_route.gen.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +/* + * NOTICE: Do not edit this file manually. + * This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator. + * + * info: + * title: Automatic Import Analyze API specifications API endpoint + * version: 1 + */ + +import { z } from '@kbn/zod'; + +import { DataStreamTitle, Connector, LangSmithOptions } from '../model/common_attributes.gen'; +import { PathOptions } from '../model/cel_input_attributes.gen'; +import { AnalyzeApiAPIResponse } from '../model/response_schemas.gen'; + +export type AnalyzeApiRequestBody = z.infer; +export const AnalyzeApiRequestBody = z.object({ + dataStreamTitle: DataStreamTitle, + pathOptions: PathOptions, + connectorId: Connector, + langSmithOptions: LangSmithOptions.optional(), +}); +export type AnalyzeApiRequestBodyInput = z.input; + +export type AnalyzeApiResponse = z.infer; +export const AnalyzeApiResponse = AnalyzeApiAPIResponse; diff --git a/x-pack/platform/plugins/shared/integration_assistant/common/api/analyze_api/analyze_api_route.schema.yaml b/x-pack/platform/plugins/shared/integration_assistant/common/api/analyze_api/analyze_api_route.schema.yaml new file mode 100644 index 0000000000000..1478355e4ce92 --- /dev/null +++ b/x-pack/platform/plugins/shared/integration_assistant/common/api/analyze_api/analyze_api_route.schema.yaml @@ -0,0 +1,39 @@ +openapi: 3.0.3 +info: + title: Automatic Import Analyze API specifications API endpoint + version: "1" +paths: + /internal/automatic_import/analyzeapi: + post: + summary: Analyzes API specifications. + operationId: AnalyzeApi + x-codegen-enabled: true + description: Analyzes API specifications. + tags: + - Analyze API spec + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - dataStreamTitle + - pathOptions + - connectorId + properties: + dataStreamTitle: + $ref: "../model/common_attributes.schema.yaml#/components/schemas/DataStreamTitle" + pathOptions: + $ref: "../model/cel_input_attributes.schema.yaml#/components/schemas/PathOptions" + connectorId: + $ref: "../model/common_attributes.schema.yaml#/components/schemas/Connector" + langSmithOptions: + $ref: "../model/common_attributes.schema.yaml#/components/schemas/LangSmithOptions" + responses: + 200: + description: Indicates a successful call. + content: + application/json: + schema: + $ref: "../model/response_schemas.schema.yaml#/components/schemas/AnalyzeApiAPIResponse" \ No newline at end of file diff --git a/x-pack/platform/plugins/shared/integration_assistant/common/api/analyze_api/analyze_api_route.test.ts b/x-pack/platform/plugins/shared/integration_assistant/common/api/analyze_api/analyze_api_route.test.ts new file mode 100644 index 0000000000000..4ea2ce6e3d84c --- /dev/null +++ b/x-pack/platform/plugins/shared/integration_assistant/common/api/analyze_api/analyze_api_route.test.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { expectParseSuccess } from '@kbn/zod-helpers'; +import { AnalyzeApiRequestBody } from './analyze_api_route.gen'; +import { getAnalyzeApiRequestBody } from '../model/api_test.mock'; + +describe('Analyze API request schema', () => { + test('full request validate', () => { + const payload: AnalyzeApiRequestBody = getAnalyzeApiRequestBody(); + + const result = AnalyzeApiRequestBody.safeParse(payload); + expectParseSuccess(result); + expect(result.data).toEqual(payload); + }); +}); diff --git a/x-pack/platform/plugins/shared/integration_assistant/common/api/cel/cel_input_route.gen.ts b/x-pack/platform/plugins/shared/integration_assistant/common/api/cel/cel_input_route.gen.ts index 276fd27072c98..78acca695dcbc 100644 --- a/x-pack/platform/plugins/shared/integration_assistant/common/api/cel/cel_input_route.gen.ts +++ b/x-pack/platform/plugins/shared/integration_assistant/common/api/cel/cel_input_route.gen.ts @@ -16,14 +16,14 @@ import { z } from '@kbn/zod'; -import { DataStreamName, Connector, LangSmithOptions } from '../model/common_attributes.gen'; -import { ApiDefinition } from '../model/cel_input_attributes.gen'; +import { DataStreamTitle, Connector, LangSmithOptions } from '../model/common_attributes.gen'; +import { CelDetails } from '../model/cel_input_attributes.gen'; import { CelInputAPIResponse } from '../model/response_schemas.gen'; export type CelInputRequestBody = z.infer; export const CelInputRequestBody = z.object({ - dataStreamName: DataStreamName, - apiDefinition: ApiDefinition, + dataStreamTitle: DataStreamTitle, + celDetails: CelDetails, connectorId: Connector, langSmithOptions: LangSmithOptions.optional(), }); diff --git a/x-pack/platform/plugins/shared/integration_assistant/common/api/cel/cel_input_route.schema.yaml b/x-pack/platform/plugins/shared/integration_assistant/common/api/cel/cel_input_route.schema.yaml index dc33d9ac69e3e..3cf96994fee63 100644 --- a/x-pack/platform/plugins/shared/integration_assistant/common/api/cel/cel_input_route.schema.yaml +++ b/x-pack/platform/plugins/shared/integration_assistant/common/api/cel/cel_input_route.schema.yaml @@ -18,14 +18,14 @@ paths: schema: type: object required: - - apiDefinition - - dataStreamName + - dataStreamTitle + - celDetails - connectorId properties: - dataStreamName: - $ref: "../model/common_attributes.schema.yaml#/components/schemas/DataStreamName" - apiDefinition: - $ref: "../model/cel_input_attributes.schema.yaml#/components/schemas/ApiDefinition" + dataStreamTitle: + $ref: "../model/common_attributes.schema.yaml#/components/schemas/DataStreamTitle" + celDetails: + $ref: "../model/cel_input_attributes.schema.yaml#/components/schemas/CelDetails" connectorId: $ref: "../model/common_attributes.schema.yaml#/components/schemas/Connector" langSmithOptions: diff --git a/x-pack/platform/plugins/shared/integration_assistant/common/api/model/api_test.mock.ts b/x-pack/platform/plugins/shared/integration_assistant/common/api/model/api_test.mock.ts index c8f6967503ac3..5eebe4db17f75 100644 --- a/x-pack/platform/plugins/shared/integration_assistant/common/api/model/api_test.mock.ts +++ b/x-pack/platform/plugins/shared/integration_assistant/common/api/model/api_test.mock.ts @@ -5,6 +5,7 @@ * 2.0. */ +import type { AnalyzeApiRequestBody } from '../analyze_api/analyze_api_route.gen'; import type { AnalyzeLogsRequestBody } from '../analyze_logs/analyze_logs_route.gen'; import type { BuildIntegrationRequestBody } from '../build_integration/build_integration.gen'; import type { CategorizationRequestBody } from '../categorization/categorization_route.gen'; @@ -67,9 +68,17 @@ export const getCategorizationRequestMock = (): CategorizationRequestBody => ({ }); export const getCelRequestMock = (): CelInputRequestBody => ({ - dataStreamName: 'test-data-stream-name', - apiDefinition: 'test-api-definition', + dataStreamTitle: 'test-data-stream-title', connectorId: 'test-connector-id', + celDetails: { + path: 'test-cel-path', + auth: 'basic', + openApiDetails: { + operation: 'test-open-api-operation', + schemas: 'test-open-api-schemas', + auth: 'test-open-api-auth', + }, + }, }); export const getBuildIntegrationRequestMock = (): BuildIntegrationRequestBody => ({ @@ -101,3 +110,9 @@ export const getAnalyzeLogsRequestBody = (): AnalyzeLogsRequestBody => ({ connectorId: 'test-connector-id', logSamples: rawSamples, }); + +export const getAnalyzeApiRequestBody = (): AnalyzeApiRequestBody => ({ + connectorId: 'test-connector-id', + dataStreamTitle: 'test-data-stream-name', + pathOptions: { '/v1/events': 'the path for events', '/v1/logs': 'the path for logs' }, +}); diff --git a/x-pack/platform/plugins/shared/integration_assistant/common/api/model/cel_input_attributes.gen.ts b/x-pack/platform/plugins/shared/integration_assistant/common/api/model/cel_input_attributes.gen.ts index 9ee1ee2ed290f..ed73099f9e74c 100644 --- a/x-pack/platform/plugins/shared/integration_assistant/common/api/model/cel_input_attributes.gen.ts +++ b/x-pack/platform/plugins/shared/integration_assistant/common/api/model/cel_input_attributes.gen.ts @@ -16,18 +16,59 @@ import { z } from '@kbn/zod'; +export type PathOptions = z.infer; +export const PathOptions = z.object({}).catchall(z.unknown()); + +/** + * The type of auth utilized for the input. + */ +export type CelAuthType = z.infer; +export const CelAuthType = z.enum(['basic', 'digest', 'oauth2', 'header']); +export type CelAuthTypeEnum = typeof CelAuthType.enum; +export const CelAuthTypeEnum = CelAuthType.enum; + +/** + * Necessary OpenAPI spec details for building a CEL program. + */ +export type OpenApiDetails = z.infer; +export const OpenApiDetails = z.object({ + operation: z.string(), + schemas: z.string(), + auth: z.string().optional(), +}); + /** - * String form of the Open API schema. + * Details for building a CEL program. */ -export type ApiDefinition = z.infer; -export const ApiDefinition = z.string(); +export type CelDetails = z.infer; +export const CelDetails = z.object({ + path: z.string(), + auth: CelAuthType, + openApiDetails: OpenApiDetails.optional(), +}); + +/** + * Generated CEL details. + */ +export type GeneratedCelDetails = z.infer; +export const GeneratedCelDetails = z.object({ + configFields: z.object({}).catchall(z.unknown()), + program: z.string(), + needsAuthConfigBlock: z.boolean(), + stateSettings: z.object({}).catchall(z.unknown()), + redactVars: z.array(z.string()), +}); /** * Optional CEL input details. */ export type CelInput = z.infer; export const CelInput = z.object({ + authType: CelAuthType, + configFields: z.object({}).catchall(z.unknown()), + needsAuthConfigBlock: z.boolean(), program: z.string(), stateSettings: z.object({}).catchall(z.unknown()), redactVars: z.array(z.string()), + url: z.string(), }); diff --git a/x-pack/platform/plugins/shared/integration_assistant/common/api/model/cel_input_attributes.schema.yaml b/x-pack/platform/plugins/shared/integration_assistant/common/api/model/cel_input_attributes.schema.yaml index cd05202ddfda0..a4d575927a106 100644 --- a/x-pack/platform/plugins/shared/integration_assistant/common/api/model/cel_input_attributes.schema.yaml +++ b/x-pack/platform/plugins/shared/integration_assistant/common/api/model/cel_input_attributes.schema.yaml @@ -6,18 +6,82 @@ paths: {} components: x-codegen-enabled: true schemas: - ApiDefinition: - type: string - description: String form of the Open API schema. + PathOptions: + type: object + additionalProperties: true + + CelDetails: + type: object + description: Details for building a CEL program. + required: + - path + - auth + properties: + path: + type: string + auth: + $ref: "#/components/schemas/CelAuthType" + openApiDetails: + $ref: "#/components/schemas/OpenApiDetails" + + OpenApiDetails: + type: object + description: Necessary OpenAPI spec details for building a CEL program. + required: + - operation + - schemas + properties: + operation: + type: string + schemas: + type: string + auth: + type: string + GeneratedCelDetails: + type: object + description: Generated CEL details. + required: + - configFields + - program + - needsAuthConfigBlock + - stateSettings + - redactVars + properties: + configFields: + type: object + additionalProperties: true + program: + type: string + needsAuthConfigBlock: + type: boolean + stateSettings: + type: object + additionalProperties: true + redactVars: + type: array + items: + type: string + CelInput: type: object description: Optional CEL input details. required: + - authType + - configFields - program + - needsAuthConfigBlock - stateSettings - redactVars + - url properties: + authType: + $ref: "#/components/schemas/CelAuthType" + configFields: + type: object + additionalProperties: true + needsAuthConfigBlock: + type: boolean program: type: string stateSettings: @@ -27,4 +91,15 @@ components: type: array items: type: string + url: + type: string + + CelAuthType: + type: string + description: The type of auth utilized for the input. + enum: + - basic + - digest + - oauth2 + - header \ No newline at end of file diff --git a/x-pack/platform/plugins/shared/integration_assistant/common/api/model/response_schemas.gen.ts b/x-pack/platform/plugins/shared/integration_assistant/common/api/model/response_schemas.gen.ts index f13b6fc537235..407e865f3625a 100644 --- a/x-pack/platform/plugins/shared/integration_assistant/common/api/model/response_schemas.gen.ts +++ b/x-pack/platform/plugins/shared/integration_assistant/common/api/model/response_schemas.gen.ts @@ -18,7 +18,7 @@ import { z } from '@kbn/zod'; import { Mapping, Pipeline, Docs, SamplesFormat } from './common_attributes.gen'; import { ESProcessorItem } from './processor_attributes.gen'; -import { CelInput } from './cel_input_attributes.gen'; +import { GeneratedCelDetails } from './cel_input_attributes.gen'; export type EcsMappingAPIResponse = z.infer; export const EcsMappingAPIResponse = z.object({ @@ -62,5 +62,12 @@ export const AnalyzeLogsAPIResponse = z.object({ export type CelInputAPIResponse = z.infer; export const CelInputAPIResponse = z.object({ - results: CelInput, + results: GeneratedCelDetails, +}); + +export type AnalyzeApiAPIResponse = z.infer; +export const AnalyzeApiAPIResponse = z.object({ + results: z.object({ + suggestedPaths: z.array(z.string()), + }), }); diff --git a/x-pack/platform/plugins/shared/integration_assistant/common/api/model/response_schemas.schema.yaml b/x-pack/platform/plugins/shared/integration_assistant/common/api/model/response_schemas.schema.yaml index 62776b9dc5c13..b784bda3e6a95 100644 --- a/x-pack/platform/plugins/shared/integration_assistant/common/api/model/response_schemas.schema.yaml +++ b/x-pack/platform/plugins/shared/integration_assistant/common/api/model/response_schemas.schema.yaml @@ -95,4 +95,19 @@ components: - results properties: results: - $ref: "./cel_input_attributes.schema.yaml#/components/schemas/CelInput" \ No newline at end of file + $ref: "./cel_input_attributes.schema.yaml#/components/schemas/GeneratedCelDetails" + + AnalyzeApiAPIResponse: + type: object + required: + - results + properties: + results: + type: object + required: + - suggestedPaths + properties: + suggestedPaths: + type: array + items: + type: string \ No newline at end of file diff --git a/x-pack/platform/plugins/shared/integration_assistant/common/constants.ts b/x-pack/platform/plugins/shared/integration_assistant/common/constants.ts index e05692f4ac085..352a84a96eabf 100644 --- a/x-pack/platform/plugins/shared/integration_assistant/common/constants.ts +++ b/x-pack/platform/plugins/shared/integration_assistant/common/constants.ts @@ -20,6 +20,7 @@ export const ECS_GRAPH_PATH = `${INTEGRATION_ASSISTANT_BASE_PATH}/ecs`; export const CATEGORIZATION_GRAPH_PATH = `${INTEGRATION_ASSISTANT_BASE_PATH}/categorization`; export const ANALYZE_LOGS_PATH = `${INTEGRATION_ASSISTANT_BASE_PATH}/analyzelogs`; export const RELATED_GRAPH_PATH = `${INTEGRATION_ASSISTANT_BASE_PATH}/related`; +export const ANALYZE_API_PATH = `${INTEGRATION_ASSISTANT_BASE_PATH}/analyzeapi`; export const CEL_INPUT_GRAPH_PATH = `${INTEGRATION_ASSISTANT_BASE_PATH}/cel`; export const CHECK_PIPELINE_PATH = `${INTEGRATION_ASSISTANT_BASE_PATH}/pipeline`; export const INTEGRATION_BUILDER_PATH = `${INTEGRATION_ASSISTANT_BASE_PATH}/build`; diff --git a/x-pack/platform/plugins/shared/integration_assistant/common/experimental_features.ts b/x-pack/platform/plugins/shared/integration_assistant/common/experimental_features.ts index 76b4ed6f28fee..d4c5a48b0e9ec 100644 --- a/x-pack/platform/plugins/shared/integration_assistant/common/experimental_features.ts +++ b/x-pack/platform/plugins/shared/integration_assistant/common/experimental_features.ts @@ -9,6 +9,7 @@ export type ExperimentalFeatures = typeof allowedExperimentalValues; const _allowedExperimentalValues = { /** + * -- Deprecated -- * Enables whether the user is able to utilize the LLM to generate the CEL input configuration. */ generateCel: false, diff --git a/x-pack/platform/plugins/shared/integration_assistant/common/index.ts b/x-pack/platform/plugins/shared/integration_assistant/common/index.ts index 0b13f7f692695..60ee134cf9acf 100644 --- a/x-pack/platform/plugins/shared/integration_assistant/common/index.ts +++ b/x-pack/platform/plugins/shared/integration_assistant/common/index.ts @@ -19,6 +19,7 @@ export { AnalyzeLogsRequestBody, AnalyzeLogsResponse, } from './api/analyze_logs/analyze_logs_route.gen'; +export { AnalyzeApiRequestBody, AnalyzeApiResponse } from './api/analyze_api/analyze_api_route.gen'; export { CelInputRequestBody, CelInputResponse } from './api/cel/cel_input_route.gen'; export { partialShuffleArray } from './utils'; @@ -33,7 +34,7 @@ export type { } from './api/model/common_attributes.gen'; export { SamplesFormat, SamplesFormatName } from './api/model/common_attributes.gen'; export type { ESProcessorItem } from './api/model/processor_attributes.gen'; -export type { CelInput } from './api/model/cel_input_attributes.gen'; +export type { CelInput, CelAuthType } from './api/model/cel_input_attributes.gen'; export { CATEGORIZATION_GRAPH_PATH, @@ -46,4 +47,5 @@ export { RELATED_GRAPH_PATH, CHECK_PIPELINE_PATH, ANALYZE_LOGS_PATH, + ANALYZE_API_PATH, } from './constants'; diff --git a/x-pack/platform/plugins/shared/integration_assistant/docs/imgs/analyze_api_graph.png b/x-pack/platform/plugins/shared/integration_assistant/docs/imgs/analyze_api_graph.png new file mode 100644 index 0000000000000000000000000000000000000000..085e4ae161c5411c25c953484a01a02d26db9c99 GIT binary patch literal 12498 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 X?y%Gkow|kRm|+d=e@zMl?xz12=dDwt literal 0 HcmV?d00001 diff --git a/x-pack/platform/plugins/shared/integration_assistant/docs/imgs/cel_graph.png b/x-pack/platform/plugins/shared/integration_assistant/docs/imgs/cel_graph.png index 8b339caa83798f06a7149b92f998fd9c2f409453..7ae98632553e99d3450e89bbe46abb6a8a840427 100644 GIT binary patch literal 75906 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 z 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 diff --git a/x-pack/platform/plugins/shared/integration_assistant/public/common/lib/api.ts b/x-pack/platform/plugins/shared/integration_assistant/public/common/lib/api.ts index 0a6d91375219e..3694715b4d31b 100644 --- a/x-pack/platform/plugins/shared/integration_assistant/public/common/lib/api.ts +++ b/x-pack/platform/plugins/shared/integration_assistant/public/common/lib/api.ts @@ -20,6 +20,8 @@ import type { AnalyzeLogsResponse, CelInputRequestBody, CelInputResponse, + AnalyzeApiRequestBody, + AnalyzeApiResponse, } from '../../../common'; import { INTEGRATION_BUILDER_PATH, @@ -29,7 +31,11 @@ import { CEL_INPUT_GRAPH_PATH, CHECK_PIPELINE_PATH, } from '../../../common'; -import { ANALYZE_LOGS_PATH, FLEET_PACKAGES_PATH } from '../../../common/constants'; +import { + ANALYZE_API_PATH, + ANALYZE_LOGS_PATH, + FLEET_PACKAGES_PATH, +} from '../../../common/constants'; export interface EpmPackageResponse { items: [{ id: string; type: string }]; @@ -47,6 +53,16 @@ export interface RequestDeps { abortSignal: AbortSignal; } +export const runAnalyzeApiGraph = async ( + body: AnalyzeApiRequestBody, + { http, abortSignal }: RequestDeps +): Promise => + http.post(ANALYZE_API_PATH, { + headers: defaultHeaders, + body: JSON.stringify(body), + signal: abortSignal, + }); + export const runAnalyzeLogsGraph = async ( body: AnalyzeLogsRequestBody, { http, abortSignal }: RequestDeps diff --git a/x-pack/platform/plugins/shared/integration_assistant/public/components/create_integration/create_integration_assistant/create_integration_assistant.test.tsx b/x-pack/platform/plugins/shared/integration_assistant/public/components/create_integration/create_integration_assistant/create_integration_assistant.test.tsx index 133da383e4e99..5e40b2dcbc86a 100644 --- a/x-pack/platform/plugins/shared/integration_assistant/public/components/create_integration/create_integration_assistant/create_integration_assistant.test.tsx +++ b/x-pack/platform/plugins/shared/integration_assistant/public/components/create_integration/create_integration_assistant/create_integration_assistant.test.tsx @@ -10,7 +10,6 @@ import { render, act } from '@testing-library/react'; import { TestProvider } from '../../../mocks/test_provider'; import { CreateIntegrationAssistant } from './create_integration_assistant'; import type { State } from './state'; -import { ExperimentalFeaturesService } from '../../../services'; import { mockReportEvent } from '../../../services/telemetry/mocks/service'; import { TelemetryEventType } from '../../../services/telemetry/types'; @@ -19,8 +18,9 @@ export const defaultInitialState: State = { connector: undefined, integrationSettings: undefined, isGenerating: false, - hasCelInput: false, result: undefined, + showCelCreateFlyout: false, + isFlyoutGenerating: false, }; const mockInitialState = jest.fn((): State => defaultInitialState); @@ -31,23 +31,17 @@ jest.mock('./state', () => ({ }, })); -jest.mock('../../../services'); -const mockedExperimentalFeaturesService = jest.mocked(ExperimentalFeaturesService); - const mockConnectorStep = jest.fn(() =>
); const mockIntegrationStep = jest.fn(() =>
); const mockDataStreamStep = jest.fn(() =>
); +const mockCelCreateFlyout = jest.fn(() =>
); const mockReviewStep = jest.fn(() =>
); -const mockCelInputStep = jest.fn(() =>
); -const mockReviewCelStep = jest.fn(() =>
); const mockDeployStep = jest.fn(() =>
); const mockIsConnectorStepReadyToComplete = jest.fn(); const mockIsIntegrationStepReadyToComplete = jest.fn(); const mockIsDataStreamStepReadyToComplete = jest.fn(); const mockIsReviewStepReadyToComplete = jest.fn(); -const mockIsCelInputStepReadyToComplete = jest.fn(); -const mockIsCelReviewStepReadyToComplete = jest.fn(); jest.mock('./steps/connector_step', () => ({ ConnectorStep: () => mockConnectorStep(), @@ -61,18 +55,13 @@ jest.mock('./steps/data_stream_step', () => ({ DataStreamStep: () => mockDataStreamStep(), isDataStreamStepReadyToComplete: () => mockIsDataStreamStepReadyToComplete(), })); +jest.mock('./flyout/cel_configuration', () => ({ + CreateCelConfigFlyout: () => mockCelCreateFlyout(), +})); jest.mock('./steps/review_step', () => ({ ReviewStep: () => mockReviewStep(), isReviewStepReadyToComplete: () => mockIsReviewStepReadyToComplete(), })); -jest.mock('./steps/cel_input_step', () => ({ - CelInputStep: () => mockCelInputStep(), - isCelInputStepReadyToComplete: () => mockIsCelInputStepReadyToComplete(), -})); -jest.mock('./steps/review_cel_step', () => ({ - ReviewCelStep: () => mockReviewCelStep(), - isCelReviewStepReadyToComplete: () => mockIsCelReviewStepReadyToComplete(), -})); jest.mock('./steps/deploy_step', () => ({ DeployStep: () => mockDeployStep() })); const mockNavigate = jest.fn(); @@ -87,10 +76,6 @@ const renderIntegrationAssistant = () => describe('CreateIntegration', () => { beforeEach(() => { jest.clearAllMocks(); - - mockedExperimentalFeaturesService.get.mockReturnValue({ - generateCel: false, - } as never); }); describe('when step is 1', () => { @@ -440,6 +425,21 @@ describe('CreateIntegration', () => { }); }); + describe('when step is 3 and showCelCreateFlyout=true', () => { + beforeEach(() => { + mockInitialState.mockReturnValueOnce({ + ...defaultInitialState, + step: 3, + showCelCreateFlyout: true, + }); + }); + + it('should render cel creation flyout', () => { + const result = renderIntegrationAssistant(); + expect(result.queryByTestId('celCreateFlyoutMock')).toBeInTheDocument(); + }); + }); + describe('when step is 4', () => { beforeEach(() => { mockInitialState.mockReturnValueOnce({ ...defaultInitialState, step: 4 }); @@ -567,289 +567,3 @@ describe('CreateIntegration', () => { }); }); }); - -describe('CreateIntegration with generateCel enabled', () => { - beforeEach(() => { - jest.clearAllMocks(); - - mockedExperimentalFeaturesService.get.mockReturnValue({ - generateCel: true, - } as never); - }); - - describe('when step is 5 and has celInput', () => { - beforeEach(() => { - mockInitialState.mockReturnValueOnce({ ...defaultInitialState, step: 5, hasCelInput: true }); - }); - - it('should render cel input', () => { - const result = renderIntegrationAssistant(); - expect(result.queryByTestId('celInputStepMock')).toBeInTheDocument(); - }); - - it('should call isCelInputStepReadyToComplete', () => { - renderIntegrationAssistant(); - expect(mockIsCelInputStepReadyToComplete).toHaveBeenCalled(); - }); - - it('should show "Generate CEL input configuration" on the next button', () => { - const result = renderIntegrationAssistant(); - expect(result.getByTestId('buttonsFooter-nextButton')).toHaveTextContent( - 'Generate CEL input configuration' - ); - }); - - it('should enable the back button', () => { - const result = renderIntegrationAssistant(); - expect(result.getByTestId('buttonsFooter-backButton')).toBeEnabled(); - }); - - describe('when cel input step is not done', () => { - beforeEach(() => { - mockIsCelInputStepReadyToComplete.mockReturnValue(false); - }); - - it('should disable the next button', () => { - const result = renderIntegrationAssistant(); - // Not sure why there are two buttons when testing. - const nextButton = result - .getAllByTestId('buttonsFooter-nextButton') - .filter((button) => button.textContent !== 'Next')[0]; - expect(nextButton).toBeDisabled(); - }); - }); - - describe('when cel input step is done', () => { - beforeEach(() => { - mockIsCelInputStepReadyToComplete.mockReturnValue(true); - }); - - it('should enable the next button', () => { - const result = renderIntegrationAssistant(); - expect(result.getByTestId('buttonsFooter-nextButton')).toBeEnabled(); - }); - - describe('when next button is clicked', () => { - beforeEach(() => { - const result = renderIntegrationAssistant(); - mockReportEvent.mockClear(); - act(() => { - result.getByTestId('buttonsFooter-nextButton').click(); - }); - }); - - it('should report telemetry for cel input step completion', () => { - expect(mockReportEvent).toHaveBeenCalledWith( - TelemetryEventType.IntegrationAssistantStepComplete, - { - sessionId: expect.any(String), - step: 5, - stepName: 'CEL Input Step', - durationMs: expect.any(Number), - sessionElapsedTime: expect.any(Number), - } - ); - }); - - it('should show loader on the next button', () => { - const result = renderIntegrationAssistant(); - expect(result.getByTestId('generatingLoader')).toBeInTheDocument(); - }); - - it('should disable the next button', () => { - const result = renderIntegrationAssistant(); - // Not sure why there are two buttons when testing. - const nextButton = result - .getAllByTestId('buttonsFooter-nextButton') - .filter((button) => button.textContent !== 'Next')[0]; - expect(nextButton).toBeDisabled(); - }); - }); - }); - - describe('when back button is clicked', () => { - let result: ReturnType; - beforeEach(() => { - result = renderIntegrationAssistant(); - mockReportEvent.mockClear(); - act(() => { - result.getByTestId('buttonsFooter-backButton').click(); - }); - }); - - it('should not report telemetry', () => { - expect(mockReportEvent).not.toHaveBeenCalled(); - }); - - it('should show review step', () => { - expect(result.queryByTestId('reviewStepMock')).toBeInTheDocument(); - }); - - it('should enable the next button', () => { - expect(result.getByTestId('buttonsFooter-nextButton')).toBeEnabled(); - }); - }); - }); - - describe('when step is 5 and does not have celInput', () => { - beforeEach(() => { - mockInitialState.mockReturnValueOnce({ ...defaultInitialState, step: 5 }); - }); - - it('should render deploy', () => { - const result = renderIntegrationAssistant(); - expect(result.queryByTestId('deployStepMock')).toBeInTheDocument(); - }); - - it('should hide the back button', () => { - const result = renderIntegrationAssistant(); - expect(result.queryByTestId('buttonsFooter-backButton')).toBe(null); - }); - - it('should hide the next button', () => { - const result = renderIntegrationAssistant(); - expect(result.queryByTestId('buttonsFooter-backButton')).toBe(null); - }); - - it('should enable the cancel button', () => { - const result = renderIntegrationAssistant(); - expect(result.getByTestId('buttonsFooter-cancelButton')).toBeEnabled(); - }); - - it('should show "Close" on the cancel button', () => { - const result = renderIntegrationAssistant(); - expect(result.getByTestId('buttonsFooter-cancelButton')).toHaveTextContent('Close'); - }); - }); - - describe('when step is 6', () => { - beforeEach(() => { - mockInitialState.mockReturnValueOnce({ - ...defaultInitialState, - step: 6, - celInputResult: { program: 'program', stateSettings: {}, redactVars: [] }, - }); - }); - - it('should render review', () => { - const result = renderIntegrationAssistant(); - expect(result.queryByTestId('reviewCelStepMock')).toBeInTheDocument(); - }); - - it('should call isReviewCelStepReadyToComplete', () => { - renderIntegrationAssistant(); - expect(mockIsCelReviewStepReadyToComplete).toHaveBeenCalled(); - }); - - it('should show the "Add to Elastic" on the next button', () => { - const result = renderIntegrationAssistant(); - expect(result.getByTestId('buttonsFooter-nextButton')).toHaveTextContent('Add to Elastic'); - }); - - describe('when cel review step is not done', () => { - beforeEach(() => { - mockIsCelReviewStepReadyToComplete.mockReturnValue(false); - }); - - it('should disable the next button', () => { - const result = renderIntegrationAssistant(); - expect(result.getByTestId('buttonsFooter-nextButton')).toBeDisabled(); - }); - - it('should still enable the back button', () => { - const result = renderIntegrationAssistant(); - expect(result.getByTestId('buttonsFooter-backButton')).toBeEnabled(); - }); - - it('should still enable the cancel button', () => { - const result = renderIntegrationAssistant(); - expect(result.getByTestId('buttonsFooter-cancelButton')).toBeEnabled(); - }); - }); - - describe('when cel review step is done', () => { - beforeEach(() => { - mockIsCelReviewStepReadyToComplete.mockReturnValue(true); - }); - - it('should enable the next button', () => { - const result = renderIntegrationAssistant(); - expect(result.getByTestId('buttonsFooter-nextButton')).toBeEnabled(); - }); - - it('should enable the back button', () => { - const result = renderIntegrationAssistant(); - expect(result.getByTestId('buttonsFooter-backButton')).toBeEnabled(); - }); - - it('should enable the cancel button', () => { - const result = renderIntegrationAssistant(); - expect(result.getByTestId('buttonsFooter-cancelButton')).toBeEnabled(); - }); - - describe('when next button is clicked', () => { - beforeEach(() => { - const result = renderIntegrationAssistant(); - mockReportEvent.mockClear(); - act(() => { - result.getByTestId('buttonsFooter-nextButton').click(); - }); - }); - - it('should report telemetry for review step completion', () => { - expect(mockReportEvent).toHaveBeenCalledWith( - TelemetryEventType.IntegrationAssistantStepComplete, - { - sessionId: expect.any(String), - step: 6, - stepName: 'CEL Review Step', - durationMs: expect.any(Number), - sessionElapsedTime: expect.any(Number), - } - ); - }); - - it('should show deploy step', () => { - const result = renderIntegrationAssistant(); - expect(result.queryByTestId('deployStepMock')).toBeInTheDocument(); - }); - - it('should enable the next button', () => { - const result = renderIntegrationAssistant(); - expect(result.getByTestId('buttonsFooter-nextButton')).toBeEnabled(); - }); - }); - }); - }); - - describe('when step is 7', () => { - beforeEach(() => { - mockInitialState.mockReturnValueOnce({ ...defaultInitialState, step: 7 }); - }); - - it('should render deploy', () => { - const result = renderIntegrationAssistant(); - expect(result.queryByTestId('deployStepMock')).toBeInTheDocument(); - }); - - it('should hide the back button', () => { - const result = renderIntegrationAssistant(); - expect(result.queryByTestId('buttonsFooter-backButton')).toBe(null); - }); - - it('should hide the next button', () => { - const result = renderIntegrationAssistant(); - expect(result.queryByTestId('buttonsFooter-backButton')).toBe(null); - }); - - it('should enable the cancel button', () => { - const result = renderIntegrationAssistant(); - expect(result.getByTestId('buttonsFooter-cancelButton')).toBeEnabled(); - }); - - it('should show "Close" on the cancel button', () => { - const result = renderIntegrationAssistant(); - expect(result.getByTestId('buttonsFooter-cancelButton')).toHaveTextContent('Close'); - }); - }); -}); diff --git a/x-pack/platform/plugins/shared/integration_assistant/public/components/create_integration/create_integration_assistant/create_integration_assistant.tsx b/x-pack/platform/plugins/shared/integration_assistant/public/components/create_integration/create_integration_assistant/create_integration_assistant.tsx index 81cb5a9b3b137..1f353f187bb2a 100644 --- a/x-pack/platform/plugins/shared/integration_assistant/public/components/create_integration/create_integration_assistant/create_integration_assistant.tsx +++ b/x-pack/platform/plugins/shared/integration_assistant/public/components/create_integration/create_integration_assistant/create_integration_assistant.tsx @@ -9,48 +9,29 @@ import React, { useReducer, useMemo, useEffect, useCallback } from 'react'; import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template'; import { Header } from './header'; import { Footer } from './footer'; +import { CreateCelConfigFlyout } from './flyout/cel_configuration'; import { useNavigate, Page } from '../../../common/hooks/use_navigate'; import { ConnectorStep, isConnectorStepReadyToComplete } from './steps/connector_step'; import { IntegrationStep, isIntegrationStepReadyToComplete } from './steps/integration_step'; import { DataStreamStep, isDataStreamStepReadyToComplete } from './steps/data_stream_step'; import { ReviewStep, isReviewStepReadyToComplete } from './steps/review_step'; -import { CelInputStep, isCelInputStepReadyToComplete } from './steps/cel_input_step'; -import { ReviewCelStep, isCelReviewStepReadyToComplete } from './steps/review_cel_step'; import { DeployStep } from './steps/deploy_step'; import { reducer, initialState, ActionsProvider, type Actions } from './state'; import { useTelemetry } from '../telemetry'; -import { ExperimentalFeaturesService } from '../../../services'; const stepNames: Record = { 1: 'Connector Step', 2: 'Integration Step', 3: 'DataStream Step', 4: 'Review Step', - cel_input: 'CEL Input Step', - cel_review: 'CEL Review Step', - deploy: 'Deploy Step', + 5: 'Deploy Step', }; export const CreateIntegrationAssistant = React.memo(() => { const [state, dispatch] = useReducer(reducer, initialState); const navigate = useNavigate(); - const { generateCel: isGenerateCelEnabled } = ExperimentalFeaturesService.get(); - const celInputStepIndex = isGenerateCelEnabled && state.hasCelInput ? 5 : null; - const celReviewStepIndex = isGenerateCelEnabled && state.celInputResult ? 6 : null; - const deployStepIndex = - celInputStepIndex !== null || celReviewStepIndex !== null || state.step === 7 ? 7 : 5; - - const stepName = - state.step === deployStepIndex - ? stepNames.deploy - : state.step === celReviewStepIndex - ? stepNames.cel_review - : state.step === celInputStepIndex - ? stepNames.cel_input - : state.step in stepNames - ? stepNames[state.step] - : 'Unknown Step'; + const stepName = stepNames[state.step]; const telemetry = useTelemetry(); useEffect(() => { @@ -66,13 +47,9 @@ export const CreateIntegrationAssistant = React.memo(() => { return isDataStreamStepReadyToComplete(state); } else if (state.step === 4) { return isReviewStepReadyToComplete(state); - } else if (isGenerateCelEnabled && state.step === 5) { - return isCelInputStepReadyToComplete(state); - } else if (isGenerateCelEnabled && state.step === 6) { - return isCelReviewStepReadyToComplete(state); } return false; - }, [state, isGenerateCelEnabled]); + }, [state]); const goBackStep = useCallback(() => { if (state.step === 1) { @@ -88,12 +65,12 @@ export const CreateIntegrationAssistant = React.memo(() => { return; } telemetry.reportAssistantStepComplete({ step: state.step, stepName }); - if (state.step === 3 || state.step === celInputStepIndex) { + if (state.step === 3) { dispatch({ type: 'SET_IS_GENERATING', payload: true }); } else { dispatch({ type: 'SET_STEP', payload: state.step + 1 }); } - }, [telemetry, state.step, stepName, celInputStepIndex, isThisStepReadyToComplete]); + }, [telemetry, state.step, stepName, isThisStepReadyToComplete]); const actions = useMemo( () => ({ @@ -109,8 +86,11 @@ export const CreateIntegrationAssistant = React.memo(() => { setIsGenerating: (payload) => { dispatch({ type: 'SET_IS_GENERATING', payload }); }, - setHasCelInput: (payload) => { - dispatch({ type: 'SET_HAS_CEL_INPUT', payload }); + setShowCelCreateFlyout: (payload) => { + dispatch({ type: 'SET_SHOW_CEL_CREATE_FLYOUT', payload }); + }, + setIsFlyoutGenerating: (payload) => { + dispatch({ type: 'SET_IS_FLYOUT_GENERATING', payload }); }, setResult: (payload) => { dispatch({ type: 'SET_GENERATED_RESULT', payload }); @@ -133,32 +113,26 @@ export const CreateIntegrationAssistant = React.memo(() => { {state.step === 3 && ( )} - {state.step === 4 && ( - - )} - {state.step === celInputStepIndex && ( - )} - {state.step === celReviewStepIndex && ( - )} - - {state.step === deployStepIndex && ( + {state.step === 5 && ( {