mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Cloud Security] Asset Inventory table flyout controls (#208452)
This commit is contained in:
parent
06801d82fe
commit
000e913e7a
11 changed files with 494 additions and 22 deletions
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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<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) {
|
||||
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,
|
||||
};
|
||||
};
|
|
@ -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<unknown>) | 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<DataTableRecord | undefined>(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<DataTableRecord | undefined>(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}
|
||||
|
|
|
@ -49,12 +49,7 @@ export const AssetInventoryRoutes = () => {
|
|||
<DataViewContext.Provider value={dataViewContextValue}>
|
||||
<SecuritySolutionPageWrapper noPadding>
|
||||
<Suspense fallback={<EuiLoadingSpinner />}>
|
||||
<AllAssetsLazy
|
||||
rows={[]}
|
||||
isLoading={false}
|
||||
loadMore={() => {}}
|
||||
flyoutComponent={() => <></>}
|
||||
/>
|
||||
<AllAssetsLazy rows={[]} isLoading={false} loadMore={() => {}} />
|
||||
</Suspense>
|
||||
</SecuritySolutionPageWrapper>
|
||||
</DataViewContext.Provider>
|
||||
|
|
|
@ -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, string | undefined> = {
|
||||
[EntityType.host]: HostPanelKey,
|
||||
|
|
|
@ -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 <FlyoutBody>{entity.type}</FlyoutBody>;
|
||||
};
|
|
@ -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 (
|
||||
<FlyoutHeader data-test-subj="service-panel-header">
|
||||
<EuiFlexGroup gutterSize="s" responsive={false} direction="column">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText size="xs" data-test-subj={'service-panel-header-lastSeen'}>
|
||||
<PreferenceFormattedDate value={entity?.timestamp} />
|
||||
<EuiSpacer size="xs" />
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<FlyoutTitle title={entity?.id} iconType={EntityIconByType[entity?.type]} />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</FlyoutHeader>
|
||||
);
|
||||
};
|
|
@ -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<string, unknown> {@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 (
|
||||
<>
|
||||
<FlyoutNavigation flyoutIsExpandable={false} />
|
||||
<UniversalEntityFlyoutHeader entity={entity} />
|
||||
<UniversalEntityFlyoutContent entity={entity} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
UniversalEntityPanel.displayName = 'UniversalEntityPanel';
|
|
@ -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']
|
|||
<ServiceDetailsPanel {...(props as ServiceDetailsExpandableFlyoutProps).params} />
|
||||
),
|
||||
},
|
||||
{
|
||||
key: UniversalEntityPanelKey,
|
||||
component: (props) => (
|
||||
<UniversalEntityPanel {...(props as UniversalEntityPanelExpandableFlyoutProps).params} />
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
export const SECURITY_SOLUTION_ON_CLOSE_EVENT = `expandable-flyout-on-close-${Flyouts.securitySolution}`;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue