mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Security Solution][Alert details] - move table tab content to flyout folder (#189140)
This commit is contained in:
parent
bdc9a6c98e
commit
deb69fb948
25 changed files with 474 additions and 1481 deletions
|
@ -1,93 +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 { ReactWrapper } from 'enzyme';
|
||||
import React from 'react';
|
||||
import { getColumns } from './columns';
|
||||
import { TestProviders } from '../../mock';
|
||||
import { useMountAppended } from '../../utils/use_mount_appended';
|
||||
import { mockBrowserFields } from '../../containers/source/mock';
|
||||
import type { EventFieldsData } from './types';
|
||||
|
||||
jest.mock('../../lib/kibana');
|
||||
|
||||
jest.mock('@kbn/cell-actions/src/hooks/use_load_actions', () => {
|
||||
const actual = jest.requireActual('@kbn/cell-actions/src/hooks/use_load_actions');
|
||||
return {
|
||||
...actual,
|
||||
useLoadActions: jest.fn().mockImplementation(() => ({
|
||||
value: [],
|
||||
error: undefined,
|
||||
loading: false,
|
||||
})),
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('../../hooks/use_get_field_spec');
|
||||
|
||||
interface Column {
|
||||
field: string;
|
||||
name: string | JSX.Element;
|
||||
sortable: boolean;
|
||||
render: (field: string, data: EventFieldsData) => JSX.Element;
|
||||
}
|
||||
|
||||
describe('getColumns', () => {
|
||||
const mount = useMountAppended();
|
||||
const defaultProps = {
|
||||
browserFields: mockBrowserFields,
|
||||
columnHeaders: [],
|
||||
contextId: 'some-context',
|
||||
eventId: 'some-event',
|
||||
getLinkValue: jest.fn(),
|
||||
onUpdateColumns: jest.fn(),
|
||||
scopeId: 'some-timeline',
|
||||
toggleColumn: jest.fn(),
|
||||
};
|
||||
|
||||
test('should have expected fields', () => {
|
||||
const columns = getColumns(defaultProps);
|
||||
columns.forEach((column) => {
|
||||
expect(column).toHaveProperty('field');
|
||||
expect(column).toHaveProperty('name');
|
||||
expect(column).toHaveProperty('render');
|
||||
expect(column).toHaveProperty('sortable');
|
||||
});
|
||||
});
|
||||
|
||||
describe('column actions', () => {
|
||||
let actionsColumn: Column;
|
||||
const mockDataToUse = mockBrowserFields.agent.fields;
|
||||
const testValue = 'testValue';
|
||||
const testData = {
|
||||
type: 'someType',
|
||||
category: 'agent',
|
||||
field: 'agent.id',
|
||||
...mockDataToUse,
|
||||
} as EventFieldsData;
|
||||
|
||||
beforeEach(() => {
|
||||
actionsColumn = getColumns(defaultProps)[0] as Column;
|
||||
});
|
||||
|
||||
test('it renders inline actions', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>{actionsColumn.render(testValue, testData)}</TestProviders>
|
||||
) as ReactWrapper;
|
||||
|
||||
expect(wrapper.find('[data-test-subj="inlineActions"]').exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
test('it does not render inline actions when readOnly prop is passed', () => {
|
||||
actionsColumn = getColumns({ ...defaultProps, isReadOnly: true })[0] as Column;
|
||||
const wrapper = mount(
|
||||
<TestProviders>{actionsColumn.render(testValue, testData)}</TestProviders>
|
||||
) as ReactWrapper;
|
||||
|
||||
expect(wrapper.find('[data-test-subj="inlineActions"]').exists()).toBeFalsy();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,126 +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 { EuiPanel, EuiText } from '@elastic/eui';
|
||||
import memoizeOne from 'memoize-one';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { getCategory } from '@kbn/triggers-actions-ui-plugin/public';
|
||||
import { SecurityCellActions, CellActionsMode, SecurityCellActionsTrigger } from '../cell_actions';
|
||||
import type { BrowserFields } from '../../containers/source';
|
||||
import * as i18n from './translations';
|
||||
import type { EventFieldsData } from './types';
|
||||
import type { BrowserField } from '../../../../common/search_strategy';
|
||||
import { FieldValueCell } from './table/field_value_cell';
|
||||
import { FieldNameCell } from './table/field_name_cell';
|
||||
import { getSourcererScopeId } from '../../../helpers';
|
||||
import type { ColumnsProvider } from './event_fields_browser';
|
||||
|
||||
const HoverActionsContainer = styled(EuiPanel)`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: 25px;
|
||||
justify-content: center;
|
||||
left: 5px;
|
||||
position: absolute;
|
||||
top: -10px;
|
||||
width: 30px;
|
||||
`;
|
||||
|
||||
HoverActionsContainer.displayName = 'HoverActionsContainer';
|
||||
|
||||
export const getFieldFromBrowserField = memoizeOne(
|
||||
(field: string, browserFields: BrowserFields): BrowserField | undefined => {
|
||||
const category = getCategory(field);
|
||||
|
||||
return browserFields[category]?.fields?.[field] as BrowserField;
|
||||
},
|
||||
(newArgs, lastArgs) => newArgs[0] === lastArgs[0]
|
||||
);
|
||||
|
||||
export const getColumns: ColumnsProvider = ({
|
||||
browserFields,
|
||||
eventId,
|
||||
contextId,
|
||||
scopeId,
|
||||
getLinkValue,
|
||||
isDraggable,
|
||||
isReadOnly,
|
||||
}) => [
|
||||
...(!isReadOnly
|
||||
? ([
|
||||
{
|
||||
field: 'values',
|
||||
name: (
|
||||
<EuiText size="xs">
|
||||
<strong>{i18n.ACTIONS}</strong>
|
||||
</EuiText>
|
||||
),
|
||||
sortable: false,
|
||||
truncateText: false,
|
||||
width: '132px',
|
||||
render: (values, data) => {
|
||||
return (
|
||||
<SecurityCellActions
|
||||
data={{
|
||||
field: data.field,
|
||||
value: values,
|
||||
}}
|
||||
triggerId={SecurityCellActionsTrigger.DETAILS_FLYOUT}
|
||||
mode={CellActionsMode.INLINE}
|
||||
visibleCellActions={3}
|
||||
sourcererScopeId={getSourcererScopeId(scopeId)}
|
||||
metadata={{ scopeId, isObjectArray: data.isObjectArray }}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
] as ReturnType<ColumnsProvider>)
|
||||
: []),
|
||||
{
|
||||
field: 'field',
|
||||
className: 'eventFieldsTable__fieldNameCell',
|
||||
name: (
|
||||
<EuiText size="xs">
|
||||
<strong>{i18n.FIELD}</strong>
|
||||
</EuiText>
|
||||
),
|
||||
sortable: true,
|
||||
truncateText: false,
|
||||
render: (field, data) => {
|
||||
return (
|
||||
<FieldNameCell data={data as EventFieldsData} field={field} fieldMapping={undefined} />
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'values',
|
||||
className: 'eventFieldsTable__fieldValueCell',
|
||||
name: (
|
||||
<EuiText size="xs">
|
||||
<strong>{i18n.VALUE}</strong>
|
||||
</EuiText>
|
||||
),
|
||||
sortable: true,
|
||||
truncateText: false,
|
||||
render: (values, data) => {
|
||||
const fieldFromBrowserField = getFieldFromBrowserField(data.field, browserFields);
|
||||
return (
|
||||
<FieldValueCell
|
||||
contextId={contextId}
|
||||
data={data as EventFieldsData}
|
||||
eventId={eventId}
|
||||
fieldFromBrowserField={fieldFromBrowserField}
|
||||
getLinkValue={getLinkValue}
|
||||
isDraggable={isDraggable}
|
||||
values={values}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
|
@ -1,210 +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 from 'react';
|
||||
|
||||
import { mockDetailItemData, mockDetailItemDataId } from '../../mock/mock_detail_item';
|
||||
import { TestProviders } from '../../mock/test_providers';
|
||||
import { EventFieldsBrowser } from './event_fields_browser';
|
||||
import { mockBrowserFields } from '../../containers/source/mock';
|
||||
import { useMountAppended } from '../../utils/use_mount_appended';
|
||||
import { TimelineTabs } from '../../../../common/types/timeline';
|
||||
|
||||
jest.mock('../../lib/kibana');
|
||||
|
||||
jest.mock('../../hooks/use_get_field_spec');
|
||||
|
||||
jest.mock('@kbn/cell-actions/src/hooks/use_load_actions', () => {
|
||||
const actual = jest.requireActual('@kbn/cell-actions/src/hooks/use_load_actions');
|
||||
return {
|
||||
...actual,
|
||||
useLoadActions: jest.fn().mockImplementation(() => ({
|
||||
value: [],
|
||||
error: undefined,
|
||||
loading: false,
|
||||
})),
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('../link_to');
|
||||
|
||||
const mockDispatch = jest.fn();
|
||||
jest.mock('react-redux', () => {
|
||||
const original = jest.requireActual('react-redux');
|
||||
|
||||
return {
|
||||
...original,
|
||||
useDispatch: () => mockDispatch,
|
||||
};
|
||||
});
|
||||
|
||||
describe('EventFieldsBrowser', () => {
|
||||
const mount = useMountAppended();
|
||||
|
||||
describe('column headers', () => {
|
||||
['Actions', 'Field', 'Value'].forEach((header) => {
|
||||
test(`it renders the ${header} column header`, () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<EventFieldsBrowser
|
||||
browserFields={mockBrowserFields}
|
||||
data={mockDetailItemData}
|
||||
eventId={mockDetailItemDataId}
|
||||
scopeId="timeline-test"
|
||||
timelineTabType={TimelineTabs.query}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(wrapper.find('thead').contains(header)).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('filter input', () => {
|
||||
test('it renders a filter input with the expected placeholder', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<EventFieldsBrowser
|
||||
browserFields={mockBrowserFields}
|
||||
data={mockDetailItemData}
|
||||
eventId={mockDetailItemDataId}
|
||||
scopeId="timeline-test"
|
||||
timelineTabType={TimelineTabs.query}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(wrapper.find('input[type="search"]').props().placeholder).toEqual(
|
||||
'Filter by Field, Value, or Description...'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Hover Actions', () => {
|
||||
const eventId = 'pEMaMmkBUV60JmNWmWVi';
|
||||
|
||||
test('it renders inline actions', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<EventFieldsBrowser
|
||||
browserFields={mockBrowserFields}
|
||||
data={mockDetailItemData}
|
||||
eventId={eventId}
|
||||
scopeId="timeline-test"
|
||||
timelineTabType={TimelineTabs.query}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
expect(wrapper.find('[data-test-subj="inlineActions"]').exists()).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('field type icon', () => {
|
||||
test('it renders the expected icon type for the data provided', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<EventFieldsBrowser
|
||||
browserFields={mockBrowserFields}
|
||||
data={mockDetailItemData}
|
||||
eventId={mockDetailItemDataId}
|
||||
scopeId="timeline-test"
|
||||
timelineTabType={TimelineTabs.query}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(
|
||||
wrapper
|
||||
.find('tr.euiTableRow')
|
||||
.find('td.euiTableRowCell')
|
||||
.at(1)
|
||||
.find('[data-euiicon-type]')
|
||||
.exists()
|
||||
).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('field', () => {
|
||||
test('it renders the field name for the data provided', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<EventFieldsBrowser
|
||||
browserFields={mockBrowserFields}
|
||||
data={mockDetailItemData}
|
||||
eventId={mockDetailItemDataId}
|
||||
scopeId="timeline-test"
|
||||
timelineTabType={TimelineTabs.query}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
expect(wrapper.find('[data-test-subj="field-name"]').at(0).text()).toEqual('@timestamp');
|
||||
});
|
||||
|
||||
test('it renders the expected icon for description', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<EventFieldsBrowser
|
||||
browserFields={mockBrowserFields}
|
||||
data={mockDetailItemData}
|
||||
eventId={mockDetailItemDataId}
|
||||
scopeId="timeline-test"
|
||||
timelineTabType={TimelineTabs.query}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
expect(
|
||||
wrapper
|
||||
.find('tr.euiTableRow')
|
||||
.find('td.euiTableRowCell')
|
||||
.at(1)
|
||||
.find('[data-euiicon-type]')
|
||||
.last()
|
||||
.prop('data-euiicon-type')
|
||||
).toEqual('tokenDate');
|
||||
});
|
||||
});
|
||||
|
||||
describe('value', () => {
|
||||
test('it renders the expected value for the data provided', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<EventFieldsBrowser
|
||||
browserFields={mockBrowserFields}
|
||||
data={mockDetailItemData}
|
||||
eventId={mockDetailItemDataId}
|
||||
scopeId="timeline-test"
|
||||
timelineTabType={TimelineTabs.query}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
expect(wrapper.find('[data-test-subj="localized-date-tool-tip"]').at(0).text()).toEqual(
|
||||
'Feb 28, 2019 @ 16:50:54.621'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('description', () => {
|
||||
test('it renders the expected field description the data provided', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<EventFieldsBrowser
|
||||
browserFields={mockBrowserFields}
|
||||
data={mockDetailItemData}
|
||||
eventId={mockDetailItemDataId}
|
||||
scopeId="timeline-test"
|
||||
timelineTabType={TimelineTabs.query}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="field-name-cell"]').at(0).find('EuiToolTip').prop('content')
|
||||
).toContain('Date/time when the event originated.');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,308 +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 { getOr, noop, sortBy } from 'lodash/fp';
|
||||
import type { EuiBasicTableColumn } from '@elastic/eui';
|
||||
import { EuiInMemoryTable } from '@elastic/eui';
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { rgba } from 'polished';
|
||||
import styled from 'styled-components';
|
||||
import {
|
||||
arrayIndexToAriaIndex,
|
||||
DATA_COLINDEX_ATTRIBUTE,
|
||||
DATA_ROWINDEX_ATTRIBUTE,
|
||||
isTab,
|
||||
onKeyDownFocusHandler,
|
||||
} from '@kbn/timelines-plugin/public';
|
||||
import { dataTableSelectors, tableDefaults } from '@kbn/securitysolution-data-table';
|
||||
import { isInTableScope, isTimelineScope } from '../../../helpers';
|
||||
import { timelineSelectors } from '../../../timelines/store';
|
||||
import type { BrowserFields } from '../../containers/source';
|
||||
import { getAllFieldsByName } from '../../containers/source';
|
||||
import type { TimelineEventsDetailsItem } from '../../../../common/search_strategy/timeline';
|
||||
import { getColumnHeaders } from '../../../timelines/components/timeline/body/column_headers/helpers';
|
||||
import { timelineDefaults } from '../../../timelines/store/defaults';
|
||||
import { getColumns } from './columns';
|
||||
import { EVENT_FIELDS_TABLE_CLASS_NAME, onEventDetailsTabKeyPressed, search } from './helpers';
|
||||
import { useDeepEqualSelector } from '../../hooks/use_selector';
|
||||
import type { TimelineTabs } from '../../../../common/types/timeline';
|
||||
|
||||
export type ColumnsProvider = (providerOptions: {
|
||||
browserFields: BrowserFields;
|
||||
eventId: string;
|
||||
contextId: string;
|
||||
scopeId: string;
|
||||
getLinkValue: (field: string) => string | null;
|
||||
isDraggable?: boolean;
|
||||
isReadOnly?: boolean;
|
||||
}) => Array<EuiBasicTableColumn<TimelineEventsDetailsItem>>;
|
||||
|
||||
interface Props {
|
||||
browserFields: BrowserFields;
|
||||
data: TimelineEventsDetailsItem[];
|
||||
eventId: string;
|
||||
isDraggable?: boolean;
|
||||
scopeId: string;
|
||||
timelineTabType: TimelineTabs | 'flyout';
|
||||
isReadOnly?: boolean;
|
||||
columnsProvider?: ColumnsProvider;
|
||||
}
|
||||
|
||||
const TableWrapper = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
> div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
> .euiFlexGroup:first-of-type {
|
||||
flex: 0;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const StyledEuiInMemoryTable = styled(EuiInMemoryTable as any)`
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
overflow-x: hidden;
|
||||
&::-webkit-scrollbar {
|
||||
height: ${({ theme }) => theme.eui.euiScrollBar};
|
||||
width: ${({ theme }) => theme.eui.euiScrollBar};
|
||||
}
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-clip: content-box;
|
||||
background-color: ${({ theme }) => rgba(theme.eui.euiColorDarkShade, 0.5)};
|
||||
border: ${({ theme }) => theme.eui.euiScrollBarCorner} solid transparent;
|
||||
}
|
||||
&::-webkit-scrollbar-corner,
|
||||
&::-webkit-scrollbar-track {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.eventFieldsTable__fieldIcon {
|
||||
padding-top: ${({ theme }) => parseFloat(theme.eui.euiSizeXS) * 1.5}px;
|
||||
}
|
||||
|
||||
.eventFieldsTable__fieldName {
|
||||
line-height: ${({ theme }) => theme.eui.euiLineHeight};
|
||||
padding: ${({ theme }) => theme.eui.euiSizeXS};
|
||||
}
|
||||
|
||||
// TODO: Use this logic from discover
|
||||
/* .eventFieldsTable__multiFieldBadge {
|
||||
font: ${({ theme }) => theme.eui.euiFont};
|
||||
} */
|
||||
|
||||
.inlineActions {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.eventFieldsTable__tableRow {
|
||||
font-size: ${({ theme }) => theme.eui.euiFontSizeXS};
|
||||
font-family: ${({ theme }) => theme.eui.euiCodeFontFamily};
|
||||
|
||||
.inlineActions-popoverOpen {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.inlineActions {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.eventFieldsTable__actionCell,
|
||||
.eventFieldsTable__fieldNameCell {
|
||||
align-items: flex-start;
|
||||
padding: ${({ theme }) => theme.eui.euiSizeXS};
|
||||
}
|
||||
|
||||
.eventFieldsTable__fieldValue {
|
||||
display: inline-block;
|
||||
word-break: break-all;
|
||||
word-wrap: break-word;
|
||||
white-space: pre-wrap;
|
||||
line-height: ${({ theme }) => theme.eui.euiLineHeight};
|
||||
color: ${({ theme }) => theme.eui.euiColorFullShade};
|
||||
vertical-align: top;
|
||||
}
|
||||
`;
|
||||
|
||||
// Match structure in discover
|
||||
const COUNT_PER_PAGE_OPTIONS = [25, 50, 100];
|
||||
|
||||
// Encapsulating the pagination logic for the table.
|
||||
const useFieldBrowserPagination = () => {
|
||||
const [pagination, setPagination] = useState<{ pageIndex: number }>({
|
||||
pageIndex: 0,
|
||||
});
|
||||
|
||||
const onTableChange = useCallback(({ page: { index } }: { page: { index: number } }) => {
|
||||
setPagination({ pageIndex: index });
|
||||
}, []);
|
||||
const paginationTableProp = useMemo(
|
||||
() => ({
|
||||
...pagination,
|
||||
pageSizeOptions: COUNT_PER_PAGE_OPTIONS,
|
||||
}),
|
||||
[pagination]
|
||||
);
|
||||
|
||||
return {
|
||||
onTableChange,
|
||||
paginationTableProp,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* This callback, invoked via `EuiInMemoryTable`'s `rowProps, assigns
|
||||
* attributes to every `<tr>`.
|
||||
*/
|
||||
/** Renders a table view or JSON view of the `ECS` `data` */
|
||||
// TODO: MOVE TO FLYOUT FOLDER - https://github.com/elastic/security-team/issues/7462
|
||||
export const EventFieldsBrowser = React.memo<Props>(
|
||||
({
|
||||
browserFields,
|
||||
data,
|
||||
eventId,
|
||||
isDraggable,
|
||||
timelineTabType,
|
||||
scopeId,
|
||||
isReadOnly,
|
||||
columnsProvider = getColumns,
|
||||
}) => {
|
||||
const containerElement = useRef<HTMLDivElement | null>(null);
|
||||
const getScope = useMemo(() => {
|
||||
if (isTimelineScope(scopeId)) {
|
||||
return timelineSelectors.getTimelineByIdSelector();
|
||||
} else if (isInTableScope(scopeId)) {
|
||||
return dataTableSelectors.getTableByIdSelector();
|
||||
}
|
||||
}, [scopeId]);
|
||||
const defaults = isTimelineScope(scopeId) ? timelineDefaults : tableDefaults;
|
||||
const columnHeaders = useDeepEqualSelector((state) => {
|
||||
const { columns } = (getScope && getScope(state, scopeId)) ?? defaults;
|
||||
return getColumnHeaders(columns, browserFields);
|
||||
});
|
||||
|
||||
const fieldsByName = useMemo(() => getAllFieldsByName(browserFields), [browserFields]);
|
||||
const items = useMemo(
|
||||
() =>
|
||||
sortBy(['field'], data).map((item, i) => ({
|
||||
...item,
|
||||
...fieldsByName[item.field],
|
||||
valuesConcatenated: item.values != null ? item.values.join() : '',
|
||||
ariaRowindex: arrayIndexToAriaIndex(i),
|
||||
})),
|
||||
[data, fieldsByName]
|
||||
);
|
||||
|
||||
const getLinkValue = useCallback(
|
||||
(field: string) => {
|
||||
const linkField = (columnHeaders.find((col) => col.id === field) ?? {}).linkField;
|
||||
if (!linkField) {
|
||||
return null;
|
||||
}
|
||||
const linkFieldData = (data ?? []).find((d) => d.field === linkField);
|
||||
const linkFieldValue = getOr(null, 'originalValue', linkFieldData);
|
||||
return Array.isArray(linkFieldValue) ? linkFieldValue[0] : linkFieldValue;
|
||||
},
|
||||
[data, columnHeaders]
|
||||
);
|
||||
|
||||
const onSetRowProps = useCallback(({ ariaRowindex, field }: TimelineEventsDetailsItem) => {
|
||||
const rowIndex = ariaRowindex != null ? { 'data-rowindex': ariaRowindex } : {};
|
||||
return {
|
||||
...rowIndex,
|
||||
className: 'eventFieldsTable__tableRow',
|
||||
'data-test-subj': `event-fields-table-row-${field}`,
|
||||
};
|
||||
}, []);
|
||||
|
||||
const columns = useMemo(
|
||||
() =>
|
||||
columnsProvider({
|
||||
browserFields,
|
||||
eventId,
|
||||
contextId: `event-fields-browser-for-${scopeId}-${timelineTabType}`,
|
||||
scopeId,
|
||||
getLinkValue,
|
||||
isDraggable,
|
||||
isReadOnly,
|
||||
}),
|
||||
[
|
||||
browserFields,
|
||||
eventId,
|
||||
scopeId,
|
||||
columnsProvider,
|
||||
timelineTabType,
|
||||
getLinkValue,
|
||||
isDraggable,
|
||||
isReadOnly,
|
||||
]
|
||||
);
|
||||
|
||||
const focusSearchInput = useCallback(() => {
|
||||
// the selector below is used to focus the input because EuiInMemoryTable does not expose a ref to its built-in search input
|
||||
containerElement.current?.querySelector<HTMLInputElement>('input[type="search"]')?.focus();
|
||||
}, []);
|
||||
|
||||
const onKeyDown = useCallback(
|
||||
(keyboardEvent: React.KeyboardEvent) => {
|
||||
if (isTab(keyboardEvent)) {
|
||||
onEventDetailsTabKeyPressed({
|
||||
containerElement: containerElement.current,
|
||||
keyboardEvent,
|
||||
onSkipFocusBeforeEventsTable: focusSearchInput,
|
||||
onSkipFocusAfterEventsTable: noop,
|
||||
});
|
||||
} else {
|
||||
onKeyDownFocusHandler({
|
||||
colindexAttribute: DATA_COLINDEX_ATTRIBUTE,
|
||||
containerElement: containerElement?.current,
|
||||
event: keyboardEvent,
|
||||
maxAriaColindex: 3,
|
||||
maxAriaRowindex: data.length,
|
||||
onColumnFocused: noop,
|
||||
rowindexAttribute: DATA_ROWINDEX_ATTRIBUTE,
|
||||
});
|
||||
}
|
||||
},
|
||||
[data, focusSearchInput]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
focusSearchInput();
|
||||
}, [focusSearchInput]);
|
||||
|
||||
// Pagination
|
||||
const { onTableChange, paginationTableProp } = useFieldBrowserPagination();
|
||||
|
||||
return (
|
||||
<TableWrapper onKeyDown={onKeyDown} ref={containerElement}>
|
||||
<StyledEuiInMemoryTable
|
||||
className={EVENT_FIELDS_TABLE_CLASS_NAME}
|
||||
items={items}
|
||||
itemId="field"
|
||||
columns={columns}
|
||||
onTableChange={onTableChange}
|
||||
pagination={paginationTableProp}
|
||||
rowProps={onSetRowProps}
|
||||
search={search}
|
||||
sorting={false}
|
||||
data-test-subj="event-fields-browser"
|
||||
/>
|
||||
</TableWrapper>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
EventFieldsBrowser.displayName = 'EventFieldsBrowser';
|
|
@ -18,25 +18,12 @@ import type { BrowserFields } from '../../containers/source';
|
|||
import type { TimelineEventsDetailsItem } from '../../../../common/search_strategy/timeline';
|
||||
import type { EnrichedFieldInfo, EventSummaryField } from './types';
|
||||
|
||||
import * as i18n from './translations';
|
||||
import {
|
||||
AGENT_STATUS_FIELD_NAME,
|
||||
QUARANTINED_PATH_FIELD_NAME,
|
||||
} from '../../../timelines/components/timeline/body/renderers/constants';
|
||||
import { RESPONSE_ACTIONS_ALERT_AGENT_ID_FIELD } from '../../../../common/endpoint/service/response_actions/constants';
|
||||
|
||||
/**
|
||||
* Defines the behavior of the search input that appears above the table of data
|
||||
*/
|
||||
export const search = {
|
||||
box: {
|
||||
incremental: true,
|
||||
placeholder: i18n.PLACEHOLDER,
|
||||
schema: true,
|
||||
'data-test-subj': 'search-input',
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* An item rendered in the table
|
||||
*/
|
||||
|
|
|
@ -1,93 +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 from 'react';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiBadge, EuiText, EuiToolTip } from '@elastic/eui';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { FieldIcon } from '@kbn/react-field';
|
||||
import type { DataViewField } from '@kbn/data-views-plugin/common';
|
||||
import { EcsFlat } from '@elastic/ecs';
|
||||
import * as i18n from '../translations';
|
||||
import { getExampleText } from '../helpers';
|
||||
import type { EventFieldsData } from '../types';
|
||||
import { getFieldTypeName } from './get_field_type_name';
|
||||
|
||||
const getEcsField = (field: string): { example?: string; description?: string } | undefined => {
|
||||
return EcsFlat[field as keyof typeof EcsFlat] as
|
||||
| {
|
||||
example?: string;
|
||||
description?: string;
|
||||
}
|
||||
| undefined;
|
||||
};
|
||||
|
||||
export interface FieldNameCellProps {
|
||||
data: EventFieldsData;
|
||||
field: string;
|
||||
fieldMapping?: DataViewField;
|
||||
scripted?: boolean;
|
||||
}
|
||||
export const FieldNameCell = React.memo(
|
||||
({ data, field, fieldMapping, scripted }: FieldNameCellProps) => {
|
||||
const ecsField = getEcsField(field);
|
||||
const typeName = getFieldTypeName(data.type);
|
||||
// TODO: We don't have fieldMapping or isMultiField until kibana indexPatterns is implemented. Will default to field for now
|
||||
const displayName = fieldMapping && fieldMapping.displayName ? fieldMapping.displayName : field;
|
||||
const defaultTooltip = displayName !== field ? `${field} (${displayName})` : field;
|
||||
const isMultiField = fieldMapping?.isSubtypeMulti();
|
||||
return (
|
||||
<>
|
||||
<EuiFlexItem grow={false} className="eventFieldsTable__fieldIcon">
|
||||
<FieldIcon
|
||||
data-test-subj="field-type-icon"
|
||||
type={data.type}
|
||||
label={typeName}
|
||||
scripted={scripted} // TODO: Will get with kibana indexPatterns;
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexGroup
|
||||
wrap={true}
|
||||
gutterSize="none"
|
||||
responsive={false}
|
||||
alignItems="flexStart"
|
||||
data-test-subj="field-name-cell"
|
||||
>
|
||||
<EuiFlexItem className="eventFieldsTable__fieldName eui-textBreakAll" grow={false}>
|
||||
<EuiToolTip
|
||||
position="top"
|
||||
content={
|
||||
!isEmpty(ecsField?.description)
|
||||
? `${ecsField?.description} ${getExampleText(ecsField?.example)}`
|
||||
: defaultTooltip
|
||||
}
|
||||
delay="long"
|
||||
anchorClassName="eui-textBreakAll"
|
||||
>
|
||||
<EuiText size="xs" data-test-subj="field-name">
|
||||
{field}
|
||||
</EuiText>
|
||||
</EuiToolTip>
|
||||
</EuiFlexItem>
|
||||
{isMultiField && (
|
||||
<EuiToolTip position="top" delay="long" content={i18n.MULTI_FIELD_TOOLTIP}>
|
||||
<EuiBadge
|
||||
title=""
|
||||
className="eventFieldsTable__multiFieldBadge"
|
||||
color="default"
|
||||
data-test-subj={`eventFieldsTableRow-${field}-multifieldBadge`}
|
||||
>
|
||||
{i18n.MULTI_FIELD_BADGE}
|
||||
</EuiBadge>
|
||||
</EuiToolTip>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
FieldNameCell.displayName = 'FieldNameCell';
|
|
@ -1,93 +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 { CSSProperties } from 'react';
|
||||
import React from 'react';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui';
|
||||
import type { BrowserField } from '../../../containers/source';
|
||||
import { OverflowField } from '../../tables/helpers';
|
||||
import { FormattedFieldValue } from '../../../../timelines/components/timeline/body/renderers/formatted_field';
|
||||
import { MESSAGE_FIELD_NAME } from '../../../../timelines/components/timeline/body/renderers/constants';
|
||||
import type { EventFieldsData, FieldsData } from '../types';
|
||||
import { getFieldFormat } from '../get_field_format';
|
||||
|
||||
export interface FieldValueCellProps {
|
||||
contextId: string;
|
||||
data: EventFieldsData | FieldsData;
|
||||
eventId: string;
|
||||
fieldFromBrowserField?: Partial<BrowserField>;
|
||||
getLinkValue?: (field: string) => string | null;
|
||||
isDraggable?: boolean;
|
||||
linkValue?: string | null | undefined;
|
||||
style?: CSSProperties | undefined;
|
||||
values: string[] | null | undefined;
|
||||
}
|
||||
|
||||
export const FieldValueCell = React.memo(
|
||||
({
|
||||
contextId,
|
||||
data,
|
||||
eventId,
|
||||
fieldFromBrowserField,
|
||||
getLinkValue,
|
||||
isDraggable = false,
|
||||
linkValue,
|
||||
style,
|
||||
values,
|
||||
}: FieldValueCellProps) => {
|
||||
return (
|
||||
<EuiFlexGroup
|
||||
alignItems="flexStart"
|
||||
data-test-subj={`event-field-${data.field}`}
|
||||
direction="column"
|
||||
gutterSize="none"
|
||||
style={style}
|
||||
>
|
||||
{values != null &&
|
||||
values.map((value, i) => {
|
||||
if (fieldFromBrowserField == null) {
|
||||
return (
|
||||
<EuiFlexItem grow={false} key={`${i}-${value}`}>
|
||||
<EuiText size="xs" key={`${i}-${value}`}>
|
||||
{value}
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<EuiFlexItem
|
||||
className="eventFieldsTable__fieldValue"
|
||||
grow={false}
|
||||
key={`${i}-${value}`}
|
||||
>
|
||||
{data.field === MESSAGE_FIELD_NAME ? (
|
||||
<OverflowField value={value} />
|
||||
) : (
|
||||
<FormattedFieldValue
|
||||
contextId={`${contextId}-${eventId}-${data.field}-${i}-${value}`}
|
||||
eventId={eventId}
|
||||
fieldFormat={getFieldFormat(data)}
|
||||
fieldName={data.field}
|
||||
fieldFromBrowserField={fieldFromBrowserField}
|
||||
fieldType={data.type}
|
||||
isAggregatable={fieldFromBrowserField.aggregatable}
|
||||
isDraggable={isDraggable}
|
||||
isObjectArray={data.isObjectArray}
|
||||
value={value}
|
||||
linkValue={(getLinkValue && getLinkValue(data.field)) ?? linkValue}
|
||||
truncate={false}
|
||||
/>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
);
|
||||
})}
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
FieldValueCell.displayName = 'FieldValueCell';
|
|
@ -1,62 +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 { i18n } from '@kbn/i18n';
|
||||
|
||||
export function getFieldTypeName(type: string) {
|
||||
switch (type) {
|
||||
case 'boolean':
|
||||
return i18n.translate('xpack.securitySolution.fieldNameIcons.booleanAriaLabel', {
|
||||
defaultMessage: 'Boolean field',
|
||||
});
|
||||
case 'conflict':
|
||||
return i18n.translate('xpack.securitySolution.fieldNameIcons.conflictFieldAriaLabel', {
|
||||
defaultMessage: 'Conflicting field',
|
||||
});
|
||||
case 'date':
|
||||
return i18n.translate('xpack.securitySolution.fieldNameIcons.dateFieldAriaLabel', {
|
||||
defaultMessage: 'Date field',
|
||||
});
|
||||
case 'geo_point':
|
||||
return i18n.translate('xpack.securitySolution.fieldNameIcons.geoPointFieldAriaLabel', {
|
||||
defaultMessage: 'Geo point field',
|
||||
});
|
||||
case 'geo_shape':
|
||||
return i18n.translate('xpack.securitySolution.fieldNameIcons.geoShapeFieldAriaLabel', {
|
||||
defaultMessage: 'Geo shape field',
|
||||
});
|
||||
case 'ip':
|
||||
return i18n.translate('xpack.securitySolution.fieldNameIcons.ipAddressFieldAriaLabel', {
|
||||
defaultMessage: 'IP address field',
|
||||
});
|
||||
case 'murmur3':
|
||||
return i18n.translate('xpack.securitySolution.fieldNameIcons.murmur3FieldAriaLabel', {
|
||||
defaultMessage: 'Murmur3 field',
|
||||
});
|
||||
case 'number':
|
||||
return i18n.translate('xpack.securitySolution.fieldNameIcons.numberFieldAriaLabel', {
|
||||
defaultMessage: 'Number field',
|
||||
});
|
||||
case 'source':
|
||||
// Note that this type is currently not provided, type for _source is undefined
|
||||
return i18n.translate('xpack.securitySolution.fieldNameIcons.sourceFieldAriaLabel', {
|
||||
defaultMessage: 'Source field',
|
||||
});
|
||||
case 'string':
|
||||
return i18n.translate('xpack.securitySolution.fieldNameIcons.stringFieldAriaLabel', {
|
||||
defaultMessage: 'String field',
|
||||
});
|
||||
case 'nested':
|
||||
return i18n.translate('xpack.securitySolution.fieldNameIcons.nestedFieldAriaLabel', {
|
||||
defaultMessage: 'Nested field',
|
||||
});
|
||||
default:
|
||||
return i18n.translate('xpack.securitySolution.fieldNameIcons.unknownFieldAriaLabel', {
|
||||
defaultMessage: 'Unknown field',
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,104 +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 { render, screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
|
||||
import type { BrowserField } from '../../../containers/source';
|
||||
import { PrevalenceCellRenderer } from './prevalence_cell';
|
||||
import { TestProviders } from '../../../mock';
|
||||
import type { EventFieldsData } from '../types';
|
||||
import { TimelineId } from '../../../../../common/types';
|
||||
import type { AlertSummaryRow } from '../helpers';
|
||||
import { useAlertPrevalence } from '../../../containers/alerts/use_alert_prevalence';
|
||||
import { getEmptyValue } from '../../empty_value';
|
||||
|
||||
jest.mock('../../../lib/kibana');
|
||||
jest.mock('../../../containers/alerts/use_alert_prevalence', () => ({
|
||||
useAlertPrevalence: jest.fn(),
|
||||
}));
|
||||
const mockUseAlertPrevalence = useAlertPrevalence as jest.Mock;
|
||||
|
||||
const eventId = 'TUWyf3wBFCFU0qRJTauW';
|
||||
const hostIpValues = ['127.0.0.1', '::1', '10.1.2.3', '2001:0DB8:AC10:FE01::'];
|
||||
const hostIpFieldFromBrowserField: BrowserField = {
|
||||
aggregatable: true,
|
||||
name: 'host.ip',
|
||||
readFromDocValues: false,
|
||||
searchable: true,
|
||||
type: 'ip',
|
||||
};
|
||||
const hostIpData: EventFieldsData = {
|
||||
...hostIpFieldFromBrowserField,
|
||||
ariaRowindex: 35,
|
||||
field: 'host.ip',
|
||||
isObjectArray: false,
|
||||
originalValue: [...hostIpValues],
|
||||
values: [...hostIpValues],
|
||||
};
|
||||
|
||||
const enrichedHostIpData: AlertSummaryRow['description'] = {
|
||||
data: { ...hostIpData },
|
||||
eventId,
|
||||
fieldFromBrowserField: { ...hostIpFieldFromBrowserField },
|
||||
isDraggable: false,
|
||||
scopeId: TimelineId.test,
|
||||
values: [...hostIpValues],
|
||||
};
|
||||
|
||||
describe('PrevalenceCellRenderer', () => {
|
||||
describe('When data is loading', () => {
|
||||
test('it should show the loading spinner', async () => {
|
||||
mockUseAlertPrevalence.mockImplementation(() => ({
|
||||
loading: true,
|
||||
count: 123,
|
||||
error: true,
|
||||
}));
|
||||
const { container } = render(
|
||||
<TestProviders>
|
||||
<PrevalenceCellRenderer {...enrichedHostIpData} />
|
||||
</TestProviders>
|
||||
);
|
||||
expect(container.getElementsByClassName('euiLoadingSpinner')).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('When an error was returned', () => {
|
||||
test('it should return empty value placeholder', async () => {
|
||||
mockUseAlertPrevalence.mockImplementation(() => ({
|
||||
loading: false,
|
||||
count: undefined,
|
||||
error: true,
|
||||
}));
|
||||
const { container } = render(
|
||||
<TestProviders>
|
||||
<PrevalenceCellRenderer {...enrichedHostIpData} />
|
||||
</TestProviders>
|
||||
);
|
||||
expect(container.getElementsByClassName('euiLoadingSpinner')).toHaveLength(0);
|
||||
expect(screen.queryByText('123')).toBeNull();
|
||||
expect(screen.queryByText(getEmptyValue())).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('When an actual count is returned', () => {
|
||||
test('it should show the count', async () => {
|
||||
mockUseAlertPrevalence.mockImplementation(() => ({
|
||||
loading: false,
|
||||
count: 123,
|
||||
error: false,
|
||||
}));
|
||||
const { container } = render(
|
||||
<TestProviders>
|
||||
<PrevalenceCellRenderer {...enrichedHostIpData} />
|
||||
</TestProviders>
|
||||
);
|
||||
expect(container.getElementsByClassName('euiLoadingSpinner')).toHaveLength(0);
|
||||
expect(screen.queryByText('123')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,74 +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 from 'react';
|
||||
import { EuiLoadingSpinner } from '@elastic/eui';
|
||||
|
||||
import { TimelineId } from '../../../../../common/types';
|
||||
import type { AlertSummaryRow } from '../helpers';
|
||||
import { getEmptyTagValue } from '../../empty_value';
|
||||
import { InvestigateInTimelineButton } from './investigate_in_timeline_button';
|
||||
import { useActionCellDataProvider } from './use_action_cell_data_provider';
|
||||
import { useAlertPrevalence } from '../../../containers/alerts/use_alert_prevalence';
|
||||
import { getFieldFormat } from '../get_field_format';
|
||||
|
||||
/**
|
||||
* Renders a Prevalence cell based on a regular alert prevalence query
|
||||
*/
|
||||
const PrevalenceCell: React.FC<AlertSummaryRow['description']> = ({
|
||||
data,
|
||||
eventId,
|
||||
fieldFromBrowserField,
|
||||
linkValue,
|
||||
scopeId,
|
||||
values,
|
||||
}) => {
|
||||
const { loading, count } = useAlertPrevalence({
|
||||
field: data.field,
|
||||
isActiveTimelines: scopeId === TimelineId.active,
|
||||
value: values,
|
||||
signalIndexName: null,
|
||||
});
|
||||
|
||||
const cellDataProviders = useActionCellDataProvider({
|
||||
contextId: scopeId,
|
||||
eventId,
|
||||
field: data.field,
|
||||
fieldFormat: getFieldFormat(data),
|
||||
fieldFromBrowserField,
|
||||
fieldType: data.type,
|
||||
isObjectArray: data.isObjectArray,
|
||||
linkValue,
|
||||
values,
|
||||
});
|
||||
|
||||
if (loading) {
|
||||
return <EuiLoadingSpinner />;
|
||||
} else if (
|
||||
typeof count === 'number' &&
|
||||
cellDataProviders?.dataProviders &&
|
||||
cellDataProviders?.dataProviders.length
|
||||
) {
|
||||
return (
|
||||
<InvestigateInTimelineButton
|
||||
asEmptyButton={true}
|
||||
dataProviders={cellDataProviders.dataProviders}
|
||||
filters={cellDataProviders.filters}
|
||||
>
|
||||
<span data-test-subj="alert-prevalence">{count}</span>
|
||||
</InvestigateInTimelineButton>
|
||||
);
|
||||
} else {
|
||||
return getEmptyTagValue();
|
||||
}
|
||||
};
|
||||
|
||||
PrevalenceCell.displayName = 'PrevalenceCell';
|
||||
|
||||
export const PrevalenceCellRenderer = (data: AlertSummaryRow['description']) => (
|
||||
<PrevalenceCell {...data} />
|
||||
);
|
|
@ -1,107 +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 { act, render, screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
|
||||
import type { BrowserField } from '../../../containers/source';
|
||||
import { SummaryValueCell } from './summary_value_cell';
|
||||
import { TestProviders } from '../../../mock';
|
||||
import type { EventFieldsData } from '../types';
|
||||
import type { AlertSummaryRow } from '../helpers';
|
||||
import { TimelineId } from '../../../../../common/types';
|
||||
import { AGENT_STATUS_FIELD_NAME } from '../../../../timelines/components/timeline/body/renderers/constants';
|
||||
|
||||
jest.mock('../../../lib/kibana');
|
||||
|
||||
jest.mock('../../../hooks/use_get_field_spec');
|
||||
|
||||
const eventId = 'TUWyf3wBFCFU0qRJTauW';
|
||||
const hostIpValues = ['127.0.0.1', '::1', '10.1.2.3', '2001:0DB8:AC10:FE01::'];
|
||||
const hostIpFieldFromBrowserField: BrowserField = {
|
||||
aggregatable: true,
|
||||
name: 'host.ip',
|
||||
readFromDocValues: false,
|
||||
searchable: true,
|
||||
type: 'ip',
|
||||
};
|
||||
const hostIpData: EventFieldsData = {
|
||||
...hostIpFieldFromBrowserField,
|
||||
ariaRowindex: 35,
|
||||
field: 'host.ip',
|
||||
isObjectArray: false,
|
||||
originalValue: [...hostIpValues],
|
||||
values: [...hostIpValues],
|
||||
};
|
||||
|
||||
const enrichedHostIpData: AlertSummaryRow['description'] = {
|
||||
data: { ...hostIpData },
|
||||
eventId,
|
||||
fieldFromBrowserField: { ...hostIpFieldFromBrowserField },
|
||||
isDraggable: false,
|
||||
scopeId: TimelineId.test,
|
||||
values: [...hostIpValues],
|
||||
};
|
||||
|
||||
const enrichedAgentStatusData: AlertSummaryRow['description'] = {
|
||||
data: {
|
||||
field: AGENT_STATUS_FIELD_NAME,
|
||||
format: '',
|
||||
type: '',
|
||||
aggregatable: false,
|
||||
name: AGENT_STATUS_FIELD_NAME,
|
||||
searchable: false,
|
||||
readFromDocValues: false,
|
||||
isObjectArray: false,
|
||||
},
|
||||
eventId,
|
||||
values: [],
|
||||
scopeId: TimelineId.test,
|
||||
};
|
||||
|
||||
describe('SummaryValueCell', () => {
|
||||
test('it should render', async () => {
|
||||
await act(async () => {
|
||||
render(
|
||||
<TestProviders>
|
||||
<SummaryValueCell {...enrichedHostIpData} />
|
||||
</TestProviders>
|
||||
);
|
||||
});
|
||||
|
||||
hostIpValues.forEach((ipValue) => expect(screen.getByText(ipValue)).toBeInTheDocument());
|
||||
expect(screen.getByTestId('inlineActions')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe('Without hover actions', () => {
|
||||
test('When in the timeline flyout with timelineId active', async () => {
|
||||
await act(async () => {
|
||||
render(
|
||||
<TestProviders>
|
||||
<SummaryValueCell {...enrichedHostIpData} scopeId={TimelineId.active} />
|
||||
</TestProviders>
|
||||
);
|
||||
});
|
||||
|
||||
hostIpValues.forEach((ipValue) => expect(screen.getByText(ipValue)).toBeInTheDocument());
|
||||
expect(screen.queryByTestId('inlineActions')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('When rendering the host status field', async () => {
|
||||
await act(async () => {
|
||||
render(
|
||||
<TestProviders>
|
||||
<SummaryValueCell {...enrichedAgentStatusData} />
|
||||
</TestProviders>
|
||||
);
|
||||
});
|
||||
|
||||
expect(screen.getByTestId('event-field-agent.status')).toBeInTheDocument();
|
||||
expect(screen.queryByTestId('inlineActions')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,64 +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 from 'react';
|
||||
|
||||
import {
|
||||
SecurityCellActions,
|
||||
CellActionsMode,
|
||||
SecurityCellActionsTrigger,
|
||||
} from '../../cell_actions';
|
||||
import { FieldValueCell } from './field_value_cell';
|
||||
import type { AlertSummaryRow } from '../helpers';
|
||||
import { hasHoverOrRowActions } from '../helpers';
|
||||
import { TimelineId } from '../../../../../common/types';
|
||||
import { getSourcererScopeId } from '../../../../helpers';
|
||||
|
||||
const style = { flexGrow: 0 };
|
||||
|
||||
export const SummaryValueCell: React.FC<AlertSummaryRow['description']> = ({
|
||||
data,
|
||||
eventId,
|
||||
fieldFromBrowserField,
|
||||
isDraggable,
|
||||
linkValue,
|
||||
scopeId,
|
||||
values,
|
||||
isReadOnly,
|
||||
}) => {
|
||||
const hoverActionsEnabled = hasHoverOrRowActions(data.field);
|
||||
|
||||
return (
|
||||
<>
|
||||
<FieldValueCell
|
||||
contextId={scopeId}
|
||||
data={data}
|
||||
eventId={eventId}
|
||||
fieldFromBrowserField={fieldFromBrowserField}
|
||||
linkValue={linkValue}
|
||||
isDraggable={isDraggable}
|
||||
style={style}
|
||||
values={values}
|
||||
/>
|
||||
{scopeId !== TimelineId.active && !isReadOnly && hoverActionsEnabled && (
|
||||
<SecurityCellActions
|
||||
data={{
|
||||
field: data.field,
|
||||
value: values,
|
||||
}}
|
||||
triggerId={SecurityCellActionsTrigger.DETAILS_FLYOUT}
|
||||
mode={CellActionsMode.INLINE}
|
||||
visibleCellActions={3}
|
||||
sourcererScopeId={getSourcererScopeId(scopeId)}
|
||||
metadata={{ scopeId }}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
SummaryValueCell.displayName = 'SummaryValueCell';
|
|
@ -29,25 +29,10 @@ export const RESPONSE_ACTIONS_VIEW = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const FIELD = i18n.translate('xpack.securitySolution.eventDetails.field', {
|
||||
defaultMessage: 'Field',
|
||||
});
|
||||
|
||||
export const VALUE = i18n.translate('xpack.securitySolution.eventDetails.value', {
|
||||
defaultMessage: 'Value',
|
||||
});
|
||||
|
||||
export const DESCRIPTION = i18n.translate('xpack.securitySolution.eventDetails.description', {
|
||||
defaultMessage: 'Description',
|
||||
});
|
||||
|
||||
export const PLACEHOLDER = i18n.translate(
|
||||
'xpack.securitySolution.eventDetails.filter.placeholder',
|
||||
{
|
||||
defaultMessage: 'Filter by Field, Value, or Description...',
|
||||
}
|
||||
);
|
||||
|
||||
export const AGENT_STATUS = i18n.translate('xpack.securitySolution.detections.alerts.agentStatus', {
|
||||
defaultMessage: 'Agent status',
|
||||
});
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* 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 { render } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
|
||||
import { TableFieldNameCell } from './table_field_name_cell';
|
||||
import {
|
||||
FLYOUT_TABLE_FIELD_NAME_CELL_ICON_TEST_ID,
|
||||
FLYOUT_TABLE_FIELD_NAME_CELL_TEXT_TEST_ID,
|
||||
} from './test_ids';
|
||||
|
||||
const mockDataType = 'date';
|
||||
const mockField = '@timestamp';
|
||||
|
||||
describe('TableFieldNameCell', () => {
|
||||
it('should render icon and text', () => {
|
||||
const { getByTestId } = render(
|
||||
<TableFieldNameCell dataType={mockDataType} field={mockField} />
|
||||
);
|
||||
|
||||
expect(getByTestId(FLYOUT_TABLE_FIELD_NAME_CELL_ICON_TEST_ID)).toBeInTheDocument();
|
||||
expect(
|
||||
getByTestId(FLYOUT_TABLE_FIELD_NAME_CELL_ICON_TEST_ID).querySelector('span')
|
||||
).toHaveAttribute('data-euiicon-type', 'tokenDate');
|
||||
expect(getByTestId(FLYOUT_TABLE_FIELD_NAME_CELL_TEXT_TEST_ID)).toHaveTextContent(mockField);
|
||||
});
|
||||
|
||||
it('should render default icon', () => {
|
||||
const { getByTestId } = render(
|
||||
<TableFieldNameCell dataType={'wrong_type'} field={mockField} />
|
||||
);
|
||||
|
||||
expect(
|
||||
getByTestId(FLYOUT_TABLE_FIELD_NAME_CELL_ICON_TEST_ID).querySelector('span')
|
||||
).toHaveAttribute('data-euiicon-type', 'questionInCircle');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,81 @@
|
|||
/*
|
||||
* 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, { memo } from 'react';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiText, EuiToolTip } from '@elastic/eui';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { FieldIcon } from '@kbn/react-field';
|
||||
import { EcsFlat } from '@elastic/ecs';
|
||||
import { getFieldTypeName } from '@kbn/field-utils';
|
||||
import {
|
||||
FLYOUT_TABLE_FIELD_NAME_CELL_ICON_TEST_ID,
|
||||
FLYOUT_TABLE_FIELD_NAME_CELL_TEXT_TEST_ID,
|
||||
} from './test_ids';
|
||||
import { getExampleText } from '../../../../common/components/event_details/helpers';
|
||||
|
||||
const getEcsField = (field: string): { example?: string; description?: string } | undefined => {
|
||||
return EcsFlat[field as keyof typeof EcsFlat] as
|
||||
| {
|
||||
example?: string;
|
||||
description?: string;
|
||||
}
|
||||
| undefined;
|
||||
};
|
||||
|
||||
export interface TableFieldNameCellProps {
|
||||
/**
|
||||
* Type used to pick the correct icon
|
||||
*/
|
||||
dataType: string;
|
||||
/**
|
||||
* Field name
|
||||
*/
|
||||
field: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders an icon/text couple in the first column of the table
|
||||
*/
|
||||
export const TableFieldNameCell = memo(({ dataType, field }: TableFieldNameCellProps) => {
|
||||
const ecsField = getEcsField(field);
|
||||
const typeName = getFieldTypeName(dataType);
|
||||
|
||||
return (
|
||||
<EuiFlexGroup gutterSize="xs" alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<FieldIcon
|
||||
data-test-subj={FLYOUT_TABLE_FIELD_NAME_CELL_ICON_TEST_ID}
|
||||
type={dataType}
|
||||
label={typeName}
|
||||
scripted={undefined}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup wrap={true} gutterSize="none" responsive={false}>
|
||||
<EuiFlexItem className="eui-textBreakAll" grow={false}>
|
||||
<EuiToolTip
|
||||
position="top"
|
||||
content={
|
||||
!isEmpty(ecsField?.description)
|
||||
? `${ecsField?.description} ${getExampleText(ecsField?.example)}`
|
||||
: field
|
||||
}
|
||||
delay="long"
|
||||
anchorClassName="eui-textBreakAll"
|
||||
>
|
||||
<EuiText size="xs" data-test-subj={FLYOUT_TABLE_FIELD_NAME_CELL_TEXT_TEST_ID}>
|
||||
{field}
|
||||
</EuiText>
|
||||
</EuiToolTip>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
});
|
||||
|
||||
TableFieldNameCell.displayName = 'TableFieldNameCell';
|
|
@ -8,10 +8,10 @@
|
|||
import { render, screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
|
||||
import type { BrowserField } from '../../../containers/source';
|
||||
import { FieldValueCell } from './field_value_cell';
|
||||
import { TestProviders } from '../../../mock';
|
||||
import type { EventFieldsData } from '../types';
|
||||
import type { BrowserField } from '@kbn/timelines-plugin/common';
|
||||
import type { EventFieldsData } from '../../../../common/components/event_details/types';
|
||||
import { TableFieldValueCell } from './table_field_value_cell';
|
||||
import { TestProviders } from '../../../../common/mock';
|
||||
|
||||
const contextId = 'test';
|
||||
|
||||
|
@ -31,12 +31,12 @@ const hostIpData: EventFieldsData = {
|
|||
};
|
||||
const hostIpValues = ['127.0.0.1', '::1', '10.1.2.3', 'fe80::4001:aff:fec8:32'];
|
||||
|
||||
describe('FieldValueCell', () => {
|
||||
describe('TableFieldValueCell', () => {
|
||||
describe('common behavior', () => {
|
||||
beforeEach(() => {
|
||||
render(
|
||||
<TestProviders>
|
||||
<FieldValueCell
|
||||
<TableFieldValueCell
|
||||
contextId={contextId}
|
||||
data={hostIpData}
|
||||
eventId={eventId}
|
||||
|
@ -46,7 +46,7 @@ describe('FieldValueCell', () => {
|
|||
);
|
||||
});
|
||||
|
||||
test('it formats multiple values such that each value is displayed on a single line', () => {
|
||||
it('should format multiple values such that each value is displayed on a single line', () => {
|
||||
expect(screen.getByTestId(`event-field-${hostIpData.field}`).className).toContain('column');
|
||||
});
|
||||
});
|
||||
|
@ -55,7 +55,7 @@ describe('FieldValueCell', () => {
|
|||
beforeEach(() => {
|
||||
render(
|
||||
<TestProviders>
|
||||
<FieldValueCell
|
||||
<TableFieldValueCell
|
||||
contextId={contextId}
|
||||
data={hostIpData}
|
||||
eventId={eventId}
|
||||
|
@ -66,17 +66,11 @@ describe('FieldValueCell', () => {
|
|||
);
|
||||
});
|
||||
|
||||
test('it renders each of the expected values when `fieldFromBrowserField` is undefined', () => {
|
||||
it('should render each of the expected values when `fieldFromBrowserField` is undefined', () => {
|
||||
hostIpValues.forEach((value) => {
|
||||
expect(screen.getByText(value)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test('it renders values formatted as plain text (without `eventFieldsTable__fieldValue` formatting)', () => {
|
||||
expect(screen.getByTestId(`event-field-${hostIpData.field}`).firstChild).not.toHaveClass(
|
||||
'eventFieldsTable__fieldValue'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('`message` field formatting', () => {
|
||||
|
@ -105,7 +99,7 @@ describe('FieldValueCell', () => {
|
|||
beforeEach(() => {
|
||||
render(
|
||||
<TestProviders>
|
||||
<FieldValueCell
|
||||
<TableFieldValueCell
|
||||
contextId={contextId}
|
||||
data={messageData}
|
||||
eventId={eventId}
|
||||
|
@ -116,11 +110,11 @@ describe('FieldValueCell', () => {
|
|||
);
|
||||
});
|
||||
|
||||
test('it renders special formatting for the `message` field', () => {
|
||||
it('should render special formatting for the `message` field', () => {
|
||||
expect(screen.getByTestId('event-field-message')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('it renders the expected message value', () => {
|
||||
it('should render the expected message value', () => {
|
||||
messageValues.forEach((value) => {
|
||||
expect(screen.getByText(value)).toBeInTheDocument();
|
||||
});
|
||||
|
@ -139,7 +133,7 @@ describe('FieldValueCell', () => {
|
|||
beforeEach(() => {
|
||||
render(
|
||||
<TestProviders>
|
||||
<FieldValueCell
|
||||
<TableFieldValueCell
|
||||
contextId={contextId}
|
||||
data={hostIpData}
|
||||
eventId={eventId}
|
||||
|
@ -150,23 +144,17 @@ describe('FieldValueCell', () => {
|
|||
);
|
||||
});
|
||||
|
||||
test('it renders values formatted with the expected class', () => {
|
||||
expect(screen.getByTestId(`event-field-${hostIpData.field}`).firstChild).toHaveClass(
|
||||
'eventFieldsTable__fieldValue'
|
||||
);
|
||||
});
|
||||
|
||||
test('it aligns items at the start of the group to prevent content from stretching (by default)', () => {
|
||||
it('should align items at the start of the group to prevent content from stretching (by default)', () => {
|
||||
expect(screen.getByTestId(`event-field-${hostIpData.field}`).className).toContain(
|
||||
'flexStart'
|
||||
);
|
||||
});
|
||||
|
||||
test('it renders link buttons for each of the host ip addresses', () => {
|
||||
it('should render link buttons for each of the host ip addresses', () => {
|
||||
expect(screen.getAllByRole('button').length).toBe(hostIpValues.length);
|
||||
});
|
||||
|
||||
test('it renders each of the expected values when `fieldFromBrowserField` is provided', () => {
|
||||
it('should render each of the expected values when `fieldFromBrowserField` is provided', () => {
|
||||
hostIpValues.forEach((value) => {
|
||||
expect(screen.getByText(value)).toBeInTheDocument();
|
||||
});
|
|
@ -0,0 +1,105 @@
|
|||
/*
|
||||
* 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, { memo } from 'react';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui';
|
||||
import type { BrowserField } from '@kbn/timelines-plugin/common';
|
||||
import { getFieldFormat } from '../../../../common/components/event_details/get_field_format';
|
||||
import type { EventFieldsData } from '../../../../common/components/event_details/types';
|
||||
import { OverflowField } from '../../../../common/components/tables/helpers';
|
||||
import { FormattedFieldValue } from '../../../../timelines/components/timeline/body/renderers/formatted_field';
|
||||
import { MESSAGE_FIELD_NAME } from '../../../../timelines/components/timeline/body/renderers/constants';
|
||||
|
||||
export interface FieldValueCellProps {
|
||||
/**
|
||||
* Value used to create a unique identifier in children components
|
||||
*/
|
||||
contextId: string;
|
||||
/**
|
||||
* Datq retrieved from the row
|
||||
*/
|
||||
data: EventFieldsData;
|
||||
/**
|
||||
* Id of the document
|
||||
*/
|
||||
eventId: string;
|
||||
/**
|
||||
* Field retrieved from the BrowserField
|
||||
*/
|
||||
fieldFromBrowserField?: Partial<BrowserField>;
|
||||
/**
|
||||
* Value of the link field if it exists. Allows to navigate to other pages like host, user, network...
|
||||
*/
|
||||
getLinkValue?: (field: string) => string | null;
|
||||
/**
|
||||
* Values for the field, to render in the second column of the table
|
||||
*/
|
||||
values: string[] | null | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the value of a field in the second column of the table
|
||||
*/
|
||||
export const TableFieldValueCell = memo(
|
||||
({
|
||||
contextId,
|
||||
data,
|
||||
eventId,
|
||||
fieldFromBrowserField,
|
||||
getLinkValue,
|
||||
values,
|
||||
}: FieldValueCellProps) => {
|
||||
if (values == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiFlexGroup
|
||||
data-test-subj={`event-field-${data.field}`}
|
||||
direction="column"
|
||||
gutterSize="none"
|
||||
>
|
||||
{values.map((value, i) => {
|
||||
if (fieldFromBrowserField == null) {
|
||||
return (
|
||||
<EuiFlexItem grow={false} key={`${i}-${value}`}>
|
||||
<EuiText size="xs" key={`${i}-${value}`}>
|
||||
{value}
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiFlexItem grow={false} key={`${i}-${value}`}>
|
||||
{data.field === MESSAGE_FIELD_NAME ? (
|
||||
<OverflowField value={value} />
|
||||
) : (
|
||||
<FormattedFieldValue
|
||||
contextId={`${contextId}-${eventId}-${data.field}-${i}-${value}`}
|
||||
eventId={eventId}
|
||||
fieldFormat={getFieldFormat(data)}
|
||||
fieldName={data.field}
|
||||
fieldFromBrowserField={fieldFromBrowserField}
|
||||
fieldType={data.type}
|
||||
isAggregatable={fieldFromBrowserField.aggregatable}
|
||||
isDraggable={false}
|
||||
isObjectArray={data.isObjectArray}
|
||||
value={value}
|
||||
linkValue={getLinkValue && getLinkValue(data.field)}
|
||||
truncate={false}
|
||||
/>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
);
|
||||
})}
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
TableFieldValueCell.displayName = 'TableFieldValueCell';
|
|
@ -8,6 +8,14 @@
|
|||
import { PREFIX } from '../../../shared/test_ids';
|
||||
import { CONTENT_TEST_ID, HEADER_TEST_ID } from './expandable_section';
|
||||
|
||||
/* Table */
|
||||
|
||||
const FLYOUT_TABLE_TEST_ID = `${PREFIX}Table` as const;
|
||||
export const FLYOUT_TABLE_FIELD_NAME_CELL_ICON_TEST_ID =
|
||||
`${FLYOUT_TABLE_TEST_ID}FieldNameCellIcon` as const;
|
||||
export const FLYOUT_TABLE_FIELD_NAME_CELL_TEXT_TEST_ID =
|
||||
`${FLYOUT_TABLE_TEST_ID}FieldNameCellText` as const;
|
||||
|
||||
/* Header */
|
||||
|
||||
const FLYOUT_HEADER_TEST_ID = `${PREFIX}Header` as const;
|
||||
|
|
|
@ -7,10 +7,13 @@
|
|||
|
||||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { DocumentDetailsContext } from '../../shared/context';
|
||||
import { TABLE_TAB_CONTENT_TEST_ID } from './test_ids';
|
||||
import { TABLE_TAB_CONTENT_TEST_ID, TABLE_TAB_SEARCH_INPUT_TEST_ID } from './test_ids';
|
||||
import { TableTab } from './table_tab';
|
||||
import { TestProviders } from '../../../../common/mock';
|
||||
import { mockContextValue } from '../../shared/mocks/mock_context';
|
||||
import { FLYOUT_TABLE_FIELD_NAME_CELL_ICON_TEST_ID } from '../components/test_ids';
|
||||
|
||||
const mockDispatch = jest.fn();
|
||||
jest.mock('react-redux', () => {
|
||||
|
@ -40,4 +43,35 @@ describe('<TableTab />', () => {
|
|||
|
||||
expect(getByTestId(TABLE_TAB_CONTENT_TEST_ID)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should renders the column headers and a field/value pair', () => {
|
||||
const { getAllByTestId, getByText } = render(
|
||||
<TestProviders>
|
||||
<DocumentDetailsContext.Provider value={mockContextValue}>
|
||||
<TableTab />
|
||||
</DocumentDetailsContext.Provider>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(getByText('Field')).toBeInTheDocument();
|
||||
expect(getByText('Value')).toBeInTheDocument();
|
||||
expect(getByText('kibana.alert.workflow_status')).toBeInTheDocument();
|
||||
expect(getByText('open')).toBeInTheDocument();
|
||||
expect(getAllByTestId(FLYOUT_TABLE_FIELD_NAME_CELL_ICON_TEST_ID).length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should filter the table correctly', () => {
|
||||
const { getByTestId, queryByTestId, queryByText } = render(
|
||||
<TestProviders>
|
||||
<DocumentDetailsContext.Provider value={mockContextValue}>
|
||||
<TableTab />
|
||||
</DocumentDetailsContext.Provider>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
userEvent.type(getByTestId(TABLE_TAB_SEARCH_INPUT_TEST_ID), 'test');
|
||||
expect(queryByText('kibana.alert.workflow_status')).not.toBeInTheDocument();
|
||||
expect(queryByText('open')).not.toBeInTheDocument();
|
||||
expect(queryByTestId(FLYOUT_TABLE_FIELD_NAME_CELL_ICON_TEST_ID)).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,46 +5,104 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { memo } from 'react';
|
||||
import { EuiText } from '@elastic/eui';
|
||||
import { getFieldFromBrowserField } from '../../../../common/components/event_details/columns';
|
||||
import React, { memo, useCallback, useMemo, useState } from 'react';
|
||||
import { getOr, sortBy } from 'lodash/fp';
|
||||
import memoizeOne from 'memoize-one';
|
||||
import { css } from '@emotion/react';
|
||||
import { type EuiBasicTableColumn, EuiText, EuiInMemoryTable, useEuiFontSize } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { dataTableSelectors, tableDefaults } from '@kbn/securitysolution-data-table';
|
||||
import { getCategory } from '@kbn/triggers-actions-ui-plugin/public';
|
||||
import type {
|
||||
BrowserField,
|
||||
BrowserFields,
|
||||
TimelineEventsDetailsItem,
|
||||
} from '@kbn/timelines-plugin/common';
|
||||
import { TableFieldNameCell } from '../components/table_field_name_cell';
|
||||
import { TableFieldValueCell } from '../components/table_field_value_cell';
|
||||
import { TABLE_TAB_CONTENT_TEST_ID, TABLE_TAB_SEARCH_INPUT_TEST_ID } from './test_ids';
|
||||
import { getAllFieldsByName } from '../../../../common/containers/source';
|
||||
import { useDeepEqualSelector } from '../../../../common/hooks/use_selector';
|
||||
import { timelineDefaults } from '../../../../timelines/store/defaults';
|
||||
import { timelineSelectors } from '../../../../timelines/store';
|
||||
import type { EventFieldsData } from '../../../../common/components/event_details/types';
|
||||
import { FieldValueCell } from '../../../../common/components/event_details/table/field_value_cell';
|
||||
import { FieldNameCell } from '../../../../common/components/event_details/table/field_name_cell';
|
||||
import { CellActions } from '../components/cell_actions';
|
||||
import * as i18n from '../../../../common/components/event_details/translations';
|
||||
import { useDocumentDetailsContext } from '../../shared/context';
|
||||
import type { ColumnsProvider } from '../../../../common/components/event_details/event_fields_browser';
|
||||
import { EventFieldsBrowser } from '../../../../common/components/event_details/event_fields_browser';
|
||||
import { TimelineTabs } from '../../../../../common/types';
|
||||
import { isInTableScope, isTimelineScope } from '../../../../helpers';
|
||||
|
||||
export const getColumns: ColumnsProvider = ({
|
||||
browserFields,
|
||||
eventId,
|
||||
contextId,
|
||||
scopeId,
|
||||
getLinkValue,
|
||||
isDraggable,
|
||||
}) => [
|
||||
const COUNT_PER_PAGE_OPTIONS = [25, 50, 100];
|
||||
|
||||
const PLACEHOLDER = i18n.translate('xpack.securitySolution.flyout.table.filterPlaceholderLabel', {
|
||||
defaultMessage: 'Filter by field or value...',
|
||||
});
|
||||
export const FIELD = i18n.translate('xpack.securitySolution.flyout.table.fieldCellLabel', {
|
||||
defaultMessage: 'Field',
|
||||
});
|
||||
const VALUE = i18n.translate('xpack.securitySolution.flyout.table.valueCellLabel', {
|
||||
defaultMessage: 'Value',
|
||||
});
|
||||
|
||||
/**
|
||||
* Defines the behavior of the search input that appears above the table of data
|
||||
*/
|
||||
const search = {
|
||||
box: {
|
||||
incremental: true,
|
||||
placeholder: PLACEHOLDER,
|
||||
schema: true,
|
||||
'data-test-subj': TABLE_TAB_SEARCH_INPUT_TEST_ID,
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the correct field from the BrowserField
|
||||
*/
|
||||
export const getFieldFromBrowserField = memoizeOne(
|
||||
(field: string, browserFields: BrowserFields): BrowserField | undefined => {
|
||||
const category = getCategory(field);
|
||||
|
||||
return browserFields[category]?.fields?.[field] as BrowserField;
|
||||
},
|
||||
(newArgs, lastArgs) => newArgs[0] === lastArgs[0]
|
||||
);
|
||||
|
||||
export type ColumnsProvider = (providerOptions: {
|
||||
/**
|
||||
* An object containing fields by type
|
||||
*/
|
||||
browserFields: BrowserFields;
|
||||
/**
|
||||
* Id of the document
|
||||
*/
|
||||
eventId: string;
|
||||
/**
|
||||
* Maintain backwards compatibility // TODO remove when possible
|
||||
*/
|
||||
scopeId: string;
|
||||
/**
|
||||
* Value of the link field if it exists. Allows to navigate to other pages like host, user, network...
|
||||
*/
|
||||
getLinkValue: (field: string) => string | null;
|
||||
}) => Array<EuiBasicTableColumn<TimelineEventsDetailsItem>>;
|
||||
|
||||
export const getColumns: ColumnsProvider = ({ browserFields, eventId, scopeId, getLinkValue }) => [
|
||||
{
|
||||
field: 'field',
|
||||
name: (
|
||||
<EuiText size="xs">
|
||||
<strong>{i18n.FIELD}</strong>
|
||||
<strong>{FIELD}</strong>
|
||||
</EuiText>
|
||||
),
|
||||
width: '30%',
|
||||
render: (field, data) => {
|
||||
return (
|
||||
<FieldNameCell data={data as EventFieldsData} field={field} fieldMapping={undefined} />
|
||||
);
|
||||
return <TableFieldNameCell dataType={(data as EventFieldsData).type} field={field} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'values',
|
||||
name: (
|
||||
<EuiText size="xs">
|
||||
<strong>{i18n.VALUE}</strong>
|
||||
<strong>{VALUE}</strong>
|
||||
</EuiText>
|
||||
),
|
||||
width: '70%',
|
||||
|
@ -52,13 +110,12 @@ export const getColumns: ColumnsProvider = ({
|
|||
const fieldFromBrowserField = getFieldFromBrowserField(data.field, browserFields);
|
||||
return (
|
||||
<CellActions field={data.field} value={values} isObjectArray={data.isObjectArray}>
|
||||
<FieldValueCell
|
||||
contextId={contextId}
|
||||
<TableFieldValueCell
|
||||
contextId={scopeId}
|
||||
data={data as EventFieldsData}
|
||||
eventId={eventId}
|
||||
fieldFromBrowserField={fieldFromBrowserField}
|
||||
getLinkValue={getLinkValue}
|
||||
isDraggable={isDraggable}
|
||||
values={values}
|
||||
/>
|
||||
</CellActions>
|
||||
|
@ -68,23 +125,106 @@ export const getColumns: ColumnsProvider = ({
|
|||
];
|
||||
|
||||
/**
|
||||
* Table view displayed in the document details expandable flyout right section
|
||||
* Table view displayed in the document details expandable flyout right section Table tab
|
||||
*/
|
||||
// TODO: MOVE TO FLYOUT FOLDER - https://github.com/elastic/security-team/issues/7462
|
||||
export const TableTab = memo(() => {
|
||||
const smallFontSize = useEuiFontSize('xs').fontSize;
|
||||
|
||||
const { browserFields, dataFormattedForFieldBrowser, eventId, scopeId } =
|
||||
useDocumentDetailsContext();
|
||||
|
||||
const [pagination, setPagination] = useState<{ pageIndex: number }>({
|
||||
pageIndex: 0,
|
||||
});
|
||||
const onTableChange = useCallback(({ page: { index } }: { page: { index: number } }) => {
|
||||
setPagination({ pageIndex: index });
|
||||
}, []);
|
||||
|
||||
const getScope = useMemo(() => {
|
||||
if (isTimelineScope(scopeId)) {
|
||||
return timelineSelectors.getTimelineByIdSelector();
|
||||
} else if (isInTableScope(scopeId)) {
|
||||
return dataTableSelectors.getTableByIdSelector();
|
||||
}
|
||||
}, [scopeId]);
|
||||
|
||||
const defaults = useMemo(
|
||||
() => (isTimelineScope(scopeId) ? timelineDefaults : tableDefaults),
|
||||
[scopeId]
|
||||
);
|
||||
|
||||
const columnHeaders = useDeepEqualSelector((state) => {
|
||||
const { columns } = (getScope && getScope(state, scopeId)) ?? defaults;
|
||||
return columns;
|
||||
});
|
||||
|
||||
const fieldsByName = useMemo(() => getAllFieldsByName(browserFields), [browserFields]);
|
||||
|
||||
const items = useMemo(
|
||||
() =>
|
||||
sortBy(['field'], dataFormattedForFieldBrowser).map((item, i) => ({
|
||||
...item,
|
||||
...fieldsByName[item.field],
|
||||
valuesConcatenated: item.values != null ? item.values.join() : '',
|
||||
ariaRowindex: i + 1,
|
||||
})),
|
||||
[dataFormattedForFieldBrowser, fieldsByName]
|
||||
);
|
||||
|
||||
const getLinkValue = useCallback(
|
||||
(field: string) => {
|
||||
const columnHeader = columnHeaders.find((col) => col.id === field);
|
||||
if (!columnHeader || !columnHeader.linkField) {
|
||||
return null;
|
||||
}
|
||||
const linkFieldData = (dataFormattedForFieldBrowser ?? []).find(
|
||||
(d) => d.field === columnHeader.linkField
|
||||
);
|
||||
const linkFieldValue = getOr(null, 'originalValue', linkFieldData);
|
||||
return Array.isArray(linkFieldValue) ? linkFieldValue[0] : linkFieldValue;
|
||||
},
|
||||
[dataFormattedForFieldBrowser, columnHeaders]
|
||||
);
|
||||
|
||||
// forces the rows of the table to render smaller fonts
|
||||
const onSetRowProps = useCallback(
|
||||
({ field }: TimelineEventsDetailsItem) => ({
|
||||
className: 'flyout-table-row-small-font',
|
||||
'data-test-subj': `flyout-table-row-${field}`,
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
const columns = useMemo(
|
||||
() =>
|
||||
getColumns({
|
||||
browserFields,
|
||||
eventId,
|
||||
scopeId,
|
||||
getLinkValue,
|
||||
}),
|
||||
[browserFields, eventId, scopeId, getLinkValue]
|
||||
);
|
||||
|
||||
return (
|
||||
<EventFieldsBrowser
|
||||
browserFields={browserFields}
|
||||
data={dataFormattedForFieldBrowser}
|
||||
eventId={eventId}
|
||||
isDraggable={false}
|
||||
timelineTabType={TimelineTabs.query}
|
||||
scopeId={scopeId}
|
||||
isReadOnly={false}
|
||||
columnsProvider={getColumns}
|
||||
<EuiInMemoryTable
|
||||
items={items}
|
||||
itemId="field"
|
||||
columns={columns}
|
||||
onTableChange={onTableChange}
|
||||
pagination={{
|
||||
...pagination,
|
||||
pageSizeOptions: COUNT_PER_PAGE_OPTIONS,
|
||||
}}
|
||||
rowProps={onSetRowProps}
|
||||
search={search}
|
||||
sorting={false}
|
||||
data-test-subj={TABLE_TAB_CONTENT_TEST_ID}
|
||||
css={css`
|
||||
.euiTableRow {
|
||||
font-size: ${smallFontSize};
|
||||
}
|
||||
`}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import { PREFIX } from '../../../shared/test_ids';
|
||||
|
||||
export const TABLE_TAB_CONTENT_TEST_ID = 'event-fields-browser' as const;
|
||||
export const TABLE_TAB_CONTENT_TEST_ID = `${PREFIX}DocumentTable` as const;
|
||||
export const TABLE_TAB_SEARCH_INPUT_TEST_ID = `${PREFIX}DocumentTableSearchInput` as const;
|
||||
export const JSON_TAB_CONTENT_TEST_ID = 'jsonView' as const;
|
||||
export const JSON_TAB_COPY_TO_CLIPBOARD_BUTTON_TEST_ID = `${PREFIX}JsonTabCopyToClipboard` as const;
|
||||
|
|
|
@ -35942,8 +35942,6 @@
|
|||
"xpack.securitySolution.eventDetails.ctiSummary.investigationEnrichmentTitle": "Enrichi avec la Threat Intelligence",
|
||||
"xpack.securitySolution.eventDetails.ctiSummary.investigationEnrichmentTooltipContent": "Affiche des renseignements supplémentaires sur les menaces (Threat Intelligence) concernant l'alerte. La recherche a porté sur les 30 derniers jours par défaut.",
|
||||
"xpack.securitySolution.eventDetails.description": "Description",
|
||||
"xpack.securitySolution.eventDetails.field": "Champ",
|
||||
"xpack.securitySolution.eventDetails.filter.placeholder": "Filtre par Champ, Valeur ou Description...",
|
||||
"xpack.securitySolution.eventDetails.multiFieldBadge": "champ multiple",
|
||||
"xpack.securitySolution.eventDetails.multiFieldTooltipContent": "Les champs multiples peuvent avoir plusieurs valeurs.",
|
||||
"xpack.securitySolution.eventDetails.osqueryView": "Résultats Osquery",
|
||||
|
@ -35955,7 +35953,6 @@
|
|||
"xpack.securitySolution.eventDetails.summaryView": "résumé",
|
||||
"xpack.securitySolution.eventDetails.table": "Tableau",
|
||||
"xpack.securitySolution.eventDetails.table.actions": "Actions",
|
||||
"xpack.securitySolution.eventDetails.value": "Valeur",
|
||||
"xpack.securitySolution.eventFilter.flyoutForm.creationSuccessToastTitle": "\"{name}\" a été ajouté à la liste de filtres d'événements.",
|
||||
"xpack.securitySolution.eventFilter.form.description.placeholder": "Description",
|
||||
"xpack.securitySolution.eventFilter.form.name.error": "Le nom doit être indiqué",
|
||||
|
@ -36200,18 +36197,6 @@
|
|||
"xpack.securitySolution.fieldBrowser.removeButtonDescription": "Supprimer un champ de temps d'exécution",
|
||||
"xpack.securitySolution.fieldBrowser.runtimeLabel": "Temps d'exécution",
|
||||
"xpack.securitySolution.fieldBrowser.runtimeTitle": "Champ de temps d'exécution",
|
||||
"xpack.securitySolution.fieldNameIcons.booleanAriaLabel": "Champ booléen",
|
||||
"xpack.securitySolution.fieldNameIcons.conflictFieldAriaLabel": "Champ conflictuel",
|
||||
"xpack.securitySolution.fieldNameIcons.dateFieldAriaLabel": "Champ de date",
|
||||
"xpack.securitySolution.fieldNameIcons.geoPointFieldAriaLabel": "Champ de point géographique",
|
||||
"xpack.securitySolution.fieldNameIcons.geoShapeFieldAriaLabel": "Champ de forme géométrique",
|
||||
"xpack.securitySolution.fieldNameIcons.ipAddressFieldAriaLabel": "Champ d'adresse IP",
|
||||
"xpack.securitySolution.fieldNameIcons.murmur3FieldAriaLabel": "Champ Murmur3",
|
||||
"xpack.securitySolution.fieldNameIcons.nestedFieldAriaLabel": "Champ imbriqué",
|
||||
"xpack.securitySolution.fieldNameIcons.numberFieldAriaLabel": "Champ numérique",
|
||||
"xpack.securitySolution.fieldNameIcons.sourceFieldAriaLabel": "Champ source",
|
||||
"xpack.securitySolution.fieldNameIcons.stringFieldAriaLabel": "Champ de chaîne",
|
||||
"xpack.securitySolution.fieldNameIcons.unknownFieldAriaLabel": "Champ inconnu",
|
||||
"xpack.securitySolution.fieldRenderers.moreLabel": "Plus",
|
||||
"xpack.securitySolution.filtersGroup.assignees.buttonTitle": "Utilisateurs affectés",
|
||||
"xpack.securitySolution.filtersGroup.assignees.popoverTooltip": "Filtrer par utilisateurs assignés",
|
||||
|
|
|
@ -35812,8 +35812,6 @@
|
|||
"xpack.securitySolution.eventDetails.ctiSummary.investigationEnrichmentTitle": "脅威インテリジェンスで強化",
|
||||
"xpack.securitySolution.eventDetails.ctiSummary.investigationEnrichmentTooltipContent": "アラートに関する追加の脅威インテリジェンスを表示します。デフォルトでは過去30日分が照会されます。",
|
||||
"xpack.securitySolution.eventDetails.description": "説明",
|
||||
"xpack.securitySolution.eventDetails.field": "フィールド",
|
||||
"xpack.securitySolution.eventDetails.filter.placeholder": "フィールド、値、または説明でフィルター...",
|
||||
"xpack.securitySolution.eventDetails.multiFieldBadge": "複数フィールド",
|
||||
"xpack.securitySolution.eventDetails.multiFieldTooltipContent": "複数フィールドにはフィールドごとに複数の値を入力できます",
|
||||
"xpack.securitySolution.eventDetails.osqueryView": "Osquery結果",
|
||||
|
@ -35825,7 +35823,6 @@
|
|||
"xpack.securitySolution.eventDetails.summaryView": "まとめ",
|
||||
"xpack.securitySolution.eventDetails.table": "表",
|
||||
"xpack.securitySolution.eventDetails.table.actions": "アクション",
|
||||
"xpack.securitySolution.eventDetails.value": "値",
|
||||
"xpack.securitySolution.eventFilter.flyoutForm.creationSuccessToastTitle": "\"{name}\"がイベントフィルターリストに追加されました。",
|
||||
"xpack.securitySolution.eventFilter.form.description.placeholder": "説明",
|
||||
"xpack.securitySolution.eventFilter.form.name.error": "名前を空にすることはできません",
|
||||
|
@ -36070,18 +36067,6 @@
|
|||
"xpack.securitySolution.fieldBrowser.removeButtonDescription": "ランタイムフィールドを削除",
|
||||
"xpack.securitySolution.fieldBrowser.runtimeLabel": "ランタイム",
|
||||
"xpack.securitySolution.fieldBrowser.runtimeTitle": "ランタイムフィールド",
|
||||
"xpack.securitySolution.fieldNameIcons.booleanAriaLabel": "ブールフィールド",
|
||||
"xpack.securitySolution.fieldNameIcons.conflictFieldAriaLabel": "矛盾フィールド",
|
||||
"xpack.securitySolution.fieldNameIcons.dateFieldAriaLabel": "日付フィールド",
|
||||
"xpack.securitySolution.fieldNameIcons.geoPointFieldAriaLabel": "地理ポイントフィールド",
|
||||
"xpack.securitySolution.fieldNameIcons.geoShapeFieldAriaLabel": "地理情報シェイプフィールド",
|
||||
"xpack.securitySolution.fieldNameIcons.ipAddressFieldAriaLabel": "IPアドレスフィールド",
|
||||
"xpack.securitySolution.fieldNameIcons.murmur3FieldAriaLabel": "Murmur3フィールド",
|
||||
"xpack.securitySolution.fieldNameIcons.nestedFieldAriaLabel": "入れ子フィールド",
|
||||
"xpack.securitySolution.fieldNameIcons.numberFieldAriaLabel": "数値フィールド",
|
||||
"xpack.securitySolution.fieldNameIcons.sourceFieldAriaLabel": "ソースフィールド",
|
||||
"xpack.securitySolution.fieldNameIcons.stringFieldAriaLabel": "文字列フィールド",
|
||||
"xpack.securitySolution.fieldNameIcons.unknownFieldAriaLabel": "不明なフィールド",
|
||||
"xpack.securitySolution.fieldRenderers.moreLabel": "詳細",
|
||||
"xpack.securitySolution.filtersGroup.assignees.buttonTitle": "担当者",
|
||||
"xpack.securitySolution.filtersGroup.assignees.popoverTooltip": "担当者でフィルター",
|
||||
|
|
|
@ -35982,8 +35982,6 @@
|
|||
"xpack.securitySolution.eventDetails.ctiSummary.investigationEnrichmentTitle": "已使用威胁情报扩充",
|
||||
"xpack.securitySolution.eventDetails.ctiSummary.investigationEnrichmentTooltipContent": "显示该告警的其他威胁情报。默认会查询过去 30 天的数据。",
|
||||
"xpack.securitySolution.eventDetails.description": "描述",
|
||||
"xpack.securitySolution.eventDetails.field": "字段",
|
||||
"xpack.securitySolution.eventDetails.filter.placeholder": "按字段、值或描述筛选......",
|
||||
"xpack.securitySolution.eventDetails.multiFieldBadge": "多字段",
|
||||
"xpack.securitySolution.eventDetails.multiFieldTooltipContent": "多字段的每个字段可以有多个值",
|
||||
"xpack.securitySolution.eventDetails.osqueryView": "Osquery 结果",
|
||||
|
@ -35995,7 +35993,6 @@
|
|||
"xpack.securitySolution.eventDetails.summaryView": "摘要",
|
||||
"xpack.securitySolution.eventDetails.table": "表",
|
||||
"xpack.securitySolution.eventDetails.table.actions": "操作",
|
||||
"xpack.securitySolution.eventDetails.value": "值",
|
||||
"xpack.securitySolution.eventFilter.flyoutForm.creationSuccessToastTitle": "“{name}”已添加到事件筛选列表。",
|
||||
"xpack.securitySolution.eventFilter.form.description.placeholder": "描述",
|
||||
"xpack.securitySolution.eventFilter.form.name.error": "名称不能为空",
|
||||
|
@ -36240,18 +36237,6 @@
|
|||
"xpack.securitySolution.fieldBrowser.removeButtonDescription": "删除运行时字段",
|
||||
"xpack.securitySolution.fieldBrowser.runtimeLabel": "运行时",
|
||||
"xpack.securitySolution.fieldBrowser.runtimeTitle": "运行时字段",
|
||||
"xpack.securitySolution.fieldNameIcons.booleanAriaLabel": "布尔值字段",
|
||||
"xpack.securitySolution.fieldNameIcons.conflictFieldAriaLabel": "冲突字段",
|
||||
"xpack.securitySolution.fieldNameIcons.dateFieldAriaLabel": "日期字段",
|
||||
"xpack.securitySolution.fieldNameIcons.geoPointFieldAriaLabel": "地理点字段",
|
||||
"xpack.securitySolution.fieldNameIcons.geoShapeFieldAriaLabel": "几何形状字段",
|
||||
"xpack.securitySolution.fieldNameIcons.ipAddressFieldAriaLabel": "IP 地址字段",
|
||||
"xpack.securitySolution.fieldNameIcons.murmur3FieldAriaLabel": "Murmur3 字段",
|
||||
"xpack.securitySolution.fieldNameIcons.nestedFieldAriaLabel": "嵌套字段",
|
||||
"xpack.securitySolution.fieldNameIcons.numberFieldAriaLabel": "数字字段",
|
||||
"xpack.securitySolution.fieldNameIcons.sourceFieldAriaLabel": "源字段",
|
||||
"xpack.securitySolution.fieldNameIcons.stringFieldAriaLabel": "字符串字段",
|
||||
"xpack.securitySolution.fieldNameIcons.unknownFieldAriaLabel": "未知字段",
|
||||
"xpack.securitySolution.fieldRenderers.moreLabel": "更多",
|
||||
"xpack.securitySolution.filtersGroup.assignees.buttonTitle": "被分配人",
|
||||
"xpack.securitySolution.filtersGroup.assignees.popoverTooltip": "按被分配人筛选",
|
||||
|
|
|
@ -5,22 +5,23 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { getClassSelector, getDataTestSubjectSelector } from '../../helpers/common';
|
||||
import { getDataTestSubjectSelector } from '../../helpers/common';
|
||||
|
||||
export const DOCUMENT_DETAILS_FLYOUT_TABLE_TAB_FILTER = getClassSelector('euiFieldSearch');
|
||||
export const DOCUMENT_DETAILS_FLYOUT_TABLE_TAB_FILTER = getDataTestSubjectSelector(
|
||||
'securitySolutionFlyoutDocumentTableSearchInput'
|
||||
);
|
||||
export const DOCUMENT_DETAILS_FLYOUT_TABLE_TAB_TIMESTAMP_ROW = getDataTestSubjectSelector(
|
||||
'event-fields-table-row-@timestamp'
|
||||
'flyout-table-row-@timestamp'
|
||||
);
|
||||
export const DOCUMENT_DETAILS_FLYOUT_TABLE_TAB_TIMESTAMP_CELL =
|
||||
getDataTestSubjectSelector('event-field-@timestamp');
|
||||
export const DOCUMENT_DETAILS_FLYOUT_TABLE_TAB_ID_ROW = getDataTestSubjectSelector(
|
||||
'event-fields-table-row-_id'
|
||||
);
|
||||
export const DOCUMENT_DETAILS_FLYOUT_TABLE_TAB_ID_ROW =
|
||||
getDataTestSubjectSelector('flyout-table-row-_id');
|
||||
export const DOCUMENT_DETAILS_FLYOUT_TABLE_TAB_HOST_OS_BUILD_ROW = getDataTestSubjectSelector(
|
||||
'event-fields-table-row-host.os.build'
|
||||
'flyout-table-row-host.os.build'
|
||||
);
|
||||
export const DOCUMENT_DETAILS_FLYOUT_TABLE_TAB_THREAT_ENRICHMENTS = getDataTestSubjectSelector(
|
||||
'event-fields-table-row-threat.enrichments'
|
||||
'flyout-table-row-threat.enrichments'
|
||||
);
|
||||
const DOCUMENT_DETAILS_FLYOUT_TABLE_TAB_ROW_CELL_ACTIONS =
|
||||
'actionItem-security-detailsFlyout-cellActions-';
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue