From e3c05d6503cec405cd0d5eb650e937e863d3566b Mon Sep 17 00:00:00 2001 From: Jon Date: Mon, 6 Nov 2023 13:22:07 -0600 Subject: [PATCH 01/12] [build] Create CDN assets (#169707) Closes https://github.com/elastic/kibana/issues/169427 Adds a new build step `createCdnAssets` that will create an archive `kibana--cdn-assets.tar.gz` with static assets organized using the request structure of the kibana client. - By default CDN assets are created - Adding the flag `node scripts/build --skip-cdn-assets` will skip creation - `ci:build-cdn-assets` can be used to create and upload the archive for testing Testing: see https://github.com/elastic/kibana/pull/169408. Builds are available in the artifacts tab on the `Build Distribution` step. 1) Extract builds 2) ``` python3 -m http.server -b localhost -d kibana-8.12.0-SNAPSHOT-cdn-assets 8000 ``` 3) ``` echo 'server.cdn.url: http://localhost:8000' >> kibana-8.12.0-SNAPSHOT/config/kibana.yml ``` 4) ``` kibana-8.12.0-SNAPSHOT/bin/kibana ``` --- .buildkite/scripts/build_kibana.sh | 2 + .../scripts/steps/artifacts/docker_image.sh | 3 +- .../scripts/steps/package_testing/build.sh | 2 +- src/dev/build/args.test.ts | 7 ++ src/dev/build/args.ts | 2 + src/dev/build/build_distributables.ts | 5 + src/dev/build/tasks/create_cdn_assets_task.ts | 102 ++++++++++++++++++ src/dev/build/tasks/index.ts | 1 + 8 files changed, 122 insertions(+), 2 deletions(-) create mode 100644 src/dev/build/tasks/create_cdn_assets_task.ts diff --git a/.buildkite/scripts/build_kibana.sh b/.buildkite/scripts/build_kibana.sh index 01810d26758c2..66e53f80bdf5a 100755 --- a/.buildkite/scripts/build_kibana.sh +++ b/.buildkite/scripts/build_kibana.sh @@ -14,6 +14,7 @@ is_pr_with_label "ci:build-docker-cross-compile" && BUILD_ARGS+=("--docker-cross is_pr_with_label "ci:build-os-packages" || BUILD_ARGS+=("--skip-os-packages") is_pr_with_label "ci:build-canvas-shareable-runtime" || BUILD_ARGS+=("--skip-canvas-shareable-runtime") is_pr_with_label "ci:build-docker-contexts" || BUILD_ARGS+=("--skip-docker-contexts") +is_pr_with_label "ci:build-cdn-assets" || BUILD_ARGS+=("--skip-cdn-assets") echo "> node scripts/build" "${BUILD_ARGS[@]}" node scripts/build "${BUILD_ARGS[@]}" @@ -24,6 +25,7 @@ if is_pr_with_label "ci:build-cloud-image"; then --skip-initialize \ --skip-generic-folders \ --skip-platform-folders \ + --skip-cdn-assets \ --skip-archives \ --docker-images \ --docker-tag-qualifier="$GIT_COMMIT" \ diff --git a/.buildkite/scripts/steps/artifacts/docker_image.sh b/.buildkite/scripts/steps/artifacts/docker_image.sh index 9b4edc16024f0..0518e17f6524e 100755 --- a/.buildkite/scripts/steps/artifacts/docker_image.sh +++ b/.buildkite/scripts/steps/artifacts/docker_image.sh @@ -37,7 +37,8 @@ node scripts/build \ --skip-docker-ubuntu \ --skip-docker-ubi \ --skip-docker-cloud \ - --skip-docker-contexts + --skip-docker-contexts \ + --skip-cdn-assets echo "--- Tag images" docker rmi "$KIBANA_IMAGE" diff --git a/.buildkite/scripts/steps/package_testing/build.sh b/.buildkite/scripts/steps/package_testing/build.sh index 3cd7b3d154989..892b8bf312d51 100755 --- a/.buildkite/scripts/steps/package_testing/build.sh +++ b/.buildkite/scripts/steps/package_testing/build.sh @@ -4,7 +4,7 @@ set -euo pipefail .buildkite/scripts/bootstrap.sh -node scripts/build --all-platforms --debug --skip-docker-cloud --skip-docker-serverless --skip-docker-ubi --skip-docker-contexts +node scripts/build --all-platforms --debug --skip-docker-cloud --skip-docker-serverless --skip-docker-ubi --skip-docker-contexts --skip-cdn-assets DOCKER_FILE="kibana-$KIBANA_PKG_VERSION-SNAPSHOT-docker-image.tar.gz" diff --git a/src/dev/build/args.test.ts b/src/dev/build/args.test.ts index 4f351bba980b2..e379ae02ceba3 100644 --- a/src/dev/build/args.test.ts +++ b/src/dev/build/args.test.ts @@ -29,6 +29,7 @@ it('build default and oss dist for current platform, without packages, by defaul "buildOptions": Object { "buildCanvasShareableRuntime": true, "createArchives": true, + "createCdnAssets": true, "createDebPackage": false, "createDockerCloud": false, "createDockerContexts": true, @@ -67,6 +68,7 @@ it('builds packages if --all-platforms is passed', () => { "buildOptions": Object { "buildCanvasShareableRuntime": true, "createArchives": true, + "createCdnAssets": true, "createDebPackage": true, "createDockerCloud": true, "createDockerContexts": true, @@ -105,6 +107,7 @@ it('limits packages if --rpm passed with --all-platforms', () => { "buildOptions": Object { "buildCanvasShareableRuntime": true, "createArchives": true, + "createCdnAssets": true, "createDebPackage": false, "createDockerCloud": false, "createDockerContexts": true, @@ -143,6 +146,7 @@ it('limits packages if --deb passed with --all-platforms', () => { "buildOptions": Object { "buildCanvasShareableRuntime": true, "createArchives": true, + "createCdnAssets": true, "createDebPackage": true, "createDockerCloud": false, "createDockerContexts": true, @@ -182,6 +186,7 @@ it('limits packages if --docker passed with --all-platforms', () => { "buildOptions": Object { "buildCanvasShareableRuntime": true, "createArchives": true, + "createCdnAssets": true, "createDebPackage": false, "createDockerCloud": true, "createDockerContexts": true, @@ -228,6 +233,7 @@ it('limits packages if --docker passed with --skip-docker-ubi and --all-platform "buildOptions": Object { "buildCanvasShareableRuntime": true, "createArchives": true, + "createCdnAssets": true, "createDebPackage": false, "createDockerCloud": true, "createDockerContexts": true, @@ -267,6 +273,7 @@ it('limits packages if --all-platforms passed with --skip-docker-ubuntu', () => "buildOptions": Object { "buildCanvasShareableRuntime": true, "createArchives": true, + "createCdnAssets": true, "createDebPackage": true, "createDockerCloud": true, "createDockerContexts": true, diff --git a/src/dev/build/args.ts b/src/dev/build/args.ts index 68e111912722f..8f16d5ce571c0 100644 --- a/src/dev/build/args.ts +++ b/src/dev/build/args.ts @@ -16,6 +16,7 @@ export function readCliArgs(argv: string[]) { const flags = getopts(argv, { boolean: [ 'skip-archives', + 'skip-cdn-assets', 'skip-initialize', 'skip-generic-folders', 'skip-platform-folders', @@ -132,6 +133,7 @@ export function readCliArgs(argv: string[]) { createGenericFolders: !Boolean(flags['skip-generic-folders']), createPlatformFolders: !Boolean(flags['skip-platform-folders']), createArchives: !Boolean(flags['skip-archives']), + createCdnAssets: !Boolean(flags['skip-cdn-assets']), createRpmPackage: isOsPackageDesired('rpm'), createDebPackage: isOsPackageDesired('deb'), createDockerUbuntu: diff --git a/src/dev/build/build_distributables.ts b/src/dev/build/build_distributables.ts index 23203491e0d13..b324780e15672 100644 --- a/src/dev/build/build_distributables.ts +++ b/src/dev/build/build_distributables.ts @@ -26,6 +26,7 @@ export interface BuildOptions { createGenericFolders: boolean; createPlatformFolders: boolean; createArchives: boolean; + createCdnAssets: boolean; createRpmPackage: boolean; createDebPackage: boolean; createDockerUBI: boolean; @@ -113,6 +114,10 @@ export async function buildDistributables(log: ToolingLog, options: BuildOptions await run(Tasks.AssertPathLength); await run(Tasks.AssertNoUUID); } + // control w/ --skip-cdn-assets + if (options.createCdnAssets) { + await run(Tasks.CreateCdnAssets); + } /** * package platform-specific builds into archives diff --git a/src/dev/build/tasks/create_cdn_assets_task.ts b/src/dev/build/tasks/create_cdn_assets_task.ts new file mode 100644 index 0000000000000..e03e1f6775672 --- /dev/null +++ b/src/dev/build/tasks/create_cdn_assets_task.ts @@ -0,0 +1,102 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { readFileSync } from 'fs'; +import { access } from 'fs/promises'; + +import { resolve, dirname } from 'path'; +import { asyncForEach } from '@kbn/std'; +import { Jsonc } from '@kbn/repo-packages'; + +import del from 'del'; +import globby from 'globby'; + +import { mkdirp, compressTar, Task, copyAll } from '../lib'; + +export const CreateCdnAssets: Task = { + description: 'Creating CDN assets', + + async run(config, log, build) { + const buildSource = build.resolvePath(); + const buildNum = config.getBuildNumber(); + const buildVersion = config.getBuildVersion(); + const assets = config.resolveFromRepo('build', 'cdn-assets'); + const bundles = resolve(assets, String(buildNum), 'bundles'); + + await del(assets); + await mkdirp(assets); + + // Plugins + + const plugins = globby.sync([`${buildSource}/node_modules/@kbn/**/*/kibana.jsonc`]); + await asyncForEach(plugins, async (path) => { + const manifest = Jsonc.parse(readFileSync(path, 'utf8')) as any; + if (manifest?.plugin?.id) { + const pluginRoot = resolve(dirname(path)); + + try { + // packages/core/plugins/core-plugins-server-internal/src/plugins_service.ts + const assetsSource = resolve(pluginRoot, 'assets'); + const assetsDest = resolve('plugins', manifest.plugin.id, 'assets'); + await access(assetsSource); + await mkdirp(assetsDest); + await copyAll(assetsSource, assetsDest); + } catch (e) { + // assets are optional + if (!(e.code === 'ENOENT' && e.syscall === 'access')) throw e; + } + + try { + // packages/core/apps/core-apps-server-internal/src/bundle_routes/register_bundle_routes.ts + const bundlesSource = resolve(pluginRoot, 'target', 'public'); + const bundlesDest = resolve(bundles, 'plugin', manifest.plugin.id, '1.0.0'); + await access(bundlesSource); + await mkdirp(bundlesDest); + await copyAll(bundlesSource, bundlesDest); + } catch (e) { + // bundles are optional + if (!(e.code === 'ENOENT' && e.syscall === 'access')) throw e; + } + } + }); + + // packages/core/apps/core-apps-server-internal/src/bundle_routes/register_bundle_routes.ts + await copyAll( + resolve(buildSource, 'node_modules/@kbn/ui-shared-deps-npm/shared_built_assets'), + resolve(bundles, 'kbn-ui-shared-deps-npm') + ); + await copyAll( + resolve(buildSource, 'node_modules/@kbn/ui-shared-deps-src/shared_built_assets'), + resolve(bundles, 'kbn-ui-shared-deps-src') + ); + await copyAll( + resolve(buildSource, 'node_modules/@kbn/core/target/public'), + resolve(bundles, 'core') + ); + await copyAll(resolve(buildSource, 'node_modules/@kbn/monaco'), resolve(bundles, 'kbn-monaco')); + + // packages/core/apps/core-apps-server-internal/src/core_app.ts + await copyAll( + resolve(buildSource, 'node_modules/@kbn/core-apps-server-internal/assets'), + resolve(assets, 'ui') + ); + + await compressTar({ + source: assets, + destination: config.resolveFromTarget(`kibana-${buildVersion}-cdn-assets.tar.gz`), + archiverOptions: { + gzip: true, + gzipOptions: { + level: 9, + }, + }, + createRootDirectory: true, + rootDirectoryName: `kibana-${buildVersion}-cdn-assets`, + }); + }, +}; diff --git a/src/dev/build/tasks/index.ts b/src/dev/build/tasks/index.ts index 9abd34a2d9f51..e62c09e637d6a 100644 --- a/src/dev/build/tasks/index.ts +++ b/src/dev/build/tasks/index.ts @@ -14,6 +14,7 @@ export * from './clean_tasks'; export * from './copy_legacy_source_task'; export * from './create_archives_sources_task'; export * from './create_archives_task'; +export * from './create_cdn_assets_task'; export * from './create_empty_dirs_and_files_task'; export * from './create_readme_task'; export * from './download_cloud_dependencies'; From 6f1fbce97cae8238e5b0d8fb385a1140c3904c41 Mon Sep 17 00:00:00 2001 From: Gloria Hornero Date: Mon, 6 Nov 2023 20:30:47 +0100 Subject: [PATCH 02/12] [Security Solution] Unskips `detection_page_filters` tests (#170163) --- .../alerts/detection_page_filters.cy.ts | 25 ++++++------------- .../cypress/tasks/alerts.ts | 2 +- .../cypress/tasks/date_picker.ts | 12 ++++----- 3 files changed, 15 insertions(+), 24 deletions(-) diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/detection_page_filters.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/detection_page_filters.cy.ts index 9231b73843b1c..44b6f688b3474 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/detection_page_filters.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/detection_page_filters.cy.ts @@ -26,7 +26,7 @@ import { createRule } from '../../../tasks/api_calls/rules'; import { cleanKibana } from '../../../tasks/common'; import { login } from '../../../tasks/login'; import { visitWithTimeRange } from '../../../tasks/navigation'; -import { ALERTS_URL } from '../../../urls/navigation'; +import { ALERTS_URL, CASES_URL } from '../../../urls/navigation'; import { closePageFilterPopover, markAcknowledgedFirstAlert, @@ -39,8 +39,7 @@ import { waitForPageFilters, } from '../../../tasks/alerts'; import { ALERTS_COUNT, ALERTS_REFRESH_BTN, EMPTY_ALERT_TABLE } from '../../../screens/alerts'; -import { kqlSearch, navigateFromHeaderTo } from '../../../tasks/security_header'; -import { ALERTS, CASES } from '../../../screens/security_header'; +import { kqlSearch } from '../../../tasks/security_header'; import { addNewFilterGroupControlValues, cancelFieldEditing, @@ -108,10 +107,7 @@ const assertFilterControlsWithFilterObject = ( }); }; -// Failing: See https://github.com/elastic/kibana/issues/167914 -// Failing: See https://github.com/elastic/kibana/issues/167915 -// Failing: See https://github.com/elastic/kibana/issues/167914 -describe.skip(`Detections : Page Filters`, { tags: ['@ess', '@brokenInServerless'] }, () => { +describe(`Detections : Page Filters`, { tags: ['@ess', '@serverless'] }, () => { before(() => { cleanKibana(); createRule(getNewRule({ rule_id: 'custom_rule_filters' })); @@ -238,7 +234,7 @@ describe.skip(`Detections : Page Filters`, { tags: ['@ess', '@brokenInServerless cy.get(FILTER_GROUP_CHANGED_BANNER).should('be.visible'); }); - context.skip('with data modificiation', () => { + context('with data modificiation', () => { after(() => { cleanKibana(); createRule(getNewRule({ rule_id: 'custom_rule_filters' })); @@ -267,7 +263,7 @@ describe.skip(`Detections : Page Filters`, { tags: ['@ess', '@brokenInServerless it(`URL is updated when filters are updated`, () => { openPageFilterPopover(1); cy.get(OPTION_SELECTABLE(1, 'high')).should('be.visible'); - cy.get(OPTION_SELECTABLE(1, 'high')).click({}); + cy.get(OPTION_SELECTABLE(1, 'high')).click(); closePageFilterPopover(1); const NEW_FILTERS = DEFAULT_DETECTION_PAGE_FILTERS.map((filter) => { @@ -289,9 +285,8 @@ describe.skip(`Detections : Page Filters`, { tags: ['@ess', '@brokenInServerless cy.get(OPTION_LIST_VALUES(1)).contains('high'); waitForPageFilters(); - navigateFromHeaderTo(CASES); // navigate away from alert page - - navigateFromHeaderTo(ALERTS); // navigate back to alert page + visitWithTimeRange(CASES_URL); // navigate away from alert page + visitWithTimeRange(ALERTS_URL); // navigate back to alert page waitForPageFilters(); @@ -332,14 +327,11 @@ describe.skip(`Detections : Page Filters`, { tags: ['@ess', '@brokenInServerless }); context('Impact of inputs', () => { - afterEach(() => { - resetFilters(); - }); it('should recover from invalid kql Query result', () => { // do an invalid search // kqlSearch('\\'); - cy.get(ALERTS_REFRESH_BTN).trigger('click'); + cy.get(ALERTS_REFRESH_BTN).click(); waitForPageFilters(); cy.get(TOASTER).should('contain.text', 'KQLSyntaxError'); togglePageFilterPopover(0); @@ -372,7 +364,6 @@ describe.skip(`Detections : Page Filters`, { tags: ['@ess', '@brokenInServerless it('should take timeRange into account', () => { const startDateWithZeroAlerts = 'Jan 1, 2002 @ 00:00:00.000'; const endDateWithZeroAlerts = 'Jan 1, 2010 @ 00:00:00.000'; - setStartDate(startDateWithZeroAlerts); setEndDate(endDateWithZeroAlerts); diff --git a/x-pack/test/security_solution_cypress/cypress/tasks/alerts.ts b/x-pack/test/security_solution_cypress/cypress/tasks/alerts.ts index a81cc24ae9653..23b5d106cb6cf 100644 --- a/x-pack/test/security_solution_cypress/cypress/tasks/alerts.ts +++ b/x-pack/test/security_solution_cypress/cypress/tasks/alerts.ts @@ -171,7 +171,7 @@ export const refreshAlertPageFilter = () => { }; export const togglePageFilterPopover = (filterIndex: number) => { - cy.get(OPTION_LIST_VALUES(filterIndex)).click({ force: true }); + cy.get(OPTION_LIST_VALUES(filterIndex)).click(); }; export const openPageFilterPopover = (filterIndex: number) => { diff --git a/x-pack/test/security_solution_cypress/cypress/tasks/date_picker.ts b/x-pack/test/security_solution_cypress/cypress/tasks/date_picker.ts index 7ae1fa8c30b73..00d5a892593b6 100644 --- a/x-pack/test/security_solution_cypress/cypress/tasks/date_picker.ts +++ b/x-pack/test/security_solution_cypress/cypress/tasks/date_picker.ts @@ -31,12 +31,12 @@ export const setEndDateNow = (container: string = GLOBAL_FILTERS_CONTAINER) => { export const setEndDate = (date: string, container: string = GLOBAL_FILTERS_CONTAINER) => { cy.get(GET_LOCAL_DATE_PICKER_END_DATE_POPOVER_BUTTON(container)).first().click(); - cy.get(DATE_PICKER_ABSOLUTE_TAB).first().click({ force: true }); + cy.get(DATE_PICKER_ABSOLUTE_TAB).first().click(); - cy.get(DATE_PICKER_ABSOLUTE_INPUT).click({ force: true }); + cy.get(DATE_PICKER_ABSOLUTE_INPUT).click(); cy.get(DATE_PICKER_ABSOLUTE_INPUT).then(($el) => { if (Cypress.dom.isAttached($el)) { - cy.wrap($el).click({ force: true }); + cy.wrap($el).click(); } cy.wrap($el).type(`{selectall}{backspace}${date}{enter}`); }); @@ -45,12 +45,12 @@ export const setEndDate = (date: string, container: string = GLOBAL_FILTERS_CONT export const setStartDate = (date: string, container: string = GLOBAL_FILTERS_CONTAINER) => { cy.get(GET_LOCAL_DATE_PICKER_START_DATE_POPOVER_BUTTON(container)).first().click({}); - cy.get(DATE_PICKER_ABSOLUTE_TAB).first().click({ force: true }); + cy.get(DATE_PICKER_ABSOLUTE_TAB).first().click(); - cy.get(DATE_PICKER_ABSOLUTE_INPUT).click({ force: true }); + cy.get(DATE_PICKER_ABSOLUTE_INPUT).click(); cy.get(DATE_PICKER_ABSOLUTE_INPUT).then(($el) => { if (Cypress.dom.isAttached($el)) { - cy.wrap($el).click({ force: true }); + cy.wrap($el).click(); } cy.wrap($el).type(`{selectall}{backspace}${date}{enter}`); }); From e2645a7468a50e74842ffc4f97c8e38b53ae0b80 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Mon, 6 Nov 2023 12:38:32 -0700 Subject: [PATCH 03/12] [Security solution] Fix non-langchain error handling (#170672) --- .../server/lib/executor.test.ts | 79 +++++++++++++++++++ .../elastic_assistant/server/lib/executor.ts | 22 ++++-- .../lib/langchain/llm/actions_client_llm.ts | 2 +- 3 files changed, 95 insertions(+), 8 deletions(-) create mode 100644 x-pack/plugins/elastic_assistant/server/lib/executor.test.ts diff --git a/x-pack/plugins/elastic_assistant/server/lib/executor.test.ts b/x-pack/plugins/elastic_assistant/server/lib/executor.test.ts new file mode 100644 index 0000000000000..fda0b54995233 --- /dev/null +++ b/x-pack/plugins/elastic_assistant/server/lib/executor.test.ts @@ -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 { executeAction } from './executor'; +import { KibanaRequest } from '@kbn/core-http-server'; +import { RequestBody } from './langchain/types'; +import { PluginStartContract as ActionsPluginStart } from '@kbn/actions-plugin/server'; +const request = { + body: { + params: {}, + }, +} as KibanaRequest; + +describe('executeAction', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + it('should execute an action with the provided connectorId and request body params', async () => { + const actions = { + getActionsClientWithRequest: jest.fn().mockResolvedValue({ + execute: jest.fn().mockResolvedValue({ + status: 'ok', + data: { + message: 'Test message', + }, + }), + }), + } as unknown as ActionsPluginStart; + + const connectorId = '12345'; + + const response = await executeAction({ actions, request, connectorId }); + + expect(actions.getActionsClientWithRequest).toHaveBeenCalledWith(request); + expect(actions.getActionsClientWithRequest).toHaveBeenCalledTimes(1); + expect(response.connector_id).toBe(connectorId); + expect(response.data).toBe('Test message'); + expect(response.status).toBe('ok'); + }); + + it('should throw an error if action result status is "error"', async () => { + const actions = { + getActionsClientWithRequest: jest.fn().mockResolvedValue({ + execute: jest.fn().mockResolvedValue({ + status: 'error', + message: 'Error message', + serviceMessage: 'Service error message', + }), + }), + } as unknown as ActionsPluginStart; + const connectorId = '12345'; + + await expect(executeAction({ actions, request, connectorId })).rejects.toThrowError( + 'Action result status is error: Error message - Service error message' + ); + }); + + it('should throw an error if content of response data is not a string', async () => { + const actions = { + getActionsClientWithRequest: jest.fn().mockResolvedValue({ + execute: jest.fn().mockResolvedValue({ + status: 'ok', + data: { + message: 12345, + }, + }), + }), + } as unknown as ActionsPluginStart; + const connectorId = '12345'; + + await expect(executeAction({ actions, request, connectorId })).rejects.toThrowError( + 'Action result status is error: content should be a string, but it had an unexpected type: number' + ); + }); +}); diff --git a/x-pack/plugins/elastic_assistant/server/lib/executor.ts b/x-pack/plugins/elastic_assistant/server/lib/executor.ts index 936e3781731d8..21c0962e1c370 100644 --- a/x-pack/plugins/elastic_assistant/server/lib/executor.ts +++ b/x-pack/plugins/elastic_assistant/server/lib/executor.ts @@ -31,13 +31,21 @@ export const executeAction = async ({ actionId: connectorId, params: request.body.params, }); + + if (actionResult.status === 'error') { + throw new Error( + `Action result status is error: ${actionResult?.message} - ${actionResult?.serviceMessage}` + ); + } const content = get('data.message', actionResult); - if (typeof content === 'string') { - return { - connector_id: connectorId, - data: content, // the response from the actions framework - status: 'ok', - }; + if (typeof content !== 'string') { + throw new Error( + `Action result status is error: content should be a string, but it had an unexpected type: ${typeof content}` + ); } - throw new Error('Unexpected action result'); + return { + connector_id: connectorId, + data: content, // the response from the actions framework + status: 'ok', + }; }; diff --git a/x-pack/plugins/elastic_assistant/server/lib/langchain/llm/actions_client_llm.ts b/x-pack/plugins/elastic_assistant/server/lib/langchain/llm/actions_client_llm.ts index f499452e1d764..99fa1ac946909 100644 --- a/x-pack/plugins/elastic_assistant/server/lib/langchain/llm/actions_client_llm.ts +++ b/x-pack/plugins/elastic_assistant/server/lib/langchain/llm/actions_client_llm.ts @@ -92,7 +92,7 @@ export class ActionsClientLlm extends LLM { `${LLM_TYPE}: action result status is error: ${actionResult?.message} - ${actionResult?.serviceMessage}` ); } - // TODO: handle errors from the connector + const content = get('data.message', actionResult); if (typeof content !== 'string') { From 4189b26499bd864036c4af6ab6582cce4cd91d8d Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Mon, 6 Nov 2023 19:52:13 +0000 Subject: [PATCH 04/12] skip flaky suite (#170666) --- .../serverless/policy_details_with_security_essentials.cy.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/public/management/cypress/e2e/serverless/policy_details_with_security_essentials.cy.ts b/x-pack/plugins/security_solution/public/management/cypress/e2e/serverless/policy_details_with_security_essentials.cy.ts index 1cb0c382ca707..efb48f6543542 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/e2e/serverless/policy_details_with_security_essentials.cy.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/e2e/serverless/policy_details_with_security_essentials.cy.ts @@ -9,7 +9,8 @@ import { login } from '../../tasks/login'; import { visitPolicyDetailsPage } from '../../screens/policy_details'; import type { IndexedFleetEndpointPolicyResponse } from '../../../../../common/endpoint/data_loaders/index_fleet_endpoint_policy'; -describe( +// FLAKY: https://github.com/elastic/kibana/issues/170666 +describe.skip( 'When displaying the Policy Details in Security Essentials PLI', { tags: ['@serverless'], From f9ac6e98084eb9e0a335ad1ec95b33a8a1636d7f Mon Sep 17 00:00:00 2001 From: Philippe Oberti Date: Mon, 6 Nov 2023 14:31:16 -0600 Subject: [PATCH 05/12] [Security Solution] fix weird page height scroll due to timeline bottom bar (#170362) --- .../home/template_wrapper/bottom_bar/index.tsx | 10 ---------- .../public/app/home/template_wrapper/index.tsx | 16 ++-------------- 2 files changed, 2 insertions(+), 24 deletions(-) diff --git a/x-pack/plugins/security_solution/public/app/home/template_wrapper/bottom_bar/index.tsx b/x-pack/plugins/security_solution/public/app/home/template_wrapper/bottom_bar/index.tsx index 1967c80fea257..81d80666fd583 100644 --- a/x-pack/plugins/security_solution/public/app/home/template_wrapper/bottom_bar/index.tsx +++ b/x-pack/plugins/security_solution/public/app/home/template_wrapper/bottom_bar/index.tsx @@ -6,14 +6,11 @@ */ import React from 'react'; -import type { EuiBottomBarProps } from '@elastic/eui'; import { useKibana } from '../../../../common/lib/kibana/kibana_react'; import { TimelineId } from '../../../../../common/types/timeline'; import { Flyout } from '../../../../timelines/components/flyout'; import { useResolveRedirect } from '../../../../common/hooks/use_resolve_redirect'; -export const BOTTOM_BAR_CLASSNAME = 'timeline-bottom-bar'; - // eslint-disable-next-line react/display-name export const SecuritySolutionBottomBar = React.memo(() => { useResolveRedirect(); @@ -22,10 +19,3 @@ export const SecuritySolutionBottomBar = React.memo(() => { return ; }); - -export const SecuritySolutionBottomBarProps: EuiBottomBarProps & { - restrictWidth?: boolean | number | string; -} = { - className: BOTTOM_BAR_CLASSNAME, - 'data-test-subj': 'timeline-bottom-bar-container', -}; diff --git a/x-pack/plugins/security_solution/public/app/home/template_wrapper/index.tsx b/x-pack/plugins/security_solution/public/app/home/template_wrapper/index.tsx index ba2312b1740a1..1f174580ee0fc 100644 --- a/x-pack/plugins/security_solution/public/app/home/template_wrapper/index.tsx +++ b/x-pack/plugins/security_solution/public/app/home/template_wrapper/index.tsx @@ -17,11 +17,7 @@ import { TimelineId } from '../../../../common/types/timeline'; import { getTimelineShowStatusByIdSelector } from '../../../timelines/components/flyout/selectors'; import { useDeepEqualSelector } from '../../../common/hooks/use_selector'; import { GlobalKQLHeader } from './global_kql_header'; -import { - BOTTOM_BAR_CLASSNAME, - SecuritySolutionBottomBar, - SecuritySolutionBottomBarProps, -} from './bottom_bar'; +import { SecuritySolutionBottomBar } from './bottom_bar'; import { useShowTimeline } from '../../../common/utils/timeline/use_show_timeline'; /** @@ -39,14 +35,6 @@ const StyledKibanaPageTemplate = styled(KibanaPageTemplate)< background-color: ${({ theme }) => theme.colors.emptyShade}; } - .${BOTTOM_BAR_CLASSNAME} { - animation: 'none !important'; // disable the default bottom bar slide animation - background: ${({ theme }) => theme.colors.emptyShade}; // Override bottom bar black background - color: inherit; // Necessary to override the bottom bar 'white text' - transform: ${( - { $isShowingTimelineOverlay } // Since the bottom bar wraps the whole overlay now, need to override any transforms when it is open - ) => ($isShowingTimelineOverlay ? 'none' : 'translateY(calc(100% - 50px))')}; - .${IS_DRAGGING_CLASS_NAME} & { // When a drag is in process the bottom flyout should slide up to allow a drop transform: none; @@ -95,7 +83,7 @@ export const SecuritySolutionTemplateWrapper: React.FC {isTimelineBottomBarVisible && ( - + From 2beb8f7965049205913ad39711f106bb2f844b8a Mon Sep 17 00:00:00 2001 From: Davis Plumlee <56367316+dplumlee@users.noreply.github.com> Date: Mon, 6 Nov 2023 15:36:01 -0500 Subject: [PATCH 06/12] [Security Solution] Coverage overview rule duplication fix (#169708) --- ..._coverage_overview_dashboard_model.test.ts | 288 ++++++++++++++++++ ...build_coverage_overview_dashboard_model.ts | 8 +- ...uild_coverage_overview_mitre_graph.test.ts | 65 +--- .../build_coverage_overview_mitre_graph.ts | 19 +- .../coverage_overview/__mocks__/index.ts | 57 ++++ 5 files changed, 370 insertions(+), 67 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/coverage_overview/build_coverage_overview_dashboard_model.test.ts diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/coverage_overview/build_coverage_overview_dashboard_model.test.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/coverage_overview/build_coverage_overview_dashboard_model.test.ts new file mode 100644 index 0000000000000..01e57bc17948b --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/coverage_overview/build_coverage_overview_dashboard_model.test.ts @@ -0,0 +1,288 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { CoverageOverviewRuleActivity } from '../../../../../common/api/detection_engine'; +import type { CoverageOverviewResponse } from '../../../../../common/api/detection_engine'; +import { buildCoverageOverviewDashboardModel } from './build_coverage_overview_dashboard_model'; +import { + getMockCoverageOverviewSubtechniques, + getMockCoverageOverviewTactics, + getMockCoverageOverviewTechniques, +} from '../../model/coverage_overview/__mocks__'; + +const mockTactics = getMockCoverageOverviewTactics(); +const mockTechniques = getMockCoverageOverviewTechniques(); +const mockSubtechniques = getMockCoverageOverviewSubtechniques(); + +describe('buildCoverageOverviewDashboardModel', () => { + beforeEach(() => { + jest.mock('../../../../detections/mitre/mitre_tactics_techniques', () => { + return { + tactics: mockTactics, + techniques: mockTechniques, + subtechniques: mockSubtechniques, + }; + }); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('maps API response', async () => { + const mockApiResponse: CoverageOverviewResponse = { + coverage: { + TA001: ['test-rule-1'], + TA002: ['test-rule-1'], + T001: ['test-rule-1'], + T002: ['test-rule-1'], + 'T001.001': ['test-rule-1'], + }, + unmapped_rule_ids: ['test-rule-2'], + rules_data: { + 'test-rule-1': { + name: 'Test rule 1', + activity: CoverageOverviewRuleActivity.Enabled, + }, + 'test-rule-2': { + name: 'Test rule 2', + activity: CoverageOverviewRuleActivity.Enabled, + }, + }, + }; + + const model = await buildCoverageOverviewDashboardModel(mockApiResponse); + + expect(model).toEqual({ + metrics: { + totalEnabledRulesCount: 2, + totalRulesCount: 2, + }, + mitreTactics: [ + { + availableRules: [], + disabledRules: [], + enabledRules: [ + { + id: 'test-rule-1', + name: 'Test rule 1', + }, + ], + id: 'TA001', + name: 'Tactic 1', + reference: 'https://some-link/TA001', + techniques: [ + { + availableRules: [], + disabledRules: [], + enabledRules: [ + { + id: 'test-rule-1', + name: 'Test rule 1', + }, + ], + id: 'T001', + name: 'Technique 1', + reference: 'https://some-link/T001', + subtechniques: [ + { + availableRules: [], + disabledRules: [], + enabledRules: [ + { + id: 'test-rule-1', + name: 'Test rule 1', + }, + ], + id: 'T001.001', + name: 'Subtechnique 1', + reference: 'https://some-link/T001/001', + }, + { + availableRules: [], + disabledRules: [], + enabledRules: [], + id: 'T001.002', + name: 'Subtechnique 2', + reference: 'https://some-link/T001/002', + }, + ], + }, + { + availableRules: [], + disabledRules: [], + enabledRules: [ + { + id: 'test-rule-1', + name: 'Test rule 1', + }, + ], + id: 'T002', + name: 'Technique 2', + reference: 'https://some-link/T002', + subtechniques: [], + }, + ], + }, + { + availableRules: [], + disabledRules: [], + enabledRules: [ + { + id: 'test-rule-1', + name: 'Test rule 1', + }, + ], + id: 'TA002', + name: 'Tactic 2', + reference: 'https://some-link/TA002', + techniques: [ + { + availableRules: [], + disabledRules: [], + enabledRules: [ + { + id: 'test-rule-1', + name: 'Test rule 1', + }, + ], + id: 'T002', + name: 'Technique 2', + reference: 'https://some-link/T002', + subtechniques: [], + }, + ], + }, + ], + unmappedRules: { + availableRules: [], + disabledRules: [], + enabledRules: [expect.objectContaining({ id: 'test-rule-2' })], + }, + }); + }); + + it('maps techniques that appear in multiple tactics', async () => { + const mockApiResponse: CoverageOverviewResponse = { + coverage: { + TA001: ['test-rule-1', 'test-rule-2'], + TA002: ['test-rule-2'], + T002: ['test-rule-1', 'test-rule-2'], + }, + unmapped_rule_ids: [], + rules_data: { + 'test-rule-1': { + name: 'Test rule', + activity: CoverageOverviewRuleActivity.Enabled, + }, + 'test-rule-2': { + name: 'Test rule 2', + activity: CoverageOverviewRuleActivity.Enabled, + }, + }, + }; + + const model = await buildCoverageOverviewDashboardModel(mockApiResponse); + + expect(model.mitreTactics).toEqual([ + expect.objectContaining({ + id: 'TA001', + enabledRules: [ + expect.objectContaining({ id: 'test-rule-1' }), + expect.objectContaining({ id: 'test-rule-2' }), + ], + techniques: [ + expect.objectContaining({ id: 'T001', enabledRules: [] }), + expect.objectContaining({ + id: 'T002', + enabledRules: [ + expect.objectContaining({ id: 'test-rule-1' }), + expect.objectContaining({ id: 'test-rule-2' }), + ], + }), + ], + }), + expect.objectContaining({ + id: 'TA002', + enabledRules: [expect.objectContaining({ id: 'test-rule-2' })], + techniques: [ + expect.objectContaining({ + id: 'T002', + enabledRules: [expect.objectContaining({ id: 'test-rule-2' })], + }), + ], + }), + ]); + }); + + it('maps unmapped rules', async () => { + const mockApiResponse: CoverageOverviewResponse = { + coverage: { + TA001: ['test-rule-1'], + T002: ['test-rule-1'], + }, + unmapped_rule_ids: ['test-rule-2'], + rules_data: { + 'test-rule-1': { + name: 'Test rule 1', + activity: CoverageOverviewRuleActivity.Enabled, + }, + 'test-rule-2': { + name: 'Test rule 2', + activity: CoverageOverviewRuleActivity.Enabled, + }, + }, + }; + + const model = await buildCoverageOverviewDashboardModel(mockApiResponse); + + expect(model.unmappedRules).toEqual({ + availableRules: [], + disabledRules: [], + enabledRules: [expect.objectContaining({ id: 'test-rule-2' })], + }); + expect(model.mitreTactics).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: 'TA001', + enabledRules: expect.not.arrayContaining(['test-rule-2']), + }), + ]) + ); + }); + + it('maps metrics fields', async () => { + const mockApiResponse: CoverageOverviewResponse = { + coverage: { + TA001: ['test-rule-1'], + T002: ['test-rule-1'], + }, + unmapped_rule_ids: ['test-rule-2'], + rules_data: { + 'test-rule-1': { + name: 'Test rule 1', + activity: CoverageOverviewRuleActivity.Enabled, + }, + 'test-rule-2': { + name: 'Test rule 2', + activity: CoverageOverviewRuleActivity.Enabled, + }, + 'test-rule-3': { + name: 'Test rule 3', + activity: CoverageOverviewRuleActivity.Disabled, + }, + }, + }; + + const model = await buildCoverageOverviewDashboardModel(mockApiResponse); + + expect(model.metrics).toEqual({ + totalEnabledRulesCount: 2, + totalRulesCount: 3, + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/coverage_overview/build_coverage_overview_dashboard_model.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/coverage_overview/build_coverage_overview_dashboard_model.ts index 04d9c2e4cf4d0..fd0055d480e0b 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/coverage_overview/build_coverage_overview_dashboard_model.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/coverage_overview/build_coverage_overview_dashboard_model.ts @@ -40,12 +40,16 @@ export async function buildCoverageOverviewDashboardModel( for (const technique of tactic.techniques) { for (const ruleId of apiResponse.coverage[technique.id] ?? []) { - addRule(technique, ruleId, apiResponse.rules_data[ruleId]); + if (apiResponse.coverage[tactic.id]?.includes(ruleId)) { + addRule(technique, ruleId, apiResponse.rules_data[ruleId]); + } } for (const subtechnique of technique.subtechniques) { for (const ruleId of apiResponse.coverage[subtechnique.id] ?? []) { - addRule(subtechnique, ruleId, apiResponse.rules_data[ruleId]); + if (apiResponse.coverage[tactic.id]?.includes(ruleId)) { + addRule(subtechnique, ruleId, apiResponse.rules_data[ruleId]); + } } } } diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/coverage_overview/build_coverage_overview_mitre_graph.test.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/coverage_overview/build_coverage_overview_mitre_graph.test.ts index 3a95160c96356..edb56fd2d3d64 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/coverage_overview/build_coverage_overview_mitre_graph.test.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/coverage_overview/build_coverage_overview_mitre_graph.test.ts @@ -5,66 +5,19 @@ * 2.0. */ +import { + getMockCoverageOverviewTactics, + getMockCoverageOverviewTechniques, + getMockCoverageOverviewSubtechniques, +} from '../../model/coverage_overview/__mocks__'; import { buildCoverageOverviewMitreGraph } from './build_coverage_overview_mitre_graph'; describe('buildCoverageOverviewModel', () => { it('builds domain model', () => { - const tactics = [ - { - name: 'Tactic 1', - id: 'TA001', - reference: 'https://some-link/TA001', - label: 'Tactic 1', - value: 'tactic1', - }, - { - name: 'Tactic 2', - id: 'TA002', - reference: 'https://some-link/TA002', - label: 'Tactic 2', - value: 'tactic2', - }, - ]; - const techniques = [ - { - name: 'Technique 1', - id: 'T001', - reference: 'https://some-link/T001', - tactics: ['tactic-1'], - label: 'Technique 1', - value: 'technique1', - }, - { - name: 'Technique 2', - id: 'T002', - reference: 'https://some-link/T002', - tactics: ['tactic-1', 'tactic-2'], - label: 'Technique 2', - value: 'technique2', - }, - ]; - const subtechniques = [ - { - name: 'Subtechnique 1', - id: 'T001.001', - reference: 'https://some-link/T001/001', - tactics: ['tactic-1'], - techniqueId: 'T001', - label: 'Subtechnique 1', - value: 'subtechnique1', - }, - { - name: 'Subtechnique 2', - id: 'T001.002', - reference: 'https://some-link/T001/002', - tactics: ['tactic-1'], - techniqueId: 'T001', - label: 'Subtechnique 2', - value: 'subtechnique2', - }, - ]; - - const model = buildCoverageOverviewMitreGraph(tactics, techniques, subtechniques); + const mockTactics = getMockCoverageOverviewTactics(); + const mockTechniques = getMockCoverageOverviewTechniques(); + const mockSubtechniques = getMockCoverageOverviewSubtechniques(); + const model = buildCoverageOverviewMitreGraph(mockTactics, mockTechniques, mockSubtechniques); expect(model).toEqual([ { diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/coverage_overview/build_coverage_overview_mitre_graph.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/coverage_overview/build_coverage_overview_mitre_graph.ts index acae46d0f3701..51c14661ddba1 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/coverage_overview/build_coverage_overview_mitre_graph.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/coverage_overview/build_coverage_overview_mitre_graph.ts @@ -64,17 +64,18 @@ export function buildCoverageOverviewMitreGraph( const tacticToTechniquesMap = new Map(); // Map(kebabCase(tactic name) -> CoverageOverviewMitreTechnique) for (const technique of techniques) { - const coverageOverviewMitreTechnique: CoverageOverviewMitreTechnique = { - id: technique.id, - name: technique.name, - reference: technique.reference, - subtechniques: techniqueToSubtechniquesMap.get(technique.id) ?? [], - enabledRules: [], - disabledRules: [], - availableRules: [], - }; + const relatedSubtechniques = techniqueToSubtechniquesMap.get(technique.id) ?? []; for (const kebabCaseTacticName of technique.tactics) { + const coverageOverviewMitreTechnique: CoverageOverviewMitreTechnique = { + id: technique.id, + name: technique.name, + reference: technique.reference, + subtechniques: relatedSubtechniques, + enabledRules: [], + disabledRules: [], + availableRules: [], + }; const tacticTechniques = tacticToTechniquesMap.get(kebabCaseTacticName); if (!tacticTechniques) { diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/model/coverage_overview/__mocks__/index.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/model/coverage_overview/__mocks__/index.ts index 21a32787aa5db..d6751152b77a9 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/model/coverage_overview/__mocks__/index.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/model/coverage_overview/__mocks__/index.ts @@ -57,3 +57,60 @@ export const getMockCoverageOverviewDashboard = (): CoverageOverviewDashboard => totalEnabledRulesCount: 1, }, }); + +export const getMockCoverageOverviewTactics = () => [ + { + name: 'Tactic 1', + id: 'TA001', + reference: 'https://some-link/TA001', + label: 'Tactic 1', + value: 'tactic1', + }, + { + name: 'Tactic 2', + id: 'TA002', + reference: 'https://some-link/TA002', + label: 'Tactic 2', + value: 'tactic2', + }, +]; + +export const getMockCoverageOverviewTechniques = () => [ + { + name: 'Technique 1', + id: 'T001', + reference: 'https://some-link/T001', + tactics: ['tactic-1'], + label: 'Technique 1', + value: 'technique1', + }, + { + name: 'Technique 2', + id: 'T002', + reference: 'https://some-link/T002', + tactics: ['tactic-1', 'tactic-2'], + label: 'Technique 2', + value: 'technique2', + }, +]; + +export const getMockCoverageOverviewSubtechniques = () => [ + { + name: 'Subtechnique 1', + id: 'T001.001', + reference: 'https://some-link/T001/001', + tactics: ['tactic-1'], + techniqueId: 'T001', + label: 'Subtechnique 1', + value: 'subtechnique1', + }, + { + name: 'Subtechnique 2', + id: 'T001.002', + reference: 'https://some-link/T001/002', + tactics: ['tactic-1'], + techniqueId: 'T001', + label: 'Subtechnique 2', + value: 'subtechnique2', + }, +]; From 3a4e761cf38c924c5a69c68eb5a65ca293e52876 Mon Sep 17 00:00:00 2001 From: Tomasz Ciecierski Date: Mon, 6 Nov 2023 21:46:32 +0100 Subject: [PATCH 07/12] [EDR Workflows] Remove cypress-react-selector from Osquery (#170607) --- package.json | 1 - .../enterprise_search/cypress/tsconfig.json | 2 +- .../plugins/osquery/cypress/cypress.config.ts | 3 - .../cypress/e2e/all/add_integration.cy.ts | 6 +- .../cypress/e2e/all/alerts_linked_apps.cy.ts | 7 +- .../cypress/e2e/all/custom_space.cy.ts | 4 +- .../cypress/e2e/all/edit_saved_queries.cy.ts | 70 +++++-------------- .../osquery/cypress/e2e/all/live_query.cy.ts | 7 +- .../cypress/e2e/all/live_query_run.cy.ts | 51 ++++++-------- .../cypress/e2e/all/packs_create_edit.cy.ts | 9 ++- .../cypress/e2e/all/packs_integration.cy.ts | 14 ++-- .../cypress/e2e/all/saved_queries.cy.ts | 12 ++-- .../osquery/cypress/e2e/roles/reader.cy.ts | 46 +++++------- .../cypress/e2e/roles/t1_and_t2_analyst.cy.ts | 12 ++-- .../osquery/cypress/screens/integrations.ts | 3 +- .../osquery/cypress/screens/live_query.ts | 4 -- .../plugins/osquery/cypress/screens/packs.ts | 3 + .../cypress/serverless_cypress.config.ts | 3 - x-pack/plugins/osquery/cypress/support/e2e.ts | 1 - .../osquery/cypress/tasks/live_query.ts | 14 +--- .../osquery/cypress/tasks/navigation.ts | 22 ------ .../osquery/cypress/tasks/response_actions.ts | 1 + .../osquery/cypress/tasks/saved_queries.ts | 6 +- x-pack/plugins/osquery/cypress/tsconfig.json | 1 - .../osquery/public/packs/packs_table.tsx | 7 +- .../queries/platform_checkbox_group_field.tsx | 2 +- .../public/routes/saved_queries/edit/form.tsx | 2 +- .../osquery_investigation_guide_panel.tsx | 2 +- .../management/cypress/cypress_base.config.ts | 3 - .../public/management/cypress/support/e2e.ts | 1 - .../public/management/cypress/tsconfig.json | 1 - yarn.lock | 14 ---- 32 files changed, 108 insertions(+), 226 deletions(-) diff --git a/package.json b/package.json index 9af2ce5973b30..3f3a05f840cb0 100644 --- a/package.json +++ b/package.json @@ -1466,7 +1466,6 @@ "cypress-axe": "^1.5.0", "cypress-file-upload": "^5.0.8", "cypress-multi-reporters": "^1.6.3", - "cypress-react-selector": "^3.0.0", "cypress-real-events": "^1.10.3", "cypress-recurse": "^1.35.2", "date-fns": "^2.29.3", diff --git a/x-pack/plugins/enterprise_search/cypress/tsconfig.json b/x-pack/plugins/enterprise_search/cypress/tsconfig.json index 06640dca03363..a6b602cb28192 100644 --- a/x-pack/plugins/enterprise_search/cypress/tsconfig.json +++ b/x-pack/plugins/enterprise_search/cypress/tsconfig.json @@ -4,7 +4,7 @@ "exclude": ["target/**/*"], "compilerOptions": { "outDir": "target/types", - "types": ["cypress", "node", "cypress-react-selector"], + "types": ["cypress", "node"], "resolveJsonModule": true }, "kbn_references": [ diff --git a/x-pack/plugins/osquery/cypress/cypress.config.ts b/x-pack/plugins/osquery/cypress/cypress.config.ts index defc409a76719..b8c0ef91a8217 100644 --- a/x-pack/plugins/osquery/cypress/cypress.config.ts +++ b/x-pack/plugins/osquery/cypress/cypress.config.ts @@ -45,9 +45,6 @@ export default defineCypressConfig({ experimentalStudio: true, env: { - 'cypress-react-selector': { - root: '#osquery-app', - }, grepFilterSpecs: true, grepTags: '@ess', grepOmitFiltered: true, diff --git a/x-pack/plugins/osquery/cypress/e2e/all/add_integration.cy.ts b/x-pack/plugins/osquery/cypress/e2e/all/add_integration.cy.ts index 0712c3b157571..882bfcba65f43 100644 --- a/x-pack/plugins/osquery/cypress/e2e/all/add_integration.cy.ts +++ b/x-pack/plugins/osquery/cypress/e2e/all/add_integration.cy.ts @@ -22,7 +22,7 @@ import { createOldOsqueryPath, FLEET_AGENT_POLICIES, NAV_SEARCH_INPUT_OSQUERY_RESULTS, - navigateToWithoutWaitForReact, + navigateTo, OSQUERY, } from '../../tasks/navigation'; import { @@ -175,7 +175,7 @@ describe('ALL - Add Integration', { tags: ['@ess', '@serverless'] }, () => { cy.contains(`version: ${oldVersion}`); cy.getBySel('euiFlyoutCloseButton').click(); - navigateToWithoutWaitForReact('app/osquery/packs'); + navigateTo('app/osquery/packs'); cy.getBySel(ADD_PACK_HEADER_BUTTON).click(); cy.get(formFieldInputSelector('name')).type(`${packName}{downArrow}{enter}`); cy.getBySel('policyIdsComboBox').type(`${policyName} {downArrow}{enter}`); @@ -205,7 +205,7 @@ describe('ALL - Add Integration', { tags: ['@ess', '@serverless'] }, () => { integrationExistsWithinPolicyDetails(integrationName); // test list of prebuilt queries - navigateToWithoutWaitForReact('/app/osquery/saved_queries'); + navigateTo('/app/osquery/saved_queries'); cy.get(TABLE_ROWS).should('have.length.above', 5); }); }); diff --git a/x-pack/plugins/osquery/cypress/e2e/all/alerts_linked_apps.cy.ts b/x-pack/plugins/osquery/cypress/e2e/all/alerts_linked_apps.cy.ts index 945410e656beb..96a7d785dc178 100644 --- a/x-pack/plugins/osquery/cypress/e2e/all/alerts_linked_apps.cy.ts +++ b/x-pack/plugins/osquery/cypress/e2e/all/alerts_linked_apps.cy.ts @@ -42,15 +42,14 @@ describe( }); it('should be able to add investigation guides to response actions', () => { - const investigationGuideNote = - 'You have queries in the investigation guide. Add them as response actions?'; cy.getBySel('editRuleSettingsLink').click(); cy.getBySel('globalLoadingIndicator').should('not.exist'); cy.getBySel('edit-rule-actions-tab').click(); - cy.contains(investigationGuideNote); + cy.getBySel('osquery-investigation-guide-text').should('exist'); + cy.getBySel('osqueryAddInvestigationGuideQueries').should('not.be.disabled'); cy.getBySel('osqueryAddInvestigationGuideQueries').click(); - cy.contains(investigationGuideNote).should('not.exist'); + cy.getBySel('osquery-investigation-guide-text').should('not.exist'); cy.getBySel(RESPONSE_ACTIONS_ITEM_0).within(() => { cy.contains("SELECT * FROM os_version where name='{{host.os.name}}';"); diff --git a/x-pack/plugins/osquery/cypress/e2e/all/custom_space.cy.ts b/x-pack/plugins/osquery/cypress/e2e/all/custom_space.cy.ts index ba85ec700ccc2..f8e95c64eb7e2 100644 --- a/x-pack/plugins/osquery/cypress/e2e/all/custom_space.cy.ts +++ b/x-pack/plugins/osquery/cypress/e2e/all/custom_space.cy.ts @@ -100,9 +100,7 @@ describe('ALL - Custom space', () => { it('runs packs normally', () => { cy.contains('Packs').click(); cy.contains('Create pack').click(); - cy.react('CustomItemAction', { - props: { item: { name: packName } }, - }).click(); + cy.getBySel(`play-${packName}-button`).click(); selectAllAgents(); cy.contains('Submit').click(); checkResults(); diff --git a/x-pack/plugins/osquery/cypress/e2e/all/edit_saved_queries.cy.ts b/x-pack/plugins/osquery/cypress/e2e/all/edit_saved_queries.cy.ts index 99381684e5b7a..75a720327f5ef 100644 --- a/x-pack/plugins/osquery/cypress/e2e/all/edit_saved_queries.cy.ts +++ b/x-pack/plugins/osquery/cypress/e2e/all/edit_saved_queries.cy.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { customActionEditSavedQuerySelector, UPDATE_QUERY_BUTTON } from '../../screens/packs'; import { navigateTo } from '../../tasks/navigation'; import { loadSavedQuery, cleanupSavedQuery } from '../../tasks/api_fixtures'; import { ServerlessRoleName } from '../../support/roles'; @@ -30,75 +31,36 @@ describe('ALL - Edit saved query', { tags: ['@ess', '@serverless'] }, () => { }); it('by changing ecs mappings and platforms', () => { - cy.react('CustomItemAction', { - props: { index: 1, item: { id: savedQueryName } }, - }).click(); + cy.get(customActionEditSavedQuerySelector(savedQueryName)).click(); cy.contains('Custom key/value pairs.').should('exist'); cy.contains('Hours of uptime').should('exist'); cy.get('[data-test-subj="ECSMappingEditorForm"]') .first() .within(() => { - cy.react('EuiButtonIcon', { props: { iconType: 'trash' } }).click(); + cy.get(`[aria-label="Delete ECS mapping row"]`).click(); }); - cy.react('PlatformCheckBoxGroupField') - .first() - .within(() => { - cy.react('EuiCheckbox', { - props: { - id: 'linux', - checked: true, - }, - }).should('exist'); - cy.react('EuiCheckbox', { - props: { - id: 'darwin', - checked: true, - }, - }).should('exist'); - - cy.react('EuiCheckbox', { - props: { - id: 'windows', - checked: false, - }, - }).should('exist'); - }); + cy.getBySel('osquery-platform-checkbox-group').within(() => { + cy.get('input[id="linux"]').should('be.checked'); + cy.get('input[id="darwin"]').should('be.checked'); + cy.get('input[id="windows"]').should('not.be.checked'); + }); cy.get('#windows').check({ force: true }); - cy.react('EuiButton').contains('Update query').click(); + cy.getBySel(UPDATE_QUERY_BUTTON).click(); cy.wait(5000); - cy.react('CustomItemAction', { - props: { index: 1, item: { id: savedQueryName } }, - }).click(); + cy.get(customActionEditSavedQuerySelector(savedQueryName)).click(); + cy.contains('Custom key/value pairs').should('not.exist'); cy.contains('Hours of uptime').should('not.exist'); - cy.react('PlatformCheckBoxGroupField') - .first() - .within(() => { - cy.react('EuiCheckbox', { - props: { - id: 'linux', - checked: true, - }, - }).should('exist'); - cy.react('EuiCheckbox', { - props: { - id: 'darwin', - checked: true, - }, - }).should('exist'); - - cy.react('EuiCheckbox', { - props: { - id: 'windows', - checked: true, - }, - }).should('exist'); - }); + cy.getBySel('osquery-platform-checkbox-group').within(() => { + cy.get('input[id="linux"]').should('be.checked'); + cy.get('input[id="darwin"]').should('be.checked'); + cy.get('input[id="windows"]').should('be.checked'); + }); }); }); diff --git a/x-pack/plugins/osquery/cypress/e2e/all/live_query.cy.ts b/x-pack/plugins/osquery/cypress/e2e/all/live_query.cy.ts index 21308b52ba057..a5f9fdab66a99 100644 --- a/x-pack/plugins/osquery/cypress/e2e/all/live_query.cy.ts +++ b/x-pack/plugins/osquery/cypress/e2e/all/live_query.cy.ts @@ -44,10 +44,7 @@ describe('ALL - Live Query', { tags: ['@ess', '@serverless'] }, () => { cy.contains('ECS field is required.').should('not.exist'); checkResults(); - cy.react('Cell', { props: { colIndex: 0 } }) - .should('exist') - .first() - .click(); + cy.get('[data-gridcell-column-index="0"][data-gridcell-row-index="0"]').should('exist').click(); cy.url().should('include', 'app/fleet/agents/'); }); @@ -82,7 +79,7 @@ describe('ALL - Live Query', { tags: ['@ess', '@serverless'] }, () => { // check if it get's bigger when we add more lines cy.get(LIVE_QUERY_EDITOR).invoke('height').should('be.gt', 220).and('be.lt', 300); inputQuery(multilineQuery); - cy.get(LIVE_QUERY_EDITOR).invoke('height').should('be.gt', 350).and('be.lt', 550); + cy.get(LIVE_QUERY_EDITOR).invoke('height').should('be.gt', 350).and('be.lt', 600); inputQuery('{selectall}{backspace}{selectall}{backspace}'); // not sure if this is how it used to work when I implemented the functionality, but let's leave it like this for now diff --git a/x-pack/plugins/osquery/cypress/e2e/all/live_query_run.cy.ts b/x-pack/plugins/osquery/cypress/e2e/all/live_query_run.cy.ts index f1907c506959b..1200e3e6f610d 100644 --- a/x-pack/plugins/osquery/cypress/e2e/all/live_query_run.cy.ts +++ b/x-pack/plugins/osquery/cypress/e2e/all/live_query_run.cy.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { SAVED_QUERY_DROPDOWN_SELECT } from '../../screens/packs'; import { navigateTo } from '../../tasks/navigation'; import { checkActionItemsInResults, @@ -15,12 +16,7 @@ import { typeInECSFieldInput, typeInOsqueryFieldInput, } from '../../tasks/live_query'; -import { - LIVE_QUERY_EDITOR, - RESULTS_TABLE, - RESULTS_TABLE_BUTTON, - RESULTS_TABLE_CELL_WRRAPER, -} from '../../screens/live_query'; +import { LIVE_QUERY_EDITOR, RESULTS_TABLE, RESULTS_TABLE_BUTTON } from '../../screens/live_query'; import { getAdvancedButton } from '../../screens/integrations'; import { loadSavedQuery, cleanupSavedQuery } from '../../tasks/api_fixtures'; import { ServerlessRoleName } from '../../support/roles'; @@ -36,7 +32,7 @@ describe('ALL - Live Query run custom and saved', { tags: ['@ess', '@serverless' ecs_mapping: {}, }).then((savedQuery) => { savedQueryId = savedQuery.saved_object_id; - savedQueryName = savedQuery.name; + savedQueryName = savedQuery.id; }); }); @@ -64,12 +60,12 @@ describe('ALL - Live Query run custom and saved', { tags: ['@ess', '@serverless' cases: true, timeline: false, }); - cy.react(RESULTS_TABLE_CELL_WRRAPER, { - props: { id: 'osquery.days.number', index: 1 }, - }).should('exist'); - cy.react(RESULTS_TABLE_CELL_WRRAPER, { - props: { id: 'osquery.hours.number', index: 2 }, - }).should('exist'); + cy.get( + '[data-gridcell-column-index="1"][data-test-subj="dataGridHeaderCell-osquery.days.number"]' + ).should('exist'); + cy.get( + '[data-gridcell-column-index="2"][data-test-subj="dataGridHeaderCell-osquery.hours.number"]' + ).should('exist'); getAdvancedButton().click(); typeInECSFieldInput('message{downArrow}{enter}'); @@ -80,37 +76,34 @@ describe('ALL - Live Query run custom and saved', { tags: ['@ess', '@serverless' cy.getBySel(RESULTS_TABLE).within(() => { cy.getBySel(RESULTS_TABLE_BUTTON).should('exist'); }); - cy.react(RESULTS_TABLE_CELL_WRRAPER, { - props: { id: 'message', index: 1 }, - }).should('exist'); - cy.react(RESULTS_TABLE_CELL_WRRAPER, { - props: { id: 'osquery.days.number', index: 2 }, - }) - .react('EuiIconTip', { props: { type: 'indexMapping' } }) - .should('exist'); + cy.get('[data-gridcell-column-index="1"][data-test-subj="dataGridHeaderCell-message"]').should( + 'exist' + ); + cy.get( + '[data-gridcell-column-index="2"][data-test-subj="dataGridHeaderCell-osquery.days.number"]' + ) + .should('exist') + .within(() => { + cy.get(`.euiToolTipAnchor`); + }); }); it('should run customized saved query', () => { cy.contains('New live query').click(); selectAllAgents(); - cy.react('SavedQueriesDropdown').type(`${savedQueryName}{downArrow}{enter}`); + cy.getBySel(SAVED_QUERY_DROPDOWN_SELECT).type(`${savedQueryName}{downArrow}{enter}`); inputQuery('{selectall}{backspace}select * from users;'); cy.wait(1000); submitQuery(); checkResults(); navigateTo('/app/osquery'); - cy.react('EuiButtonIcon', { props: { iconType: 'play' } }) - .eq(0) - .should('be.visible') - .click(); + cy.get('[aria-label="Run query"]').first().should('be.visible').click(); cy.get(LIVE_QUERY_EDITOR).contains('select * from users;'); }); it('should open query details by clicking the details icon', () => { - cy.react('EuiButtonIcon', { props: { iconType: 'visTable' } }) - .first() - .click(); + cy.get('[aria-label="Details"]').first().should('be.visible').click(); cy.contains('Live query details'); cy.contains('select * from users;'); }); diff --git a/x-pack/plugins/osquery/cypress/e2e/all/packs_create_edit.cy.ts b/x-pack/plugins/osquery/cypress/e2e/all/packs_create_edit.cy.ts index 469b2ce955853..64cb28d93d22c 100644 --- a/x-pack/plugins/osquery/cypress/e2e/all/packs_create_edit.cy.ts +++ b/x-pack/plugins/osquery/cypress/e2e/all/packs_create_edit.cy.ts @@ -20,9 +20,10 @@ import { TABLE_ROWS, formFieldInputSelector, FLYOUT_SAVED_QUERY_CANCEL_BUTTON, + customActionRunSavedQuerySelector, } from '../../screens/packs'; import { API_VERSIONS } from '../../../common/constants'; -import { navigateToWithoutWaitForReact } from '../../tasks/navigation'; +import { navigateTo } from '../../tasks/navigation'; import { deleteAndConfirm, inputQuery } from '../../tasks/live_query'; import { changePackActiveStatus, preparePack } from '../../tasks/packs'; import { @@ -96,7 +97,7 @@ describe('Packs - Create and Edit', { tags: ['@ess', '@serverless'] }, () => { beforeEach(() => { cy.login(ServerlessRoleName.SOC_MANAGER); - navigateToWithoutWaitForReact('/app/osquery'); + navigateTo('/app/osquery'); }); after(() => { @@ -470,9 +471,7 @@ describe('Packs - Create and Edit', { tags: ['@ess', '@serverless'] }, () => { it('', () => { preparePack(packName); - cy.react('CustomItemAction', { - props: { index: 0, item: { id: savedQueryName } }, - }) + cy.get(customActionRunSavedQuerySelector(savedQueryName)) .should('exist') .within(() => { cy.get('a') diff --git a/x-pack/plugins/osquery/cypress/e2e/all/packs_integration.cy.ts b/x-pack/plugins/osquery/cypress/e2e/all/packs_integration.cy.ts index c6a7a7087d302..73dc45837e2b3 100644 --- a/x-pack/plugins/osquery/cypress/e2e/all/packs_integration.cy.ts +++ b/x-pack/plugins/osquery/cypress/e2e/all/packs_integration.cy.ts @@ -17,7 +17,7 @@ import { formFieldInputSelector, } from '../../screens/packs'; import { API_VERSIONS } from '../../../common/constants'; -import { FLEET_AGENT_POLICIES, navigateToWithoutWaitForReact } from '../../tasks/navigation'; +import { FLEET_AGENT_POLICIES, navigateTo } from '../../tasks/navigation'; import { checkActionItemsInResults, checkResults, @@ -69,7 +69,7 @@ describe('ALL - Packs', { tags: ['@ess', '@serverless'] }, () => { cy.contains(integration).click(); addIntegration(AGENT_POLICY_NAME); cy.contains('Add Elastic Agent later').click(); - navigateToWithoutWaitForReact('app/osquery/packs'); + navigateTo('app/osquery/packs'); cy.getBySel(ADD_PACK_HEADER_BUTTON).click(); cy.get(formFieldInputSelector('name')).type(`${REMOVING_PACK}{downArrow}{enter}`); cy.getBySel(POLICY_SELECT_COMBOBOX).type(`${AGENT_POLICY_NAME}{downArrow}{enter}`); @@ -93,7 +93,7 @@ describe('ALL - Packs', { tags: ['@ess', '@serverless'] }, () => { cy.contains(/^Delete integration$/).click(); closeModalIfVisible(); cy.contains(/^Deleted integration 'osquery_manager-*/); - navigateToWithoutWaitForReact('app/osquery/packs'); + navigateTo('app/osquery/packs'); cy.contains(REMOVING_PACK).click(); cy.contains(`${REMOVING_PACK} details`).should('exist'); cy.wait(1000); @@ -113,7 +113,7 @@ describe('ALL - Packs', { tags: ['@ess', '@serverless'] }, () => { describe('', () => { beforeEach(() => { cy.login(ServerlessRoleName.SOC_MANAGER); - navigateToWithoutWaitForReact('/app/osquery/packs'); + navigateTo('/app/osquery/packs'); }); it('should load prebuilt packs', () => { cy.contains('Load Elastic prebuilt packs').click(); @@ -159,7 +159,7 @@ describe('ALL - Packs', { tags: ['@ess', '@serverless'] }, () => { }); it('should be able to run live prebuilt pack', () => { - navigateToWithoutWaitForReact('/app/osquery/live_queries'); + navigateTo('/app/osquery/live_queries'); cy.contains('New live query').click(); cy.contains('Run a set of queries in a pack.').click(); cy.get(LIVE_QUERY_EDITOR).should('not.exist'); @@ -174,7 +174,7 @@ describe('ALL - Packs', { tags: ['@ess', '@serverless'] }, () => { cases: true, timeline: false, }); - navigateToWithoutWaitForReact('/app/osquery'); + navigateTo('/app/osquery'); cy.contains('osquery-monitoring'); }); }); @@ -183,7 +183,7 @@ describe('ALL - Packs', { tags: ['@ess', '@serverless'] }, () => { describe('Global packs', { tags: ['@ess', '@serverless'] }, () => { beforeEach(() => { cy.login(ServerlessRoleName.PLATFORM_ENGINEER); - navigateToWithoutWaitForReact('/app/osquery/packs'); + navigateTo('/app/osquery/packs'); }); describe('add proper shard to policies packs config', () => { diff --git a/x-pack/plugins/osquery/cypress/e2e/all/saved_queries.cy.ts b/x-pack/plugins/osquery/cypress/e2e/all/saved_queries.cy.ts index 99d62007ea60b..1319d4e173ec4 100644 --- a/x-pack/plugins/osquery/cypress/e2e/all/saved_queries.cy.ts +++ b/x-pack/plugins/osquery/cypress/e2e/all/saved_queries.cy.ts @@ -17,13 +17,12 @@ import { addToCase, checkResults, deleteAndConfirm, - findFormFieldByRowsLabelAndType, inputQuery, selectAllAgents, submitQuery, viewRecentCaseAndCheckResults, } from '../../tasks/live_query'; -import { navigateToWithoutWaitForReact } from '../../tasks/navigation'; +import { navigateTo } from '../../tasks/navigation'; import { getSavedQueriesComplexTest } from '../../tasks/saved_queries'; import { loadCase, @@ -46,7 +45,7 @@ describe('ALL - Saved queries', { tags: ['@ess', '@serverless'] }, () => { beforeEach(() => { cy.login(ServerlessRoleName.SOC_MANAGER); - navigateToWithoutWaitForReact('/app/osquery'); + navigateTo('/app/osquery'); }); after(() => { @@ -59,7 +58,8 @@ describe('ALL - Saved queries', { tags: ['@ess', '@serverless'] }, () => { cy.contains('Saved queries').click(); cy.contains('Add saved query').click(); - findFormFieldByRowsLabelAndType('ID', 'users_elastic'); + cy.get('input[name="id"]').type(`users_elastic{downArrow}{enter}`); + cy.contains('ID must be unique').should('not.exist'); inputQuery('test'); cy.contains('Save query').click(); @@ -100,7 +100,7 @@ describe('ALL - Saved queries', { tags: ['@ess', '@serverless'] }, () => { beforeEach(() => { cy.login(ServerlessRoleName.SOC_MANAGER); - navigateToWithoutWaitForReact('/app/osquery/saved_queries'); + navigateTo('/app/osquery/saved_queries'); cy.getBySel('tablePaginationPopoverButton').click(); cy.getBySel('tablePagination-50-rows').click(); }); @@ -130,7 +130,7 @@ describe('ALL - Saved queries', { tags: ['@ess', '@serverless'] }, () => { it('user can not delete prebuilt saved query but can delete normal saved query', () => { cy.get(customActionEditSavedQuerySelector('users_elastic')).click(); cy.contains('Delete query').should('not.exist'); - navigateToWithoutWaitForReact(`/app/osquery/saved_queries/${savedQueryId}`); + navigateTo(`/app/osquery/saved_queries/${savedQueryId}`); deleteAndConfirm('query'); }); diff --git a/x-pack/plugins/osquery/cypress/e2e/roles/reader.cy.ts b/x-pack/plugins/osquery/cypress/e2e/roles/reader.cy.ts index 67f7eff7f3ed4..3f3ffa067b61f 100644 --- a/x-pack/plugins/osquery/cypress/e2e/roles/reader.cy.ts +++ b/x-pack/plugins/osquery/cypress/e2e/roles/reader.cy.ts @@ -5,6 +5,12 @@ * 2.0. */ +import { + activeStateSwitchComponentSelector, + customActionEditSavedQuerySelector, + customActionRunSavedQuerySelector, + formFieldInputSelector, +} from '../../screens/packs'; import { navigateTo } from '../../tasks/navigation'; import { cleanupPack, @@ -49,19 +55,10 @@ describe('Reader - only READ', { tags: ['@ess'] }, () => { navigateTo('/app/osquery/saved_queries'); cy.contains(savedQueryName); cy.contains('Add saved query').should('be.disabled'); - cy.react('PlayButtonComponent', { - props: { savedQuery: { id: savedQueryName } }, - options: { timeout: 3000 }, - }).should('not.exist'); - cy.react('CustomItemAction', { - props: { index: 1, item: { id: savedQueryName } }, - }).click(); - cy.react('EuiFormRow', { props: { label: 'ID' } }) - .getBySel('input') - .should('be.disabled'); - cy.react('EuiFormRow', { props: { label: 'Description (optional)' } }) - .getBySel('input') - .should('be.disabled'); + cy.get(customActionRunSavedQuerySelector(savedQueryName)).should('not.exist'); + cy.get(customActionEditSavedQuerySelector(savedQueryName)).click(); + cy.get(formFieldInputSelector('id')).should('be.disabled'); + cy.get(formFieldInputSelector('description')).should('be.disabled'); cy.contains('Update query').should('not.exist'); cy.contains(`Delete query`).should('not.exist'); @@ -76,8 +73,8 @@ describe('Reader - only READ', { tags: ['@ess'] }, () => { navigateTo('/app/osquery/live_queries'); cy.contains('New live query').should('be.disabled'); cy.contains(liveQueryQuery); - cy.react('EuiIconPlay', { options: { timeout: 3000 } }).should('not.exist'); - cy.react('ActionTableResultsButton').should('exist'); + cy.get(customActionRunSavedQuerySelector(savedQueryName)).should('not.exist'); + cy.get(`[aria-label="Details"]`).should('exist'); }); it('should not be able to add nor edit packs', () => { @@ -85,22 +82,13 @@ describe('Reader - only READ', { tags: ['@ess'] }, () => { cy.contains('Add pack').should('be.disabled'); cy.getBySel('tablePaginationPopoverButton').click(); cy.getBySel('tablePagination-50-rows').click(); - cy.react('ActiveStateSwitchComponent', { - props: { item: { name: packName } }, - }) - .find('button') - .should('be.disabled'); + + cy.get(activeStateSwitchComponentSelector(packName)).should('be.disabled'); + cy.contains(packName).click(); cy.contains(`${packName} details`); cy.contains('Edit').should('be.disabled'); - // TODO: Verify assertions - cy.react('CustomItemAction', { - props: { index: 0, item: { id: savedQueryName } }, - options: { timeout: 3000 }, - }).should('not.exist'); - cy.react('CustomItemAction', { - props: { index: 1, item: { id: savedQueryName } }, - options: { timeout: 3000 }, - }).should('not.exist'); + cy.get(customActionRunSavedQuerySelector(savedQueryName)).should('not.exist'); + cy.get(customActionEditSavedQuerySelector(savedQueryName)).should('not.exist'); }); }); diff --git a/x-pack/plugins/osquery/cypress/e2e/roles/t1_and_t2_analyst.cy.ts b/x-pack/plugins/osquery/cypress/e2e/roles/t1_and_t2_analyst.cy.ts index 1f8fce2415e55..470c844530452 100644 --- a/x-pack/plugins/osquery/cypress/e2e/roles/t1_and_t2_analyst.cy.ts +++ b/x-pack/plugins/osquery/cypress/e2e/roles/t1_and_t2_analyst.cy.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { navigateToWithoutWaitForReact } from '../../tasks/navigation'; +import { navigateTo } from '../../tasks/navigation'; import { checkActionItemsInResults, checkResults, @@ -55,7 +55,7 @@ describe(`T1 and T2 analysts`, { tags: ['@ess', '@serverless'] }, () => { }); it('should be able to run saved queries but not add new ones', () => { - navigateToWithoutWaitForReact('/app/osquery/saved_queries'); + navigateTo('/app/osquery/saved_queries'); cy.contains(savedQueryName); cy.contains('Add saved query').should('be.disabled'); cy.get(`[aria-label="Run ${savedQueryName}"]`).should('not.be.disabled'); @@ -74,7 +74,7 @@ describe(`T1 and T2 analysts`, { tags: ['@ess', '@serverless'] }, () => { }); it('should be able to play in live queries history', () => { - navigateToWithoutWaitForReact('/app/osquery/live_queries'); + navigateTo('/app/osquery/live_queries'); cy.contains('New live query').should('not.be.disabled'); cy.contains(liveQueryQuery); cy.get(`[aria-label="Run query"]`).first().should('not.be.disabled'); @@ -85,7 +85,7 @@ describe(`T1 and T2 analysts`, { tags: ['@ess', '@serverless'] }, () => { }); it('should be able to use saved query in a new query', () => { - navigateToWithoutWaitForReact('/app/osquery/live_queries'); + navigateTo('/app/osquery/live_queries'); cy.contains('New live query').should('not.be.disabled').click(); selectAllAgents(); cy.getBySel('savedQuerySelect').type(`${savedQueryName}{downArrow} {enter}`); @@ -95,7 +95,7 @@ describe(`T1 and T2 analysts`, { tags: ['@ess', '@serverless'] }, () => { }); it('should not be able to add nor edit packs', () => { - navigateToWithoutWaitForReact('/app/osquery/packs'); + navigateTo('/app/osquery/packs'); cy.getBySel('tablePaginationPopoverButton').click(); cy.getBySel('tablePagination-50-rows').click(); cy.contains('Add pack').should('be.disabled'); @@ -109,7 +109,7 @@ describe(`T1 and T2 analysts`, { tags: ['@ess', '@serverless'] }, () => { }); it('should not be able to create new liveQuery from scratch', () => { - navigateToWithoutWaitForReact('/app/osquery'); + navigateTo('/app/osquery'); cy.contains('New live query').click(); selectAllAgents(); diff --git a/x-pack/plugins/osquery/cypress/screens/integrations.ts b/x-pack/plugins/osquery/cypress/screens/integrations.ts index 97a6b382543cd..5c3b7408e3047 100644 --- a/x-pack/plugins/osquery/cypress/screens/integrations.ts +++ b/x-pack/plugins/osquery/cypress/screens/integrations.ts @@ -27,8 +27,7 @@ export const LATEST_VERSION = 'latestVersion'; export const PACKAGE_VERSION = 'packageVersionText'; export const SAVE_PACKAGE_CONFIRM = '[data-test-subj=confirmModalConfirmButton]'; -export const getAdvancedButton = () => - cy.react('EuiAccordionClass', { props: { buttonContent: 'Advanced' } }).last(); +export const getAdvancedButton = () => cy.get(`[data-test-subj="advanced-accordion-content"]`); export const DATE_PICKER_ABSOLUTE_TAB = 'superDatePickerAbsoluteTab'; export const DATE_PICKER_ABSOLUTE_TAB_SEL = `[data-test-subj=${DATE_PICKER_ABSOLUTE_TAB}]`; diff --git a/x-pack/plugins/osquery/cypress/screens/live_query.ts b/x-pack/plugins/osquery/cypress/screens/live_query.ts index a8adb91121ef3..9dc543072fe07 100644 --- a/x-pack/plugins/osquery/cypress/screens/live_query.ts +++ b/x-pack/plugins/osquery/cypress/screens/live_query.ts @@ -15,9 +15,5 @@ export const SUBMIT_BUTTON = '#submit-button'; export const RESULTS_TABLE = 'osqueryResultsTable'; export const RESULTS_TABLE_BUTTON = 'dataGridFullScreenButton'; export const RESULTS_TABLE_CELL_WRRAPER = 'EuiDataGridHeaderCellWrapper'; -export const getSavedQueriesDropdown = () => - cy.react('EuiComboBox', { - props: { placeholder: 'Search for a query to run, or write a new query below' }, - }); export const getIdFormField = () => cy.get('input[name="id"]'); diff --git a/x-pack/plugins/osquery/cypress/screens/packs.ts b/x-pack/plugins/osquery/cypress/screens/packs.ts index de3a9571625ab..433871d4840a3 100644 --- a/x-pack/plugins/osquery/cypress/screens/packs.ts +++ b/x-pack/plugins/osquery/cypress/screens/packs.ts @@ -11,6 +11,7 @@ export const SAVE_PACK_BUTTON = 'save-pack-button'; export const UPDATE_PACK_BUTTON = 'update-pack-button'; export const ADD_QUERY_BUTTON = 'add-query-button'; +export const UPDATE_QUERY_BUTTON = 'update-query-button'; export const FLYOUT_SAVED_QUERY_SAVE_BUTTON = 'query-flyout-save-button'; export const FLYOUT_SAVED_QUERY_CANCEL_BUTTON = 'query-flyout-cancel-button'; @@ -22,6 +23,8 @@ export const customActionRunSavedQuerySelector = (savedQueryName: string) => `[aria-label="Run ${savedQueryName}"]`; export const formFieldInputSelector = (fieldName: string) => `input[name="${fieldName}"]`; +export const activeStateSwitchComponentSelector = (packName: string) => + `[aria-label="${packName}"]`; export const POLICY_SELECT_COMBOBOX = 'policyIdsComboBox'; export const SAVED_QUERY_DROPDOWN_SELECT = 'savedQuerySelect'; diff --git a/x-pack/plugins/osquery/cypress/serverless_cypress.config.ts b/x-pack/plugins/osquery/cypress/serverless_cypress.config.ts index fac363698170a..c116fb3830e39 100644 --- a/x-pack/plugins/osquery/cypress/serverless_cypress.config.ts +++ b/x-pack/plugins/osquery/cypress/serverless_cypress.config.ts @@ -29,9 +29,6 @@ export default defineCypressConfig({ viewportWidth: 1680, env: { - 'cypress-react-selector': { - root: '#osquery-app', - }, grepFilterSpecs: true, grepTags: '@serverless --@brokenInServerless', grepOmitFiltered: true, diff --git a/x-pack/plugins/osquery/cypress/support/e2e.ts b/x-pack/plugins/osquery/cypress/support/e2e.ts index d5943e655e5ae..52060571b2c0e 100644 --- a/x-pack/plugins/osquery/cypress/support/e2e.ts +++ b/x-pack/plugins/osquery/cypress/support/e2e.ts @@ -36,7 +36,6 @@ import { login } from '@kbn/security-solution-plugin/public/management/cypress/t import type { ServerlessRoleName } from './roles'; -import 'cypress-react-selector'; import { waitUntil } from '../tasks/wait_until'; import { isServerless } from '../tasks/serverless'; diff --git a/x-pack/plugins/osquery/cypress/tasks/live_query.ts b/x-pack/plugins/osquery/cypress/tasks/live_query.ts index 4b1b2d41b2283..6a342246517ac 100644 --- a/x-pack/plugins/osquery/cypress/tasks/live_query.ts +++ b/x-pack/plugins/osquery/cypress/tasks/live_query.ts @@ -57,7 +57,7 @@ export const typeInECSFieldInput = (text: string, index = 0) => cy.getBySel('ECS-field-input').eq(index).type(text); export const typeInOsqueryFieldInput = (text: string, index = 0) => cy - .react('OsqueryColumnFieldComponent') + .getBySel('osqueryColumnValueSelect') .eq(index) .within(() => { cy.getBySel('comboBoxInput').type(text); @@ -74,10 +74,6 @@ export const getOsqueryFieldTypes = (value: 'Osquery value' | 'Static value', in } }; -export const findFormFieldByRowsLabelAndType = (label: string, text: string) => { - cy.react('EuiFormRow', { props: { label } }).type(`${text}{downArrow}{enter}`); -}; - export const deleteAndConfirm = (type: string) => { cy.get('span').contains(`Delete ${type}`).click(); cy.contains(`Are you sure you want to delete this ${type}?`); @@ -88,10 +84,6 @@ export const deleteAndConfirm = (type: string) => { .contains(type); }; -export const findAndClickButton = (text: string) => { - cy.react('EuiButton').contains(text).click(); -}; - export const toggleRuleOffAndOn = (ruleName: string) => { cy.visit('/app/security/rules'); cy.wait(2000); @@ -120,8 +112,8 @@ export const addToCase = (caseId: string) => { }; export const addLiveQueryToCase = (actionId: string, caseId: string) => { - cy.react('ActionsTableComponent').within(() => { - cy.getBySel(`row-${actionId}`).react('ActionTableResultsButton').click(); + cy.getBySel(`row-${actionId}`).within(() => { + cy.get('[aria-label="Details"]').click(); }); cy.contains('Live query details'); addToCase(caseId); diff --git a/x-pack/plugins/osquery/cypress/tasks/navigation.ts b/x-pack/plugins/osquery/cypress/tasks/navigation.ts index 7a4b9df573370..73179e8acaa82 100644 --- a/x-pack/plugins/osquery/cypress/tasks/navigation.ts +++ b/x-pack/plugins/osquery/cypress/tasks/navigation.ts @@ -21,28 +21,6 @@ export const navigateTo = (page: string, opts?: Partial) = // There's a security warning toast that seemingly makes ui elements in the bottom right unavailable, so we close it closeToastIfVisible(); - waitForReact(); -}; - -// We're moving away from using react-cypress-selector, I'll be adjusting this on file by file approach -export const navigateToWithoutWaitForReact = ( - page: string, - opts?: Partial -) => { - cy.visit(page, opts); - cy.contains('Loading Elastic').should('exist'); - cy.contains('Loading Elastic').should('not.exist'); - - // There's a security warning toast that seemingly makes ui elements in the bottom right unavailable, so we close it - closeToastIfVisible(); -}; - -export const waitForReact = () => { - cy.waitForReact( - 10000, - Cypress.env('cypress-react-selector')?.root, - '../../../node_modules/resq/dist/index.js' - ); }; export const openNavigationFlyout = () => { diff --git a/x-pack/plugins/osquery/cypress/tasks/response_actions.ts b/x-pack/plugins/osquery/cypress/tasks/response_actions.ts index bfdb437540f07..3f59ebca9f560 100644 --- a/x-pack/plugins/osquery/cypress/tasks/response_actions.ts +++ b/x-pack/plugins/osquery/cypress/tasks/response_actions.ts @@ -40,6 +40,7 @@ export const checkOsqueryResponseActionsPermissions = (enabled: boolean) => { it(`response actions should ${enabled ? 'be available ' : 'not be available'}`, () => { cy.visit('/app/security/rules'); clickRuleName(ruleName); + cy.getBySel('globalLoadingIndicator').should('not.exist'); cy.getBySel('editRuleSettingsLink').click(); cy.getBySel('globalLoadingIndicator').should('not.exist'); closeDateTabIfVisible(); diff --git a/x-pack/plugins/osquery/cypress/tasks/saved_queries.ts b/x-pack/plugins/osquery/cypress/tasks/saved_queries.ts index 1c936da56b3e3..b01effba17e65 100644 --- a/x-pack/plugins/osquery/cypress/tasks/saved_queries.ts +++ b/x-pack/plugins/osquery/cypress/tasks/saved_queries.ts @@ -15,7 +15,7 @@ import { selectAllAgents, submitQuery, } from './live_query'; -import { navigateToWithoutWaitForReact } from './navigation'; +import { navigateTo } from './navigation'; export const getSavedQueriesComplexTest = () => describe('Saved queries Complex Test', () => { @@ -88,7 +88,7 @@ export const getSavedQueriesComplexTest = () => closeToastIfVisible(); // play saved query - navigateToWithoutWaitForReact('/app/osquery/saved_queries'); + navigateTo('/app/osquery/saved_queries'); cy.contains(savedQueryId); cy.get(`[aria-label="Run ${savedQueryId}"]`).click(); selectAllAgents(); @@ -120,7 +120,7 @@ export const getSavedQueriesComplexTest = () => // Save edited cy.getBySel('euiFlyoutCloseButton').click(); - cy.getBySel('savedQueryFormUpdateButton').click(); + cy.getBySel('update-query-button').click(); cy.contains(`${savedQueryDescription} Edited`); // delete saved query diff --git a/x-pack/plugins/osquery/cypress/tsconfig.json b/x-pack/plugins/osquery/cypress/tsconfig.json index 4142a60edc776..2670568ae91c5 100644 --- a/x-pack/plugins/osquery/cypress/tsconfig.json +++ b/x-pack/plugins/osquery/cypress/tsconfig.json @@ -15,7 +15,6 @@ "types": [ "cypress", "node", - "cypress-react-selector" ], "resolveJsonModule": true, }, diff --git a/x-pack/plugins/osquery/public/packs/packs_table.tsx b/x-pack/plugins/osquery/public/packs/packs_table.tsx index c522818c4aad3..3574d45891755 100644 --- a/x-pack/plugins/osquery/public/packs/packs_table.tsx +++ b/x-pack/plugins/osquery/public/packs/packs_table.tsx @@ -132,7 +132,12 @@ const PacksTableComponent = () => { return ( - + ); }, diff --git a/x-pack/plugins/osquery/public/packs/queries/platform_checkbox_group_field.tsx b/x-pack/plugins/osquery/public/packs/queries/platform_checkbox_group_field.tsx index 62e0a7bdaef6d..53c8baf688f18 100644 --- a/x-pack/plugins/osquery/public/packs/queries/platform_checkbox_group_field.tsx +++ b/x-pack/plugins/osquery/public/packs/queries/platform_checkbox_group_field.tsx @@ -137,7 +137,7 @@ export const PlatformCheckBoxGroupField = (props: Props) => { idToSelectedMap={checkboxIdToSelectedMap} options={options} onChange={handleChange} - data-test-subj="input" + data-test-subj="osquery-platform-checkbox-group" {...euiFieldProps} /> diff --git a/x-pack/plugins/osquery/public/routes/saved_queries/edit/form.tsx b/x-pack/plugins/osquery/public/routes/saved_queries/edit/form.tsx index 2449ed30ea0d0..da587da375456 100644 --- a/x-pack/plugins/osquery/public/routes/saved_queries/edit/form.tsx +++ b/x-pack/plugins/osquery/public/routes/saved_queries/edit/form.tsx @@ -76,7 +76,7 @@ const EditSavedQueryFormComponent: React.FC = ({ - + Date: Mon, 6 Nov 2023 12:56:57 -0800 Subject: [PATCH 08/12] [DOCS] Elasticsearch query rule group by multiple terms (#170675) --- .../alerting/rule-types/es-query.asciidoc | 5 +++- docs/user/whats-new.asciidoc | 2 +- .../alerting/docs/openapi/bundled.json | 26 +++++++++++++++++-- .../alerting/docs/openapi/bundled.yaml | 16 ++++++++++-- .../openapi/components/schemas/termfield.yaml | 9 +++++-- .../s@{spaceid}@api@alerting@rule_types.yaml | 7 +++++ 6 files changed, 57 insertions(+), 8 deletions(-) diff --git a/docs/user/alerting/rule-types/es-query.asciidoc b/docs/user/alerting/rule-types/es-query.asciidoc index 5a86606a22035..a95403f2a0329 100644 --- a/docs/user/alerting/rule-types/es-query.asciidoc +++ b/docs/user/alerting/rule-types/es-query.asciidoc @@ -61,7 +61,10 @@ FROM kibana_sample_data_logs + -- When::: Specify how to calculate the value that is compared to the threshold. The value is calculated by aggregating a numeric field within the time window. The aggregation options are: `count`, `average`, `sum`, `min`, and `max`. When using `count` the document count is used and an aggregation field is not necessary. -Over or Grouped Over::: Specify whether the aggregation is applied over all documents or split into groups using a grouping field. If grouping is used, an alert will be created for each group when it meets the condition. To limit the number of alerts on high cardinality fields, you must specify the number of groups to check against the threshold. Only the top groups are checked. +Over or Grouped Over::: Specify whether the aggregation is applied over all documents or split into groups using up to four grouping fields. +If you choose to use grouping, it's a {ref}/search-aggregations-bucket-terms-aggregation.html[terms] or {ref}/search-aggregations-bucket-multi-terms-aggregation.html[multi terms aggregation]; an alert will be created for each unique set of values when it meets the condition. +To limit the number of alerts on high cardinality fields, you must specify the number of groups to check against the threshold. +Only the top groups are checked. Threshold::: Defines a threshold value and a comparison operator (`is above`, `is above or equals`, `is below`, `is below or equals`, or `is between`). The value calculated by the aggregation is compared to this threshold. diff --git a/docs/user/whats-new.asciidoc b/docs/user/whats-new.asciidoc index 4dacf5ce2842d..5aca50de79675 100644 --- a/docs/user/whats-new.asciidoc +++ b/docs/user/whats-new.asciidoc @@ -180,7 +180,7 @@ image::images/custom-field.gif[An example of creating a custom case field in {ki [discrete] ==== Supporting multi levels of term aggregations in {es} rule type -The existing {es} alerting rule (KQL-based) is now supported by multiple selection when grouping by alert fields, which allows you to define multiple layers of term aggregations. +The existing {es} alerting rule is now supported by multiple selection when grouping by alert fields, which allows you to define multiple layers of term aggregations. [role="screenshot"] image::images/term-aggs.png[An example of creating multiple layers of term aggregations] diff --git a/x-pack/plugins/alerting/docs/openapi/bundled.json b/x-pack/plugins/alerting/docs/openapi/bundled.json index 7ed5304f54200..6e09c658ae407 100644 --- a/x-pack/plugins/alerting/docs/openapi/bundled.json +++ b/x-pack/plugins/alerting/docs/openapi/bundled.json @@ -957,6 +957,17 @@ } } }, + "slo": { + "type": "object", + "properties": { + "all": { + "type": "boolean" + }, + "read": { + "type": "boolean" + } + } + }, "stackAlerts": { "type": "object", "properties": { @@ -3217,8 +3228,19 @@ "type": "integer" }, "termfield": { - "description": "This property is required when `groupBy` is `top`. The name of the field that is used for grouping the aggregation.\n", - "type": "string" + "description": "The names of up to four fields that are used for grouping the aggregation. This property is required when `groupBy` is `top`.\n", + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + }, + "maxItems": 4 + } + ] }, "threshold": { "description": "The threshold value that is used with the `thresholdComparator`. If the `thresholdComparator` is `between` or `notBetween`, you must specify the boundary values.\n", diff --git a/x-pack/plugins/alerting/docs/openapi/bundled.yaml b/x-pack/plugins/alerting/docs/openapi/bundled.yaml index f733903025e0a..424326e12c8f6 100644 --- a/x-pack/plugins/alerting/docs/openapi/bundled.yaml +++ b/x-pack/plugins/alerting/docs/openapi/bundled.yaml @@ -612,6 +612,13 @@ paths: type: boolean read: type: boolean + slo: + type: object + properties: + all: + type: boolean + read: + type: boolean stackAlerts: type: object properties: @@ -2180,8 +2187,13 @@ components: type: integer termfield: description: | - This property is required when `groupBy` is `top`. The name of the field that is used for grouping the aggregation. - type: string + The names of up to four fields that are used for grouping the aggregation. This property is required when `groupBy` is `top`. + oneOf: + - type: string + - type: array + items: + type: string + maxItems: 4 threshold: description: | The threshold value that is used with the `thresholdComparator`. If the `thresholdComparator` is `between` or `notBetween`, you must specify the boundary values. diff --git a/x-pack/plugins/alerting/docs/openapi/components/schemas/termfield.yaml b/x-pack/plugins/alerting/docs/openapi/components/schemas/termfield.yaml index 95fc7b6335612..f9d0a01136da2 100644 --- a/x-pack/plugins/alerting/docs/openapi/components/schemas/termfield.yaml +++ b/x-pack/plugins/alerting/docs/openapi/components/schemas/termfield.yaml @@ -1,4 +1,9 @@ description: > + The names of up to four fields that are used for grouping the aggregation. This property is required when `groupBy` is `top`. - The name of the field that is used for grouping the aggregation. -type: string +oneOf: + - type: string + - type: array + items: + type: string + maxItems: 4 \ No newline at end of file diff --git a/x-pack/plugins/alerting/docs/openapi/paths/s@{spaceid}@api@alerting@rule_types.yaml b/x-pack/plugins/alerting/docs/openapi/paths/s@{spaceid}@api@alerting@rule_types.yaml index c40065d82e669..1dba3e085e2b7 100644 --- a/x-pack/plugins/alerting/docs/openapi/paths/s@{spaceid}@api@alerting@rule_types.yaml +++ b/x-pack/plugins/alerting/docs/openapi/paths/s@{spaceid}@api@alerting@rule_types.yaml @@ -136,6 +136,13 @@ get: type: boolean read: type: boolean + slo: + type: object + properties: + all: + type: boolean + read: + type: boolean stackAlerts: type: object properties: From a7728e44c440a116484a9b307e8e97a6fccc9d7a Mon Sep 17 00:00:00 2001 From: Hannah Mudge Date: Mon, 6 Nov 2023 14:15:04 -0700 Subject: [PATCH 09/12] [Dashboard Navigation] Make locator generic (#167928) Closes https://github.com/elastic/kibana/issues/164748 ## Summary Previously, the link embeddable **always** used the Dashboard plugin's locator - this meant that, for portable dashboards (such as those in Security or in APM), navigation through the link embeddable would always send the user to the Dashboard plugin rather than staying in whatever context the portable dashboard was in. This PR fixes that by ensuring that each `DashboardRenderer` consumer can provide their **own** locator as a prop - this means that the Dashboard app can still use its own locator, while the Security/APM plugins (for example) can **also** define a locator so that navigation remains in the security/APM app. ### Security @elastic/security-solution-platform While I did my best to modify each portable dashboard implementation to include this new locator prop, without the full context on how each plugin wants to handle the link embeddable, I've had to make a few assumptions about the behaviour. For example, in security.... - The Security dashboards **not** respond to the query bar in the same way that they do in the Dashboard app - therefore, the link embeddable settings for "Use filters and query from origin dashboard" and "Use date range from origin dashboard" do not make sense in this context. For example, in Security, it is (from my understanding) **expected** that the query bar would always remain constant, regardless of these settings; therefore, these settings are more-or-less ignored. Unfortunately, this opens up a potential confusion for users, especially if the are editing/creating a link embeddable in the security context. - The Security app does not currently use real locators, and my attempts to remedy this were unsuccessful. Therefore, rather than requiring a **true** `Locator` object to be passed in as a prop to the `DashboardRenderer`, I had to instead accept any object that has both a `navigate` and `getRedirectUrl` method defined. It would be **much** cleaner for the Security plugin to define its own locator - that way, the `DashboardRenderer` could instead just accept a locator ID as a prop, and the link embeddable could use that ID fetch the appropriate locator as necessary; however, this is a pretty major refactor, since the Security app handles URLs/navigation in a much different way than any other Kibana app. **Before:** https://github.com/elastic/kibana/assets/8698078/67fdf34f-60e3-47fc-b205-8a6443a0452d **After:** https://github.com/elastic/kibana/assets/8698078/f92f1eb0-1467-4408-8792-f881e355188b ### APM @elastic/apm-ui While I did my best to modify each portable dashboard implementation to include this new locator prop, without the full context on how each plugin wants to handle the link embeddable, I've had to make a few assumptions about the behaviour. For example, in APM.... - Similar to the Security implementation, the APM dashboards **not** respond to the query bar in the same way that they do in the Dashboard app - therefore, the link embeddable settings for "Use filters and query from origin dashboard" and "Use date range from origin dashboard" do not make sense in this context. For example, in APM, it is (from my understanding) **expected** that the query bar would always remain constant, regardless of these settings; therefore, these settings are more-or-less ignored. That being said, because APM does not currently support dashboard editing, I believe this is less of a concern than it is for other implementations. - Because of the unique content management system that APM is using, where Dashboards must be linked in order for them to be viewed, the link embeddable would not work **unless** the user was navigating to a dashboard that was added to the APM Dashboard CM. I've had to change this so that **any** dashboard can be viewed in the APM context - that way, even if someone is trying to load a dashboard that is **not** linked, it will still load. This goes against how APM typically handles its dashboards, so I am open to suggestions if this behaviour is undesirable. It just felt odd to me that I would have to link every single referenced dashboard in the APM context if I wanted the link embeddable to work.... **Before:** https://github.com/elastic/kibana/assets/8698078/59397d42-2289-4721-9d4a-74c3e7a4d871 **After:** https://github.com/elastic/kibana/assets/8698078/87924826-e766-4b50-87c6-132ae936f2fc ### 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 ### For maintainers - [ ] 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) --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../public/dashboard_app/dashboard_app.tsx | 5 + .../locator/get_dashboard_locator_params.ts | 9 +- .../public/dashboard_app/locator/locator.ts | 55 +-------- .../top_nav/share/show_share_modal.test.tsx | 10 +- .../top_nav/share/show_share_modal.tsx | 9 +- .../url/search_sessions_integration.ts | 10 +- .../embeddable/dashboard_container.tsx | 5 +- .../external_api/dashboard_renderer.tsx | 43 ++++--- .../public/dashboard_container/index.ts | 1 + .../public/dashboard_container/types.ts | 47 ++++++++ src/plugins/dashboard/public/index.ts | 9 +- .../public/services/share/share.stub.ts | 1 + .../public/services/share/share_services.ts | 3 +- .../dashboard/public/services/share/types.ts | 1 + .../dashboard_link_component.test.tsx | 76 +++++++------ .../dashboard_link_component.tsx | 25 +++-- .../dashboard_link/dashboard_link_tools.ts | 67 +---------- src/plugins/links/tsconfig.json | 2 - .../actions/save_dashboard_modal.tsx | 17 ++- .../actions/unlink_dashboard.tsx | 15 +++ .../service_dashboards/dashboard_selector.tsx | 50 ++++++--- .../app/service_dashboards/index.tsx | 64 +++++++---- x-pack/plugins/apm/public/locator/helpers.ts | 54 ++++++--- ...embeddable_to_dashboard_drilldown.test.tsx | 4 +- .../embeddable_to_dashboard_drilldown.tsx | 10 +- .../quick_create_job_base.ts | 4 +- .../components/dashboard_renderer.tsx | 105 +++++++++++++----- 27 files changed, 396 insertions(+), 305 deletions(-) diff --git a/src/plugins/dashboard/public/dashboard_app/dashboard_app.tsx b/src/plugins/dashboard/public/dashboard_app/dashboard_app.tsx index 00b68c2abc547..ea1bea52c83de 100644 --- a/src/plugins/dashboard/public/dashboard_app/dashboard_app.tsx +++ b/src/plugins/dashboard/public/dashboard_app/dashboard_app.tsx @@ -31,6 +31,7 @@ import { } from './url/search_sessions_integration'; import { DashboardAPI, DashboardRenderer } from '..'; import { type DashboardEmbedSettings } from './types'; +import { DASHBOARD_APP_LOCATOR } from './locator/locator'; import { pluginServices } from '../services/plugin_services'; import { AwaitingDashboardAPI } from '../dashboard_container'; import { DashboardRedirect } from '../dashboard_container/types'; @@ -82,6 +83,7 @@ export function DashboardApp({ settings: { uiSettings }, data: { search }, customBranding, + share: { url }, } = pluginServices.getServices(); const showPlainSpinner = useObservable(customBranding.hasCustomBranding$, false); const { scopedHistory: getScopedHistory } = useDashboardMountContext(); @@ -188,6 +190,8 @@ export function DashboardApp({ return () => stopWatchingAppStateInUrl(); }, [dashboardAPI, kbnUrlStateStorage, savedDashboardId]); + const locator = useMemo(() => url?.locators.get(DASHBOARD_APP_LOCATOR), [url]); + return ( <> {showNoDataPage && ( @@ -206,6 +210,7 @@ export function DashboardApp({ {getLegacyConflictWarning?.()} , options: DashboardDrilldownOptions -): Partial => { - const params: DashboardAppLocatorParams = {}; +): Partial => { + const params: DashboardLocatorParams = {}; const input = source.getInput(); if (isQuery(input.query) && options.useCurrentFilters) { diff --git a/src/plugins/dashboard/public/dashboard_app/locator/locator.ts b/src/plugins/dashboard/public/dashboard_app/locator/locator.ts index 4b8a93d8ad900..c902bc369e044 100644 --- a/src/plugins/dashboard/public/dashboard_app/locator/locator.ts +++ b/src/plugins/dashboard/public/dashboard_app/locator/locator.ts @@ -11,13 +11,11 @@ import { flow } from 'lodash'; import type { Filter } from '@kbn/es-query'; import { setStateToKbnUrl } from '@kbn/kibana-utils-plugin/public'; -import { SerializableControlGroupInput } from '@kbn/controls-plugin/common'; import type { LocatorDefinition, LocatorPublic } from '@kbn/share-plugin/public'; import type { GlobalQueryStateFromUrl } from '@kbn/data-plugin/public'; -import type { DashboardContainerInput } from '../../../common'; -import { SavedDashboardPanel } from '../../../common/content_management'; import { DASHBOARD_APP_ID, SEARCH_SESSION_ID } from '../../dashboard_constants'; +import { DashboardLocatorParams } from '../..'; /** * Useful for ensuring that we don't pass any non-serializable values to history.push (for example, functions). @@ -35,50 +33,7 @@ export const cleanEmptyKeys = (stateObj: Record) => { export const DASHBOARD_APP_LOCATOR = 'DASHBOARD_APP_LOCATOR'; -export type DashboardAppLocatorParams = Partial< - Omit< - DashboardContainerInput, - 'panels' | 'controlGroupInput' | 'executionContext' | 'isEmbeddedExternally' - > -> & { - /** - * If given, the dashboard saved object with this id will be loaded. If not given, - * a new, unsaved dashboard will be loaded up. - */ - dashboardId?: string; - - /** - * If not given, will use the uiSettings configuration for `storeInSessionStorage`. useHash determines - * whether to hash the data in the url to avoid url length issues. - */ - useHash?: boolean; - - /** - * When `true` filters from saved filters from destination dashboard as merged with applied filters - * When `false` applied filters take precedence and override saved filters - * - * true is default - */ - preserveSavedFilters?: boolean; - - /** - * Search search session ID to restore. - * (Background search) - */ - searchSessionId?: string; - - /** - * List of dashboard panels - */ - panels?: Array; // used SerializableRecord here to force the GridData type to be read as serializable - - /** - * Control group input - */ - controlGroupInput?: SerializableControlGroupInput; -}; - -export type DashboardAppLocator = LocatorPublic; +export type DashboardAppLocator = LocatorPublic; export interface DashboardAppLocatorDependencies { useHashedUrl: boolean; @@ -86,16 +41,16 @@ export interface DashboardAppLocatorDependencies { } export type ForwardedDashboardState = Omit< - DashboardAppLocatorParams, + DashboardLocatorParams, 'dashboardId' | 'preserveSavedFilters' | 'useHash' | 'searchSessionId' >; -export class DashboardAppLocatorDefinition implements LocatorDefinition { +export class DashboardAppLocatorDefinition implements LocatorDefinition { public readonly id = DASHBOARD_APP_LOCATOR; constructor(protected readonly deps: DashboardAppLocatorDependencies) {} - public readonly getLocation = async (params: DashboardAppLocatorParams) => { + public readonly getLocation = async (params: DashboardLocatorParams) => { const { filters, useHash: paramsUseHash, diff --git a/src/plugins/dashboard/public/dashboard_app/top_nav/share/show_share_modal.test.tsx b/src/plugins/dashboard/public/dashboard_app/top_nav/share/show_share_modal.test.tsx index 0af39e9257307..da4cb0b637398 100644 --- a/src/plugins/dashboard/public/dashboard_app/top_nav/share/show_share_modal.test.tsx +++ b/src/plugins/dashboard/public/dashboard_app/top_nav/share/show_share_modal.test.tsx @@ -7,9 +7,9 @@ */ import { Capabilities } from '@kbn/core/public'; +import { DashboardLocatorParams } from '../../../dashboard_container'; import { convertPanelMapToSavedPanels, DashboardContainerInput } from '../../../../common'; -import { DashboardAppLocatorParams } from '../../..'; import { pluginServices } from '../../../services/plugin_services'; import { showPublicUrlSwitch, ShowShareModal, ShowShareModalProps } from './show_share_modal'; @@ -56,7 +56,7 @@ describe('showPublicUrlSwitch', () => { describe('ShowShareModal', () => { const unsavedStateKeys = ['query', 'filters', 'options', 'savedQuery', 'panels'] as Array< - keyof DashboardAppLocatorParams + keyof DashboardLocatorParams >; const toggleShareMenuSpy = jest.spyOn( pluginServices.getServices().share, @@ -83,7 +83,7 @@ describe('ShowShareModal', () => { expect(toggleShareMenuSpy).toHaveBeenCalledTimes(1); const shareLocatorParams = ( toggleShareMenuSpy.mock.calls[0][0].sharingData as { - locatorParams: { params: DashboardAppLocatorParams }; + locatorParams: { params: DashboardLocatorParams }; } ).locatorParams.params; unsavedStateKeys.forEach((key) => { @@ -125,7 +125,7 @@ describe('ShowShareModal', () => { expect(toggleShareMenuSpy).toHaveBeenCalledTimes(1); const shareLocatorParams = ( toggleShareMenuSpy.mock.calls[0][0].sharingData as { - locatorParams: { params: DashboardAppLocatorParams }; + locatorParams: { params: DashboardLocatorParams }; } ).locatorParams.params; const rawDashboardState = { @@ -134,7 +134,7 @@ describe('ShowShareModal', () => { }; unsavedStateKeys.forEach((key) => { expect(shareLocatorParams[key]).toStrictEqual( - (rawDashboardState as unknown as Partial)[key] + (rawDashboardState as unknown as Partial)[key] ); }); }); diff --git a/src/plugins/dashboard/public/dashboard_app/top_nav/share/show_share_modal.tsx b/src/plugins/dashboard/public/dashboard_app/top_nav/share/show_share_modal.tsx index 5147982d66e07..b2d5cd9f4fe98 100644 --- a/src/plugins/dashboard/public/dashboard_app/top_nav/share/show_share_modal.tsx +++ b/src/plugins/dashboard/public/dashboard_app/top_nav/share/show_share_modal.tsx @@ -23,7 +23,8 @@ import { dashboardUrlParams } from '../../dashboard_router'; import { shareModalStrings } from '../../_dashboard_app_strings'; import { pluginServices } from '../../../services/plugin_services'; import { convertPanelMapToSavedPanels } from '../../../../common'; -import { DashboardAppLocatorParams, DASHBOARD_APP_LOCATOR } from '../../locator/locator'; +import { DASHBOARD_APP_LOCATOR } from '../../locator/locator'; +import { DashboardLocatorParams } from '../../../dashboard_container'; const showFilterBarId = 'showFilterBar'; @@ -120,7 +121,7 @@ export function ShowShareModal({ ); }; - let unsavedStateForLocator: DashboardAppLocatorParams = {}; + let unsavedStateForLocator: DashboardLocatorParams = {}; const unsavedDashboardState = dashboardBackup.getState(savedObjectId); if (unsavedDashboardState) { @@ -131,7 +132,7 @@ export function ShowShareModal({ panels: unsavedDashboardState.panels ? (convertPanelMapToSavedPanels( unsavedDashboardState.panels - ) as DashboardAppLocatorParams['panels']) + ) as DashboardLocatorParams['panels']) : undefined, // options @@ -143,7 +144,7 @@ export function ShowShareModal({ }; } - const locatorParams: DashboardAppLocatorParams = { + const locatorParams: DashboardLocatorParams = { dashboardId: savedObjectId, preserveSavedFilters: true, refreshInterval: undefined, // We don't share refresh interval externally diff --git a/src/plugins/dashboard/public/dashboard_app/url/search_sessions_integration.ts b/src/plugins/dashboard/public/dashboard_app/url/search_sessions_integration.ts index c66042030bc1c..91451b225658c 100644 --- a/src/plugins/dashboard/public/dashboard_app/url/search_sessions_integration.ts +++ b/src/plugins/dashboard/public/dashboard_app/url/search_sessions_integration.ts @@ -19,10 +19,10 @@ import type { Query } from '@kbn/es-query'; import { SearchSessionInfoProvider } from '@kbn/data-plugin/public'; import { SEARCH_SESSION_ID } from '../../dashboard_constants'; -import { DashboardContainer } from '../../dashboard_container'; +import { DashboardContainer, DashboardLocatorParams } from '../../dashboard_container'; import { convertPanelMapToSavedPanels } from '../../../common'; import { pluginServices } from '../../services/plugin_services'; -import { DashboardAppLocatorParams, DASHBOARD_APP_LOCATOR } from '../locator/locator'; +import { DASHBOARD_APP_LOCATOR } from '../locator/locator'; export const removeSearchSessionIdFromURL = (kbnUrlStateStorage: IKbnUrlStateStorage) => { kbnUrlStateStorage.kbnUrlControls.updateAsync((nextUrl) => { @@ -46,7 +46,7 @@ export const getSessionURLObservable = (history: History) => export function createSessionRestorationDataProvider( container: DashboardContainer -): SearchSessionInfoProvider { +): SearchSessionInfoProvider { return { getName: async () => container.getTitle(), getLocatorData: async () => ({ @@ -67,7 +67,7 @@ function getLocatorParams({ }: { container: DashboardContainer; shouldRestoreSearchSession: boolean; -}): DashboardAppLocatorParams { +}): DashboardLocatorParams { const { data: { query: { @@ -101,6 +101,6 @@ function getLocatorParams({ : undefined, panels: lastSavedId ? undefined - : (convertPanelMapToSavedPanels(panels) as DashboardAppLocatorParams['panels']), + : (convertPanelMapToSavedPanels(panels) as DashboardLocatorParams['panels']), }; } diff --git a/src/plugins/dashboard/public/dashboard_container/embeddable/dashboard_container.tsx b/src/plugins/dashboard/public/dashboard_container/embeddable/dashboard_container.tsx index 0899fa0ebc97e..ccdebdb6818cf 100644 --- a/src/plugins/dashboard/public/dashboard_container/embeddable/dashboard_container.tsx +++ b/src/plugins/dashboard/public/dashboard_container/embeddable/dashboard_container.tsx @@ -31,6 +31,7 @@ import { reportPerformanceMetricEvent } from '@kbn/ebt-tools'; import { KibanaThemeProvider } from '@kbn/kibana-react-plugin/public'; import type { ControlGroupContainer } from '@kbn/controls-plugin/public'; import type { KibanaExecutionContext, OverlayRef } from '@kbn/core/public'; +import { LocatorPublic } from '@kbn/share-plugin/common'; import { ExitFullScreenButtonKibanaProvider } from '@kbn/shared-ux-button-exit-full-screen'; import { ReduxToolsPackage, ReduxEmbeddableTools } from '@kbn/presentation-util-plugin/public'; @@ -49,13 +50,13 @@ import { DashboardReduxState, DashboardRenderPerformanceStats, } from '../types'; -import { DASHBOARD_CONTAINER_TYPE } from '../..'; import { placePanel } from '../component/panel_placement'; import { pluginServices } from '../../services/plugin_services'; import { initializeDashboard } from './create/create_dashboard'; import { DASHBOARD_APP_ID, DASHBOARD_LOADED_EVENT } from '../../dashboard_constants'; import { DashboardCreationOptions } from './dashboard_container_factory'; import { DashboardAnalyticsService } from '../../services/analytics/types'; +import { DashboardLocatorParams, DASHBOARD_CONTAINER_TYPE } from '../..'; import { DashboardViewport } from '../component/viewport/dashboard_viewport'; import { DashboardPanelState, DashboardContainerInput } from '../../../common'; import { dashboardContainerReducers } from '../state/dashboard_container_reducers'; @@ -107,6 +108,8 @@ export class DashboardContainer extends Container, 'navigate' | 'getRedirectUrl'>; + // cleanup public stopSyncingWithUnifiedSearch?: () => void; private cleanupStateTools: () => void; diff --git a/src/plugins/dashboard/public/dashboard_container/external_api/dashboard_renderer.tsx b/src/plugins/dashboard/public/dashboard_container/external_api/dashboard_renderer.tsx index 8bd064d268015..7537d80fd222f 100644 --- a/src/plugins/dashboard/public/dashboard_container/external_api/dashboard_renderer.tsx +++ b/src/plugins/dashboard/public/dashboard_container/external_api/dashboard_renderer.tsx @@ -8,48 +8,50 @@ import '../_dashboard_container.scss'; +import classNames from 'classnames'; import React, { - useRef, - useMemo, - useState, - useEffect, forwardRef, + useEffect, useImperativeHandle, useLayoutEffect, + useMemo, + useRef, + useState, } from 'react'; -import { v4 as uuidv4 } from 'uuid'; -import classNames from 'classnames'; import useUnmount from 'react-use/lib/useUnmount'; +import { v4 as uuidv4 } from 'uuid'; import { EuiLoadingElastic, EuiLoadingSpinner } from '@elastic/eui'; -import { SavedObjectNotFound } from '@kbn/kibana-utils-plugin/common'; import { ErrorEmbeddable, isErrorEmbeddable } from '@kbn/embeddable-plugin/public'; +import { SavedObjectNotFound } from '@kbn/kibana-utils-plugin/common'; +import { LocatorPublic } from '@kbn/share-plugin/common'; +import { DASHBOARD_CONTAINER_TYPE } from '..'; +import { DashboardContainerInput } from '../../../common'; +import type { DashboardContainer } from '../embeddable/dashboard_container'; import { - DashboardAPI, - AwaitingDashboardAPI, - buildApiFromDashboardContainer, -} from './dashboard_api'; -import { - DashboardCreationOptions, DashboardContainerFactory, DashboardContainerFactoryDefinition, + DashboardCreationOptions, } from '../embeddable/dashboard_container_factory'; -import { DashboardRedirect } from '../types'; -import { DASHBOARD_CONTAINER_TYPE } from '..'; -import { DashboardContainerInput } from '../../../common'; -import type { DashboardContainer } from '../embeddable/dashboard_container'; +import { DashboardLocatorParams, DashboardRedirect } from '../types'; import { Dashboard404Page } from './dashboard_404'; +import { + AwaitingDashboardAPI, + buildApiFromDashboardContainer, + DashboardAPI, +} from './dashboard_api'; export interface DashboardRendererProps { savedObjectId?: string; showPlainSpinner?: boolean; dashboardRedirect?: DashboardRedirect; getCreationOptions?: () => Promise; + locator?: Pick, 'navigate' | 'getRedirectUrl'>; } export const DashboardRenderer = forwardRef( - ({ savedObjectId, getCreationOptions, dashboardRedirect, showPlainSpinner }, ref) => { + ({ savedObjectId, getCreationOptions, dashboardRedirect, showPlainSpinner, locator }, ref) => { const dashboardRoot = useRef(null); const dashboardViewport = useRef(null); const [loading, setLoading] = useState(true); @@ -77,6 +79,11 @@ export const DashboardRenderer = forwardRef uuidv4(), []); + useEffect(() => { + /* In case the locator prop changes, we need to reassign the value in the container */ + if (dashboardContainer) dashboardContainer.locator = locator; + }, [dashboardContainer, locator]); + useEffect(() => { /** * Here we attempt to build a dashboard or navigate to a new dashboard. Clear all error states diff --git a/src/plugins/dashboard/public/dashboard_container/index.ts b/src/plugins/dashboard/public/dashboard_container/index.ts index 471df33c08e37..d09b5014064d9 100644 --- a/src/plugins/dashboard/public/dashboard_container/index.ts +++ b/src/plugins/dashboard/public/dashboard_container/index.ts @@ -21,3 +21,4 @@ export { export { DashboardRenderer } from './external_api/dashboard_renderer'; export type { DashboardAPI, AwaitingDashboardAPI } from './external_api/dashboard_api'; +export type { DashboardLocatorParams } from './types'; diff --git a/src/plugins/dashboard/public/dashboard_container/types.ts b/src/plugins/dashboard/public/dashboard_container/types.ts index b71876b1ea724..26f37d7f7d993 100644 --- a/src/plugins/dashboard/public/dashboard_container/types.ts +++ b/src/plugins/dashboard/public/dashboard_container/types.ts @@ -6,9 +6,13 @@ * Side Public License, v 1. */ +import { SerializableControlGroupInput } from '@kbn/controls-plugin/common'; import type { ContainerOutput } from '@kbn/embeddable-plugin/public'; import type { ReduxEmbeddableState } from '@kbn/presentation-util-plugin/public'; +import { SerializableRecord } from '@kbn/utility-types'; + import type { DashboardContainerInput, DashboardOptions } from '../../common'; +import { SavedDashboardPanel } from '../../common/content_management'; export type DashboardReduxState = ReduxEmbeddableState< DashboardContainerInput, @@ -73,3 +77,46 @@ export interface DashboardSaveOptions { onTitleDuplicate: () => void; isTitleDuplicateConfirmed: boolean; } + +export type DashboardLocatorParams = Partial< + Omit< + DashboardContainerInput, + 'panels' | 'controlGroupInput' | 'executionContext' | 'isEmbeddedExternally' + > +> & { + /** + * If given, the dashboard saved object with this id will be loaded. If not given, + * a new, unsaved dashboard will be loaded up. + */ + dashboardId?: string; + + /** + * If not given, will use the uiSettings configuration for `storeInSessionStorage`. useHash determines + * whether to hash the data in the url to avoid url length issues. + */ + useHash?: boolean; + + /** + * When `true` filters from saved filters from destination dashboard as merged with applied filters + * When `false` applied filters take precedence and override saved filters + * + * true is default + */ + preserveSavedFilters?: boolean; + + /** + * Search search session ID to restore. + * (Background search) + */ + searchSessionId?: string; + + /** + * List of dashboard panels + */ + panels?: Array; // used SerializableRecord here to force the GridData type to be read as serializable + + /** + * Control group input + */ + controlGroupInput?: SerializableControlGroupInput; +}; diff --git a/src/plugins/dashboard/public/index.ts b/src/plugins/dashboard/public/index.ts index 89cc7b1aabed8..03cd4e03d52a5 100644 --- a/src/plugins/dashboard/public/index.ts +++ b/src/plugins/dashboard/public/index.ts @@ -21,17 +21,14 @@ export { DashboardRenderer, DASHBOARD_CONTAINER_TYPE, type DashboardCreationOptions, + type DashboardLocatorParams, } from './dashboard_container'; export type { DashboardSetup, DashboardStart, DashboardFeatureFlagConfig } from './plugin'; export { DashboardListingTable } from './dashboard_listing'; export { DashboardTopNav } from './dashboard_top_nav'; -export { - type DashboardAppLocator, - type DashboardAppLocatorParams, - cleanEmptyKeys, -} from './dashboard_app/locator/locator'; -export { getEmbeddableParams } from './dashboard_app/locator/get_dashboard_locator_params'; +export { type DashboardAppLocator, cleanEmptyKeys } from './dashboard_app/locator/locator'; +export { getDashboardLocatorParamsFromEmbeddable } from './dashboard_app/locator/get_dashboard_locator_params'; export function plugin(initializerContext: PluginInitializerContext) { return new DashboardPlugin(initializerContext); diff --git a/src/plugins/dashboard/public/services/share/share.stub.ts b/src/plugins/dashboard/public/services/share/share.stub.ts index 70c7e374a393e..74a9e143ebb97 100644 --- a/src/plugins/dashboard/public/services/share/share.stub.ts +++ b/src/plugins/dashboard/public/services/share/share.stub.ts @@ -15,6 +15,7 @@ export const shareServiceFactory: ShareServiceFactory = () => { const pluginMock = sharePluginMock.createStartContract(); return { + url: pluginMock.url, toggleShareContextMenu: pluginMock.toggleShareContextMenu, }; }; diff --git a/src/plugins/dashboard/public/services/share/share_services.ts b/src/plugins/dashboard/public/services/share/share_services.ts index b96ba0d8f412e..5349a1d6d32da 100644 --- a/src/plugins/dashboard/public/services/share/share_services.ts +++ b/src/plugins/dashboard/public/services/share/share_services.ts @@ -19,9 +19,10 @@ export const shareServiceFactory: ShareServiceFactory = ({ startPlugins }) => { const { share } = startPlugins; if (!share) return {}; - const { toggleShareContextMenu } = share; + const { toggleShareContextMenu, url } = share; return { + url, toggleShareContextMenu, }; }; diff --git a/src/plugins/dashboard/public/services/share/types.ts b/src/plugins/dashboard/public/services/share/types.ts index 5920b4b3bcbc4..7b1a180c6e5d7 100644 --- a/src/plugins/dashboard/public/services/share/types.ts +++ b/src/plugins/dashboard/public/services/share/types.ts @@ -9,5 +9,6 @@ import { SharePluginStart } from '@kbn/share-plugin/public'; export interface DashboardShareService { + url?: SharePluginStart['url']; toggleShareContextMenu?: SharePluginStart['toggleShareContextMenu']; } diff --git a/src/plugins/links/public/components/dashboard_link/dashboard_link_component.test.tsx b/src/plugins/links/public/components/dashboard_link/dashboard_link_component.test.tsx index cb2479bfb5f51..a63636d40df26 100644 --- a/src/plugins/links/public/components/dashboard_link/dashboard_link_component.test.tsx +++ b/src/plugins/links/public/components/dashboard_link/dashboard_link_component.test.tsx @@ -8,19 +8,30 @@ import React from 'react'; -import userEvent from '@testing-library/user-event'; -import { createEvent, fireEvent, render, screen, waitFor } from '@testing-library/react'; +import { getDashboardLocatorParamsFromEmbeddable } from '@kbn/dashboard-plugin/public'; +import { DashboardContainer } from '@kbn/dashboard-plugin/public/dashboard_container'; import { DEFAULT_DASHBOARD_DRILLDOWN_OPTIONS } from '@kbn/presentation-util-plugin/public'; -import { DashboardLinkStrings } from './dashboard_link_strings'; -import { LinksEmbeddable, LinksContext } from '../../embeddable/links_embeddable'; -import { mockLinksPanel } from '../../../common/mocks'; +import { createEvent, fireEvent, render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + import { LINKS_VERTICAL_LAYOUT } from '../../../common/content_management'; +import { mockLinksPanel } from '../../../common/mocks'; +import { LinksContext, LinksEmbeddable } from '../../embeddable/links_embeddable'; import { DashboardLinkComponent } from './dashboard_link_component'; -import { fetchDashboard, getDashboardHref, getDashboardLocator } from './dashboard_link_tools'; -import { coreServices } from '../../services/kibana_services'; +import { DashboardLinkStrings } from './dashboard_link_strings'; +import { fetchDashboard } from './dashboard_link_tools'; jest.mock('./dashboard_link_tools'); +jest.mock('@kbn/dashboard-plugin/public', () => { + const originalModule = jest.requireActual('@kbn/dashboard-plugin/public'); + return { + __esModule: true, + ...originalModule, + getDashboardLocatorParamsFromEmbeddable: jest.fn(), + }; +}); + describe('Dashboard link component', () => { const mockDashboards = [ { @@ -58,25 +69,25 @@ describe('Dashboard link component', () => { const onRender = jest.fn(); let linksEmbeddable: LinksEmbeddable; + let dashboardContainer: DashboardContainer; beforeEach(async () => { window.open = jest.fn(); (fetchDashboard as jest.Mock).mockResolvedValue(mockDashboards[0]); - (getDashboardLocator as jest.Mock).mockResolvedValue({ - app: 'dashboard', - path: '/dashboardItem/456', - state: {}, - }); - (getDashboardHref as jest.Mock).mockReturnValue('https://my-kibana.com/dashboard/123'); linksEmbeddable = await mockLinksPanel({ dashboardExplicitInput: mockDashboards[1].attributes, }); + dashboardContainer = linksEmbeddable.parent as DashboardContainer; + dashboardContainer.locator = { + getRedirectUrl: jest.fn().mockReturnValue('https://my-kibana.com/dashboard/123'), + navigate: jest.fn(), + }; }); afterEach(() => { jest.clearAllMocks(); }); - test('by default uses navigateToApp to open in same tab', async () => { + test('by default uses navigate to open in same tab', async () => { render( { await waitFor(() => expect(onLoading).toHaveBeenCalledTimes(1)); await waitFor(() => expect(fetchDashboard).toHaveBeenCalledTimes(1)); expect(fetchDashboard).toHaveBeenCalledWith(defaultLinkInfo.destination); - expect(getDashboardLocator).toHaveBeenCalledTimes(1); - expect(getDashboardLocator).toHaveBeenCalledWith({ - link: { - ...defaultLinkInfo, - options: DEFAULT_DASHBOARD_DRILLDOWN_OPTIONS, - }, - linksEmbeddable, - }); await waitFor(() => expect(onRender).toHaveBeenCalledTimes(1)); const link = await screen.findByTestId('dashboardLink--foo'); expect(link).toHaveTextContent('another dashboard'); - await userEvent.click(link); - expect(coreServices.application.navigateToApp).toBeCalledTimes(1); - expect(coreServices.application.navigateToApp).toBeCalledWith('dashboard', { - path: '/dashboardItem/456', - state: {}, + userEvent.click(link); + expect(dashboardContainer.locator?.getRedirectUrl).toBeCalledWith({ + dashboardId: '456', }); + expect(dashboardContainer.locator?.navigate).toBeCalledTimes(1); }); test('modified click does not trigger event.preventDefault', async () => { @@ -150,16 +152,15 @@ describe('Dashboard link component', () => { await waitFor(() => expect(onLoading).toHaveBeenCalledTimes(1)); await waitFor(() => expect(fetchDashboard).toHaveBeenCalledTimes(1)); expect(fetchDashboard).toHaveBeenCalledWith(linkInfo.destination); - expect(getDashboardLocator).toHaveBeenCalledWith({ link: linkInfo, linksEmbeddable }); await waitFor(() => expect(onRender).toHaveBeenCalledTimes(1)); const link = await screen.findByTestId('dashboardLink--foo'); expect(link).toBeInTheDocument(); - await userEvent.click(link); - expect(coreServices.application.navigateToApp).toBeCalledTimes(0); + userEvent.click(link); + expect(dashboardContainer.locator?.navigate).toBeCalledTimes(0); expect(window.open).toHaveBeenCalledWith('https://my-kibana.com/dashboard/123', '_blank'); }); - test('passes linkOptions to getDashboardLocator', async () => { + test('passes linkOptions to getDashboardLocatorParamsFromEmbeddable', async () => { const linkInfo = { ...defaultLinkInfo, options: { @@ -181,7 +182,10 @@ describe('Dashboard link component', () => { ); await waitFor(() => expect(onLoading).toHaveBeenCalledTimes(1)); await waitFor(() => expect(fetchDashboard).toHaveBeenCalledTimes(1)); - expect(getDashboardLocator).toHaveBeenCalledWith({ link: linkInfo, linksEmbeddable }); + expect(getDashboardLocatorParamsFromEmbeddable).toHaveBeenCalledWith( + linksEmbeddable, + linkInfo.options + ); await waitFor(() => expect(onRender).toHaveBeenCalledTimes(1)); }); @@ -229,8 +233,8 @@ describe('Dashboard link component', () => { await waitFor(() => expect(onRender).toHaveBeenCalledTimes(1)); const link = await screen.findByTestId('dashboardLink--bar'); expect(link).toHaveTextContent('current dashboard'); - await userEvent.click(link); - expect(coreServices.application.navigateToApp).toBeCalledTimes(0); + userEvent.click(link); + expect(dashboardContainer.locator?.navigate).toBeCalledTimes(0); expect(window.open).toBeCalledTimes(0); }); @@ -249,7 +253,7 @@ describe('Dashboard link component', () => { await waitFor(() => expect(fetchDashboard).toHaveBeenCalledTimes(1)); await waitFor(() => expect(onRender).toHaveBeenCalledTimes(1)); const link = await screen.findByTestId('dashboardLink--foo'); - await userEvent.hover(link); + userEvent.hover(link); const tooltip = await screen.findByTestId('dashboardLink--foo--tooltip'); expect(tooltip).toHaveTextContent('another dashboard'); // title expect(tooltip).toHaveTextContent('something awesome'); // description @@ -276,7 +280,7 @@ describe('Dashboard link component', () => { await waitFor(() => expect(onRender).toHaveBeenCalledTimes(1)); const link = await screen.findByTestId('dashboardLink--foo'); expect(link).toHaveTextContent(label); - await userEvent.hover(link); + userEvent.hover(link); const tooltip = await screen.findByTestId('dashboardLink--foo--tooltip'); expect(tooltip).toHaveTextContent(label); }); diff --git a/src/plugins/links/public/components/dashboard_link/dashboard_link_component.tsx b/src/plugins/links/public/components/dashboard_link/dashboard_link_component.tsx index 38097b478509a..5ff2bacaf49fe 100644 --- a/src/plugins/links/public/components/dashboard_link/dashboard_link_component.tsx +++ b/src/plugins/links/public/components/dashboard_link/dashboard_link_component.tsx @@ -17,12 +17,15 @@ import { } from '@kbn/presentation-util-plugin/public'; import { EuiButtonEmpty, EuiListGroupItem } from '@elastic/eui'; import { DashboardContainer } from '@kbn/dashboard-plugin/public/dashboard_container'; +import { + DashboardLocatorParams, + getDashboardLocatorParamsFromEmbeddable, +} from '@kbn/dashboard-plugin/public'; import { LINKS_VERTICAL_LAYOUT, LinksLayoutType, Link } from '../../../common/content_management'; -import { coreServices } from '../../services/kibana_services'; import { DashboardLinkStrings } from './dashboard_link_strings'; import { useLinks } from '../../embeddable/links_embeddable'; -import { fetchDashboard, getDashboardHref, getDashboardLocator } from './dashboard_link_tools'; +import { fetchDashboard } from './dashboard_link_tools'; export const DashboardLinkComponent = ({ link, @@ -103,13 +106,15 @@ export const DashboardLinkComponent = ({ ...link.options, } as DashboardDrilldownOptions; - const locator = await getDashboardLocator({ - link: { ...link, options: linkOptions }, - linksEmbeddable, - }); + const params: DashboardLocatorParams = { + dashboardId: link.destination, + ...getDashboardLocatorParamsFromEmbeddable(linksEmbeddable, linkOptions), + }; + + const locator = dashboardContainer.locator; if (!locator) return; - const href = getDashboardHref(locator); + const href = locator.getRedirectUrl(params); return { href, onClick: async (event: React.MouseEvent) => { @@ -127,11 +132,7 @@ export const DashboardLinkComponent = ({ if (linkOptions.openInNewTab) { window.open(href, '_blank'); } else { - const { app, path, state } = locator; - await coreServices.application.navigateToApp(app, { - path, - state, - }); + locator.navigate(params); } }, }; diff --git a/src/plugins/links/public/components/dashboard_link/dashboard_link_tools.ts b/src/plugins/links/public/components/dashboard_link/dashboard_link_tools.ts index eb51758bd9b68..9081b17815320 100644 --- a/src/plugins/links/public/components/dashboard_link/dashboard_link_tools.ts +++ b/src/plugins/links/public/components/dashboard_link/dashboard_link_tools.ts @@ -8,20 +8,8 @@ import { isEmpty, filter } from 'lodash'; -import { - cleanEmptyKeys, - getEmbeddableParams, - DashboardAppLocatorParams, -} from '@kbn/dashboard-plugin/public'; -import { isFilterPinned } from '@kbn/es-query'; -import { KibanaLocation } from '@kbn/share-plugin/public'; -import { setStateToKbnUrl } from '@kbn/kibana-utils-plugin/public'; -import { DashboardDrilldownOptions } from '@kbn/presentation-util-plugin/public'; - import { DashboardItem } from '../../embeddable/types'; -import type { LinksEmbeddable } from '../../embeddable'; -import { Link } from '../../../common/content_management'; -import { coreServices, dashboardServices } from '../../services/kibana_services'; +import { dashboardServices } from '../../services/kibana_services'; /** * ---------------------------------- @@ -96,56 +84,3 @@ export const fetchDashboards = async ({ return simplifiedDashboardList; }; - -/** - * ---------------------------------- - * Navigate from one dashboard to another - * ---------------------------------- - */ - -interface GetDashboardLocatorProps { - link: Link & { options: DashboardDrilldownOptions }; - linksEmbeddable: LinksEmbeddable; -} - -/** - * Fetch the locator to use for dashboard navigation - * @param props `GetDashboardLocatorProps` - * @returns The locator to use for dashboard navigation - */ -export const getDashboardLocator = async ({ link, linksEmbeddable }: GetDashboardLocatorProps) => { - const params: DashboardAppLocatorParams = { - dashboardId: link.destination, - ...getEmbeddableParams(linksEmbeddable, link.options), - }; - - const locator = dashboardServices.locator; // TODO: Make this generic as part of https://github.com/elastic/kibana/issues/164748 - if (locator) { - const location: KibanaLocation = await locator.getLocation(params); - return location; - } -}; - -/** - * Get URL for dashboard app - should only be used when relying on native `href` functionality - * @param locator Locator that should be used to get the URL - * @returns A full URL to the dashboard, with all state included - */ -export const getDashboardHref = ({ - app, - path, - state, -}: KibanaLocation): string => { - return coreServices.application.getUrlForApp(app, { - path: setStateToKbnUrl( - '_a', - cleanEmptyKeys({ - query: state.query, - filters: state.filters?.filter((f) => !isFilterPinned(f)), - }), - { useHash: false, storeInHashQuery: true }, - path - ), - absolute: true, - }); -}; diff --git a/src/plugins/links/tsconfig.json b/src/plugins/links/tsconfig.json index ba9b5b67d058f..e9814f4e107e7 100644 --- a/src/plugins/links/tsconfig.json +++ b/src/plugins/links/tsconfig.json @@ -20,8 +20,6 @@ "@kbn/core-saved-objects-server", "@kbn/saved-objects-plugin", "@kbn/ui-actions-enhanced-plugin", - "@kbn/es-query", - "@kbn/share-plugin", "@kbn/kibana-utils-plugin", "@kbn/utility-types", "@kbn/ui-actions-plugin", diff --git a/x-pack/plugins/apm/public/components/app/service_dashboards/actions/save_dashboard_modal.tsx b/x-pack/plugins/apm/public/components/app/service_dashboards/actions/save_dashboard_modal.tsx index 81dc0ba157a01..3083e41c2dac2 100644 --- a/x-pack/plugins/apm/public/components/app/service_dashboards/actions/save_dashboard_modal.tsx +++ b/x-pack/plugins/apm/public/components/app/service_dashboards/actions/save_dashboard_modal.tsx @@ -6,6 +6,7 @@ */ import React, { useCallback, useState } from 'react'; +import { useHistory } from 'react-router-dom'; import { EuiButton, EuiModal, @@ -29,6 +30,7 @@ import { FETCH_STATUS } from '../../../../hooks/use_fetcher'; import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context'; import { useApmParams } from '../../../../hooks/use_apm_params'; import { SERVICE_NAME } from '../../../../../common/es_fields/apm'; +import { fromQuery, toQuery } from '../../../shared/links/url_helpers'; import { MergedServiceDashboard } from '..'; interface Props { @@ -48,6 +50,7 @@ export function SaveDashboardModal({ core: { notifications }, } = useApmPluginContext(); const { data: allAvailableDashboards, status } = useDashboardFetcher(); + const history = useHistory(); let defaultOption: EuiComboBoxOptionOption | undefined; @@ -87,7 +90,7 @@ export function SaveDashboardModal({ ) ?? false, }) ); - const onSave = useCallback( + const onClickSave = useCallback( async function () { const [newDashboard] = selectedDashboard; try { @@ -110,6 +113,13 @@ export function SaveDashboardModal({ ? getEditSuccessToastLabels(newDashboard.label) : getLinkSuccessToastLabels(newDashboard.label) ); + history.push({ + ...history.location, + search: fromQuery({ + ...toQuery(location.search), + dashboardId: newDashboard.value, + }), + }); reloadCustomDashboards(); } } catch (error) { @@ -136,6 +146,7 @@ export function SaveDashboardModal({ isEditMode, serviceName, currentDashboard, + history, ] ); @@ -167,7 +178,7 @@ export function SaveDashboardModal({ placeholder={i18n.translate( 'xpack.apm.serviceDashboards.selectDashboard.placeholder', { - defaultMessage: 'Select dasbboard', + defaultMessage: 'Select dashboard', } )} singleSelection={{ asPlainText: true }} @@ -222,7 +233,7 @@ export function SaveDashboardModal({ {isEditMode diff --git a/x-pack/plugins/apm/public/components/app/service_dashboards/actions/unlink_dashboard.tsx b/x-pack/plugins/apm/public/components/app/service_dashboards/actions/unlink_dashboard.tsx index b0dbda84bb6cf..c43d3d289b767 100644 --- a/x-pack/plugins/apm/public/components/app/service_dashboards/actions/unlink_dashboard.tsx +++ b/x-pack/plugins/apm/public/components/app/service_dashboards/actions/unlink_dashboard.tsx @@ -7,21 +7,26 @@ import { EuiButtonEmpty, EuiConfirmModal } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { useCallback, useState } from 'react'; +import { useHistory } from 'react-router-dom'; import { MergedServiceDashboard } from '..'; +import { fromQuery, toQuery } from '../../../shared/links/url_helpers'; import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context'; import { callApmApi } from '../../../../services/rest/create_call_apm_api'; export function UnlinkDashboard({ currentDashboard, + defaultDashboard, onRefresh, }: { currentDashboard: MergedServiceDashboard; + defaultDashboard: MergedServiceDashboard; onRefresh: () => void; }) { const [isModalVisible, setIsModalVisible] = useState(false); const { core: { notifications }, } = useApmPluginContext(); + const history = useHistory(); const onConfirm = useCallback( async function () { @@ -31,6 +36,14 @@ export function UnlinkDashboard({ signal: null, }); + history.push({ + ...history.location, + search: fromQuery({ + ...toQuery(location.search), + dashboardId: defaultDashboard.dashboardSavedObjectId, + }), + }); + notifications.toasts.addSuccess({ title: i18n.translate( 'xpack.apm.serviceDashboards.unlinkSuccess.toast.title', @@ -63,6 +76,8 @@ export function UnlinkDashboard({ setIsModalVisible, onRefresh, isModalVisible, + history, + defaultDashboard, ] ); return ( diff --git a/x-pack/plugins/apm/public/components/app/service_dashboards/dashboard_selector.tsx b/x-pack/plugins/apm/public/components/app/service_dashboards/dashboard_selector.tsx index 115b97ad41cc8..7dbcbe714a428 100644 --- a/x-pack/plugins/apm/public/components/app/service_dashboards/dashboard_selector.tsx +++ b/x-pack/plugins/apm/public/components/app/service_dashboards/dashboard_selector.tsx @@ -5,8 +5,9 @@ * 2.0. */ -import React, { useEffect } from 'react'; +import React, { useEffect, useState } from 'react'; import { useHistory } from 'react-router-dom'; +import useMount from 'react-use/lib/useMount'; import { EuiComboBox } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { MergedServiceDashboard } from '.'; @@ -14,30 +15,43 @@ import { fromQuery, toQuery } from '../../shared/links/url_helpers'; interface Props { serviceDashboards: MergedServiceDashboard[]; - currentDashboard?: MergedServiceDashboard; - handleOnChange: (selectedId?: string) => void; + currentDashboardId?: string; + setCurrentDashboard: (newDashboard: MergedServiceDashboard) => void; } export function DashboardSelector({ serviceDashboards, - currentDashboard, - handleOnChange, + currentDashboardId, + setCurrentDashboard, }: Props) { const history = useHistory(); - useEffect( - () => + const [selectedDashboard, setSelectedDashboard] = + useState(); + + useMount(() => { + if (!currentDashboardId) { history.push({ ...history.location, search: fromQuery({ ...toQuery(location.search), - dashboardId: currentDashboard?.id, + dashboardId: serviceDashboards[0].dashboardSavedObjectId, }), - }), - // It should only update when loaded - // eslint-disable-next-line react-hooks/exhaustive-deps - [] - ); + }); + } + }); + + useEffect(() => { + const preselectedDashboard = serviceDashboards.find( + ({ dashboardSavedObjectId }) => + dashboardSavedObjectId === currentDashboardId + ); + // preselect dashboard + if (preselectedDashboard) { + setSelectedDashboard(preselectedDashboard); + setCurrentDashboard(preselectedDashboard); + } + }, [serviceDashboards, currentDashboardId, setCurrentDashboard]); function onChange(newDashboardId?: string) { history.push({ @@ -47,8 +61,8 @@ export function DashboardSelector({ dashboardId: newDashboardId, }), }); - handleOnChange(newDashboardId); } + return ( (); const { data: allAvailableDashboards } = useDashboardFetcher(); const { start, end } = useTimeRange({ rangeFrom, rangeTo }); - const { dataView } = useApmDataView(); + const { share } = useApmPluginContext(); const { data, status, refetch } = useFetcher( (callApmApi) => { @@ -95,15 +100,7 @@ export function ServiceDashboards() { ); setServiceDashboards(filteredServiceDashbords); - - const preselectedDashboard = - filteredServiceDashbords.find( - ({ dashboardSavedObjectId }) => dashboardSavedObjectId === dashboardId - ) ?? filteredServiceDashbords[0]; - - // preselect dashboard - setCurrentDashboard(preselectedDashboard); - }, [allAvailableDashboards, data?.serviceDashboards, dashboardId]); + }, [allAvailableDashboards, data?.serviceDashboards]); const getCreationOptions = useCallback((): Promise => { @@ -138,13 +135,34 @@ export function ServiceDashboards() { rangeTo, ]); - const handleOnChange = (selectedId?: string) => { - setCurrentDashboard( - serviceDashboards?.find( - ({ dashboardSavedObjectId }) => dashboardSavedObjectId === selectedId - ) - ); - }; + const getLocatorParams = useCallback( + (params) => { + return { + serviceName, + dashboardId: params.dashboardId, + query: { + environment, + kuery, + rangeFrom, + rangeTo, + }, + }; + }, + [serviceName, environment, kuery, rangeFrom, rangeTo] + ); + + const locator = useMemo(() => { + const baseLocator = share.url.locators.get(APM_APP_LOCATOR_ID); + if (!baseLocator) return; + + return { + ...baseLocator, + getRedirectUrl: (params: SerializableRecord) => + baseLocator.getRedirectUrl(getLocatorParams(params)), + navigate: (params: SerializableRecord) => + baseLocator.navigate(getLocatorParams(params)), + }; + }, [share, getLocatorParams]); return ( @@ -177,9 +195,9 @@ export function ServiceDashboards() { @@ -199,6 +217,7 @@ export function ServiceDashboards() { />, , ]} @@ -208,9 +227,10 @@ export function ServiceDashboards() { - {currentDashboard && ( + {dashboardId && ( diff --git a/x-pack/plugins/apm/public/locator/helpers.ts b/x-pack/plugins/apm/public/locator/helpers.ts index 40fe4dd682502..8cc727dfa11e8 100644 --- a/x-pack/plugins/apm/public/locator/helpers.ts +++ b/x-pack/plugins/apm/public/locator/helpers.ts @@ -13,10 +13,16 @@ import type { TimePickerTimeDefaults } from '../components/shared/date_picker/ty export const APMLocatorPayloadValidator = t.union([ t.type({ serviceName: t.undefined }), + t.intersection([ + t.type({ serviceName: t.string }), + t.type({ dashboardId: t.string }), + t.type({ query: environmentRt }), + ]), t.intersection([ t.type({ serviceName: t.string, }), + t.partial({ dashboardId: t.undefined }), t.partial({ serviceOverviewTab: t.keyof({ traces: null, @@ -65,24 +71,40 @@ export function getPathForServiceDetail( }); } - const mapObj = { - logs: '/services/{serviceName}/logs', - metrics: '/services/{serviceName}/metrics', - traces: '/services/{serviceName}/transactions', - errors: '/services/{serviceName}/errors', - default: '/services/{serviceName}/overview', - } as const; - const apmPath = mapObj[payload.serviceOverviewTab || 'default']; + let path; + if (payload.dashboardId !== undefined) { + const apmPath = '/services/{serviceName}/dashboards'; + path = apmRouter.link(apmPath, { + path: { + serviceName: payload.serviceName, + }, + query: { + ...defaultQueryParams, + ...payload.query, + dashboardId: payload.dashboardId, + }, + }); + return path; + } else { + const mapObj = { + logs: '/services/{serviceName}/logs', + metrics: '/services/{serviceName}/metrics', + traces: '/services/{serviceName}/transactions', + errors: '/services/{serviceName}/errors', + default: '/services/{serviceName}/overview', + } as const; + const apmPath = mapObj[payload.serviceOverviewTab || 'default']; - const query = { - ...defaultQueryParams, - ...payload.query, - }; + const query = { + ...defaultQueryParams, + ...payload.query, + }; - const path = apmRouter.link(apmPath, { - path: { serviceName: payload.serviceName }, - query, - }); + path = apmRouter.link(apmPath, { + path: { serviceName: payload.serviceName }, + query, + }); + } return path; } diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/embeddable_to_dashboard_drilldown/embeddable_to_dashboard_drilldown.test.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/embeddable_to_dashboard_drilldown/embeddable_to_dashboard_drilldown.test.tsx index 27aad87222945..e3bf4a4d468c8 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/embeddable_to_dashboard_drilldown/embeddable_to_dashboard_drilldown.test.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/embeddable_to_dashboard_drilldown/embeddable_to_dashboard_drilldown.test.tsx @@ -10,7 +10,7 @@ import { EmbeddableToDashboardDrilldown } from './embeddable_to_dashboard_drilld import { AbstractDashboardDrilldownConfig as Config } from '../abstract_dashboard_drilldown'; import { savedObjectsServiceMock } from '@kbn/core/public/mocks'; import { ApplyGlobalFilterActionContext } from '@kbn/unified-search-plugin/public'; -import { DashboardAppLocatorParams } from '@kbn/dashboard-plugin/public'; +import { DashboardLocatorParams } from '@kbn/dashboard-plugin/public'; import { StartDependencies } from '../../../plugin'; import { StartServicesGetter } from '@kbn/kibana-utils-plugin/public/core'; import { EnhancedEmbeddableContext } from '@kbn/embeddable-enhanced-plugin/public'; @@ -95,7 +95,7 @@ describe('.execute() & getHref', () => { uiActionsEnhanced: {}, dashboard: { locator: { - getLocation: async (params: DashboardAppLocatorParams) => { + getLocation: async (params: DashboardLocatorParams) => { return await definition.getLocation(params); }, }, diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/embeddable_to_dashboard_drilldown/embeddable_to_dashboard_drilldown.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/embeddable_to_dashboard_drilldown/embeddable_to_dashboard_drilldown.tsx index 9a984af52d21c..0838297ee72e3 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/embeddable_to_dashboard_drilldown/embeddable_to_dashboard_drilldown.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/embeddable_to_dashboard_drilldown/embeddable_to_dashboard_drilldown.tsx @@ -8,8 +8,8 @@ import { extractTimeRange, isFilterPinned } from '@kbn/es-query'; import type { KibanaLocation } from '@kbn/share-plugin/public'; import { cleanEmptyKeys, - DashboardAppLocatorParams, - getEmbeddableParams, + DashboardLocatorParams, + getDashboardLocatorParamsFromEmbeddable, } from '@kbn/dashboard-plugin/public'; import { setStateToKbnUrl } from '@kbn/kibana-utils-plugin/public'; import { APPLY_FILTER_TRIGGER } from '@kbn/data-plugin/public'; @@ -44,12 +44,12 @@ export class EmbeddableToDashboardDrilldown extends AbstractDashboardDrilldown { - let params: DashboardAppLocatorParams = { dashboardId: config.dashboardId }; + let params: DashboardLocatorParams = { dashboardId: config.dashboardId }; if (context.embeddable) { params = { ...params, - ...getEmbeddableParams(context.embeddable, config), + ...getDashboardLocatorParamsFromEmbeddable(context.embeddable, config), }; } @@ -75,7 +75,7 @@ export class EmbeddableToDashboardDrilldown extends AbstractDashboardDrilldown) { + private useUrlForState(location: KibanaLocation) { const state = location.state; location.path = setStateToKbnUrl( '_a', diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/job_from_dashboard/quick_create_job_base.ts b/x-pack/plugins/ml/public/application/jobs/new_job/job_from_dashboard/quick_create_job_base.ts index 3f4e91e925ee1..4b4ce72992775 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/job_from_dashboard/quick_create_job_base.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/job_from_dashboard/quick_create_job_base.ts @@ -11,7 +11,7 @@ import type { IUiSettingsClient } from '@kbn/core/public'; import type { TimefilterContract } from '@kbn/data-plugin/public'; import { firstValueFrom } from 'rxjs'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import type { DashboardAppLocatorParams, DashboardStart } from '@kbn/dashboard-plugin/public'; +import type { DashboardLocatorParams, DashboardStart } from '@kbn/dashboard-plugin/public'; import type { Filter, Query, DataViewBase } from '@kbn/es-query'; import { FilterStateStore } from '@kbn/es-query'; import type { Embeddable } from '@kbn/lens-plugin/public'; @@ -245,7 +245,7 @@ export class QuickJobCreatorBase { return null; } - const params: DashboardAppLocatorParams = { + const params: DashboardLocatorParams = { dashboardId: foundDashboard.id, timeRange: { from: '$earliest$', diff --git a/x-pack/plugins/security_solution/public/dashboards/components/dashboard_renderer.tsx b/x-pack/plugins/security_solution/public/dashboards/components/dashboard_renderer.tsx index 73538439de568..9fc53f1a4175b 100644 --- a/x-pack/plugins/security_solution/public/dashboards/components/dashboard_renderer.tsx +++ b/x-pack/plugins/security_solution/public/dashboards/components/dashboard_renderer.tsx @@ -4,19 +4,26 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React, { useCallback, useEffect, useState } from 'react'; + +import React, { useCallback, useEffect, useMemo, useState } from 'react'; + +import type { DashboardContainerInput } from '@kbn/dashboard-plugin/common'; import type { DashboardAPI, DashboardCreationOptions } from '@kbn/dashboard-plugin/public'; import { DashboardRenderer as DashboardContainerRenderer } from '@kbn/dashboard-plugin/public'; import { ViewMode } from '@kbn/embeddable-plugin/public'; import type { Filter, Query } from '@kbn/es-query'; import { useDispatch } from 'react-redux'; -import { InputsModelId } from '../../common/store/inputs/constants'; -import { inputsActions } from '../../common/store/inputs'; -import { useKibana } from '../../common/lib/kibana'; +import { BehaviorSubject } from 'rxjs'; import { APP_UI_ID } from '../../../common'; +import { DASHBOARDS_PATH, SecurityPageName } from '../../../common/constants'; +import { useGetSecuritySolutionUrl } from '../../common/components/link_to'; +import { useKibana, useNavigateTo } from '../../common/lib/kibana'; +import { inputsActions } from '../../common/store/inputs'; +import { InputsModelId } from '../../common/store/inputs/constants'; import { useSecurityTags } from '../context/dashboard_context'; -import { DASHBOARDS_PATH } from '../../../common/constants'; + +const initialInput = new BehaviorSubject>({}); const DashboardRendererComponent = ({ canReadDashboard, @@ -50,32 +57,66 @@ const DashboardRendererComponent = ({ const dispatch = useDispatch(); const securityTags = useSecurityTags(); + const { navigateTo } = useNavigateTo(); + const getSecuritySolutionUrl = useGetSecuritySolutionUrl(); const firstSecurityTagId = securityTags?.[0]?.id; const isCreateDashboard = !savedObjectId; - const getCreationOptions: () => Promise = useCallback( - () => - Promise.resolve({ - useSessionStorageIntegration: true, - useControlGroupIntegration: true, - getInitialInput: () => ({ - timeRange, - viewMode, - query, - filters, - }), - getIncomingEmbeddable: () => - embeddable.getStateTransfer().getIncomingEmbeddablePackage(APP_UI_ID, true), - getEmbeddableAppContext: (dashboardId?: string) => ({ - getCurrentPath: () => - dashboardId ? `${DASHBOARDS_PATH}/${dashboardId}/edit` : `${DASHBOARDS_PATH}/create`, - currentAppId: APP_UI_ID, - }), - }), - [embeddable, filters, query, timeRange, viewMode] + const getSecuritySolutionDashboardUrl = useCallback( + ({ dashboardId }) => { + return getSecuritySolutionUrl({ + deepLinkId: SecurityPageName.dashboards, + path: dashboardId, + }); + }, + [getSecuritySolutionUrl] ); + const goToDashboard = useCallback( + /** + * Note: Due to the query bar being separate from the portable dashboard, the "Use filters and query from origin + * dashboard" and "Use date range from origin dashboard" Link embeddable settings do not make sense in this context. + * Regardless of these settings, navigation to a different dashboard will **always** keep the query state the same. + * I have chosen to keep this consistent **even when** the dashboard is opened in a new tab. + * + * If we want portable dashboard to interact with the query bar in the same way it does in the dashboard app so these + * settings apply, we would need to refactor this portable dashboard. We might also want to make the security app use + * locators in that refactor, as well - not only would this clean up some tech debt, it would also make it so that + * control selections could also be translated to filter pills on navigation. + */ + async (params) => { + navigateTo({ + url: getSecuritySolutionDashboardUrl(params), + }); + }, + [getSecuritySolutionDashboardUrl, navigateTo] + ); + + const locator = useMemo(() => { + return { + navigate: goToDashboard, + getRedirectUrl: getSecuritySolutionDashboardUrl, + }; + }, [goToDashboard, getSecuritySolutionDashboardUrl]); + + const getCreationOptions: () => Promise = useCallback(() => { + return Promise.resolve({ + useSessionStorageIntegration: true, + useControlGroupIntegration: true, + getInitialInput: () => { + return initialInput.value; + }, + getIncomingEmbeddable: () => + embeddable.getStateTransfer().getIncomingEmbeddablePackage(APP_UI_ID, true), + getEmbeddableAppContext: (dashboardId?: string) => ({ + getCurrentPath: () => + dashboardId ? `${DASHBOARDS_PATH}/${dashboardId}/edit` : `${DASHBOARDS_PATH}/create`, + currentAppId: APP_UI_ID, + }), + }); + }, [embeddable]); + const refetchByForceRefresh = useCallback(() => { dashboardContainer?.forceRefresh(); }, [dashboardContainer]); @@ -104,6 +145,11 @@ const DashboardRendererComponent = ({ dashboardContainer?.updateInput({ tags: [firstSecurityTagId] }); }, [dashboardContainer, firstSecurityTagId, isCreateDashboard]); + useEffect(() => { + /** We need to update the initial input on navigation so that changes to filter pills, queries, etc. get applied */ + initialInput.next({ timeRange, viewMode, query, filters }); + }, [timeRange, viewMode, query, filters]); + /** Dashboard renderer is stored in the state as it's a temporary solution for * https://github.com/elastic/kibana/issues/167751 **/ @@ -114,6 +160,7 @@ const DashboardRendererComponent = ({ useEffect(() => { setDashboardContainerRenderer( { setDashboardContainerRenderer(undefined); }; - }, [getCreationOptions, onDashboardContainerLoaded, refetchByForceRefresh, savedObjectId]); + }, [ + getCreationOptions, + onDashboardContainerLoaded, + refetchByForceRefresh, + savedObjectId, + locator, + ]); return canReadDashboard ? <>{dashboardContainerRenderer} : null; }; From 02758c5bfbfd72ed1be45cd8b321a2efa6abc2b7 Mon Sep 17 00:00:00 2001 From: Jon Date: Mon, 6 Nov 2023 15:17:12 -0600 Subject: [PATCH 10/12] [docs] Add gatekeeper error as known issue (#170668) Adds a known issue for #169170 --------- Co-authored-by: James Rodewig --- docs/CHANGELOG.asciidoc | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/docs/CHANGELOG.asciidoc b/docs/CHANGELOG.asciidoc index 5a052532847bc..c10d674fb4812 100644 --- a/docs/CHANGELOG.asciidoc +++ b/docs/CHANGELOG.asciidoc @@ -60,6 +60,25 @@ Review important information about the {kib} 8.x releases. For information about the {kib} 8.11.0 release, review the following information. +[float] +[[known-issues-8.11.0]] +=== Known issues + +// tag::known-issue-169170[] +[discrete] +.Gatekeeper error on macOS +[%collapsible] +==== +*Details* + +Due to a version upgrade of the server binary used by {kib} and an upstream notarization issue, a Gatekeeper error may display for "node" +when starting {kib} in macOS environments. + +*Workaround* + +More information can be found at <>. + +==== +// end::known-issue-169170[] + [float] [[breaking-changes-8.11.0]] === Breaking changes From 66ba3295b2fccae7528aebf28b03725209086d1d Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Mon, 6 Nov 2023 16:24:24 -0500 Subject: [PATCH 11/12] skip failing test suite (#170690) --- x-pack/test/fleet_api_integration/apis/agents/list.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/fleet_api_integration/apis/agents/list.ts b/x-pack/test/fleet_api_integration/apis/agents/list.ts index 845a0b1a83659..28223e7490fe1 100644 --- a/x-pack/test/fleet_api_integration/apis/agents/list.ts +++ b/x-pack/test/fleet_api_integration/apis/agents/list.ts @@ -17,7 +17,8 @@ export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const es = getService('es'); let elasticAgentpkgVersion: string; - describe('fleet_list_agent', () => { + // Failing: See https://github.com/elastic/kibana/issues/170690 + describe.skip('fleet_list_agent', () => { before(async () => { await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/fleet/agents'); const getPkRes = await supertest From 002e685f0f167652efcdb12ed2770a92089ebd42 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Mon, 6 Nov 2023 16:29:56 -0500 Subject: [PATCH 12/12] skip failing test suite (#170690) --- x-pack/test/fleet_api_integration/apis/agents/list.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/test/fleet_api_integration/apis/agents/list.ts b/x-pack/test/fleet_api_integration/apis/agents/list.ts index 28223e7490fe1..995a0393637c6 100644 --- a/x-pack/test/fleet_api_integration/apis/agents/list.ts +++ b/x-pack/test/fleet_api_integration/apis/agents/list.ts @@ -18,6 +18,7 @@ export default function ({ getService }: FtrProviderContext) { const es = getService('es'); let elasticAgentpkgVersion: string; // Failing: See https://github.com/elastic/kibana/issues/170690 + // Failing: See https://github.com/elastic/kibana/issues/170690 describe.skip('fleet_list_agent', () => { before(async () => { await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/fleet/agents');