diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_save_to_library.tsx b/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_save_to_library.tsx index a47195f8e57bb..8940853444d1b 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_save_to_library.tsx +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_save_to_library.tsx @@ -9,7 +9,6 @@ import React, { useCallback, useMemo } from 'react'; import { toMountPoint } from '@kbn/react-kibana-mount'; import type { LensEmbeddableInput } from '@kbn/lens-plugin/public'; import { unmountComponentAtNode } from 'react-dom'; -import type { SaveProps } from '@kbn/lens-plugin/public/plugin'; import { useKibana } from '../../lib/kibana'; import type { LensAttributes } from './types'; import { useRedirectToDashboardFromLens } from './use_redirect_to_dashboard_from_lens'; @@ -21,8 +20,8 @@ export const useSaveToLibrary = ({ }: { attributes: LensAttributes | undefined | null; }) => { - const { lens, ...startServices } = useKibana().services; - const { SaveModalComponent, canUseEditor } = lens; + const startServices = useKibana().services; + const { SaveModalComponent, canUseEditor } = startServices.lens; const getSecuritySolutionUrl = useGetSecuritySolutionUrl(); const { redirectTo, getEditOrCreateDashboardPath } = useRedirectToDashboardFromLens({ getSecuritySolutionUrl, @@ -33,12 +32,8 @@ export const useSaveToLibrary = ({ const mount = toMountPoint( { - unmountComponentAtNode(targetDomElement); - }} - onClose={() => { - unmountComponentAtNode(targetDomElement); - }} + onSave={() => unmountComponentAtNode(targetDomElement)} + onClose={() => unmountComponentAtNode(targetDomElement)} originatingApp={APP_UI_ID} getOriginatingPath={(dashboardId) => `${SecurityPageName.dashboards}/${getEditOrCreateDashboardPath(dashboardId)}` diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/rule_details/non_default_space.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/rule_details/non_default_space.cy.ts new file mode 100644 index 0000000000000..6b7cb3309e921 --- /dev/null +++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/rule_details/non_default_space.cy.ts @@ -0,0 +1,84 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { createRule } from '../../../../tasks/api_calls/rules'; +import { ruleFields } from '../../../../data/detection_engine'; +import { getExistingRule, getNewRule } from '../../../../objects/rule'; + +import { RULE_NAME_HEADER, RULE_SWITCH } from '../../../../screens/rule_details'; + +import { createTimeline } from '../../../../tasks/api_calls/timelines'; +import { deleteAlertsAndRules, deleteConnectors } from '../../../../tasks/api_calls/common'; +import { login } from '../../../../tasks/login'; +import { activateSpace, getSpaceUrl } from '../../../../tasks/space'; +import { visit } from '../../../../tasks/navigation'; +import { ruleDetailsUrl } from '../../../../urls/rule_details'; + +describe('Non-default space rule detail page', { tags: ['@ess'] }, function () { + const SPACE_ID = 'test'; + + beforeEach(() => { + login(); + activateSpace(SPACE_ID); + deleteAlertsAndRules(); + deleteConnectors(); + createTimeline().then((response) => { + createRule({ + ...getNewRule({ + rule_id: 'rulez', + description: ruleFields.ruleDescription, + name: ruleFields.ruleName, + severity: ruleFields.ruleSeverity, + risk_score: ruleFields.riskScore, + tags: ruleFields.ruleTags, + false_positives: ruleFields.falsePositives, + note: ruleFields.investigationGuide, + timeline_id: response.body.data.persistTimeline.timeline.savedObjectId, + timeline_title: response.body.data.persistTimeline.timeline.title ?? '', + interval: ruleFields.ruleInterval, + from: `now-1h`, + query: ruleFields.ruleQuery, + enabled: false, + max_signals: 500, + threat: [ + { + ...ruleFields.threat, + technique: [ + { + ...ruleFields.threatTechnique, + subtechnique: [ruleFields.threatSubtechnique], + }, + ], + }, + ], + }), + }).then((rule) => { + cy.wrap(rule.body.id).as('ruleId'); + }); + }); + }); + + it('Check responsiveness by enabling/disabling the rule', function () { + visit(getSpaceUrl(SPACE_ID, ruleDetailsUrl(this.ruleId))); + cy.get(RULE_NAME_HEADER).should('contain', ruleFields.ruleName); + + cy.intercept( + 'POST', + getSpaceUrl(SPACE_ID, '/api/detection_engine/rules/_bulk_action?dry_run=false') + ).as('bulk_action'); + cy.get(RULE_SWITCH).should('be.visible'); + cy.get(RULE_SWITCH).click(); + cy.wait('@bulk_action').then(({ response }) => { + cy.wrap(response?.statusCode).should('eql', 200); + cy.wrap(response?.body.attributes.results.updated[0].max_signals).should( + 'eql', + getExistingRule().max_signals + ); + cy.wrap(response?.body.attributes.results.updated[0].enabled).should('eql', true); + }); + }); +}); diff --git a/x-pack/test/security_solution_cypress/cypress/support/commands.js b/x-pack/test/security_solution_cypress/cypress/support/commands.js index ecee00b106be1..413473f29e6d6 100644 --- a/x-pack/test/security_solution_cypress/cypress/support/commands.js +++ b/x-pack/test/security_solution_cypress/cypress/support/commands.js @@ -84,3 +84,8 @@ const waitUntil = (subject, fn, options = {}) => { }; Cypress.Commands.add('waitUntil', { prevSubject: 'optional' }, waitUntil); + +// Sets non-default space id +Cypress.Commands.add('setCurrentSpace', (spaceId) => cy.state('currentSpaceId', spaceId)); +// Reads non-default space id +Cypress.Commands.add('currentSpace', () => cy.state('currentSpaceId')); diff --git a/x-pack/test/security_solution_cypress/cypress/support/index.d.ts b/x-pack/test/security_solution_cypress/cypress/support/index.d.ts index 6928ba89a56f0..5473e3ac7132d 100644 --- a/x-pack/test/security_solution_cypress/cypress/support/index.d.ts +++ b/x-pack/test/security_solution_cypress/cypress/support/index.d.ts @@ -16,6 +16,14 @@ declare namespace Cypress { timeout: number; } ): Chainable; + /** + * Sets a new non-default space id as current + */ + setCurrentSpace(spaceId: string): void; + /** + * Reads current space id value. `undefined` is returned for default space. + */ + currentSpace(): Chainable; } } diff --git a/x-pack/test/security_solution_cypress/cypress/tasks/api_calls/common.ts b/x-pack/test/security_solution_cypress/cypress/tasks/api_calls/common.ts index 5db0b30a2ecce..e25b15b9b4439 100644 --- a/x-pack/test/security_solution_cypress/cypress/tasks/api_calls/common.ts +++ b/x-pack/test/security_solution_cypress/cypress/tasks/api_calls/common.ts @@ -8,9 +8,11 @@ import { DATA_VIEW_PATH, INITIAL_REST_VERSION } from '@kbn/data-views-plugin/server/constants'; import { ELASTIC_HTTP_VERSION_HEADER } from '@kbn/core-http-common'; import { AllConnectorsResponse } from '@kbn/actions-plugin/common/routes/connector/response'; +import { DETECTION_ENGINE_RULES_BULK_ACTION } from '@kbn/security-solution-plugin/common/constants'; import { ELASTICSEARCH_PASSWORD, ELASTICSEARCH_USERNAME } from '../../env_var_names_constants'; import { deleteAllDocuments } from './elasticsearch'; import { DEFAULT_ALERTS_INDEX_PATTERN } from './alerts'; +import { getSpaceUrl } from '../space'; export const API_AUTH = Object.freeze({ user: Cypress.env(ELASTICSEARCH_USERNAME), @@ -41,18 +43,24 @@ export const rootRequest = ({ export const deleteAlertsAndRules = () => { cy.log('Delete all alerts and rules'); - rootRequest({ - method: 'POST', - url: '/api/detection_engine/rules/_bulk_action', - body: { - query: '', - action: 'delete', - }, - failOnStatusCode: false, - timeout: 300000, - }); + cy.currentSpace().then((spaceId) => { + const url = spaceId + ? `/s/${spaceId}${DETECTION_ENGINE_RULES_BULK_ACTION}` + : DETECTION_ENGINE_RULES_BULK_ACTION; + + rootRequest({ + method: 'POST', + url, + body: { + query: '', + action: 'delete', + }, + failOnStatusCode: false, + timeout: 300000, + }); - deleteAllDocuments(`.lists-*,.items-*,${DEFAULT_ALERTS_INDEX_PATTERN}`); + deleteAllDocuments(`.lists-*,.items-*,${DEFAULT_ALERTS_INDEX_PATTERN}`); + }); }; export const getConnectors = () => @@ -62,20 +70,24 @@ export const getConnectors = () => }); export const deleteConnectors = () => { - getConnectors().then(($response) => { - if ($response.body.length > 0) { - const ids = $response.body.map((connector) => { - return connector.id; - }); - ids.forEach((id) => { - if (!INTERNAL_CLOUD_CONNECTORS.includes(id)) { - rootRequest({ - method: 'DELETE', - url: `api/actions/connector/${id}`, - }); - } - }); - } + cy.currentSpace().then((spaceId) => { + getConnectors().then(($response) => { + if ($response.body.length > 0) { + const ids = $response.body.map((connector) => { + return connector.id; + }); + ids.forEach((id) => { + if (!INTERNAL_CLOUD_CONNECTORS.includes(id)) { + rootRequest({ + method: 'DELETE', + url: spaceId + ? getSpaceUrl(spaceId, `api/actions/connector/${id}`) + : `api/actions/connector/${id}`, + }); + } + }); + } + }); }); }; diff --git a/x-pack/test/security_solution_cypress/cypress/tasks/api_calls/rules.ts b/x-pack/test/security_solution_cypress/cypress/tasks/api_calls/rules.ts index d8b4c9f3bf7cb..008fc92bd0224 100644 --- a/x-pack/test/security_solution_cypress/cypress/tasks/api_calls/rules.ts +++ b/x-pack/test/security_solution_cypress/cypress/tasks/api_calls/rules.ts @@ -18,6 +18,7 @@ import type { import type { FetchRulesResponse } from '@kbn/security-solution-plugin/public/detection_engine/rule_management/logic/types'; import { internalAlertingSnoozeRule } from '../../urls/routes'; import { rootRequest } from './common'; +import { getSpaceUrl } from '../space'; export const findAllRules = () => { return rootRequest({ @@ -28,12 +29,14 @@ export const findAllRules = () => { export const createRule = ( rule: RuleCreateProps ): Cypress.Chainable> => { - return rootRequest({ - method: 'POST', - url: DETECTION_ENGINE_RULES_URL, - body: rule, - failOnStatusCode: false, - }); + return cy.currentSpace().then((spaceId) => + rootRequest({ + method: 'POST', + url: spaceId ? getSpaceUrl(spaceId, DETECTION_ENGINE_RULES_URL) : DETECTION_ENGINE_RULES_URL, + body: rule, + failOnStatusCode: false, + }) + ); }; /** diff --git a/x-pack/test/security_solution_cypress/cypress/tasks/space.ts b/x-pack/test/security_solution_cypress/cypress/tasks/space.ts new file mode 100644 index 0000000000000..4c0fa8aa3ed9c --- /dev/null +++ b/x-pack/test/security_solution_cypress/cypress/tasks/space.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { API_HEADERS } from './api_calls/common'; + +/** + * Creates a space and sets it as the current one + */ +export function activateSpace(spaceId: string): void { + const baseUrl = Cypress.config().baseUrl; + if (!baseUrl) { + throw Error(`Cypress config baseUrl not set!`); + } + + cy.request({ + url: `${baseUrl}/api/spaces/space`, + method: 'POST', + body: { + id: spaceId, + name: spaceId, + }, + headers: API_HEADERS, + // For the majority cases the specified space already exists and + // this request would fail. To avoid condition logic and an extra + // request to check for space existence it fails silently. + // + // While it will make errors less transparent when a user doesn't + // have credentials to create spaces. But it's a trade off for now + // choosing simplicity over errors transparency. + failOnStatusCode: false, + }); + + cy.setCurrentSpace(spaceId); +} + +/** + * Constructs a space aware url + */ +export function getSpaceUrl(spaceId: string, url: string): string { + return spaceId ? `/s/${spaceId}${url}` : url; +}