From 0fd5ff51e50ddc243ae9eea0a038becdd84b42d8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 23 Oct 2023 03:03:25 -0400 Subject: [PATCH 01/11] Update dependency elastic-apm-node to ^4.1.0 (main) (#169470) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Change | Age | Adoption | Passing | Confidence | |---|---|---|---|---|---| | [elastic-apm-node](https://togithub.com/elastic/apm-agent-nodejs) | [`^4.0.0` -> `^4.1.0`](https://renovatebot.com/diffs/npm/elastic-apm-node/4.0.0/4.1.0) | [![age](https://developer.mend.io/api/mc/badges/age/npm/elastic-apm-node/4.1.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/elastic-apm-node/4.1.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/elastic-apm-node/4.0.0/4.1.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/elastic-apm-node/4.0.0/4.1.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | --- ### Release Notes
elastic/apm-agent-nodejs (elastic-apm-node) ### [`v4.1.0`](https://togithub.com/elastic/apm-agent-nodejs/releases/tag/v4.1.0) [Compare Source](https://togithub.com/elastic/apm-agent-nodejs/compare/v4.0.0...v4.1.0) For more information, please see the [changelog](https://www.elastic.co/guide/en/apm/agent/nodejs/current/release-notes-4.x.html#release-notes-4.1.0). ##### Elastic APM Node.js agent layer ARNs |Region|ARN| |------|---| |af-south-1|arn:aws:lambda:af-south-1:267093732750:layer:elastic-apm-node-ver-4-1-0:1| |ap-east-1|arn:aws:lambda:ap-east-1:267093732750:layer:elastic-apm-node-ver-4-1-0:1| |ap-northeast-1|arn:aws:lambda:ap-northeast-1:267093732750:layer:elastic-apm-node-ver-4-1-0:1| |ap-northeast-2|arn:aws:lambda:ap-northeast-2:267093732750:layer:elastic-apm-node-ver-4-1-0:1| |ap-northeast-3|arn:aws:lambda:ap-northeast-3:267093732750:layer:elastic-apm-node-ver-4-1-0:1| |ap-south-1|arn:aws:lambda:ap-south-1:267093732750:layer:elastic-apm-node-ver-4-1-0:1| |ap-southeast-1|arn:aws:lambda:ap-southeast-1:267093732750:layer:elastic-apm-node-ver-4-1-0:1| |ap-southeast-2|arn:aws:lambda:ap-southeast-2:267093732750:layer:elastic-apm-node-ver-4-1-0:1| |ap-southeast-3|arn:aws:lambda:ap-southeast-3:267093732750:layer:elastic-apm-node-ver-4-1-0:1| |ca-central-1|arn:aws:lambda:ca-central-1:267093732750:layer:elastic-apm-node-ver-4-1-0:1| |eu-central-1|arn:aws:lambda:eu-central-1:267093732750:layer:elastic-apm-node-ver-4-1-0:1| |eu-north-1|arn:aws:lambda:eu-north-1:267093732750:layer:elastic-apm-node-ver-4-1-0:1| |eu-south-1|arn:aws:lambda:eu-south-1:267093732750:layer:elastic-apm-node-ver-4-1-0:1| |eu-west-1|arn:aws:lambda:eu-west-1:267093732750:layer:elastic-apm-node-ver-4-1-0:1| |eu-west-2|arn:aws:lambda:eu-west-2:267093732750:layer:elastic-apm-node-ver-4-1-0:1| |eu-west-3|arn:aws:lambda:eu-west-3:267093732750:layer:elastic-apm-node-ver-4-1-0:1| |me-south-1|arn:aws:lambda:me-south-1:267093732750:layer:elastic-apm-node-ver-4-1-0:1| |sa-east-1|arn:aws:lambda:sa-east-1:267093732750:layer:elastic-apm-node-ver-4-1-0:1| |us-east-1|arn:aws:lambda:us-east-1:267093732750:layer:elastic-apm-node-ver-4-1-0:1| |us-east-2|arn:aws:lambda:us-east-2:267093732750:layer:elastic-apm-node-ver-4-1-0:1| |us-west-1|arn:aws:lambda:us-west-1:267093732750:layer:elastic-apm-node-ver-4-1-0:1| |us-west-2|arn:aws:lambda:us-west-2:267093732750:layer:elastic-apm-node-ver-4-1-0:1|
--- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View repository job log [here](https://developer.mend.io/github/elastic/kibana). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 21 +++++++++++++++++---- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 28d0a8e48b29b..b52c7716906cd 100644 --- a/package.json +++ b/package.json @@ -887,7 +887,7 @@ "deep-freeze-strict": "^1.1.1", "deepmerge": "^4.2.2", "del": "^6.1.0", - "elastic-apm-node": "^4.0.0", + "elastic-apm-node": "^4.1.0", "email-addresses": "^5.0.0", "execa": "^5.1.1", "expiry-js": "0.1.7", diff --git a/yarn.lock b/yarn.lock index 89a4b66f451a1..494e5324d4d83 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11826,6 +11826,11 @@ big.js@^5.2.2: resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ== +bignumber.js@^9.0.0: + version "9.1.2" + resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.1.2.tgz#b7c4242259c008903b13707983b5f4bbd31eda0c" + integrity sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug== + binary-extensions@^2.0.0, binary-extensions@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" @@ -15131,10 +15136,10 @@ elastic-apm-node@3.46.0: traverse "^0.6.6" unicode-byte-truncate "^1.0.0" -elastic-apm-node@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/elastic-apm-node/-/elastic-apm-node-4.0.0.tgz#14963e5bc8cdd073400a708bd09517e198c4c605" - integrity sha512-0rf5k4UL+oNc6Xr57PKDDGDVuvW9nsLPOEI0YLqPSMBDaMdN1iW0n6MEsa4TPFtXgT1aWCdTSDUVjlgvIWKmFQ== +elastic-apm-node@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/elastic-apm-node/-/elastic-apm-node-4.1.0.tgz#b1154a2d8e17b7762badf4fc696d8de7439ce928" + integrity sha512-8t9lbyfi4WUPxjPvRNO80QX2Ysf8I+D21wq+aphY+97Fk7kk6SDeZH+5U+o7HWSbqZpo/PYJGuKDUYc9PXuEWw== dependencies: "@elastic/ecs-pino-format" "^1.2.0" "@opentelemetry/api" "^1.4.1" @@ -15155,6 +15160,7 @@ elastic-apm-node@^4.0.0: fast-stream-to-buffer "^1.0.0" http-headers "^3.0.2" import-in-the-middle "1.4.2" + json-bigint "^1.0.0" lru-cache "^10.0.1" measured-reporting "^1.51.1" module-details-from-path "^1.0.3" @@ -20262,6 +20268,13 @@ jsesc@~0.5.0: resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" integrity sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0= +json-bigint@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/json-bigint/-/json-bigint-1.0.0.tgz#ae547823ac0cad8398667f8cd9ef4730f5b01ff1" + integrity sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ== + dependencies: + bignumber.js "^9.0.0" + json-buffer@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.0.tgz#5b1f397afc75d677bde8bcfc0e47e1f9a3d9a898" From 17c78db794be9ff8bbaed2b000cc2ee70ae46972 Mon Sep 17 00:00:00 2001 From: Miriam <31922082+MiriamAparicio@users.noreply.github.com> Date: Mon, 23 Oct 2023 08:21:46 +0100 Subject: [PATCH 02/11] [ObsUX] Fix timestamp for anomaly alert test (#169255) Closes https://github.com/elastic/kibana/issues/160769 ### What was done The anomaly alert test was failing with timeout because was not generating alerts, the data and spikes were added to far away in time, so when the job is created doesn't take into account data that far in the past We fixed the timerange of the data generated and the spikes. BEFORE: image AFTER: image --- .../anomaly/register_anomaly_rule_type.ts | 1 - .../tests/alerts/anomaly_alert.spec.ts | 41 +++++++++---------- 2 files changed, 19 insertions(+), 23 deletions(-) diff --git a/x-pack/plugins/apm/server/routes/alerts/rule_types/anomaly/register_anomaly_rule_type.ts b/x-pack/plugins/apm/server/routes/alerts/rule_types/anomaly/register_anomaly_rule_type.ts index b74db63061306..2f4f3b75c8807 100644 --- a/x-pack/plugins/apm/server/routes/alerts/rule_types/anomaly/register_anomaly_rule_type.ts +++ b/x-pack/plugins/apm/server/routes/alerts/rule_types/anomaly/register_anomaly_rule_type.ts @@ -174,7 +174,6 @@ export function registerAnomalyRuleType({ range: { timestamp: { gte: dateStart, - format: 'epoch_millis', }, }, }, diff --git a/x-pack/test/apm_api_integration/tests/alerts/anomaly_alert.spec.ts b/x-pack/test/apm_api_integration/tests/alerts/anomaly_alert.spec.ts index c08503baa37f9..3475888a5407e 100644 --- a/x-pack/test/apm_api_integration/tests/alerts/anomaly_alert.spec.ts +++ b/x-pack/test/apm_api_integration/tests/alerts/anomaly_alert.spec.ts @@ -5,6 +5,7 @@ * 2.0. */ +import moment from 'moment'; import { ApmRuleType } from '@kbn/apm-plugin/common/rules/apm_rule_types'; import { apm, timerange } from '@kbn/apm-synthtrace-client'; import expect from '@kbn/expect'; @@ -23,34 +24,32 @@ export default function ApiTest({ getService }: FtrProviderContext) { const logger = getService('log'); const synthtraceEsClient = getService('synthtraceEsClient'); - // FLAKY https://github.com/elastic/kibana/issues/160298 - registry.when.skip( + registry.when( 'fetching service anomalies with a trial license', { config: 'trial', archives: [] }, () => { - const start = '2021-01-01T00:00:00.000Z'; - const end = '2021-01-08T00:15:00.000Z'; + const start = moment().subtract(1, 'days').valueOf(); + const end = moment().valueOf(); - const spikeStart = new Date('2021-01-07T23:15:00.000Z').getTime(); - const spikeEnd = new Date('2021-01-08T00:15:00.000Z').getTime(); - - const NORMAL_DURATION = 100; - const NORMAL_RATE = 1; + const spikeStart = moment().subtract(15, 'minutes').valueOf(); + const spikeEnd = moment().valueOf(); let ruleId: string; before(async () => { + await cleanup(); + const serviceA = apm .service({ name: 'a', environment: 'production', agentName: 'java' }) .instance('a'); - const events = timerange(new Date(start).getTime(), new Date(end).getTime()) + const events = timerange(start, end) .interval('1m') .rate(1) .generator((timestamp) => { const isInSpike = timestamp >= spikeStart && timestamp < spikeEnd; - const count = isInSpike ? 4 : NORMAL_RATE; - const duration = isInSpike ? 1000 : NORMAL_DURATION; + const count = isInSpike ? 4 : 1; + const duration = isInSpike ? 1000 : 100; const outcome = isInSpike ? 'failure' : 'success'; return [ @@ -65,26 +64,25 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); await synthtraceEsClient.index(events); + + await createAndRunApmMlJobs({ es, ml, environments: ['production'] }); }); after(async () => { + await cleanup(); + }); + + async function cleanup() { try { await synthtraceEsClient.clean(); await deleteRuleById({ supertest, ruleId }); + await ml.cleanMlIndices(); } catch (e) { logger.info('Could not delete rule by id', e); } - }); + } describe('with ml jobs', () => { - before(async () => { - await createAndRunApmMlJobs({ es, ml, environments: ['production'] }); - }); - - after(async () => { - await ml.cleanMlIndices(); - }); - it('checks if alert is active', async () => { const createdRule = await createApmRule({ supertest, @@ -97,7 +95,6 @@ export default function ApiTest({ getService }: FtrProviderContext) { }, ruleTypeId: ApmRuleType.Anomaly, }); - ruleId = createdRule.id; if (!ruleId) { expect(ruleId).to.not.eql(undefined); From a8a22c39b374958b1c57e88d849c03ca2495b096 Mon Sep 17 00:00:00 2001 From: Sergi Massaneda Date: Mon, 23 Oct 2023 10:36:07 +0200 Subject: [PATCH 03/11] [Security Solution][Serverless] Implements panelContentProvider on the DefaultNavigation (#169270) ## Summary Implements the `panelContentProvider` for the `DefaultNavigation` component, so the content of the panels when open is provided by the Security Solution plugin. In order to test it, the experimental flag needs to be enabled. In `config/serverless.security.yml` add: ``` xpack.securitySolutionServerless.enableExperimental: ['platformNavEnabled'] ``` ## Screenshot Captura de pantalla 2023-10-18 a les 18 38 04 (The vertical separation of the main nav links is still not implemented by the DefaultNavigation) --------- Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../src/chrome_service.mock.ts | 1 + .../security-solution/side_nav/panel.ts | 2 +- .../side_nav/src/solution_side_nav_panel.tsx | 60 +++-- .../src/telemetry/telemetry_context.tsx | 6 +- .../{side_navigation => }/categories.ts | 28 +- .../public/navigation/default_navigation.tsx | 47 ---- .../public/navigation/index.ts | 12 +- .../links/sections/project_settings_links.ts | 4 - .../public/navigation/links/util.ts | 16 +- .../navigation/navigation_tree/index.ts | 14 +- .../navigation_tree/navigation_tree.test.ts | 63 +++-- .../navigation_tree/navigation_tree.ts | 251 ++++++++++-------- .../navigation/project_navigation/index.tsx | 17 ++ .../project_navigation/project_navigation.tsx | 80 ++++++ .../side_navigation/side_navigation.tsx | 19 +- .../side_navigation_footer.test.tsx | 58 ++-- .../side_navigation_footer.tsx | 56 ++-- .../side_navigation/use_side_nav_items.ts | 26 +- 18 files changed, 455 insertions(+), 305 deletions(-) rename x-pack/plugins/security_solution_serverless/public/navigation/{side_navigation => }/categories.ts (56%) delete mode 100644 x-pack/plugins/security_solution_serverless/public/navigation/default_navigation.tsx create mode 100644 x-pack/plugins/security_solution_serverless/public/navigation/project_navigation/index.tsx create mode 100644 x-pack/plugins/security_solution_serverless/public/navigation/project_navigation/project_navigation.tsx diff --git a/packages/core/chrome/core-chrome-browser-mocks/src/chrome_service.mock.ts b/packages/core/chrome/core-chrome-browser-mocks/src/chrome_service.mock.ts index d6a7a2e065945..191edc708b64a 100644 --- a/packages/core/chrome/core-chrome-browser-mocks/src/chrome_service.mock.ts +++ b/packages/core/chrome/core-chrome-browser-mocks/src/chrome_service.mock.ts @@ -89,6 +89,7 @@ const createStartContractMock = () => { startContract.getIsNavDrawerLocked$.mockReturnValue(new BehaviorSubject(false)); startContract.getBodyClasses$.mockReturnValue(new BehaviorSubject([])); startContract.hasHeaderBanner$.mockReturnValue(new BehaviorSubject(false)); + startContract.getIsSideNavCollapsed$.mockReturnValue(new BehaviorSubject(false)); return startContract; }; diff --git a/x-pack/packages/security-solution/side_nav/panel.ts b/x-pack/packages/security-solution/side_nav/panel.ts index a0341cd000812..d52f519c79b35 100644 --- a/x-pack/packages/security-solution/side_nav/panel.ts +++ b/x-pack/packages/security-solution/side_nav/panel.ts @@ -5,4 +5,4 @@ * 2.0. */ -export { SolutionSideNavPanel } from './src/solution_side_nav_panel'; +export { SolutionSideNavPanelContent } from './src/solution_side_nav_panel'; diff --git a/x-pack/packages/security-solution/side_nav/src/solution_side_nav_panel.tsx b/x-pack/packages/security-solution/side_nav/src/solution_side_nav_panel.tsx index e04f042f02960..dfe2f643d4783 100644 --- a/x-pack/packages/security-solution/side_nav/src/solution_side_nav_panel.tsx +++ b/x-pack/packages/security-solution/side_nav/src/solution_side_nav_panel.tsx @@ -50,12 +50,14 @@ import { accordionButtonClassName, } from './solution_side_nav_panel.styles'; -export interface SolutionSideNavPanelProps { - onClose: () => void; - onOutsideClick: () => void; +export interface SolutionSideNavPanelContentProps { title: string; + onClose: () => void; items: SolutionSideNavItem[]; categories?: LinkCategories; +} +export interface SolutionSideNavPanelProps extends SolutionSideNavPanelContentProps { + onOutsideClick: () => void; bottomOffset?: string; topOffset?: string; } @@ -85,7 +87,6 @@ export const SolutionSideNavPanel: React.FC = React.m $topOffset, }); const panelClasses = classNames(panelClassName, 'eui-yScroll', solutionSideNavPanelStyles); - const titleClasses = classNames(SolutionSideNavTitleStyles(euiTheme)); // ESC key closes PanelNav const onKeyDown = useCallback( @@ -110,24 +111,12 @@ export const SolutionSideNavPanel: React.FC = React.m paddingSize="m" data-test-subj="solutionSideNavPanel" > - - - - {title} - - - - {categories ? ( - - ) : ( - - )} - - + @@ -137,6 +126,33 @@ export const SolutionSideNavPanel: React.FC = React.m } ); +export const SolutionSideNavPanelContent: React.FC = React.memo( + function SolutionSideNavPanelContent({ title, onClose, categories, items }) { + const { euiTheme } = useEuiTheme(); + const titleClasses = classNames(SolutionSideNavTitleStyles(euiTheme)); + return ( + + + + {title} + + + + {categories ? ( + + ) : ( + + )} + + + ); + } +); + interface SolutionSideNavPanelCategoriesProps { categories: LinkCategories; items: SolutionSideNavItem[]; diff --git a/x-pack/packages/security-solution/side_nav/src/telemetry/telemetry_context.tsx b/x-pack/packages/security-solution/side_nav/src/telemetry/telemetry_context.tsx index c7e97969ad31c..aec5460ddc67a 100644 --- a/x-pack/packages/security-solution/side_nav/src/telemetry/telemetry_context.tsx +++ b/x-pack/packages/security-solution/side_nav/src/telemetry/telemetry_context.tsx @@ -20,9 +20,5 @@ export const TelemetryContextProvider: FC = ({ children, }; export const useTelemetryContext = () => { - const context = useContext(TelemetryContext); - if (!context) { - throw new Error('No TelemetryContext found.'); - } - return context; + return useContext(TelemetryContext) ?? {}; }; diff --git a/x-pack/plugins/security_solution_serverless/public/navigation/side_navigation/categories.ts b/x-pack/plugins/security_solution_serverless/public/navigation/categories.ts similarity index 56% rename from x-pack/plugins/security_solution_serverless/public/navigation/side_navigation/categories.ts rename to x-pack/plugins/security_solution_serverless/public/navigation/categories.ts index e7c9057d950ff..fe5e0bf6c1d2a 100644 --- a/x-pack/plugins/security_solution_serverless/public/navigation/side_navigation/categories.ts +++ b/x-pack/plugins/security_solution_serverless/public/navigation/categories.ts @@ -5,14 +5,17 @@ * 2.0. */ +import { i18n } from '@kbn/i18n'; import { SecurityPageName, LinkCategoryType, + type LinkCategory, type SeparatorLinkCategory, } from '@kbn/security-solution-navigation'; -import { ExternalPageName } from '../links/constants'; +import { ExternalPageName } from './links/constants'; +import type { ProjectPageName } from './links/types'; -export const CATEGORIES: SeparatorLinkCategory[] = [ +export const CATEGORIES: Array> = [ { type: LinkCategoryType.separator, linkIds: [ExternalPageName.discover, SecurityPageName.dashboards], @@ -43,3 +46,24 @@ export const CATEGORIES: SeparatorLinkCategory[] = [ linkIds: [SecurityPageName.mlLanding], }, ]; + +export const FOOTER_CATEGORIES: Array> = [ + { + type: LinkCategoryType.separator, + linkIds: [SecurityPageName.landing, ExternalPageName.devTools], + }, + { + type: LinkCategoryType.accordion, + label: i18n.translate('xpack.securitySolutionServerless.nav.projectSettings.title', { + defaultMessage: 'Project settings', + }), + iconType: 'gear', + linkIds: [ + ExternalPageName.management, + ExternalPageName.integrationsSecurity, + ExternalPageName.cloudUsersAndRoles, + ExternalPageName.cloudPerformance, + ExternalPageName.cloudBilling, + ], + }, +]; diff --git a/x-pack/plugins/security_solution_serverless/public/navigation/default_navigation.tsx b/x-pack/plugins/security_solution_serverless/public/navigation/default_navigation.tsx deleted file mode 100644 index 74f3d0e3afd3d..0000000000000 --- a/x-pack/plugins/security_solution_serverless/public/navigation/default_navigation.tsx +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import React, { Suspense } from 'react'; -import { EuiLoadingSpinner } from '@elastic/eui'; -import type { NavigationTreeDefinition } from '@kbn/shared-ux-chrome-navigation'; -import type { SideNavComponent } from '@kbn/core-chrome-browser'; -import type { Services } from '../common/services'; - -const SecurityDefaultNavigationLazy = React.lazy(() => - import('@kbn/shared-ux-chrome-navigation').then( - ({ DefaultNavigation, NavigationKibanaProvider }) => ({ - default: React.memo<{ - navigationTree: NavigationTreeDefinition; - services: Services; - }>(function SecurityDefaultNavigation({ navigationTree, services }) { - return ( - - - - ); - }), - }) - ) -); - -export const getDefaultNavigationComponent = ( - navigationTree: NavigationTreeDefinition, - services: Services -): SideNavComponent => - function SecuritySideNavComponent() { - return ( - }> - - - ); - }; diff --git a/x-pack/plugins/security_solution_serverless/public/navigation/index.ts b/x-pack/plugins/security_solution_serverless/public/navigation/index.ts index 1504f8dfea8c2..0dd14ebacd544 100644 --- a/x-pack/plugins/security_solution_serverless/public/navigation/index.ts +++ b/x-pack/plugins/security_solution_serverless/public/navigation/index.ts @@ -9,10 +9,11 @@ import { APP_PATH } from '@kbn/security-solution-plugin/common'; import type { CoreSetup } from '@kbn/core/public'; import type { SecuritySolutionServerlessPluginSetupDeps } from '../types'; import type { Services } from '../common/services'; +import { withServicesProvider } from '../common/services'; import { subscribeBreadcrumbs } from './breadcrumbs'; import { ProjectNavigationTree } from './navigation_tree'; import { getSecuritySideNavComponent } from './side_navigation'; -import { getDefaultNavigationComponent } from './default_navigation'; +import { SecuritySideNavComponent } from './project_navigation'; import { projectAppLinksSwitcher } from './links/app_links'; import { formatProjectDeepLinks } from './links/deep_links'; @@ -28,15 +29,14 @@ export const startNavigation = (services: Services) => { const { serverless, management } = services; serverless.setProjectHome(APP_PATH); + management.setupCardsNavigation({ enabled: true }); + const projectNavigationTree = new ProjectNavigationTree(services); if (services.experimentalFeatures.platformNavEnabled) { - projectNavigationTree.getNavigationTree$().subscribe((navigationTree) => { - serverless.setSideNavComponent(getDefaultNavigationComponent(navigationTree, services)); - }); + const SideNavComponentWithServices = withServicesProvider(SecuritySideNavComponent, services); + serverless.setSideNavComponent(SideNavComponentWithServices); } else { - management.setupCardsNavigation({ enabled: true }); - projectNavigationTree.getChromeNavigationTree$().subscribe((chromeNavigationTree) => { serverless.setNavigation({ navigationTree: chromeNavigationTree }); }); diff --git a/x-pack/plugins/security_solution_serverless/public/navigation/links/sections/project_settings_links.ts b/x-pack/plugins/security_solution_serverless/public/navigation/links/sections/project_settings_links.ts index b5c4d746c2af1..f2deadd1fdf25 100644 --- a/x-pack/plugins/security_solution_serverless/public/navigation/links/sections/project_settings_links.ts +++ b/x-pack/plugins/security_solution_serverless/public/navigation/links/sections/project_settings_links.ts @@ -31,10 +31,6 @@ export const projectSettingsNavLinks: ProjectNavigationLink[] = [ id: ExternalPageName.cloudUsersAndRoles, title: i18n.CLOUD_USERS_ROLES_TITLE, }, - { - id: ExternalPageName.cloudPerformance, - title: i18n.CLOUD_PERFORMANCE_TITLE, - }, { id: ExternalPageName.cloudBilling, title: i18n.CLOUD_BILLING_TITLE, diff --git a/x-pack/plugins/security_solution_serverless/public/navigation/links/util.ts b/x-pack/plugins/security_solution_serverless/public/navigation/links/util.ts index 0572dcdedb2ea..edaf40529e16d 100644 --- a/x-pack/plugins/security_solution_serverless/public/navigation/links/util.ts +++ b/x-pack/plugins/security_solution_serverless/public/navigation/links/util.ts @@ -5,7 +5,8 @@ * 2.0. */ -import { APP_UI_ID } from '@kbn/security-solution-plugin/common'; +import { APP_UI_ID, SecurityPageName } from '@kbn/security-solution-plugin/common'; +import { ExternalPageName } from './constants'; import type { GetCloudUrl, ProjectPageName } from './types'; export const getNavLinkIdFromProjectPageName = (projectNavLinkId: ProjectPageName): string => { @@ -42,3 +43,16 @@ export const getCloudUrl: GetCloudUrl = (cloudUrlKey, cloud) => { return undefined; } }; + +/** + * Defines the navigation items that should be in the footer of the side navigation. + * @todo Make it a new property in the `NavigationLink` type `position?: 'top' | 'bottom' (default: 'top')` + */ +export const isBottomNavItemId = (id: string) => + id === SecurityPageName.landing || + id === ExternalPageName.devTools || + id === ExternalPageName.management || + id === ExternalPageName.integrationsSecurity || + id === ExternalPageName.cloudUsersAndRoles || + id === ExternalPageName.cloudPerformance || + id === ExternalPageName.cloudBilling; diff --git a/x-pack/plugins/security_solution_serverless/public/navigation/navigation_tree/index.ts b/x-pack/plugins/security_solution_serverless/public/navigation/navigation_tree/index.ts index 4653756dc435c..a1a04b9ca236e 100644 --- a/x-pack/plugins/security_solution_serverless/public/navigation/navigation_tree/index.ts +++ b/x-pack/plugins/security_solution_serverless/public/navigation/navigation_tree/index.ts @@ -7,16 +7,10 @@ import type { Observable } from 'rxjs'; import { map } from 'rxjs'; -import type { NavigationTreeDefinition } from '@kbn/shared-ux-chrome-navigation'; import type { ChromeProjectNavigationNode } from '@kbn/core-chrome-browser'; -import type { LinkCategory } from '@kbn/security-solution-navigation'; import type { Services } from '../../common/services'; -import type { ProjectNavLinks, ProjectPageName } from '../links/types'; +import type { ProjectNavLinks } from '../links/types'; import { getFormatChromeProjectNavNodes } from './chrome_navigation_tree'; -import { formatNavigationTree } from './navigation_tree'; -import { CATEGORIES } from '../side_navigation/categories'; - -const projectCategories = CATEGORIES as Array>; /** * This class is temporary until we can remove the chrome navigation tree and use only the formatNavigationTree @@ -29,12 +23,6 @@ export class ProjectNavigationTree { this.projectNavLinks$ = getProjectNavLinks$(); } - public getNavigationTree$(): Observable { - return this.projectNavLinks$.pipe( - map((projectNavLinks) => formatNavigationTree(projectNavLinks, projectCategories)) - ); - } - public getChromeNavigationTree$(): Observable { const formatChromeProjectNavNodes = getFormatChromeProjectNavNodes(this.services); return this.projectNavLinks$.pipe( diff --git a/x-pack/plugins/security_solution_serverless/public/navigation/navigation_tree/navigation_tree.test.ts b/x-pack/plugins/security_solution_serverless/public/navigation/navigation_tree/navigation_tree.test.ts index f4971a271d7fb..377f01ca0d784 100644 --- a/x-pack/plugins/security_solution_serverless/public/navigation/navigation_tree/navigation_tree.test.ts +++ b/x-pack/plugins/security_solution_serverless/public/navigation/navigation_tree/navigation_tree.test.ts @@ -48,11 +48,12 @@ describe('formatNavigationTree', () => { }); it('should format flat nav nodes', async () => { - const navigationTree = formatNavigationTree([link1]); + const navigationTree = formatNavigationTree([link1], [], []); const securityNode = navigationTree.body?.[0] as GroupDefinition; expect(securityNode?.children).toEqual([ { + id: link1.id, link: chromeNavLink1.id, title: link1.title, }, @@ -65,15 +66,17 @@ describe('formatNavigationTree', () => { type: LinkCategoryType.title, linkIds: [link1Id], }; - const navigationTree = formatNavigationTree([link1], [category]); + const navigationTree = formatNavigationTree([link1], [category], []); const securityNode = navigationTree.body?.[0] as GroupDefinition; expect(securityNode?.children).toEqual([ { title: category.label, id: expect.any(String), + breadcrumbStatus: 'hidden', children: [ { + id: link1.id, link: chromeNavLink1.id, title: link1.title, }, @@ -88,17 +91,24 @@ describe('formatNavigationTree', () => { type: LinkCategoryType.separator, linkIds: [link1Id, link2Id], }; - const navigationTree = formatNavigationTree([link1, link2], [category]); + const navigationTree = formatNavigationTree([link1, link2], [category], []); const securityNode = navigationTree.body?.[0] as GroupDefinition; expect(securityNode?.children).toEqual([ { - link: chromeNavLink1.id, - title: link1.title, - }, - { - link: chromeNavLink2.id, - title: link2.title, + breadcrumbStatus: 'hidden', + children: [ + { + id: link1.id, + link: chromeNavLink1.id, + title: link1.title, + }, + { + id: link2.id, + link: chromeNavLink2.id, + title: link2.title, + }, + ], }, ]); }); @@ -109,15 +119,17 @@ describe('formatNavigationTree', () => { type: LinkCategoryType.title, linkIds: [link1Id, link2Id], }; - const navigationTree = formatNavigationTree([link1], [category]); + const navigationTree = formatNavigationTree([link1], [category], []); const securityNode = navigationTree.body?.[0] as GroupDefinition; expect(securityNode?.children).toEqual([ { title: category.label, id: expect.any(String), + breadcrumbStatus: 'hidden', children: [ { + id: link1.id, link: chromeNavLink1.id, title: link1.title, }, @@ -132,15 +144,17 @@ describe('formatNavigationTree', () => { type: LinkCategoryType.title, linkIds: [link1Id], }; - const navigationTree = formatNavigationTree([link1, link2], [category]); + const navigationTree = formatNavigationTree([link1, link2], [category], []); const securityNode = navigationTree.body?.[0] as GroupDefinition; expect(securityNode?.children).toEqual([ { title: category.label, id: expect.any(String), + breadcrumbStatus: 'hidden', children: [ { + id: link1.id, link: chromeNavLink1.id, title: link1.title, }, @@ -150,11 +164,12 @@ describe('formatNavigationTree', () => { }); it('should format external chrome nav nodes', async () => { - const navigationTree = formatNavigationTree([link3]); + const navigationTree = formatNavigationTree([link3], [], []); const securityNode = navigationTree.body?.[0] as GroupDefinition; expect(securityNode?.children).toEqual([ { + id: link3.id, link: chromeNavLink3.id, title: link3.title, }, @@ -162,20 +177,25 @@ describe('formatNavigationTree', () => { }); it('should set nested links', async () => { - const navigationTree = formatNavigationTree([ - { ...link1, links: [{ ...link2, links: [link3] }] }, - ]); + const navigationTree = formatNavigationTree( + [{ ...link1, links: [{ ...link2, links: [link3] }] }], + [], + [] + ); const securityNode = navigationTree.body?.[0] as GroupDefinition; expect(securityNode?.children).toEqual([ { + id: link1.id, link: chromeNavLink1.id, title: link1.title, children: [ { + id: link2.id, link: chromeNavLink2.id, title: link2.title, - children: [{ link: chromeNavLink3.id, title: link3.title }], + children: [{ id: link3.id, link: chromeNavLink3.id, title: link3.title }], + renderAs: 'panelOpener', }, ], }, @@ -188,19 +208,22 @@ describe('formatNavigationTree', () => { id: `${APP_UI_ID}:${SecurityPageName.usersEvents}`, // userEvents link is blacklisted }; - const navigationTree = formatNavigationTree([ - { ...link1, id: SecurityPageName.usersEvents }, - link2, - ]); + const navigationTree = formatNavigationTree( + [{ ...link1, id: SecurityPageName.usersEvents }, link2], + [], + [] + ); const securityNode = navigationTree.body?.[0] as GroupDefinition; expect(securityNode?.children).toEqual([ { + id: SecurityPageName.usersEvents, link: chromeNavLinkTest.id, title: link1.title, breadcrumbStatus: 'hidden', }, { + id: link2.id, link: chromeNavLink2.id, title: link2.title, }, diff --git a/x-pack/plugins/security_solution_serverless/public/navigation/navigation_tree/navigation_tree.ts b/x-pack/plugins/security_solution_serverless/public/navigation/navigation_tree/navigation_tree.ts index 926e54ad8bb33..2d9ad8c1bb2d5 100644 --- a/x-pack/plugins/security_solution_serverless/public/navigation/navigation_tree/navigation_tree.ts +++ b/x-pack/plugins/security_solution_serverless/public/navigation/navigation_tree/navigation_tree.ts @@ -4,118 +4,66 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import { partition } from 'lodash/fp'; import { i18n } from '@kbn/i18n'; -import type { NavigationTreeDefinition } from '@kbn/shared-ux-chrome-navigation'; +import type { + NavigationTreeDefinition, + RootNavigationItemDefinition, +} from '@kbn/shared-ux-chrome-navigation'; import type { AppDeepLinkId, NodeDefinition } from '@kbn/core-chrome-browser'; import type { LinkCategory } from '@kbn/security-solution-navigation'; import { - SecurityPageName, isSeparatorLinkCategory, isTitleLinkCategory, + isAccordionLinkCategory, } from '@kbn/security-solution-navigation'; import type { ProjectNavigationLink, ProjectPageName } from '../links/types'; -import { getNavLinkIdFromProjectPageName } from '../links/util'; +import { getNavLinkIdFromProjectPageName, isBottomNavItemId, isCloudLink } from '../links/util'; import { isBreadcrumbHidden } from './utils'; +import { ExternalPageName } from '../links/constants'; -const SECURITY_TITLE = i18n.translate('xpack.securitySolutionServerless.nav.solution.title', { - defaultMessage: 'Security', -}); -const GET_STARTED_TITLE = i18n.translate('xpack.securitySolutionServerless.nav.getStarted.title', { - defaultMessage: 'Get Started', -}); -const DEV_TOOLS_TITLE = i18n.translate('xpack.securitySolutionServerless.nav.devTools.title', { - defaultMessage: 'Developer tools', -}); -const PROJECT_SETTINGS_TITLE = i18n.translate( - 'xpack.securitySolutionServerless.nav.projectSettings.title', +const SECURITY_PROJECT_TITLE = i18n.translate( + 'xpack.securitySolutionServerless.nav.solution.title', { - defaultMessage: 'Project settings', + defaultMessage: 'Security', } ); export const formatNavigationTree = ( projectNavLinks: ProjectNavigationLink[], - categories?: Readonly>> + bodyCategories: Readonly>>, + footerCategories: Readonly>> ): NavigationTreeDefinition => { - const children = formatNodesFromLinks(projectNavLinks, categories); + const [bodyNavItems, footerNavItems] = partition( + ({ id }) => !isBottomNavItemId(id), + projectNavLinks + ); + + const bodyChildren = addMainLinksPanelOpenerProp( + formatNodesFromLinks(bodyNavItems, bodyCategories) + ); return { body: [ - children - ? { - type: 'navGroup', - id: 'security_project_nav', - title: SECURITY_TITLE, - icon: 'logoSecurity', - breadcrumbStatus: 'hidden', - defaultIsCollapsed: false, - children, - } - : { - type: 'navItem', - id: 'security_project_nav', - title: SECURITY_TITLE, - icon: 'logoSecurity', - breadcrumbStatus: 'hidden', - }, - ], - footer: [ - { - type: 'navItem', - id: 'getStarted', - title: GET_STARTED_TITLE, - link: getNavLinkIdFromProjectPageName(SecurityPageName.landing) as AppDeepLinkId, - icon: 'launch', - }, - { - type: 'navItem', - id: 'devTools', - title: DEV_TOOLS_TITLE, - link: 'dev_tools', - icon: 'editorCodeBlock', - }, { type: 'navGroup', - id: 'project_settings_project_nav', - title: PROJECT_SETTINGS_TITLE, - icon: 'gear', + id: 'security_project_nav', + title: SECURITY_PROJECT_TITLE, + icon: 'logoSecurity', breadcrumbStatus: 'hidden', - children: [ - { - id: 'settings', - children: [ - { - link: 'management', - title: 'Management', - }, - { - link: 'integrations', - }, - { - link: 'fleet', - }, - { - id: 'cloudLinkUserAndRoles', - cloudLink: 'userAndRoles', - }, - { - id: 'cloudLinkBilling', - cloudLink: 'billingAndSub', - }, - ], - }, - ], + defaultIsCollapsed: false, + children: bodyChildren, }, ], + footer: formatFooterNodesFromLinks(footerNavItems, footerCategories), }; }; +// Body + const formatNodesFromLinks = ( projectNavLinks: ProjectNavigationLink[], parentCategories?: Readonly>> -): NodeDefinition[] | undefined => { - if (projectNavLinks.length === 0) { - return undefined; - } +): NodeDefinition[] => { const nodes: NodeDefinition[] = []; if (parentCategories?.length) { parentCategories.forEach((category) => { @@ -124,10 +72,7 @@ const formatNodesFromLinks = ( } else { nodes.push(...formatNodesFromLinksWithoutCategory(projectNavLinks)); } - if (nodes.length === 0) { - return undefined; - } - return nodes as NodeDefinition[]; + return nodes; }; const formatNodesFromLinksWithCategory = ( @@ -137,7 +82,8 @@ const formatNodesFromLinksWithCategory = ( if (!category?.linkIds) { return []; } - if (isTitleLinkCategory(category)) { + + if (category.linkIds) { const children = category.linkIds.reduce((acc, linkId) => { const projectNavLink = projectNavLinks.find(({ id }) => id === linkId); if (projectNavLink != null) { @@ -145,48 +91,135 @@ const formatNodesFromLinksWithCategory = ( } return acc; }, []); - if (children.length === 0) { + if (!children.length) { return []; } + + const id = isTitleLinkCategory(category) ? getCategoryIdFromLabel(category.label) : undefined; + return [ { - id: `category-${category.label.toLowerCase().replace(' ', '_')}`, - title: category.label, - children: children as NodeDefinition[], + id, + ...(isTitleLinkCategory(category) && { title: category.label }), + breadcrumbStatus: 'hidden', + children, }, ]; - } else if (isSeparatorLinkCategory(category)) { - // TODO: Add separator support when implemented in the shared-ux navigation - const categoryProjectNavLinks = category.linkIds.reduce( - (acc, linkId) => { - const projectNavLink = projectNavLinks.find(({ id }) => id === linkId); - if (projectNavLink != null) { - acc.push(projectNavLink); - } - return acc; - }, - [] - ); - return formatNodesFromLinksWithoutCategory(categoryProjectNavLinks); } return []; }; -const formatNodesFromLinksWithoutCategory = (projectNavLinks: ProjectNavigationLink[]) => - projectNavLinks.map((projectNavLink) => - createNodeFromProjectNavLink(projectNavLink) - ) as NodeDefinition[]; +const formatNodesFromLinksWithoutCategory = ( + projectNavLinks: ProjectNavigationLink[] +): NodeDefinition[] => + projectNavLinks.map((projectNavLink) => createNodeFromProjectNavLink(projectNavLink)); const createNodeFromProjectNavLink = (projectNavLink: ProjectNavigationLink): NodeDefinition => { - const { id, title, links, categories } = projectNavLink; + const { id, title, links, categories, disabled } = projectNavLink; const link = getNavLinkIdFromProjectPageName(id); const node: NodeDefinition = { + id, link: link as AppDeepLinkId, title, ...(isBreadcrumbHidden(id) && { breadcrumbStatus: 'hidden' }), + ...(disabled && { sideNavStatus: 'hidden' }), }; if (links?.length) { node.children = formatNodesFromLinks(links, categories); } return node; }; + +// Footer + +const formatFooterNodesFromLinks = ( + projectNavLinks: ProjectNavigationLink[], + parentCategories?: Readonly>> +): RootNavigationItemDefinition[] => { + const nodes: RootNavigationItemDefinition[] = []; + if (parentCategories?.length) { + parentCategories.forEach((category) => { + if (isSeparatorLinkCategory(category)) { + nodes.push( + ...category.linkIds.reduce((acc, linkId) => { + const projectNavLink = projectNavLinks.find(({ id }) => id === linkId); + if (projectNavLink != null) { + acc.push({ + type: 'navItem', + link: getNavLinkIdFromProjectPageName(projectNavLink.id) as AppDeepLinkId, + title: projectNavLink.title, + icon: projectNavLink.sideNavIcon, + }); + } + return acc; + }, []) + ); + } + if (isAccordionLinkCategory(category)) { + nodes.push({ + type: 'navGroup', + id: getCategoryIdFromLabel(category.label), + title: category.label, + icon: category.iconType, + breadcrumbStatus: 'hidden', + defaultIsCollapsed: true, + children: + category.linkIds?.reduce((acc, linkId) => { + const projectNavLink = projectNavLinks.find(({ id }) => id === linkId); + if (projectNavLink != null) { + acc.push({ + title: projectNavLink.title, + ...(isCloudLink(projectNavLink.id) + ? { + cloudLink: getCloudLink(projectNavLink.id), + openInNewTab: true, + } + : { + link: getNavLinkIdFromProjectPageName(projectNavLink.id) as AppDeepLinkId, + }), + }); + } + return acc; + }, []) ?? [], + }); + } + }, []); + } + return nodes; +}; + +// Utils + +const getCategoryIdFromLabel = (label: string): string => + `category-${label.toLowerCase().replace(' ', '_')}`; + +/** + * Adds the `renderAs: 'panelOpener'` prop to the main links that have children + * This function expects all main links to be in nested groups to add the separation between them. + * If these "separator" groups change this function will need to be updated. + */ +const addMainLinksPanelOpenerProp = (nodes: NodeDefinition[]): NodeDefinition[] => + nodes.map((node): NodeDefinition => { + if (node.children?.length) { + return { + ...node, + children: node.children.map((child) => ({ + ...child, + ...(child.children && { renderAs: 'panelOpener' }), + })), + }; + } + return node; + }); + +/** Returns the cloud link entry the default navigation expects */ +const getCloudLink = (id: ProjectPageName) => { + switch (id) { + case ExternalPageName.cloudUsersAndRoles: + return 'userAndRoles'; + case ExternalPageName.cloudBilling: + return 'billingAndSub'; + default: + return undefined; + } +}; diff --git a/x-pack/plugins/security_solution_serverless/public/navigation/project_navigation/index.tsx b/x-pack/plugins/security_solution_serverless/public/navigation/project_navigation/index.tsx new file mode 100644 index 0000000000000..53d64a88af6d6 --- /dev/null +++ b/x-pack/plugins/security_solution_serverless/public/navigation/project_navigation/index.tsx @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { Suspense } from 'react'; +import { EuiLoadingSpinner } from '@elastic/eui'; + +const SecuritySideNavComponentLazy = React.lazy(() => import('./project_navigation')); + +export const SecuritySideNavComponent = () => ( + }> + + +); diff --git a/x-pack/plugins/security_solution_serverless/public/navigation/project_navigation/project_navigation.tsx b/x-pack/plugins/security_solution_serverless/public/navigation/project_navigation/project_navigation.tsx new file mode 100644 index 0000000000000..b26700eb8e4b3 --- /dev/null +++ b/x-pack/plugins/security_solution_serverless/public/navigation/project_navigation/project_navigation.tsx @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React, { useCallback, useMemo } from 'react'; +import { DefaultNavigation, NavigationKibanaProvider } from '@kbn/shared-ux-chrome-navigation'; +import type { + ContentProvider, + PanelComponentProps, +} from '@kbn/shared-ux-chrome-navigation/src/ui/components/panel/types'; +import { SolutionSideNavPanelContent } from '@kbn/security-solution-side-nav/panel'; +import useObservable from 'react-use/lib/useObservable'; +import { useKibana } from '../../common/services'; +import type { ProjectNavigationLink, ProjectPageName } from '../links/types'; +import { useFormattedSideNavItems } from '../side_navigation/use_side_nav_items'; +import { CATEGORIES, FOOTER_CATEGORIES } from '../categories'; +import { formatNavigationTree } from '../navigation_tree/navigation_tree'; + +const getPanelContentProvider = ( + projectNavLinks: ProjectNavigationLink[] +): React.FC => + React.memo(function PanelContentProvider({ selectedNode: { path }, closePanel }) { + const linkId = path[path.length - 1] as ProjectPageName; + const currentPanelItem = projectNavLinks.find((item) => item.id === linkId); + + const { title = '', links = [], categories } = currentPanelItem ?? {}; + const items = useFormattedSideNavItems(links); + + if (items.length === 0) { + return null; + } + return ( + + ); + }); + +const usePanelContentProvider = (projectNavLinks: ProjectNavigationLink[]): ContentProvider => { + return useCallback( + () => ({ + content: getPanelContentProvider(projectNavLinks), + }), + [projectNavLinks] + ); +}; + +export const SecuritySideNavComponent = React.memo(function SecuritySideNavComponent() { + const services = useKibana().services; + const projectNavLinks = useObservable(services.getProjectNavLinks$(), []); + + const navigationTree = useMemo( + () => formatNavigationTree(projectNavLinks, CATEGORIES, FOOTER_CATEGORIES), + [projectNavLinks] + ); + + const panelContentProvider = usePanelContentProvider(projectNavLinks); + + return ( + + + + ); +}); + +// eslint-disable-next-line import/no-default-export +export default SecuritySideNavComponent; diff --git a/x-pack/plugins/security_solution_serverless/public/navigation/side_navigation/side_navigation.tsx b/x-pack/plugins/security_solution_serverless/public/navigation/side_navigation/side_navigation.tsx index d56e98eef3c2d..3d87403b0ff3c 100644 --- a/x-pack/plugins/security_solution_serverless/public/navigation/side_navigation/side_navigation.tsx +++ b/x-pack/plugins/security_solution_serverless/public/navigation/side_navigation/side_navigation.tsx @@ -20,7 +20,7 @@ import { useObservable } from 'react-use'; import { css } from '@emotion/react'; import { partition } from 'lodash/fp'; import { useSideNavItems } from './use_side_nav_items'; -import { CATEGORIES } from './categories'; +import { CATEGORIES, FOOTER_CATEGORIES } from '../categories'; import { getProjectPageNameFromNavLinkId } from '../links/util'; import { useKibana } from '../../common/services'; import { SideNavigationFooter } from './side_navigation_footer'; @@ -39,12 +39,7 @@ export const SecuritySideNavigation: SideNavComponent = React.memo(function Secu const { chrome } = useKibana().services; const { euiTheme } = useEuiTheme(); const hasHeaderBanner = useObservable(chrome.hasHeaderBanner$()); - - /** - * TODO: Uncomment this when we have the getIsSideNavCollapsed API available - * const isCollapsed = useObservable(chrome.getIsSideNavCollapsed$()); - */ - const isCollapsed = false; + const isCollapsed = useObservable(chrome.getIsSideNavCollapsed$()); const items = useSideNavItems(); @@ -70,7 +65,7 @@ export const SecuritySideNavigation: SideNavComponent = React.memo(function Secu padding-right: ${euiTheme.size.s}; `; - const collapsedNavItems = useMemo(() => { + const collapsedBodyItems = useMemo(() => { return CATEGORIES.reduce((links, category) => { const categoryLinks = items.filter((item) => category.linkIds.includes(item.id)); links.push(...categoryLinks.map((link) => getEuiNavItemFromSideNavItem(link, selectedId))); @@ -93,7 +88,7 @@ export const SecuritySideNavigation: SideNavComponent = React.memo(function Secu icon="logoSecurity" iconProps={{ size: 'm' }} data-test-subj="securitySolutionNavHeading" - items={isCollapsed ? collapsedNavItems : undefined} + items={isCollapsed ? collapsedBodyItems : undefined} /> {!isCollapsed && (
@@ -107,7 +102,11 @@ export const SecuritySideNavigation: SideNavComponent = React.memo(function Secu )} - + ); diff --git a/x-pack/plugins/security_solution_serverless/public/navigation/side_navigation/side_navigation_footer.test.tsx b/x-pack/plugins/security_solution_serverless/public/navigation/side_navigation/side_navigation_footer.test.tsx index 02e4979c1fba2..fdfd3216d606d 100644 --- a/x-pack/plugins/security_solution_serverless/public/navigation/side_navigation/side_navigation_footer.test.tsx +++ b/x-pack/plugins/security_solution_serverless/public/navigation/side_navigation/side_navigation_footer.test.tsx @@ -12,6 +12,7 @@ import { SideNavigationFooter } from './side_navigation_footer'; import { ExternalPageName } from '../links/constants'; import { I18nProvider } from '@kbn/i18n-react'; import type { ProjectSideNavItem } from './types'; +import { FOOTER_CATEGORIES } from '../categories'; jest.mock('../../common/services'); @@ -54,9 +55,12 @@ describe('SideNavigationFooter', () => { }); it('should render all the items', () => { - const component = render(, { - wrapper: I18nProvider, - }); + const component = render( + , + { + wrapper: I18nProvider, + } + ); items.forEach((item) => { expect(component.queryByTestId(`solutionSideNavItemLink-${item.id}`)).toBeInTheDocument(); @@ -64,9 +68,16 @@ describe('SideNavigationFooter', () => { }); it('should highlight the active node', () => { - const component = render(, { - wrapper: I18nProvider, - }); + const component = render( + , + { + wrapper: I18nProvider, + } + ); items.forEach((item) => { const isSelected = component @@ -82,9 +93,16 @@ describe('SideNavigationFooter', () => { }); it('should highlight the active node inside the collapsible', () => { - const component = render(, { - wrapper: I18nProvider, - }); + const component = render( + , + { + wrapper: I18nProvider, + } + ); items.forEach((item) => { const isSelected = component @@ -100,9 +118,12 @@ describe('SideNavigationFooter', () => { }); it('should render closed collapsible if it has no active node', () => { - const component = render(, { - wrapper: I18nProvider, - }); + const component = render( + , + { + wrapper: I18nProvider, + } + ); const isOpen = component .queryByTestId('navFooterCollapsible-project-settings') @@ -112,9 +133,16 @@ describe('SideNavigationFooter', () => { }); it('should open collapsible if it has an active node', () => { - const component = render(, { - wrapper: I18nProvider, - }); + const component = render( + , + { + wrapper: I18nProvider, + } + ); const isOpen = component .queryByTestId('navFooterCollapsible-project-settings') diff --git a/x-pack/plugins/security_solution_serverless/public/navigation/side_navigation/side_navigation_footer.tsx b/x-pack/plugins/security_solution_serverless/public/navigation/side_navigation/side_navigation_footer.tsx index 2c8cf2369c50b..0ed8c1e80f256 100644 --- a/x-pack/plugins/security_solution_serverless/public/navigation/side_navigation/side_navigation_footer.tsx +++ b/x-pack/plugins/security_solution_serverless/public/navigation/side_navigation/side_navigation_footer.tsx @@ -8,50 +8,32 @@ import React, { useEffect, useMemo, useState } from 'react'; import type { EuiCollapsibleNavSubItemProps, IconType } from '@elastic/eui'; import { EuiCollapsibleNavItem } from '@elastic/eui'; -import { SecurityPageName } from '@kbn/security-solution-navigation'; -import { ExternalPageName } from '../links/constants'; +import { + isAccordionLinkCategory, + isSeparatorLinkCategory, + type LinkCategory, +} from '@kbn/security-solution-navigation'; import { getNavLinkIdFromProjectPageName } from '../links/util'; import type { ProjectSideNavItem } from './types'; -interface FooterCategory { - type: 'standalone' | 'collapsible'; - title?: string; - icon?: IconType; - linkIds: string[]; -} - -const categories: FooterCategory[] = [ - { type: 'standalone', linkIds: [SecurityPageName.landing, ExternalPageName.devTools] }, - { - type: 'collapsible', - title: 'Project Settings', - icon: 'gear', - linkIds: [ - ExternalPageName.management, - ExternalPageName.integrationsSecurity, - ExternalPageName.cloudUsersAndRoles, - ExternalPageName.cloudPerformance, - ExternalPageName.cloudBilling, - ], - }, -]; - export const SideNavigationFooter: React.FC<{ activeNodeId: string; items: ProjectSideNavItem[]; -}> = ({ activeNodeId, items }) => { + categories: LinkCategory[]; +}> = ({ activeNodeId, items, categories }) => { return ( <> {categories.map((category, index) => { - const categoryItems = category.linkIds.reduce((acc, linkId) => { - const item = items.find(({ id }) => id === linkId); - if (item) { - acc.push(item); - } - return acc; - }, []); + const categoryItems = + category.linkIds?.reduce((acc, linkId) => { + const item = items.find(({ id }) => id === linkId); + if (item) { + acc.push(item); + } + return acc; + }, []) ?? []; - if (category.type === 'standalone') { + if (isSeparatorLinkCategory(category)) { return ( ); } - if (category.type === 'collapsible') { + if (isAccordionLinkCategory(category)) { return ( ); } diff --git a/x-pack/plugins/security_solution_serverless/public/navigation/side_navigation/use_side_nav_items.ts b/x-pack/plugins/security_solution_serverless/public/navigation/side_navigation/use_side_nav_items.ts index 9b61439712221..32945e89765e1 100644 --- a/x-pack/plugins/security_solution_serverless/public/navigation/side_navigation/use_side_nav_items.ts +++ b/x-pack/plugins/security_solution_serverless/public/navigation/side_navigation/use_side_nav_items.ts @@ -6,27 +6,18 @@ */ import { useCallback, useMemo } from 'react'; -import { SecurityPageName, type NavigationLink } from '@kbn/security-solution-navigation'; +import { type NavigationLink } from '@kbn/security-solution-navigation'; import { useGetLinkProps } from '@kbn/security-solution-navigation/links'; import { SolutionSideNavItemPosition } from '@kbn/security-solution-side-nav'; import { useNavLinks } from '../../common/hooks/use_nav_links'; -import { ExternalPageName } from '../links/constants'; import type { ProjectSideNavItem } from './types'; -import type { ProjectPageName } from '../links/types'; +import type { ProjectNavigationLink, ProjectPageName } from '../links/types'; +import { isBottomNavItemId } from '../links/util'; type GetLinkProps = (link: NavigationLink) => { href: string & Partial; }; -const isBottomNavItem = (id: string) => - id === SecurityPageName.landing || - id === ExternalPageName.devTools || - id === ExternalPageName.management || - id === ExternalPageName.integrationsSecurity || - id === ExternalPageName.cloudUsersAndRoles || - id === ExternalPageName.cloudPerformance || - id === ExternalPageName.cloudBilling; - /** * Formats generic navigation links into the shape expected by the `SolutionSideNav` */ @@ -52,7 +43,7 @@ const formatLink = ( id: navLink.id, label: navLink.title, iconType: navLink.sideNavIcon, - position: isBottomNavItem(navLink.id) + position: isBottomNavItemId(navLink.id) ? SolutionSideNavItemPosition.bottom : SolutionSideNavItemPosition.top, ...getLinkProps(navLink), @@ -66,6 +57,15 @@ const formatLink = ( */ export const useSideNavItems = (): ProjectSideNavItem[] => { const navLinks = useNavLinks(); + return useFormattedSideNavItems(navLinks); +}; + +/** + * Returns all the formatted SideNavItems, including external links + */ +export const useFormattedSideNavItems = ( + navLinks: ProjectNavigationLink[] +): ProjectSideNavItem[] => { const getKibanaLinkProps = useGetLinkProps(); const getLinkProps = useCallback( From 2146a7ef1634a06cf6b4b70edc55dd22fd5b6a4c Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Mon, 23 Oct 2023 12:21:25 +0300 Subject: [PATCH 04/11] [Cases] Unskip MKI tests (#168924) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../kbn_client/kbn_client_saved_objects.ts | 5 +++ .../test/functional/services/cases/common.ts | 1 - .../apps/cases/group1/view_case.ts | 21 ++++++----- .../apps/cases/group2/attachment_framework.ts | 5 +++ .../api_integration/services/svl_cases/api.ts | 4 +-- .../observability/cases/get_case.ts | 2 +- .../observability/cases/post_case.ts | 2 +- .../test_suites/security/cases/get_case.ts | 2 +- .../test_suites/security/cases/post_case.ts | 2 +- .../cases/attachment_framework.ts | 17 +++++---- .../observability/cases/configure.ts | 5 ++- .../observability/cases/create_case_form.ts | 5 ++- .../test_suites/observability/cases/index.ts | 18 ++++++++++ .../observability/cases/list_view.ts | 12 +++---- .../observability/cases/view_case.ts | 31 ++++++++-------- .../test_suites/observability/index.ts | 6 +--- .../ftr/cases/attachment_framework.ts | 35 +++++++++---------- .../security/ftr/cases/configure.ts | 5 ++- .../security/ftr/cases/create_case_form.ts | 4 +-- .../test_suites/security/ftr/cases/index.ts | 18 ++++++++++ .../security/ftr/cases/list_view.ts | 14 ++++---- .../security/ftr/cases/view_case.ts | 31 ++++++++-------- .../functional/test_suites/security/index.ts | 6 +--- .../shared/lib/cases/helpers.ts | 8 ++--- 24 files changed, 149 insertions(+), 110 deletions(-) create mode 100644 x-pack/test_serverless/functional/test_suites/observability/cases/index.ts create mode 100644 x-pack/test_serverless/functional/test_suites/security/ftr/cases/index.ts diff --git a/packages/kbn-test/src/kbn_client/kbn_client_saved_objects.ts b/packages/kbn-test/src/kbn_client/kbn_client_saved_objects.ts index 63e60a0eb19e4..caaf072c934ca 100644 --- a/packages/kbn-test/src/kbn_client/kbn_client_saved_objects.ts +++ b/packages/kbn-test/src/kbn_client/kbn_client_saved_objects.ts @@ -98,7 +98,12 @@ const STANDARD_LIST_TYPES = [ 'lens', 'links', 'map', + // cases saved objects 'cases', + 'cases-comments', + 'cases-user-actions', + 'cases-configure', + 'cases-connector-mappings', // synthetics based objects 'synthetics-monitor', 'uptime-dynamic-settings', diff --git a/x-pack/test/functional/services/cases/common.ts b/x-pack/test/functional/services/cases/common.ts index 4fe8523a6bb87..4809fe4278814 100644 --- a/x-pack/test/functional/services/cases/common.ts +++ b/x-pack/test/functional/services/cases/common.ts @@ -96,7 +96,6 @@ export function CasesCommonServiceProvider({ getService, getPageObject }: FtrPro async expectToasterToContain(content: string) { const toast = await toasts.getToastElement(1); expect(await toast.getVisibleText()).to.contain(content); - await toasts.dismissAllToasts(); }, async assertCaseModalVisible(expectVisible = true) { diff --git a/x-pack/test/functional_with_es_ssl/apps/cases/group1/view_case.ts b/x-pack/test/functional_with_es_ssl/apps/cases/group1/view_case.ts index 9391c9f1e77db..77ceb4b1a2117 100644 --- a/x-pack/test/functional_with_es_ssl/apps/cases/group1/view_case.ts +++ b/x-pack/test/functional_with_es_ssl/apps/cases/group1/view_case.ts @@ -1154,8 +1154,7 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { }); }); - // FLAKY: https://github.com/elastic/kibana/issues/168534 - describe.skip('customFields', () => { + describe('customFields', () => { const customFields = [ { key: 'valid_key_1', @@ -1198,13 +1197,13 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { }); it('updates a custom field correctly', async () => { - const summary = await testSubjects.find(`case-text-custom-field-${customFields[0].key}`); - expect(await summary.getVisibleText()).equal('this is a text field value'); + const textField = await testSubjects.find(`case-text-custom-field-${customFields[0].key}`); + expect(await textField.getVisibleText()).equal('this is a text field value'); - const sync = await testSubjects.find( + const toggle = await testSubjects.find( `case-toggle-custom-field-form-field-${customFields[1].key}` ); - expect(await sync.getAttribute('aria-checked')).equal('true'); + expect(await toggle.getAttribute('aria-checked')).equal('true'); await testSubjects.click(`case-text-custom-field-edit-button-${customFields[0].key}`); @@ -1222,19 +1221,23 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { await testSubjects.click(`case-text-custom-field-submit-button-${customFields[0].key}`); + await header.waitUntilLoadingHasFinished(); + await retry.waitFor('update toast exist', async () => { return await testSubjects.exists('toastCloseButton'); }); await testSubjects.click('toastCloseButton'); - await sync.click(); + await header.waitUntilLoadingHasFinished(); + + await toggle.click(); await header.waitUntilLoadingHasFinished(); - expect(await summary.getVisibleText()).equal('this is a text field value edited!!'); + expect(await textField.getVisibleText()).equal('this is a text field value edited!!'); - expect(await sync.getAttribute('aria-checked')).equal('false'); + expect(await toggle.getAttribute('aria-checked')).equal('false'); // validate user action const userActions = await find.allByCssSelector( diff --git a/x-pack/test/functional_with_es_ssl/apps/cases/group2/attachment_framework.ts b/x-pack/test/functional_with_es_ssl/apps/cases/group2/attachment_framework.ts index ca9c4ad3af49a..5ac08acfbc6ed 100644 --- a/x-pack/test/functional_with_es_ssl/apps/cases/group2/attachment_framework.ts +++ b/x-pack/test/functional_with_es_ssl/apps/cases/group2/attachment_framework.ts @@ -61,6 +61,7 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { const dashboard = getPageObject('dashboard'); const lens = getPageObject('lens'); const listingTable = getService('listingTable'); + const toasts = getService('toasts'); const createAttachmentAndNavigate = async (attachment: AttachmentRequest) => { const caseData = await cases.api.createCase({ @@ -249,6 +250,7 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { */ await cases.create.createCase({ owner }); await cases.common.expectToasterToContain('has been updated'); + await toasts.dismissAllToastsWithChecks(); } const casesCreatedFromFlyout = await findCases({ supertest }); @@ -325,6 +327,7 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { await testSubjects.click(`cases-table-row-select-${currentCaseId}`); await cases.common.expectToasterToContain('has been updated'); + await toasts.dismissAllToastsWithChecks(); await ensureFirstCommentOwner(currentCaseId, owner); } }); @@ -387,6 +390,7 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { await cases.common.expectToasterToContain(`${caseTitle} has been updated`); await testSubjects.click('toaster-content-case-view-link'); + await toasts.dismissAllToastsWithChecks(); const title = await find.byCssSelector('[data-test-subj="editable-title-header-value"]'); expect(await title.getVisibleText()).toEqual(caseTitle); @@ -414,6 +418,7 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { await cases.common.expectToasterToContain(`${theCaseTitle} has been updated`); await testSubjects.click('toaster-content-case-view-link'); + await toasts.dismissAllToastsWithChecks(); const title = await find.byCssSelector('[data-test-subj="editable-title-header-value"]'); expect(await title.getVisibleText()).toEqual(theCaseTitle); diff --git a/x-pack/test_serverless/api_integration/services/svl_cases/api.ts b/x-pack/test_serverless/api_integration/services/svl_cases/api.ts index a504b71240dd5..8f23eba1ea981 100644 --- a/x-pack/test_serverless/api_integration/services/svl_cases/api.ts +++ b/x-pack/test_serverless/api_integration/services/svl_cases/api.ts @@ -79,7 +79,7 @@ export function SvlCasesApiServiceProvider({ getService }: FtrProviderContext) { async deleteAllCaseItems() { await Promise.all([ - this.deleteCasesByESQuery(), + this.deleteCases(), this.deleteCasesUserActions(), this.deleteComments(), this.deleteConfiguration(), @@ -91,7 +91,7 @@ export function SvlCasesApiServiceProvider({ getService }: FtrProviderContext) { await kbnServer.savedObjects.clean({ types: ['cases-user-actions'] }); }, - async deleteCasesByESQuery(): Promise { + async deleteCases(): Promise { await kbnServer.savedObjects.clean({ types: ['cases'] }); }, diff --git a/x-pack/test_serverless/api_integration/test_suites/observability/cases/get_case.ts b/x-pack/test_serverless/api_integration/test_suites/observability/cases/get_case.ts index d1f601f709af2..30fbe518085cb 100644 --- a/x-pack/test_serverless/api_integration/test_suites/observability/cases/get_case.ts +++ b/x-pack/test_serverless/api_integration/test_suites/observability/cases/get_case.ts @@ -14,7 +14,7 @@ export default ({ getService }: FtrProviderContext): void => { describe('get_case', () => { afterEach(async () => { - await svlCases.api.deleteCasesByESQuery(); + await svlCases.api.deleteCases(); }); it('should return a case', async () => { diff --git a/x-pack/test_serverless/api_integration/test_suites/observability/cases/post_case.ts b/x-pack/test_serverless/api_integration/test_suites/observability/cases/post_case.ts index 01cf60d424f90..79f180a5092ff 100644 --- a/x-pack/test_serverless/api_integration/test_suites/observability/cases/post_case.ts +++ b/x-pack/test_serverless/api_integration/test_suites/observability/cases/post_case.ts @@ -15,7 +15,7 @@ export default ({ getService }: FtrProviderContext): void => { describe('post_case', () => { afterEach(async () => { - await svlCases.api.deleteCasesByESQuery(); + await svlCases.api.deleteCases(); }); it('should create a case', async () => { diff --git a/x-pack/test_serverless/api_integration/test_suites/security/cases/get_case.ts b/x-pack/test_serverless/api_integration/test_suites/security/cases/get_case.ts index 719841ff28ab5..fe3c02a7342a9 100644 --- a/x-pack/test_serverless/api_integration/test_suites/security/cases/get_case.ts +++ b/x-pack/test_serverless/api_integration/test_suites/security/cases/get_case.ts @@ -13,7 +13,7 @@ export default ({ getService }: FtrProviderContext): void => { describe('get_case', () => { afterEach(async () => { - await svlCases.api.deleteCasesByESQuery(); + await svlCases.api.deleteCases(); }); it('should return a case', async () => { diff --git a/x-pack/test_serverless/api_integration/test_suites/security/cases/post_case.ts b/x-pack/test_serverless/api_integration/test_suites/security/cases/post_case.ts index 77917915d4bcc..db5967654de85 100644 --- a/x-pack/test_serverless/api_integration/test_suites/security/cases/post_case.ts +++ b/x-pack/test_serverless/api_integration/test_suites/security/cases/post_case.ts @@ -15,7 +15,7 @@ export default ({ getService }: FtrProviderContext): void => { describe('post_case', () => { afterEach(async () => { - await svlCases.api.deleteCasesByESQuery(); + await svlCases.api.deleteCases(); }); it('should create a case', async () => { diff --git a/x-pack/test_serverless/functional/test_suites/observability/cases/attachment_framework.ts b/x-pack/test_serverless/functional/test_suites/observability/cases/attachment_framework.ts index cd0647a1a35d7..e1a13b456c0f3 100644 --- a/x-pack/test_serverless/functional/test_suites/observability/cases/attachment_framework.ts +++ b/x-pack/test_serverless/functional/test_suites/observability/cases/attachment_framework.ts @@ -18,39 +18,37 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { const esArchiver = getService('esArchiver'); const kibanaServer = getService('kibanaServer'); const cases = getService('cases'); + const svlCases = getService('svlCases'); const find = getService('find'); + const toasts = getService('toasts'); - // failing test https://github.com/elastic/kibana/issues/166592 - describe.skip('Cases persistable attachments', function () { - // security_exception: action [indices:data/write/delete/byquery] is unauthorized for user [elastic] with effective roles [superuser] on restricted indices [.kibana_alerting_cases], this action is granted by the index privileges [delete,write,all] - this.tags(['failsOnMKI']); + describe('Cases persistable attachments', function () { describe('lens visualization', () => { before(async () => { await svlCommonPage.login(); + await kibanaServer.savedObjects.cleanStandardList(); await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/logstash_functional'); await kibanaServer.importExport.load( 'x-pack/test/functional/fixtures/kbn_archiver/lens/lens_basic.json' ); await svlObltNavigation.navigateToLandingPage(); - await svlCommonNavigation.sidenav.clickLink({ deepLinkId: 'dashboards' }); await dashboard.clickNewDashboard(); - await lens.createAndAddLensFromDashboard({}); - await dashboard.waitForRenderComplete(); }); after(async () => { - await cases.api.deleteAllCases(); + await svlCases.api.deleteAllCaseItems(); await esArchiver.unload('x-pack/test/functional/es_archives/logstash_functional'); await kibanaServer.importExport.unload( 'x-pack/test/functional/fixtures/kbn_archiver/lens/lens_basic.json' ); + await kibanaServer.savedObjects.cleanStandardList(); await svlCommonPage.forceLogout(); }); @@ -73,8 +71,8 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { await testSubjects.click('create-case-submit'); await cases.common.expectToasterToContain(`${caseTitle} has been updated`); - await testSubjects.click('toaster-content-case-view-link'); + await toasts.dismissAllToastsWithChecks(); if (await testSubjects.exists('appLeaveConfirmModal')) { await testSubjects.exists('confirmModalConfirmButton'); @@ -108,6 +106,7 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { await cases.common.expectToasterToContain(`${theCaseTitle} has been updated`); await testSubjects.click('toaster-content-case-view-link'); + await toasts.dismissAllToastsWithChecks(); if (await testSubjects.exists('appLeaveConfirmModal')) { await testSubjects.exists('confirmModalConfirmButton'); diff --git a/x-pack/test_serverless/functional/test_suites/observability/cases/configure.ts b/x-pack/test_serverless/functional/test_suites/observability/cases/configure.ts index f63ce94ead3fe..79339d0dc345b 100644 --- a/x-pack/test_serverless/functional/test_suites/observability/cases/configure.ts +++ b/x-pack/test_serverless/functional/test_suites/observability/cases/configure.ts @@ -16,13 +16,12 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { const svlObltNavigation = getService('svlObltNavigation'); const testSubjects = getService('testSubjects'); const cases = getService('cases'); + const svlCases = getService('svlCases'); const toasts = getService('toasts'); const retry = getService('retry'); const find = getService('find'); describe('Configure Case', function () { - // security_exception: action [indices:data/write/delete/byquery] is unauthorized for user [elastic] with effective roles [superuser] on restricted indices [.kibana_alerting_cases], this action is granted by the index privileges [delete,write,all] - this.tags(['failsOnMKI']); before(async () => { await svlCommonPage.login(); await svlObltNavigation.navigateToLandingPage(); @@ -42,7 +41,7 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { }); after(async () => { - await cases.api.deleteAllCases(); + await svlCases.api.deleteAllCaseItems(); await svlCommonPage.forceLogout(); }); diff --git a/x-pack/test_serverless/functional/test_suites/observability/cases/create_case_form.ts b/x-pack/test_serverless/functional/test_suites/observability/cases/create_case_form.ts index 58fb83ce2c877..9bc03b8e232b8 100644 --- a/x-pack/test_serverless/functional/test_suites/observability/cases/create_case_form.ts +++ b/x-pack/test_serverless/functional/test_suites/observability/cases/create_case_form.ts @@ -16,10 +16,9 @@ const owner = OBSERVABILITY_OWNER; export default ({ getService, getPageObject }: FtrProviderContext) => { describe('Create Case', function () { - // security_exception: action [indices:data/write/delete/byquery] is unauthorized for user [elastic] with effective roles [superuser] on restricted indices [.kibana_alerting_cases], this action is granted by the index privileges [delete,write,all] - this.tags(['failsOnMKI']); const find = getService('find'); const cases = getService('cases'); + const svlCases = getService('svlCases'); const testSubjects = getService('testSubjects'); const svlCommonPage = getPageObject('svlCommonPage'); const config = getService('config'); @@ -35,7 +34,7 @@ export default ({ getService, getPageObject }: FtrProviderContext) => { }); after(async () => { - await cases.api.deleteAllCases(); + await svlCases.api.deleteAllCaseItems(); await svlCommonPage.forceLogout(); }); diff --git a/x-pack/test_serverless/functional/test_suites/observability/cases/index.ts b/x-pack/test_serverless/functional/test_suites/observability/cases/index.ts new file mode 100644 index 0000000000000..801166c562b45 --- /dev/null +++ b/x-pack/test_serverless/functional/test_suites/observability/cases/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('Serverless Observability Cases', function () { + loadTestFile(require.resolve('./attachment_framework')); + loadTestFile(require.resolve('./view_case')); + loadTestFile(require.resolve('./configure')); + loadTestFile(require.resolve('./create_case_form')); + loadTestFile(require.resolve('./list_view')); + }); +} diff --git a/x-pack/test_serverless/functional/test_suites/observability/cases/list_view.ts b/x-pack/test_serverless/functional/test_suites/observability/cases/list_view.ts index 30fd021e5c6f5..b001adb306a44 100644 --- a/x-pack/test_serverless/functional/test_suites/observability/cases/list_view.ts +++ b/x-pack/test_serverless/functional/test_suites/observability/cases/list_view.ts @@ -14,13 +14,12 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { const header = getPageObject('header'); const testSubjects = getService('testSubjects'); const cases = getService('cases'); + const svlCases = getService('svlCases'); const svlCommonNavigation = getPageObject('svlCommonNavigation'); const svlCommonPage = getPageObject('svlCommonPage'); const svlObltNavigation = getService('svlObltNavigation'); describe('Cases list', function () { - // multiple errors in after hook due to delete permission - this.tags(['failsOnMKI']); before(async () => { await svlCommonPage.login(); await svlObltNavigation.navigateToLandingPage(); @@ -28,7 +27,7 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { }); after(async () => { - await cases.api.deleteAllCases(); + await svlCases.api.deleteAllCaseItems(); await cases.casesTable.waitForCasesToBeDeleted(); await svlCommonPage.forceLogout(); }); @@ -107,7 +106,7 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { }); afterEach(async () => { - await cases.api.deleteAllCases(); + await svlCases.api.deleteAllCaseItems(); await cases.casesTable.waitForCasesToBeDeleted(); }); @@ -170,7 +169,7 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { }); after(async () => { - await cases.api.deleteAllCases(); + await svlCases.api.deleteAllCaseItems(); await cases.casesTable.waitForCasesToBeDeleted(); }); @@ -274,6 +273,7 @@ const createNCasesBeforeDeleteAllAfter = ( getService: FtrProviderContext['getService'] ) => { const cases = getService('cases'); + const svlCases = getService('svlCases'); const header = getPageObject('header'); before(async () => { @@ -283,7 +283,7 @@ const createNCasesBeforeDeleteAllAfter = ( }); after(async () => { - await cases.api.deleteAllCases(); + await svlCases.api.deleteAllCaseItems(); await cases.casesTable.waitForCasesToBeDeleted(); }); }; diff --git a/x-pack/test_serverless/functional/test_suites/observability/cases/view_case.ts b/x-pack/test_serverless/functional/test_suites/observability/cases/view_case.ts index a0fbd090ea97a..c60b7a8ed103c 100644 --- a/x-pack/test_serverless/functional/test_suites/observability/cases/view_case.ts +++ b/x-pack/test_serverless/functional/test_suites/observability/cases/view_case.ts @@ -26,6 +26,7 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { const header = getPageObject('header'); const testSubjects = getService('testSubjects'); const cases = getService('cases'); + const svlCases = getService('svlCases'); const find = getService('find'); const retry = getService('retry'); @@ -34,14 +35,12 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { const svlCommonPage = getPageObject('svlCommonPage'); describe('Case View', function () { - // security_exception: action [indices:data/write/delete/byquery] is unauthorized for user [elastic] with effective roles [superuser] on restricted indices [.kibana_alerting_cases], this action is granted by the index privileges [delete,write,all] - this.tags(['failsOnMKI']); before(async () => { await svlCommonPage.login(); }); after(async () => { - await cases.api.deleteAllCases(); + await svlCases.api.deleteAllCaseItems(); await svlCommonPage.forceLogout(); }); @@ -280,7 +279,7 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { after(async () => { await cases.testResources.removeKibanaSampleData('logs'); - await cases.api.deleteAllCases(); + await svlCases.api.deleteAllCaseItems(); }); it('adds lens visualization in description', async () => { @@ -325,7 +324,7 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { }); after(async () => { - await cases.api.deleteAllCases(); + await svlCases.api.deleteAllCaseItems(); }); it('initially renders user actions list correctly', async () => { @@ -437,7 +436,7 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { }); after(async () => { - await cases.api.deleteAllCases(); + await svlCases.api.deleteAllCaseItems(); }); it('should set the cases title', async () => { @@ -498,17 +497,17 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { }); afterEach(async () => { - await cases.api.deleteAllCases(); + await svlCases.api.deleteAllCaseItems(); }); it('updates a custom field correctly', async () => { - const summary = await testSubjects.find(`case-text-custom-field-${customFields[0].key}`); - expect(await summary.getVisibleText()).equal('this is a text field value'); + const textField = await testSubjects.find(`case-text-custom-field-${customFields[0].key}`); + expect(await textField.getVisibleText()).equal('this is a text field value'); - const sync = await testSubjects.find( + const toggle = await testSubjects.find( `case-toggle-custom-field-form-field-${customFields[1].key}` ); - expect(await sync.getAttribute('aria-checked')).equal('true'); + expect(await toggle.getAttribute('aria-checked')).equal('true'); await testSubjects.click(`case-text-custom-field-edit-button-${customFields[0].key}`); @@ -526,19 +525,23 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { await testSubjects.click(`case-text-custom-field-submit-button-${customFields[0].key}`); + await header.waitUntilLoadingHasFinished(); + await retry.waitFor('update toast exist', async () => { return await testSubjects.exists('toastCloseButton'); }); await testSubjects.click('toastCloseButton'); - await sync.click(); + await header.waitUntilLoadingHasFinished(); + + await toggle.click(); await header.waitUntilLoadingHasFinished(); - expect(await summary.getVisibleText()).equal('this is a text field value edited!!'); + expect(await textField.getVisibleText()).equal('this is a text field value edited!!'); - expect(await sync.getAttribute('aria-checked')).equal('false'); + expect(await toggle.getAttribute('aria-checked')).equal('false'); // validate user action const userActions = await find.allByCssSelector( diff --git a/x-pack/test_serverless/functional/test_suites/observability/index.ts b/x-pack/test_serverless/functional/test_suites/observability/index.ts index 8611ba5c3abbc..3de929288c253 100644 --- a/x-pack/test_serverless/functional/test_suites/observability/index.ts +++ b/x-pack/test_serverless/functional/test_suites/observability/index.ts @@ -12,12 +12,8 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./landing_page')); loadTestFile(require.resolve('./navigation')); loadTestFile(require.resolve('./observability_log_explorer')); - loadTestFile(require.resolve('./cases/attachment_framework')); loadTestFile(require.resolve('./rules/rules_list')); - loadTestFile(require.resolve('./cases/view_case')); - loadTestFile(require.resolve('./cases/configure')); - loadTestFile(require.resolve('./cases/create_case_form')); - loadTestFile(require.resolve('./cases/list_view')); + loadTestFile(require.resolve('./cases')); loadTestFile(require.resolve('./advanced_settings')); loadTestFile(require.resolve('./infra')); loadTestFile(require.resolve('./ml')); diff --git a/x-pack/test_serverless/functional/test_suites/security/ftr/cases/attachment_framework.ts b/x-pack/test_serverless/functional/test_suites/security/ftr/cases/attachment_framework.ts index 6fb8c2ae94e44..24da4464e9fb3 100644 --- a/x-pack/test_serverless/functional/test_suites/security/ftr/cases/attachment_framework.ts +++ b/x-pack/test_serverless/functional/test_suites/security/ftr/cases/attachment_framework.ts @@ -12,40 +12,38 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { const dashboard = getPageObject('dashboard'); const lens = getPageObject('lens'); const svlSecNavigation = getService('svlSecNavigation'); + const svlCommonPage = getPageObject('svlCommonPage'); const testSubjects = getService('testSubjects'); - const esArchiver = getService('esArchiver'); - const kibanaServer = getService('kibanaServer'); const cases = getService('cases'); + const svlCases = getService('svlCases'); const find = getService('find'); + const retry = getService('retry'); + const header = getPageObject('header'); + const toasts = getService('toasts'); - // Failing - // Issue: https://github.com/elastic/kibana/issues/165135 - describe.skip('Cases persistable attachments', () => { + describe('Cases persistable attachments', () => { describe('lens visualization', () => { before(async () => { - await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/logstash_functional'); - await kibanaServer.importExport.load( - 'x-pack/test/functional/fixtures/kbn_archiver/dashboard/feature_controls/security/security.json' - ); - + await svlCommonPage.login(); await svlSecNavigation.navigateToLandingPage(); await testSubjects.click('solutionSideNavItemLink-dashboards'); + await header.waitUntilLoadingHasFinished(); + + await retry.waitFor('createDashboardButton', async () => { + return await testSubjects.exists('createDashboardButton'); + }); await testSubjects.click('createDashboardButton'); + await header.waitUntilLoadingHasFinished(); await lens.createAndAddLensFromDashboard({}); - await dashboard.waitForRenderComplete(); }); after(async () => { - await cases.api.deleteAllCases(); - - await esArchiver.unload('x-pack/test/functional/es_archives/logstash_functional'); - await kibanaServer.importExport.unload( - 'x-pack/test/functional/fixtures/kbn_archiver/lens/lens_basic.json' - ); + await svlCases.api.deleteAllCaseItems(); + await svlCommonPage.forceLogout(); }); it('adds lens visualization to a new case', async () => { @@ -68,8 +66,8 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { await testSubjects.click('create-case-submit'); await cases.common.expectToasterToContain(`${caseTitle} has been updated`); - await testSubjects.click('toaster-content-case-view-link'); + await toasts.dismissAllToastsWithChecks(); if (await testSubjects.exists('appLeaveConfirmModal')) { await testSubjects.exists('confirmModalConfirmButton'); @@ -107,6 +105,7 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { await cases.common.expectToasterToContain(`${theCaseTitle} has been updated`); await testSubjects.click('toaster-content-case-view-link'); + await toasts.dismissAllToastsWithChecks(); if (await testSubjects.exists('appLeaveConfirmModal')) { await testSubjects.exists('confirmModalConfirmButton'); diff --git a/x-pack/test_serverless/functional/test_suites/security/ftr/cases/configure.ts b/x-pack/test_serverless/functional/test_suites/security/ftr/cases/configure.ts index 19cd7a3ccdce0..320f83790d301 100644 --- a/x-pack/test_serverless/functional/test_suites/security/ftr/cases/configure.ts +++ b/x-pack/test_serverless/functional/test_suites/security/ftr/cases/configure.ts @@ -15,13 +15,12 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { const svlSecNavigation = getService('svlSecNavigation'); const testSubjects = getService('testSubjects'); const cases = getService('cases'); + const svlCases = getService('svlCases'); const toasts = getService('toasts'); const retry = getService('retry'); const find = getService('find'); describe('Configure Case', function () { - // security_exception: action [indices:data/write/delete/byquery] is unauthorized for user [elastic] with effective roles [superuser] on restricted indices [.kibana_alerting_cases], this action is granted by the index privileges [delete,write,all] - this.tags(['failsOnMKI']); before(async () => { await svlCommonPage.login(); await svlSecNavigation.navigateToLandingPage(); @@ -41,7 +40,7 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { }); after(async () => { - await cases.api.deleteAllCases(); + await svlCases.api.deleteAllCaseItems(); await svlCommonPage.forceLogout(); }); diff --git a/x-pack/test_serverless/functional/test_suites/security/ftr/cases/create_case_form.ts b/x-pack/test_serverless/functional/test_suites/security/ftr/cases/create_case_form.ts index 9acc6378b620e..b5ef129b467dc 100644 --- a/x-pack/test_serverless/functional/test_suites/security/ftr/cases/create_case_form.ts +++ b/x-pack/test_serverless/functional/test_suites/security/ftr/cases/create_case_form.ts @@ -16,9 +16,9 @@ const owner = SECURITY_SOLUTION_OWNER; export default ({ getService, getPageObject }: FtrProviderContext) => { describe('Create Case', function () { - this.tags(['failsOnMKI']); const find = getService('find'); const cases = getService('cases'); + const svlCases = getService('svlCases'); const testSubjects = getService('testSubjects'); const config = getService('config'); const svlCommonPage = getPageObject('svlCommonPage'); @@ -34,7 +34,7 @@ export default ({ getService, getPageObject }: FtrProviderContext) => { }); after(async () => { - await cases.api.deleteAllCases(); + await svlCases.api.deleteAllCaseItems(); await svlCommonPage.forceLogout(); }); diff --git a/x-pack/test_serverless/functional/test_suites/security/ftr/cases/index.ts b/x-pack/test_serverless/functional/test_suites/security/ftr/cases/index.ts new file mode 100644 index 0000000000000..998c74e23f121 --- /dev/null +++ b/x-pack/test_serverless/functional/test_suites/security/ftr/cases/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('Serverless Security Cases', function () { + loadTestFile(require.resolve('./attachment_framework')); + loadTestFile(require.resolve('./view_case')); + loadTestFile(require.resolve('./create_case_form')); + loadTestFile(require.resolve('./configure')); + loadTestFile(require.resolve('./list_view')); + }); +} diff --git a/x-pack/test_serverless/functional/test_suites/security/ftr/cases/list_view.ts b/x-pack/test_serverless/functional/test_suites/security/ftr/cases/list_view.ts index 045d56b9c5dd8..8a753e5d4829a 100644 --- a/x-pack/test_serverless/functional/test_suites/security/ftr/cases/list_view.ts +++ b/x-pack/test_serverless/functional/test_suites/security/ftr/cases/list_view.ts @@ -14,12 +14,11 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { const header = getPageObject('header'); const testSubjects = getService('testSubjects'); const cases = getService('cases'); + const svlCases = getService('svlCases'); const svlSecNavigation = getService('svlSecNavigation'); const svlCommonPage = getPageObject('svlCommonPage'); describe('Cases List', function () { - // multiple errors in after hook due to delete permission - this.tags(['failsOnMKI']); before(async () => { await svlCommonPage.login(); @@ -29,7 +28,7 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { }); after(async () => { - await cases.api.deleteAllCases(); + await svlCases.api.deleteAllCaseItems(); await cases.casesTable.waitForCasesToBeDeleted(); await svlCommonPage.forceLogout(); }); @@ -49,8 +48,6 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { }); describe('bulk actions', () => { - // security_exception: action [indices:data/write/delete/byquery] is unauthorized for user [elastic] with effective roles [superuser] on restricted indices [.kibana_alerting_cases], this action is granted by the index privileges [delete,write,all] - // action [indices:data/write/delete/byquery] is unauthorized for user [elastic] with effective roles [superuser] on restricted indices [.kibana_alerting_cases], this action is granted by the index privileges [delete,write,all] describe('delete', () => { createNCasesBeforeDeleteAllAfter(8, getPageObject, getService); @@ -110,7 +107,7 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { }); afterEach(async () => { - await cases.api.deleteAllCases(); + await svlCases.api.deleteAllCaseItems(); await cases.casesTable.waitForCasesToBeDeleted(); }); @@ -174,7 +171,7 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { }); after(async () => { - await cases.api.deleteAllCases(); + await svlCases.api.deleteAllCaseItems(); await cases.casesTable.waitForCasesToBeDeleted(); }); @@ -279,6 +276,7 @@ const createNCasesBeforeDeleteAllAfter = ( getService: FtrProviderContext['getService'] ) => { const cases = getService('cases'); + const svlCases = getService('svlCases'); const header = getPageObject('header'); before(async () => { @@ -288,7 +286,7 @@ const createNCasesBeforeDeleteAllAfter = ( }); after(async () => { - await cases.api.deleteAllCases(); + await svlCases.api.deleteAllCaseItems(); await cases.casesTable.waitForCasesToBeDeleted(); }); }; diff --git a/x-pack/test_serverless/functional/test_suites/security/ftr/cases/view_case.ts b/x-pack/test_serverless/functional/test_suites/security/ftr/cases/view_case.ts index 85ed51c191ba5..c3d8285857634 100644 --- a/x-pack/test_serverless/functional/test_suites/security/ftr/cases/view_case.ts +++ b/x-pack/test_serverless/functional/test_suites/security/ftr/cases/view_case.ts @@ -26,6 +26,7 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { const header = getPageObject('header'); const testSubjects = getService('testSubjects'); const cases = getService('cases'); + const svlCases = getService('svlCases'); const find = getService('find'); const retry = getService('retry'); @@ -34,14 +35,12 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { const svlCommonPage = getPageObject('svlCommonPage'); describe('Case View', function () { - // security_exception: action [indices:data/write/delete/byquery] is unauthorized for user [elastic] with effective roles [superuser] on restricted indices [.kibana_alerting_cases], this action is granted by the index privileges [delete,write,all] - this.tags(['failsOnMKI']); before(async () => { await svlCommonPage.login(); }); after(async () => { - await cases.api.deleteAllCases(); + await svlCases.api.deleteAllCaseItems(); await svlCommonPage.forceLogout(); }); @@ -279,7 +278,7 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { after(async () => { await cases.testResources.removeKibanaSampleData('logs'); - await cases.api.deleteAllCases(); + await svlCases.api.deleteAllCaseItems(); }); it('adds lens visualization in description', async () => { @@ -324,7 +323,7 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { }); after(async () => { - await cases.api.deleteAllCases(); + await svlCases.api.deleteAllCaseItems(); }); it('initially renders user actions list correctly', async () => { @@ -436,7 +435,7 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { }); after(async () => { - await cases.api.deleteAllCases(); + await svlCases.api.deleteAllCaseItems(); }); it('should set the cases title', async () => { @@ -498,17 +497,17 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { }); afterEach(async () => { - await cases.api.deleteAllCases(); + await svlCases.api.deleteAllCaseItems(); }); it('updates a custom field correctly', async () => { - const summary = await testSubjects.find(`case-text-custom-field-${customFields[0].key}`); - expect(await summary.getVisibleText()).equal('this is a text field value'); + const textField = await testSubjects.find(`case-text-custom-field-${customFields[0].key}`); + expect(await textField.getVisibleText()).equal('this is a text field value'); - const sync = await testSubjects.find( + const toggle = await testSubjects.find( `case-toggle-custom-field-form-field-${customFields[1].key}` ); - expect(await sync.getAttribute('aria-checked')).equal('true'); + expect(await toggle.getAttribute('aria-checked')).equal('true'); await testSubjects.click(`case-text-custom-field-edit-button-${customFields[0].key}`); @@ -526,19 +525,23 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { await testSubjects.click(`case-text-custom-field-submit-button-${customFields[0].key}`); + await header.waitUntilLoadingHasFinished(); + await retry.waitFor('update toast exist', async () => { return await testSubjects.exists('toastCloseButton'); }); await testSubjects.click('toastCloseButton'); - await sync.click(); + await header.waitUntilLoadingHasFinished(); + + await toggle.click(); await header.waitUntilLoadingHasFinished(); - expect(await summary.getVisibleText()).equal('this is a text field value edited!!'); + expect(await textField.getVisibleText()).equal('this is a text field value edited!!'); - expect(await sync.getAttribute('aria-checked')).equal('false'); + expect(await toggle.getAttribute('aria-checked')).equal('false'); // validate user action const userActions = await find.allByCssSelector( diff --git a/x-pack/test_serverless/functional/test_suites/security/index.ts b/x-pack/test_serverless/functional/test_suites/security/index.ts index cadf61fdf5eda..fabe48960b111 100644 --- a/x-pack/test_serverless/functional/test_suites/security/index.ts +++ b/x-pack/test_serverless/functional/test_suites/security/index.ts @@ -11,11 +11,7 @@ export default function ({ loadTestFile }: FtrProviderContext) { describe('serverless security UI', function () { loadTestFile(require.resolve('./ftr/landing_page')); loadTestFile(require.resolve('./ftr/navigation')); - loadTestFile(require.resolve('./ftr/cases/attachment_framework')); - loadTestFile(require.resolve('./ftr/cases/view_case')); - loadTestFile(require.resolve('./ftr/cases/create_case_form')); - loadTestFile(require.resolve('./ftr/cases/configure')); - loadTestFile(require.resolve('./ftr/cases/list_view')); + loadTestFile(require.resolve('./ftr/cases')); loadTestFile(require.resolve('./advanced_settings')); loadTestFile(require.resolve('./ml')); }); diff --git a/x-pack/test_serverless/shared/lib/cases/helpers.ts b/x-pack/test_serverless/shared/lib/cases/helpers.ts index 98dcfb6d31ebc..5cc4aa637ec43 100644 --- a/x-pack/test_serverless/shared/lib/cases/helpers.ts +++ b/x-pack/test_serverless/shared/lib/cases/helpers.ts @@ -13,14 +13,14 @@ export const createOneCaseBeforeDeleteAllAfter = ( getService: FtrProviderContext['getService'], owner: string ) => { - const cases = getService('cases'); + const svlCases = getService('svlCases'); before(async () => { await createAndNavigateToCase(getPageObject, getService, owner); }); after(async () => { - await cases.api.deleteAllCases(); + await svlCases.api.deleteAllCaseItems(); }); }; @@ -29,14 +29,14 @@ export const createOneCaseBeforeEachDeleteAllAfterEach = ( getService: FtrProviderContext['getService'], owner: string ) => { - const cases = getService('cases'); + const svlCases = getService('svlCases'); beforeEach(async () => { await createAndNavigateToCase(getPageObject, getService, owner); }); afterEach(async () => { - await cases.api.deleteAllCases(); + await svlCases.api.deleteAllCaseItems(); }); }; From 71bbbacd594885553ae10fc964ad1be9e0f63cf2 Mon Sep 17 00:00:00 2001 From: Sid Date: Mon, 23 Oct 2023 11:51:43 +0200 Subject: [PATCH 05/11] Add screen reader attributes for API key flyout code editors (#169265) ## Summary Closes #169127 ## Fixes - Added aria-labels for Access control, Role Descriptors and Metadata code editors --- .../api_keys/api_keys_grid/api_key_flyout.tsx | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_key_flyout.tsx b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_key_flyout.tsx index b3792941fc6d0..b6287b423589c 100644 --- a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_key_flyout.tsx +++ b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_key_flyout.tsx @@ -437,6 +437,12 @@ export const ApiKeyFlyout: FunctionComponent = ({ formik.setFieldValue('access', value)} @@ -500,6 +506,12 @@ export const ApiKeyFlyout: FunctionComponent = ({ @@ -631,6 +643,13 @@ export const ApiKeyFlyout: FunctionComponent = ({ formik.setFieldValue('metadata', value)} From 6adb8bde2b969671abddb66a019b0253bc511cfd Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Mon, 23 Oct 2023 04:27:16 -0700 Subject: [PATCH 06/11] [DOCS] Automate Observability and Security case setting screenshots (#168774) --- .../observability_cases/custom_fields.ts | 40 +++++++++++++++++++ .../observability_cases/index.ts | 1 + .../observability_cases/list_view.ts | 6 +++ .../security_cases/custom_fields.ts | 39 ++++++++++++++++++ .../response_ops_docs/security_cases/index.ts | 1 + 5 files changed, 87 insertions(+) create mode 100644 x-pack/test/screenshot_creation/apps/response_ops_docs/observability_cases/custom_fields.ts create mode 100644 x-pack/test/screenshot_creation/apps/response_ops_docs/security_cases/custom_fields.ts diff --git a/x-pack/test/screenshot_creation/apps/response_ops_docs/observability_cases/custom_fields.ts b/x-pack/test/screenshot_creation/apps/response_ops_docs/observability_cases/custom_fields.ts new file mode 100644 index 0000000000000..1cd8ae736afbc --- /dev/null +++ b/x-pack/test/screenshot_creation/apps/response_ops_docs/observability_cases/custom_fields.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getPageObjects, getService }: FtrProviderContext) { + const cases = getService('cases'); + const commonScreenshots = getService('commonScreenshots'); + const pageObjects = getPageObjects(['common', 'header']); + const screenshotDirectories = ['response_ops_docs', 'observability_cases']; + const testSubjects = getService('testSubjects'); + + describe('Observability case settings and custom fields', function () { + it('case settings screenshots', async () => { + await cases.navigation.navigateToApp('observability/cases', 'cases-all-title'); + await pageObjects.header.waitUntilLoadingHasFinished(); + await testSubjects.click('configure-case-button'); + await commonScreenshots.takeScreenshot('cases-settings', screenshotDirectories); + await testSubjects.click('add-custom-field'); + await commonScreenshots.takeScreenshot( + 'cases-add-custom-field', + screenshotDirectories, + 1400, + 600 + ); + await testSubjects.setValue('custom-field-label-input', 'my-field'); + await testSubjects.click('custom-field-flyout-save'); + await commonScreenshots.takeScreenshot( + 'cases-custom-field-settings', + screenshotDirectories, + 1400, + 1024 + ); + }); + }); +} diff --git a/x-pack/test/screenshot_creation/apps/response_ops_docs/observability_cases/index.ts b/x-pack/test/screenshot_creation/apps/response_ops_docs/observability_cases/index.ts index 0e0e81dca10c9..2b513391209a5 100644 --- a/x-pack/test/screenshot_creation/apps/response_ops_docs/observability_cases/index.ts +++ b/x-pack/test/screenshot_creation/apps/response_ops_docs/observability_cases/index.ts @@ -10,5 +10,6 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('observability cases', function () { loadTestFile(require.resolve('./list_view')); + loadTestFile(require.resolve('./custom_fields')); }); } diff --git a/x-pack/test/screenshot_creation/apps/response_ops_docs/observability_cases/list_view.ts b/x-pack/test/screenshot_creation/apps/response_ops_docs/observability_cases/list_view.ts index 309c17f6ee498..e156ef4fbfa30 100644 --- a/x-pack/test/screenshot_creation/apps/response_ops_docs/observability_cases/list_view.ts +++ b/x-pack/test/screenshot_creation/apps/response_ops_docs/observability_cases/list_view.ts @@ -96,5 +96,11 @@ export default function ({ getPageObject, getService }: FtrProviderContext) { 1024 ); }); + + it('case settings screenshot', async () => { + await cases.navigation.navigateToApp('observability/cases', 'cases-all-title'); + await testSubjects.click('configure-case-button'); + await commonScreenshots.takeScreenshot('add-case-connector', screenshotDirectories); + }); }); } diff --git a/x-pack/test/screenshot_creation/apps/response_ops_docs/security_cases/custom_fields.ts b/x-pack/test/screenshot_creation/apps/response_ops_docs/security_cases/custom_fields.ts new file mode 100644 index 0000000000000..412f102353cc0 --- /dev/null +++ b/x-pack/test/screenshot_creation/apps/response_ops_docs/security_cases/custom_fields.ts @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const commonScreenshots = getService('commonScreenshots'); + const pageObjects = getPageObjects(['common', 'header']); + const screenshotDirectories = ['response_ops_docs', 'security_cases']; + const testSubjects = getService('testSubjects'); + + describe('Security case settings and custom fields', function () { + it('case settings screenshots', async () => { + await pageObjects.common.navigateToApp('security', { path: 'cases' }); + await pageObjects.header.waitUntilLoadingHasFinished(); + await testSubjects.click('configure-case-button'); + await commonScreenshots.takeScreenshot('cases-settings', screenshotDirectories); + await testSubjects.click('add-custom-field'); + await commonScreenshots.takeScreenshot( + 'cases-add-custom-field', + screenshotDirectories, + 1400, + 600 + ); + await testSubjects.setValue('custom-field-label-input', 'my-field'); + await testSubjects.click('custom-field-flyout-save'); + await commonScreenshots.takeScreenshot( + 'cases-custom-field-settings', + screenshotDirectories, + 1400, + 1024 + ); + }); + }); +} diff --git a/x-pack/test/screenshot_creation/apps/response_ops_docs/security_cases/index.ts b/x-pack/test/screenshot_creation/apps/response_ops_docs/security_cases/index.ts index dc038fc7cf46f..599b8f49c6b09 100644 --- a/x-pack/test/screenshot_creation/apps/response_ops_docs/security_cases/index.ts +++ b/x-pack/test/screenshot_creation/apps/response_ops_docs/security_cases/index.ts @@ -10,5 +10,6 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('security cases', function () { loadTestFile(require.resolve('./list_view')); + loadTestFile(require.resolve('./custom_fields')); }); } From 672d8f2caa798f213aa5d20a49f6d7232f74a7ee Mon Sep 17 00:00:00 2001 From: Abdul Wahab Zahid Date: Mon, 23 Oct 2023 14:06:21 +0200 Subject: [PATCH 07/11] [Synthetics] Fix monitor error duration on Monitor Errors page (#168755) Fixes #168724 ## Summary The PR fixes the issue by sorting the data as needed. --- .../components/monitor_details/hooks/use_monitor_errors.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/hooks/use_monitor_errors.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/hooks/use_monitor_errors.tsx index 969e98a21c36c..5a7673f0513d5 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/hooks/use_monitor_errors.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/hooks/use_monitor_errors.tsx @@ -120,9 +120,13 @@ export function useMonitorErrors(monitorIdArg?: string) { (hits[0]?._source as Ping).monitor?.status === 'down' && !!errorStates?.length; + const upStatesSortedAsc = upStates.sort( + (a, b) => Number(new Date(a.state.started_at)) - Number(new Date(b.state.started_at)) + ); + return { errorStates, - upStates, + upStates: upStatesSortedAsc, loading, data, hasActiveError, From 4bf4c05b482edde65f1d4532447ed3c6b5068f1f Mon Sep 17 00:00:00 2001 From: Yngrid Coello Date: Mon, 23 Oct 2023 14:22:12 +0200 Subject: [PATCH 08/11] [APM] conditionally waiting for index status (#166438) Closes https://github.com/elastic/kibana/issues/166831. Checking for cluster health is not available on serverless. (More context in this [internal conversation](https://elastic.slack.com/archives/C877NKGBY/p1694595738607269?thread_ts=1694006744.669319&cid=C877NKGBY)) image This Pr aims to conditionally waitForIndex status only if we are in a non-serverless deployment --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../create_anomaly_detection_jobs.ts | 24 ++++++++++++++----- .../server/lib/helpers/get_es_capabilities.ts | 14 +++++++++++ .../settings/anomaly_detection/route.ts | 14 ++++++++++- .../anomaly_detection/update_to_v3.ts | 4 ++++ 4 files changed, 49 insertions(+), 7 deletions(-) create mode 100644 x-pack/plugins/apm/server/lib/helpers/get_es_capabilities.ts diff --git a/x-pack/plugins/apm/server/lib/anomaly_detection/create_anomaly_detection_jobs.ts b/x-pack/plugins/apm/server/lib/anomaly_detection/create_anomaly_detection_jobs.ts index e90776a4ee666..347a6de3b7a3f 100644 --- a/x-pack/plugins/apm/server/lib/anomaly_detection/create_anomaly_detection_jobs.ts +++ b/x-pack/plugins/apm/server/lib/anomaly_detection/create_anomaly_detection_jobs.ts @@ -13,6 +13,7 @@ import { v4 as uuidv4 } from 'uuid'; import { ProcessorEvent } from '@kbn/observability-plugin/common'; import { waitForIndexStatus } from '@kbn/core-saved-objects-migration-server-internal'; import type { APMIndices } from '@kbn/apm-data-access-plugin/server'; +import { ElasticsearchCapabilities } from '@kbn/core-elasticsearch-server'; import { ML_ERRORS } from '../../../common/anomaly_detection'; import { METRICSET_NAME, PROCESSOR_EVENT } from '../../../common/es_fields/apm'; import { Environment } from '../../../common/environment_rt'; @@ -30,12 +31,14 @@ export async function createAnomalyDetectionJobs({ indices, environments, logger, + esCapabilities, }: { mlClient?: MlClient; esClient: ElasticsearchClient; indices: APMIndices; environments: Environment[]; logger: Logger; + esCapabilities: ElasticsearchCapabilities; }) { if (!mlClient) { throw Boom.notImplemented(ML_ERRORS.ML_NOT_AVAILABLE); @@ -68,6 +71,7 @@ export async function createAnomalyDetectionJobs({ esClient, environment, apmMetricIndex, + esCapabilities, }) ); } catch (e) { @@ -97,12 +101,16 @@ async function createAnomalyDetectionJob({ esClient, environment, apmMetricIndex, + esCapabilities, }: { mlClient: Required; esClient: ElasticsearchClient; environment: string; apmMetricIndex: string; + esCapabilities: ElasticsearchCapabilities; }) { + const { serverless } = esCapabilities; + return withApmSpan('create_anomaly_detection_job', async () => { const randomToken = uuidv4().substr(-4); @@ -136,12 +144,16 @@ async function createAnomalyDetectionJob({ ], }); - await waitForIndexStatus({ - client: esClient, - index: '.ml-*', - timeout: DEFAULT_TIMEOUT, - status: 'yellow', - })(); + // Waiting for the index is not enabled in serverless, this could potentially cause + // problems when creating jobs in parallels + if (!serverless) { + await waitForIndexStatus({ + client: esClient, + index: '.ml-*', + timeout: DEFAULT_TIMEOUT, + status: 'yellow', + })(); + } return anomalyDetectionJob; }); diff --git a/x-pack/plugins/apm/server/lib/helpers/get_es_capabilities.ts b/x-pack/plugins/apm/server/lib/helpers/get_es_capabilities.ts new file mode 100644 index 0000000000000..ab262270075f0 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/helpers/get_es_capabilities.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { APMRouteHandlerResources } from '../../routes/apm_routes/register_apm_server_routes'; + +export async function getESCapabilities({ core }: APMRouteHandlerResources) { + const es = (await core.start()).elasticsearch; + + return es.getCapabilities(); +} diff --git a/x-pack/plugins/apm/server/routes/settings/anomaly_detection/route.ts b/x-pack/plugins/apm/server/routes/settings/anomaly_detection/route.ts index 8ad3d6bb92b6c..33b8897bd5aea 100644 --- a/x-pack/plugins/apm/server/routes/settings/anomaly_detection/route.ts +++ b/x-pack/plugins/apm/server/routes/settings/anomaly_detection/route.ts @@ -9,6 +9,7 @@ import * as t from 'io-ts'; import Boom from '@hapi/boom'; import { maxSuggestions } from '@kbn/observability-plugin/common'; import { ElasticsearchClient } from '@kbn/core/server'; +import { getESCapabilities } from '../../../lib/helpers/get_es_capabilities'; import { isActivePlatinumLicense } from '../../../../common/license_check'; import { ML_ERRORS } from '../../../../common/anomaly_detection'; import { createApmServerRoute } from '../../apm_routes/create_apm_server_route'; @@ -72,6 +73,8 @@ const createAnomalyDetectionJobsRoute = createApmServerRoute({ const licensingContext = await context.licensing; const esClient = (await context.core).elasticsearch.client; + const esCapabilities = await getESCapabilities(resources); + const [mlClient, indices] = await Promise.all([ getMlClient(resources), getApmIndices(), @@ -87,6 +90,7 @@ const createAnomalyDetectionJobsRoute = createApmServerRoute({ indices, environments, logger, + esCapabilities, }); notifyFeatureUsage({ @@ -149,10 +153,18 @@ const anomalyDetectionUpdateToV3Route = createApmServerRoute({ ), ]); + const esCapabilities = await getESCapabilities(resources); + const { logger } = resources; return { - update: await updateToV3({ mlClient, logger, indices, esClient }), + update: await updateToV3({ + mlClient, + logger, + indices, + esClient, + esCapabilities, + }), }; }, }); diff --git a/x-pack/plugins/apm/server/routes/settings/anomaly_detection/update_to_v3.ts b/x-pack/plugins/apm/server/routes/settings/anomaly_detection/update_to_v3.ts index 577273cbfb03c..e1b621766293f 100644 --- a/x-pack/plugins/apm/server/routes/settings/anomaly_detection/update_to_v3.ts +++ b/x-pack/plugins/apm/server/routes/settings/anomaly_detection/update_to_v3.ts @@ -10,6 +10,7 @@ import pLimit from 'p-limit'; import { ElasticsearchClient } from '@kbn/core/server'; import { JOB_STATE } from '@kbn/ml-plugin/common'; import type { APMIndices } from '@kbn/apm-data-access-plugin/server'; +import { ElasticsearchCapabilities } from '@kbn/core-elasticsearch-server'; import { createAnomalyDetectionJobs } from '../../../lib/anomaly_detection/create_anomaly_detection_jobs'; import { getAnomalyDetectionJobs } from '../../../lib/anomaly_detection/get_anomaly_detection_jobs'; import { MlClient } from '../../../lib/helpers/get_ml_client'; @@ -20,11 +21,13 @@ export async function updateToV3({ indices, mlClient, esClient, + esCapabilities, }: { logger: Logger; mlClient?: MlClient; indices: APMIndices; esClient: ElasticsearchClient; + esCapabilities: ElasticsearchCapabilities; }) { const allJobs = await getAnomalyDetectionJobs(mlClient); @@ -63,6 +66,7 @@ export async function updateToV3({ indices, environments, logger, + esCapabilities, }); return true; From 8805e74f5d957ac5751227c5463032cc82794852 Mon Sep 17 00:00:00 2001 From: Kevin Logan <56395104+kevinlog@users.noreply.github.com> Date: Mon, 23 Oct 2023 08:34:34 -0400 Subject: [PATCH 09/11] [EDR Workflows] Catch docker container deletion error (#168982) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary This PR will have our `after:run` task catch potential errors when cleaning up the docker container used. There were instances where our tests were failing to complete properly because we were failing at this step: https://buildkite.com/elastic/kibana-pull-request/builds/168052#018b37fc-b2b1-4986-8c11-0701f447d972/6-2209 ``` An error was thrown in your plugins file while executing the handler for the after:run event. --   |     | The error we received was:   |     | Error: Command failed with exit code 1: docker kill 4080ac06a71871298f2a3163c915f0170e8f3dd9f6814e022d727e9958401361   | Error response from daemon: Cannot kill container: 4080ac06a71871298f2a3163c915f0170e8f3dd9f6814e022d727e9958401361: No such container: 4080ac06a71871298f2a3163c915f0170e8f3dd9f6814e022d727e9958401361   | at makeError (/opt/local-ssd/buildkite/builds/kb-n2-4-virt-debbcb53f059032d/elastic/kibana-pull-request/kibana/node_modules/execa/lib/error.js:60:11)   | at Function.module.exports.sync (/opt/local-ssd/buildkite/builds/kb-n2-4-virt-debbcb53f059032d/elastic/kibana-pull-request/kibana/node_modules/execa/index.js:194:17)   | at Object.handler (/opt/local-ssd/buildkite/builds/kb-n2-4-virt-debbcb53f059032d/elastic/kibana-pull-request/kibana/x-pack/plugins/security_solution/public/management/cypress/support/data_loaders.ts:321:13)   | at invoke (/var/lib/buildkite-agent/.cache/Cypress/13.3.0/Cypress/resources/app/node_modules/@packages/server/lib/plugins/child/run_plugins.js:183:18)   | at /var/lib/buildkite-agent/.cache/Cypress/13.3.0/Cypress/resources/app/node_modules/@packages/server/lib/plugins/util.js:59:14   | at tryCatcher (/var/lib/buildkite-agent/.cache/Cypress/13.3.0/Cypress/resources/app/node_modules/bluebird/js/release/util.js:16:23)   | at Function.Promise.attempt.Promise.try (/var/lib/buildkite-agent/.cache/Cypress/13.3.0/Cypress/resources/app/node_modules/bluebird/js/release/method.js:39:29)   | at Object.wrapChildPromise (/var/lib/buildkite-agent/.cache/Cypress/13.3.0/Cypress/resources/app/node_modules/@packages/server/lib/plugins/util.js:58:23)   | at RunPlugins.execute (/var/lib/buildkite-agent/.cache/Cypress/13.3.0/Cypress/resources/app/node_modules/@packages/server/lib/plugins/child/run_plugins.js:164:21)   | at EventEmitter. (/var/lib/buildkite-agent/.cache/Cypress/13.3.0/Cypress/resources/app/node_modules/@packages/server/lib/plugins/child/run_plugins.js:56:12)   | at EventEmitter.emit (node:events:514:28)   | at EventEmitter.emit (node:domain:489:12)   | at process. (/var/lib/buildkite-agent/.cache/Cypress/13.3.0/Cypress/resources/app/node_modules/@packages/server/lib/plugins/util.js:33:22)   | at process.emit (node:events:514:28)   | at process.emit (node:domain:489:12)   | at process.emit.sharedData.processEmitHook.installedValue [as emit] (/var/lib/buildkite-agent/.cache/Cypress/13.3.0/Cypress/resources/app/node_modules/@cspotcode/source-map-support/source-map-support.js:745:40)   | at emit (node:internal/child_process:937:14)   | at processTicksAndRejections (node:internal/process/task_queues:83:21) ``` In addition, I have a flaky test runner to ensure that we don't fail due to this error again: https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/3527 2nd flaky run: https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/3535 3rd flaky run ✅ : https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/3592#_ - Note this run has a couple failures but they are on unrelated flaky tests that are being addressed in other PRs. ### 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 --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Patryk Kopyciński --- .../public/management/cypress/support/data_loaders.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/security_solution/public/management/cypress/support/data_loaders.ts b/x-pack/plugins/security_solution/public/management/cypress/support/data_loaders.ts index 93614b6dbb86d..82f9af7e114d2 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/support/data_loaders.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/support/data_loaders.ts @@ -316,9 +316,14 @@ export const dataLoadersForRealEndpoints = ( fleetServerContainerId = data?.fleetServerContainerId; }); - on('after:run', () => { + on('after:run', async () => { + const { log } = await stackServicesPromise; if (fleetServerContainerId) { - execa.sync('docker', ['kill', fleetServerContainerId]); + try { + execa.sync('docker', ['kill', fleetServerContainerId]); + } catch (error) { + log.error(error); + } } }); From 882e0bf81a5b415671c82f0b980de672fb05f630 Mon Sep 17 00:00:00 2001 From: Shahzad Date: Mon, 23 Oct 2023 15:14:37 +0200 Subject: [PATCH 10/11] [Uptime] Settings public API (#163400) --- docs/api/uptime-api.asciidoc | 11 ++ docs/api/uptime/get-settings.asciidoc | 39 ++++++ docs/api/uptime/update-settings.asciidoc | 117 ++++++++++++++++++ docs/user/api.asciidoc | 1 + .../common/constants/synthetics/rest_api.ts | 2 +- .../journeys/overview_scrolling.journey.ts | 1 - .../apps/synthetics/state/settings/api.ts | 17 ++- .../public/utils/api_service/api_service.ts | 26 ++-- .../uptime/common/constants/rest_api.ts | 2 +- x-pack/plugins/uptime/common/constants/ui.ts | 2 + .../state/api/dynamic_settings.ts | 14 ++- .../lib/saved_objects/saved_objects.ts | 26 ++-- .../routes/dynamic_settings.test.ts | 83 ------------- .../legacy_uptime/routes/dynamic_settings.ts | 104 ++++++---------- .../server/legacy_uptime/routes/index.ts | 7 +- .../server/legacy_uptime/uptime_server.ts | 79 +++++++++++- .../observability/synthetics_rule.ts | 4 +- .../apis/uptime/rest/dynamic_settings.ts | 15 ++- 18 files changed, 365 insertions(+), 185 deletions(-) create mode 100644 docs/api/uptime-api.asciidoc create mode 100644 docs/api/uptime/get-settings.asciidoc create mode 100644 docs/api/uptime/update-settings.asciidoc delete mode 100644 x-pack/plugins/uptime/server/legacy_uptime/routes/dynamic_settings.test.ts diff --git a/docs/api/uptime-api.asciidoc b/docs/api/uptime-api.asciidoc new file mode 100644 index 0000000000000..afc5013cb9af7 --- /dev/null +++ b/docs/api/uptime-api.asciidoc @@ -0,0 +1,11 @@ +[[uptime-apis]] +== Uptime APIs + +The following APIs are available for Uptime. + +* <> to get a settings + +* <> to update the attributes for existing settings + +include::uptime/get-settings.asciidoc[leveloffset=+1] +include::uptime/update-settings.asciidoc[leveloffset=+1] diff --git a/docs/api/uptime/get-settings.asciidoc b/docs/api/uptime/get-settings.asciidoc new file mode 100644 index 0000000000000..1b193c9f338b5 --- /dev/null +++ b/docs/api/uptime/get-settings.asciidoc @@ -0,0 +1,39 @@ +[[get-settings-api]] +== Get settings API +++++ +Get settings +++++ + +Retrieve uptime settings existing settings. + +[[get-settings-api-request]] +=== {api-request-title} + +`GET :/api/uptime/settings` + +`GET :/s//api/uptime/settings` + +=== {api-prereq-title} + +You must have `read` privileges for the *uptime* feature in *{observability}* section of the +<>. + +The API returns the following: + +[source,sh] +-------------------------------------------------- +{ + "heartbeatIndices": "heartbeat-8*", + "certExpirationThreshold": 30, + "certAgeThreshold": 730, + "defaultConnectors": [ + "08990f40-09c5-11ee-97ae-912b222b13d4", + "db25f830-2318-11ee-9391-6b0c030836d6" + ], + "defaultEmail": { + "to": [], + "cc": [], + "bcc": [] + } +} +-------------------------------------------------- diff --git a/docs/api/uptime/update-settings.asciidoc b/docs/api/uptime/update-settings.asciidoc new file mode 100644 index 0000000000000..c3dc34a8c150c --- /dev/null +++ b/docs/api/uptime/update-settings.asciidoc @@ -0,0 +1,117 @@ +[[update-settings-api]] +== Update settings API +++++ +Update settings +++++ + +Updates uptime settings attributes like heartbeatIndices, certExpirationThreshold, certAgeThreshold, defaultConnectors or defaultEmail + +=== {api-request-title} + +`PUT :/api/uptime/settings` + +`PUT :/s//api/uptime/settings` + +=== {api-prereq-title} + +You must have `all` privileges for the *uptime* feature in *{observability}* section of the +<>. + +[[settings-api-update-path-params]] +==== Path parameters + +`space_id`:: +(Optional, string) An identifier for the space. If `space_id` is not provided in the URL, the default space is used. + +[[api-update-request-body]] +==== Request body + +A partial update is supported, provided settings keys will be merged with existing settings. + +`heartbeatIndices`:: +(Optional, string) index pattern string to be used within uptime app/alerts to query heartbeat data. Defaults to `heartbeat-*`. + + +`certExpirationThreshold`:: +(Optional, number) Number of days before a certificate expires to trigger an alert. Defaults to `30`. + +`certAgeThreshold`:: +(Optional, number) Number of days after a certificate is created to trigger an alert. Defaults to `730`. + +`defaultConnectors`:: +(Optional, array) List of connector IDs to be used as default connectors for new alerts. Defaults to `[]`. + +`defaultEmail`:: +(Optional, object) Default email configuration for new alerts. Defaults to `{"to": [], "cc": [], "bcc": []}`. + +[[settings-api-update-example]] +==== Example + +[source,sh] +-------------------------------------------------- +PUT api/uptime/settings +{ + "heartbeatIndices": "heartbeat-8*", + "certExpirationThreshold": 30, + "certAgeThreshold": 730, + "defaultConnectors": [ + "08990f40-09c5-11ee-97ae-912b222b13d4", + "db25f830-2318-11ee-9391-6b0c030836d6" + ], + "defaultEmail": { + "to": [], + "cc": [], + "bcc": [] + } +} +-------------------------------------------------- + +The API returns the following: + +[source,json] +-------------------------------------------------- +{ + "heartbeatIndices": "heartbeat-8*", + "certExpirationThreshold": 30, + "certAgeThreshold": 730, + "defaultConnectors": [ + "08990f40-09c5-11ee-97ae-912b222b13d4", + "db25f830-2318-11ee-9391-6b0c030836d6" + ], + "defaultEmail": { + "to": [], + "cc": [], + "bcc": [] + } +} +-------------------------------------------------- +[[settings-api-partial-update-example]] +==== Partial update example + +[source,sh] +-------------------------------------------------- +PUT api/uptime/settings +{ + "heartbeatIndices": "heartbeat-8*", +} +-------------------------------------------------- + +The API returns the following: + +[source,json] +-------------------------------------------------- +{ + "heartbeatIndices": "heartbeat-8*", + "certExpirationThreshold": 30, + "certAgeThreshold": 730, + "defaultConnectors": [ + "08990f40-09c5-11ee-97ae-912b222b13d4", + "db25f830-2318-11ee-9391-6b0c030836d6" + ], + "defaultEmail": { + "to": [], + "cc": [], + "bcc": [] + } +} +-------------------------------------------------- \ No newline at end of file diff --git a/docs/user/api.asciidoc b/docs/user/api.asciidoc index 32e4115fe59dc..4358c448f3634 100644 --- a/docs/user/api.asciidoc +++ b/docs/user/api.asciidoc @@ -109,3 +109,4 @@ include::{kib-repo-dir}/api/osquery-manager.asciidoc[] include::{kib-repo-dir}/api/short-urls.asciidoc[] include::{kib-repo-dir}/api/task-manager/health.asciidoc[] include::{kib-repo-dir}/api/upgrade-assistant.asciidoc[] +include::{kib-repo-dir}/api/uptime-api.asciidoc[] diff --git a/x-pack/plugins/synthetics/common/constants/synthetics/rest_api.ts b/x-pack/plugins/synthetics/common/constants/synthetics/rest_api.ts index 02df09faae126..40ae2656e2b26 100644 --- a/x-pack/plugins/synthetics/common/constants/synthetics/rest_api.ts +++ b/x-pack/plugins/synthetics/common/constants/synthetics/rest_api.ts @@ -48,5 +48,5 @@ export enum SYNTHETICS_API_URLS { SYNTHETICS_MONITORS_PROJECT_UPDATE = '/api/synthetics/project/{projectName}/monitors/_bulk_update', SYNTHETICS_MONITORS_PROJECT_DELETE = '/api/synthetics/project/{projectName}/monitors/_bulk_delete', - DYNAMIC_SETTINGS = `/internal/uptime/dynamic_settings`, + DYNAMIC_SETTINGS = `/api/uptime/settings`, } diff --git a/x-pack/plugins/synthetics/e2e/synthetics/journeys/overview_scrolling.journey.ts b/x-pack/plugins/synthetics/e2e/synthetics/journeys/overview_scrolling.journey.ts index 59c47cf9ce20b..7bec9a7a4b389 100644 --- a/x-pack/plugins/synthetics/e2e/synthetics/journeys/overview_scrolling.journey.ts +++ b/x-pack/plugins/synthetics/e2e/synthetics/journeys/overview_scrolling.journey.ts @@ -24,7 +24,6 @@ journey('OverviewScrolling', async ({ page, params }) => { const listOfRequests: string[] = []; const expected = [ 'http://localhost:5620/internal/synthetics/service/enablement', - 'http://localhost:5620/internal/uptime/dynamic_settings', 'http://localhost:5620/internal/synthetics/monitor/filters', 'http://localhost:5620/internal/uptime/service/locations', 'http://localhost:5620/internal/synthetics/overview?sortField=status&sortOrder=asc&', diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/settings/api.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/settings/api.ts index 46e8111c8ca9f..62bc2e4226087 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/state/settings/api.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/settings/api.ts @@ -21,20 +21,29 @@ import { import { SYNTHETICS_API_URLS } from '../../../../../common/constants'; import { LocationMonitor } from '.'; -const apiPath = SYNTHETICS_API_URLS.DYNAMIC_SETTINGS; - interface SaveApiRequest { settings: DynamicSettings; } export const getDynamicSettings = async (): Promise => { - return await apiService.get(apiPath, undefined, DynamicSettingsCodec); + return await apiService.get( + SYNTHETICS_API_URLS.DYNAMIC_SETTINGS, + { version: '2023-10-31' }, + DynamicSettingsCodec + ); }; export const setDynamicSettings = async ({ settings, }: SaveApiRequest): Promise => { - return await apiService.post(apiPath, settings, DynamicSettingsSaveCodec); + return await apiService.put( + SYNTHETICS_API_URLS.DYNAMIC_SETTINGS, + settings, + DynamicSettingsSaveCodec, + { + version: '2023-10-31', + } + ); }; export const fetchLocationMonitors = async (): Promise => { diff --git a/x-pack/plugins/synthetics/public/utils/api_service/api_service.ts b/x-pack/plugins/synthetics/public/utils/api_service/api_service.ts index 9dead89809ee9..f1eb2607dd25b 100644 --- a/x-pack/plugins/synthetics/public/utils/api_service/api_service.ts +++ b/x-pack/plugins/synthetics/public/utils/api_service/api_service.ts @@ -9,7 +9,7 @@ import { isRight } from 'fp-ts/lib/Either'; import { formatErrors } from '@kbn/securitysolution-io-ts-utils'; import { HttpFetchQuery, HttpSetup } from '@kbn/core/public'; import { FETCH_STATUS, AddInspectorRequest } from '@kbn/observability-shared-plugin/public'; - +type Params = HttpFetchQuery & { version?: string }; class ApiService { private static instance: ApiService; private _http!: HttpSetup; @@ -59,16 +59,13 @@ class ApiService { return response; } - public async get( - apiUrl: string, - params?: HttpFetchQuery, - decodeType?: any, - asResponse = false - ) { + public async get(apiUrl: string, params: Params = {}, decodeType?: any, asResponse = false) { + const { version, ...queryParams } = params; const response = await this._http!.fetch({ path: apiUrl, - query: params, + query: queryParams, asResponse, + version, }); this.addInspectorRequest?.({ data: response, status: FETCH_STATUS.SUCCESS, loading: false }); @@ -76,11 +73,14 @@ class ApiService { return this.parseResponse(response, apiUrl, decodeType); } - public async post(apiUrl: string, data?: any, decodeType?: any, params?: HttpFetchQuery) { + public async post(apiUrl: string, data?: any, decodeType?: any, params: Params = {}) { + const { version, ...queryParams } = params; + const response = await this._http!.post(apiUrl, { method: 'POST', body: JSON.stringify(data), - query: params, + query: queryParams, + version, }); this.addInspectorRequest?.({ data: response, status: FETCH_STATUS.SUCCESS, loading: false }); @@ -88,10 +88,14 @@ class ApiService { return this.parseResponse(response, apiUrl, decodeType); } - public async put(apiUrl: string, data?: any, decodeType?: any) { + public async put(apiUrl: string, data?: any, decodeType?: any, params: Params = {}) { + const { version, ...queryParams } = params; + const response = await this._http!.put(apiUrl, { method: 'PUT', body: JSON.stringify(data), + query: queryParams, + version, }); return this.parseResponse(response, apiUrl, decodeType); diff --git a/x-pack/plugins/uptime/common/constants/rest_api.ts b/x-pack/plugins/uptime/common/constants/rest_api.ts index bdc9fcd04dd12..eaefdb71f7ba5 100644 --- a/x-pack/plugins/uptime/common/constants/rest_api.ts +++ b/x-pack/plugins/uptime/common/constants/rest_api.ts @@ -6,7 +6,7 @@ */ export enum API_URLS { - DYNAMIC_SETTINGS = `/internal/uptime/dynamic_settings`, + DYNAMIC_SETTINGS = `/api/uptime/settings`, INDEX_STATUS = '/internal/uptime/index_status', MONITOR_LIST = `/internal/uptime/monitor/list`, MONITOR_LOCATIONS = `/internal/uptime/monitor/locations`, diff --git a/x-pack/plugins/uptime/common/constants/ui.ts b/x-pack/plugins/uptime/common/constants/ui.ts index d014b8b8ea6ff..19d980dfa1534 100644 --- a/x-pack/plugins/uptime/common/constants/ui.ts +++ b/x-pack/plugins/uptime/common/constants/ui.ts @@ -109,3 +109,5 @@ export const SYNTHETICS_INDEX_PATTERN = 'synthetics-*'; export const LICENSE_NOT_ACTIVE_ERROR = 'License not active'; export const LICENSE_MISSING_ERROR = 'Missing license information'; export const LICENSE_NOT_SUPPORTED_ERROR = 'License not supported'; + +export const INITIAL_REST_VERSION = '2023-10-31'; diff --git a/x-pack/plugins/uptime/public/legacy_uptime/state/api/dynamic_settings.ts b/x-pack/plugins/uptime/public/legacy_uptime/state/api/dynamic_settings.ts index 661fdcf46ad89..e1cb67987af5d 100644 --- a/x-pack/plugins/uptime/public/legacy_uptime/state/api/dynamic_settings.ts +++ b/x-pack/plugins/uptime/public/legacy_uptime/state/api/dynamic_settings.ts @@ -12,20 +12,24 @@ import { DynamicSettingsSaveCodec, } from '../../../../common/runtime_types'; import { apiService } from './utils'; -import { API_URLS } from '../../../../common/constants'; - -const apiPath = API_URLS.DYNAMIC_SETTINGS; +import { API_URLS, INITIAL_REST_VERSION } from '../../../../common/constants'; interface SaveApiRequest { settings: DynamicSettings; } export const getDynamicSettings = async (): Promise => { - return await apiService.get(apiPath, undefined, DynamicSettingsCodec); + return await apiService.get( + API_URLS.DYNAMIC_SETTINGS, + { version: INITIAL_REST_VERSION }, + DynamicSettingsCodec + ); }; export const setDynamicSettings = async ({ settings, }: SaveApiRequest): Promise => { - return await apiService.post(apiPath, settings, DynamicSettingsSaveCodec); + return await apiService.put(API_URLS.DYNAMIC_SETTINGS, settings, DynamicSettingsSaveCodec, { + version: INITIAL_REST_VERSION, + }); }; diff --git a/x-pack/plugins/uptime/server/legacy_uptime/lib/saved_objects/saved_objects.ts b/x-pack/plugins/uptime/server/legacy_uptime/lib/saved_objects/saved_objects.ts index 3108f29a97d95..99d2c717e4f94 100644 --- a/x-pack/plugins/uptime/server/legacy_uptime/lib/saved_objects/saved_objects.ts +++ b/x-pack/plugins/uptime/server/legacy_uptime/lib/saved_objects/saved_objects.ts @@ -5,7 +5,11 @@ * 2.0. */ -import { SavedObjectsErrorHelpers, SavedObjectsServiceSetup } from '@kbn/core/server'; +import { + SavedObjectsClientContract, + SavedObjectsErrorHelpers, + SavedObjectsServiceSetup, +} from '@kbn/core/server'; import { DYNAMIC_SETTINGS_DEFAULT_ATTRIBUTES } from '../../../constants/settings'; import { DynamicSettingsAttributes } from '../../../runtime_types/settings'; @@ -20,7 +24,10 @@ export const registerUptimeSavedObjects = (savedObjectsService: SavedObjectsServ export interface UMSavedObjectsAdapter { config: UptimeConfig | null; getUptimeDynamicSettings: UMSavedObjectsQueryFn; - setUptimeDynamicSettings: UMSavedObjectsQueryFn; + setUptimeDynamicSettings: ( + client: SavedObjectsClientContract, + attr: DynamicSettingsAttributes + ) => Promise; } export const savedObjectsAdapter: UMSavedObjectsAdapter = { @@ -43,10 +50,15 @@ export const savedObjectsAdapter: UMSavedObjectsAdapter = { throw getErr; } }, - setUptimeDynamicSettings: async (client, settings: DynamicSettingsAttributes | undefined) => { - await client.create(umDynamicSettings.name, settings, { - id: settingsObjectId, - overwrite: true, - }); + setUptimeDynamicSettings: async (client, settings: DynamicSettingsAttributes) => { + const newObj = await client.create( + umDynamicSettings.name, + settings, + { + id: settingsObjectId, + overwrite: true, + } + ); + return newObj.attributes; }, }; diff --git a/x-pack/plugins/uptime/server/legacy_uptime/routes/dynamic_settings.test.ts b/x-pack/plugins/uptime/server/legacy_uptime/routes/dynamic_settings.test.ts deleted file mode 100644 index 117e39c0b6437..0000000000000 --- a/x-pack/plugins/uptime/server/legacy_uptime/routes/dynamic_settings.test.ts +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { validateCertsValues } from './dynamic_settings'; - -describe('dynamic settings', () => { - describe('validateCertValues', () => { - it(`doesn't allow age threshold values less than 0`, () => { - expect( - validateCertsValues({ - certAgeThreshold: -1, - certExpirationThreshold: 2, - heartbeatIndices: 'foo', - defaultConnectors: [], - }) - ).toMatchInlineSnapshot(` - Object { - "certAgeThreshold": "Value must be greater than 0.", - } - `); - }); - - it(`doesn't allow non-integer age threshold values`, () => { - expect( - validateCertsValues({ - certAgeThreshold: 10.2, - certExpirationThreshold: 2, - heartbeatIndices: 'foo', - defaultConnectors: [], - }) - ).toMatchInlineSnapshot(` - Object { - "certAgeThreshold": "Value must be an integer.", - } - `); - }); - - it(`doesn't allow expiration threshold values less than 0`, () => { - expect( - validateCertsValues({ - certAgeThreshold: 2, - certExpirationThreshold: -1, - heartbeatIndices: 'foo', - defaultConnectors: [], - }) - ).toMatchInlineSnapshot(` - Object { - "certExpirationThreshold": "Value must be greater than 0.", - } - `); - }); - - it(`doesn't allow non-integer expiration threshold values`, () => { - expect( - validateCertsValues({ - certAgeThreshold: 2, - certExpirationThreshold: 1.23, - heartbeatIndices: 'foo', - defaultConnectors: [], - }) - ).toMatchInlineSnapshot(` - Object { - "certExpirationThreshold": "Value must be an integer.", - } - `); - }); - - it('allows valid values', () => { - expect( - validateCertsValues({ - certAgeThreshold: 2, - certExpirationThreshold: 13, - heartbeatIndices: 'foo', - defaultConnectors: [], - }) - ).toBeUndefined(); - }); - }); -}); diff --git a/x-pack/plugins/uptime/server/legacy_uptime/routes/dynamic_settings.ts b/x-pack/plugins/uptime/server/legacy_uptime/routes/dynamic_settings.ts index 36c2de9a37cba..e5fdcf3aa7f61 100644 --- a/x-pack/plugins/uptime/server/legacy_uptime/routes/dynamic_settings.ts +++ b/x-pack/plugins/uptime/server/legacy_uptime/routes/dynamic_settings.ts @@ -6,17 +6,12 @@ */ import { schema } from '@kbn/config-schema'; -import { isRight } from 'fp-ts/lib/Either'; -import { PathReporter } from 'io-ts/lib/PathReporter'; import { UMServerLibs } from '../lib/lib'; -import { DynamicSettings, DynamicSettingsCodec } from '../../../common/runtime_types'; +import { DynamicSettings } from '../../../common/runtime_types'; import { DynamicSettingsAttributes } from '../../runtime_types/settings'; import { UMRestApiRouteFactory } from '.'; import { savedObjectsAdapter } from '../lib/saved_objects/saved_objects'; -import { - VALUE_MUST_BE_GREATER_THAN_ZERO, - VALUE_MUST_BE_AN_INTEGER, -} from '../../../common/translations'; +import { VALUE_MUST_BE_AN_INTEGER } from '../../../common/translations'; import { API_URLS } from '../../../common/constants'; export const createGetDynamicSettingsRoute: UMRestApiRouteFactory = ( @@ -28,75 +23,56 @@ export const createGetDynamicSettingsRoute: UMRestApiRouteFactory { const dynamicSettingsAttributes: DynamicSettingsAttributes = await savedObjectsAdapter.getUptimeDynamicSettings(savedObjectsClient); - return { - heartbeatIndices: dynamicSettingsAttributes.heartbeatIndices, - certExpirationThreshold: dynamicSettingsAttributes.certExpirationThreshold, - certAgeThreshold: dynamicSettingsAttributes.certAgeThreshold, - defaultConnectors: dynamicSettingsAttributes.defaultConnectors, - defaultEmail: dynamicSettingsAttributes.defaultEmail, - }; + return fromAttribute(dynamicSettingsAttributes); }, }); -export const validateCertsValues = ( - settings: DynamicSettings -): Record | undefined => { - const errors: any = {}; - if (settings.certAgeThreshold <= 0) { - errors.certAgeThreshold = VALUE_MUST_BE_GREATER_THAN_ZERO; - } else if (settings.certAgeThreshold % 1) { - errors.certAgeThreshold = VALUE_MUST_BE_AN_INTEGER; - } - if (settings.certExpirationThreshold <= 0) { - errors.certExpirationThreshold = VALUE_MUST_BE_GREATER_THAN_ZERO; - } else if (settings.certExpirationThreshold % 1) { - errors.certExpirationThreshold = VALUE_MUST_BE_AN_INTEGER; - } - if (errors.certAgeThreshold || errors.certExpirationThreshold) { - return errors; +export const validateInteger = (value: number): string | undefined => { + if (value % 1) { + return VALUE_MUST_BE_AN_INTEGER; } }; +export const DynamicSettingsSchema = schema.object({ + heartbeatIndices: schema.maybe(schema.string({ minLength: 1 })), + certAgeThreshold: schema.maybe(schema.number({ min: 1, validate: validateInteger })), + certExpirationThreshold: schema.maybe(schema.number({ min: 1, validate: validateInteger })), + defaultConnectors: schema.maybe(schema.arrayOf(schema.string())), + defaultEmail: schema.maybe( + schema.object({ + to: schema.arrayOf(schema.string()), + cc: schema.maybe(schema.arrayOf(schema.string())), + bcc: schema.maybe(schema.arrayOf(schema.string())), + }) + ), +}); + export const createPostDynamicSettingsRoute: UMRestApiRouteFactory = (_libs: UMServerLibs) => ({ - method: 'POST', + method: 'PUT', path: API_URLS.DYNAMIC_SETTINGS, validate: { - body: schema.object({ - heartbeatIndices: schema.string(), - certAgeThreshold: schema.number(), - certExpirationThreshold: schema.number(), - defaultConnectors: schema.arrayOf(schema.string()), - defaultEmail: schema.maybe( - schema.object({ - to: schema.arrayOf(schema.string()), - cc: schema.maybe(schema.arrayOf(schema.string())), - bcc: schema.maybe(schema.arrayOf(schema.string())), - }) - ), - }), + body: DynamicSettingsSchema, }, writeAccess: true, - handler: async ({ savedObjectsClient, request, response }): Promise => { - const decoded = DynamicSettingsCodec.decode(request.body); - const certThresholdErrors = validateCertsValues(request.body as DynamicSettings); + handler: async ({ savedObjectsClient, request }): Promise => { + const newSettings = request.body; + const prevSettings = await savedObjectsAdapter.getUptimeDynamicSettings(savedObjectsClient); - if (isRight(decoded) && !certThresholdErrors) { - const newSettings: DynamicSettings = decoded.right; - await savedObjectsAdapter.setUptimeDynamicSettings( - savedObjectsClient, - newSettings as DynamicSettingsAttributes - ); + const attr = await savedObjectsAdapter.setUptimeDynamicSettings(savedObjectsClient, { + ...prevSettings, + ...newSettings, + } as DynamicSettingsAttributes); - return response.ok({ - body: { - success: true, - }, - }); - } else { - const error = PathReporter.report(decoded).join(', '); - return response.badRequest({ - body: JSON.stringify(certThresholdErrors) || error, - }); - } + return fromAttribute(attr); }, }); + +const fromAttribute = (attr: DynamicSettingsAttributes) => { + return { + heartbeatIndices: attr.heartbeatIndices, + certExpirationThreshold: attr.certExpirationThreshold, + certAgeThreshold: attr.certAgeThreshold, + defaultConnectors: attr.defaultConnectors, + defaultEmail: attr.defaultEmail, + }; +}; diff --git a/x-pack/plugins/uptime/server/legacy_uptime/routes/index.ts b/x-pack/plugins/uptime/server/legacy_uptime/routes/index.ts index bdffc2a44fd0d..7253c3a9096c4 100644 --- a/x-pack/plugins/uptime/server/legacy_uptime/routes/index.ts +++ b/x-pack/plugins/uptime/server/legacy_uptime/routes/index.ts @@ -34,8 +34,6 @@ export { uptimeRouteWrapper } from './uptime_route_wrapper'; export const legacyUptimeRestApiRoutes: UMRestApiRouteFactory[] = [ createGetPingsRoute, createGetIndexStatusRoute, - createGetDynamicSettingsRoute, - createPostDynamicSettingsRoute, createGetMonitorDetailsRoute, createGetMonitorLocationsRoute, createMonitorListRoute, @@ -50,3 +48,8 @@ export const legacyUptimeRestApiRoutes: UMRestApiRouteFactory[] = [ createLastSuccessfulCheckRoute, createJourneyScreenshotBlocksRoute, ]; + +export const legacyUptimePublicRestApiRoutes: UMRestApiRouteFactory[] = [ + createGetDynamicSettingsRoute, + createPostDynamicSettingsRoute, +]; diff --git a/x-pack/plugins/uptime/server/legacy_uptime/uptime_server.ts b/x-pack/plugins/uptime/server/legacy_uptime/uptime_server.ts index cd61d40948ab1..dd01c36662e06 100644 --- a/x-pack/plugins/uptime/server/legacy_uptime/uptime_server.ts +++ b/x-pack/plugins/uptime/server/legacy_uptime/uptime_server.ts @@ -7,9 +7,16 @@ import { Logger } from '@kbn/core/server'; import { createLifecycleRuleTypeFactory, IRuleDataClient } from '@kbn/rule-registry-plugin/server'; +import { INITIAL_REST_VERSION } from '../../common/constants'; +import { DynamicSettingsSchema } from './routes/dynamic_settings'; import { UptimeRouter } from '../types'; import { uptimeRequests } from './lib/requests'; -import { createRouteWithAuth, legacyUptimeRestApiRoutes, uptimeRouteWrapper } from './routes'; +import { + createRouteWithAuth, + legacyUptimePublicRestApiRoutes, + legacyUptimeRestApiRoutes, + uptimeRouteWrapper, +} from './routes'; import { UptimeServerSetup, UptimeCorePluginsSetup } from './lib/adapters'; import { statusCheckAlertFactory } from './lib/alerts/status_check'; @@ -62,6 +69,76 @@ export const initUptimeServer = ( } }); + legacyUptimePublicRestApiRoutes.forEach((route) => { + const { method, options, handler, validate, path } = uptimeRouteWrapper( + createRouteWithAuth(libs, route), + server + ); + + const routeDefinition = { + path, + validate, + options, + }; + + switch (method) { + case 'GET': + router.versioned + .get({ + access: 'public', + path: routeDefinition.path, + options: { + tags: options?.tags, + }, + }) + .addVersion( + { + version: INITIAL_REST_VERSION, + validate: { + request: { + body: validate ? validate?.body : undefined, + }, + response: { + 200: { + body: DynamicSettingsSchema, + }, + }, + }, + }, + handler + ); + break; + case 'PUT': + router.versioned + .put({ + access: 'public', + path: routeDefinition.path, + options: { + tags: options?.tags, + }, + }) + .addVersion( + { + version: INITIAL_REST_VERSION, + validate: { + request: { + body: validate ? validate?.body : undefined, + }, + response: { + 200: { + body: DynamicSettingsSchema, + }, + }, + }, + }, + handler + ); + break; + default: + throw new Error(`Handler for method ${method} is not defined`); + } + }); + const { alerting: { registerType }, } = plugins; diff --git a/x-pack/test/alerting_api_integration/observability/synthetics_rule.ts b/x-pack/test/alerting_api_integration/observability/synthetics_rule.ts index 21a3749fc3365..328384e8a96d1 100644 --- a/x-pack/test/alerting_api_integration/observability/synthetics_rule.ts +++ b/x-pack/test/alerting_api_integration/observability/synthetics_rule.ts @@ -36,7 +36,7 @@ export default function ({ getService }: FtrProviderContext) { it('creates rule when settings are configured', async () => { await supertest - .post(SYNTHETICS_API_URLS.DYNAMIC_SETTINGS) + .put(SYNTHETICS_API_URLS.DYNAMIC_SETTINGS) .set('kbn-xsrf', 'true') .send({ heartbeatIndices: 'heartbeat-*', @@ -76,7 +76,7 @@ export default function ({ getService }: FtrProviderContext) { it('updates rules when settings are updated', async () => { await supertest - .post(SYNTHETICS_API_URLS.DYNAMIC_SETTINGS) + .put(SYNTHETICS_API_URLS.DYNAMIC_SETTINGS) .set('kbn-xsrf', 'true') .send({ heartbeatIndices: 'heartbeat-*', diff --git a/x-pack/test/api_integration/apis/uptime/rest/dynamic_settings.ts b/x-pack/test/api_integration/apis/uptime/rest/dynamic_settings.ts index 8ecdbc9b615da..987bb8c1cd64d 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/dynamic_settings.ts +++ b/x-pack/test/api_integration/apis/uptime/rest/dynamic_settings.ts @@ -29,15 +29,24 @@ export default function ({ getService }: FtrProviderContext) { defaultConnectors: [], }; const postResponse = await supertest - .post(API_URLS.DYNAMIC_SETTINGS) + .put(API_URLS.DYNAMIC_SETTINGS) .set('kbn-xsrf', 'true') .send(newSettings); - expect(postResponse.body).to.eql({ success: true }); + expect(postResponse.body).to.eql({ + heartbeatIndices: 'myIndex1*', + certExpirationThreshold: 5, + certAgeThreshold: 15, + defaultConnectors: [], + defaultEmail: { to: [], cc: [], bcc: [] }, + }); expect(postResponse.status).to.eql(200); const getResponse = await supertest.get(API_URLS.DYNAMIC_SETTINGS); - expect(getResponse.body).to.eql(newSettings); + expect(getResponse.body).to.eql({ + ...newSettings, + defaultEmail: { to: [], cc: [], bcc: [] }, + }); expect(isRight(DynamicSettingsCodec.decode(getResponse.body))).to.be.ok(); }); }); From 0a44a58800b3dfd550aff64e6e8414edee36ea02 Mon Sep 17 00:00:00 2001 From: Gloria Hornero Date: Mon, 23 Oct 2023 15:17:32 +0200 Subject: [PATCH 11/11] [Security Solution] Unskipping `x-pack/test/security_solution_cypress/cypress/e2e/explore/users/` working tests on serverless (#169475) --- .../cypress/e2e/explore/users/user_details.cy.ts | 2 +- .../cypress/e2e/explore/users/users_tabs.cy.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/explore/users/user_details.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/explore/users/user_details.cy.ts index 2e7f81f3cd1fe..3076a7f1fcccd 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/explore/users/user_details.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/explore/users/user_details.cy.ts @@ -21,7 +21,7 @@ import { } from '../../../tasks/alerts'; import { USER_COLUMN } from '../../../screens/alerts'; -describe('user details flyout', () => { +describe('user details flyout', { tags: ['@ess', '@serverless'] }, () => { beforeEach(() => { cleanKibana(); login(); diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/explore/users/users_tabs.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/explore/users/users_tabs.cy.ts index fad136becf1a2..e066c5c9ec533 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/explore/users/users_tabs.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/explore/users/users_tabs.cy.ts @@ -16,11 +16,11 @@ import { RISK_SCORE_TAB, RISK_SCORE_TAB_CONTENT } from '../../../screens/users/u import { cleanKibana } from '../../../tasks/common'; import { login } from '../../../tasks/login'; -import { visit, visitUserDetailsPage } from '../../../tasks/navigation'; +import { visitUserDetailsPage, visitWithTimeRange } from '../../../tasks/navigation'; import { USERS_URL } from '../../../urls/navigation'; -describe('Users stats and tables', () => { +describe('Users stats and tables', { tags: ['@ess', '@serverless'] }, () => { before(() => { cleanKibana(); cy.task('esArchiverLoad', { archiveName: 'users' }); @@ -30,7 +30,7 @@ describe('Users stats and tables', () => { beforeEach(() => { login(); - visit(USERS_URL); + visitWithTimeRange(USERS_URL); }); after(() => {