mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Security Solution][Alert/Event Flyout] Add pinning and settings to table tab (#218686)
## Summary This PR added enhancements to the table tab: - User can now pin fields - Highlighted fields have a highlighted background - Added settings to: - Filter table to only show highlighted fields - Hide empty values - Hide alert fields (`kibana.alert.*` and `signal.*`) ### Checklist - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/src/platform/packages/shared/kbn-i18n/README.md) - [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 - [x] The PR description includes the appropriate Release Notes section, and the correct `release_note:*` label is applied per the [guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)
This commit is contained in:
parent
e73c8ddcb9
commit
f15c96d6e8
9 changed files with 920 additions and 104 deletions
|
@ -0,0 +1,101 @@
|
|||
/*
|
||||
* 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 { render, act } from '@testing-library/react';
|
||||
import { TableTabSettingButton } from './table_tab_setting_button';
|
||||
import {
|
||||
TABLE_TAB_SETTING_BUTTON_TEST_ID,
|
||||
TABLE_TAB_SETTING_HIGHLIGHTED_FIELDS_ONLY_TEST_ID,
|
||||
TABLE_TAB_SETTING_HIDE_EMPTY_FIELDS_TEST_ID,
|
||||
TABLE_TAB_SETTING_HIDE_ALERT_FIELDS_TEST_ID,
|
||||
} from './test_ids';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
const mockTableTabState = {
|
||||
pinnedFields: [],
|
||||
showHighlightedFields: false,
|
||||
hideEmptyFields: false,
|
||||
hideAlertFields: false,
|
||||
};
|
||||
const mockSetTableTabState = jest.fn();
|
||||
|
||||
const renderComponent = () => {
|
||||
return render(
|
||||
<TableTabSettingButton
|
||||
tableTabState={mockTableTabState}
|
||||
setTableTabState={mockSetTableTabState}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
describe('<TableTabSettingButton />', () => {
|
||||
it('should render button', () => {
|
||||
const { getByTestId } = renderComponent();
|
||||
expect(getByTestId(TABLE_TAB_SETTING_BUTTON_TEST_ID)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render highlighted fields only setting correctly', async () => {
|
||||
const { getByTestId } = renderComponent();
|
||||
const button = getByTestId(TABLE_TAB_SETTING_BUTTON_TEST_ID);
|
||||
expect(button).toBeInTheDocument();
|
||||
|
||||
act(async () => {
|
||||
await userEvent.click(button);
|
||||
const option = getByTestId(TABLE_TAB_SETTING_HIGHLIGHTED_FIELDS_ONLY_TEST_ID);
|
||||
expect(option).toBeInTheDocument();
|
||||
expect(option).not.toBeChecked();
|
||||
|
||||
await userEvent.click(option);
|
||||
expect(option).toBeChecked();
|
||||
expect(mockSetTableTabState).toHaveBeenCalledWith({
|
||||
...mockTableTabState,
|
||||
showHighlightedFields: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should render hide empty fields setting correctly', async () => {
|
||||
const { getByTestId } = renderComponent();
|
||||
const button = getByTestId(TABLE_TAB_SETTING_BUTTON_TEST_ID);
|
||||
expect(button).toBeInTheDocument();
|
||||
|
||||
act(async () => {
|
||||
await userEvent.click(button);
|
||||
const option = getByTestId(TABLE_TAB_SETTING_HIDE_EMPTY_FIELDS_TEST_ID);
|
||||
expect(option).toBeInTheDocument();
|
||||
expect(option).not.toBeChecked();
|
||||
|
||||
await userEvent.click(option);
|
||||
expect(option).toBeChecked();
|
||||
expect(mockSetTableTabState).toHaveBeenCalledWith({
|
||||
...mockTableTabState,
|
||||
hideEmptyFields: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should render hide alert fields setting correctly', async () => {
|
||||
const { getByTestId } = renderComponent();
|
||||
const button = getByTestId(TABLE_TAB_SETTING_BUTTON_TEST_ID);
|
||||
expect(button).toBeInTheDocument();
|
||||
|
||||
act(async () => {
|
||||
await userEvent.click(button);
|
||||
const option = getByTestId(TABLE_TAB_SETTING_HIDE_ALERT_FIELDS_TEST_ID);
|
||||
expect(option).toBeInTheDocument();
|
||||
expect(option).not.toBeChecked();
|
||||
|
||||
await userEvent.click(option);
|
||||
expect(option).toBeChecked();
|
||||
expect(mockSetTableTabState).toHaveBeenCalledWith({
|
||||
...mockTableTabState,
|
||||
hideAlertFields: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,162 @@
|
|||
/*
|
||||
* 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, { useState, useCallback } from 'react';
|
||||
import {
|
||||
EuiButtonIcon,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiPopover,
|
||||
EuiSwitch,
|
||||
EuiToolTip,
|
||||
useEuiTheme,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { css } from '@emotion/react';
|
||||
import type { TableTabState } from '../tabs/table_tab';
|
||||
import {
|
||||
TABLE_TAB_SETTING_BUTTON_TEST_ID,
|
||||
TABLE_TAB_SETTING_HIGHLIGHTED_FIELDS_ONLY_TEST_ID,
|
||||
TABLE_TAB_SETTING_HIDE_EMPTY_FIELDS_TEST_ID,
|
||||
TABLE_TAB_SETTING_HIDE_ALERT_FIELDS_TEST_ID,
|
||||
} from './test_ids';
|
||||
|
||||
const TABLE_TAB_SETTING_BUTTON_LABEL = i18n.translate(
|
||||
'xpack.securitySolution.flyout.documentDetails.right.tableTabSettingButton.label',
|
||||
{
|
||||
defaultMessage: 'Table settings',
|
||||
}
|
||||
);
|
||||
|
||||
const HIGHLIGHTED_FIELDS_ONLY_LABEL = i18n.translate(
|
||||
'xpack.securitySolution.flyout.documentDetails.right.tableTabSettingButton.highlightedFieldsOnlyLabel',
|
||||
{
|
||||
defaultMessage: 'Show highlighted fields only',
|
||||
}
|
||||
);
|
||||
|
||||
const HIDE_EMPTY_FIELDS_LABEL = i18n.translate(
|
||||
'xpack.securitySolution.flyout.documentDetails.right.tableTabSettingButton.hideEmptyFieldsLabel',
|
||||
{
|
||||
defaultMessage: 'Hide empty fields',
|
||||
}
|
||||
);
|
||||
|
||||
const HIDE_ALERT_FIELDS_LABEL = i18n.translate(
|
||||
'xpack.securitySolution.flyout.documentDetails.right.tableTabSettingButton.hideAlertFieldsLabel',
|
||||
{
|
||||
defaultMessage: 'Hide Kibana alert fields',
|
||||
}
|
||||
);
|
||||
|
||||
interface TableTabSettingButtonProps {
|
||||
/**
|
||||
* The current state of the table tab
|
||||
*/
|
||||
tableTabState: TableTabState;
|
||||
/**
|
||||
* The function to set the state of the table tab
|
||||
*/
|
||||
setTableTabState: (tableTabState: TableTabState) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Settings button for the table tab.
|
||||
*/
|
||||
export const TableTabSettingButton = ({
|
||||
tableTabState,
|
||||
setTableTabState,
|
||||
}: TableTabSettingButtonProps) => {
|
||||
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
|
||||
const { showHighlightedFields, hideEmptyFields, hideAlertFields } = tableTabState;
|
||||
const { euiTheme } = useEuiTheme();
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
setIsPopoverOpen(!isPopoverOpen);
|
||||
}, [isPopoverOpen]);
|
||||
|
||||
const closePopover = useCallback(() => {
|
||||
setIsPopoverOpen(false);
|
||||
}, []);
|
||||
|
||||
const onToggleShowHighlightedFields = useCallback(() => {
|
||||
setTableTabState({
|
||||
...tableTabState,
|
||||
showHighlightedFields: !showHighlightedFields,
|
||||
});
|
||||
}, [showHighlightedFields, setTableTabState, tableTabState]);
|
||||
|
||||
const onToggleHideEmptyFields = useCallback(() => {
|
||||
setTableTabState({
|
||||
...tableTabState,
|
||||
hideEmptyFields: !hideEmptyFields,
|
||||
});
|
||||
}, [hideEmptyFields, setTableTabState, tableTabState]);
|
||||
|
||||
const onToggleHideAlertFields = useCallback(() => {
|
||||
setTableTabState({
|
||||
...tableTabState,
|
||||
hideAlertFields: !hideAlertFields,
|
||||
});
|
||||
}, [hideAlertFields, setTableTabState, tableTabState]);
|
||||
|
||||
return (
|
||||
<EuiToolTip content={TABLE_TAB_SETTING_BUTTON_LABEL}>
|
||||
<EuiPopover
|
||||
button={
|
||||
<EuiButtonIcon
|
||||
aria-label={TABLE_TAB_SETTING_BUTTON_LABEL}
|
||||
onClick={onClick}
|
||||
iconType="gear"
|
||||
size="m"
|
||||
css={css`
|
||||
border: 1px solid ${euiTheme.colors.backgroundLightText};
|
||||
margin-left: -5px;
|
||||
`}
|
||||
/>
|
||||
}
|
||||
isOpen={isPopoverOpen}
|
||||
closePopover={closePopover}
|
||||
display="block"
|
||||
data-test-subj={TABLE_TAB_SETTING_BUTTON_TEST_ID}
|
||||
>
|
||||
<EuiFlexGroup justifyContent="spaceBetween" alignItems="flexStart" direction="column">
|
||||
<EuiFlexItem>
|
||||
<EuiSwitch
|
||||
data-test-subj={TABLE_TAB_SETTING_HIGHLIGHTED_FIELDS_ONLY_TEST_ID}
|
||||
label={HIGHLIGHTED_FIELDS_ONLY_LABEL}
|
||||
aria-label={HIGHLIGHTED_FIELDS_ONLY_LABEL}
|
||||
checked={showHighlightedFields}
|
||||
onChange={onToggleShowHighlightedFields}
|
||||
compressed
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiSwitch
|
||||
data-test-subj={TABLE_TAB_SETTING_HIDE_EMPTY_FIELDS_TEST_ID}
|
||||
label={HIDE_EMPTY_FIELDS_LABEL}
|
||||
aria-label={HIDE_EMPTY_FIELDS_LABEL}
|
||||
checked={hideEmptyFields}
|
||||
onChange={onToggleHideEmptyFields}
|
||||
compressed
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiSwitch
|
||||
data-test-subj={TABLE_TAB_SETTING_HIDE_ALERT_FIELDS_TEST_ID}
|
||||
label={HIDE_ALERT_FIELDS_LABEL}
|
||||
aria-label={HIDE_ALERT_FIELDS_LABEL}
|
||||
checked={hideAlertFields}
|
||||
onChange={onToggleHideAlertFields}
|
||||
compressed
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPopover>
|
||||
</EuiToolTip>
|
||||
);
|
||||
};
|
|
@ -17,6 +17,14 @@ export const FLYOUT_TABLE_FIELD_NAME_CELL_TEXT_TEST_ID =
|
|||
`${FLYOUT_TABLE_TEST_ID}FieldNameCellText` as const;
|
||||
export const FLYOUT_TABLE_PREVIEW_LINK_FIELD_TEST_ID =
|
||||
`${FLYOUT_TABLE_TEST_ID}PreviewLinkField` as const;
|
||||
export const FLYOUT_TABLE_PIN_ACTION_TEST_ID = `${FLYOUT_TABLE_TEST_ID}PinAction` as const;
|
||||
export const TABLE_TAB_SETTING_BUTTON_TEST_ID = `${PREFIX}TableTabSettingButton` as const;
|
||||
export const TABLE_TAB_SETTING_HIGHLIGHTED_FIELDS_ONLY_TEST_ID =
|
||||
`${FLYOUT_TABLE_TEST_ID}HighlightedFieldsOnly` as const;
|
||||
export const TABLE_TAB_SETTING_HIDE_EMPTY_FIELDS_TEST_ID =
|
||||
`${FLYOUT_TABLE_TEST_ID}HideEmptyFields` as const;
|
||||
export const TABLE_TAB_SETTING_HIDE_ALERT_FIELDS_TEST_ID =
|
||||
`${FLYOUT_TABLE_TEST_ID}HideAlertFields` as const;
|
||||
|
||||
/* Header */
|
||||
|
||||
|
|
|
@ -6,14 +6,21 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import { act, render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { DocumentDetailsContext } from '../../shared/context';
|
||||
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';
|
||||
import {
|
||||
FLYOUT_TABLE_FIELD_NAME_CELL_ICON_TEST_ID,
|
||||
TABLE_TAB_SETTING_BUTTON_TEST_ID,
|
||||
TABLE_TAB_SETTING_HIDE_ALERT_FIELDS_TEST_ID,
|
||||
TABLE_TAB_SETTING_HIDE_EMPTY_FIELDS_TEST_ID,
|
||||
TABLE_TAB_SETTING_HIGHLIGHTED_FIELDS_ONLY_TEST_ID,
|
||||
} from '../components/test_ids';
|
||||
import { FLYOUT_STORAGE_KEYS } from '../../shared/constants/local_storage';
|
||||
|
||||
const mockDispatch = jest.fn();
|
||||
jest.mock('react-redux', () => {
|
||||
|
@ -24,14 +31,36 @@ jest.mock('react-redux', () => {
|
|||
useDispatch: () => mockDispatch,
|
||||
};
|
||||
});
|
||||
const mockGet = jest.fn();
|
||||
const mockSet = jest.fn();
|
||||
|
||||
jest.mock('../../../../common/lib/kibana', () => {
|
||||
const original = jest.requireActual('../../../../common/lib/kibana');
|
||||
return {
|
||||
...original,
|
||||
useKibana: () => ({
|
||||
services: {
|
||||
storage: {
|
||||
get: mockGet,
|
||||
set: mockSet,
|
||||
},
|
||||
},
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
// FLAKY: https://github.com/elastic/kibana/issues/216393
|
||||
describe.skip('<TableTab />', () => {
|
||||
describe('<TableTab />', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should render table component', () => {
|
||||
const contextValue = {
|
||||
eventId: 'some_Id',
|
||||
browserFields: {},
|
||||
dataFormattedForFieldBrowser: [],
|
||||
investigationFields: [],
|
||||
} as unknown as DocumentDetailsContext;
|
||||
|
||||
const { getByTestId } = render(
|
||||
|
@ -43,6 +72,7 @@ describe.skip('<TableTab />', () => {
|
|||
);
|
||||
|
||||
expect(getByTestId(TABLE_TAB_CONTENT_TEST_ID)).toBeInTheDocument();
|
||||
expect(getByTestId(TABLE_TAB_SETTING_BUTTON_TEST_ID)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should renders the column headers and a field/value pair', () => {
|
||||
|
@ -75,4 +105,31 @@ describe.skip('<TableTab />', () => {
|
|||
expect(queryByText('open')).not.toBeInTheDocument();
|
||||
expect(queryByTestId(FLYOUT_TABLE_FIELD_NAME_CELL_ICON_TEST_ID)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should fetch the table state from local storage', async () => {
|
||||
mockGet.mockReturnValue({
|
||||
[FLYOUT_STORAGE_KEYS.TABLE_TAB_STATE]: {
|
||||
pinnedFields: [],
|
||||
showHighlightedFields: true,
|
||||
hideEmptyFields: false,
|
||||
hideAlertFields: true,
|
||||
},
|
||||
});
|
||||
|
||||
const { getByTestId } = render(
|
||||
<TestProviders>
|
||||
<DocumentDetailsContext.Provider value={mockContextValue}>
|
||||
<TableTab />
|
||||
</DocumentDetailsContext.Provider>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
const settingsButton = getByTestId(TABLE_TAB_SETTING_BUTTON_TEST_ID);
|
||||
act(async () => {
|
||||
await userEvent.click(settingsButton);
|
||||
expect(screen.getByTestId(TABLE_TAB_SETTING_HIGHLIGHTED_FIELDS_ONLY_TEST_ID)).toBeChecked();
|
||||
expect(screen.getByTestId(TABLE_TAB_SETTING_HIDE_EMPTY_FIELDS_TEST_ID)).not.toBeChecked();
|
||||
expect(screen.getByTestId(TABLE_TAB_SETTING_HIDE_ALERT_FIELDS_TEST_ID)).toBeChecked();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,45 +5,81 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { memo, useCallback, useMemo, useState } from 'react';
|
||||
import { getOr, sortBy } from 'lodash/fp';
|
||||
import React, { memo, useCallback, useMemo, useState, useEffect } from 'react';
|
||||
import { getOr } from 'lodash/fp';
|
||||
import memoizeOne from 'memoize-one';
|
||||
import { css } from '@emotion/react';
|
||||
import { type EuiBasicTableColumn, EuiText, EuiInMemoryTable, useEuiFontSize } from '@elastic/eui';
|
||||
import { EuiInMemoryTable, useEuiFontSize, useEuiTheme } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { dataTableSelectors, tableDefaults } from '@kbn/securitysolution-data-table';
|
||||
import type { BrowserFields, TimelineEventsDetailsItem } from '@kbn/timelines-plugin/common';
|
||||
import type { FieldSpec } from '@kbn/data-plugin/common';
|
||||
import { getCategory } from '@kbn/response-ops-alerts-fields-browser/helpers';
|
||||
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 { CellActions } from '../../shared/components/cell_actions';
|
||||
import { useDocumentDetailsContext } from '../../shared/context';
|
||||
import { isInTableScope, isTimelineScope } from '../../../../helpers';
|
||||
import { useBasicDataFromDetailsData } from '../../shared/hooks/use_basic_data_from_details_data';
|
||||
import { getTableItems } from '../utils/table_tab_utils';
|
||||
import { TableTabSettingButton } from '../components/table_tab_setting_button';
|
||||
import { useKibana } from '../../../../common/lib/kibana';
|
||||
import { FLYOUT_STORAGE_KEYS } from '../../shared/constants/local_storage';
|
||||
import { getTableTabColumns } from '../utils/table_tab_columns';
|
||||
import { useHighlightedFields } from '../../shared/hooks/use_highlighted_fields';
|
||||
|
||||
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',
|
||||
});
|
||||
|
||||
const PIN_ACTION_CSS = css`
|
||||
.flyout_table__unPinAction {
|
||||
opacity: 1;
|
||||
}
|
||||
.flyout_table__pinAction {
|
||||
opacity: 0;
|
||||
}
|
||||
&:hover {
|
||||
.flyout_table__pinAction {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export interface TableTabState {
|
||||
/**
|
||||
* The fields that are pinned
|
||||
*/
|
||||
pinnedFields: string[];
|
||||
/**
|
||||
* Whether to show highlighted fields only
|
||||
*/
|
||||
showHighlightedFields: boolean;
|
||||
/**
|
||||
* Whether to hide empty fields
|
||||
*/
|
||||
hideEmptyFields: boolean;
|
||||
/**
|
||||
* Whether to hide alert fields
|
||||
*/
|
||||
hideAlertFields: boolean;
|
||||
}
|
||||
|
||||
const DEFAULT_TABLE_TAB_STATE: TableTabState = {
|
||||
pinnedFields: [],
|
||||
showHighlightedFields: false,
|
||||
hideEmptyFields: false,
|
||||
hideAlertFields: false,
|
||||
};
|
||||
|
||||
/**
|
||||
* Defines the behavior of the search input that appears above the table of data
|
||||
*/
|
||||
const search = {
|
||||
const SEARCH_CONFIG = {
|
||||
box: {
|
||||
incremental: true,
|
||||
placeholder: PLACEHOLDER,
|
||||
|
@ -64,91 +100,54 @@ export const getFieldFromBrowserField = memoizeOne(
|
|||
(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;
|
||||
/**
|
||||
* Id of the rule
|
||||
*/
|
||||
ruleId: string;
|
||||
/**
|
||||
* Whether the preview link is in preview mode
|
||||
*/
|
||||
isPreview: boolean;
|
||||
/**
|
||||
* 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,
|
||||
ruleId,
|
||||
isPreview,
|
||||
}) => [
|
||||
{
|
||||
field: 'field',
|
||||
name: (
|
||||
<EuiText size="xs">
|
||||
<strong>{FIELD}</strong>
|
||||
</EuiText>
|
||||
),
|
||||
width: '30%',
|
||||
render: (field, data) => {
|
||||
return <TableFieldNameCell dataType={(data as EventFieldsData).type} field={field} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'values',
|
||||
name: (
|
||||
<EuiText size="xs">
|
||||
<strong>{VALUE}</strong>
|
||||
</EuiText>
|
||||
),
|
||||
width: '70%',
|
||||
render: (values, data) => {
|
||||
const fieldFromBrowserField = getFieldFromBrowserField(data.field, browserFields);
|
||||
return (
|
||||
<CellActions field={data.field} value={values} isObjectArray={data.isObjectArray}>
|
||||
<TableFieldValueCell
|
||||
scopeId={scopeId}
|
||||
data={data as EventFieldsData}
|
||||
eventId={eventId}
|
||||
fieldFromBrowserField={fieldFromBrowserField}
|
||||
getLinkValue={getLinkValue}
|
||||
ruleId={ruleId}
|
||||
isPreview={isPreview}
|
||||
values={values}
|
||||
/>
|
||||
</CellActions>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Table view displayed in the document details expandable flyout right section Table tab
|
||||
*/
|
||||
export const TableTab = memo(() => {
|
||||
const smallFontSize = useEuiFontSize('xs').fontSize;
|
||||
const { euiTheme } = useEuiTheme();
|
||||
const {
|
||||
services: { storage },
|
||||
} = useKibana();
|
||||
|
||||
const { browserFields, dataFormattedForFieldBrowser, eventId, scopeId, isPreview } =
|
||||
useDocumentDetailsContext();
|
||||
const {
|
||||
browserFields,
|
||||
dataFormattedForFieldBrowser,
|
||||
scopeId,
|
||||
isPreview,
|
||||
eventId,
|
||||
investigationFields,
|
||||
} = useDocumentDetailsContext();
|
||||
const { ruleId } = useBasicDataFromDetailsData(dataFormattedForFieldBrowser);
|
||||
|
||||
const highlightedFieldsResult = useHighlightedFields({
|
||||
dataFormattedForFieldBrowser,
|
||||
investigationFields,
|
||||
});
|
||||
const highlightedFields = useMemo(
|
||||
() => Object.keys(highlightedFieldsResult),
|
||||
[highlightedFieldsResult]
|
||||
);
|
||||
|
||||
const [tableTabState, setTableTabState] = useState<TableTabState>(() => {
|
||||
const restoredTableTabState = storage.get(FLYOUT_STORAGE_KEYS.TABLE_TAB_STATE);
|
||||
if (restoredTableTabState != null) {
|
||||
return restoredTableTabState;
|
||||
}
|
||||
return DEFAULT_TABLE_TAB_STATE;
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
storage.set(FLYOUT_STORAGE_KEYS.TABLE_TAB_STATE, tableTabState);
|
||||
}, [tableTabState, storage]);
|
||||
|
||||
const renderToolsRight = useCallback(
|
||||
() => [
|
||||
<TableTabSettingButton tableTabState={tableTabState} setTableTabState={setTableTabState} />,
|
||||
],
|
||||
[tableTabState, setTableTabState]
|
||||
);
|
||||
|
||||
const [pagination, setPagination] = useState<{ pageIndex: number }>({
|
||||
pageIndex: 0,
|
||||
});
|
||||
|
@ -176,15 +175,33 @@ export const TableTab = memo(() => {
|
|||
|
||||
const fieldsByName = useMemo(() => getAllFieldsByName(browserFields), [browserFields]);
|
||||
|
||||
const { pinnedFields } = useMemo(() => tableTabState, [tableTabState]);
|
||||
const onTogglePinned = useCallback(
|
||||
(field: string, action: 'pin' | 'unpin') => {
|
||||
if (action === 'pin') {
|
||||
setTableTabState({
|
||||
...tableTabState,
|
||||
pinnedFields: [...pinnedFields, field],
|
||||
});
|
||||
} else if (action === 'unpin') {
|
||||
setTableTabState({
|
||||
...tableTabState,
|
||||
pinnedFields: pinnedFields.filter((f) => f !== field),
|
||||
});
|
||||
}
|
||||
},
|
||||
[pinnedFields, tableTabState, setTableTabState]
|
||||
);
|
||||
|
||||
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]
|
||||
getTableItems({
|
||||
dataFormattedForFieldBrowser,
|
||||
fieldsByName,
|
||||
highlightedFields,
|
||||
tableTabState,
|
||||
}),
|
||||
[dataFormattedForFieldBrowser, highlightedFields, tableTabState, fieldsByName]
|
||||
);
|
||||
|
||||
const getLinkValue = useCallback(
|
||||
|
@ -207,23 +224,32 @@ export const TableTab = memo(() => {
|
|||
({ field }: TimelineEventsDetailsItem) => ({
|
||||
className: 'flyout-table-row-small-font',
|
||||
'data-test-subj': `flyout-table-row-${field}`,
|
||||
...(highlightedFields.includes(field) && {
|
||||
style: { backgroundColor: euiTheme.colors.backgroundBaseWarning },
|
||||
}),
|
||||
css: PIN_ACTION_CSS,
|
||||
}),
|
||||
[]
|
||||
[highlightedFields, euiTheme.colors]
|
||||
);
|
||||
|
||||
const columns = useMemo(
|
||||
() =>
|
||||
getColumns({
|
||||
getTableTabColumns({
|
||||
browserFields,
|
||||
eventId,
|
||||
scopeId,
|
||||
getLinkValue,
|
||||
ruleId,
|
||||
isPreview,
|
||||
onTogglePinned,
|
||||
}),
|
||||
[browserFields, eventId, scopeId, getLinkValue, ruleId, isPreview]
|
||||
[browserFields, eventId, scopeId, getLinkValue, ruleId, isPreview, onTogglePinned]
|
||||
);
|
||||
|
||||
const search = useMemo(() => {
|
||||
return { ...SEARCH_CONFIG, toolsRight: renderToolsRight() };
|
||||
}, [renderToolsRight]);
|
||||
|
||||
return (
|
||||
<EuiInMemoryTable
|
||||
items={items}
|
||||
|
|
|
@ -0,0 +1,134 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
import { EuiButtonIcon, EuiText, type EuiBasicTableColumn } from '@elastic/eui';
|
||||
import type { BrowserFields, TimelineEventsDetailsItem } from '@kbn/timelines-plugin/common';
|
||||
import { getFieldFromBrowserField } from '../tabs/table_tab';
|
||||
import { TableFieldNameCell } from '../components/table_field_name_cell';
|
||||
import { TableFieldValueCell } from '../components/table_field_value_cell';
|
||||
import type { EventFieldsData } from '../../../../common/components/event_details/types';
|
||||
import { CellActions } from '../../shared/components/cell_actions';
|
||||
import { FLYOUT_TABLE_PIN_ACTION_TEST_ID } from '../components/test_ids';
|
||||
|
||||
export const FIELD = i18n.translate('xpack.securitySolution.flyout.table.fieldCellLabel', {
|
||||
defaultMessage: 'Field',
|
||||
});
|
||||
const VALUE = i18n.translate('xpack.securitySolution.flyout.table.valueCellLabel', {
|
||||
defaultMessage: 'Value',
|
||||
});
|
||||
|
||||
const PIN = i18n.translate('xpack.securitySolution.flyout.table.pinCellLabel', {
|
||||
defaultMessage: 'Pin',
|
||||
});
|
||||
|
||||
const UNPIN = i18n.translate('xpack.securitySolution.flyout.table.unpinCellLabel', {
|
||||
defaultMessage: 'Unpin',
|
||||
});
|
||||
|
||||
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;
|
||||
/**
|
||||
* Id of the rule
|
||||
*/
|
||||
ruleId: string;
|
||||
/**
|
||||
* Whether the preview link is in preview mode
|
||||
*/
|
||||
isPreview: boolean;
|
||||
/**
|
||||
* Value of the link field if it exists. Allows to navigate to other pages like host, user, network...
|
||||
*/
|
||||
getLinkValue: (field: string) => string | null;
|
||||
/**
|
||||
* Function to toggle pinned fields
|
||||
*/
|
||||
onTogglePinned: (field: string, action: 'pin' | 'unpin') => void;
|
||||
}) => Array<EuiBasicTableColumn<TimelineEventsDetailsItem>>;
|
||||
|
||||
/**
|
||||
* Returns the columns for the table tab
|
||||
*/
|
||||
export const getTableTabColumns: ColumnsProvider = ({
|
||||
browserFields,
|
||||
eventId,
|
||||
scopeId,
|
||||
getLinkValue,
|
||||
ruleId,
|
||||
isPreview,
|
||||
onTogglePinned,
|
||||
}) => [
|
||||
{
|
||||
name: ' ',
|
||||
field: 'isPinned',
|
||||
render: (isPinned: boolean, data: TimelineEventsDetailsItem) => {
|
||||
return (
|
||||
<EuiButtonIcon
|
||||
aria-label={isPinned ? UNPIN : PIN}
|
||||
className={isPinned ? 'flyout_table__unPinAction' : 'flyout_table__pinAction'}
|
||||
iconType={isPinned ? 'pinFilled' : 'pin'}
|
||||
color="text"
|
||||
iconSize="m"
|
||||
onClick={() => {
|
||||
onTogglePinned(data.field, isPinned ? 'unpin' : 'pin');
|
||||
}}
|
||||
data-test-subj={FLYOUT_TABLE_PIN_ACTION_TEST_ID}
|
||||
/>
|
||||
);
|
||||
},
|
||||
width: '32px',
|
||||
},
|
||||
{
|
||||
field: 'field',
|
||||
name: (
|
||||
<EuiText size="xs">
|
||||
<strong>{FIELD}</strong>
|
||||
</EuiText>
|
||||
),
|
||||
width: '30%',
|
||||
render: (field, data) => {
|
||||
return <TableFieldNameCell dataType={(data as EventFieldsData).type} field={field} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'values',
|
||||
name: (
|
||||
<EuiText size="xs">
|
||||
<strong>{VALUE}</strong>
|
||||
</EuiText>
|
||||
),
|
||||
render: (values, data) => {
|
||||
const fieldFromBrowserField = getFieldFromBrowserField(data.field, browserFields);
|
||||
return (
|
||||
<CellActions field={data.field} value={values} isObjectArray={data.isObjectArray}>
|
||||
<TableFieldValueCell
|
||||
scopeId={scopeId}
|
||||
data={data as EventFieldsData}
|
||||
eventId={eventId}
|
||||
fieldFromBrowserField={fieldFromBrowserField}
|
||||
getLinkValue={getLinkValue}
|
||||
ruleId={ruleId}
|
||||
isPreview={isPreview}
|
||||
values={values}
|
||||
/>
|
||||
</CellActions>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
|
@ -0,0 +1,238 @@
|
|||
/*
|
||||
* 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 { getTableItems } from './table_tab_utils';
|
||||
import type { TimelineEventsDetailsItem } from '@kbn/timelines-plugin/common';
|
||||
|
||||
const testData = [
|
||||
{
|
||||
field: 'b',
|
||||
values: ['valueb'],
|
||||
},
|
||||
{
|
||||
field: 'kibana.alert.rule.name',
|
||||
values: ['rule name'],
|
||||
},
|
||||
{
|
||||
field: 'a',
|
||||
values: ['valuea'],
|
||||
},
|
||||
{
|
||||
field: 'empty',
|
||||
values: [''],
|
||||
},
|
||||
];
|
||||
const result = {
|
||||
a: {
|
||||
name: 'fieldA',
|
||||
field: 'a',
|
||||
values: ['valuea'],
|
||||
valuesConcatenated: 'valuea',
|
||||
isPinned: false,
|
||||
ariaRowindex: 1,
|
||||
},
|
||||
b: {
|
||||
name: 'fieldB',
|
||||
field: 'b',
|
||||
values: ['valueb'],
|
||||
valuesConcatenated: 'valueb',
|
||||
isPinned: false,
|
||||
ariaRowindex: 2,
|
||||
},
|
||||
empty: {
|
||||
name: 'emptyField',
|
||||
field: 'empty',
|
||||
values: [''],
|
||||
valuesConcatenated: '',
|
||||
isPinned: false,
|
||||
ariaRowindex: 3,
|
||||
},
|
||||
alert: {
|
||||
name: 'kibana.alert.rule.name',
|
||||
field: 'kibana.alert.rule.name',
|
||||
values: ['rule name'],
|
||||
valuesConcatenated: 'rule name',
|
||||
isPinned: false,
|
||||
ariaRowindex: 4,
|
||||
},
|
||||
};
|
||||
const mockFieldsByName = {
|
||||
a: { name: 'fieldA' },
|
||||
b: { name: 'fieldB' },
|
||||
empty: { name: 'emptyField' },
|
||||
'kibana.alert.rule.name': { name: 'kibana.alert.rule.name' },
|
||||
};
|
||||
|
||||
describe('getTableItems', () => {
|
||||
it('should return the table items in alphabetical order', () => {
|
||||
const tableItems = getTableItems({
|
||||
dataFormattedForFieldBrowser: testData as unknown as TimelineEventsDetailsItem[],
|
||||
fieldsByName: mockFieldsByName,
|
||||
highlightedFields: [],
|
||||
tableTabState: {
|
||||
pinnedFields: [],
|
||||
showHighlightedFields: false,
|
||||
hideEmptyFields: false,
|
||||
hideAlertFields: false,
|
||||
},
|
||||
});
|
||||
expect(tableItems).toEqual([result.a, result.b, result.empty, result.alert]);
|
||||
});
|
||||
|
||||
it('should return only highlighted fields if showHighlightedFields is true', () => {
|
||||
const tableItems = getTableItems({
|
||||
dataFormattedForFieldBrowser: testData as unknown as TimelineEventsDetailsItem[],
|
||||
fieldsByName: mockFieldsByName,
|
||||
highlightedFields: ['a', 'b'],
|
||||
tableTabState: {
|
||||
pinnedFields: [],
|
||||
showHighlightedFields: true,
|
||||
hideEmptyFields: false,
|
||||
hideAlertFields: false,
|
||||
},
|
||||
});
|
||||
expect(tableItems).toEqual([result.a, result.b]);
|
||||
});
|
||||
|
||||
it('should return pinned fields first', () => {
|
||||
const tableItems = getTableItems({
|
||||
dataFormattedForFieldBrowser: testData as unknown as TimelineEventsDetailsItem[],
|
||||
fieldsByName: mockFieldsByName,
|
||||
highlightedFields: [],
|
||||
tableTabState: {
|
||||
pinnedFields: ['kibana.alert.rule.name', 'empty'],
|
||||
showHighlightedFields: false,
|
||||
hideEmptyFields: false,
|
||||
hideAlertFields: false,
|
||||
},
|
||||
});
|
||||
expect(tableItems).toEqual([
|
||||
{ ...result.empty, isPinned: true },
|
||||
{ ...result.alert, isPinned: true },
|
||||
|
||||
result.a,
|
||||
result.b,
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return correct items when there are pinned fields and showHighlightedFields is true', () => {
|
||||
const tableItems = getTableItems({
|
||||
dataFormattedForFieldBrowser: testData as unknown as TimelineEventsDetailsItem[],
|
||||
fieldsByName: mockFieldsByName,
|
||||
highlightedFields: ['a', 'b', 'kibana.alert.rule.name'],
|
||||
tableTabState: {
|
||||
pinnedFields: ['b', 'empty'],
|
||||
showHighlightedFields: true,
|
||||
hideEmptyFields: false,
|
||||
hideAlertFields: false,
|
||||
},
|
||||
});
|
||||
expect(tableItems).toEqual([{ ...result.b, isPinned: true }, result.a, result.alert]);
|
||||
});
|
||||
|
||||
describe('hideEmptyFields', () => {
|
||||
it('should hide empty fields if hideEmptyFields is true', () => {
|
||||
const tableItems = getTableItems({
|
||||
dataFormattedForFieldBrowser: testData as unknown as TimelineEventsDetailsItem[],
|
||||
fieldsByName: mockFieldsByName,
|
||||
highlightedFields: [],
|
||||
tableTabState: {
|
||||
pinnedFields: [],
|
||||
showHighlightedFields: false,
|
||||
hideEmptyFields: true,
|
||||
hideAlertFields: false,
|
||||
},
|
||||
});
|
||||
expect(tableItems).toEqual([result.a, result.b, result.alert]);
|
||||
});
|
||||
|
||||
it('should hide empty fields correctly for highlighted fields', () => {
|
||||
const tableItems = getTableItems({
|
||||
dataFormattedForFieldBrowser: testData as unknown as TimelineEventsDetailsItem[],
|
||||
fieldsByName: mockFieldsByName,
|
||||
highlightedFields: ['a', 'empty'],
|
||||
tableTabState: {
|
||||
pinnedFields: [],
|
||||
showHighlightedFields: true,
|
||||
hideEmptyFields: true,
|
||||
hideAlertFields: false,
|
||||
},
|
||||
});
|
||||
expect(tableItems).toEqual([result.a]);
|
||||
});
|
||||
|
||||
it('should hide empty fields correctly for pinned fields', () => {
|
||||
const tableItems = getTableItems({
|
||||
dataFormattedForFieldBrowser: testData as unknown as TimelineEventsDetailsItem[],
|
||||
fieldsByName: mockFieldsByName,
|
||||
highlightedFields: [],
|
||||
tableTabState: {
|
||||
pinnedFields: ['b', 'kibana.alert.rule.name', 'empty'],
|
||||
showHighlightedFields: false,
|
||||
hideEmptyFields: true,
|
||||
hideAlertFields: false,
|
||||
},
|
||||
});
|
||||
expect(tableItems).toEqual([
|
||||
{ ...result.b, isPinned: true },
|
||||
{ ...result.alert, isPinned: true },
|
||||
result.a,
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('hideAlertFields', () => {
|
||||
it('should hide alert fields if hideAlertFields is true', () => {
|
||||
const tableItems = getTableItems({
|
||||
dataFormattedForFieldBrowser: testData as unknown as TimelineEventsDetailsItem[],
|
||||
fieldsByName: mockFieldsByName,
|
||||
highlightedFields: [],
|
||||
tableTabState: {
|
||||
pinnedFields: ['a'],
|
||||
showHighlightedFields: false,
|
||||
hideEmptyFields: false,
|
||||
hideAlertFields: true,
|
||||
},
|
||||
});
|
||||
expect(tableItems).toEqual([
|
||||
{ ...result.a, isPinned: true },
|
||||
{ ...result.b, isPinned: false },
|
||||
result.empty,
|
||||
]);
|
||||
});
|
||||
|
||||
it('should hide alert fields correctly for highlighted fields', () => {
|
||||
const tableItems = getTableItems({
|
||||
dataFormattedForFieldBrowser: testData as unknown as TimelineEventsDetailsItem[],
|
||||
fieldsByName: mockFieldsByName,
|
||||
highlightedFields: ['a', 'kibana.alert.rule.name'],
|
||||
tableTabState: {
|
||||
pinnedFields: [],
|
||||
showHighlightedFields: true,
|
||||
hideEmptyFields: false,
|
||||
hideAlertFields: true,
|
||||
},
|
||||
});
|
||||
expect(tableItems).toEqual([result.a]);
|
||||
});
|
||||
|
||||
it('should hide alert fields correctly for pinned fields', () => {
|
||||
const tableItems = getTableItems({
|
||||
dataFormattedForFieldBrowser: testData as unknown as TimelineEventsDetailsItem[],
|
||||
fieldsByName: mockFieldsByName,
|
||||
highlightedFields: [],
|
||||
tableTabState: {
|
||||
pinnedFields: ['a', 'kibana.alert.rule.name'],
|
||||
showHighlightedFields: false,
|
||||
hideEmptyFields: false,
|
||||
hideAlertFields: true,
|
||||
},
|
||||
});
|
||||
expect(tableItems).toEqual([{ ...result.a, isPinned: true }, result.b, result.empty]);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,89 @@
|
|||
/*
|
||||
* 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 { sortBy } from 'lodash/fp';
|
||||
import { ALERT_NAMESPACE } from '@kbn/rule-data-utils';
|
||||
import type { TimelineEventsDetailsItem } from '@kbn/timelines-plugin/common';
|
||||
import type { FieldSpec } from '@kbn/data-plugin/common';
|
||||
import type { TableTabState } from '../tabs/table_tab';
|
||||
|
||||
interface ItemsEntry {
|
||||
pinnedRows: TimelineEventsDetailsItem[];
|
||||
restRows: TimelineEventsDetailsItem[];
|
||||
}
|
||||
|
||||
interface GetTableItemsProps {
|
||||
/**
|
||||
* Array of data formatted for field browser
|
||||
*/
|
||||
dataFormattedForFieldBrowser: TimelineEventsDetailsItem[];
|
||||
/**
|
||||
* Object of fields by name
|
||||
*/
|
||||
fieldsByName: { [fieldName: string]: Partial<FieldSpec> };
|
||||
/**
|
||||
* Array of highlighted fields
|
||||
*/
|
||||
highlightedFields: string[];
|
||||
/**
|
||||
* Table tab state
|
||||
*/
|
||||
tableTabState: TableTabState;
|
||||
}
|
||||
|
||||
export const getTableItems: (props: GetTableItemsProps) => TimelineEventsDetailsItem[] = ({
|
||||
dataFormattedForFieldBrowser,
|
||||
fieldsByName,
|
||||
highlightedFields,
|
||||
tableTabState,
|
||||
}: GetTableItemsProps) => {
|
||||
const { pinnedFields, showHighlightedFields, hideEmptyFields, hideAlertFields } = tableTabState;
|
||||
const pinnedFieldsSet = new Set(pinnedFields);
|
||||
|
||||
const sortedFields = sortBy(['field'], dataFormattedForFieldBrowser).map((item, i) => ({
|
||||
...item,
|
||||
...fieldsByName[item.field],
|
||||
valuesConcatenated: item.values != null ? item.values.join() : '',
|
||||
ariaRowindex: i + 1,
|
||||
isPinned: pinnedFieldsSet.has(item.field),
|
||||
}));
|
||||
|
||||
const { pinnedRows, restRows } = sortedFields.reduce<ItemsEntry>(
|
||||
(acc, curField) => {
|
||||
// Hide empty fields
|
||||
if (hideEmptyFields && curField.valuesConcatenated === '') {
|
||||
return acc;
|
||||
}
|
||||
|
||||
// Hide alert fields
|
||||
if (
|
||||
hideAlertFields &&
|
||||
(curField.field.startsWith(ALERT_NAMESPACE) || curField.field.startsWith('signal.'))
|
||||
) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
// Process highlighted fields
|
||||
if (showHighlightedFields && !highlightedFields.includes(curField.field)) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
// Process pinned fields when showHighlightedFields is false
|
||||
if (curField.isPinned) {
|
||||
acc.pinnedRows.push(curField);
|
||||
} else {
|
||||
acc.restRows.push(curField);
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{
|
||||
pinnedRows: [],
|
||||
restRows: [],
|
||||
}
|
||||
);
|
||||
return [...pinnedRows, ...restRows];
|
||||
};
|
|
@ -9,4 +9,5 @@ export const FLYOUT_STORAGE_KEYS = {
|
|||
OVERVIEW_TAB_EXPANDED_SECTIONS:
|
||||
'securitySolution.documentDetailsFlyout.overviewSectionExpanded.v8.14',
|
||||
RIGHT_PANEL_SELECTED_TABS: 'securitySolution.documentDetailsFlyout.rightPanel.selectedTabs.v8.14',
|
||||
TABLE_TAB_STATE: 'securitySolution.documentDetailsFlyout.tableTabState.v8.19',
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue