[Security Solutions] Update Events/alerts table to use FieldSpec for CellActions (#161361)

EPIC: https://github.com/elastic/kibana/issues/144943

## Summary

Update Events/alerts table to provide `CellActions` with a complete
`FieldSpec`object from DataView

### Affected pages:
* Alerts page
* Security Dashboards
* Rule preview
* Host events
* Users events

### How to test it
Use CellActions on one of the affected pages.




### Checklist

- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Pablo Machado 2023-07-10 16:57:04 +02:00 committed by GitHub
parent ff6099eb3f
commit 6db79db1e0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 67 additions and 71 deletions

View file

@ -6,9 +6,9 @@
* Side Public License, v 1.
*/
import { DataView } from '@kbn/data-views-plugin/public';
import { DataView, DataViewField } from '@kbn/data-views-plugin/public';
export const fields = [
export const shallowMockedFields = [
{
name: '_source',
type: '_source',
@ -73,6 +73,10 @@ export const fields = [
},
] as DataView['fields'];
export const deepMockedFields = shallowMockedFields.map(
(field) => new DataViewField(field)
) as DataView['fields'];
export const buildDataViewMock = ({
name,
fields: definedFields,
@ -120,4 +124,7 @@ export const buildDataViewMock = ({
return dataView;
};
export const dataViewMock = buildDataViewMock({ name: 'the-data-view', fields });
export const dataViewMock = buildDataViewMock({
name: 'the-data-view',
fields: shallowMockedFields,
});

View file

@ -11,7 +11,7 @@ import { EuiCopy } from '@elastic/eui';
import { act } from 'react-dom/test-utils';
import { findTestSubject } from '@elastic/eui/lib/test';
import { esHits } from '../../__mocks__/es_hits';
import { buildDataViewMock, fields } from '../../__mocks__/data_view';
import { buildDataViewMock, deepMockedFields } from '../../__mocks__/data_view';
import { mountWithIntl } from '@kbn/test-jest-helpers';
import { DiscoverGrid, DiscoverGridProps } from './discover_grid';
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
@ -28,7 +28,7 @@ jest.mock('@kbn/cell-actions', () => ({
export const dataViewMock = buildDataViewMock({
name: 'the-data-view',
fields,
fields: deepMockedFields,
timeFieldName: '@timestamp',
});
@ -259,18 +259,8 @@ describe('DiscoverGrid', () => {
triggerId: 'test',
getCellValue: expect.any(Function),
fields: [
{
name: '@timestamp',
type: 'date',
aggregatable: true,
searchable: undefined,
},
{
name: 'message',
type: 'string',
aggregatable: false,
searchable: undefined,
},
dataViewMock.getFieldByName('@timestamp')?.toSpec(),
dataViewMock.getFieldByName('message')?.toSpec(),
],
})
);

View file

@ -456,23 +456,15 @@ export const DiscoverGrid = ({
const cellActionsFields = useMemo<UseDataGridColumnsCellActionsProps['fields']>(
() =>
cellActionsTriggerId && !isPlainRecord
? visibleColumns.map((columnName) => {
const field = dataView.getFieldByName(columnName);
if (!field) {
return {
? visibleColumns.map(
(columnName) =>
dataView.getFieldByName(columnName)?.toSpec() ?? {
name: '',
type: '',
aggregatable: false,
searchable: false,
};
}
return {
name: columnName,
type: field.type,
aggregatable: field.aggregatable,
searchable: field.searchable,
};
})
}
)
: undefined,
[cellActionsTriggerId, isPlainRecord, visibleColumns, dataView]
);

View file

@ -12,7 +12,6 @@ import { createFilterManagerMock } from '@kbn/data-plugin/public/query/filter_ma
import { SearchInput } from '..';
import { getSavedSearchUrl } from '@kbn/saved-search-plugin/public';
import { DiscoverServices } from '../build_services';
import { dataViewMock } from '../__mocks__/data_view';
import { discoverServiceMock } from '../__mocks__/services';
import { SavedSearchEmbeddable, SearchEmbeddableConfig } from './saved_search_embeddable';
import { render } from 'react-dom';
@ -23,6 +22,7 @@ import { SHOW_FIELD_STATISTICS } from '../../common';
import { IUiSettingsClient } from '@kbn/core-ui-settings-browser';
import { SavedSearchEmbeddableComponent } from './saved_search_embeddable_component';
import { VIEW_MODE } from '../../common/constants';
import { buildDataViewMock, deepMockedFields } from '../__mocks__/data_view';
let discoverComponent: ReactWrapper;
@ -48,6 +48,8 @@ function getSearchResponse(nrOfHits: number) {
});
}
const dataViewMock = buildDataViewMock({ name: 'the-data-view', fields: deepMockedFields });
describe('saved search embeddable', () => {
let mountpoint: HTMLDivElement;
let filterManagerMock: jest.Mocked<FilterManager>;

View file

@ -84,6 +84,7 @@ export const DataTable = () => {
<StoryProviders>
<DataTableComponent
browserFields={{}}
getFieldSpec={() => undefined}
data={mockTimelineData}
id={TableId.test}
renderCellValue={StoryCellRenderer}

View file

@ -71,6 +71,7 @@ describe('DataTable', () => {
const mount = useMountAppended();
const props: DataTableProps = {
browserFields: mockBrowserFields,
getFieldSpec: () => undefined,
data: mockTimelineData,
id: TableId.test,
loadPage: jest.fn(),
@ -158,11 +159,21 @@ describe('DataTable', () => {
describe('cellActions', () => {
test('calls useDataGridColumnsCellActions properly', () => {
const data = mockTimelineData.slice(0, 1);
const timestampFieldSpec = {
name: '@timestamp',
type: 'date',
aggregatable: true,
esTypes: ['date'],
searchable: true,
};
const wrapper = mount(
<TestProviders>
<DataTableComponent
cellActionsTriggerId="mockCellActionsTrigger"
{...props}
getFieldSpec={(name) =>
timestampFieldSpec.name === name ? timestampFieldSpec : undefined
}
data={data}
/>
</TestProviders>
@ -171,16 +182,7 @@ describe('DataTable', () => {
expect(mockUseDataGridColumnsCellActions).toHaveBeenCalledWith({
triggerId: 'mockCellActionsTrigger',
fields: [
{
name: '@timestamp',
type: 'date',
aggregatable: true,
esTypes: ['date'],
searchable: true,
subType: undefined,
},
],
fields: [timestampFieldSpec],
getCellValue: expect.any(Function),
metadata: {
scopeId: 'table-test',

View file

@ -42,6 +42,7 @@ import {
useDataGridColumnsCellActions,
UseDataGridColumnsCellActionsProps,
} from '@kbn/cell-actions';
import { FieldSpec } from '@kbn/data-views-plugin/common';
import { DataTableModel, DataTableState } from '../../store/data_table/types';
import { getColumnHeader, getColumnHeaders } from './column_headers/helpers';
@ -96,6 +97,7 @@ interface BaseDataTableProps {
rowHeightsOptions?: EuiDataGridRowHeightsOptions;
isEventRenderedView?: boolean;
getFieldBrowser: GetFieldBrowser;
getFieldSpec: (fieldName: string) => FieldSpec | undefined;
cellActionsTriggerId?: string;
}
@ -154,6 +156,7 @@ export const DataTableComponent = React.memo<DataTableProps>(
rowHeightsOptions,
isEventRenderedView = false,
getFieldBrowser,
getFieldSpec,
cellActionsTriggerId,
...otherProps
}) => {
@ -331,21 +334,20 @@ export const DataTableComponent = React.memo<DataTableProps>(
);
const cellActionsMetadata = useMemo(() => ({ scopeId: id }), [id]);
const cellActionsFields = useMemo<UseDataGridColumnsCellActionsProps['fields']>(
() =>
cellActionsTriggerId
? // TODO use FieldSpec object instead of column
columnHeaders.map((column) => ({
name: column.id,
type: column.type ?? '', // When type is an empty string all cell actions are incompatible
aggregatable: column.aggregatable ?? false,
searchable: column.searchable ?? false,
esTypes: column.esTypes ?? [],
subType: column.subType,
}))
? columnHeaders.map(
(column) =>
getFieldSpec(column.id) ?? {
name: column.id,
type: '', // When type is an empty string all cell actions are incompatible
aggregatable: false,
searchable: false,
}
)
: undefined,
[cellActionsTriggerId, columnHeaders]
[cellActionsTriggerId, columnHeaders, getFieldSpec]
);
const getCellValue = useCallback<UseDataGridColumnsCellActionsProps['getCellValue']>(

View file

@ -19,6 +19,7 @@
"@kbn/kibana-react-plugin",
"@kbn/kibana-utils-plugin",
"@kbn/i18n-react",
"@kbn/ui-actions-plugin"
"@kbn/ui-actions-plugin",
"@kbn/data-views-plugin"
]
}

View file

@ -79,6 +79,7 @@ import { useAlertBulkActions } from './use_alert_bulk_actions';
import type { BulkActionsProp } from '../toolbar/bulk_actions/types';
import { StatefulEventContext } from './stateful_event_context';
import { defaultUnit } from '../toolbar/unit';
import { useGetFieldSpec } from '../../hooks/use_get_field_spec';
const storage = new Storage(localStorage);
@ -184,6 +185,8 @@ const StatefulEventsViewerComponent: React.FC<EventsViewerProps & PropsFromRedux
loading: isLoadingIndexPattern,
} = useSourcererDataView(sourcererScope);
const getFieldSpec = useGetFieldSpec(sourcererScope);
const { globalFullScreen } = useGlobalFullScreen();
const editorActionsRef = useRef<FieldEditorActions>(null);
@ -602,6 +605,7 @@ const StatefulEventsViewerComponent: React.FC<EventsViewerProps & PropsFromRedux
isEventRenderedView={tableView === 'eventRenderedView'}
rowHeightsOptions={rowHeightsOptions}
getFieldBrowser={getFieldBrowser}
getFieldSpec={getFieldSpec}
/>
</StatefulEventContext.Provider>
</ScrollableFlexItem>

View file

@ -5,18 +5,17 @@
* 2.0.
*/
import type { BrowserField, TimelineNonEcsData } from '@kbn/timelines-plugin/common';
import type { TimelineNonEcsData } from '@kbn/timelines-plugin/common';
import type { AlertsTableConfigurationRegistry } from '@kbn/triggers-actions-ui-plugin/public/types';
import { useCallback, useMemo } from 'react';
import { TableId, tableDefaults, dataTableSelectors } from '@kbn/securitysolution-data-table';
import { getAllFieldsByName } from '../../../common/containers/source';
import type { UseDataGridColumnsSecurityCellActionsProps } from '../../../common/components/cell_actions';
import { useDataGridColumnsSecurityCellActions } from '../../../common/components/cell_actions';
import { SecurityCellActionsTrigger, SecurityCellActionType } from '../../../actions/constants';
import { VIEW_SELECTION } from '../../../../common/constants';
import { useSourcererDataView } from '../../../common/containers/sourcerer';
import { SourcererScopeName } from '../../../common/store/sourcerer/model';
import { useShallowEqualSelector } from '../../../common/hooks/use_selector';
import { useGetFieldSpec } from '../../../common/hooks/use_get_field_spec';
export const getUseCellActionsHook = (tableId: TableId) => {
const useCellActions: AlertsTableConfigurationRegistry['useCellActions'] = ({
@ -24,7 +23,7 @@ export const getUseCellActionsHook = (tableId: TableId) => {
data,
dataGridRef,
}) => {
const { browserFields } = useSourcererDataView(SourcererScopeName.detections);
const getFieldSpec = useGetFieldSpec(SourcererScopeName.detections);
/**
* There is difference between how `triggers actions` fetched data v/s
* how security solution fetches data via timelineSearchStrategy
@ -35,7 +34,6 @@ export const getUseCellActionsHook = (tableId: TableId) => {
*
*/
const browserFieldsByName = useMemo(() => getAllFieldsByName(browserFields), [browserFields]);
const finalData = useMemo(
() =>
(data as TimelineNonEcsData[][]).map((row) =>
@ -66,19 +64,16 @@ export const getUseCellActionsHook = (tableId: TableId) => {
if (viewMode === VIEW_SELECTION.eventRenderedView) {
return undefined;
}
return columns.map((column) => {
// TODO use FieldSpec object instead of browserField
const browserField: Partial<BrowserField> | undefined = browserFieldsByName[column.id];
return {
name: column.id,
type: browserField?.type ?? '', // When type is an empty string all cell actions are incompatible
esTypes: browserField?.esTypes ?? [],
aggregatable: browserField?.aggregatable ?? false,
searchable: browserField?.searchable ?? false,
subType: browserField?.subType,
};
});
}, [browserFieldsByName, columns, viewMode]);
return columns.map(
(column) =>
getFieldSpec(column.id) ?? {
name: '',
type: '', // When type is an empty string all cell actions are incompatible
aggregatable: false,
searchable: false,
}
);
}, [getFieldSpec, columns, viewMode]);
const getCellValue = useCallback<UseDataGridColumnsSecurityCellActionsProps['getCellValue']>(
(fieldName, rowIndex) => {