diff --git a/src/platform/packages/shared/kbn-securitysolution-ecs/src/entity/index.ts b/src/platform/packages/shared/kbn-securitysolution-ecs/src/entity/index.ts new file mode 100644 index 0000000000000..f9ba173a38c58 --- /dev/null +++ b/src/platform/packages/shared/kbn-securitysolution-ecs/src/entity/index.ts @@ -0,0 +1,16 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +// TODO: Asset Inventory - This file is a placeholder for the ECS schema that will be used in the Asset Inventory app +export interface EntityEcs { + id: string; + name: string; + type: 'universal' | 'user' | 'host' | 'service'; + timestamp: Date; +} diff --git a/x-pack/platform/packages/shared/kbn-cloud-security-posture/common/utils/ui_metrics.ts b/x-pack/platform/packages/shared/kbn-cloud-security-posture/common/utils/ui_metrics.ts index 23f0144729986..b8e613bd4326a 100644 --- a/x-pack/platform/packages/shared/kbn-cloud-security-posture/common/utils/ui_metrics.ts +++ b/x-pack/platform/packages/shared/kbn-cloud-security-posture/common/utils/ui_metrics.ts @@ -48,6 +48,11 @@ export const CHANGE_RULE_STATE = 'change-rule-state' as const; export const GRAPH_PREVIEW = 'graph-preview' as const; export const GRAPH_INVESTIGATION = 'graph-investigation' as const; +export const ASSET_INVENTORY_EXPAND_FLYOUT_SUCCESS = + 'asset-inventory-expand-flyout-success' as const; +export const ASSET_INVENTORY_EXPAND_FLYOUT_ERROR = 'asset-inventory-expand-flyout-error' as const; +export const UNIVERSAL_ENTITY_FLYOUT_OPENED = 'universal-entity-flyout-opened' as const; + export type CloudSecurityUiCounters = | typeof ENTITY_FLYOUT_WITH_MISCONFIGURATION_VISIT | typeof ENTITY_FLYOUT_WITH_VULNERABILITY_PREVIEW @@ -68,7 +73,10 @@ export type CloudSecurityUiCounters = | typeof VULNERABILITIES_INSIGHT_HOST_DETAILS | typeof VULNERABILITIES_INSIGHT_HOST_ENTITY_OVERVIEW | typeof GRAPH_PREVIEW - | typeof GRAPH_INVESTIGATION; + | typeof GRAPH_INVESTIGATION + | typeof ASSET_INVENTORY_EXPAND_FLYOUT_SUCCESS + | typeof ASSET_INVENTORY_EXPAND_FLYOUT_ERROR + | typeof UNIVERSAL_ENTITY_FLYOUT_OPENED; export class UiMetricService { private usageCollection: UsageCollectionSetup | undefined; diff --git a/x-pack/solutions/security/plugins/security_solution/public/asset_inventory/hooks/use_dynamic_entity_flyout.test.ts b/x-pack/solutions/security/plugins/security_solution/public/asset_inventory/hooks/use_dynamic_entity_flyout.test.ts new file mode 100644 index 0000000000000..5dabdbac38e8d --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/asset_inventory/hooks/use_dynamic_entity_flyout.test.ts @@ -0,0 +1,199 @@ +/* + * 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'; +import { useDynamicEntityFlyout } from './use_dynamic_entity_flyout'; +import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; +import { useKibana } from '../../common/lib/kibana'; +import { useOnExpandableFlyoutClose } from '../../flyout/shared/hooks/use_on_expandable_flyout_close'; +import { + UniversalEntityPanelKey, + UserPanelKey, + HostPanelKey, + ServicePanelKey, +} from '../../flyout/entity_details/shared/constants'; +import type { EntityEcs } from '@kbn/securitysolution-ecs/src/entity'; + +jest.mock('@kbn/expandable-flyout', () => ({ + useExpandableFlyoutApi: jest.fn(), +})); + +jest.mock('../../common/lib/kibana', () => ({ + useKibana: jest.fn(), +})); + +jest.mock('../../flyout/shared/hooks/use_on_expandable_flyout_close', () => ({ + useOnExpandableFlyoutClose: jest.fn(), +})); + +const entity = { + id: '123', + name: 'test-entity', + type: 'universal', + timestamp: new Date(), +}; + +describe('useDynamicEntityFlyout', () => { + let openFlyoutMock: jest.Mock; + let closeFlyoutMock: jest.Mock; + let toastsMock: { addDanger: jest.Mock }; + let onFlyoutCloseMock: jest.Mock; + + beforeEach(() => { + openFlyoutMock = jest.fn(); + closeFlyoutMock = jest.fn(); + toastsMock = { addDanger: jest.fn() }; + onFlyoutCloseMock = jest.fn(); + + (useExpandableFlyoutApi as jest.Mock).mockReturnValue({ + openFlyout: openFlyoutMock, + closeFlyout: closeFlyoutMock, + }); + (useKibana as jest.Mock).mockReturnValue({ + services: { notifications: { toasts: toastsMock } }, + }); + (useOnExpandableFlyoutClose as jest.Mock).mockImplementation(({ callback }) => callback); + }); + + it('should open the flyout with correct params for a universal entity', () => { + const { result } = renderHook(() => + useDynamicEntityFlyout({ onFlyoutClose: onFlyoutCloseMock }) + ); + + act(() => { + result.current.openDynamicFlyout({ + entity: { ...entity, type: 'universal', name: 'testUniversal' }, + scopeId: 'scope1', + contextId: 'context1', + }); + }); + + expect(openFlyoutMock).toHaveBeenCalledWith({ + right: { + id: UniversalEntityPanelKey, + params: { entity: { ...entity, type: 'universal', name: 'testUniversal' } }, + }, + }); + }); + + it('should open the flyout with correct params for a user entity', () => { + const { result } = renderHook(() => + useDynamicEntityFlyout({ onFlyoutClose: onFlyoutCloseMock }) + ); + + act(() => { + result.current.openDynamicFlyout({ + entity: { ...entity, type: 'user', name: 'testUser' }, + scopeId: 'scope1', + contextId: 'context1', + }); + }); + + expect(openFlyoutMock).toHaveBeenCalledWith({ + right: { + id: UserPanelKey, + params: { userName: 'testUser', scopeId: 'scope1', contextId: 'context1' }, + }, + }); + }); + + it('should open the flyout with correct params for a host entity', () => { + const { result } = renderHook(() => + useDynamicEntityFlyout({ onFlyoutClose: onFlyoutCloseMock }) + ); + + act(() => { + result.current.openDynamicFlyout({ + entity: { ...entity, type: 'host', name: 'testHost' }, + scopeId: 'scope1', + contextId: 'context1', + }); + }); + + expect(openFlyoutMock).toHaveBeenCalledWith({ + right: { + id: HostPanelKey, + params: { hostName: 'testHost', scopeId: 'scope1', contextId: 'context1' }, + }, + }); + }); + + it('should open the flyout with correct params for a service entity', () => { + const { result } = renderHook(() => + useDynamicEntityFlyout({ onFlyoutClose: onFlyoutCloseMock }) + ); + + act(() => { + result.current.openDynamicFlyout({ + entity: { ...entity, type: 'service', name: 'testService' }, + scopeId: 'scope1', + contextId: 'context1', + }); + }); + + expect(openFlyoutMock).toHaveBeenCalledWith({ + right: { + id: ServicePanelKey, + params: { serviceName: 'testService', scopeId: 'scope1', contextId: 'context1' }, + }, + }); + }); + + it('should show an error toast and close flyout if entity name is missing for user, host, or service entities', () => { + const { result } = renderHook(() => + useDynamicEntityFlyout({ onFlyoutClose: onFlyoutCloseMock }) + ); + + act(() => { + result.current.openDynamicFlyout({ entity: { type: 'user' } as EntityEcs }); + }); + + expect(toastsMock.addDanger).toHaveBeenCalledWith( + expect.objectContaining({ + title: expect.any(String), + text: expect.any(String), + }) + ); + expect(onFlyoutCloseMock).toHaveBeenCalled(); + + act(() => { + result.current.openDynamicFlyout({ entity: { type: 'host' } as EntityEcs }); + }); + + expect(toastsMock.addDanger).toHaveBeenCalledWith( + expect.objectContaining({ + title: expect.any(String), + text: expect.any(String), + }) + ); + expect(onFlyoutCloseMock).toHaveBeenCalled(); + + act(() => { + result.current.openDynamicFlyout({ entity: { type: 'service' } as EntityEcs }); + }); + + expect(toastsMock.addDanger).toHaveBeenCalledWith( + expect.objectContaining({ + title: expect.any(String), + text: expect.any(String), + }) + ); + expect(onFlyoutCloseMock).toHaveBeenCalled(); + }); + + it('should close the flyout when closeDynamicFlyout is called', () => { + const { result } = renderHook(() => + useDynamicEntityFlyout({ onFlyoutClose: onFlyoutCloseMock }) + ); + + act(() => { + result.current.closeDynamicFlyout(); + }); + + expect(closeFlyoutMock).toHaveBeenCalled(); + }); +}); diff --git a/x-pack/solutions/security/plugins/security_solution/public/asset_inventory/hooks/use_dynamic_entity_flyout.ts b/x-pack/solutions/security/plugins/security_solution/public/asset_inventory/hooks/use_dynamic_entity_flyout.ts new file mode 100644 index 0000000000000..c0f14b9627ca2 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/asset_inventory/hooks/use_dynamic_entity_flyout.ts @@ -0,0 +1,109 @@ +/* + * 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 { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; +import type { EntityEcs } from '@kbn/securitysolution-ecs/src/entity'; +import { + ASSET_INVENTORY_EXPAND_FLYOUT_SUCCESS, + ASSET_INVENTORY_EXPAND_FLYOUT_ERROR, + uiMetricService, +} from '@kbn/cloud-security-posture-common/utils/ui_metrics'; +import { METRIC_TYPE } from '@kbn/analytics'; +import { i18n } from '@kbn/i18n'; +import { useKibana } from '../../common/lib/kibana'; +import { + HostPanelKey, + ServicePanelKey, + UniversalEntityPanelKey, + UserPanelKey, +} from '../../flyout/entity_details/shared/constants'; +import { useOnExpandableFlyoutClose } from '../../flyout/shared/hooks/use_on_expandable_flyout_close'; + +interface InventoryFlyoutProps { + entity: EntityEcs; + scopeId?: string; + contextId?: string; +} + +interface SecurityFlyoutPanelsCommonParams { + scopeId?: string; + contextId?: string; + [key: string]: unknown; +} + +type FlyoutParams = + | { + id: typeof UniversalEntityPanelKey; + params: { entity: EntityEcs }; + } + | { id: typeof UserPanelKey; params: { userName: string } & SecurityFlyoutPanelsCommonParams } + | { id: typeof HostPanelKey; params: { hostName: string } & SecurityFlyoutPanelsCommonParams } + | { + id: typeof ServicePanelKey; + params: { serviceName: string } & SecurityFlyoutPanelsCommonParams; + }; + +const getFlyoutParamsByEntity = ({ + entity, + scopeId, + contextId, +}: InventoryFlyoutProps): FlyoutParams => { + const entitiesFlyoutParams: Record = { + universal: { id: UniversalEntityPanelKey, params: { entity } }, + user: { id: UserPanelKey, params: { userName: entity.name, scopeId, contextId } }, + host: { id: HostPanelKey, params: { hostName: entity.name, scopeId, contextId } }, + service: { id: ServicePanelKey, params: { serviceName: entity.name, scopeId, contextId } }, + } as const; + + return entitiesFlyoutParams[entity.type]; +}; + +export const useDynamicEntityFlyout = ({ onFlyoutClose }: { onFlyoutClose: () => void }) => { + const { openFlyout, closeFlyout } = useExpandableFlyoutApi(); + const { notifications } = useKibana().services; + useOnExpandableFlyoutClose({ callback: onFlyoutClose }); + + const openDynamicFlyout = ({ entity, scopeId, contextId }: InventoryFlyoutProps) => { + const entityFlyoutParams = getFlyoutParamsByEntity({ entity, scopeId, contextId }); + + // User, Host, and Service entity flyouts rely on entity name to fetch required data + if (entity.type !== 'universal' && !entity.name) { + notifications.toasts.addDanger({ + title: i18n.translate( + 'xpack.securitySolution.assetInventory.openFlyout.missingEntityNameTitle', + { defaultMessage: 'Missing Entity Name' } + ), + text: i18n.translate( + 'xpack.securitySolution.assetInventory.openFlyout.missingEntityNameText', + { defaultMessage: 'Entity name is required for User, Host, and Service entities' } + ), + }); + + uiMetricService.trackUiMetric(METRIC_TYPE.CLICK, ASSET_INVENTORY_EXPAND_FLYOUT_ERROR); + onFlyoutClose(); + return; + } + + uiMetricService.trackUiMetric(METRIC_TYPE.CLICK, ASSET_INVENTORY_EXPAND_FLYOUT_SUCCESS); + + openFlyout({ + right: { + id: entityFlyoutParams.id || UniversalEntityPanelKey, + params: entityFlyoutParams.params, + }, + }); + }; + + const closeDynamicFlyout = () => { + closeFlyout(); + }; + + return { + openDynamicFlyout, + closeDynamicFlyout, + }; +}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/asset_inventory/pages/all_assets.tsx b/x-pack/solutions/security/plugins/security_solution/public/asset_inventory/pages/all_assets.tsx index 324e6f01cf637..1e1bd229ffa24 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/asset_inventory/pages/all_assets.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/asset_inventory/pages/all_assets.tsx @@ -39,6 +39,9 @@ import { type DocViewFilterFn } from '@kbn/unified-doc-viewer/types'; import useLocalStorage from 'react-use/lib/useLocalStorage'; import { css } from '@emotion/react'; +import type { EntityEcs } from '@kbn/securitysolution-ecs/src/entity'; +import { EmptyComponent } from '../../common/lib/cell_actions/helpers'; +import { useDynamicEntityFlyout } from '../hooks/use_dynamic_entity_flyout'; import { type CriticalityLevelWithUnassigned } from '../../../common/entity_analytics/asset_criticality/types'; import { useKibana } from '../../common/lib/kibana'; @@ -135,14 +138,21 @@ export interface AllAssetsProps { * This function will be used in the control column to create a rule for a specific finding. */ createFn?: (rowIndex: number) => ((http: HttpSetup) => Promise) | undefined; - /** - * This is the component that will be rendered in the flyout when a row is expanded. - * This component will receive the row data and a function to close the flyout. - */ - flyoutComponent: (hit: DataTableRecord, onCloseFlyout: () => void) => JSX.Element; 'data-test-subj'?: string; } +// TODO: Asset Inventory - adjust and remove type casting once we have real universal entity data +const getEntity = (row: DataTableRecord): EntityEcs => { + return { + id: (row.flattened['asset.name'] as string) || '', + name: (row.flattened['asset.name'] as string) || '', + timestamp: row.flattened['@timestamp'] as Date, + type: 'universal', + }; +}; + +const ASSET_INVENTORY_TABLE_ID = 'asset-inventory-table'; + const AllAssets = ({ rows, isLoading, @@ -151,7 +161,6 @@ const AllAssets = ({ height, hasDistributionBar = true, createFn, - flyoutComponent, ...rest }: AllAssetsProps) => { const { euiTheme } = useEuiTheme(); @@ -162,6 +171,31 @@ const AllAssets = ({ nonPersistedFilters, }); + // Table Flyout Controls ------------------------------------------------------------------- + + const [expandedDoc, setExpandedDoc] = useState(undefined); + + const { openDynamicFlyout, closeDynamicFlyout } = useDynamicEntityFlyout({ + onFlyoutClose: () => setExpandedDoc(undefined), + }); + + const onExpandDocClick = (doc?: DataTableRecord | undefined) => { + if (doc) { + const entity = getEntity(doc); + setExpandedDoc(doc); // Table is expecting the same doc ref to highlight the selected row + openDynamicFlyout({ + entity, + scopeId: ASSET_INVENTORY_TABLE_ID, + contextId: ASSET_INVENTORY_TABLE_ID, + }); + } else { + closeDynamicFlyout(); + setExpandedDoc(undefined); + } + }; + + // ----------------------------------------------------------------------------------------- + const { // columnsLocalStorageKey, pageSize, @@ -206,11 +240,6 @@ const AllAssets = ({ const { dataView, dataViewIsLoading, dataViewIsRefetching } = useDataViewContext(); - const [expandedDoc, setExpandedDoc] = useState(undefined); - - const renderDocumentView = (hit: DataTableRecord) => - flyoutComponent(hit, () => setExpandedDoc(undefined)); - const { uiActions, uiSettings, @@ -413,7 +442,6 @@ const AllAssets = ({ className={styles.gridStyle} ariaLabelledBy={title} columns={currentColumns} - expandedDoc={expandedDoc} dataView={dataView} loadingState={loadingState} onFilter={onAddFilter as DocViewFilterFn} @@ -422,8 +450,9 @@ const AllAssets = ({ onSort={onSort} rows={rows} sampleSizeState={MAX_ASSETS_TO_LOAD} - setExpandedDoc={setExpandedDoc} - renderDocumentView={renderDocumentView} + expandedDoc={expandedDoc} + setExpandedDoc={onExpandDocClick} + renderDocumentView={EmptyComponent} sort={sort} rowsPerPageState={pageSize} totalHits={rows.length} diff --git a/x-pack/solutions/security/plugins/security_solution/public/asset_inventory/routes.tsx b/x-pack/solutions/security/plugins/security_solution/public/asset_inventory/routes.tsx index e4fc499106186..ce7993fd68a2f 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/asset_inventory/routes.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/asset_inventory/routes.tsx @@ -49,12 +49,7 @@ export const AssetInventoryRoutes = () => { }> - {}} - flyoutComponent={() => <>} - /> + {}} /> diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/shared/constants.ts b/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/shared/constants.ts index ae123243aa04e..64428b35056e0 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/shared/constants.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/shared/constants.ts @@ -5,6 +5,7 @@ * 2.0. */ +import type { UniversalEntityPanelExpandableFlyoutProps } from '../universal_right'; import { EntityType } from '../../../../common/entity_analytics/types'; import type { HostPanelExpandableFlyoutProps } from '../host_right'; import type { ServicePanelExpandableFlyoutProps } from '../service_right'; @@ -27,6 +28,8 @@ export const MANAGED_USER_QUERY_ID = 'managedUserDetailsQuery'; export const HostPanelKey: HostPanelExpandableFlyoutProps['key'] = 'host-panel'; export const UserPanelKey: UserPanelExpandableFlyoutProps['key'] = 'user-panel'; export const ServicePanelKey: ServicePanelExpandableFlyoutProps['key'] = 'service-panel'; +export const UniversalEntityPanelKey: UniversalEntityPanelExpandableFlyoutProps['key'] = + 'universal-entity-panel'; export const EntityPanelKeyByType: Record = { [EntityType.host]: HostPanelKey, diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/universal_right/content.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/universal_right/content.tsx new file mode 100644 index 0000000000000..5226ed0379a2e --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/universal_right/content.tsx @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import type { EntityEcs } from '@kbn/securitysolution-ecs/src/entity'; +import { FlyoutBody } from '../../shared/components/flyout_body'; + +interface UniversalEntityFlyoutContentProps { + entity: EntityEcs; +} + +export const UniversalEntityFlyoutContent = ({ entity }: UniversalEntityFlyoutContentProps) => { + return {entity.type}; +}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/universal_right/header.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/universal_right/header.tsx new file mode 100644 index 0000000000000..5aee5f15930d1 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/universal_right/header.tsx @@ -0,0 +1,37 @@ +/* + * 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 { EuiSpacer, EuiText, EuiFlexItem, EuiFlexGroup } from '@elastic/eui'; +import React from 'react'; + +import type { EntityEcs } from '@kbn/securitysolution-ecs/src/entity'; +import { EntityIconByType } from '../../../entity_analytics/components/entity_store/helpers'; +import { PreferenceFormattedDate } from '../../../common/components/formatted_date'; +import { FlyoutHeader } from '../../shared/components/flyout_header'; +import { FlyoutTitle } from '../../shared/components/flyout_title'; + +interface UniversalEntityFlyoutHeaderProps { + entity: EntityEcs; +} + +export const UniversalEntityFlyoutHeader = ({ entity }: UniversalEntityFlyoutHeaderProps) => { + return ( + + + + + + + + + + + + + + ); +}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/universal_right/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/universal_right/index.tsx new file mode 100644 index 0000000000000..ed9f1a7074eec --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/universal_right/index.tsx @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useEffect } from 'react'; +import type { FlyoutPanelProps } from '@kbn/expandable-flyout'; +import { + uiMetricService, + UNIVERSAL_ENTITY_FLYOUT_OPENED, +} from '@kbn/cloud-security-posture-common/utils/ui_metrics'; +import { METRIC_TYPE } from '@kbn/analytics'; +import type { EntityEcs } from '@kbn/securitysolution-ecs/src/entity'; +import { UniversalEntityFlyoutHeader } from './header'; +import { UniversalEntityFlyoutContent } from './content'; +import { FlyoutNavigation } from '../../shared/components/flyout_navigation'; + +export interface UniversalEntityPanelProps { + entity: EntityEcs; + /** this is because FlyoutPanelProps defined params as Record {@link FlyoutPanelProps#params} */ + [key: string]: unknown; +} + +export interface UniversalEntityPanelExpandableFlyoutProps extends FlyoutPanelProps { + key: 'universal-entity-panel'; + params: UniversalEntityPanelProps; +} + +export const UniversalEntityPanel = ({ entity }: UniversalEntityPanelProps) => { + useEffect(() => { + uiMetricService.trackUiMetric(METRIC_TYPE.COUNT, UNIVERSAL_ENTITY_FLYOUT_OPENED); + }, [entity]); + + return ( + <> + + + + + ); +}; + +UniversalEntityPanel.displayName = 'UniversalEntityPanel'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/index.tsx index 6ec67155e01a5..9fa489ab9b334 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/index.tsx @@ -8,6 +8,8 @@ import React, { memo, useCallback } from 'react'; import { ExpandableFlyout, type ExpandableFlyoutProps } from '@kbn/expandable-flyout'; import { useEuiTheme } from '@elastic/eui'; +import type { UniversalEntityPanelExpandableFlyoutProps } from './entity_details/universal_right'; +import { UniversalEntityPanel } from './entity_details/universal_right'; import { SessionViewPanelProvider } from './document_details/session_view/context'; import type { SessionViewPanelProps } from './document_details/session_view'; import { SessionViewPanel } from './document_details/session_view'; @@ -46,7 +48,12 @@ import { HostDetailsPanel, HostDetailsPanelKey } from './entity_details/host_det import { NetworkPanel, NetworkPanelKey, NetworkPreviewPanelKey } from './network_details'; import type { AnalyzerPanelExpandableFlyoutProps } from './document_details/analyzer_panels'; import { AnalyzerPanel } from './document_details/analyzer_panels'; -import { UserPanelKey, HostPanelKey, ServicePanelKey } from './entity_details/shared/constants'; +import { + UserPanelKey, + HostPanelKey, + ServicePanelKey, + UniversalEntityPanelKey, +} from './entity_details/shared/constants'; import type { ServicePanelExpandableFlyoutProps } from './entity_details/service_right'; import { ServicePanel } from './entity_details/service_right'; import type { ServiceDetailsExpandableFlyoutProps } from './entity_details/service_details_left'; @@ -174,6 +181,12 @@ const expandableFlyoutDocumentsPanels: ExpandableFlyoutProps['registeredPanels'] ), }, + { + key: UniversalEntityPanelKey, + component: (props) => ( + + ), + }, ]; export const SECURITY_SOLUTION_ON_CLOSE_EVENT = `expandable-flyout-on-close-${Flyouts.securitySolution}`;