From a6f9595f22c689df07abfb1c7236a35ab11ddc29 Mon Sep 17 00:00:00 2001 From: Tiago Vila Verde Date: Tue, 12 Nov 2024 12:25:26 +0100 Subject: [PATCH 1/9] simplifying enablement via new APIs --- .../entity_analytics/api/entity_store.ts | 32 ++++++- .../components/dashboard_panels.tsx | 83 +++++++++++-------- .../hooks/use_entity_engine_status.ts | 71 ---------------- .../entity_store/hooks/use_entity_store.ts | 72 +++++++--------- 4 files changed, 106 insertions(+), 152 deletions(-) delete mode 100644 x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/hooks/use_entity_engine_status.ts diff --git a/x-pack/plugins/security_solution/public/entity_analytics/api/entity_store.ts b/x-pack/plugins/security_solution/public/entity_analytics/api/entity_store.ts index 54f5415d24a35..f1afa13637bb8 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/api/entity_store.ts +++ b/x-pack/plugins/security_solution/public/entity_analytics/api/entity_store.ts @@ -5,6 +5,11 @@ * 2.0. */ import { useMemo } from 'react'; +import type { + GetEntityStoreStatusResponse, + InitEntityStoreRequestBody, + InitEntityStoreResponse, +} from '../../../common/api/entity_analytics/entity_store/enablement.gen'; import type { DeleteEntityEngineResponse, EntityType, @@ -20,7 +25,24 @@ export const useEntityStoreRoutes = () => { const http = useKibana().services.http; return useMemo(() => { - const initEntityStore = async (entityType: EntityType) => { + const enableEntityStore = async ( + options: InitEntityStoreRequestBody = { fieldHistoryLength: 10 } + ) => { + return http.fetch('/api/entity_store/enable', { + method: 'POST', + version: API_VERSIONS.public.v1, + body: JSON.stringify(options), + }); + }; + + const getEntityStoreStatus = async () => { + return http.fetch('/api/entity_store/status', { + method: 'GET', + version: API_VERSIONS.public.v1, + }); + }; + + const initEntityEngine = async (entityType: EntityType) => { return http.fetch(`/api/entity_store/engines/${entityType}/init`, { method: 'POST', version: API_VERSIONS.public.v1, @@ -28,7 +50,7 @@ export const useEntityStoreRoutes = () => { }); }; - const stopEntityStore = async (entityType: EntityType) => { + const stopEntityEngine = async (entityType: EntityType) => { return http.fetch(`/api/entity_store/engines/${entityType}/stop`, { method: 'POST', version: API_VERSIONS.public.v1, @@ -59,8 +81,10 @@ export const useEntityStoreRoutes = () => { }; return { - initEntityStore, - stopEntityStore, + enableEntityStore, + getEntityStoreStatus, + initEntityEngine, + stopEntityEngine, getEntityEngine, deleteEntityEngine, listEntityEngines, diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/components/dashboard_panels.tsx b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/components/dashboard_panels.tsx index d70eb9fe34b51..8df069ea15adf 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/components/dashboard_panels.tsx +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/components/dashboard_panels.tsx @@ -24,12 +24,11 @@ import { RiskScoreEntity } from '../../../../../common/search_strategy'; import { EntitiesList } from '../entities_list'; -import { useEntityStoreEnablement } from '../hooks/use_entity_store'; +import { useEnableEntityStoreMutation, useEntityStoreStatus } from '../hooks/use_entity_store'; import { EntityStoreEnablementModal, type Enablements } from './enablement_modal'; import { EntityAnalyticsRiskScores } from '../../entity_analytics_risk_score'; import { useInitRiskEngineMutation } from '../../../api/hooks/use_init_risk_engine_mutation'; -import { useEntityEngineStatus } from '../hooks/use_entity_engine_status'; import dashboardEnableImg from '../../../images/entity_store_dashboard.png'; import { @@ -48,27 +47,42 @@ const EntityStoreDashboardPanelsComponent = () => { const [modal, setModalState] = useState({ visible: false }); const [riskEngineInitializing, setRiskEngineInitializing] = useState(false); - const entityStore = useEntityEngineStatus(); const riskEngineStatus = useRiskEngineStatus(); - - const { enable: enableStore, query } = useEntityStoreEnablement(); - const { mutate: initRiskEngine } = useInitRiskEngineMutation(); - const callouts = entityStore.errors.map((err) => ( - + const storeStatusQuery = useEntityStoreStatus({ + enabled: false, + refetchInterval: (data) => { + if (data?.status !== 'installing') { + return false; } - color="danger" - iconType="error" - > -

{err?.message}

-
- )); + return 5000; + }, + }); + + const enableStore = useEnableEntityStoreMutation({ + onSuccess: () => storeStatusQuery.refetch(), + }); + + const callouts = (storeStatusQuery.data?.engines ?? []).map((engine) => { + const err = engine.error as { + message: string; + }; + return ( + + } + color="danger" + iconType="error" + > +

{err?.message}

+
+ ); + }); const enableEntityStore = (enable: Enablements) => () => { setModalState({ visible: false }); @@ -77,7 +91,7 @@ const EntityStoreDashboardPanelsComponent = () => { onSuccess: () => { setRiskEngineInitializing(false); if (enable.entityStore) { - enableStore(); + enableStore.mutate(); } }, }; @@ -87,11 +101,11 @@ const EntityStoreDashboardPanelsComponent = () => { } if (enable.entityStore) { - enableStore(); + enableStore.mutate(); } }; - if (query.error) { + if (enableStore.error) { return ( <> { color="danger" iconType="error" > -

{(query.error as { body: { message: string } }).body.message}

+

{(enableStore.error as { body: { message: string } }).body.message}

{callouts} ); } - if (entityStore.status === 'loading') { + if (storeStatusQuery.status === 'loading') { return ( { ); } - if (entityStore.status === 'installing') { + if (storeStatusQuery.data?.status === 'installing') { return ( { ); } - // TODO Rename variable because the Risk score could be installed but disabled const isRiskScoreAvailable = riskEngineStatus.data && riskEngineStatus.data.risk_engine_status !== RiskEngineStatusEnum.NOT_INSTALLED; return ( - {entityStore.status === 'error' && isRiskScoreAvailable && ( + {storeStatusQuery.status === 'error' && isRiskScoreAvailable && ( <> {callouts} @@ -159,7 +172,7 @@ const EntityStoreDashboardPanelsComponent = () => { )} - {entityStore.status === 'error' && !isRiskScoreAvailable && ( + {storeStatusQuery.status === 'error' && !isRiskScoreAvailable && ( <> {callouts} @@ -171,7 +184,7 @@ const EntityStoreDashboardPanelsComponent = () => { )} - {entityStore.status === 'enabled' && isRiskScoreAvailable && ( + {storeStatusQuery.data?.status === 'running' && isRiskScoreAvailable && ( <> @@ -184,7 +197,7 @@ const EntityStoreDashboardPanelsComponent = () => { )} - {entityStore.status === 'enabled' && !isRiskScoreAvailable && ( + {storeStatusQuery.data?.status === 'running' && !isRiskScoreAvailable && ( <> { )} - {(entityStore.status === 'not_installed' || entityStore.status === 'stopped') && + {(storeStatusQuery.data?.status === 'not_installed' || + storeStatusQuery.data?.status === 'stopped') && !isRiskScoreAvailable && ( // TODO: Move modal inside EnableEntityStore component, eliminating the onEnable prop in favour of forwarding the riskScoreEnabled status { /> )} - {(entityStore.status === 'not_installed' || entityStore.status === 'stopped') && + {(storeStatusQuery.data?.status === 'not_installed' || + storeStatusQuery.data?.status === 'stopped') && isRiskScoreAvailable && ( <> @@ -238,8 +253,8 @@ const EntityStoreDashboardPanelsComponent = () => { enableStore={enableEntityStore} riskScore={{ disabled: isRiskScoreAvailable, checked: !isRiskScoreAvailable }} entityStore={{ - disabled: entityStore.status === 'enabled', - checked: entityStore.status !== 'enabled', + disabled: storeStatusQuery.data?.status === 'running', + checked: storeStatusQuery.data?.status !== 'running', }} /> diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/hooks/use_entity_engine_status.ts b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/hooks/use_entity_engine_status.ts deleted file mode 100644 index 8a1760728074b..0000000000000 --- a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/hooks/use_entity_engine_status.ts +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { UseQueryOptions } from '@tanstack/react-query'; -import { useQuery } from '@tanstack/react-query'; -import type { ListEntityEnginesResponse } from '../../../../../common/api/entity_analytics'; -import { useEntityStoreRoutes } from '../../../api/entity_store'; - -export const ENTITY_STORE_ENGINE_STATUS = 'ENTITY_STORE_ENGINE_STATUS'; - -interface Options { - disabled?: boolean; - polling?: UseQueryOptions['refetchInterval']; -} - -interface EngineError { - message: string; -} - -export const useEntityEngineStatus = (opts: Options = {}) => { - // QUESTION: Maybe we should have an `EnablementStatus` API route for this? - const { listEntityEngines } = useEntityStoreRoutes(); - - const { isLoading, data } = useQuery({ - queryKey: [ENTITY_STORE_ENGINE_STATUS], - queryFn: () => listEntityEngines(), - refetchInterval: opts.polling, - enabled: !opts.disabled, - }); - - const status = (() => { - if (data?.count === 0) { - return 'not_installed'; - } - - if (data?.engines?.some((engine) => engine.status === 'error')) { - return 'error'; - } - - if (data?.engines?.every((engine) => engine.status === 'stopped')) { - return 'stopped'; - } - - if (data?.engines?.some((engine) => engine.status === 'installing')) { - return 'installing'; - } - - if (isLoading) { - return 'loading'; - } - - if (!data) { - return 'error'; - } - - return 'enabled'; - })(); - - const errors = (data?.engines - ?.filter((engine) => engine.status === 'error') - .map((engine) => engine.error) ?? []) as EngineError[]; - - return { - status, - errors, - }; -}; diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/hooks/use_entity_store.ts b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/hooks/use_entity_store.ts index 8aefbe2b44af1..703e0e8eebd19 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/hooks/use_entity_store.ts +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/hooks/use_entity_store.ts @@ -5,10 +5,14 @@ * 2.0. */ -import type { UseMutationOptions } from '@tanstack/react-query'; +import type { UseMutationOptions, UseQueryOptions } from '@tanstack/react-query'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; -import { useCallback, useState } from 'react'; +import { useCallback } from 'react'; +import type { + GetEntityStoreStatusResponse, + InitEntityStoreResponse, +} from '../../../../../common/api/entity_analytics/entity_store/enablement.gen'; import { useKibana } from '../../../../common/lib/kibana/kibana_react'; import type { DeleteEntityEngineResponse, @@ -16,47 +20,29 @@ import type { StopEntityEngineResponse, } from '../../../../../common/api/entity_analytics'; import { useEntityStoreRoutes } from '../../../api/entity_store'; -import { ENTITY_STORE_ENGINE_STATUS, useEntityEngineStatus } from './use_entity_engine_status'; import { EntityEventTypes } from '../../../../common/lib/telemetry'; -const ENTITY_STORE_ENABLEMENT_INIT = 'ENTITY_STORE_ENABLEMENT_INIT'; - -export const useEntityStoreEnablement = () => { - const [polling, setPolling] = useState(false); - const { telemetry } = useKibana().services; +const ENTITY_STORE_ENGINE_STATUS = 'ENTITY_STORE_ENGINE_STATUS'; - useEntityEngineStatus({ - disabled: !polling, - polling: (data) => { - const shouldStopPolling = - data?.engines && - data.engines.length > 0 && - data.engines.every((engine) => engine.status === 'started'); - - if (shouldStopPolling) { - setPolling(false); - return false; - } - return 5000; - }, - }); +export const useEntityStoreStatus = (options: UseQueryOptions) => { + const { getEntityStoreStatus } = useEntityStoreRoutes(); - const { initEntityStore } = useEntityStoreRoutes(); - const { refetch: initialize, ...query } = useQuery({ - queryKey: [ENTITY_STORE_ENABLEMENT_INIT], - queryFn: async () => - initEntityStore('user').then((usr) => initEntityStore('host').then((host) => [usr, host])), - enabled: false, - }); + const query = useQuery( + [ENTITY_STORE_ENGINE_STATUS], + getEntityStoreStatus, + options + ); + return query; +}; - const enable = useCallback(() => { - telemetry?.reportEvent(EntityEventTypes.EntityStoreDashboardInitButtonClicked, { - timestamp: new Date().toISOString(), - }); - return initialize().then(() => setPolling(true)); - }, [initialize, telemetry]); +export const ENABLE_ENTITY_STORE_STATUS_KEY = ['POST', 'ENABLE_ENTITY_STORE']; +export const useEnableEntityStoreMutation = (options?: UseMutationOptions<{}>) => { + const { enableEntityStore } = useEntityStoreRoutes(); - return { enable, query }; + return useMutation(() => enableEntityStore(), { + ...options, + mutationKey: ENABLE_ENTITY_STORE_STATUS_KEY, + }); }; export const INIT_ENTITY_ENGINE_STATUS_KEY = ['POST', 'INIT_ENTITY_ENGINE']; @@ -74,15 +60,15 @@ export const useInvalidateEntityEngineStatusQuery = () => { export const useInitEntityEngineMutation = (options?: UseMutationOptions<{}>) => { const { telemetry } = useKibana().services; const invalidateEntityEngineStatusQuery = useInvalidateEntityEngineStatusQuery(); - const { initEntityStore } = useEntityStoreRoutes(); + const { initEntityEngine } = useEntityStoreRoutes(); return useMutation( () => { telemetry?.reportEvent(EntityEventTypes.EntityStoreEnablementToggleClicked, { timestamp: new Date().toISOString(), action: 'start', }); - return initEntityStore('user').then((usr) => - initEntityStore('host').then((host) => [usr, host]) + return initEntityEngine('user').then((usr) => + initEntityEngine('host').then((host) => [usr, host]) ); }, { @@ -104,15 +90,15 @@ export const STOP_ENTITY_ENGINE_STATUS_KEY = ['POST', 'STOP_ENTITY_ENGINE']; export const useStopEntityEngineMutation = (options?: UseMutationOptions<{}>) => { const { telemetry } = useKibana().services; const invalidateEntityEngineStatusQuery = useInvalidateEntityEngineStatusQuery(); - const { stopEntityStore } = useEntityStoreRoutes(); + const { stopEntityEngine } = useEntityStoreRoutes(); return useMutation( () => { telemetry?.reportEvent(EntityEventTypes.EntityStoreEnablementToggleClicked, { timestamp: new Date().toISOString(), action: 'stop', }); - return stopEntityStore('user').then((usr) => - stopEntityStore('host').then((host) => [usr, host]) + return stopEntityEngine('user').then((usr) => + stopEntityEngine('host').then((host) => [usr, host]) ); }, { From 019392ac01f2b606aae8ae9e507a71dbb23a720f Mon Sep 17 00:00:00 2001 From: Tiago Vila Verde Date: Thu, 14 Nov 2024 15:03:50 +0100 Subject: [PATCH 2/9] finish UI rework --- .../api/hooks/use_risk_engine_status.ts | 2 +- .../components/dashboard_enablement_panel.tsx | 198 +++++++++ .../dashboard_entity_store_panels.tsx | 95 ++++ .../components/dashboard_panels.tsx | 328 -------------- .../entity_store/hooks/use_entity_store.ts | 110 ++--- .../pages/entity_analytics_dashboard.tsx | 2 +- .../pages/entity_store_management_page.tsx | 408 +++++++++--------- 7 files changed, 543 insertions(+), 600 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/components/dashboard_enablement_panel.tsx create mode 100644 x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/components/dashboard_entity_store_panels.tsx delete mode 100644 x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/components/dashboard_panels.tsx diff --git a/x-pack/plugins/security_solution/public/entity_analytics/api/hooks/use_risk_engine_status.ts b/x-pack/plugins/security_solution/public/entity_analytics/api/hooks/use_risk_engine_status.ts index a9c9dc0939b03..14ab3fc7ca15b 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/api/hooks/use_risk_engine_status.ts +++ b/x-pack/plugins/security_solution/public/entity_analytics/api/hooks/use_risk_engine_status.ts @@ -38,7 +38,7 @@ export const useIsNewRiskScoreModuleInstalled = (): RiskScoreModuleStatus => { return { isLoading: false, installed: !!riskEngineStatus?.isNewRiskScoreModuleInstalled }; }; -interface RiskEngineStatus extends RiskEngineStatusResponse { +export interface RiskEngineStatus extends RiskEngineStatusResponse { isUpdateAvailable: boolean; isNewRiskScoreModuleInstalled: boolean; isNewRiskScoreModuleAvailable: boolean; diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/components/dashboard_enablement_panel.tsx b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/components/dashboard_enablement_panel.tsx new file mode 100644 index 0000000000000..54e6063d80258 --- /dev/null +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/components/dashboard_enablement_panel.tsx @@ -0,0 +1,198 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React, { useCallback, useState } from 'react'; +import { + EuiCallOut, + EuiPanel, + EuiEmptyPrompt, + EuiLoadingLogo, + EuiToolTip, + EuiButton, + EuiImage, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import type { UseQueryResult } from '@tanstack/react-query'; +import type { GetEntityStoreStatusResponse } from '../../../../../common/api/entity_analytics/entity_store/enablement.gen'; +import type { StoreStatus } from '../../../../../common/api/entity_analytics'; +import { RiskEngineStatusEnum } from '../../../../../common/api/entity_analytics'; +import { useInitRiskEngineMutation } from '../../../api/hooks/use_init_risk_engine_mutation'; +import { useEnableEntityStoreMutation } from '../hooks/use_entity_store'; +import { + ENABLEMENT_INITIALIZING_RISK_ENGINE, + ENABLEMENT_INITIALIZING_ENTITY_STORE, + ENABLE_ALL_TITLE, + ENABLEMENT_DESCRIPTION_BOTH, + ENABLE_RISK_SCORE_TITLE, + ENABLEMENT_DESCRIPTION_RISK_ENGINE_ONLY, + ENABLE_ENTITY_STORE_TITLE, + ENABLEMENT_DESCRIPTION_ENTITY_STORE_ONLY, +} from '../translations'; +import type { Enablements } from './enablement_modal'; +import { EntityStoreEnablementModal } from './enablement_modal'; +import dashboardEnableImg from '../../../images/entity_store_dashboard.png'; +import type { RiskEngineStatus } from '../../../api/hooks/use_risk_engine_status'; + +interface EnableEntityStorePanelProps { + state: { + riskEngine: UseQueryResult; + entityStore: UseQueryResult; + }; +} + +export const EnablementPanel: React.FC = ({ state }) => { + const riskEngineStatus = state.riskEngine.data?.risk_engine_status; + const entityStoreStatus = state.entityStore.data?.status; + + const [modal, setModalState] = useState({ visible: false }); + const [riskEngineInitializing, setRiskEngineInitializing] = useState(false); + + const initRiskEngine = useInitRiskEngineMutation(); + const storeEnablement = useEnableEntityStoreMutation(); + + const enableEntityStore = useCallback( + (enable: Enablements) => () => { + if (enable.riskScore) { + const options = { + onSuccess: () => { + setRiskEngineInitializing(false); + if (enable.entityStore) { + storeEnablement.mutate(); + } + }, + }; + setRiskEngineInitializing(true); + initRiskEngine.mutate(undefined, options); + setModalState({ visible: false }); + return; + } + + if (enable.entityStore) { + storeEnablement.mutate(); + setModalState({ visible: false }); + } + }, + [storeEnablement, initRiskEngine] + ); + + if (storeEnablement.error) { + return ( + <> + + } + color="danger" + iconType="error" + > +

{(storeEnablement.error as { body: { message: string } }).body.message}

+
+ + ); + } + + if (riskEngineInitializing) { + return ( + + } + title={

{ENABLEMENT_INITIALIZING_RISK_ENGINE}

} + /> +
+ ); + } + + if (entityStoreStatus === 'installing' || storeEnablement.isLoading) { + return ( + + } + title={

{ENABLEMENT_INITIALIZING_ENTITY_STORE}

} + body={ +

+ +

+ } + /> +
+ ); + } + + if ( + riskEngineStatus !== RiskEngineStatusEnum.NOT_INSTALLED && + (entityStoreStatus === 'running' || entityStoreStatus === 'stopped') + ) { + return null; + } + + const [title, body] = getEnablementTexts(entityStoreStatus, riskEngineStatus); + return ( + <> + {title}} + body={

{body}

} + actions={ + + setModalState({ visible: true })} + data-test-subj={`enable_entity_store_btn`} + > + + + + } + icon={} + /> + + setModalState({ visible })} + enableStore={enableEntityStore} + riskScore={{ + disabled: riskEngineStatus !== RiskEngineStatusEnum.NOT_INSTALLED, + checked: riskEngineStatus === RiskEngineStatusEnum.NOT_INSTALLED, + }} + entityStore={{ + disabled: entityStoreStatus === 'running', + checked: entityStoreStatus === 'not_installed', + }} + /> + + ); +}; + +const getEnablementTexts = ( + entityStoreStatus?: StoreStatus, + riskEngineStatus?: RiskEngineStatus['risk_engine_status'] +): [string, string] => { + if ( + (entityStoreStatus === 'not_installed' || entityStoreStatus === 'stopped') && + riskEngineStatus === RiskEngineStatusEnum.NOT_INSTALLED + ) { + return [ENABLE_ALL_TITLE, ENABLEMENT_DESCRIPTION_BOTH]; + } + + if (riskEngineStatus === RiskEngineStatusEnum.NOT_INSTALLED) { + return [ENABLE_RISK_SCORE_TITLE, ENABLEMENT_DESCRIPTION_RISK_ENGINE_ONLY]; + } + + return [ENABLE_ENTITY_STORE_TITLE, ENABLEMENT_DESCRIPTION_ENTITY_STORE_ONLY]; +}; diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/components/dashboard_entity_store_panels.tsx b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/components/dashboard_entity_store_panels.tsx new file mode 100644 index 0000000000000..48fb5e56b7408 --- /dev/null +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/components/dashboard_entity_store_panels.tsx @@ -0,0 +1,95 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React from 'react'; +import { + EuiEmptyPrompt, + EuiLoadingSpinner, + EuiFlexItem, + EuiFlexGroup, + EuiPanel, + EuiCallOut, +} from '@elastic/eui'; + +import { FormattedMessage } from '@kbn/i18n-react'; +import { RiskEngineStatusEnum } from '../../../../../common/api/entity_analytics'; +import { RiskScoreEntity } from '../../../../../common/search_strategy'; +import { EntitiesList } from '../entities_list'; +import { useEntityStoreStatus } from '../hooks/use_entity_store'; +import { EntityAnalyticsRiskScores } from '../../entity_analytics_risk_score'; +import { useRiskEngineStatus } from '../../../api/hooks/use_risk_engine_status'; + +import { EnablementPanel } from './dashboard_enablement_panel'; + +const EntityStoreDashboardPanelsComponent = () => { + const riskEngineStatus = useRiskEngineStatus(); + const storeStatusQuery = useEntityStoreStatus({}); + + const callouts = (storeStatusQuery.data?.engines ?? []) + .filter((engine) => engine.status === 'error') + .map((engine) => { + const err = engine.error as { + message: string; + }; + return ( + + } + color="danger" + iconType="error" + > +

{err?.message}

+
+ ); + }); + + if (storeStatusQuery.status === 'loading') { + return ( + + } /> + + ); + } + + return ( + + {storeStatusQuery.status === 'error' ? ( + callouts + ) : ( + + )} + + {riskEngineStatus.data?.risk_engine_status !== RiskEngineStatusEnum.NOT_INSTALLED && ( + <> + + + + + + + + )} + {storeStatusQuery.data?.status !== 'not_installed' && + storeStatusQuery.data?.status !== 'installing' && ( + + + + )} + + ); +}; + +export const EntityStoreDashboardPanels = React.memo(EntityStoreDashboardPanelsComponent); +EntityStoreDashboardPanels.displayName = 'EntityStoreDashboardPanels'; diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/components/dashboard_panels.tsx b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/components/dashboard_panels.tsx deleted file mode 100644 index 8df069ea15adf..0000000000000 --- a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/components/dashboard_panels.tsx +++ /dev/null @@ -1,328 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import React, { useState } from 'react'; -import { - EuiEmptyPrompt, - EuiToolTip, - EuiButton, - EuiLoadingSpinner, - EuiFlexItem, - EuiFlexGroup, - EuiLoadingLogo, - EuiPanel, - EuiImage, - EuiCallOut, -} from '@elastic/eui'; - -import { FormattedMessage } from '@kbn/i18n-react'; -import { RiskEngineStatusEnum } from '../../../../../common/api/entity_analytics'; -import { RiskScoreEntity } from '../../../../../common/search_strategy'; - -import { EntitiesList } from '../entities_list'; - -import { useEnableEntityStoreMutation, useEntityStoreStatus } from '../hooks/use_entity_store'; -import { EntityStoreEnablementModal, type Enablements } from './enablement_modal'; - -import { EntityAnalyticsRiskScores } from '../../entity_analytics_risk_score'; -import { useInitRiskEngineMutation } from '../../../api/hooks/use_init_risk_engine_mutation'; - -import dashboardEnableImg from '../../../images/entity_store_dashboard.png'; -import { - ENABLEMENT_DESCRIPTION_BOTH, - ENABLEMENT_DESCRIPTION_ENTITY_STORE_ONLY, - ENABLEMENT_DESCRIPTION_RISK_ENGINE_ONLY, - ENABLEMENT_INITIALIZING_ENTITY_STORE, - ENABLEMENT_INITIALIZING_RISK_ENGINE, - ENABLE_ALL_TITLE, - ENABLE_ENTITY_STORE_TITLE, - ENABLE_RISK_SCORE_TITLE, -} from '../translations'; -import { useRiskEngineStatus } from '../../../api/hooks/use_risk_engine_status'; - -const EntityStoreDashboardPanelsComponent = () => { - const [modal, setModalState] = useState({ visible: false }); - const [riskEngineInitializing, setRiskEngineInitializing] = useState(false); - - const riskEngineStatus = useRiskEngineStatus(); - const { mutate: initRiskEngine } = useInitRiskEngineMutation(); - - const storeStatusQuery = useEntityStoreStatus({ - enabled: false, - refetchInterval: (data) => { - if (data?.status !== 'installing') { - return false; - } - return 5000; - }, - }); - - const enableStore = useEnableEntityStoreMutation({ - onSuccess: () => storeStatusQuery.refetch(), - }); - - const callouts = (storeStatusQuery.data?.engines ?? []).map((engine) => { - const err = engine.error as { - message: string; - }; - return ( - - } - color="danger" - iconType="error" - > -

{err?.message}

-
- ); - }); - - const enableEntityStore = (enable: Enablements) => () => { - setModalState({ visible: false }); - if (enable.riskScore) { - const options = { - onSuccess: () => { - setRiskEngineInitializing(false); - if (enable.entityStore) { - enableStore.mutate(); - } - }, - }; - setRiskEngineInitializing(true); - initRiskEngine(undefined, options); - return; - } - - if (enable.entityStore) { - enableStore.mutate(); - } - }; - - if (enableStore.error) { - return ( - <> - - } - color="danger" - iconType="error" - > -

{(enableStore.error as { body: { message: string } }).body.message}

-
- {callouts} - - ); - } - - if (storeStatusQuery.status === 'loading') { - return ( - - } - title={

{ENABLEMENT_INITIALIZING_ENTITY_STORE}

} - /> -
- ); - } - - if (storeStatusQuery.data?.status === 'installing') { - return ( - - } - title={

{ENABLEMENT_INITIALIZING_ENTITY_STORE}

} - body={ -

- -

- } - /> -
- ); - } - - const isRiskScoreAvailable = - riskEngineStatus.data && - riskEngineStatus.data.risk_engine_status !== RiskEngineStatusEnum.NOT_INSTALLED; - - return ( - - {storeStatusQuery.status === 'error' && isRiskScoreAvailable && ( - <> - {callouts} - - - - - - - - )} - {storeStatusQuery.status === 'error' && !isRiskScoreAvailable && ( - <> - {callouts} - - setModalState({ visible: true })} - loadingRiskEngine={riskEngineInitializing} - enablements="riskScore" - /> - - - )} - {storeStatusQuery.data?.status === 'running' && isRiskScoreAvailable && ( - <> - - - - - - - - - - - )} - {storeStatusQuery.data?.status === 'running' && !isRiskScoreAvailable && ( - <> - - setModalState({ visible: true })} - loadingRiskEngine={riskEngineInitializing} - enablements="riskScore" - /> - - - - - - - )} - - {(storeStatusQuery.data?.status === 'not_installed' || - storeStatusQuery.data?.status === 'stopped') && - !isRiskScoreAvailable && ( - // TODO: Move modal inside EnableEntityStore component, eliminating the onEnable prop in favour of forwarding the riskScoreEnabled status - setModalState({ visible: true })} - loadingRiskEngine={riskEngineInitializing} - /> - )} - - {(storeStatusQuery.data?.status === 'not_installed' || - storeStatusQuery.data?.status === 'stopped') && - isRiskScoreAvailable && ( - <> - - - setModalState({ - visible: true, - }) - } - /> - - - - - - - - - )} - - setModalState({ visible })} - enableStore={enableEntityStore} - riskScore={{ disabled: isRiskScoreAvailable, checked: !isRiskScoreAvailable }} - entityStore={{ - disabled: storeStatusQuery.data?.status === 'running', - checked: storeStatusQuery.data?.status !== 'running', - }} - /> - - ); -}; - -interface EnableEntityStoreProps { - onEnable: () => void; - enablements: 'store' | 'riskScore' | 'both'; - loadingRiskEngine?: boolean; -} - -export const EnableEntityStore: React.FC = ({ - onEnable, - enablements, - loadingRiskEngine, -}) => { - const title = - enablements === 'store' - ? ENABLE_ENTITY_STORE_TITLE - : enablements === 'riskScore' - ? ENABLE_RISK_SCORE_TITLE - : ENABLE_ALL_TITLE; - - const body = - enablements === 'store' - ? ENABLEMENT_DESCRIPTION_ENTITY_STORE_ONLY - : enablements === 'riskScore' - ? ENABLEMENT_DESCRIPTION_RISK_ENGINE_ONLY - : ENABLEMENT_DESCRIPTION_BOTH; - - if (loadingRiskEngine) { - return ( - - } - title={

{ENABLEMENT_INITIALIZING_RISK_ENGINE}

} - /> -
- ); - } - return ( - {title}} - body={

{body}

} - actions={ - - - - - - } - icon={} - /> - ); -}; - -export const EntityStoreDashboardPanels = React.memo(EntityStoreDashboardPanelsComponent); -EntityStoreDashboardPanels.displayName = 'EntityStoreDashboardPanels'; diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/hooks/use_entity_store.ts b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/hooks/use_entity_store.ts index 703e0e8eebd19..145752c0e6a44 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/hooks/use_entity_store.ts +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/hooks/use_entity_store.ts @@ -7,7 +7,6 @@ import type { UseMutationOptions, UseQueryOptions } from '@tanstack/react-query'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; -import { useCallback } from 'react'; import type { GetEntityStoreStatusResponse, @@ -22,74 +21,65 @@ import type { import { useEntityStoreRoutes } from '../../../api/entity_store'; import { EntityEventTypes } from '../../../../common/lib/telemetry'; -const ENTITY_STORE_ENGINE_STATUS = 'ENTITY_STORE_ENGINE_STATUS'; - +const ENTITY_STORE_STATUS = ['GET', 'ENTITY_STORE_STATUS']; export const useEntityStoreStatus = (options: UseQueryOptions) => { const { getEntityStoreStatus } = useEntityStoreRoutes(); - const query = useQuery( - [ENTITY_STORE_ENGINE_STATUS], - getEntityStoreStatus, - options - ); - return query; -}; - -export const ENABLE_ENTITY_STORE_STATUS_KEY = ['POST', 'ENABLE_ENTITY_STORE']; -export const useEnableEntityStoreMutation = (options?: UseMutationOptions<{}>) => { - const { enableEntityStore } = useEntityStoreRoutes(); - - return useMutation(() => enableEntityStore(), { + const query = useQuery(ENTITY_STORE_STATUS, getEntityStoreStatus, { + refetchInterval: (data) => { + if (data?.status === 'installing') { + return 5000; + } + return false; + }, ...options, - mutationKey: ENABLE_ENTITY_STORE_STATUS_KEY, }); + return query; }; -export const INIT_ENTITY_ENGINE_STATUS_KEY = ['POST', 'INIT_ENTITY_ENGINE']; - -export const useInvalidateEntityEngineStatusQuery = () => { +export const ENABLE_STORE_STATUS_KEY = ['POST', 'ENABLE_ENTITY_STORE']; +export const useEnableEntityStoreMutation = (options?: UseMutationOptions<{}>) => { + const { telemetry } = useKibana().services; const queryClient = useQueryClient(); + const { enableEntityStore } = useEntityStoreRoutes(); - return useCallback(() => { - queryClient.invalidateQueries([ENTITY_STORE_ENGINE_STATUS], { - refetchType: 'active', - }); - }, [queryClient]); -}; - -export const useInitEntityEngineMutation = (options?: UseMutationOptions<{}>) => { - const { telemetry } = useKibana().services; - const invalidateEntityEngineStatusQuery = useInvalidateEntityEngineStatusQuery(); - const { initEntityEngine } = useEntityStoreRoutes(); - return useMutation( + return useMutation( () => { telemetry?.reportEvent(EntityEventTypes.EntityStoreEnablementToggleClicked, { timestamp: new Date().toISOString(), action: 'start', }); - return initEntityEngine('user').then((usr) => - initEntityEngine('host').then((host) => [usr, host]) - ); + return enableEntityStore(); }, { + mutationKey: ENABLE_STORE_STATUS_KEY, + onSuccess: () => queryClient.refetchQueries(ENTITY_STORE_STATUS), ...options, - mutationKey: INIT_ENTITY_ENGINE_STATUS_KEY, - onSettled: (...args) => { - invalidateEntityEngineStatusQuery(); + } + ); +}; + +export const INIT_ENTITY_ENGINE_STATUS_KEY = ['POST', 'INIT_ENTITY_ENGINE']; +export const useInitEntityEngineMutation = (options?: UseMutationOptions<{}>) => { + const queryClient = useQueryClient(); - if (options?.onSettled) { - options.onSettled(...args); - } - }, + const { initEntityEngine } = useEntityStoreRoutes(); + return useMutation( + () => Promise.all([initEntityEngine('user'), initEntityEngine('host')]), + + { + mutationKey: INIT_ENTITY_ENGINE_STATUS_KEY, + onSuccess: () => queryClient.refetchQueries({ queryKey: ENTITY_STORE_STATUS }), + ...options, } ); }; export const STOP_ENTITY_ENGINE_STATUS_KEY = ['POST', 'STOP_ENTITY_ENGINE']; - export const useStopEntityEngineMutation = (options?: UseMutationOptions<{}>) => { const { telemetry } = useKibana().services; - const invalidateEntityEngineStatusQuery = useInvalidateEntityEngineStatusQuery(); + const queryClient = useQueryClient(); + const { stopEntityEngine } = useEntityStoreRoutes(); return useMutation( () => { @@ -97,44 +87,26 @@ export const useStopEntityEngineMutation = (options?: UseMutationOptions<{}>) => timestamp: new Date().toISOString(), action: 'stop', }); - return stopEntityEngine('user').then((usr) => - stopEntityEngine('host').then((host) => [usr, host]) - ); + return Promise.all([stopEntityEngine('user'), stopEntityEngine('host')]); }, { - ...options, mutationKey: STOP_ENTITY_ENGINE_STATUS_KEY, - onSettled: (...args) => { - invalidateEntityEngineStatusQuery(); - - if (options?.onSettled) { - options.onSettled(...args); - } - }, + onSuccess: () => queryClient.refetchQueries({ queryKey: ENTITY_STORE_STATUS }), + ...options, } ); }; export const DELETE_ENTITY_ENGINE_STATUS_KEY = ['POST', 'STOP_ENTITY_ENGINE']; - export const useDeleteEntityEngineMutation = (options?: UseMutationOptions<{}>) => { - const invalidateEntityEngineStatusQuery = useInvalidateEntityEngineStatusQuery(); + const queryClient = useQueryClient(); const { deleteEntityEngine } = useEntityStoreRoutes(); return useMutation( - () => - deleteEntityEngine('user', true).then((usr) => - deleteEntityEngine('host', true).then((host) => [usr, host]) - ), + () => Promise.all([deleteEntityEngine('user', true), deleteEntityEngine('host', true)]), { - ...options, mutationKey: DELETE_ENTITY_ENGINE_STATUS_KEY, - onSettled: (...args) => { - invalidateEntityEngineStatusQuery(); - - if (options?.onSettled) { - options.onSettled(...args); - } - }, + onSuccess: () => queryClient.refetchQueries({ queryKey: ENTITY_STORE_STATUS }), + ...options, } ); }; diff --git a/x-pack/plugins/security_solution/public/entity_analytics/pages/entity_analytics_dashboard.tsx b/x-pack/plugins/security_solution/public/entity_analytics/pages/entity_analytics_dashboard.tsx index 2fbc4f67ab6ef..0ed10b886cd8f 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/pages/entity_analytics_dashboard.tsx +++ b/x-pack/plugins/security_solution/public/entity_analytics/pages/entity_analytics_dashboard.tsx @@ -24,7 +24,7 @@ import { useHasSecurityCapability } from '../../helper_hooks'; import { EntityAnalyticsHeader } from '../components/entity_analytics_header'; import { EntityAnalyticsAnomalies } from '../components/entity_analytics_anomalies'; -import { EntityStoreDashboardPanels } from '../components/entity_store/components/dashboard_panels'; +import { EntityStoreDashboardPanels } from '../components/entity_store/components/dashboard_entity_store_panels'; import { EntityAnalyticsRiskScores } from '../components/entity_analytics_risk_score'; import { useIsExperimentalFeatureEnabled } from '../../common/hooks/use_experimental_features'; diff --git a/x-pack/plugins/security_solution/public/entity_analytics/pages/entity_store_management_page.tsx b/x-pack/plugins/security_solution/public/entity_analytics/pages/entity_store_management_page.tsx index 84648d89f912d..7c00f61f62fbb 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/pages/entity_store_management_page.tsx +++ b/x-pack/plugins/security_solution/public/entity_analytics/pages/entity_store_management_page.tsx @@ -26,9 +26,12 @@ import { EuiToolTip, EuiBetaBadge, } from '@elastic/eui'; +import type { ReactNode } from 'react'; import React, { useCallback, useState } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; -import { useEntityEngineStatus } from '../components/entity_store/hooks/use_entity_engine_status'; + +import type { SecurityAppError } from '@kbn/securitysolution-t-grid'; +import type { StoreStatus } from '../../../common/api/entity_analytics'; import { useIsExperimentalFeatureEnabled } from '../../common/hooks/use_experimental_features'; import { ASSET_CRITICALITY_INDEX_PATTERN } from '../../../common/entity_analytics/asset_criticality'; import { useKibana } from '../../common/lib/kibana'; @@ -37,16 +40,18 @@ import { useAssetCriticalityPrivileges } from '../components/asset_criticality/u import { useHasSecurityCapability } from '../../helper_hooks'; import { useDeleteEntityEngineMutation, - useInitEntityEngineMutation, + useEnableEntityStoreMutation, + useEntityStoreStatus, useStopEntityEngineMutation, } from '../components/entity_store/hooks/use_entity_store'; import { TECHNICAL_PREVIEW, TECHNICAL_PREVIEW_TOOLTIP } from '../../common/translations'; import { useEntityEnginePrivileges } from '../components/entity_store/hooks/use_entity_engine_privileges'; import { MissingPrivilegesCallout } from '../components/entity_store/components/missing_privileges_callout'; -const entityStoreEnabledStatuses = ['enabled']; -const switchDisabledStatuses = ['error', 'loading', 'installing']; -const entityStoreInstallingStatuses = ['installing', 'loading']; +const isSwitchDisabled = (status?: StoreStatus) => status === 'error' || status === 'installing'; +const isEntityStoreEnabled = (status?: StoreStatus) => status === 'running'; +const canDeleteEntityEngine = (status?: StoreStatus) => + !['not_installed', 'installing'].includes(status || ''); export const EntityStoreManagementPage = () => { const hasEntityAnalyticsCapability = useHasSecurityCapability('entity-analytics'); @@ -58,25 +63,9 @@ export const EntityStoreManagementPage = () => { } = useAssetCriticalityPrivileges('AssetCriticalityUploadPage'); const hasAssetCriticalityWritePermissions = assetCriticalityPrivileges?.has_write_permissions; - const [polling, setPolling] = useState(false); - const entityStoreStatus = useEntityEngineStatus({ - disabled: false, - polling: !polling - ? undefined - : (data) => { - const shouldStopPolling = - data?.engines && - data.engines.length > 0 && - data.engines.every((engine) => engine.status === 'started'); + const entityStoreStatus = useEntityStoreStatus({}); - if (shouldStopPolling) { - setPolling(false); - return false; - } - return 1000; - }, - }); - const initEntityEngineMutation = useInitEntityEngineMutation(); + const enableStoreMutation = useEnableEntityStoreMutation(); const stopEntityEngineMutation = useStopEntityEngineMutation(); const deleteEntityEngineMutation = useDeleteEntityEngineMutation({ onSuccess: () => { @@ -89,17 +78,16 @@ export const EntityStoreManagementPage = () => { const showClearModal = useCallback(() => setIsClearModalVisible(true), []); const onSwitchClick = useCallback(() => { - if (switchDisabledStatuses.includes(entityStoreStatus.status)) { + if (isSwitchDisabled(entityStoreStatus.data?.status)) { return; } - if (entityStoreEnabledStatuses.includes(entityStoreStatus.status)) { + if (isEntityStoreEnabled(entityStoreStatus.data?.status)) { stopEntityEngineMutation.mutate(); } else { - setPolling(true); - initEntityEngineMutation.mutate(); + enableStoreMutation.mutate(); } - }, [initEntityEngineMutation, stopEntityEngineMutation, entityStoreStatus]); + }, [entityStoreStatus.data?.status, stopEntityEngineMutation, enableStoreMutation]); const { data: privileges } = useEntityEnginePrivileges(); @@ -108,168 +96,33 @@ export const EntityStoreManagementPage = () => { return null; } - const AssetCriticalityIssueCallout: React.FC = () => { - const errorMessage = assetCriticalityPrivilegesError?.body.message ?? ( - - ); + const isMutationLoading = + enableStoreMutation.isLoading || + stopEntityEngineMutation.isLoading || + deleteEntityEngineMutation.isLoading; - return ( - + const callouts = (entityStoreStatus.data?.engines || []) + .filter((engine) => engine.status === 'error') + .map((engine) => { + const err = engine.error as { + message: string; + }; + + return ( } - color="primary" - iconType="iInCircle" + color="danger" + iconType="alert" > - {errorMessage} +

{err?.message}

-
- ); - }; - - const ClearEntityDataPanel: React.FC = () => { - return ( - <> - - -

- -

- - -
- - { - showClearModal(); - }} - > - - -
- {isClearModalVisible && ( - - } - onCancel={closeClearModal} - onConfirm={() => { - deleteEntityEngineMutation.mutate(); - }} - cancelButtonText={ - - } - confirmButtonText={ - - } - buttonColor="danger" - defaultFocusedButton="confirm" - > - - - )} - - ); - }; - - const FileUploadSection: React.FC = () => { - if ( - !hasEntityAnalyticsCapability || - assetCriticalityPrivilegesError?.body.status_code === 403 - ) { - return ; - } - if (!hasAssetCriticalityWritePermissions) { - return ; - } - return ( - - -

- -

-
- - - - - - -
- ); - }; - - const canDeleteEntityEngine = !['not_installed', 'loading', 'installing'].includes( - entityStoreStatus.status - ); - - const isMutationLoading = - initEntityEngineMutation.isLoading || - stopEntityEngineMutation.isLoading || - deleteEntityEngineMutation.isLoading; - - const callouts = entityStoreStatus.errors.map((error) => ( - - } - color="danger" - iconType="alert" - > -

{error.message}

-
- )); + ); + }); return ( <> @@ -291,15 +144,10 @@ export const EntityStoreManagementPage = () => { !isEntityStoreFeatureFlagDisabled && privileges?.has_all_required ? [ , ] : [] @@ -324,10 +172,14 @@ export const EntityStoreManagementPage = () => { - + - {initEntityEngineMutation.isError && ( + {enableStoreMutation.isError && ( { color="danger" iconType="alert" > -

- {(initEntityEngineMutation.error as { body: { message: string } }).body.message} -

+

{(enableStoreMutation.error as { body: { message: string } }).body.message}

)} {deleteEntityEngineMutation.isError && ( @@ -363,7 +213,16 @@ export const EntityStoreManagementPage = () => { {!isEntityStoreFeatureFlagDisabled && privileges?.has_all_required && - canDeleteEntityEngine && } + canDeleteEntityEngine(entityStoreStatus.data?.status) && ( + + )}
@@ -452,15 +311,15 @@ const EntityStoreFeatureFlagNotAvailableCallout: React.FC = () => { ); }; -const EntityStoreHealth: React.FC<{ currentEntityStoreStatus: string }> = ({ +const EntityStoreHealth: React.FC<{ currentEntityStoreStatus?: StoreStatus }> = ({ currentEntityStoreStatus, }) => { return ( - {entityStoreEnabledStatuses.includes(currentEntityStoreStatus) ? 'On' : 'Off'} + {isEntityStoreEnabled(currentEntityStoreStatus) ? 'On' : 'Off'} ); }; @@ -468,7 +327,7 @@ const EntityStoreHealth: React.FC<{ currentEntityStoreStatus: string }> = ({ const EnablementButton: React.FC<{ isLoading: boolean; isDisabled: boolean; - status: string; + status?: StoreStatus; onSwitch: () => void; }> = ({ isLoading, isDisabled, status, onSwitch }) => { return ( @@ -484,7 +343,7 @@ const EnablementButton: React.FC<{ label="" onChange={onSwitch} data-test-subj="entity-store-switch" - checked={entityStoreEnabledStatuses.includes(status)} + checked={isEntityStoreEnabled(status)} disabled={isDisabled} /> @@ -515,3 +374,150 @@ const InsufficientAssetCriticalityPrivilegesCallout: React.FC = () => { ); }; + +const AssetCriticalityIssueCallout: React.FC = ({ + errorMessage, +}: { + errorMessage?: string | ReactNode; +}) => { + const msg = errorMessage ?? ( + + ); + + return ( + + + } + color="primary" + iconType="iInCircle" + > + {msg} + + + ); +}; + +const ClearEntityDataPanel: React.FC<{ + deleteEntityEngineMutation: ReturnType; + isClearModalVisible: boolean; + closeClearModal: () => void; + showClearModal: () => void; +}> = ({ deleteEntityEngineMutation, isClearModalVisible, closeClearModal, showClearModal }) => { + return ( + <> + + +

+ +

+ + +
+ + { + showClearModal(); + }} + > + + +
+ {isClearModalVisible && ( + + } + onCancel={closeClearModal} + onConfirm={() => { + deleteEntityEngineMutation.mutate(); + }} + cancelButtonText={ + + } + confirmButtonText={ + + } + buttonColor="danger" + defaultFocusedButton="confirm" + > + + + )} + + ); +}; + +const FileUploadSection: React.FC<{ + assetCriticalityPrivilegesError: SecurityAppError | null; + hasEntityAnalyticsCapability: boolean; + hasAssetCriticalityWritePermissions?: boolean; +}> = ({ + assetCriticalityPrivilegesError, + hasEntityAnalyticsCapability, + hasAssetCriticalityWritePermissions, +}) => { + if (!hasEntityAnalyticsCapability || assetCriticalityPrivilegesError?.body.status_code === 403) { + return ; + } + if (!hasAssetCriticalityWritePermissions) { + return ; + } + return ( + + +

+ +

+
+ + + + + + +
+ ); +}; From 4bd0ecb4461b846bb9e2ee6ad97f9ff0b2f29ead Mon Sep 17 00:00:00 2001 From: Tiago Vila Verde Date: Mon, 18 Nov 2024 15:39:10 +0100 Subject: [PATCH 3/9] add cypress tests --- .../components/dashboard_enablement_panel.tsx | 3 +- .../components/enablement_modal.tsx | 5 +- .../e2e/entity_analytics/dashboard.cy.ts | 87 +++++++++++++++++++ .../screens/entity_analytics/dashboard.ts | 20 +++++ .../cypress/urls/navigation.ts | 3 + 5 files changed, 116 insertions(+), 2 deletions(-) create mode 100644 x-pack/test/security_solution_cypress/cypress/e2e/entity_analytics/dashboard.cy.ts create mode 100644 x-pack/test/security_solution_cypress/cypress/screens/entity_analytics/dashboard.ts diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/components/dashboard_enablement_panel.tsx b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/components/dashboard_enablement_panel.tsx index 54e6063d80258..9ea4d6f849bf2 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/components/dashboard_enablement_panel.tsx +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/components/dashboard_enablement_panel.tsx @@ -150,7 +150,7 @@ export const EnablementPanel: React.FC = ({ state } color="primary" fill onClick={() => setModalState({ visible: true })} - data-test-subj={`enable_entity_store_btn`} + data-test-subj={`entityStoreEnablementButton`} > = ({ state } } icon={} + data-test-subj="entityStoreEnablementPanel" /> ); return ( - toggle(false)}> + toggle(false)} data-test-subj="entityStoreEnablementModal"> setEnablements((prev) => ({ ...prev, riskScore: !prev.riskScore }))} + data-test-subj="enablementRiskScoreSwitch" /> {!riskEnginePrivileges.isLoading && !riskEnginePrivileges.hasAllRequiredPrivileges && ( @@ -142,6 +143,7 @@ export const EntityStoreEnablementModal: React.FC setEnablements((prev) => ({ ...prev, entityStore: !prev.entityStore })) } + data-test-subj="enablementEntityStoreSwitch" /> @@ -170,6 +172,7 @@ export const EntityStoreEnablementModal: React.FC { + before(() => { + cy.task('esArchiverLoad', { archiveName: 'all_users' }); + }); + + beforeEach(() => { + login(); + deleteRiskEngineConfiguration(); + visit(ENTITY_ANALYTICS_DASHBOARD_URL); + }); + + after(() => { + cy.task('esArchiverUnload', { archiveName: 'all_users' }); + }); + + it('renders page as expected', () => { + cy.get(PAGE_TITLE).should('have.text', 'Entity Analytics'); + }); + + describe('Entity Store enablement', () => { + it('renders enablement panel', () => { + cy.get(ENTITY_STORE_ENABLEMENT_PANEL).should('exist'); + cy.get(ENTITY_STORE_ENABLEMENT_PANEL).should( + 'have.text', + 'Enable entity store and risk score' + ); + }); + + it('enables risk score followed by the store', () => { + cy.get(ENTITY_STORE_ENABLEMENT_BUTTON).click(); + + cy.get(ENTITY_STORE_ENABLEMENT_MODAL).should('exist'); + cy.get(ENTITY_STORE_ENABLEMENT_MODAL).should('have.text', 'Entity Analytics Enablement'); + + cy.get(ENABLEMENT_MODAL_RISK_SCORE_SWITCH).should('exist'); + cy.get(ENABLEMENT_MODAL_ENTITY_STORE_SWITCH).should('exist'); + + cy.get(ENABLEMENT_MODAL_CONFIRM_BUTTON).should('exist').click(); + }); + }); + } +); diff --git a/x-pack/test/security_solution_cypress/cypress/screens/entity_analytics/dashboard.ts b/x-pack/test/security_solution_cypress/cypress/screens/entity_analytics/dashboard.ts new file mode 100644 index 0000000000000..a3231dc6c9291 --- /dev/null +++ b/x-pack/test/security_solution_cypress/cypress/screens/entity_analytics/dashboard.ts @@ -0,0 +1,20 @@ +/* + * 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. + */ + +export const PAGE_TITLE = '[data-test-subj="entityAnalyticsPage"]'; + +export const ENTITY_STORE_ENABLEMENT_PANEL = '[data-test-subj="entityStoreEnablementPanel"]'; +export const ENTITY_STORE_ENABLEMENT_BUTTON = '[data-test-subj="entityStoreEnablementButton"]'; + +export const ENTITY_STORE_ENABLEMENT_MODAL = '[data-test-subj="entityStoreEnablementModal"]'; + +export const ENABLEMENT_MODAL_RISK_SCORE_SWITCH = '[data-test-subj="enablementRiskScoreSwitch"]'; +export const ENABLEMENT_MODAL_ENTITY_STORE_SWITCH = + '[data-test-subj="enablementEntityStoreSwitch"]'; + +export const ENABLEMENT_MODAL_CONFIRM_BUTTON = + '[data-test-subj="entityStoreEnablementModalButton"]'; diff --git a/x-pack/test/security_solution_cypress/cypress/urls/navigation.ts b/x-pack/test/security_solution_cypress/cypress/urls/navigation.ts index b5855192d18e1..6f6f4dc1a108a 100644 --- a/x-pack/test/security_solution_cypress/cypress/urls/navigation.ts +++ b/x-pack/test/security_solution_cypress/cypress/urls/navigation.ts @@ -79,3 +79,6 @@ export const exceptionsListDetailsUrl = (listId: string) => export const DISCOVER_URL = '/app/discover'; export const OSQUERY_URL = '/app/osquery'; export const FLEET_URL = '/app/fleet'; + +// Entity Analytics +export const ENTITY_ANALYTICS_DASHBOARD_URL = '/app/security/entity_analytics'; From 2c8485f06b2c927053346c61c7938c24bca24ff8 Mon Sep 17 00:00:00 2001 From: Tiago Vila Verde Date: Wed, 20 Nov 2024 10:38:06 +0100 Subject: [PATCH 4/9] fix cypress --- .../components/dashboard_enablement_panel.tsx | 4 +- .../dashboard_entity_store_panels.tsx | 2 +- .../cypress/cypress.config.ts | 2 +- .../cypress/cypress_ci.config.ts | 2 +- .../e2e/entity_analytics/dashboard.cy.ts | 50 +++++++------------ .../screens/entity_analytics/dashboard.ts | 8 +++ 6 files changed, 30 insertions(+), 38 deletions(-) diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/components/dashboard_enablement_panel.tsx b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/components/dashboard_enablement_panel.tsx index 9ea4d6f849bf2..d35a8c3f20f3e 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/components/dashboard_enablement_panel.tsx +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/components/dashboard_enablement_panel.tsx @@ -99,7 +99,7 @@ export const EnablementPanel: React.FC = ({ state } if (riskEngineInitializing) { return ( - + } title={

{ENABLEMENT_INITIALIZING_RISK_ENGINE}

} @@ -110,7 +110,7 @@ export const EnablementPanel: React.FC = ({ state } if (entityStoreStatus === 'installing' || storeEnablement.isLoading) { return ( - + } title={

{ENABLEMENT_INITIALIZING_ENTITY_STORE}

} diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/components/dashboard_entity_store_panels.tsx b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/components/dashboard_entity_store_panels.tsx index 48fb5e56b7408..0870d37a6b2b5 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/components/dashboard_entity_store_panels.tsx +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/components/dashboard_entity_store_panels.tsx @@ -83,7 +83,7 @@ const EntityStoreDashboardPanelsComponent = () => { )} {storeStatusQuery.data?.status !== 'not_installed' && storeStatusQuery.data?.status !== 'installing' && ( - + )} diff --git a/x-pack/test/security_solution_cypress/cypress/cypress.config.ts b/x-pack/test/security_solution_cypress/cypress/cypress.config.ts index 3b42b205e6030..345a2d22b9012 100644 --- a/x-pack/test/security_solution_cypress/cypress/cypress.config.ts +++ b/x-pack/test/security_solution_cypress/cypress/cypress.config.ts @@ -21,7 +21,7 @@ export default defineCypressConfig({ responseTimeout: 60000, screenshotsFolder: '../../../target/kibana-security-solution/cypress/screenshots', trashAssetsBeforeRuns: false, - video: false, + video: true, videosFolder: '../../../target/kibana-security-solution/cypress/videos', viewportHeight: 1200, viewportWidth: 1920, diff --git a/x-pack/test/security_solution_cypress/cypress/cypress_ci.config.ts b/x-pack/test/security_solution_cypress/cypress/cypress_ci.config.ts index d621b844786dd..c51874b672296 100644 --- a/x-pack/test/security_solution_cypress/cypress/cypress_ci.config.ts +++ b/x-pack/test/security_solution_cypress/cypress/cypress_ci.config.ts @@ -30,7 +30,7 @@ export default defineCypressConfig({ }, screenshotsFolder: '../../../target/kibana-security-solution/cypress/screenshots', trashAssetsBeforeRuns: false, - video: false, + video: true, videosFolder: '../../../target/kibana-security-solution/cypress/videos', viewportHeight: 1200, viewportWidth: 1920, diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/entity_analytics/dashboard.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/entity_analytics/dashboard.cy.ts index 76c1a19afe0c8..ace65956a258e 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/entity_analytics/dashboard.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/entity_analytics/dashboard.cy.ts @@ -5,38 +5,22 @@ * 2.0. */ -import { - PAGE_TITLE, - HOST_RISK_PREVIEW_TABLE, - HOST_RISK_PREVIEW_TABLE_ROWS, - USER_RISK_PREVIEW_TABLE, - USER_RISK_PREVIEW_TABLE_ROWS, - RISK_PREVIEW_ERROR, - LOCAL_QUERY_BAR_SELECTOR, - RISK_SCORE_ERROR_PANEL, - RISK_SCORE_STATUS, - LOCAL_QUERY_BAR_SEARCH_INPUT_SELECTOR, -} from '../../screens/entity_analytics_management'; - -import { deleteRiskScore, installRiskScoreModule } from '../../tasks/api_calls/risk_scores'; -import { RiskScoreEntity } from '../../tasks/risk_scores/common'; import { login } from '../../tasks/login'; import { visit } from '../../tasks/navigation'; -import { ENTITY_ANALYTICS_MANAGEMENT_URL } from '../../urls/navigation'; +import { ENTITY_ANALYTICS_DASHBOARD_URL } from '../../urls/navigation'; + +import { deleteRiskEngineConfiguration } from '../../tasks/api_calls/risk_engine'; import { - deleteRiskEngineConfiguration, - interceptRiskPreviewError, - interceptRiskPreviewSuccess, - interceptRiskInitError, -} from '../../tasks/api_calls/risk_engine'; -import { updateDateRangeInLocalDatePickers } from '../../tasks/date_picker'; -import { submitLocalSearch } from '../../tasks/search_bar'; -import { - riskEngineStatusChange, - upgradeRiskEngine, - previewErrorButtonClick, -} from '../../tasks/entity_analytics'; + PAGE_TITLE, + ENTITIES_LIST_PANEL, + ENTITY_STORE_ENABLEMENT_PANEL, + ENTITY_STORE_ENABLEMENT_BUTTON, + ENTITY_STORE_ENABLEMENT_MODAL, + ENABLEMENT_MODAL_RISK_SCORE_SWITCH, + ENABLEMENT_MODAL_ENTITY_STORE_SWITCH, + ENABLEMENT_MODAL_CONFIRM_BUTTON, +} from '../../screens/entity_analytics/dashboard'; describe( 'Entity analytics dashboard page', @@ -65,22 +49,22 @@ describe( describe('Entity Store enablement', () => { it('renders enablement panel', () => { cy.get(ENTITY_STORE_ENABLEMENT_PANEL).should('exist'); - cy.get(ENTITY_STORE_ENABLEMENT_PANEL).should( - 'have.text', - 'Enable entity store and risk score' - ); + cy.get(ENTITY_STORE_ENABLEMENT_PANEL).contains('Enable entity store and risk score'); }); it('enables risk score followed by the store', () => { cy.get(ENTITY_STORE_ENABLEMENT_BUTTON).click(); cy.get(ENTITY_STORE_ENABLEMENT_MODAL).should('exist'); - cy.get(ENTITY_STORE_ENABLEMENT_MODAL).should('have.text', 'Entity Analytics Enablement'); + cy.get(ENTITY_STORE_ENABLEMENT_MODAL).contains('Entity Analytics Enablement'); cy.get(ENABLEMENT_MODAL_RISK_SCORE_SWITCH).should('exist'); cy.get(ENABLEMENT_MODAL_ENTITY_STORE_SWITCH).should('exist'); cy.get(ENABLEMENT_MODAL_CONFIRM_BUTTON).should('exist').click(); + + cy.get(ENTITIES_LIST_PANEL, { timeout: 30000 }).scrollIntoView(); + cy.get(ENTITIES_LIST_PANEL).contains('Entities'); }); }); } diff --git a/x-pack/test/security_solution_cypress/cypress/screens/entity_analytics/dashboard.ts b/x-pack/test/security_solution_cypress/cypress/screens/entity_analytics/dashboard.ts index a3231dc6c9291..78b619979e969 100644 --- a/x-pack/test/security_solution_cypress/cypress/screens/entity_analytics/dashboard.ts +++ b/x-pack/test/security_solution_cypress/cypress/screens/entity_analytics/dashboard.ts @@ -18,3 +18,11 @@ export const ENABLEMENT_MODAL_ENTITY_STORE_SWITCH = export const ENABLEMENT_MODAL_CONFIRM_BUTTON = '[data-test-subj="entityStoreEnablementModalButton"]'; + +export const ENABLEMENT_RISK_ENGINE_INITIALIZING_PANEL = + '[data-test-subj="riskEngineInitializingPanel"]'; + +export const ENABLEMENT_ENTITY_STORE_INITIALIZING_PANEL = + '[data-test-subj="entityStoreInitializingPanel"]'; + +export const ENTITIES_LIST_PANEL = '[data-test-subj="entitiesListPanel"]'; From fca713a0357b17f7c0a7478c58688b884e03229c Mon Sep 17 00:00:00 2001 From: Tiago Vila Verde Date: Wed, 20 Nov 2024 11:29:40 +0100 Subject: [PATCH 5/9] revert cypress config --- x-pack/test/security_solution_cypress/cypress/cypress.config.ts | 2 +- .../test/security_solution_cypress/cypress/cypress_ci.config.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/test/security_solution_cypress/cypress/cypress.config.ts b/x-pack/test/security_solution_cypress/cypress/cypress.config.ts index 345a2d22b9012..3b42b205e6030 100644 --- a/x-pack/test/security_solution_cypress/cypress/cypress.config.ts +++ b/x-pack/test/security_solution_cypress/cypress/cypress.config.ts @@ -21,7 +21,7 @@ export default defineCypressConfig({ responseTimeout: 60000, screenshotsFolder: '../../../target/kibana-security-solution/cypress/screenshots', trashAssetsBeforeRuns: false, - video: true, + video: false, videosFolder: '../../../target/kibana-security-solution/cypress/videos', viewportHeight: 1200, viewportWidth: 1920, diff --git a/x-pack/test/security_solution_cypress/cypress/cypress_ci.config.ts b/x-pack/test/security_solution_cypress/cypress/cypress_ci.config.ts index c51874b672296..d621b844786dd 100644 --- a/x-pack/test/security_solution_cypress/cypress/cypress_ci.config.ts +++ b/x-pack/test/security_solution_cypress/cypress/cypress_ci.config.ts @@ -30,7 +30,7 @@ export default defineCypressConfig({ }, screenshotsFolder: '../../../target/kibana-security-solution/cypress/screenshots', trashAssetsBeforeRuns: false, - video: true, + video: false, videosFolder: '../../../target/kibana-security-solution/cypress/videos', viewportHeight: 1200, viewportWidth: 1920, From 9c911b379cb876cf28af8e4943a99348aa264a2a Mon Sep 17 00:00:00 2001 From: Tiago Vila Verde Date: Thu, 21 Nov 2024 13:46:29 +0100 Subject: [PATCH 6/9] review --- .../entity_store/components/dashboard_enablement_panel.tsx | 3 +-- .../components/entity_store/hooks/use_entity_store.ts | 6 +++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/components/dashboard_enablement_panel.tsx b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/components/dashboard_enablement_panel.tsx index d35a8c3f20f3e..8d0426fd99ceb 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/components/dashboard_enablement_panel.tsx +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/components/dashboard_enablement_panel.tsx @@ -91,7 +91,7 @@ export const EnablementPanel: React.FC = ({ state } color="danger" iconType="error" > -

{(storeEnablement.error as { body: { message: string } }).body.message}

+

{storeEnablement.error.body.message}

); @@ -141,7 +141,6 @@ export const EnablementPanel: React.FC = ({ state } css={{ minWidth: '100%' }} hasBorder layout="horizontal" - className="eui-fullWidth" title={

{title}

} body={

{body}

} actions={ diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/hooks/use_entity_store.ts b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/hooks/use_entity_store.ts index 145752c0e6a44..b27b5b4cdf26a 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/hooks/use_entity_store.ts +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/hooks/use_entity_store.ts @@ -22,6 +22,10 @@ import { useEntityStoreRoutes } from '../../../api/entity_store'; import { EntityEventTypes } from '../../../../common/lib/telemetry'; const ENTITY_STORE_STATUS = ['GET', 'ENTITY_STORE_STATUS']; + +interface ResponseError { + body: { message: string }; +} export const useEntityStoreStatus = (options: UseQueryOptions) => { const { getEntityStoreStatus } = useEntityStoreRoutes(); @@ -43,7 +47,7 @@ export const useEnableEntityStoreMutation = (options?: UseMutationOptions<{}>) = const queryClient = useQueryClient(); const { enableEntityStore } = useEntityStoreRoutes(); - return useMutation( + return useMutation( () => { telemetry?.reportEvent(EntityEventTypes.EntityStoreEnablementToggleClicked, { timestamp: new Date().toISOString(), From 631726ece6d3724e631e0651f73e855f0acc1e2c Mon Sep 17 00:00:00 2001 From: Tiago Vila Verde Date: Fri, 22 Nov 2024 14:45:34 +0100 Subject: [PATCH 7/9] review: cypress test fixes --- .../cypress/cypress_ci_serverless.config.ts | 2 +- .../e2e/entity_analytics/dashboard.cy.ts | 24 ++++++++----------- .../cypress/tasks/entity_analytics.ts | 19 +++++++++++++++ 3 files changed, 30 insertions(+), 15 deletions(-) diff --git a/x-pack/test/security_solution_cypress/cypress/cypress_ci_serverless.config.ts b/x-pack/test/security_solution_cypress/cypress/cypress_ci_serverless.config.ts index 2786ce7539092..868a9538b55c6 100644 --- a/x-pack/test/security_solution_cypress/cypress/cypress_ci_serverless.config.ts +++ b/x-pack/test/security_solution_cypress/cypress/cypress_ci_serverless.config.ts @@ -31,7 +31,7 @@ export default defineCypressConfig({ }, screenshotsFolder: '../../../target/kibana-security-solution/cypress/screenshots', trashAssetsBeforeRuns: false, - video: false, + video: true, videosFolder: '../../../../target/kibana-security-solution/cypress/videos', viewportHeight: 1200, viewportWidth: 1920, diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/entity_analytics/dashboard.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/entity_analytics/dashboard.cy.ts index ace65956a258e..272207aaf7f8f 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/entity_analytics/dashboard.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/entity_analytics/dashboard.cy.ts @@ -13,14 +13,15 @@ import { deleteRiskEngineConfiguration } from '../../tasks/api_calls/risk_engine import { PAGE_TITLE, - ENTITIES_LIST_PANEL, ENTITY_STORE_ENABLEMENT_PANEL, - ENTITY_STORE_ENABLEMENT_BUTTON, - ENTITY_STORE_ENABLEMENT_MODAL, ENABLEMENT_MODAL_RISK_SCORE_SWITCH, ENABLEMENT_MODAL_ENTITY_STORE_SWITCH, - ENABLEMENT_MODAL_CONFIRM_BUTTON, } from '../../screens/entity_analytics/dashboard'; +import { + openEntityStoreEnablementModal, + confirmEntityStoreEnablement, + waitForEntitiesListToAppear, +} from '../../tasks/entity_analytics'; describe( 'Entity analytics dashboard page', @@ -48,23 +49,18 @@ describe( describe('Entity Store enablement', () => { it('renders enablement panel', () => { - cy.get(ENTITY_STORE_ENABLEMENT_PANEL).should('exist'); cy.get(ENTITY_STORE_ENABLEMENT_PANEL).contains('Enable entity store and risk score'); }); it('enables risk score followed by the store', () => { - cy.get(ENTITY_STORE_ENABLEMENT_BUTTON).click(); - - cy.get(ENTITY_STORE_ENABLEMENT_MODAL).should('exist'); - cy.get(ENTITY_STORE_ENABLEMENT_MODAL).contains('Entity Analytics Enablement'); + openEntityStoreEnablementModal(); - cy.get(ENABLEMENT_MODAL_RISK_SCORE_SWITCH).should('exist'); - cy.get(ENABLEMENT_MODAL_ENTITY_STORE_SWITCH).should('exist'); + cy.get(ENABLEMENT_MODAL_RISK_SCORE_SWITCH).should('be.visible'); + cy.get(ENABLEMENT_MODAL_ENTITY_STORE_SWITCH).should('be.visible'); - cy.get(ENABLEMENT_MODAL_CONFIRM_BUTTON).should('exist').click(); + confirmEntityStoreEnablement(); - cy.get(ENTITIES_LIST_PANEL, { timeout: 30000 }).scrollIntoView(); - cy.get(ENTITIES_LIST_PANEL).contains('Entities'); + waitForEntitiesListToAppear(); }); }); } diff --git a/x-pack/test/security_solution_cypress/cypress/tasks/entity_analytics.ts b/x-pack/test/security_solution_cypress/cypress/tasks/entity_analytics.ts index 2265f228c2ce7..791fa48acaad1 100644 --- a/x-pack/test/security_solution_cypress/cypress/tasks/entity_analytics.ts +++ b/x-pack/test/security_solution_cypress/cypress/tasks/entity_analytics.ts @@ -27,6 +27,11 @@ import { import { visitWithTimeRange } from './navigation'; import { GET_DATE_PICKER_APPLY_BUTTON, GLOBAL_FILTERS_CONTAINER } from '../screens/date_picker'; import { REFRESH_BUTTON } from '../screens/security_header'; +import { + ENTITIES_LIST_PANEL, + ENTITY_STORE_ENABLEMENT_BUTTON, + ENTITY_STORE_ENABLEMENT_MODAL, +} from '../screens/entity_analytics/dashboard'; export const updateDashboardTimeRange = () => { // eslint-disable-next-line cypress/no-force @@ -113,3 +118,17 @@ export const upgradeRiskEngine = () => { updateRiskEngineConfirm(); cy.get(RISK_SCORE_STATUS).should('have.text', 'On'); }; + +export const openEntityStoreEnablementModal = () => { + cy.get(ENTITY_STORE_ENABLEMENT_BUTTON).click(); + cy.get(ENTITY_STORE_ENABLEMENT_MODAL).contains('Entity Analytics Enablement'); +}; + +export const confirmEntityStoreEnablement = () => { + cy.get(ENTITY_STORE_ENABLEMENT_BUTTON).click(); +}; + +export const waitForEntitiesListToAppear = () => { + cy.get(ENTITIES_LIST_PANEL, { timeout: 30000 }).scrollIntoView(); + cy.get(ENTITIES_LIST_PANEL).contains('Entities'); +}; From b6b43b930013e3471d19654228023d8698867eaf Mon Sep 17 00:00:00 2001 From: Tiago Vila Verde Date: Tue, 26 Nov 2024 13:22:52 +0100 Subject: [PATCH 8/9] fix config --- .../cypress/cypress_ci_serverless.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/test/security_solution_cypress/cypress/cypress_ci_serverless.config.ts b/x-pack/test/security_solution_cypress/cypress/cypress_ci_serverless.config.ts index 868a9538b55c6..2786ce7539092 100644 --- a/x-pack/test/security_solution_cypress/cypress/cypress_ci_serverless.config.ts +++ b/x-pack/test/security_solution_cypress/cypress/cypress_ci_serverless.config.ts @@ -31,7 +31,7 @@ export default defineCypressConfig({ }, screenshotsFolder: '../../../target/kibana-security-solution/cypress/screenshots', trashAssetsBeforeRuns: false, - video: true, + video: false, videosFolder: '../../../../target/kibana-security-solution/cypress/videos', viewportHeight: 1200, viewportWidth: 1920, From fdde799261c11d317782108a97e4a8fafbe81dcc Mon Sep 17 00:00:00 2001 From: machadoum Date: Thu, 28 Nov 2024 10:51:49 +0100 Subject: [PATCH 9/9] Fix cypress test error --- .../cypress/tasks/entity_analytics.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/security_solution_cypress/cypress/tasks/entity_analytics.ts b/x-pack/test/security_solution_cypress/cypress/tasks/entity_analytics.ts index 791fa48acaad1..91a5c36a51928 100644 --- a/x-pack/test/security_solution_cypress/cypress/tasks/entity_analytics.ts +++ b/x-pack/test/security_solution_cypress/cypress/tasks/entity_analytics.ts @@ -28,6 +28,7 @@ import { visitWithTimeRange } from './navigation'; import { GET_DATE_PICKER_APPLY_BUTTON, GLOBAL_FILTERS_CONTAINER } from '../screens/date_picker'; import { REFRESH_BUTTON } from '../screens/security_header'; import { + ENABLEMENT_MODAL_CONFIRM_BUTTON, ENTITIES_LIST_PANEL, ENTITY_STORE_ENABLEMENT_BUTTON, ENTITY_STORE_ENABLEMENT_MODAL, @@ -125,7 +126,7 @@ export const openEntityStoreEnablementModal = () => { }; export const confirmEntityStoreEnablement = () => { - cy.get(ENTITY_STORE_ENABLEMENT_BUTTON).click(); + cy.get(ENABLEMENT_MODAL_CONFIRM_BUTTON).click(); }; export const waitForEntitiesListToAppear = () => {