From 80b602db4a8e3c269b3c724f6efa0534294aa645 Mon Sep 17 00:00:00 2001 From: Ashokaditya <1849116+ashokaditya@users.noreply.github.com> Date: Mon, 3 Jul 2023 11:21:02 +0200 Subject: [PATCH 1/3] [Security Solution][Endpoint] Use fleet agent `last_checkin` status to show endpoint last seen status (#160506) ## Summary Shows agent last seen status on endpoint details/responder/hosts/alerts consistent with fleet agent status. - Removes some bit of redundant endpoint details that stored `HostMetadataInterface` from the redux store, as we already have a `hostInfo` that captures `HostInfo` ([`HostInfo` has `HostMetadataInterface`](https://github.com/elastic/kibana/blob/35f115ded0eea75b7e0dc7b57b045d5b43c876e8/x-pack/plugins/security_solution/common/endpoint/types/index.ts#L478)) instead. **Fleet:** ![Screenshot 2023-06-26 at 12 49 05](https://github.com/elastic/kibana/assets/1849116/963728be-51b3-43e8-84cf-e7934a369405) **Endpoints page** ![Screenshot 2023-06-26 at 12 49 01](https://github.com/elastic/kibana/assets/1849116/5cbb294c-d483-4c17-88f8-4560efea64cc) **Endpoint details** ![Screenshot 2023-06-30 at 16 23 26](https://github.com/elastic/kibana/assets/1849116/8c7c858e-b952-4d2c-8863-0ddb3bfd4210) **Responder** ![Screenshot 2023-06-28 at 10 28 23](https://github.com/elastic/kibana/assets/1849116/8c16889d-a89f-428d-978b-533ce92f2100) ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --------- Co-authored-by: Ashokaditya Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../endpoint_metadata_generator.ts | 1 + .../data_loaders/index_endpoint_hosts.ts | 38 +++++----- .../common/endpoint/types/index.ts | 14 +++- .../integration_tests/status_action.test.tsx | 4 +- .../status_action.tsx | 4 +- .../components/header_endpoint_info.test.tsx | 2 +- .../components/header_endpoint_info.tsx | 6 +- .../endpoint/use_get_endpoints_list.test.ts | 11 ++- .../pages/endpoint_hosts/store/builders.ts | 8 +-- .../pages/endpoint_hosts/store/index.test.ts | 8 +-- .../pages/endpoint_hosts/store/middleware.ts | 17 ++--- .../store/mock_endpoint_result_list.ts | 22 +++--- .../pages/endpoint_hosts/store/reducer.ts | 53 +++++--------- .../pages/endpoint_hosts/store/selectors.ts | 27 ++++---- .../management/pages/endpoint_hosts/types.ts | 19 ++--- .../view/details/components/actions_menu.tsx | 6 +- .../view/details/components/flyout_header.tsx | 8 +-- .../view/details/endpoint_details.tsx | 43 ++++-------- .../view/details/endpoint_details_content.tsx | 69 +++++++++---------- .../pages/endpoint_hosts/view/index.test.tsx | 32 +++++---- .../endpoint_metadata_service.test.ts | 8 ++- .../metadata/endpoint_metadata_service.ts | 14 ++-- 22 files changed, 199 insertions(+), 215 deletions(-) diff --git a/x-pack/plugins/security_solution/common/endpoint/data_generators/endpoint_metadata_generator.ts b/x-pack/plugins/security_solution/common/endpoint/data_generators/endpoint_metadata_generator.ts index 7d1b6f9086bcd..feb53ffd042b7 100644 --- a/x-pack/plugins/security_solution/common/endpoint/data_generators/endpoint_metadata_generator.ts +++ b/x-pack/plugins/security_solution/common/endpoint/data_generators/endpoint_metadata_generator.ts @@ -215,6 +215,7 @@ export class EndpointMetadataGenerator extends BaseDataGenerator { }, }, }, + last_checkin: new Date().toISOString(), }; return merge(hostInfo, overrides); } diff --git a/x-pack/plugins/security_solution/common/endpoint/data_loaders/index_endpoint_hosts.ts b/x-pack/plugins/security_solution/common/endpoint/data_loaders/index_endpoint_hosts.ts index d778e1cde027f..eed88ea4d44d2 100644 --- a/x-pack/plugins/security_solution/common/endpoint/data_loaders/index_endpoint_hosts.ts +++ b/x-pack/plugins/security_solution/common/endpoint/data_loaders/index_endpoint_hosts.ts @@ -211,7 +211,7 @@ export async function indexEndpointHostDocs({ await client .index({ index: metadataIndex, - body: hostMetadata, + document: hostMetadata, op_type: 'create', refresh: 'wait_for', }) @@ -225,7 +225,7 @@ export async function indexEndpointHostDocs({ await client .index({ index: policyResponseIndex, - body: hostPolicyResponse, + document: hostPolicyResponse, op_type: 'create', refresh: 'wait_for', }) @@ -281,11 +281,9 @@ export const deleteIndexedEndpointHosts = async ( }; if (indexedData.hosts.length) { - const body = { - query: { - bool: { - filter: [{ terms: { 'agent.id': indexedData.hosts.map((host) => host.agent.id) } }], - }, + const query = { + bool: { + filter: [{ terms: { 'agent.id': indexedData.hosts.map((host) => host.agent.id) } }], }, }; @@ -293,7 +291,7 @@ export const deleteIndexedEndpointHosts = async ( .deleteByQuery({ index: indexedData.metadataIndex, wait_for_completion: true, - body, + query, }) .catch(wrapErrorAndRejectPromise); @@ -302,7 +300,7 @@ export const deleteIndexedEndpointHosts = async ( .deleteByQuery({ index: metadataCurrentIndexPattern, wait_for_completion: true, - body, + query, }) .catch(wrapErrorAndRejectPromise); } @@ -312,19 +310,17 @@ export const deleteIndexedEndpointHosts = async ( .deleteByQuery({ index: indexedData.policyResponseIndex, wait_for_completion: true, - body: { - query: { - bool: { - filter: [ - { - terms: { - 'agent.id': indexedData.policyResponses.map( - (policyResponse) => policyResponse.agent.id - ), - }, + query: { + bool: { + filter: [ + { + terms: { + 'agent.id': indexedData.policyResponses.map( + (policyResponse) => policyResponse.agent.id + ), }, - ], - }, + }, + ], }, }, }) diff --git a/x-pack/plugins/security_solution/common/endpoint/types/index.ts b/x-pack/plugins/security_solution/common/endpoint/types/index.ts index aa1881195a0b6..3e48770cd5b39 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types/index.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types/index.ts @@ -474,7 +474,7 @@ export type PolicyInfo = Immutable<{ }>; // Host Information as returned by the Host Details API. -// NOTE: `HostInfo` type is the original and defined as Immutable. +// NOTE:The `HostInfo` type is the original and defined as Immutable. export interface HostInfoInterface { metadata: HostMetadataInterface; host_status: HostStatus; @@ -485,7 +485,7 @@ export interface HostInfoInterface { */ configured: PolicyInfo; /** - * Last reported running in agent (may lag behind configured) + * Last reported running in agent (might lag behind configured) */ applied: PolicyInfo; }; @@ -494,14 +494,22 @@ export interface HostInfoInterface { */ endpoint: PolicyInfo; }; + /** + * The time when the Elastic Agent associated with this Endpoint host checked in with fleet + * Conceptually the value is the same as Agent['last_checkin'] if present, but we fall back to + * UnitedAgentMetadataPersistedData['united']['endpoint']['metadata']['@timestamp'] + * if `Agent.last_checkin` value is `undefined` + */ + last_checkin: string; } export type HostInfo = Immutable; // Host metadata document streamed up to ES by the Endpoint running on host machines. -// NOTE: `HostMetadata` type is the original and defined as Immutable. If needing to +// NOTE: The `HostMetadata` type is the original and defined as Immutable. If you need to // work with metadata that is not mutable, use `HostMetadataInterface` export type HostMetadata = Immutable; + export interface HostMetadataInterface { '@timestamp': number; event: { diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/command_render_components/integration_tests/status_action.test.tsx b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/command_render_components/integration_tests/status_action.test.tsx index d229b297f239f..3888af95da704 100644 --- a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/command_render_components/integration_tests/status_action.test.tsx +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/command_render_components/integration_tests/status_action.test.tsx @@ -53,9 +53,10 @@ describe('When using processes action from response actions console', () => { }; const endpointDetailsMock = () => { + const newDate = new Date('2023-04-20T09:37:40.309Z'); const endpointMetadata = new EndpointMetadataGenerator('seed').generateHostInfo({ metadata: { - '@timestamp': new Date('2023-04-20T09:37:40.309Z').getTime(), + '@timestamp': newDate.getTime(), agent: { id: agentId, version: '8.8.0', @@ -69,6 +70,7 @@ describe('When using processes action from response actions console', () => { }, }, }, + last_checkin: newDate.toISOString(), }); useGetEndpointDetailsMock.mockReturnValue({ data: endpointMetadata, diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/command_render_components/status_action.tsx b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/command_render_components/status_action.tsx index e901e9b1a116d..357d0e566e328 100644 --- a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/command_render_components/status_action.tsx +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/command_render_components/status_action.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { memo, useEffect, useMemo, useCallback } from 'react'; +import React, { memo, useCallback, useEffect, useMemo } from 'react'; import { EuiDescriptionList } from '@elastic/eui'; import { v4 as uuidV4 } from 'uuid'; import { i18n } from '@kbn/i18n'; @@ -242,7 +242,7 @@ export const EndpointStatusActionResult = memo< 'xpack.securitySolution.endpointResponseActions.status.lastActive', { defaultMessage: 'Last active' } )} - value={endpointDetails.metadata['@timestamp']} + value={endpointDetails.last_checkin} /> ), diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/components/header_endpoint_info.test.tsx b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/components/header_endpoint_info.test.tsx index 4941fd59686cc..5a1c8bab4c05c 100644 --- a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/components/header_endpoint_info.test.tsx +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/components/header_endpoint_info.test.tsx @@ -58,7 +58,7 @@ describe('Responder header endpoint info', () => { ); expect(agentStatus.textContent).toBe(`UnhealthyIsolating`); }); - it('should show last updated time', async () => { + it('should show last checkin time', async () => { const lastUpdated = await renderResult.findByTestId('responderHeaderLastSeen'); expect(lastUpdated).toBeTruthy(); }); diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/components/header_endpoint_info.tsx b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/components/header_endpoint_info.tsx index b56746e7890a6..e51989ce0cb7e 100644 --- a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/components/header_endpoint_info.tsx +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/components/header_endpoint_info.tsx @@ -9,10 +9,10 @@ import React, { memo } from 'react'; import { EuiFlexGroup, EuiFlexItem, - EuiText, EuiSkeletonText, - EuiToolTip, EuiSpacer, + EuiText, + EuiToolTip, } from '@elastic/eui'; import { euiStyled } from '@kbn/kibana-react-plugin/common'; import { FormattedMessage, FormattedRelative } from '@kbn/i18n-react'; @@ -88,7 +88,7 @@ export const HeaderEndpointInfo = memo(({ endpointId }) id="xpack.securitySolution.responder.header.lastSeen" defaultMessage="Last seen {date}" values={{ - date: , + date: , }} /> diff --git a/x-pack/plugins/security_solution/public/management/hooks/endpoint/use_get_endpoints_list.test.ts b/x-pack/plugins/security_solution/public/management/hooks/endpoint/use_get_endpoints_list.test.ts index d6497c3516d82..d7f073b2a8338 100644 --- a/x-pack/plugins/security_solution/public/management/hooks/endpoint/use_get_endpoints_list.test.ts +++ b/x-pack/plugins/security_solution/public/management/hooks/endpoint/use_get_endpoints_list.test.ts @@ -142,6 +142,7 @@ describe('useGetEndpointsList hook', () => { : item.metadata.Endpoint.status, }, }, + last_checkin: item.last_checkin, }; }), }; @@ -164,9 +165,11 @@ describe('useGetEndpointsList hook', () => { const generator = new EndpointDocGenerator('seed'); const total = 60; const data = Array.from({ length: total }, () => { + const newDate = new Date(); const endpoint = { - metadata: generator.generateHostMetadata(), + metadata: generator.generateHostMetadata(newDate.getTime()), host_status: HostStatus.UNHEALTHY, + last_checkin: newDate.toISOString(), }; generator.updateCommonInfo(); @@ -200,9 +203,11 @@ describe('useGetEndpointsList hook', () => { const generator = new EndpointDocGenerator('seed'); const total = 61; const data = Array.from({ length: total }, () => { + const newDate = new Date(); const endpoint = { - metadata: generator.generateHostMetadata(), + metadata: generator.generateHostMetadata(newDate.getTime()), host_status: HostStatus.UNHEALTHY, + last_checkin: newDate.toISOString(), }; generator.updateCommonInfo(); @@ -229,7 +234,7 @@ describe('useGetEndpointsList hook', () => { .data.map((d) => d.metadata.agent.id) .slice(0, 50); - // call useGetEndpointsList with all 50 agents selected + // call useGetEndpointsList with all 50 agents selected const res = await renderReactQueryHook(() => useGetEndpointsList({ searchString: '', selectedAgentIds: agentIdsToSelect }) ); diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/builders.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/builders.ts index e3b7bb29ba2b3..fb4270ee6dad5 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/builders.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/builders.ts @@ -19,11 +19,9 @@ export const initialEndpointPageState = (): Immutable => { loading: false, error: undefined, endpointDetails: { - hostDetails: { - details: undefined, - detailsLoading: false, - detailsError: undefined, - }, + hostInfo: undefined, + hostInfoError: undefined, + isHostInfoLoading: false, }, policyResponse: undefined, policyResponseLoading: false, diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/index.test.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/index.test.ts index f83b58f57fb12..1524b721cb07c 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/index.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/index.test.ts @@ -44,11 +44,9 @@ describe('EndpointList store concerns', () => { loading: false, error: undefined, endpointDetails: { - hostDetails: { - details: undefined, - detailsLoading: false, - detailsError: undefined, - }, + hostInfo: undefined, + hostInfoError: undefined, + isHostInfoLoading: false, }, policyResponse: undefined, policyResponseLoading: false, diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts index 580a3b761d245..d407a6cc27cce 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts @@ -16,22 +16,22 @@ import type { } from '@kbn/timelines-plugin/common'; import { BASE_POLICY_RESPONSE_ROUTE, + ENDPOINT_FIELDS_SEARCH_STRATEGY, HOST_METADATA_GET_ROUTE, HOST_METADATA_LIST_ROUTE, - metadataCurrentIndexPattern, - METADATA_UNITED_INDEX, METADATA_TRANSFORMS_STATUS_ROUTE, - ENDPOINT_FIELDS_SEARCH_STRATEGY, + METADATA_UNITED_INDEX, + metadataCurrentIndexPattern, } from '../../../../../common/endpoint/constants'; import type { GetHostPolicyResponse, HostInfo, HostIsolationRequestBody, - ResponseActionApiResponse, HostResultList, Immutable, ImmutableObject, MetadataListResponse, + ResponseActionApiResponse, } from '../../../../../common/endpoint/types'; import { isolateHost, unIsolateHost } from '../../../../common/lib/endpoint_isolation'; import { fetchPendingActionsByAgentId } from '../../../../common/lib/endpoint_pending_actions'; @@ -59,9 +59,9 @@ import type { } from '../types'; import type { EndpointPackageInfoStateChanged } from './action'; import { - detailsData, endpointPackageInfo, endpointPackageVersion, + fullDetailsHostInfo, getCurrentIsolationRequestState, getIsEndpointPackageInfoUninitialized, getIsIsolationRequestPending, @@ -86,7 +86,7 @@ export const endpointMiddlewareFactory: ImmutableMiddlewareFactory { // this needs to be called after endpointPackageVersion is loaded (getEndpointPackageInfo) - // or else wrong pattern might be loaded + // or else the wrong pattern might be loaded async function fetchIndexPatterns( state: ImmutableObject ): Promise { @@ -115,6 +115,7 @@ export const endpointMiddlewareFactory: ImmutableMiddlewareFactory (next) => async (action) => { next(action); @@ -329,13 +330,13 @@ const loadEndpointsPendingActions = async ({ dispatch, }: EndpointPageStore): Promise => { const state = getState(); - const detailsEndpoint = detailsData(state); + const detailsEndpoint = fullDetailsHostInfo(state); const listEndpoints = listData(state); const agentsIds = new Set(); // get all agent ids for the endpoints in the list if (detailsEndpoint) { - agentsIds.add(detailsEndpoint.elastic.agent.id); + agentsIds.add(detailsEndpoint.metadata.elastic.agent.id); } for (const endpointInfo of listEndpoints) { diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/mock_endpoint_result_list.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/mock_endpoint_result_list.ts index 671347dcd27b3..f6c5c144f529b 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/mock_endpoint_result_list.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/mock_endpoint_result_list.ts @@ -7,11 +7,11 @@ import type { HttpStart } from '@kbn/core/public'; import type { + BulkGetPackagePoliciesResponse, GetAgentPoliciesResponse, GetAgentPoliciesResponseItem, - GetPackagesResponse, GetAgentsResponse, - BulkGetPackagePoliciesResponse, + GetPackagesResponse, } from '@kbn/fleet-plugin/common/types/rest_spec'; import type { GetHostPolicyResponse, @@ -25,8 +25,8 @@ import { EndpointDocGenerator } from '../../../../../common/endpoint/generate_da import { INGEST_API_AGENT_POLICIES, INGEST_API_EPM_PACKAGES, - INGEST_API_PACKAGE_POLICIES, INGEST_API_FLEET_AGENTS, + INGEST_API_PACKAGE_POLICIES, } from '../../../services/policies/ingest'; import type { GetPolicyListResponse } from '../../policy/types'; import { pendingActionsResponseMock } from '../../../../common/lib/endpoint_pending_actions/mocks'; @@ -54,9 +54,12 @@ export const mockEndpointResultList: (options?: { const hosts: HostInfo[] = []; for (let index = 0; index < actualCountToReturn; index++) { + const newDate = new Date(); + const metadata = generator.generateHostMetadata(newDate.getTime()); hosts.push({ - metadata: generator.generateHostMetadata(), + metadata, host_status: HostStatus.UNHEALTHY, + last_checkin: newDate.toISOString(), }); } const mock: MetadataListResponse = { @@ -72,9 +75,12 @@ export const mockEndpointResultList: (options?: { * returns a mocked API response for retrieving a single host metadata */ export const mockEndpointDetailsApiResult = (): HostInfo => { + const newDate = new Date(); + const metadata = generator.generateHostMetadata(newDate.getTime()); return { - metadata: generator.generateHostMetadata(), + metadata, host_status: HostStatus.UNHEALTHY, + last_checkin: newDate.toISOString(), }; }; @@ -118,8 +124,8 @@ const endpointListApiPathHandlerMocks = ({ }; }, - // Do policies referenced in endpoint list exist - // just returns 1 single agent policy that includes all of the packagePolicy IDs provided + // Do policies reference in endpoint list exist + // just returns 1 single agent policy that includes all the packagePolicy IDs provided [INGEST_API_AGENT_POLICIES]: (): GetAgentPoliciesResponse => { return { items: [agentPolicy], @@ -184,7 +190,7 @@ const endpointListApiPathHandlerMocks = ({ }; /** - * Sets up mock impelementations in support of the Endpoints list view + * Sets up mock implementations in support of the Endpoints list view * * @param mockedHttpService * @param endpointsResults diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/reducer.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/reducer.ts index 8ad781c60dd20..db15ebcfa7343 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/reducer.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/reducer.ts @@ -11,11 +11,11 @@ import type { MetadataTransformStatsChanged, } from './action'; import { - isOnEndpointPage, + getCurrentIsolationRequestState, + getIsOnEndpointDetailsActivityLog, hasSelectedEndpoint, + isOnEndpointPage, uiQueryParams, - getIsOnEndpointDetailsActivityLog, - getCurrentIsolationRequestState, } from './selectors'; import type { EndpointState } from '../types'; import { initialEndpointPageState } from './builders'; @@ -97,7 +97,7 @@ export const endpointListReducer: StateReducer = (state = initialEndpointPageSta }, }; } else if (action.type === 'serverReturnedMetadataPatterns') { - // handle error case + // handle an error case return { ...state, patterns: action.payload, @@ -114,12 +114,8 @@ export const endpointListReducer: StateReducer = (state = initialEndpointPageSta endpointDetails: { ...state.endpointDetails, hostInfo: action.payload, - hostDetails: { - ...state.endpointDetails.hostDetails, - details: action.payload.metadata, - detailsLoading: false, - detailsError: undefined, - }, + hostInfoError: undefined, + isHostInfoLoading: false, }, policyVersionInfo: action.payload.policy_info, hostStatus: action.payload.host_status, @@ -129,11 +125,8 @@ export const endpointListReducer: StateReducer = (state = initialEndpointPageSta ...state, endpointDetails: { ...state.endpointDetails, - hostDetails: { - ...state.endpointDetails.hostDetails, - detailsError: action.payload, - detailsLoading: false, - }, + hostInfoError: action.payload, + isHostInfoLoading: false, }, }; } else if (action.type === 'endpointPendingActionsStateChanged') { @@ -262,44 +255,35 @@ export const endpointListReducer: StateReducer = (state = initialEndpointPageSta ...stateUpdates, endpointDetails: { ...state.endpointDetails, - hostDetails: { - ...state.endpointDetails.hostDetails, - detailsError: undefined, - }, + hostInfoError: undefined, }, loading: true, policyItemsLoading: true, }; } } else if (isCurrentlyOnDetailsPage) { - // if previous page was the list or another endpoint details page, load endpoint details only + // if the previous page was the list or another endpoint details page, load endpoint details only if (wasPreviouslyOnDetailsPage || wasPreviouslyOnListPage) { return { ...state, ...stateUpdates, endpointDetails: { ...state.endpointDetails, - hostDetails: { - ...state.endpointDetails.hostDetails, - detailsLoading: !isNotLoadingDetails, - detailsError: undefined, - }, + hostInfoError: undefined, + isHostInfoLoading: !isNotLoadingDetails, }, detailsLoading: true, policyResponseLoading: true, }; } else { - // if previous page was not endpoint list or endpoint details, load both list and details + // if the previous page was not endpoint list or endpoint details, load both list and details return { ...state, ...stateUpdates, endpointDetails: { ...state.endpointDetails, - hostDetails: { - ...state.endpointDetails.hostDetails, - detailsLoading: true, - detailsError: undefined, - }, + hostInfoError: undefined, + isHostInfoLoading: true, }, loading: true, policyResponseLoading: true, @@ -307,16 +291,13 @@ export const endpointListReducer: StateReducer = (state = initialEndpointPageSta }; } } - // otherwise we are not on a endpoint list or details page + // otherwise, we are not on an endpoint list or details page return { ...state, ...stateUpdates, endpointDetails: { ...state.endpointDetails, - hostDetails: { - ...state.endpointDetails.hostDetails, - detailsError: undefined, - }, + hostInfoError: undefined, }, endpointsExist: true, }; diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/selectors.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/selectors.ts index 1b4c716c37462..c01a7ea65a8d6 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/selectors.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/selectors.ts @@ -11,9 +11,9 @@ import { createSelector } from 'reselect'; import { matchPath } from 'react-router-dom'; import { decode } from '@kbn/rison'; import type { Query } from '@kbn/es-query'; -import type { Immutable, EndpointPendingActions } from '../../../../../common/endpoint/types'; +import type { EndpointPendingActions, Immutable } from '../../../../../common/endpoint/types'; import { HostStatus } from '../../../../../common/endpoint/types'; -import type { EndpointState, EndpointIndexUIQueryParams } from '../types'; +import type { EndpointIndexUIQueryParams, EndpointState } from '../types'; import { extractListPaginationParams } from '../../../common/routing'; import { MANAGEMENT_DEFAULT_PAGE, @@ -43,26 +43,23 @@ export const listLoading = (state: Immutable): boolean => state.l export const listError = (state: Immutable) => state.error; -export const detailsData = (state: Immutable) => - state.endpointDetails.hostDetails.details; - -export const fullDetailsHostInfo = (state: Immutable) => - state.endpointDetails.hostInfo; +export const fullDetailsHostInfo = ( + state: Immutable +): EndpointState['endpointDetails']['hostInfo'] => state.endpointDetails.hostInfo; -export const detailsLoading = (state: Immutable): boolean => - state.endpointDetails.hostDetails.detailsLoading; +export const isHostInfoLoading = ( + state: Immutable +): EndpointState['endpointDetails']['isHostInfoLoading'] => state.endpointDetails.isHostInfoLoading; -export const detailsError = ( +export const hostInfoError = ( state: Immutable -): EndpointState['endpointDetails']['hostDetails']['detailsError'] => - state.endpointDetails.hostDetails.detailsError; +): EndpointState['endpointDetails']['hostInfoError'] => state.endpointDetails.hostInfoError; export const policyItems = (state: Immutable) => state.policyItems; export const policyItemsLoading = (state: Immutable) => state.policyItemsLoading; export const selectedPolicyId = (state: Immutable) => state.selectedPolicyId; - export const endpointPackageInfo = (state: Immutable) => state.endpointPackageInfo; export const getIsEndpointPackageInfoUninitialized: (state: Immutable) => boolean = createSelector(endpointPackageInfo, (packageInfo) => isUninitialisedResourceState(packageInfo)); @@ -258,8 +255,8 @@ export const getIsOnEndpointDetailsActivityLog: (state: Immutable return searchParams.show === EndpointDetailsTabsTypes.activityLog; }); -export const getIsEndpointHostIsolated = createSelector(detailsData, (details) => { - return (details && isEndpointHostIsolated(details)) || false; +export const getIsEndpointHostIsolated = createSelector(fullDetailsHostInfo, (details) => { + return (details && isEndpointHostIsolated(details.metadata)) || false; }); export const getEndpointPendingActionsState = ( diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts index cdd5020226697..c7de43f6bc374 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts @@ -8,15 +8,14 @@ import type { DataViewBase } from '@kbn/es-query'; import type { GetInfoResponse } from '@kbn/fleet-plugin/common'; import type { + AppLocation, + EndpointPendingActions, HostInfo, - Immutable, - HostMetadata, HostPolicyResponse, - AppLocation, - PolicyData, HostStatus, + Immutable, + PolicyData, ResponseActionApiResponse, - EndpointPendingActions, } from '../../../../common/endpoint/types'; import type { ServerApiError } from '../../../common/types'; import type { AsyncResourceState } from '../../state'; @@ -39,14 +38,8 @@ export interface EndpointState { // Adding `hostInfo` to store full API response in order to support the // refactoring effort with AgentStatus component hostInfo?: HostInfo; - hostDetails: { - /** details data for a specific host */ - details?: Immutable; - /** details page is retrieving data */ - detailsLoading: boolean; - /** api error from retrieving host details */ - detailsError?: ServerApiError; - }; + hostInfoError?: ServerApiError; + isHostInfoLoading: boolean; }; /** Holds the Policy Response for the Host currently being displayed in the details */ policyResponse?: HostPolicyResponse; diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/actions_menu.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/actions_menu.tsx index 69a4dd2054e6c..7942b10059bcb 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/actions_menu.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/actions_menu.tsx @@ -9,12 +9,12 @@ import React, { useState, useCallback, useMemo } from 'react'; import { EuiContextMenuPanel, EuiButton, EuiPopover } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import { useEndpointActionItems, useEndpointSelector } from '../../hooks'; -import { detailsData } from '../../../store/selectors'; +import { fullDetailsHostInfo } from '../../../store/selectors'; import { ContextMenuItemNavByRouter } from '../../../../../components/context_menu_with_router_support/context_menu_item_nav_by_router'; export const ActionsMenu = React.memo<{}>(() => { - const endpointDetails = useEndpointSelector(detailsData); - const menuOptions = useEndpointActionItems(endpointDetails); + const endpointDetails = useEndpointSelector(fullDetailsHostInfo); + const menuOptions = useEndpointActionItems(endpointDetails?.metadata); const [isPopoverOpen, setIsPopoverOpen] = useState(false); const closePopoverHandler = useCallback(() => { diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/flyout_header.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/flyout_header.tsx index 8cc41f19e94a3..256b606ad5110 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/flyout_header.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/flyout_header.tsx @@ -6,9 +6,9 @@ */ import React, { memo } from 'react'; -import { EuiFlyoutHeader, EuiSkeletonText, EuiToolTip, EuiTitle } from '@elastic/eui'; +import { EuiFlyoutHeader, EuiSkeletonText, EuiTitle, EuiToolTip } from '@elastic/eui'; import { useEndpointSelector } from '../../hooks'; -import { detailsLoading } from '../../../store/selectors'; +import { isHostInfoLoading } from '../../../store/selectors'; import { BackToEndpointDetailsFlyoutSubHeader } from './back_to_endpoint_details_flyout_subheader'; export const EndpointDetailsFlyoutHeader = memo( @@ -21,9 +21,9 @@ export const EndpointDetailsFlyoutHeader = memo( endpointId?: string; hasBorder?: boolean; hostname?: string; - children?: React.ReactNode | React.ReactNodeArray; + children?: React.ReactNode | React.ReactNode[]; }) => { - const hostDetailsLoading = useEndpointSelector(detailsLoading); + const hostDetailsLoading = useEndpointSelector(isHostInfoLoading); return ( diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_details.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_details.tsx index b52aaa0da4223..a7f02fbb5da03 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_details.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_details.tsx @@ -14,9 +14,8 @@ import type { HostMetadata } from '../../../../../../common/endpoint/types'; import { useToasts } from '../../../../../common/lib/kibana'; import { getEndpointDetailsPath } from '../../../../common/routing'; import { - detailsData, - detailsError, - hostStatusInfo, + fullDetailsHostInfo, + hostInfoError, policyVersionInfo, showView, uiQueryParams, @@ -26,8 +25,8 @@ import * as i18 from '../translations'; import { ActionsMenu } from './components/actions_menu'; import { EndpointDetailsFlyoutTabs, - EndpointDetailsTabsTypes, type EndpointDetailsTabs, + EndpointDetailsTabsTypes, } from './components/endpoint_details_tabs'; import { EndpointIsolationFlyoutPanel } from './components/endpoint_isolate_flyout_panel'; import { EndpointDetailsFlyoutHeader } from './components/flyout_header'; @@ -37,11 +36,10 @@ export const EndpointDetails = memo(() => { const toasts = useToasts(); const queryParams = useEndpointSelector(uiQueryParams); - const hostDetails = useEndpointSelector(detailsData); - const hostDetailsError = useEndpointSelector(detailsError); + const hostInfo = useEndpointSelector(fullDetailsHostInfo); + const hostDetailsError = useEndpointSelector(hostInfoError); const policyInfo = useEndpointSelector(policyVersionInfo); - const hostStatus = useEndpointSelector(hostStatusInfo); const show = useEndpointSelector(showView); const { canAccessEndpointActionsLogManagement } = useUserPrivileges().endpointPrivileges; @@ -68,14 +66,10 @@ export const EndpointDetails = memo(() => { selected_endpoint: id, }), content: - hostDetails === undefined ? ( + hostInfo === undefined ? ( ContentLoadingMarkup ) : ( - + ), }, ]; @@ -96,14 +90,7 @@ export const EndpointDetails = memo(() => { } return tabs; }, - [ - canAccessEndpointActionsLogManagement, - ContentLoadingMarkup, - hostDetails, - policyInfo, - hostStatus, - queryParams, - ] + [canAccessEndpointActionsLogManagement, ContentLoadingMarkup, hostInfo, policyInfo, queryParams] ); const showFlyoutFooter = @@ -127,11 +114,11 @@ export const EndpointDetails = memo(() => { {(show === 'policy_response' || show === 'isolate' || show === 'unisolate') && ( )} - {hostDetails === undefined ? ( + {hostInfo === undefined ? ( @@ -139,18 +126,18 @@ export const EndpointDetails = memo(() => { <> {(show === 'details' || show === 'activity_log') && ( )} - {show === 'policy_response' && } + {show === 'policy_response' && } {(show === 'isolate' || show === 'unisolate') && ( - + )} {showFlyoutFooter && ( diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_details_content.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_details_content.tsx index b33f98078b9fb..13878c9377eb4 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_details_content.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_details_content.tsx @@ -8,21 +8,20 @@ import styled from 'styled-components'; import { EuiDescriptionList, - EuiText, EuiFlexGroup, EuiFlexItem, - EuiSpacer, - EuiLink, EuiHealth, + EuiLink, + EuiSpacer, + EuiText, } from '@elastic/eui'; import React, { memo, useMemo } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; import { EndpointAgentStatus } from '../../../../../common/components/endpoint/endpoint_agent_status'; import { isPolicyOutOfDate } from '../../utils'; -import type { HostInfo, HostMetadata, HostStatus } from '../../../../../../common/endpoint/types'; +import type { HostInfo } from '../../../../../../common/endpoint/types'; import { useEndpointSelector } from '../hooks'; import { - fullDetailsHostInfo, getEndpointPendingActionsCallback, nonExistingPolicies, policyResponseStatus, @@ -39,9 +38,11 @@ const EndpointDetailsContentStyled = styled.div` dl dt { max-width: 27%; } + dl dd { max-width: 73%; } + .policyLineText { padding-right: 5px; } @@ -55,34 +56,28 @@ const ColumnTitle = ({ children }: { children: React.ReactNode }) => { ); }; -export const EndpointDetailsContent = memo( - ({ - details, - policyInfo, - hostStatus, - }: { - details: HostMetadata; - policyInfo?: HostInfo['policy_info']; - hostStatus: HostStatus; - }) => { +interface EndpointDetailsContentProps { + hostInfo: HostInfo; + policyInfo?: HostInfo['policy_info']; +} + +export const EndpointDetailsContent = memo( + ({ hostInfo, policyInfo }) => { const queryParams = useEndpointSelector(uiQueryParams); const policyStatus = useEndpointSelector( policyResponseStatus ) as keyof typeof POLICY_STATUS_TO_BADGE_COLOR; const getHostPendingActions = useEndpointSelector(getEndpointPendingActionsCallback); const missingPolicies = useEndpointSelector(nonExistingPolicies); - const hostInfo = useEndpointSelector(fullDetailsHostInfo); const policyResponseRoutePath = useMemo(() => { - // eslint-disable-next-line @typescript-eslint/naming-convention - const { selected_endpoint, show, ...currentUrlParams } = queryParams; - const path = getEndpointDetailsPath({ + const { selected_endpoint: selectedEndpoint, show, ...currentUrlParams } = queryParams; + return getEndpointDetailsPath({ name: 'endpointPolicyResponse', ...currentUrlParams, - selected_endpoint: details.agent.id, + selected_endpoint: hostInfo.metadata.agent.id, }); - return path; - }, [details.agent.id, queryParams]); + }, [hostInfo.metadata.agent.id, queryParams]); const policyStatusClickHandler = useNavigateByRouterEventHandler(policyResponseRoutePath); @@ -97,7 +92,7 @@ export const EndpointDetailsContent = memo( /> ), - description: {details.host.os.full}, + description: {hostInfo.metadata.host.os.full}, }, { title: ( @@ -108,13 +103,11 @@ export const EndpointDetailsContent = memo( /> ), - description: hostInfo ? ( + description: ( - ) : ( - <> ), }, { @@ -128,7 +121,10 @@ export const EndpointDetailsContent = memo( ), description: ( - + ), }, @@ -144,14 +140,14 @@ export const EndpointDetailsContent = memo( description: ( - {details.Endpoint.policy.applied.name} + {hostInfo.metadata.Endpoint.policy.applied.name} - {details.Endpoint.policy.applied.endpoint_policy_version && ( + {hostInfo.metadata.Endpoint.policy.applied.endpoint_policy_version && ( )} - {isPolicyOutOfDate(details.Endpoint.policy.applied, policyInfo) && } + {isPolicyOutOfDate(hostInfo.metadata.Endpoint.policy.applied, policyInfo) && ( + + )} ), }, @@ -206,7 +204,7 @@ export const EndpointDetailsContent = memo( /> ), - description: {details.agent.version}, + description: {hostInfo.metadata.agent.version}, }, { title: ( @@ -219,7 +217,7 @@ export const EndpointDetailsContent = memo( ), description: ( - {details.host.ip.map((ip: string, index: number) => ( + {hostInfo.metadata.host.ip.map((ip: string, index: number) => ( {ip} @@ -229,9 +227,8 @@ export const EndpointDetailsContent = memo( }, ]; }, [ - details, - getHostPendingActions, hostInfo, + getHostPendingActions, missingPolicies, policyInfo, policyStatus, diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx index 9b2e681e4f4be..027f2cc20780d 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx @@ -48,8 +48,8 @@ import { import type { TransformStats } from '../types'; import { HOST_METADATA_LIST_ROUTE, - metadataTransformPrefix, METADATA_UNITED_TRANSFORM, + metadataTransformPrefix, } from '../../../../../common/endpoint/constants'; import { useUserPrivileges } from '../../../../common/components/user_privileges'; import { @@ -62,7 +62,7 @@ import { getEndpointPrivilegesInitialStateMock } from '../../../../common/compon const mockUserPrivileges = useUserPrivileges as jest.Mock; // not sure why this can't be imported from '../../../../common/mock/formatted_relative'; -// but sure enough it needs to be inline in this one file +// but sure enough, it needs to be inline in this one file jest.mock('@kbn/i18n-react', () => { const originalModule = jest.requireActual('@kbn/i18n-react'); const FormattedRelative = jest.fn().mockImplementation(() => '20 hours ago'); @@ -310,6 +310,7 @@ describe('when on the endpoint list page', () => { hostListData[index].metadata.Endpoint.policy.applied, setup.policy ), + last_checkin: hostListData[index].last_checkin, }; }); hostListData.forEach((item, index) => { @@ -485,17 +486,17 @@ describe('when on the endpoint list page', () => { }); describe('when there is a selected host in the url', () => { - let hostDetails: HostInfo; + let hostInfo: HostInfo; let renderAndWaitForData: () => Promise>; const mockEndpointListApi = (mockedPolicyResponse?: HostPolicyResponse) => { const { - // eslint-disable-next-line @typescript-eslint/naming-convention - host_status, + host_status: hostStatus, + last_checkin: lastCheckin, metadata: { agent, Endpoint, ...details }, } = mockEndpointDetailsApiResult(); - hostDetails = { - host_status, + hostInfo = { + host_status: hostStatus, metadata: { ...details, Endpoint: { @@ -510,13 +511,14 @@ describe('when on the endpoint list page', () => { id: '1', }, }, + last_checkin: lastCheckin, }; const policy = docGenerator.generatePolicyPackagePolicy(); - policy.id = hostDetails.metadata.Endpoint.policy.applied.id; + policy.id = hostInfo.metadata.Endpoint.policy.applied.id; setEndpointListApiMockImplementation(coreStart.http, { - endpointsResults: [hostDetails], + endpointsResults: [hostInfo], endpointPackagePolicies: [policy], policyResponse: mockedPolicyResponse, }); @@ -617,7 +619,7 @@ describe('when on the endpoint list page', () => { const policyDetailsLink = await renderResult.findByTestId('policyDetailsValue'); expect(policyDetailsLink).not.toBeNull(); expect(policyDetailsLink.getAttribute('href')).toEqual( - `${APP_PATH}${MANAGEMENT_PATH}/policy/${hostDetails.metadata.Endpoint.policy.applied.id}/settings` + `${APP_PATH}${MANAGEMENT_PATH}/policy/${hostInfo.metadata.Endpoint.policy.applied.id}/settings` ); }); @@ -626,7 +628,7 @@ describe('when on the endpoint list page', () => { const policyDetailsRevElement = await renderResult.findByTestId('policyDetailsRevNo'); expect(policyDetailsRevElement).not.toBeNull(); expect(policyDetailsRevElement.textContent).toEqual( - `rev. ${hostDetails.metadata.Endpoint.policy.applied.endpoint_policy_version}` + `rev. ${hostInfo.metadata.Endpoint.policy.applied.endpoint_policy_version}` ); }); @@ -639,7 +641,7 @@ describe('when on the endpoint list page', () => { }); const changedUrlAction = await userChangedUrlChecker; expect(changedUrlAction.payload.pathname).toEqual( - `${MANAGEMENT_PATH}/policy/${hostDetails.metadata.Endpoint.policy.applied.id}/settings` + `${MANAGEMENT_PATH}/policy/${hostInfo.metadata.Endpoint.policy.applied.id}/settings` ); }); @@ -1019,6 +1021,7 @@ describe('when on the endpoint list page', () => { version: '7.14.0', }, }, + last_checkin: hosts[0].last_checkin, }, { host_status: hosts[1].host_status, @@ -1044,6 +1047,7 @@ describe('when on the endpoint list page', () => { version: '8.4.0', }, }, + last_checkin: hosts[1].last_checkin, }, ]; @@ -1333,7 +1337,7 @@ describe('when on the endpoint list page', () => { beforeEach(async () => { const { data: hosts } = mockEndpointResultList({ total: 2 }); - // second host is isolated, for unisolate testing + // the second host is isolated, for unisolate testing const hostInfo: HostInfo[] = [ { host_status: hosts[0].host_status, @@ -1359,6 +1363,7 @@ describe('when on the endpoint list page', () => { version: '7.14.0', }, }, + last_checkin: hosts[0].last_checkin, }, { host_status: hosts[1].host_status, @@ -1384,6 +1389,7 @@ describe('when on the endpoint list page', () => { version: '8.4.0', }, }, + last_checkin: hosts[1].last_checkin, }, ]; setEndpointListApiMockImplementation(coreStart.http, { diff --git a/x-pack/plugins/security_solution/server/endpoint/services/metadata/endpoint_metadata_service.test.ts b/x-pack/plugins/security_solution/server/endpoint/services/metadata/endpoint_metadata_service.test.ts index 27c758ca43a8b..ebe8a34580f76 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/metadata/endpoint_metadata_service.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/metadata/endpoint_metadata_service.test.ts @@ -15,8 +15,8 @@ import { } from '../../routes/metadata/support/test_support'; import { EndpointDocGenerator } from '../../../../common/endpoint/generate_data'; import { - getESQueryHostMetadataByFleetAgentIds, buildUnitedIndexQuery, + getESQueryHostMetadataByFleetAgentIds, } from '../../routes/metadata/query_builders'; import type { HostMetadata } from '../../../../common/endpoint/types'; import type { Agent, PackagePolicy } from '@kbn/fleet-plugin/common'; @@ -137,11 +137,14 @@ describe('EndpointMetadataService', () => { package_policies: packagePolicies, }), ]; + + const newDate = new Date(); const agentPolicyIds = agentPolicies.map((policy) => policy.id); - const endpointMetadataDoc = endpointDocGenerator.generateHostMetadata(); + const endpointMetadataDoc = endpointDocGenerator.generateHostMetadata(newDate.getTime()); const mockAgent = { policy_id: agentPolicies[0].id, policy_revision: agentPolicies[0].revision, + last_checkin: newDate.toISOString(), } as unknown as Agent; const mockDoc = unitedMetadataSearchResponseMock(endpointMetadataDoc, mockAgent); esClient.search.mockResponse(mockDoc); @@ -203,6 +206,7 @@ describe('EndpointMetadataService', () => { revision: packagePolicies[0].revision, }, }, + last_checkin: newDate.toISOString(), }, ], total: 1, diff --git a/x-pack/plugins/security_solution/server/endpoint/services/metadata/endpoint_metadata_service.ts b/x-pack/plugins/security_solution/server/endpoint/services/metadata/endpoint_metadata_service.ts index 98f42c1a4d8ce..d6247ad1b572b 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/metadata/endpoint_metadata_service.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/metadata/endpoint_metadata_service.ts @@ -12,7 +12,7 @@ import type { SavedObjectsServiceStart, } from '@kbn/core/server'; -import type { SearchTotalHits, SearchResponse } from '@elastic/elasticsearch/lib/api/types'; +import type { SearchResponse, SearchTotalHits } from '@elastic/elasticsearch/lib/api/types'; import type { Agent, AgentPolicy, AgentStatus, PackagePolicy } from '@kbn/fleet-plugin/common'; import type { AgentPolicyServiceInterface, PackagePolicyClient } from '@kbn/fleet-plugin/server'; import { AgentNotFoundError } from '@kbn/fleet-plugin/server'; @@ -32,9 +32,9 @@ import { FleetEndpointPackagePolicyNotFoundError, } from './errors'; import { + buildUnitedIndexQuery, getESQueryHostMetadataByFleetAgentIds, getESQueryHostMetadataByID, - buildUnitedIndexQuery, getESQueryHostMetadataByIDs, } from '../../routes/metadata/query_builders'; import { @@ -176,7 +176,7 @@ export class EndpointMetadataService { } } - // If the agent is not longer active, then that means that the Agent/Endpoint have been un-enrolled from the host + // If the agent is no longer active, then that means that the Agent/Endpoint have been un-enrolled from the host if (fleetAgent && !fleetAgent.active) { throw new EndpointHostUnEnrolledError( `Endpoint with id ${endpointId} (Fleet agent id ${fleetAgentId}) is unenrolled` @@ -251,7 +251,7 @@ export class EndpointMetadataService { } } - // The fleetAgentPolicy might have the endpoint policy in the `package_policies`, lets check that first + // The fleetAgentPolicy might have the endpoint policy in the `package_policies`, let's check that first if ( !endpointPackagePolicy && fleetAgentPolicy && @@ -262,7 +262,7 @@ export class EndpointMetadataService { ); } - // if we still don't have an endpoint package policy, try retrieving it from fleet + // if we still don't have an endpoint package policy, try retrieving it from `fleet` if (!endpointPackagePolicy) { try { endpointPackagePolicy = await this.getFleetEndpointPackagePolicy( @@ -294,6 +294,8 @@ export class EndpointMetadataService { id: endpointPackagePolicy?.id ?? '', }, }, + last_checkin: + _fleetAgent?.last_checkin || new Date(endpointMetadata['@timestamp']).toISOString(), }; } @@ -363,6 +365,8 @@ export class EndpointMetadataService { * * @param esClient * @param queryOptions + * @param soClient + * @param fleetServices * * @throws */ From a6a8f5b9ab518e3bbd254fc24b547df162ffa194 Mon Sep 17 00:00:00 2001 From: Ido Cohen <90558359+CohenIdo@users.noreply.github.com> Date: Mon, 3 Jul 2023 13:04:17 +0300 Subject: [PATCH 2/3] [Cloud Security] convert status api router to be versioned (#159548) --- .../journeys/cloud_security_dashboard.ts | 1 + .../common/constants.ts | 2 + .../public/common/api/use_setup_status_api.ts | 4 +- .../get_csp_rule_template.ts | 11 +-- .../server/routes/status/status.ts | 79 +++++++++++-------- .../cloud_security_posture/tsconfig.json | 1 + .../status/status_index_timeout.ts | 4 + .../status/status_indexed.ts | 4 + .../status/status_indexing.ts | 4 + .../status_not_deployed_not_installed.ts | 4 + .../status/status_unprivileged.ts | 5 ++ .../status/status_waiting_for_results.ts | 4 + .../telemetry/telemetry.ts | 2 + .../page_objects/csp_dashboard_page.ts | 2 + .../page_objects/findings_page.ts | 2 + 15 files changed, 88 insertions(+), 41 deletions(-) diff --git a/x-pack/performance/journeys/cloud_security_dashboard.ts b/x-pack/performance/journeys/cloud_security_dashboard.ts index 39625126e7067..a7256f44717f2 100644 --- a/x-pack/performance/journeys/cloud_security_dashboard.ts +++ b/x-pack/performance/journeys/cloud_security_dashboard.ts @@ -14,6 +14,7 @@ export const journey = new Journey({ const response = await kibanaServer.request({ path: '/internal/cloud_security_posture/status?check=init', method: 'GET', + headers: { 'elastic-api-version': '1' }, }); expect(response.status).to.eql(200); expect(response.data).to.eql({ isPluginInitialized: true }); diff --git a/x-pack/plugins/cloud_security_posture/common/constants.ts b/x-pack/plugins/cloud_security_posture/common/constants.ts index 8b6ed65bb1853..cc01567f55347 100644 --- a/x-pack/plugins/cloud_security_posture/common/constants.ts +++ b/x-pack/plugins/cloud_security_posture/common/constants.ts @@ -8,6 +8,8 @@ import { PostureTypes, VulnSeverity } from './types'; export const STATUS_ROUTE_PATH = '/internal/cloud_security_posture/status'; +export const STATUS_API_CURRENT_VERSION = '1'; + export const STATS_ROUTE_PATH = '/internal/cloud_security_posture/stats/{policy_template}'; export const VULNERABILITIES_DASHBOARD_ROUTE_PATH = diff --git a/x-pack/plugins/cloud_security_posture/public/common/api/use_setup_status_api.ts b/x-pack/plugins/cloud_security_posture/public/common/api/use_setup_status_api.ts index 99e277b30bb54..afb7a89c69e6f 100644 --- a/x-pack/plugins/cloud_security_posture/public/common/api/use_setup_status_api.ts +++ b/x-pack/plugins/cloud_security_posture/public/common/api/use_setup_status_api.ts @@ -8,7 +8,7 @@ import { useQuery, type UseQueryOptions } from '@tanstack/react-query'; import { useKibana } from '../hooks/use_kibana'; import { type CspSetupStatus } from '../../../common/types'; -import { STATUS_ROUTE_PATH } from '../../../common/constants'; +import { STATUS_API_CURRENT_VERSION, STATUS_ROUTE_PATH } from '../../../common/constants'; const getCspSetupStatusQueryKey = 'csp_status_key'; @@ -18,7 +18,7 @@ export const useCspSetupStatusApi = ( const { http } = useKibana().services; return useQuery( [getCspSetupStatusQueryKey], - () => http.get(STATUS_ROUTE_PATH), + () => http.get(STATUS_ROUTE_PATH, { version: STATUS_API_CURRENT_VERSION }), options ); }; diff --git a/x-pack/plugins/cloud_security_posture/server/routes/csp_rule_template/get_csp_rule_template.ts b/x-pack/plugins/cloud_security_posture/server/routes/csp_rule_template/get_csp_rule_template.ts index 6d41f87eaff7f..63e2bdb6ee102 100644 --- a/x-pack/plugins/cloud_security_posture/server/routes/csp_rule_template/get_csp_rule_template.ts +++ b/x-pack/plugins/cloud_security_posture/server/routes/csp_rule_template/get_csp_rule_template.ts @@ -7,7 +7,6 @@ import { NewPackagePolicy } from '@kbn/fleet-plugin/common'; import { SavedObjectsClientContract } from '@kbn/core-saved-objects-api-server'; -import pMap from 'p-map'; import { transformError } from '@kbn/securitysolution-es-utils'; import { GetCspRuleTemplateRequest, GetCspRuleTemplateResponse } from '../../../common/types'; import { CspRuleTemplate } from '../../../common/schemas'; @@ -61,13 +60,9 @@ const findCspRuleTemplateHandler = async ( filter: getBenchmarkTypeFilter(benchmarkId), }); - const cspRulesTemplates = await pMap( - cspRulesTemplatesSo.saved_objects, - async (cspRuleTemplate) => { - return { ...cspRuleTemplate.attributes }; - }, - { concurrency: 50 } - ); + const cspRulesTemplates = cspRulesTemplatesSo.saved_objects.map((cspRuleTemplate) => { + return { ...cspRuleTemplate.attributes }; + }); return { items: cspRulesTemplates, diff --git a/x-pack/plugins/cloud_security_posture/server/routes/status/status.ts b/x-pack/plugins/cloud_security_posture/server/routes/status/status.ts index 60f634b4c32b8..86b0d0a66802b 100644 --- a/x-pack/plugins/cloud_security_posture/server/routes/status/status.ts +++ b/x-pack/plugins/cloud_security_posture/server/routes/status/status.ts @@ -16,6 +16,7 @@ import type { import moment from 'moment'; import { Installation, PackagePolicy } from '@kbn/fleet-plugin/common'; import { schema } from '@kbn/config-schema'; +import { VersionedRoute } from '@kbn/core-http-server/src/versioning/types'; import { CLOUD_SECURITY_POSTURE_PACKAGE_NAME, STATUS_ROUTE_PATH, @@ -29,7 +30,12 @@ import { LATEST_VULNERABILITIES_INDEX_DEFAULT_NS, VULN_MGMT_POLICY_TEMPLATE, } from '../../../common/constants'; -import type { CspApiRequestHandlerContext, CspRouter, StatusResponseInfo } from '../../types'; +import type { + CspApiRequestHandlerContext, + CspRequestHandlerContext, + CspRouter, + StatusResponseInfo, +} from '../../types'; import type { CspSetupStatus, CspStatusCode, @@ -328,44 +334,55 @@ export const statusQueryParamsSchema = schema.object({ check: schema.oneOf([schema.literal('all'), schema.literal('init')], { defaultValue: 'all' }), }); -export const defineGetCspStatusRoute = (router: CspRouter): void => - router.get( - { +export const defineGetCspStatusRoute = ( + router: CspRouter +): VersionedRoute<'get', CspRequestHandlerContext> => + router.versioned + .get({ + access: 'internal', path: STATUS_ROUTE_PATH, - validate: { query: statusQueryParamsSchema }, options: { tags: ['access:cloud-security-posture-read'], }, - }, - async (context, request, response) => { - const cspContext = await context.csp; - try { - if (request.query.check === 'init') { + }) + .addVersion( + { + version: '1', + validate: { + request: { + query: statusQueryParamsSchema, + }, + }, + }, + async (context, request, response) => { + const cspContext = await context.csp; + try { + if (request.query.check === 'init') { + return response.ok({ + body: { + isPluginInitialized: cspContext.isPluginInitialized(), + }, + }); + } + const status: CspSetupStatus = await getCspStatus({ + ...cspContext, + esClient: cspContext.esClient.asCurrentUser, + }); return response.ok({ - body: { - isPluginInitialized: cspContext.isPluginInitialized(), - }, + body: status, + }); + } catch (err) { + cspContext.logger.error(`Error getting csp status`); + cspContext.logger.error(err); + + const error = transformError(err); + return response.customError({ + body: { message: error.message }, + statusCode: error.statusCode, }); } - const status = await getCspStatus({ - ...cspContext, - esClient: cspContext.esClient.asCurrentUser, - }); - return response.ok({ - body: status, - }); - } catch (err) { - cspContext.logger.error(`Error getting csp status`); - cspContext.logger.error(err); - - const error = transformError(err); - return response.customError({ - body: { message: error.message }, - statusCode: error.statusCode, - }); } - } - ); + ); const getStatusResponse = (statusResponseInfo: StatusResponseInfo) => { const { diff --git a/x-pack/plugins/cloud_security_posture/tsconfig.json b/x-pack/plugins/cloud_security_posture/tsconfig.json index 6dd9ba33a3165..ae8f7d610002b 100755 --- a/x-pack/plugins/cloud_security_posture/tsconfig.json +++ b/x-pack/plugins/cloud_security_posture/tsconfig.json @@ -48,6 +48,7 @@ "@kbn/shared-ux-router", "@kbn/core-saved-objects-server", "@kbn/share-plugin", + "@kbn/core-http-server", ], "exclude": [ "target/**/*", diff --git a/x-pack/test/api_integration/apis/cloud_security_posture/status/status_index_timeout.ts b/x-pack/test/api_integration/apis/cloud_security_posture/status/status_index_timeout.ts index eae7154763bc0..fe52d8d3a0773 100644 --- a/x-pack/test/api_integration/apis/cloud_security_posture/status/status_index_timeout.ts +++ b/x-pack/test/api_integration/apis/cloud_security_posture/status/status_index_timeout.ts @@ -6,6 +6,7 @@ */ import expect from '@kbn/expect'; import type { CspSetupStatus } from '@kbn/cloud-security-posture-plugin/common/types'; +import { ELASTIC_HTTP_VERSION_HEADER } from '@kbn/core-http-common'; import { FINDINGS_INDEX_DEFAULT_NS, LATEST_FINDINGS_INDEX_DEFAULT_NS, @@ -105,6 +106,7 @@ export default function (providerContext: FtrProviderContext) { const { body: res }: { body: CspSetupStatus } = await supertest .get(`/internal/cloud_security_posture/status`) + .set(ELASTIC_HTTP_VERSION_HEADER, '1') .set('kbn-xsrf', 'xxxx') .expect(200); @@ -131,6 +133,7 @@ export default function (providerContext: FtrProviderContext) { const { body: res }: { body: CspSetupStatus } = await supertest .get(`/internal/cloud_security_posture/status`) + .set(ELASTIC_HTTP_VERSION_HEADER, '1') .set('kbn-xsrf', 'xxxx') .expect(200); @@ -157,6 +160,7 @@ export default function (providerContext: FtrProviderContext) { const { body: res }: { body: CspSetupStatus } = await supertest .get(`/internal/cloud_security_posture/status`) + .set(ELASTIC_HTTP_VERSION_HEADER, '1') .set('kbn-xsrf', 'xxxx') .expect(200); diff --git a/x-pack/test/api_integration/apis/cloud_security_posture/status/status_indexed.ts b/x-pack/test/api_integration/apis/cloud_security_posture/status/status_indexed.ts index c8cd9927c72d4..aa9d6d3289e95 100644 --- a/x-pack/test/api_integration/apis/cloud_security_posture/status/status_indexed.ts +++ b/x-pack/test/api_integration/apis/cloud_security_posture/status/status_indexed.ts @@ -5,6 +5,7 @@ * 2.0. */ import expect from '@kbn/expect'; +import { ELASTIC_HTTP_VERSION_HEADER } from '@kbn/core-http-common'; import type { CspSetupStatus } from '@kbn/cloud-security-posture-plugin/common/types'; import { FINDINGS_INDEX_DEFAULT_NS, @@ -71,6 +72,7 @@ export default function (providerContext: FtrProviderContext) { const { body: res }: { body: CspSetupStatus } = await supertest .get(`/internal/cloud_security_posture/status`) + .set(ELASTIC_HTTP_VERSION_HEADER, '1') .set('kbn-xsrf', 'xxxx') .expect(200); @@ -89,6 +91,7 @@ export default function (providerContext: FtrProviderContext) { const { body: res }: { body: CspSetupStatus } = await supertest .get(`/internal/cloud_security_posture/status`) + .set(ELASTIC_HTTP_VERSION_HEADER, '1') .set('kbn-xsrf', 'xxxx') .expect(200); @@ -107,6 +110,7 @@ export default function (providerContext: FtrProviderContext) { const { body: res }: { body: CspSetupStatus } = await supertest .get(`/internal/cloud_security_posture/status`) + .set(ELASTIC_HTTP_VERSION_HEADER, '1') .set('kbn-xsrf', 'xxxx') .expect(200); diff --git a/x-pack/test/api_integration/apis/cloud_security_posture/status/status_indexing.ts b/x-pack/test/api_integration/apis/cloud_security_posture/status/status_indexing.ts index d7c11c446544a..ef16eb94d8a33 100644 --- a/x-pack/test/api_integration/apis/cloud_security_posture/status/status_indexing.ts +++ b/x-pack/test/api_integration/apis/cloud_security_posture/status/status_indexing.ts @@ -5,6 +5,7 @@ * 2.0. */ import expect from '@kbn/expect'; +import { ELASTIC_HTTP_VERSION_HEADER } from '@kbn/core-http-common'; import type { CspSetupStatus } from '@kbn/cloud-security-posture-plugin/common/types'; import { FINDINGS_INDEX_DEFAULT_NS, @@ -70,6 +71,7 @@ export default function (providerContext: FtrProviderContext) { const { body: res }: { body: CspSetupStatus } = await supertest .get(`/internal/cloud_security_posture/status`) + .set(ELASTIC_HTTP_VERSION_HEADER, '1') .set('kbn-xsrf', 'xxxx') .expect(200); @@ -88,6 +90,7 @@ export default function (providerContext: FtrProviderContext) { const { body: res }: { body: CspSetupStatus } = await supertest .get(`/internal/cloud_security_posture/status`) + .set(ELASTIC_HTTP_VERSION_HEADER, '1') .set('kbn-xsrf', 'xxxx') .expect(200); @@ -106,6 +109,7 @@ export default function (providerContext: FtrProviderContext) { const { body: res }: { body: CspSetupStatus } = await supertest .get(`/internal/cloud_security_posture/status`) + .set(ELASTIC_HTTP_VERSION_HEADER, '1') .set('kbn-xsrf', 'xxxx') .expect(200); diff --git a/x-pack/test/api_integration/apis/cloud_security_posture/status/status_not_deployed_not_installed.ts b/x-pack/test/api_integration/apis/cloud_security_posture/status/status_not_deployed_not_installed.ts index 93b8c81ad44de..d7d77c93ecad4 100644 --- a/x-pack/test/api_integration/apis/cloud_security_posture/status/status_not_deployed_not_installed.ts +++ b/x-pack/test/api_integration/apis/cloud_security_posture/status/status_not_deployed_not_installed.ts @@ -6,6 +6,7 @@ */ import expect from '@kbn/expect'; import type { CspSetupStatus } from '@kbn/cloud-security-posture-plugin/common/types'; +import { ELASTIC_HTTP_VERSION_HEADER } from '@kbn/core-http-common'; import { FtrProviderContext } from '../../../ftr_provider_context'; import { createPackagePolicy } from '../helper'; @@ -50,6 +51,7 @@ export default function (providerContext: FtrProviderContext) { const { body: res }: { body: CspSetupStatus } = await supertest .get(`/internal/cloud_security_posture/status`) + .set(ELASTIC_HTTP_VERSION_HEADER, '1') .set('kbn-xsrf', 'xxxx') .expect(200); @@ -72,6 +74,7 @@ export default function (providerContext: FtrProviderContext) { const { body: res }: { body: CspSetupStatus } = await supertest .get(`/internal/cloud_security_posture/status`) + .set(ELASTIC_HTTP_VERSION_HEADER, '1') .set('kbn-xsrf', 'xxxx') .expect(200); @@ -94,6 +97,7 @@ export default function (providerContext: FtrProviderContext) { const { body: res }: { body: CspSetupStatus } = await supertest .get(`/internal/cloud_security_posture/status`) + .set(ELASTIC_HTTP_VERSION_HEADER, '1') .set('kbn-xsrf', 'xxxx') .expect(200); diff --git a/x-pack/test/api_integration/apis/cloud_security_posture/status/status_unprivileged.ts b/x-pack/test/api_integration/apis/cloud_security_posture/status/status_unprivileged.ts index 3127519b2bc4c..2432165566de8 100644 --- a/x-pack/test/api_integration/apis/cloud_security_posture/status/status_unprivileged.ts +++ b/x-pack/test/api_integration/apis/cloud_security_posture/status/status_unprivileged.ts @@ -5,6 +5,7 @@ * 2.0. */ import expect from '@kbn/expect'; +import { ELASTIC_HTTP_VERSION_HEADER } from '@kbn/core-http-common'; import type { CspSetupStatus } from '@kbn/cloud-security-posture-plugin/common/types'; import { BENCHMARK_SCORE_INDEX_DEFAULT_NS, @@ -79,6 +80,7 @@ export default function (providerContext: FtrProviderContext) { const { body: res }: { body: CspSetupStatus } = await supertestWithoutAuth .get(`/internal/cloud_security_posture/status`) + .set(ELASTIC_HTTP_VERSION_HEADER, '1') .set('kbn-xsrf', 'xxxx') .auth(UNPRIVILEGED_USERNAME, 'changeme') .expect(200); @@ -127,6 +129,7 @@ export default function (providerContext: FtrProviderContext) { const { body: res }: { body: CspSetupStatus } = await supertestWithoutAuth .get(`/internal/cloud_security_posture/status`) + .set(ELASTIC_HTTP_VERSION_HEADER, '1') .set('kbn-xsrf', 'xxxx') .auth(UNPRIVILEGED_USERNAME, 'changeme') .expect(200); @@ -157,6 +160,7 @@ export default function (providerContext: FtrProviderContext) { const { body: res }: { body: CspSetupStatus } = await supertestWithoutAuth .get(`/internal/cloud_security_posture/status`) + .set(ELASTIC_HTTP_VERSION_HEADER, '1') .set('kbn-xsrf', 'xxxx') .auth(UNPRIVILEGED_USERNAME, 'changeme') .expect(200); @@ -190,6 +194,7 @@ export default function (providerContext: FtrProviderContext) { const { body: res }: { body: CspSetupStatus } = await supertestWithoutAuth .get(`/internal/cloud_security_posture/status`) + .set(ELASTIC_HTTP_VERSION_HEADER, '1') .set('kbn-xsrf', 'xxxx') .auth(UNPRIVILEGED_USERNAME, 'changeme') .expect(200); diff --git a/x-pack/test/api_integration/apis/cloud_security_posture/status/status_waiting_for_results.ts b/x-pack/test/api_integration/apis/cloud_security_posture/status/status_waiting_for_results.ts index 8153851124329..53692014f767a 100644 --- a/x-pack/test/api_integration/apis/cloud_security_posture/status/status_waiting_for_results.ts +++ b/x-pack/test/api_integration/apis/cloud_security_posture/status/status_waiting_for_results.ts @@ -5,6 +5,7 @@ * 2.0. */ import expect from '@kbn/expect'; +import { ELASTIC_HTTP_VERSION_HEADER } from '@kbn/core-http-common'; import type { CspSetupStatus } from '@kbn/cloud-security-posture-plugin/common/types'; import { setupFleetAndAgents } from '../../../../fleet_api_integration/apis/agents/services'; import { generateAgent } from '../../../../fleet_api_integration/helpers'; @@ -87,6 +88,7 @@ export default function (providerContext: FtrProviderContext) { const { body: res }: { body: CspSetupStatus } = await supertest .get(`/internal/cloud_security_posture/status`) + .set(ELASTIC_HTTP_VERSION_HEADER, '1') .set('kbn-xsrf', 'xxxx') .expect(200); expect(res.kspm.status).to.be('waiting_for_results'); @@ -112,6 +114,7 @@ export default function (providerContext: FtrProviderContext) { const { body: res }: { body: CspSetupStatus } = await supertest .get(`/internal/cloud_security_posture/status`) + .set(ELASTIC_HTTP_VERSION_HEADER, '1') .set('kbn-xsrf', 'xxxx') .expect(200); expect(res.cspm.status).to.be('waiting_for_results'); @@ -137,6 +140,7 @@ export default function (providerContext: FtrProviderContext) { const { body: res }: { body: CspSetupStatus } = await supertest .get(`/internal/cloud_security_posture/status`) + .set(ELASTIC_HTTP_VERSION_HEADER, '1') .set('kbn-xsrf', 'xxxx') .expect(200); expect(res.vuln_mgmt.status).to.be('waiting_for_results'); diff --git a/x-pack/test/cloud_security_posture_api/telemetry/telemetry.ts b/x-pack/test/cloud_security_posture_api/telemetry/telemetry.ts index ede036d93239f..797a88881a87b 100644 --- a/x-pack/test/cloud_security_posture_api/telemetry/telemetry.ts +++ b/x-pack/test/cloud_security_posture_api/telemetry/telemetry.ts @@ -6,6 +6,7 @@ */ import expect from '@kbn/expect'; +import { ELASTIC_HTTP_VERSION_HEADER } from '@kbn/core-http-common'; import { data, MockTelemetryFindings } from './data'; import type { FtrProviderContext } from '../ftr_provider_context'; @@ -26,6 +27,7 @@ export default function ({ getService }: FtrProviderContext) { log.debug('Check CSP plugin is initialized'); const response = await supertest .get('/internal/cloud_security_posture/status?check=init') + .set(ELASTIC_HTTP_VERSION_HEADER, '1') .expect(200); expect(response.body).to.eql({ isPluginInitialized: true }); log.debug('CSP plugin is initialized'); diff --git a/x-pack/test/cloud_security_posture_functional/page_objects/csp_dashboard_page.ts b/x-pack/test/cloud_security_posture_functional/page_objects/csp_dashboard_page.ts index 635cf88965e5f..c7265ece4744a 100644 --- a/x-pack/test/cloud_security_posture_functional/page_objects/csp_dashboard_page.ts +++ b/x-pack/test/cloud_security_posture_functional/page_objects/csp_dashboard_page.ts @@ -6,6 +6,7 @@ */ import expect from '@kbn/expect'; +import { ELASTIC_HTTP_VERSION_HEADER } from '@kbn/core-http-common'; import type { FtrProviderContext } from '../ftr_provider_context'; // Defined in CSP plugin @@ -27,6 +28,7 @@ export function CspDashboardPageProvider({ getService, getPageObjects }: FtrProv log.debug('Check CSP plugin is initialized'); const response = await supertest .get('/internal/cloud_security_posture/status?check=init') + .set(ELASTIC_HTTP_VERSION_HEADER, '1') .expect(200); expect(response.body).to.eql({ isPluginInitialized: true }); log.debug('CSP plugin is initialized'); diff --git a/x-pack/test/cloud_security_posture_functional/page_objects/findings_page.ts b/x-pack/test/cloud_security_posture_functional/page_objects/findings_page.ts index 6bb3a78e44cb4..59ec355ca770f 100644 --- a/x-pack/test/cloud_security_posture_functional/page_objects/findings_page.ts +++ b/x-pack/test/cloud_security_posture_functional/page_objects/findings_page.ts @@ -6,6 +6,7 @@ */ import expect from '@kbn/expect'; +import { ELASTIC_HTTP_VERSION_HEADER } from '@kbn/core-http-common'; import type { FtrProviderContext } from '../ftr_provider_context'; // Defined in CSP plugin @@ -28,6 +29,7 @@ export function FindingsPageProvider({ getService, getPageObjects }: FtrProvider log.debug('Check CSP plugin is initialized'); const response = await supertest .get('/internal/cloud_security_posture/status?check=init') + .set(ELASTIC_HTTP_VERSION_HEADER, '1') .expect(200); expect(response.body).to.eql({ isPluginInitialized: true }); log.debug('CSP plugin is initialized'); From 61b792f50f1841340c0f81cca1fa6a8672d9e80a Mon Sep 17 00:00:00 2001 From: Angela Chuang <6295984+angorayc@users.noreply.github.com> Date: Mon, 3 Jul 2023 11:12:03 +0100 Subject: [PATCH 3/3] [SecuritySolution] Get Started Page UI update (#160850) ## Summary 1. Add content max-width: 1150px 2. Change wording `runtime` to `realtime` 3. Enable product toggles based on `product type` if no data from local storage. (Product type `security` displayed as `Analytics` in toggle but it's product id changed to `security` to aligned with product types' configs) **Follow up:** Wait for UX to confirm if `product tiers (essential / complete)` affects the cards. Screenshot 2023-06-29 at 22 06 39 ### Checklist Delete any items that are not applicable to this PR. - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../serverless_security/common/config.ts | 13 +- .../common/pli/pli_features.test.ts | 11 +- .../get_started/get_started.test.tsx | 19 +- .../components/get_started/get_started.tsx | 49 +++- .../components/get_started/helpers.test.ts | 36 +-- .../public/components/get_started/helpers.ts | 19 +- .../public/components/get_started/index.tsx | 6 +- .../public/components/get_started/lazy.tsx | 5 +- .../get_started/product_switch.test.tsx | 8 +- .../components/get_started/product_switch.tsx | 17 +- .../components/get_started/reducer.test.ts | 22 +- .../public/components/get_started/reducer.tsx | 6 +- .../components/get_started/sections.tsx | 15 +- .../get_started/toggle_panel.test.tsx | 75 ++++-- .../components/get_started/toggle_panel.tsx | 96 ++----- .../components/get_started/translations.ts | 4 +- .../public/components/get_started/types.ts | 17 +- .../get_started/use_setup_cards.test.tsx | 4 +- .../get_started/use_setup_cards.tsx | 6 +- .../get_started/use_toggle_panel.test.tsx | 239 ++++++++++++++++++ .../get_started/use_toggle_panel.tsx | 77 ++++++ .../upselling/register_upsellings.test.tsx | 8 +- .../public/lib/get_started/storage.test.ts | 18 +- .../public/lib/get_started/storage.ts | 11 +- .../serverless_security/public/plugin.ts | 4 +- 25 files changed, 568 insertions(+), 217 deletions(-) create mode 100644 x-pack/plugins/serverless_security/public/components/get_started/use_toggle_panel.test.tsx create mode 100644 x-pack/plugins/serverless_security/public/components/get_started/use_toggle_panel.tsx diff --git a/x-pack/plugins/serverless_security/common/config.ts b/x-pack/plugins/serverless_security/common/config.ts index 05c1cb4f0b01b..63b173ff3930e 100644 --- a/x-pack/plugins/serverless_security/common/config.ts +++ b/x-pack/plugins/serverless_security/common/config.ts @@ -7,11 +7,18 @@ import { schema, TypeOf } from '@kbn/config-schema'; +export enum ProductLine { + security = 'security', + cloud = 'cloud', + endpoint = 'endpoint', +} + export const productLine = schema.oneOf([ - schema.literal('security'), - schema.literal('endpoint'), - schema.literal('cloud'), + schema.literal(ProductLine.security), + schema.literal(ProductLine.endpoint), + schema.literal(ProductLine.cloud), ]); + export type SecurityProductLine = TypeOf; export const productTier = schema.oneOf([schema.literal('essentials'), schema.literal('complete')]); diff --git a/x-pack/plugins/serverless_security/common/pli/pli_features.test.ts b/x-pack/plugins/serverless_security/common/pli/pli_features.test.ts index 0d46bcc421ec0..c00f8d7fdd9ec 100644 --- a/x-pack/plugins/serverless_security/common/pli/pli_features.test.ts +++ b/x-pack/plugins/serverless_security/common/pli/pli_features.test.ts @@ -6,6 +6,7 @@ */ import { getProductAppFeatures } from './pli_features'; import * as pliConfig from './pli_config'; +import { ProductLine } from '../config'; describe('getProductAppFeatures', () => { it('should return the essentials PLIs features', () => { @@ -18,7 +19,7 @@ describe('getProductAppFeatures', () => { }; const appFeatureKeys = getProductAppFeatures([ - { product_line: 'security', product_tier: 'essentials' }, + { product_line: ProductLine.security, product_tier: 'essentials' }, ]); expect(appFeatureKeys).toEqual(['foo']); @@ -34,7 +35,7 @@ describe('getProductAppFeatures', () => { }; const appFeatureKeys = getProductAppFeatures([ - { product_line: 'security', product_tier: 'complete' }, + { product_line: ProductLine.security, product_tier: 'complete' }, ]); expect(appFeatureKeys).toEqual(['foo', 'baz']); @@ -58,9 +59,9 @@ describe('getProductAppFeatures', () => { }; const appFeatureKeys = getProductAppFeatures([ - { product_line: 'security', product_tier: 'essentials' }, - { product_line: 'endpoint', product_tier: 'complete' }, - { product_line: 'cloud', product_tier: 'essentials' }, + { product_line: ProductLine.security, product_tier: 'essentials' }, + { product_line: ProductLine.endpoint, product_tier: 'complete' }, + { product_line: ProductLine.cloud, product_tier: 'essentials' }, ]); expect(appFeatureKeys).toEqual(['foo', 'bar', 'repeated', 'qux', 'quux', 'corge', 'garply']); diff --git a/x-pack/plugins/serverless_security/public/components/get_started/get_started.test.tsx b/x-pack/plugins/serverless_security/public/components/get_started/get_started.test.tsx index a81d2525f6a84..ddf0b17985223 100644 --- a/x-pack/plugins/serverless_security/public/components/get_started/get_started.test.tsx +++ b/x-pack/plugins/serverless_security/public/components/get_started/get_started.test.tsx @@ -7,6 +7,7 @@ import React from 'react'; import { render } from '@testing-library/react'; import { GetStartedComponent } from './get_started'; +import { SecurityProductTypes } from '../../../common/config'; jest.mock('./toggle_panel'); jest.mock('./welcome_panel'); @@ -20,9 +21,15 @@ jest.mock('@elastic/eui', () => { }; }); +const productTypes = [ + { product_line: 'security', product_tier: 'essentials' }, + { product_line: 'endpoint', product_tier: 'complete' }, + { product_line: 'cloud', product_tier: 'complete' }, +] as SecurityProductTypes; + describe('GetStartedComponent', () => { it('should render page title, subtitle, and description', () => { - const { getByText } = render(); + const { getByText } = render(); const pageTitle = getByText('Welcome'); const subtitle = getByText('Let’s get started'); @@ -35,8 +42,16 @@ describe('GetStartedComponent', () => { expect(description).toBeInTheDocument(); }); + it('should render Product Switch', () => { + const { getByTestId } = render(); + + const productSwitch = getByTestId('product-switch'); + + expect(productSwitch).toBeInTheDocument(); + }); + it('should render WelcomePanel and TogglePanel', () => { - const { getByTestId } = render(); + const { getByTestId } = render(); const welcomePanel = getByTestId('welcome-panel'); const togglePanel = getByTestId('toggle-panel'); diff --git a/x-pack/plugins/serverless_security/public/components/get_started/get_started.tsx b/x-pack/plugins/serverless_security/public/components/get_started/get_started.tsx index 1532528b6ff53..b29ba3b30d6a4 100644 --- a/x-pack/plugins/serverless_security/public/components/get_started/get_started.tsx +++ b/x-pack/plugins/serverless_security/public/components/get_started/get_started.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { EuiTitle, useEuiTheme } from '@elastic/eui'; +import { EuiTitle, useEuiTheme, useEuiShadow } from '@elastic/eui'; import React from 'react'; import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template'; import { css } from '@emotion/react'; @@ -17,10 +17,22 @@ import { GET_STARTED_PAGE_SUBTITLE, GET_STARTED_PAGE_TITLE, } from './translations'; +import { SecurityProductTypes } from '../../../common/config'; +import { ProductSwitch } from './product_switch'; +import { useTogglePanel } from './use_toggle_panel'; -export const GetStartedComponent: React.FC = () => { - const { euiTheme } = useEuiTheme(); +const CONTENT_WIDTH = 1150; +export const GetStartedComponent: React.FC<{ productTypes: SecurityProductTypes }> = ({ + productTypes, +}) => { + const { euiTheme } = useEuiTheme(); + const shadow = useEuiShadow('s'); + const { + onProductSwitchChanged, + onStepClicked, + state: { activeProducts, activeCards, finishedSteps }, + } = useTogglePanel({ productTypes }); return ( { `} > { > + + + - + ); diff --git a/x-pack/plugins/serverless_security/public/components/get_started/helpers.test.ts b/x-pack/plugins/serverless_security/public/components/get_started/helpers.test.ts index 0a4e32834bc94..f5511d4eaa85d 100644 --- a/x-pack/plugins/serverless_security/public/components/get_started/helpers.test.ts +++ b/x-pack/plugins/serverless_security/public/components/get_started/helpers.test.ts @@ -13,18 +13,18 @@ import { updateCard, } from './helpers'; import { - ActiveCard, + ActiveCards, Card, CardId, GetMoreFromElasticSecurityCardId, GetSetUpCardId, IntroductionSteps, - ProductId, Section, SectionId, StepId, } from './types'; import * as sectionsConfigs from './sections'; +import { ProductLine } from '../../../common/config'; const mockSections = jest.spyOn(sectionsConfigs, 'getSections'); describe('getCardTimeInMinutes', () => { it('should calculate the total time in minutes for a card correctly', () => { @@ -74,8 +74,8 @@ describe('getCardStepsLeft', () => { describe('isCardActive', () => { it('should return true if the card is active based on the active products', () => { - const card = { productTypeRequired: [ProductId.analytics, ProductId.cloud] } as Card; - const activeProducts = new Set([ProductId.analytics]); + const card = { productLineRequired: [ProductLine.security, ProductLine.cloud] } as Card; + const activeProducts = new Set([ProductLine.security]); const isActive = isCardActive(card, activeProducts); @@ -84,7 +84,7 @@ describe('isCardActive', () => { it('should return true if the card has no product type requirement', () => { const card = {} as Card; - const activeProducts = new Set([ProductId.analytics]); + const activeProducts = new Set([ProductLine.security]); const isActive = isCardActive(card, activeProducts); @@ -92,8 +92,8 @@ describe('isCardActive', () => { }); it('should return false if the card is not active based on the active products', () => { - const card = { productTypeRequired: [ProductId.analytics, ProductId.cloud] } as Card; - const activeProducts = new Set([ProductId.endpoint]); + const card = { productLineRequired: [ProductLine.security, ProductLine.cloud] } as Card; + const activeProducts = new Set([ProductLine.endpoint]); const isActive = isCardActive(card, activeProducts); @@ -140,7 +140,7 @@ describe('setupCards', () => { }; it('should set up active cards based on active products', () => { const finishedSteps = {} as unknown as Record>; - const activeProducts = new Set([ProductId.cloud]); + const activeProducts = new Set([ProductLine.cloud]); const activeCards = setupCards(finishedSteps, activeProducts); @@ -148,8 +148,8 @@ describe('setupCards', () => { ...analyticProductActiveCards, [SectionId.getSetUp]: { ...analyticProductActiveCards[SectionId.getSetUp], - [GetSetUpCardId.protectYourEnvironmentInRuntime]: { - id: GetSetUpCardId.protectYourEnvironmentInRuntime, + [GetSetUpCardId.protectYourEnvironmentInRealtime]: { + id: GetSetUpCardId.protectYourEnvironmentInRealtime, timeInMins: 0, stepsLeft: 0, }, @@ -159,7 +159,7 @@ describe('setupCards', () => { it('should skip inactive cards based on finished steps and active products', () => { const finishedSteps = {} as Record>; - const activeProducts = new Set([ProductId.analytics]); + const activeProducts = new Set([ProductLine.security]); const activeCards = setupCards(finishedSteps, activeProducts); @@ -171,7 +171,7 @@ describe('setupCards', () => { [GetSetUpCardId.introduction]: new Set([IntroductionSteps.watchOverviewVideo]), } as unknown as Record>; - const activeProducts: Set = new Set(); + const activeProducts: Set = new Set(); const activeCards = setupCards(finishedSteps, activeProducts); @@ -188,7 +188,7 @@ describe('setupCards', () => { const finishedSteps = { [GetSetUpCardId.introduction]: new Set([IntroductionSteps.watchOverviewVideo]), } as unknown as Record>; - const activeProducts = new Set([ProductId.analytics]); + const activeProducts = new Set([ProductLine.security]); const activeCards = setupCards(finishedSteps, activeProducts); @@ -202,7 +202,7 @@ describe('updateCard', () => { const finishedSteps = { [GetSetUpCardId.introduction]: new Set([IntroductionSteps.watchOverviewVideo]), } as unknown as Record>; - const activeProducts = new Set([ProductId.analytics, ProductId.cloud]); + const activeProducts = new Set([ProductLine.security, ProductLine.cloud]); const activeCards = { [SectionId.getSetUp]: { @@ -221,8 +221,8 @@ describe('updateCard', () => { timeInMins: 0, stepsLeft: 0, }, - [GetSetUpCardId.protectYourEnvironmentInRuntime]: { - id: GetSetUpCardId.protectYourEnvironmentInRuntime, + [GetSetUpCardId.protectYourEnvironmentInRealtime]: { + id: GetSetUpCardId.protectYourEnvironmentInRealtime, timeInMins: 0, stepsLeft: 0, }, @@ -244,7 +244,7 @@ describe('updateCard', () => { timeInMins: 0, }, }, - } as Record>; + } as ActiveCards; it('should update the active card based on finished steps and active products', () => { const sectionId = SectionId.getSetUp; @@ -273,7 +273,7 @@ describe('updateCard', () => { it('should return null if the card is inactive based on active products', () => { const sectionId = SectionId.getSetUp; - const cardId = GetSetUpCardId.protectYourEnvironmentInRuntime; + const cardId = GetSetUpCardId.protectYourEnvironmentInRealtime; const updatedCards = updateCard({ finishedSteps, diff --git a/x-pack/plugins/serverless_security/public/components/get_started/helpers.ts b/x-pack/plugins/serverless_security/public/components/get_started/helpers.ts index 1f4380e5afae6..0042550a34c4b 100644 --- a/x-pack/plugins/serverless_security/public/components/get_started/helpers.ts +++ b/x-pack/plugins/serverless_security/public/components/get_started/helpers.ts @@ -5,8 +5,9 @@ * 2.0. */ +import { ProductLine } from '../../../common/config'; import { getSections } from './sections'; -import { ActiveCard, Card, CardId, ProductId, SectionId, StepId } from './types'; +import { ActiveCard, ActiveCards, Card, CardId, SectionId, StepId } from './types'; export const getCardTimeInMinutes = (card: Card, stepsDone: Set) => card.steps?.reduce( @@ -18,13 +19,13 @@ export const getCardTimeInMinutes = (card: Card, stepsDone: Set) => export const getCardStepsLeft = (card: Card, stepsDone: Set) => (card.steps?.length ?? 0) - (stepsDone.size ?? 0); -export const isCardActive = (card: Card, activeProducts: Set) => - !card.productTypeRequired || - card.productTypeRequired?.some((condition) => activeProducts.has(condition)); +export const isCardActive = (card: Card, activeProducts: Set) => + !card.productLineRequired || + card.productLineRequired?.some((condition) => activeProducts.has(condition)); export const setupCards = ( finishedSteps: Record>, - activeProducts: Set + activeProducts: Set ) => activeProducts.size > 0 ? getSections().reduce((acc, section) => { @@ -46,7 +47,7 @@ export const setupCards = ( acc[section.id] = cardsInSections; } return acc; - }, {} as Record>) + }, {} as ActiveCards) : null; export const updateCard = ({ @@ -57,11 +58,11 @@ export const updateCard = ({ cardId, }: { finishedSteps: Record>; - activeProducts: Set; - activeCards: Record> | null; + activeProducts: Set; + activeCards: ActiveCards | null; sectionId: SectionId; cardId: CardId; -}): Record> | null => { +}): ActiveCards | null => { const sections = getSections(); const section = sections.find(({ id }) => id === sectionId); const cards = section?.cards; diff --git a/x-pack/plugins/serverless_security/public/components/get_started/index.tsx b/x-pack/plugins/serverless_security/public/components/get_started/index.tsx index 88227330bfde9..48326496d4422 100644 --- a/x-pack/plugins/serverless_security/public/components/get_started/index.tsx +++ b/x-pack/plugins/serverless_security/public/components/get_started/index.tsx @@ -13,14 +13,16 @@ import type { GetStartedComponent } from './types'; import { GetStarted } from './lazy'; import { KibanaServicesProvider } from '../../services'; import { ServerlessSecurityPluginStartDependencies } from '../../types'; +import { SecurityProductTypes } from '../../../common/config'; export const getSecurityGetStartedComponent = ( core: CoreStart, - pluginsStart: ServerlessSecurityPluginStartDependencies + pluginsStart: ServerlessSecurityPluginStartDependencies, + productTypes: SecurityProductTypes ): GetStartedComponent => { return () => ( - + ); }; diff --git a/x-pack/plugins/serverless_security/public/components/get_started/lazy.tsx b/x-pack/plugins/serverless_security/public/components/get_started/lazy.tsx index f968ad5d2ab22..3130bbe272f52 100644 --- a/x-pack/plugins/serverless_security/public/components/get_started/lazy.tsx +++ b/x-pack/plugins/serverless_security/public/components/get_started/lazy.tsx @@ -6,13 +6,14 @@ */ import React, { lazy, Suspense } from 'react'; import { EuiLoadingLogo } from '@elastic/eui'; +import { SecurityProductTypes } from '../../../common/config'; const GetStartedLazy = lazy(() => import('./get_started')); const centerLogoStyle = { display: 'flex', margin: 'auto' }; -export const GetStarted = () => ( +export const GetStarted = ({ productTypes }: { productTypes: SecurityProductTypes }) => ( }> - + ); diff --git a/x-pack/plugins/serverless_security/public/components/get_started/product_switch.test.tsx b/x-pack/plugins/serverless_security/public/components/get_started/product_switch.test.tsx index 49c0087f59830..29c01ddce5376 100644 --- a/x-pack/plugins/serverless_security/public/components/get_started/product_switch.test.tsx +++ b/x-pack/plugins/serverless_security/public/components/get_started/product_switch.test.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { render, fireEvent } from '@testing-library/react'; import { ProductSwitch } from './product_switch'; import { EuiThemeComputed } from '@elastic/eui'; -import { ProductId } from './types'; +import { ProductLine } from '../../../common/config'; describe('ProductSwitch', () => { const onProductSwitchChangedMock = jest.fn(); @@ -49,12 +49,12 @@ describe('ProductSwitch', () => { fireEvent.click(analyticsSwitch); expect(onProductSwitchChangedMock).toHaveBeenCalledWith( - expect.objectContaining({ id: 'analytics' }) + expect.objectContaining({ id: 'security' }) ); }); it('should have checked switches for activeProducts', () => { - const activeProducts = new Set([ProductId.analytics, ProductId.endpoint]); + const activeProducts = new Set([ProductLine.security, ProductLine.endpoint]); const { getByTestId } = render( { /> ); - const analyticsSwitch = getByTestId('analytics'); + const analyticsSwitch = getByTestId('security'); const cloudSwitch = getByTestId('cloud'); const endpointSwitch = getByTestId('endpoint'); diff --git a/x-pack/plugins/serverless_security/public/components/get_started/product_switch.tsx b/x-pack/plugins/serverless_security/public/components/get_started/product_switch.tsx index 910618c1a3f4c..822e5f3e69ca0 100644 --- a/x-pack/plugins/serverless_security/public/components/get_started/product_switch.tsx +++ b/x-pack/plugins/serverless_security/public/components/get_started/product_switch.tsx @@ -8,30 +8,30 @@ import { EuiPanel, EuiSwitch, EuiText, EuiThemeComputed, EuiTitle } from '@elastic/eui'; import { css } from '@emotion/react'; import React, { useMemo } from 'react'; +import { ProductLine } from '../../../common/config'; import * as i18n from './translations'; -import { ProductId, Switch } from './types'; +import { Switch } from './types'; const switches: Switch[] = [ { - id: ProductId.analytics, + id: ProductLine.security, label: i18n.ANALYTICS_SWITCH_LABEL, }, { - id: ProductId.cloud, + id: ProductLine.cloud, label: i18n.CLOUD_SWITCH_LABEL, }, { - id: ProductId.endpoint, + id: ProductLine.endpoint, label: i18n.ENDPOINT_SWITCH_LABEL, }, ]; const ProductSwitchComponent: React.FC<{ onProductSwitchChanged: (item: Switch) => void; - activeProducts: Set; - shadow?: string; + activeProducts: Set; euiTheme: EuiThemeComputed; -}> = ({ onProductSwitchChanged, activeProducts, euiTheme, shadow = '' }) => { +}> = ({ onProductSwitchChanged, activeProducts, euiTheme }) => { const switchNodes = useMemo( () => switches.map((item) => ( @@ -58,8 +58,7 @@ const ProductSwitchComponent: React.FC<{ paddingSize="none" hasShadow={false} css={css` - padding: ${euiTheme.base * 1.25}px ${euiTheme.base * 2.25}px; - ${shadow}; + padding: ${euiTheme.base * 1.25}px 0; `} borderRadius="none" > diff --git a/x-pack/plugins/serverless_security/public/components/get_started/reducer.test.ts b/x-pack/plugins/serverless_security/public/components/get_started/reducer.test.ts index 970b7ac6dbef0..259923080bfaa 100644 --- a/x-pack/plugins/serverless_security/public/components/get_started/reducer.test.ts +++ b/x-pack/plugins/serverless_security/public/components/get_started/reducer.test.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { ProductLine } from '../../../common/config'; import { reducer, getFinishedStepsInitialStates, @@ -12,12 +13,11 @@ import { getActiveCardsInitialStates, } from './reducer'; import { - ActiveCard, + ActiveCards, CardId, GetSetUpCardId, GetStartedPageActions, IntroductionSteps, - ProductId, SectionId, StepId, ToggleProductAction, @@ -28,25 +28,25 @@ import { describe('reducer', () => { it('should toggle section correctly', () => { const initialState = { - activeProducts: new Set([ProductId.analytics]), + activeProducts: new Set([ProductLine.security]), finishedSteps: {} as Record>, - activeCards: {} as Record> | null, + activeCards: {} as ActiveCards | null, }; const action: ToggleProductAction = { type: GetStartedPageActions.ToggleProduct, - payload: { section: ProductId.analytics }, + payload: { section: ProductLine.security }, }; const nextState = reducer(initialState, action); - expect(nextState.activeProducts.has(ProductId.analytics)).toBe(false); + expect(nextState.activeProducts.has(ProductLine.security)).toBe(false); expect(nextState.activeCards).toBeNull(); }); it('should add a finished step correctly', () => { const initialState = { - activeProducts: new Set([ProductId.analytics]), + activeProducts: new Set([ProductLine.security]), finishedSteps: {} as Record>, activeCards: { getSetUp: { @@ -56,7 +56,7 @@ describe('reducer', () => { timeInMins: 3, }, }, - } as unknown as Record> | null, + } as unknown as ActiveCards | null, }; const action: AddFinishedStepAction = { @@ -103,17 +103,17 @@ describe('getFinishedStepsInitialStates', () => { describe('getActiveSectionsInitialStates', () => { it('should return the initial states of active sections correctly', () => { - const activeProducts = [ProductId.analytics]; + const activeProducts = [ProductLine.security]; const initialStates = getActiveSectionsInitialStates({ activeProducts }); - expect(initialStates.has(ProductId.analytics)).toBe(true); + expect(initialStates.has(ProductLine.security)).toBe(true); }); }); describe('getActiveCardsInitialStates', () => { it('should return the initial states of active cards correctly', () => { - const activeProducts = new Set([ProductId.analytics]); + const activeProducts = new Set([ProductLine.security]); const finishedSteps = { [GetSetUpCardId.introduction]: new Set([IntroductionSteps.watchOverviewVideo]), } as unknown as Record>; diff --git a/x-pack/plugins/serverless_security/public/components/get_started/reducer.tsx b/x-pack/plugins/serverless_security/public/components/get_started/reducer.tsx index 2164bbee177bd..403bfb85e0a93 100644 --- a/x-pack/plugins/serverless_security/public/components/get_started/reducer.tsx +++ b/x-pack/plugins/serverless_security/public/components/get_started/reducer.tsx @@ -5,11 +5,11 @@ * 2.0. */ +import { ProductLine } from '../../../common/config'; import { setupCards, updateCard } from './helpers'; import { CardId, GetStartedPageActions, - ProductId, StepId, ToggleProductAction, TogglePanelReducer, @@ -80,13 +80,13 @@ export const getFinishedStepsInitialStates = ({ export const getActiveSectionsInitialStates = ({ activeProducts, }: { - activeProducts: ProductId[]; + activeProducts: ProductLine[]; }) => new Set(activeProducts); export const getActiveCardsInitialStates = ({ activeProducts, finishedSteps, }: { - activeProducts: Set; + activeProducts: Set; finishedSteps: Record>; }) => setupCards(finishedSteps, activeProducts); diff --git a/x-pack/plugins/serverless_security/public/components/get_started/sections.tsx b/x-pack/plugins/serverless_security/public/components/get_started/sections.tsx index 4a03eaff2dd76..33dbb6f62aa40 100644 --- a/x-pack/plugins/serverless_security/public/components/get_started/sections.tsx +++ b/x-pack/plugins/serverless_security/public/components/get_started/sections.tsx @@ -8,7 +8,6 @@ import React from 'react'; import { Section, - ProductId, SectionId, GetMoreFromElasticSecurityCardId, GetSetUpCardId, @@ -17,12 +16,7 @@ import { import * as i18n from './translations'; import respond from './images/respond.svg'; import protect from './images/protect.svg'; - -export const ActiveConditions = { - analyticsToggled: [ProductId.analytics], - cloudToggled: [ProductId.cloud], - endpointToggled: [ProductId.endpoint], -}; +import { ProductLine } from '../../../common/config'; export const introductionSteps = [ { @@ -85,11 +79,8 @@ export const sections: Section[] = [ { icon: { type: protect, size: 'xl' }, title: i18n.PROTECT_YOUR_ENVIRONMENT_TITLE, - id: GetSetUpCardId.protectYourEnvironmentInRuntime, - productTypeRequired: [ - ...ActiveConditions.cloudToggled, - ...ActiveConditions.endpointToggled, - ], + id: GetSetUpCardId.protectYourEnvironmentInRealtime, + productLineRequired: [ProductLine.cloud, ProductLine.endpoint], }, ], }, diff --git a/x-pack/plugins/serverless_security/public/components/get_started/toggle_panel.test.tsx b/x-pack/plugins/serverless_security/public/components/get_started/toggle_panel.test.tsx index a8350826ed6da..53f51cdc09a8b 100644 --- a/x-pack/plugins/serverless_security/public/components/get_started/toggle_panel.test.tsx +++ b/x-pack/plugins/serverless_security/public/components/get_started/toggle_panel.test.tsx @@ -5,10 +5,11 @@ * 2.0. */ import React from 'react'; -import { render, fireEvent } from '@testing-library/react'; +import { render } from '@testing-library/react'; import { TogglePanel } from './toggle_panel'; -import { getStartedStorage as mockGetStartedStorage } from '../../lib/get_started/storage'; import { useSetUpCardSections } from './use_setup_cards'; +import { ActiveCards, CardId, GetSetUpCardId, IntroductionSteps, SectionId, StepId } from './types'; +import { ProductLine } from '../../../common/config'; jest.mock('@elastic/eui', () => ({ ...jest.requireActual('@elastic/eui'), @@ -30,23 +31,57 @@ jest.mock('./use_setup_cards', () => ({ useSetUpCardSections: jest.fn(), })); +const finishedSteps = { + [GetSetUpCardId.introduction]: new Set([IntroductionSteps.watchOverviewVideo]), +} as unknown as Record>; +const activeProducts = new Set([ProductLine.security, ProductLine.cloud]); + +const activeCards = { + [SectionId.getSetUp]: { + [GetSetUpCardId.introduction]: { + id: GetSetUpCardId.introduction, + timeInMins: 3, + stepsLeft: 1, + }, + [GetSetUpCardId.bringInYourData]: { + id: GetSetUpCardId.bringInYourData, + timeInMins: 0, + stepsLeft: 0, + }, + [GetSetUpCardId.activateAndCreateRules]: { + id: GetSetUpCardId.activateAndCreateRules, + timeInMins: 0, + stepsLeft: 0, + }, + [GetSetUpCardId.protectYourEnvironmentInRealtime]: { + id: GetSetUpCardId.protectYourEnvironmentInRealtime, + timeInMins: 0, + stepsLeft: 0, + }, + }, +} as ActiveCards; + describe('TogglePanel', () => { - const mockUseSetUpCardSections = { setUpSections: jest.fn(() => null) }; + const mockUseSetUpCardSections = { + setUpSections: jest.fn(() =>
), + }; + const onStepClicked = jest.fn(); beforeEach(() => { jest.clearAllMocks(); (useSetUpCardSections as jest.Mock).mockReturnValue(mockUseSetUpCardSections); }); - it('should render the product switch ', () => { - const { getByTestId } = render(); - - expect(getByTestId('product-switch')).toBeInTheDocument(); - }); - it('should render empty prompt', () => { - const { getByText } = render(); + const { getByText } = render( + + ); expect(getByText(`Hmm, there doesn't seem to be anything there`)).toBeInTheDocument(); expect( @@ -54,16 +89,16 @@ describe('TogglePanel', () => { ).toBeInTheDocument(); }); - it('should toggle active sections when a product switch is changed', () => { - const { getByText } = render(); - - const analyticsSwitch = getByText('Analytics'); - const cloudSwitch = getByText('Cloud'); - - fireEvent.click(analyticsSwitch); - expect(mockGetStartedStorage.toggleActiveProductsInStorage).toHaveBeenCalledWith('analytics'); + it('should render sections', () => { + const { getByTestId } = render( + + ); - fireEvent.click(cloudSwitch); - expect(mockGetStartedStorage.toggleActiveProductsInStorage).toHaveBeenCalledWith('cloud'); + expect(getByTestId(`mock-sections`)).toBeInTheDocument(); }); }); diff --git a/x-pack/plugins/serverless_security/public/components/get_started/toggle_panel.tsx b/x-pack/plugins/serverless_security/public/components/get_started/toggle_panel.tsx index 3ce851fc8a232..af365b83bd80c 100644 --- a/x-pack/plugins/serverless_security/public/components/get_started/toggle_panel.tsx +++ b/x-pack/plugins/serverless_security/public/components/get_started/toggle_panel.tsx @@ -5,98 +5,46 @@ * 2.0. */ -import React, { useCallback, useMemo, useReducer } from 'react'; +import React from 'react'; import { EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem, useEuiShadow, useEuiTheme } from '@elastic/eui'; import { css } from '@emotion/react'; -import { Switch, GetStartedPageActions, StepId, CardId, SectionId } from './types'; import * as i18n from './translations'; -import { ProductSwitch } from './product_switch'; import { useSetUpCardSections } from './use_setup_cards'; -import { getStartedStorage } from '../../lib/get_started/storage'; -import { - getActiveCardsInitialStates, - getActiveSectionsInitialStates, - getFinishedStepsInitialStates, - reducer, -} from './reducer'; -const TogglePanelComponent = () => { +import { ActiveCards, CardId, IntroductionSteps, SectionId } from './types'; +import { ProductLine } from '../../../common/config'; + +const TogglePanelComponent: React.FC<{ + finishedSteps: Record>; + activeCards: ActiveCards | null; + activeProducts: Set; + onStepClicked: ({ + stepId, + cardId, + sectionId, + }: { + stepId: IntroductionSteps; + cardId: CardId; + sectionId: SectionId; + }) => void; +}> = ({ finishedSteps, activeCards, activeProducts, onStepClicked }) => { const { euiTheme } = useEuiTheme(); const shadow = useEuiShadow('s'); - const { - getAllFinishedStepsFromStorage, - getActiveProductsFromStorage, - toggleActiveProductsInStorage, - addFinishedStepToStorage, - } = getStartedStorage; - const finishedStepsInitialStates = useMemo( - () => getFinishedStepsInitialStates({ finishedSteps: getAllFinishedStepsFromStorage() }), - [getAllFinishedStepsFromStorage] - ); - - const activeSectionsInitialStates = useMemo( - () => getActiveSectionsInitialStates({ activeProducts: getActiveProductsFromStorage() }), - [getActiveProductsFromStorage] - ); - - const activeCardsInitialStates = useMemo( - () => - getActiveCardsInitialStates({ - activeProducts: activeSectionsInitialStates, - finishedSteps: finishedStepsInitialStates, - }), - [activeSectionsInitialStates, finishedStepsInitialStates] - ); - const [state, dispatch] = useReducer(reducer, { - activeProducts: activeSectionsInitialStates, - finishedSteps: finishedStepsInitialStates, - activeCards: activeCardsInitialStates, - }); const { setUpSections } = useSetUpCardSections({ euiTheme, shadow }); - const onStepClicked = useCallback( - ({ stepId, cardId, sectionId }: { stepId: StepId; cardId: CardId; sectionId: SectionId }) => { - dispatch({ - type: GetStartedPageActions.AddFinishedStep, - payload: { stepId, cardId, sectionId }, - }); - addFinishedStepToStorage(cardId, stepId); - }, - [addFinishedStepToStorage] - ); const sectionNodes = setUpSections({ onStepClicked, - finishedSteps: state.finishedSteps, - activeCards: state.activeCards, + finishedSteps, + activeCards, }); - const onProductSwitchChanged = useCallback( - (section: Switch) => { - dispatch({ type: GetStartedPageActions.ToggleProduct, payload: { section: section.id } }); - toggleActiveProductsInStorage(section.id); - }, - [toggleActiveProductsInStorage] - ); return ( - - - - - {state.activeProducts.size > 0 ? ( + + {activeProducts.size > 0 ? ( sectionNodes ) : ( >; export enum SectionId { getSetUp = 'getSetUp', @@ -71,7 +68,7 @@ export enum GetSetUpCardId { activateAndCreateRules = 'activateAndCreateRules', bringInYourData = 'bringInYourData', introduction = 'introduction', - protectYourEnvironmentInRuntime = 'protectYourEnvironmentInRuntime', + protectYourEnvironmentInRealtime = 'protectYourEnvironmentInRealtime', } export enum IntroductionSteps { @@ -90,14 +87,14 @@ export interface ActiveCard { stepsLeft: number; } export interface TogglePanelReducer { - activeProducts: Set; + activeProducts: Set; finishedSteps: Record>; activeCards: Record> | null; } export interface ToggleProductAction { type: GetStartedPageActions.ToggleProduct; - payload: { section: ProductId }; + payload: { section: ProductLine }; } export interface AddFinishedStepAction { @@ -106,7 +103,7 @@ export interface AddFinishedStepAction { } export interface Switch { - id: ProductId; + id: ProductLine; label: string; } diff --git a/x-pack/plugins/serverless_security/public/components/get_started/use_setup_cards.test.tsx b/x-pack/plugins/serverless_security/public/components/get_started/use_setup_cards.test.tsx index dcf8c6a3cfb38..6e726fd9a002f 100644 --- a/x-pack/plugins/serverless_security/public/components/get_started/use_setup_cards.test.tsx +++ b/x-pack/plugins/serverless_security/public/components/get_started/use_setup_cards.test.tsx @@ -9,7 +9,7 @@ import { renderHook } from '@testing-library/react-hooks'; import { EuiThemeComputed } from '@elastic/eui'; import { useSetUpCardSections } from './use_setup_cards'; import { - ActiveCard, + ActiveCards, CardId, GetMoreFromElasticSecurityCardId, GetSetUpCardId, @@ -44,7 +44,7 @@ describe('useSetUpCardSections', () => { id: GetMoreFromElasticSecurityCardId.masterTheInvestigationsWorkflow, }, }, - } as Record>; + } as ActiveCards; const sections = result.current.setUpSections({ activeCards, diff --git a/x-pack/plugins/serverless_security/public/components/get_started/use_setup_cards.tsx b/x-pack/plugins/serverless_security/public/components/get_started/use_setup_cards.tsx index 082aadeae88ee..bd762042196ee 100644 --- a/x-pack/plugins/serverless_security/public/components/get_started/use_setup_cards.tsx +++ b/x-pack/plugins/serverless_security/public/components/get_started/use_setup_cards.tsx @@ -9,7 +9,7 @@ import { EuiSpacer, EuiThemeComputed } from '@elastic/eui'; import React, { useCallback } from 'react'; import { css } from '@emotion/react'; import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiTitle } from '@elastic/eui'; -import { ActiveCard, CardId, SectionId, StepId } from './types'; +import { ActiveCards, CardId, SectionId, StepId } from './types'; import { CardItem } from './card_item'; import { getSections } from './sections'; @@ -30,7 +30,7 @@ export const useSetUpCardSections = ({ }: { onStepClicked: (params: { stepId: StepId; cardId: CardId; sectionId: SectionId }) => void; finishedSteps: Record>; - activeCards: Record> | null; + activeCards: ActiveCards | null; sectionId: SectionId; }) => { const section = activeCards?.[sectionId]; @@ -63,7 +63,7 @@ export const useSetUpCardSections = ({ }: { onStepClicked: (params: { stepId: StepId; cardId: CardId; sectionId: SectionId }) => void; finishedSteps: Record>; - activeCards: Record> | null; + activeCards: ActiveCards | null; }) => getSections().reduce((acc, currentSection) => { const cardNodes = setUpCards({ diff --git a/x-pack/plugins/serverless_security/public/components/get_started/use_toggle_panel.test.tsx b/x-pack/plugins/serverless_security/public/components/get_started/use_toggle_panel.test.tsx new file mode 100644 index 0000000000000..6201009395935 --- /dev/null +++ b/x-pack/plugins/serverless_security/public/components/get_started/use_toggle_panel.test.tsx @@ -0,0 +1,239 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor 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 { renderHook, act } from '@testing-library/react-hooks'; +import { useTogglePanel } from './use_toggle_panel'; +import { getStartedStorage } from '../../lib/get_started/storage'; +import { ProductLine, SecurityProductTypes } from '../../../common/config'; +import { + GetMoreFromElasticSecurityCardId, + GetSetUpCardId, + IntroductionSteps, + SectionId, +} from './types'; + +jest.mock('../../lib/get_started/storage'); + +describe('useTogglePanel', () => { + const productTypes = [ + { product_line: 'security', product_tier: 'essentials' }, + { product_line: 'endpoint', product_tier: 'complete' }, + ] as SecurityProductTypes; + + beforeEach(() => { + jest.clearAllMocks(); + + (getStartedStorage.getAllFinishedStepsFromStorage as jest.Mock).mockReturnValue({ + [GetSetUpCardId.introduction]: new Set([IntroductionSteps.watchOverviewVideo]), + }); + (getStartedStorage.getActiveProductsFromStorage as jest.Mock).mockReturnValue([ + ProductLine.security, + ProductLine.cloud, + ProductLine.endpoint, + ]); + }); + + test('should initialize state with correct initial values - when no active products from local storage', () => { + (getStartedStorage.getAllFinishedStepsFromStorage as jest.Mock).mockReturnValue({}); + (getStartedStorage.getActiveProductsFromStorage as jest.Mock).mockReturnValue([]); + + const { result } = renderHook(() => useTogglePanel({ productTypes })); + + const { state } = result.current; + + expect(state.activeProducts).toEqual(new Set([ProductLine.security, ProductLine.endpoint])); + expect(state.finishedSteps).toEqual({}); + + expect(state.activeCards).toEqual( + expect.objectContaining({ + [SectionId.getSetUp]: { + [GetSetUpCardId.introduction]: { + id: GetSetUpCardId.introduction, + timeInMins: 3, + stepsLeft: 1, + }, + [GetSetUpCardId.activateAndCreateRules]: { + id: GetSetUpCardId.activateAndCreateRules, + timeInMins: 0, + stepsLeft: 0, + }, + [GetSetUpCardId.bringInYourData]: { + id: GetSetUpCardId.bringInYourData, + timeInMins: 0, + stepsLeft: 0, + }, + [GetSetUpCardId.protectYourEnvironmentInRealtime]: { + id: GetSetUpCardId.protectYourEnvironmentInRealtime, + timeInMins: 0, + stepsLeft: 0, + }, + }, + [SectionId.getMoreFromElasticSecurity]: { + [GetMoreFromElasticSecurityCardId.masterTheInvestigationsWorkflow]: { + id: GetMoreFromElasticSecurityCardId.masterTheInvestigationsWorkflow, + timeInMins: 0, + stepsLeft: 0, + }, + [GetMoreFromElasticSecurityCardId.optimizeYourWorkSpace]: { + id: GetMoreFromElasticSecurityCardId.optimizeYourWorkSpace, + timeInMins: 0, + stepsLeft: 0, + }, + [GetMoreFromElasticSecurityCardId.respondToThreats]: { + id: GetMoreFromElasticSecurityCardId.respondToThreats, + timeInMins: 0, + stepsLeft: 0, + }, + }, + }) + ); + }); + + test('should initialize state with correct initial values - when all products active', () => { + const { result } = renderHook(() => useTogglePanel({ productTypes })); + + const { state } = result.current; + + expect(state.activeProducts).toEqual( + new Set([ProductLine.security, ProductLine.cloud, ProductLine.endpoint]) + ); + expect(state.finishedSteps).toEqual({ + [GetSetUpCardId.introduction]: new Set([IntroductionSteps.watchOverviewVideo]), + }); + + expect(state.activeCards).toEqual( + expect.objectContaining({ + [SectionId.getSetUp]: { + [GetSetUpCardId.introduction]: { + id: GetSetUpCardId.introduction, + timeInMins: 0, + stepsLeft: 0, + }, + [GetSetUpCardId.activateAndCreateRules]: { + id: GetSetUpCardId.activateAndCreateRules, + timeInMins: 0, + stepsLeft: 0, + }, + [GetSetUpCardId.bringInYourData]: { + id: GetSetUpCardId.bringInYourData, + timeInMins: 0, + stepsLeft: 0, + }, + [GetSetUpCardId.protectYourEnvironmentInRealtime]: { + id: GetSetUpCardId.protectYourEnvironmentInRealtime, + timeInMins: 0, + stepsLeft: 0, + }, + }, + [SectionId.getMoreFromElasticSecurity]: { + [GetMoreFromElasticSecurityCardId.masterTheInvestigationsWorkflow]: { + id: GetMoreFromElasticSecurityCardId.masterTheInvestigationsWorkflow, + timeInMins: 0, + stepsLeft: 0, + }, + [GetMoreFromElasticSecurityCardId.optimizeYourWorkSpace]: { + id: GetMoreFromElasticSecurityCardId.optimizeYourWorkSpace, + timeInMins: 0, + stepsLeft: 0, + }, + [GetMoreFromElasticSecurityCardId.respondToThreats]: { + id: GetMoreFromElasticSecurityCardId.respondToThreats, + timeInMins: 0, + stepsLeft: 0, + }, + }, + }) + ); + }); + + test('should initialize state with correct initial values - when only security product active', () => { + (getStartedStorage.getActiveProductsFromStorage as jest.Mock).mockReturnValue([ + ProductLine.security, + ]); + const { result } = renderHook(() => useTogglePanel({ productTypes })); + + const { state } = result.current; + + expect(state.activeProducts).toEqual(new Set([ProductLine.security])); + expect(state.finishedSteps).toEqual({ + [GetSetUpCardId.introduction]: new Set([IntroductionSteps.watchOverviewVideo]), + }); + + expect(state.activeCards).toEqual( + expect.objectContaining({ + [SectionId.getSetUp]: { + [GetSetUpCardId.introduction]: { + id: GetSetUpCardId.introduction, + timeInMins: 0, + stepsLeft: 0, + }, + [GetSetUpCardId.activateAndCreateRules]: { + id: GetSetUpCardId.activateAndCreateRules, + timeInMins: 0, + stepsLeft: 0, + }, + [GetSetUpCardId.bringInYourData]: { + id: GetSetUpCardId.bringInYourData, + timeInMins: 0, + stepsLeft: 0, + }, + }, + [SectionId.getMoreFromElasticSecurity]: { + [GetMoreFromElasticSecurityCardId.masterTheInvestigationsWorkflow]: { + id: GetMoreFromElasticSecurityCardId.masterTheInvestigationsWorkflow, + timeInMins: 0, + stepsLeft: 0, + }, + [GetMoreFromElasticSecurityCardId.optimizeYourWorkSpace]: { + id: GetMoreFromElasticSecurityCardId.optimizeYourWorkSpace, + timeInMins: 0, + stepsLeft: 0, + }, + [GetMoreFromElasticSecurityCardId.respondToThreats]: { + id: GetMoreFromElasticSecurityCardId.respondToThreats, + timeInMins: 0, + stepsLeft: 0, + }, + }, + }) + ); + }); + + test('should call addFinishedStepToStorage', () => { + const { result } = renderHook(() => useTogglePanel({ productTypes })); + + const { onStepClicked } = result.current; + + act(() => { + onStepClicked({ + stepId: IntroductionSteps.watchOverviewVideo, + cardId: GetSetUpCardId.introduction, + sectionId: SectionId.getSetUp, + }); + }); + + expect(getStartedStorage.addFinishedStepToStorage).toHaveBeenCalledTimes(1); + expect(getStartedStorage.addFinishedStepToStorage).toHaveBeenCalledWith( + GetSetUpCardId.introduction, + IntroductionSteps.watchOverviewVideo + ); + }); + + test('should call toggleActiveProductsInStorage', () => { + const { result } = renderHook(() => useTogglePanel({ productTypes })); + + const { onProductSwitchChanged } = result.current; + + act(() => { + onProductSwitchChanged({ id: ProductLine.security, label: 'Analytics' }); + }); + + expect(getStartedStorage.toggleActiveProductsInStorage).toHaveBeenCalledTimes(1); + expect(getStartedStorage.toggleActiveProductsInStorage).toHaveBeenCalledWith( + ProductLine.security + ); + }); +}); diff --git a/x-pack/plugins/serverless_security/public/components/get_started/use_toggle_panel.tsx b/x-pack/plugins/serverless_security/public/components/get_started/use_toggle_panel.tsx new file mode 100644 index 0000000000000..f449478572c45 --- /dev/null +++ b/x-pack/plugins/serverless_security/public/components/get_started/use_toggle_panel.tsx @@ -0,0 +1,77 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor 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 { useCallback, useMemo, useReducer } from 'react'; +import { ProductLine, SecurityProductTypes } from '../../../common/config'; +import { getStartedStorage } from '../../lib/get_started/storage'; +import { + getActiveCardsInitialStates, + getActiveSectionsInitialStates, + getFinishedStepsInitialStates, + reducer, +} from './reducer'; +import { CardId, GetStartedPageActions, SectionId, StepId, Switch } from './types'; + +export const useTogglePanel = ({ productTypes }: { productTypes: SecurityProductTypes }) => { + const { + getAllFinishedStepsFromStorage, + getActiveProductsFromStorage, + toggleActiveProductsInStorage, + addFinishedStepToStorage, + } = getStartedStorage; + + const finishedStepsInitialStates = useMemo( + () => getFinishedStepsInitialStates({ finishedSteps: getAllFinishedStepsFromStorage() }), + [getAllFinishedStepsFromStorage] + ); + + const activeSectionsInitialStates = useMemo(() => { + const activeProductsFromStorage = getActiveSectionsInitialStates({ + activeProducts: getActiveProductsFromStorage(), + }); + return activeProductsFromStorage.size > 0 + ? activeProductsFromStorage + : new Set(productTypes.map(({ product_line: productLine }) => ProductLine[productLine])) ?? + new Set([ProductLine.security, ProductLine.endpoint, ProductLine.cloud]); + }, [getActiveProductsFromStorage, productTypes]); + + const activeCardsInitialStates = useMemo( + () => + getActiveCardsInitialStates({ + activeProducts: activeSectionsInitialStates, + finishedSteps: finishedStepsInitialStates, + }), + [activeSectionsInitialStates, finishedStepsInitialStates] + ); + + const [state, dispatch] = useReducer(reducer, { + activeProducts: activeSectionsInitialStates, + finishedSteps: finishedStepsInitialStates, + activeCards: activeCardsInitialStates, + }); + + const onStepClicked = useCallback( + ({ stepId, cardId, sectionId }: { stepId: StepId; cardId: CardId; sectionId: SectionId }) => { + dispatch({ + type: GetStartedPageActions.AddFinishedStep, + payload: { stepId, cardId, sectionId }, + }); + addFinishedStepToStorage(cardId, stepId); + }, + [addFinishedStepToStorage] + ); + + const onProductSwitchChanged = useCallback( + (section: Switch) => { + dispatch({ type: GetStartedPageActions.ToggleProduct, payload: { section: section.id } }); + toggleActiveProductsInStorage(section.id); + }, + [toggleActiveProductsInStorage] + ); + + return { state, onStepClicked, onProductSwitchChanged }; +}; diff --git a/x-pack/plugins/serverless_security/public/components/upselling/register_upsellings.test.tsx b/x-pack/plugins/serverless_security/public/components/upselling/register_upsellings.test.tsx index b2a1390ee098f..304ee02de256c 100644 --- a/x-pack/plugins/serverless_security/public/components/upselling/register_upsellings.test.tsx +++ b/x-pack/plugins/serverless_security/public/components/upselling/register_upsellings.test.tsx @@ -8,7 +8,7 @@ import { UpsellingService } from '@kbn/security-solution-plugin/public'; import { ALL_APP_FEATURE_KEYS } from '@kbn/security-solution-plugin/common'; import { registerUpsellings, upsellingPages, upsellingSections } from './register_upsellings'; -import type { SecurityProductTypes } from '../../../common/config'; +import { ProductLine, SecurityProductTypes } from '../../../common/config'; const mockGetProductAppFeatures = jest.fn(); jest.mock('../../../common/pli/pli_features', () => ({ @@ -16,9 +16,9 @@ jest.mock('../../../common/pli/pli_features', () => ({ })); const allProductTypes: SecurityProductTypes = [ - { product_line: 'security', product_tier: 'complete' }, - { product_line: 'endpoint', product_tier: 'complete' }, - { product_line: 'cloud', product_tier: 'complete' }, + { product_line: ProductLine.security, product_tier: 'complete' }, + { product_line: ProductLine.endpoint, product_tier: 'complete' }, + { product_line: ProductLine.cloud, product_tier: 'complete' }, ]; describe('registerUpsellings', () => { diff --git a/x-pack/plugins/serverless_security/public/lib/get_started/storage.test.ts b/x-pack/plugins/serverless_security/public/lib/get_started/storage.test.ts index 158c271470768..e55c4ef28948b 100644 --- a/x-pack/plugins/serverless_security/public/lib/get_started/storage.test.ts +++ b/x-pack/plugins/serverless_security/public/lib/get_started/storage.test.ts @@ -6,14 +6,10 @@ */ import { getStartedStorage } from './storage'; -import { - GetSetUpCardId, - IntroductionSteps, - ProductId, - StepId, -} from '../../components/get_started/types'; +import { GetSetUpCardId, IntroductionSteps, StepId } from '../../components/get_started/types'; import { storage } from '../storage'; import { MockStorage } from '../__mocks__/storage'; +import { ProductLine } from '../../../common/config'; jest.mock('../storage'); @@ -33,13 +29,13 @@ describe('useStorage', () => { }); it('should toggle active products in storage', () => { - expect(getStartedStorage.toggleActiveProductsInStorage(ProductId.analytics)).toEqual([ - ProductId.analytics, + expect(getStartedStorage.toggleActiveProductsInStorage(ProductLine.security)).toEqual([ + ProductLine.security, ]); - expect(mockStorage.set).toHaveBeenCalledWith('ACTIVE_PRODUCTS', [ProductId.analytics]); + expect(mockStorage.set).toHaveBeenCalledWith('ACTIVE_PRODUCTS', [ProductLine.security]); - mockStorage.set('ACTIVE_PRODUCTS', [ProductId.analytics]); - expect(getStartedStorage.toggleActiveProductsInStorage(ProductId.analytics)).toEqual([]); + mockStorage.set('ACTIVE_PRODUCTS', [ProductLine.security]); + expect(getStartedStorage.toggleActiveProductsInStorage(ProductLine.security)).toEqual([]); expect(mockStorage.set).toHaveBeenCalledWith('ACTIVE_PRODUCTS', []); }); diff --git a/x-pack/plugins/serverless_security/public/lib/get_started/storage.ts b/x-pack/plugins/serverless_security/public/lib/get_started/storage.ts index 691cbb5f102f0..1d9c52813c832 100644 --- a/x-pack/plugins/serverless_security/public/lib/get_started/storage.ts +++ b/x-pack/plugins/serverless_security/public/lib/get_started/storage.ts @@ -5,7 +5,8 @@ * 2.0. */ -import { CardId, ProductId, StepId } from '../../components/get_started/types'; +import { ProductLine } from '../../../common/config'; +import { CardId, StepId } from '../../components/get_started/types'; import { storage } from '../storage'; export const ACTIVE_PRODUCTS_STORAGE_KEY = 'ACTIVE_PRODUCTS'; @@ -13,12 +14,12 @@ export const FINISHED_STEPS_STORAGE_KEY = 'FINISHED_STEPS'; export const getStartedStorage = { getActiveProductsFromStorage: () => { - const activeProducts: ProductId[] = storage.get(ACTIVE_PRODUCTS_STORAGE_KEY); + const activeProducts: ProductLine[] = storage.get(ACTIVE_PRODUCTS_STORAGE_KEY); return activeProducts ?? new Array(); }, - toggleActiveProductsInStorage: (productId: ProductId) => { - const activeProducts: ProductId[] = - storage.get(ACTIVE_PRODUCTS_STORAGE_KEY) ?? new Array(); + toggleActiveProductsInStorage: (productId: ProductLine) => { + const activeProducts: ProductLine[] = + storage.get(ACTIVE_PRODUCTS_STORAGE_KEY) ?? new Array(); const index = activeProducts.indexOf(productId); if (index < 0) { activeProducts.push(productId); diff --git a/x-pack/plugins/serverless_security/public/plugin.ts b/x-pack/plugins/serverless_security/public/plugin.ts index f0729330de4af..b93be1b16dcd4 100644 --- a/x-pack/plugins/serverless_security/public/plugin.ts +++ b/x-pack/plugins/serverless_security/public/plugin.ts @@ -48,7 +48,9 @@ export class ServerlessSecurityPlugin const { securitySolution, serverless } = startDeps; securitySolution.setIsSidebarEnabled(false); - securitySolution.setGetStartedPage(getSecurityGetStartedComponent(core, startDeps)); + securitySolution.setGetStartedPage( + getSecurityGetStartedComponent(core, startDeps, this.config.productTypes) + ); serverless.setProjectHome('/app/security'); serverless.setSideNavComponent(getSecuritySideNavComponent(core, startDeps));