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/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/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/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/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/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/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/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; 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)} 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); + } } }); 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( 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/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, 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(); }); }); 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); 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/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')); }); } 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(() => { 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(); }); }; 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"