-
Notifications
You must be signed in to change notification settings - Fork 8.5k
[Cloud Security] Asset Inventory table flyout controls #208452
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
d1d39aa
a3e4aff
561b852
352253f
e8bfc43
c5d37a5
fe8766b
36db7f8
b1ff93e
4814c2c
3c3bd43
b29aeab
89c54ef
bce8adf
32bde94
3fd91a4
2fd4e4d
35de92b
f401e11
940863c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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(); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -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 = | ||||||
| | { | ||||||
JordanSh marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
| 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<EntityEcs['type'], FlyoutParams> = { | ||||||
| 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) { | ||||||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Not sure if this needs a constant right now, mainly because I would prefer it to be coming from ECS and not some local value
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I agree, it would be very beneficial for consistency to have the For consistency, I also think there's a huge benefit from having the |
||||||
| 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(); | ||||||
| }; | ||||||
|
Comment on lines
+101
to
+103
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Once we'll add the details and preview panels, this function will expand. right now its mostly for comfort so you dont have to import both this hook and the expandable flyout api separately just to close the flyout |
||||||
|
|
||||||
| return { | ||||||
| openDynamicFlyout, | ||||||
| closeDynamicFlyout, | ||||||
| }; | ||||||
| }; | ||||||
Uh oh!
There was an error while loading. Please reload this page.