mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Security Solution] Stateless FieldBrowser (#134495)
* remove redux from field browser * test added Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
a59c0482ca
commit
7649da18cf
25 changed files with 441 additions and 338 deletions
|
@ -19,7 +19,7 @@ const renderUseCreateFieldButton = (props: Partial<UseCreateFieldButtonProps> =
|
|||
renderHook<UseCreateFieldButtonProps, ReturnType<UseCreateFieldButton>>(
|
||||
() =>
|
||||
useCreateFieldButton({
|
||||
hasFieldEditPermission: true,
|
||||
isAllowed: true,
|
||||
loading: false,
|
||||
openFieldEditor: mockOpenFieldEditor,
|
||||
...props,
|
||||
|
@ -40,7 +40,7 @@ describe('useCreateFieldButton', () => {
|
|||
});
|
||||
|
||||
it('should return the undefined when user do not has edit permissions', async () => {
|
||||
const { result } = renderUseCreateFieldButton({ hasFieldEditPermission: false });
|
||||
const { result } = renderUseCreateFieldButton({ isAllowed: false });
|
||||
expect(result.current).toBeUndefined();
|
||||
});
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@ const StyledButton = styled(EuiButton)`
|
|||
`;
|
||||
|
||||
export interface UseCreateFieldButtonProps {
|
||||
hasFieldEditPermission: boolean;
|
||||
isAllowed: boolean;
|
||||
loading: boolean;
|
||||
openFieldEditor: OpenFieldEditor;
|
||||
}
|
||||
|
@ -30,7 +30,7 @@ export type UseCreateFieldButton = (
|
|||
* Returns a memoised 'CreateFieldButton' with only an 'onClick' property.
|
||||
*/
|
||||
export const useCreateFieldButton: UseCreateFieldButton = ({
|
||||
hasFieldEditPermission,
|
||||
isAllowed,
|
||||
loading,
|
||||
openFieldEditor,
|
||||
}) => {
|
||||
|
@ -52,5 +52,5 @@ export const useCreateFieldButton: UseCreateFieldButton = ({
|
|||
[loading, openFieldEditor]
|
||||
);
|
||||
|
||||
return hasFieldEditPermission ? createFieldButton : undefined;
|
||||
return isAllowed ? createFieldButton : undefined;
|
||||
};
|
||||
|
|
|
@ -34,6 +34,22 @@ let mockIndexPatternFieldEditor: Start;
|
|||
jest.mock('../../../common/lib/kibana');
|
||||
const useKibanaMock = useKibana as jest.Mocked<typeof useKibana>;
|
||||
|
||||
const defaultDataviewState: {
|
||||
missingPatterns: string[];
|
||||
selectedDataViewId: string | null;
|
||||
} = {
|
||||
missingPatterns: [],
|
||||
selectedDataViewId: 'security-solution',
|
||||
};
|
||||
const mockScopeIdSelector = jest.fn(() => defaultDataviewState);
|
||||
jest.mock('../../../common/store', () => {
|
||||
const original = jest.requireActual('../../../common/store');
|
||||
return {
|
||||
...original,
|
||||
sourcererSelectors: { scopeIdSelector: () => mockScopeIdSelector },
|
||||
};
|
||||
});
|
||||
|
||||
const mockIndexFieldsSearch = jest.fn();
|
||||
jest.mock('../../../common/containers/source/use_data_view', () => ({
|
||||
useDataView: () => ({
|
||||
|
@ -100,6 +116,7 @@ describe('useFieldBrowserOptions', () => {
|
|||
...useKibanaMock().services.application.capabilities,
|
||||
indexPatterns: { save: true },
|
||||
};
|
||||
mockScopeIdSelector.mockReturnValue(defaultDataviewState);
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
|
@ -123,6 +140,22 @@ describe('useFieldBrowserOptions', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('should return the button when a dataView is present', async () => {
|
||||
const { result } = renderUseFieldBrowserOptions();
|
||||
|
||||
expect(result.current.createFieldButton).toBeDefined();
|
||||
expect(result.current.getFieldTableColumns({ highlight: '', onHide: mockOnHide })).toHaveLength(
|
||||
5
|
||||
);
|
||||
});
|
||||
|
||||
it('should not return the button when a dataView is not present', () => {
|
||||
mockScopeIdSelector.mockReturnValue({ missingPatterns: [], selectedDataViewId: null });
|
||||
const { result } = renderUseFieldBrowserOptions();
|
||||
|
||||
expect(result.current.createFieldButton).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should call onHide when button is pressed', async () => {
|
||||
useKibanaMock().services.data.dataViews.get = () => Promise.resolve({} as DataView);
|
||||
const { result } = await renderUpdatedUseFieldBrowserOptions();
|
||||
|
|
|
@ -155,7 +155,7 @@ export const useFieldBrowserOptions: UseFieldBrowserOptions = ({
|
|||
);
|
||||
|
||||
const createFieldButton = useCreateFieldButton({
|
||||
hasFieldEditPermission,
|
||||
isAllowed: hasFieldEditPermission && !!selectedDataViewId,
|
||||
loading: !dataView,
|
||||
openFieldEditor,
|
||||
});
|
||||
|
|
|
@ -0,0 +1,141 @@
|
|||
/*
|
||||
* 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 } from '@testing-library/react';
|
||||
|
||||
import { TestProviders, mockTimelineModel } from '../../../../../common/mock';
|
||||
import { HeaderActions } from './header_actions';
|
||||
import { mockTimelines } from '../../../../../common/mock/mock_timelines_plugin';
|
||||
import {
|
||||
ColumnHeaderOptions,
|
||||
HeaderActionProps,
|
||||
TimelineTabs,
|
||||
} from '../../../../../../common/types/timeline';
|
||||
import { timelineActions } from '../../../../store/timeline';
|
||||
import { getColumnHeader } from '../column_headers/helpers';
|
||||
|
||||
jest.mock('../../../row_renderers_browser', () => ({
|
||||
StatefulRowRenderersBrowser: () => null,
|
||||
}));
|
||||
|
||||
const mockDispatch = jest.fn();
|
||||
jest.mock('react-redux', () => ({
|
||||
...jest.requireActual('react-redux'),
|
||||
useDispatch: () => mockDispatch,
|
||||
}));
|
||||
|
||||
jest.mock('../../../../../common/hooks/use_selector', () => ({
|
||||
useDeepEqualSelector: () => mockTimelineModel,
|
||||
useShallowEqualSelector: jest.fn(),
|
||||
}));
|
||||
|
||||
const fieldId = 'test-field';
|
||||
const timelineId = 'test-timeline';
|
||||
|
||||
/* eslint-disable jsx-a11y/click-events-have-key-events */
|
||||
mockTimelines.getFieldBrowser.mockImplementation(
|
||||
({
|
||||
onToggleColumn,
|
||||
onResetColumns,
|
||||
}: {
|
||||
onToggleColumn: (field: string) => void;
|
||||
onResetColumns: () => void;
|
||||
}) => (
|
||||
<div data-test-subj="mock-field-browser">
|
||||
<div data-test-subj="mock-toggle-button" onClick={() => onToggleColumn(fieldId)} />
|
||||
<div data-test-subj="mock-reset-button" onClick={onResetColumns} />
|
||||
</div>
|
||||
)
|
||||
);
|
||||
|
||||
jest.mock('../../../../../common/lib/kibana', () => ({
|
||||
useKibana: () => ({
|
||||
services: {
|
||||
timelines: { ...mockTimelines },
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
const defaultProps: HeaderActionProps = {
|
||||
browserFields: {},
|
||||
columnHeaders: [],
|
||||
isSelectAllChecked: false,
|
||||
onSelectAll: jest.fn(),
|
||||
showEventsSelect: false,
|
||||
showSelectAllCheckbox: false,
|
||||
sort: [],
|
||||
tabType: TimelineTabs.query,
|
||||
timelineId,
|
||||
width: 10,
|
||||
};
|
||||
|
||||
describe('HeaderActions', () => {
|
||||
describe('FieldBrowser', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should render the field browser', () => {
|
||||
const result = render(
|
||||
<TestProviders>
|
||||
<HeaderActions {...defaultProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
expect(result.getByTestId('mock-field-browser')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should dispatch upsertColumn when non existing column is toggled', () => {
|
||||
const result = render(
|
||||
<TestProviders>
|
||||
<HeaderActions {...defaultProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
result.getByTestId('mock-toggle-button').click();
|
||||
|
||||
expect(mockDispatch).toHaveBeenCalledWith(
|
||||
timelineActions.upsertColumn({
|
||||
column: getColumnHeader(fieldId, []),
|
||||
id: timelineId,
|
||||
index: 1,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should dispatch removeColumn when existing column is toggled', () => {
|
||||
const result = render(
|
||||
<TestProviders>
|
||||
<HeaderActions
|
||||
{...defaultProps}
|
||||
columnHeaders={[{ id: fieldId } as unknown as ColumnHeaderOptions]}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
result.getByTestId('mock-toggle-button').click();
|
||||
|
||||
expect(mockDispatch).toHaveBeenCalledWith(
|
||||
timelineActions.removeColumn({
|
||||
columnId: fieldId,
|
||||
id: timelineId,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should dispatch updateColumns when columns are reset', () => {
|
||||
const result = render(
|
||||
<TestProviders>
|
||||
<HeaderActions {...defaultProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
result.getByTestId('mock-reset-button').click();
|
||||
|
||||
expect(mockDispatch).toHaveBeenCalledWith(
|
||||
timelineActions.updateColumns({ id: timelineId, columns: mockTimelineModel.defaultColumns })
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -29,13 +29,15 @@ import {
|
|||
useGlobalFullScreen,
|
||||
useTimelineFullScreen,
|
||||
} from '../../../../../common/containers/use_full_screen';
|
||||
import { useDeepEqualSelector } from '../../../../../common/hooks/use_selector';
|
||||
import { StatefulRowRenderersBrowser } from '../../../row_renderers_browser';
|
||||
import { EventsTh, EventsThContent } from '../../styles';
|
||||
import { EventsSelect } from '../column_headers/events_select';
|
||||
import * as i18n from '../column_headers/translations';
|
||||
import { timelineActions } from '../../../../store/timeline';
|
||||
import { timelineActions, timelineSelectors } from '../../../../store/timeline';
|
||||
import { isFullScreen } from '../column_headers';
|
||||
import { useKibana } from '../../../../../common/lib/kibana';
|
||||
import { getColumnHeader } from '../column_headers/helpers';
|
||||
|
||||
const SortingColumnsContainer = styled.div`
|
||||
button {
|
||||
|
@ -92,6 +94,10 @@ const HeaderActionsComponent: React.FC<HeaderActionProps> = ({
|
|||
const { globalFullScreen, setGlobalFullScreen } = useGlobalFullScreen();
|
||||
const { timelineFullScreen, setTimelineFullScreen } = useTimelineFullScreen();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const getManageTimeline = useMemo(() => timelineSelectors.getManageTimelineById(), []);
|
||||
const { defaultColumns } = useDeepEqualSelector((state) => getManageTimeline(state, timelineId));
|
||||
|
||||
const toggleFullScreen = useCallback(() => {
|
||||
if (timelineId === TimelineId.active) {
|
||||
setTimelineFullScreen(!timelineFullScreen);
|
||||
|
@ -168,6 +174,32 @@ const HeaderActionsComponent: React.FC<HeaderActionProps> = ({
|
|||
[columnHeaders]
|
||||
);
|
||||
|
||||
const onResetColumns = useCallback(() => {
|
||||
dispatch(timelineActions.updateColumns({ id: timelineId, columns: defaultColumns }));
|
||||
}, [defaultColumns, dispatch, timelineId]);
|
||||
|
||||
const onToggleColumn = useCallback(
|
||||
(fieldId: string) => {
|
||||
if (columnHeaders.some(({ id }) => id === fieldId)) {
|
||||
dispatch(
|
||||
timelineActions.removeColumn({
|
||||
columnId: fieldId,
|
||||
id: timelineId,
|
||||
})
|
||||
);
|
||||
} else {
|
||||
dispatch(
|
||||
timelineActions.upsertColumn({
|
||||
column: getColumnHeader(fieldId, defaultColumns),
|
||||
id: timelineId,
|
||||
index: 1,
|
||||
})
|
||||
);
|
||||
}
|
||||
},
|
||||
[columnHeaders, dispatch, timelineId, defaultColumns]
|
||||
);
|
||||
|
||||
const ColumnSorting = useDataGridColumnSorting(myColumns, sortedColumns, {}, [], displayValues);
|
||||
|
||||
return (
|
||||
|
@ -190,7 +222,8 @@ const HeaderActionsComponent: React.FC<HeaderActionProps> = ({
|
|||
{timelinesUi.getFieldBrowser({
|
||||
browserFields,
|
||||
columnHeaders,
|
||||
timelineId,
|
||||
onResetColumns,
|
||||
onToggleColumn,
|
||||
options: fieldBrowserOptions,
|
||||
})}
|
||||
</FieldBrowserContainer>
|
||||
|
|
|
@ -11,7 +11,12 @@ import { BrowserFields } from '../../../../../../common/search_strategy';
|
|||
import { ColumnHeaderOptions } from '../../../../../../common/types';
|
||||
import { DEFAULT_COLUMN_MIN_WIDTH, DEFAULT_DATE_COLUMN_MIN_WIDTH } from '../constants';
|
||||
import { defaultHeaders } from './default_headers';
|
||||
import { getColumnWidthFromType, getColumnHeaders, getRootCategory } from './helpers';
|
||||
import {
|
||||
getColumnWidthFromType,
|
||||
getColumnHeaders,
|
||||
getRootCategory,
|
||||
getColumnHeader,
|
||||
} from './helpers';
|
||||
|
||||
describe('helpers', () => {
|
||||
describe('getColumnWidthFromType', () => {
|
||||
|
@ -50,6 +55,40 @@ describe('helpers', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('getColumnHeader', () => {
|
||||
test('it should return column header non existing in defaultHeaders', () => {
|
||||
const field = 'test_field_1';
|
||||
|
||||
expect(getColumnHeader(field, [])).toEqual({
|
||||
columnHeaderType: 'not-filtered',
|
||||
id: field,
|
||||
initialWidth: DEFAULT_COLUMN_MIN_WIDTH,
|
||||
});
|
||||
});
|
||||
|
||||
test('it should return column header existing in defaultHeaders', () => {
|
||||
const field = 'test_field_1';
|
||||
|
||||
expect(
|
||||
getColumnHeader(field, [
|
||||
{
|
||||
columnHeaderType: 'not-filtered',
|
||||
id: field,
|
||||
initialWidth: DEFAULT_DATE_COLUMN_MIN_WIDTH,
|
||||
esTypes: ['date'],
|
||||
type: 'date',
|
||||
},
|
||||
])
|
||||
).toEqual({
|
||||
columnHeaderType: 'not-filtered',
|
||||
id: field,
|
||||
initialWidth: DEFAULT_DATE_COLUMN_MIN_WIDTH,
|
||||
esTypes: ['date'],
|
||||
type: 'date',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getColumnHeaders', () => {
|
||||
test('should return a full object of ColumnHeader from the default header', () => {
|
||||
const expectedData = [
|
||||
|
|
|
@ -10,6 +10,7 @@ import { ColumnHeaderOptions } from '../../../../../../common/types';
|
|||
|
||||
import { BrowserFields } from '../../../../../common/containers/source';
|
||||
import { DEFAULT_COLUMN_MIN_WIDTH, DEFAULT_DATE_COLUMN_MIN_WIDTH } from '../constants';
|
||||
import { defaultColumnHeaderType } from './default_headers';
|
||||
|
||||
/**
|
||||
* Returns the root category for fields that are only one level, e.g. `_id` or `test_field_1`
|
||||
|
@ -50,3 +51,16 @@ export const getColumnHeaders = (
|
|||
|
||||
export const getColumnWidthFromType = (type: string): number =>
|
||||
type !== 'date' ? DEFAULT_COLUMN_MIN_WIDTH : DEFAULT_DATE_COLUMN_MIN_WIDTH;
|
||||
|
||||
/**
|
||||
* Returns the column header with field details from the defaultHeaders
|
||||
*/
|
||||
export const getColumnHeader = (
|
||||
fieldName: string,
|
||||
defaultHeaders: ColumnHeaderOptions[]
|
||||
): ColumnHeaderOptions => ({
|
||||
columnHeaderType: defaultColumnHeaderType,
|
||||
id: fieldName,
|
||||
initialWidth: DEFAULT_COLUMN_MIN_WIDTH,
|
||||
...(defaultHeaders.find((c) => c.id === fieldName) ?? {}),
|
||||
});
|
||||
|
|
|
@ -38,14 +38,16 @@ export interface FieldBrowserOptions {
|
|||
}
|
||||
|
||||
export interface FieldBrowserProps {
|
||||
/** The timeline associated with this field browser */
|
||||
timelineId: string;
|
||||
/** The timeline's current column headers */
|
||||
columnHeaders: ColumnHeaderOptions[];
|
||||
/** A map of categoryId -> metadata about the fields in that category */
|
||||
browserFields: BrowserFields;
|
||||
/** When true, this Fields Browser is being used as an "events viewer" */
|
||||
isEventViewer?: boolean;
|
||||
/** Callback to reset the default columns */
|
||||
onResetColumns: () => void;
|
||||
/** Callback to toggle a field column */
|
||||
onToggleColumn: (fieldId: string) => void;
|
||||
/** The options to customize the field browser, supporting columns rendering and button to create fields */
|
||||
options?: FieldBrowserOptions;
|
||||
/** The width of the field browser */
|
||||
|
|
|
@ -5,12 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import type { Store } from 'redux';
|
||||
import { Provider } from 'react-redux';
|
||||
import { I18nProvider } from '@kbn/i18n-react';
|
||||
import { FieldBrowser } from '../t_grid/toolbar/field_browser';
|
||||
import { FieldBrowserProps } from '../../../common/types/field_browser';
|
||||
export type {
|
||||
CreateFieldComponent,
|
||||
FieldBrowserOptions,
|
||||
|
@ -18,28 +13,5 @@ export type {
|
|||
GetFieldTableColumns,
|
||||
} from '../../../common/types/field_browser';
|
||||
|
||||
const EMPTY_BROWSER_FIELDS = {};
|
||||
|
||||
export type FieldBrowserWrappedComponentProps = FieldBrowserProps & {
|
||||
store: Store;
|
||||
};
|
||||
|
||||
export const FieldBrowserWrappedComponent = (props: FieldBrowserWrappedComponentProps) => {
|
||||
const { store, ...restProps } = props;
|
||||
const fieldBrowserProps = {
|
||||
...restProps,
|
||||
browserFields: restProps.browserFields ?? EMPTY_BROWSER_FIELDS,
|
||||
};
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<I18nProvider>
|
||||
<FieldBrowser {...fieldBrowserProps} />
|
||||
</I18nProvider>
|
||||
</Provider>
|
||||
);
|
||||
};
|
||||
|
||||
FieldBrowserWrappedComponent.displayName = 'FieldBrowserWrappedComponent';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export { FieldBrowserWrappedComponent as default };
|
||||
export { FieldBrowser as default };
|
||||
|
|
|
@ -16,6 +16,7 @@ import {
|
|||
getColumnWidthFromType,
|
||||
getColumnHeaders,
|
||||
getSchema,
|
||||
getColumnHeader,
|
||||
} from './helpers';
|
||||
import {
|
||||
DEFAULT_ACTION_BUTTON_WIDTH,
|
||||
|
@ -72,6 +73,40 @@ describe('helpers', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('getColumnHeader', () => {
|
||||
test('it should return column header non existing in defaultHeaders', () => {
|
||||
const field = 'test_field_1';
|
||||
|
||||
expect(getColumnHeader(field, [])).toEqual({
|
||||
columnHeaderType: 'not-filtered',
|
||||
id: field,
|
||||
initialWidth: DEFAULT_COLUMN_MIN_WIDTH,
|
||||
});
|
||||
});
|
||||
|
||||
test('it should return column header existing in defaultHeaders', () => {
|
||||
const field = 'test_field_1';
|
||||
|
||||
expect(
|
||||
getColumnHeader(field, [
|
||||
{
|
||||
columnHeaderType: 'not-filtered',
|
||||
id: field,
|
||||
initialWidth: DEFAULT_DATE_COLUMN_MIN_WIDTH,
|
||||
esTypes: ['date'],
|
||||
type: 'date',
|
||||
},
|
||||
])
|
||||
).toEqual({
|
||||
columnHeaderType: 'not-filtered',
|
||||
id: field,
|
||||
initialWidth: DEFAULT_DATE_COLUMN_MIN_WIDTH,
|
||||
esTypes: ['date'],
|
||||
type: 'date',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getColumnHeaders', () => {
|
||||
// additional properties used by `EuiDataGrid`:
|
||||
const actions = {
|
||||
|
|
|
@ -21,6 +21,7 @@ import {
|
|||
DEFAULT_DATE_COLUMN_MIN_WIDTH,
|
||||
} from '../constants';
|
||||
import { allowSorting } from '../helpers';
|
||||
import { defaultColumnHeaderType } from './default_headers';
|
||||
|
||||
const defaultActions: EuiDataGridColumnActions = {
|
||||
showSortAsc: true,
|
||||
|
@ -117,6 +118,19 @@ export const getColumnHeaders = (
|
|||
: [];
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the column header with field details from the defaultHeaders
|
||||
*/
|
||||
export const getColumnHeader = (
|
||||
fieldName: string,
|
||||
defaultHeaders: ColumnHeaderOptions[]
|
||||
): ColumnHeaderOptions => ({
|
||||
columnHeaderType: defaultColumnHeaderType,
|
||||
id: fieldName,
|
||||
initialWidth: DEFAULT_COLUMN_MIN_WIDTH,
|
||||
...(defaultHeaders.find((c) => c.id === fieldName) ?? {}),
|
||||
});
|
||||
|
||||
export const getColumnWidthFromType = (type: string): number =>
|
||||
type !== 'date' ? DEFAULT_COLUMN_MIN_WIDTH : DEFAULT_DATE_COLUMN_MIN_WIDTH;
|
||||
|
||||
|
|
|
@ -54,7 +54,7 @@ import {
|
|||
|
||||
import type { TimelineItem, TimelineNonEcsData } from '../../../../common/search_strategy/timeline';
|
||||
|
||||
import { getColumnHeaders } from './column_headers/helpers';
|
||||
import { getColumnHeader, getColumnHeaders } from './column_headers/helpers';
|
||||
import {
|
||||
addBuildingBlockStyle,
|
||||
getEventIdToDataMapping,
|
||||
|
@ -341,7 +341,7 @@ export const BodyComponent = React.memo<StatefulBodyProps>(
|
|||
|
||||
const dispatch = useDispatch();
|
||||
const getManageTimeline = useMemo(() => tGridSelectors.getManageTimelineById(), []);
|
||||
const { queryFields, selectAll } = useDeepEqualSelector((state) =>
|
||||
const { queryFields, selectAll, defaultColumns } = useDeepEqualSelector((state) =>
|
||||
getManageTimeline(state, id)
|
||||
);
|
||||
|
||||
|
@ -449,6 +449,32 @@ export const BodyComponent = React.memo<StatefulBodyProps>(
|
|||
return (bulkActions?.customBulkActions?.length || bulkActions?.alertStatusActions) ?? true;
|
||||
}, [hasAlertsCrud, selectedCount, showCheckboxes, bulkActions]);
|
||||
|
||||
const onResetColumns = useCallback(() => {
|
||||
dispatch(tGridActions.updateColumns({ id, columns: defaultColumns }));
|
||||
}, [defaultColumns, dispatch, id]);
|
||||
|
||||
const onToggleColumn = useCallback(
|
||||
(fieldId: string) => {
|
||||
if (columnHeaders.some(({ id: columnId }) => columnId === fieldId)) {
|
||||
dispatch(
|
||||
tGridActions.removeColumn({
|
||||
columnId: fieldId,
|
||||
id,
|
||||
})
|
||||
);
|
||||
} else {
|
||||
dispatch(
|
||||
tGridActions.upsertColumn({
|
||||
column: getColumnHeader(fieldId, defaultColumns),
|
||||
id,
|
||||
index: 1,
|
||||
})
|
||||
);
|
||||
}
|
||||
},
|
||||
[columnHeaders, dispatch, id, defaultColumns]
|
||||
);
|
||||
|
||||
const alertToolbar = useMemo(
|
||||
() => (
|
||||
<EuiFlexGroup gutterSize="m" alignItems="center">
|
||||
|
@ -523,8 +549,9 @@ export const BodyComponent = React.memo<StatefulBodyProps>(
|
|||
data-test-subj="field-browser"
|
||||
browserFields={browserFields}
|
||||
options={fieldBrowserOptions}
|
||||
timelineId={id}
|
||||
columnHeaders={columnHeaders}
|
||||
onResetColumns={onResetColumns}
|
||||
onToggleColumn={onToggleColumn}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
@ -556,6 +583,8 @@ export const BodyComponent = React.memo<StatefulBodyProps>(
|
|||
indexNames,
|
||||
onAlertStatusActionSuccess,
|
||||
onAlertStatusActionFailure,
|
||||
onResetColumns,
|
||||
onToggleColumn,
|
||||
additionalBulkActions,
|
||||
refetch,
|
||||
additionalControls,
|
||||
|
|
|
@ -13,49 +13,39 @@ import { mockBrowserFields, TestProviders } from '../../../../mock';
|
|||
import { FIELD_BROWSER_WIDTH } from './helpers';
|
||||
|
||||
import { FieldBrowserComponent } from './field_browser';
|
||||
import { FieldBrowserProps } from '../../../field_browser';
|
||||
|
||||
const defaultProps: FieldBrowserProps = {
|
||||
browserFields: mockBrowserFields,
|
||||
columnHeaders: [],
|
||||
onToggleColumn: jest.fn(),
|
||||
onResetColumns: jest.fn(),
|
||||
};
|
||||
|
||||
const renderComponent = (props: Partial<FieldBrowserProps> = {}) =>
|
||||
render(
|
||||
<TestProviders>
|
||||
<FieldBrowserComponent {...{ ...defaultProps, ...props }} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
describe('StatefulFieldsBrowser', () => {
|
||||
const timelineId = 'test';
|
||||
|
||||
it('should render the Fields button, which displays the fields browser on click', () => {
|
||||
const result = render(
|
||||
<TestProviders>
|
||||
<FieldBrowserComponent
|
||||
browserFields={mockBrowserFields}
|
||||
columnHeaders={[]}
|
||||
timelineId={timelineId}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
const result = renderComponent();
|
||||
|
||||
expect(result.getByTestId('show-field-browser')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe('toggleShow', () => {
|
||||
it('should NOT render the fields browser until the Fields button is clicked', () => {
|
||||
const result = render(
|
||||
<TestProviders>
|
||||
<FieldBrowserComponent
|
||||
browserFields={mockBrowserFields}
|
||||
columnHeaders={[]}
|
||||
timelineId={timelineId}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
const result = renderComponent();
|
||||
|
||||
expect(result.queryByTestId('fields-browser-container')).toBeNull();
|
||||
});
|
||||
|
||||
it('should render the fields browser when the Fields button is clicked', async () => {
|
||||
const result = render(
|
||||
<TestProviders>
|
||||
<FieldBrowserComponent
|
||||
browserFields={mockBrowserFields}
|
||||
columnHeaders={[]}
|
||||
timelineId={timelineId}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
const result = renderComponent();
|
||||
|
||||
result.getByTestId('show-field-browser').click();
|
||||
await waitFor(() => {
|
||||
expect(result.getByTestId('fields-browser-container')).toBeInTheDocument();
|
||||
|
@ -65,15 +55,7 @@ describe('StatefulFieldsBrowser', () => {
|
|||
|
||||
describe('updateSelectedCategoryIds', () => {
|
||||
it('should add a selected category, which creates the category badge', async () => {
|
||||
const result = render(
|
||||
<TestProviders>
|
||||
<FieldBrowserComponent
|
||||
browserFields={mockBrowserFields}
|
||||
columnHeaders={[]}
|
||||
timelineId={timelineId}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
const result = renderComponent();
|
||||
|
||||
result.getByTestId('show-field-browser').click();
|
||||
await waitFor(() => {
|
||||
|
@ -91,15 +73,7 @@ describe('StatefulFieldsBrowser', () => {
|
|||
});
|
||||
|
||||
it('should remove a selected category, which deletes the category badge', async () => {
|
||||
const result = render(
|
||||
<TestProviders>
|
||||
<FieldBrowserComponent
|
||||
browserFields={mockBrowserFields}
|
||||
columnHeaders={[]}
|
||||
timelineId={timelineId}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
const result = renderComponent();
|
||||
|
||||
result.getByTestId('show-field-browser').click();
|
||||
await waitFor(() => {
|
||||
|
@ -121,15 +95,7 @@ describe('StatefulFieldsBrowser', () => {
|
|||
});
|
||||
|
||||
it('should update the available categories according to the search input', async () => {
|
||||
const result = render(
|
||||
<TestProviders>
|
||||
<FieldBrowserComponent
|
||||
browserFields={mockBrowserFields}
|
||||
columnHeaders={[]}
|
||||
timelineId={timelineId}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
const result = renderComponent();
|
||||
|
||||
result.getByTestId('show-field-browser').click();
|
||||
await waitFor(() => {
|
||||
|
@ -149,17 +115,7 @@ describe('StatefulFieldsBrowser', () => {
|
|||
|
||||
it('should render the Fields Browser button as a settings gear when the isEventViewer prop is true', () => {
|
||||
const isEventViewer = true;
|
||||
|
||||
const result = render(
|
||||
<TestProviders>
|
||||
<FieldBrowserComponent
|
||||
browserFields={mockBrowserFields}
|
||||
columnHeaders={[]}
|
||||
isEventViewer={isEventViewer}
|
||||
timelineId={timelineId}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
const result = renderComponent({ isEventViewer });
|
||||
|
||||
expect(result.getByTestId('show-field-browser')).toBeInTheDocument();
|
||||
});
|
||||
|
@ -167,18 +123,7 @@ describe('StatefulFieldsBrowser', () => {
|
|||
it('should render the Fields Browser button as a settings gear when the isEventViewer prop is false', () => {
|
||||
const isEventViewer = false;
|
||||
|
||||
const result = render(
|
||||
<TestProviders>
|
||||
<FieldBrowserComponent
|
||||
browserFields={mockBrowserFields}
|
||||
columnHeaders={[]}
|
||||
isEventViewer={isEventViewer}
|
||||
timelineId={timelineId}
|
||||
width={FIELD_BROWSER_WIDTH}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
const result = renderComponent({ isEventViewer, width: FIELD_BROWSER_WIDTH });
|
||||
expect(result.getByTestId('show-field-browser')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -31,9 +31,10 @@ FieldBrowserButtonContainer.displayName = 'FieldBrowserButtonContainer';
|
|||
* Manages the state of the field browser
|
||||
*/
|
||||
export const FieldBrowserComponent: React.FC<FieldBrowserProps> = ({
|
||||
timelineId,
|
||||
columnHeaders,
|
||||
browserFields,
|
||||
onResetColumns,
|
||||
onToggleColumn,
|
||||
options,
|
||||
width,
|
||||
}) => {
|
||||
|
@ -149,13 +150,14 @@ export const FieldBrowserComponent: React.FC<FieldBrowserProps> = ({
|
|||
setSelectedCategoryIds={setSelectedCategoryIds}
|
||||
onFilterSelectedChange={onFilterSelectedChange}
|
||||
onHide={onHide}
|
||||
onResetColumns={onResetColumns}
|
||||
onSearchInputChange={updateFilter}
|
||||
onToggleColumn={onToggleColumn}
|
||||
options={options}
|
||||
restoreFocusTo={customizeColumnsButtonRef}
|
||||
searchInput={filterInput}
|
||||
appliedFilterInput={appliedFilterInput}
|
||||
selectedCategoryIds={selectedCategoryIds}
|
||||
timelineId={timelineId}
|
||||
width={width}
|
||||
/>
|
||||
)}
|
||||
|
|
|
@ -9,24 +9,12 @@ import { mount } from 'enzyme';
|
|||
import React from 'react';
|
||||
|
||||
import { TestProviders, mockBrowserFields, defaultHeaders } from '../../../../mock';
|
||||
import { mockGlobalState } from '../../../../mock/global_state';
|
||||
import { tGridActions } from '../../../../store/t_grid';
|
||||
|
||||
import { FieldBrowserModal, FieldBrowserModalProps } from './field_browser_modal';
|
||||
|
||||
import { createStore, State } from '../../../../types';
|
||||
import { createSecuritySolutionStorageMock } from '../../../../mock/mock_local_storage';
|
||||
const mockOnHide = jest.fn();
|
||||
const mockOnToggleColumn = jest.fn();
|
||||
const mockOnResetColumns = jest.fn();
|
||||
|
||||
const mockDispatch = jest.fn();
|
||||
jest.mock('react-redux', () => {
|
||||
const original = jest.requireActual('react-redux');
|
||||
return {
|
||||
...original,
|
||||
useDispatch: () => mockDispatch,
|
||||
};
|
||||
});
|
||||
const timelineId = 'test';
|
||||
const onHide = jest.fn();
|
||||
const testProps: FieldBrowserModalProps = {
|
||||
columnHeaders: [],
|
||||
filteredBrowserFields: mockBrowserFields,
|
||||
|
@ -34,15 +22,15 @@ const testProps: FieldBrowserModalProps = {
|
|||
appliedFilterInput: '',
|
||||
isSearching: false,
|
||||
setSelectedCategoryIds: jest.fn(),
|
||||
onHide,
|
||||
onHide: mockOnHide,
|
||||
onResetColumns: mockOnResetColumns,
|
||||
onSearchInputChange: jest.fn(),
|
||||
onToggleColumn: mockOnToggleColumn,
|
||||
restoreFocusTo: React.createRef<HTMLButtonElement>(),
|
||||
selectedCategoryIds: [],
|
||||
timelineId,
|
||||
filterSelectedEnabled: false,
|
||||
onFilterSelectedChange: jest.fn(),
|
||||
};
|
||||
const { storage } = createSecuritySolutionStorageMock();
|
||||
|
||||
describe('FieldBrowserModal', () => {
|
||||
beforeEach(() => {
|
||||
|
@ -67,7 +55,7 @@ describe('FieldBrowserModal', () => {
|
|||
);
|
||||
|
||||
wrapper.find('[data-test-subj="close"]').first().simulate('click');
|
||||
expect(onHide).toBeCalled();
|
||||
expect(mockOnHide).toBeCalled();
|
||||
});
|
||||
|
||||
test('it renders the Reset Fields button', () => {
|
||||
|
@ -80,7 +68,7 @@ describe('FieldBrowserModal', () => {
|
|||
expect(wrapper.find('[data-test-subj="reset-fields"]').first().text()).toEqual('Reset Fields');
|
||||
});
|
||||
|
||||
test('it invokes updateColumns action when the user clicks the Reset Fields button', () => {
|
||||
test('it invokes onResetColumns callback when the user clicks the Reset Fields button', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<FieldBrowserModal {...testProps} columnHeaders={defaultHeaders} />
|
||||
|
@ -88,13 +76,7 @@ describe('FieldBrowserModal', () => {
|
|||
);
|
||||
|
||||
wrapper.find('[data-test-subj="reset-fields"]').first().simulate('click');
|
||||
|
||||
expect(mockDispatch).toBeCalledWith(
|
||||
tGridActions.updateColumns({
|
||||
id: timelineId,
|
||||
columns: defaultHeaders,
|
||||
})
|
||||
);
|
||||
expect(mockOnResetColumns).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('it invokes onHide when the user clicks the Reset Fields button', () => {
|
||||
|
@ -106,7 +88,7 @@ describe('FieldBrowserModal', () => {
|
|||
|
||||
wrapper.find('[data-test-subj="reset-fields"]').first().simulate('click');
|
||||
|
||||
expect(onHide).toBeCalled();
|
||||
expect(mockOnHide).toBeCalled();
|
||||
});
|
||||
|
||||
test('it renders the search', () => {
|
||||
|
@ -173,7 +155,7 @@ describe('FieldBrowserModal', () => {
|
|||
expect(onSearchInputChange).toBeCalledWith(inputText);
|
||||
});
|
||||
|
||||
test('does not render the CreateFieldButton when it is provided but does not have a dataViewId', () => {
|
||||
test('it renders the CreateFieldButton when it is provided', () => {
|
||||
const MyTestComponent = () => <div>{'test'}</div>;
|
||||
|
||||
const wrapper = mount(
|
||||
|
@ -187,35 +169,6 @@ describe('FieldBrowserModal', () => {
|
|||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(wrapper.find(MyTestComponent).exists()).toBeFalsy();
|
||||
});
|
||||
|
||||
test('it renders the CreateFieldButton when it is provided and have a dataViewId', () => {
|
||||
const state: State = {
|
||||
...mockGlobalState,
|
||||
timelineById: {
|
||||
...mockGlobalState.timelineById,
|
||||
test: {
|
||||
...mockGlobalState.timelineById.test,
|
||||
dataViewId: 'security-solution-default',
|
||||
},
|
||||
},
|
||||
};
|
||||
const store = createStore(state, storage);
|
||||
|
||||
const MyTestComponent = () => <div>{'test'}</div>;
|
||||
|
||||
const wrapper = mount(
|
||||
<TestProviders store={store}>
|
||||
<FieldBrowserModal
|
||||
{...testProps}
|
||||
options={{
|
||||
createFieldButton: MyTestComponent,
|
||||
}}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(wrapper.find(MyTestComponent).exists()).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -17,23 +17,23 @@ import {
|
|||
EuiButtonEmpty,
|
||||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import React, { useCallback } from 'react';
|
||||
|
||||
import type { BrowserFields } from '../../../../../common/search_strategy';
|
||||
import type { FieldBrowserProps, ColumnHeaderOptions } from '../../../../../common/types';
|
||||
import { Search } from './search';
|
||||
|
||||
import { CLOSE_BUTTON_CLASS_NAME, FIELD_BROWSER_WIDTH, RESET_FIELDS_CLASS_NAME } from './helpers';
|
||||
import { tGridActions, tGridSelectors } from '../../../../store/t_grid';
|
||||
|
||||
import * as i18n from './translations';
|
||||
import { useDeepEqualSelector } from '../../../../hooks/use_selector';
|
||||
import { CategoriesSelector } from './categories_selector';
|
||||
import { FieldTable } from './field_table';
|
||||
import { CategoriesBadges } from './categories_badges';
|
||||
|
||||
export type FieldBrowserModalProps = Pick<FieldBrowserProps, 'timelineId' | 'width' | 'options'> & {
|
||||
export type FieldBrowserModalProps = Pick<
|
||||
FieldBrowserProps,
|
||||
'width' | 'onResetColumns' | 'onToggleColumn' | 'options'
|
||||
> & {
|
||||
/**
|
||||
* The current timeline column headers
|
||||
*/
|
||||
|
@ -93,6 +93,8 @@ const FieldBrowserModalComponent: React.FC<FieldBrowserModalProps> = ({
|
|||
filterSelectedEnabled,
|
||||
isSearching,
|
||||
onFilterSelectedChange,
|
||||
onToggleColumn,
|
||||
onResetColumns,
|
||||
setSelectedCategoryIds,
|
||||
onSearchInputChange,
|
||||
onHide,
|
||||
|
@ -100,17 +102,8 @@ const FieldBrowserModalComponent: React.FC<FieldBrowserModalProps> = ({
|
|||
restoreFocusTo,
|
||||
searchInput,
|
||||
selectedCategoryIds,
|
||||
timelineId,
|
||||
width = FIELD_BROWSER_WIDTH,
|
||||
}) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const onUpdateColumns = useCallback(
|
||||
(columns: ColumnHeaderOptions[]) =>
|
||||
dispatch(tGridActions.updateColumns({ id: timelineId, columns })),
|
||||
[dispatch, timelineId]
|
||||
);
|
||||
|
||||
const closeAndRestoreFocus = useCallback(() => {
|
||||
onHide();
|
||||
setTimeout(() => {
|
||||
|
@ -119,15 +112,10 @@ const FieldBrowserModalComponent: React.FC<FieldBrowserModalProps> = ({
|
|||
}, 0);
|
||||
}, [onHide, restoreFocusTo]);
|
||||
|
||||
const getManageTimeline = useMemo(() => tGridSelectors.getManageTimelineById(), []);
|
||||
const { dataViewId, defaultColumns } = useDeepEqualSelector((state) =>
|
||||
getManageTimeline(state, timelineId)
|
||||
);
|
||||
|
||||
const onResetColumns = useCallback(() => {
|
||||
onUpdateColumns(defaultColumns);
|
||||
const resetColumns = useCallback(() => {
|
||||
onResetColumns();
|
||||
closeAndRestoreFocus();
|
||||
}, [onUpdateColumns, closeAndRestoreFocus, defaultColumns]);
|
||||
}, [closeAndRestoreFocus, onResetColumns]);
|
||||
|
||||
/** Invoked when the user types in the input to filter the field browser */
|
||||
const onInputChange = useCallback(
|
||||
|
@ -159,7 +147,6 @@ const FieldBrowserModalComponent: React.FC<FieldBrowserModalProps> = ({
|
|||
isSearching={isSearching}
|
||||
onSearchInputChange={onInputChange}
|
||||
searchInput={searchInput}
|
||||
timelineId={timelineId}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
|
@ -170,9 +157,7 @@ const FieldBrowserModalComponent: React.FC<FieldBrowserModalProps> = ({
|
|||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
{CreateFieldButton && dataViewId != null && dataViewId.length > 0 && (
|
||||
<CreateFieldButton onHide={onHide} />
|
||||
)}
|
||||
{CreateFieldButton && <CreateFieldButton onHide={onHide} />}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
||||
|
@ -184,13 +169,13 @@ const FieldBrowserModalComponent: React.FC<FieldBrowserModalProps> = ({
|
|||
<EuiSpacer size="l" />
|
||||
|
||||
<FieldTable
|
||||
timelineId={timelineId}
|
||||
columnHeaders={columnHeaders}
|
||||
filteredBrowserFields={filteredBrowserFields}
|
||||
filterSelectedEnabled={filterSelectedEnabled}
|
||||
searchInput={appliedFilterInput}
|
||||
selectedCategoryIds={selectedCategoryIds}
|
||||
onFilterSelectedChange={onFilterSelectedChange}
|
||||
onToggleColumn={onToggleColumn}
|
||||
getFieldTableColumns={getFieldTableColumns}
|
||||
onHide={onHide}
|
||||
/>
|
||||
|
@ -201,7 +186,7 @@ const FieldBrowserModalComponent: React.FC<FieldBrowserModalProps> = ({
|
|||
<EuiButtonEmpty
|
||||
className={RESET_FIELDS_CLASS_NAME}
|
||||
data-test-subj="reset-fields"
|
||||
onClick={onResetColumns}
|
||||
onClick={resetColumns}
|
||||
>
|
||||
{i18n.RESET_FIELDS}
|
||||
</EuiButtonEmpty>
|
||||
|
|
|
@ -29,12 +29,9 @@ import type {
|
|||
FieldTableColumns,
|
||||
GetFieldTableColumns,
|
||||
} from '../../../../../common/types';
|
||||
import { defaultColumnHeaderType } from '../../body/column_headers/default_headers';
|
||||
import { DEFAULT_COLUMN_MIN_WIDTH } from '../../body/constants';
|
||||
import { TruncatableText } from '../../../truncatable_text';
|
||||
import { FieldName } from './field_name';
|
||||
import * as i18n from './translations';
|
||||
import { getAlertColumnHeader } from './helpers';
|
||||
|
||||
const TypeIcon = styled(EuiIcon)`
|
||||
margin: 0 4px;
|
||||
|
@ -87,16 +84,6 @@ export const getFieldItems = ({
|
|||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the column header for a field
|
||||
*/
|
||||
export const getColumnHeader = (timelineId: string, fieldName: string): ColumnHeaderOptions => ({
|
||||
columnHeaderType: defaultColumnHeaderType,
|
||||
id: fieldName,
|
||||
initialWidth: DEFAULT_COLUMN_MIN_WIDTH,
|
||||
...getAlertColumnHeader(timelineId, fieldName),
|
||||
});
|
||||
|
||||
const getDefaultFieldTableColumns = (highlight: string): FieldTableColumns => [
|
||||
{
|
||||
field: 'name',
|
||||
|
|
|
@ -8,22 +8,12 @@
|
|||
import React from 'react';
|
||||
import { render, RenderResult } from '@testing-library/react';
|
||||
import { mockBrowserFields, TestProviders } from '../../../../mock';
|
||||
import { tGridActions } from '../../../../store/t_grid';
|
||||
import { defaultColumnHeaderType } from '../../body/column_headers/default_headers';
|
||||
import { DEFAULT_COLUMN_MIN_WIDTH, DEFAULT_DATE_COLUMN_MIN_WIDTH } from '../../body/constants';
|
||||
import { DEFAULT_DATE_COLUMN_MIN_WIDTH } from '../../body/constants';
|
||||
|
||||
import { ColumnHeaderOptions } from '../../../../../common';
|
||||
import { FieldTable, FieldTableProps } from './field_table';
|
||||
|
||||
const mockDispatch = jest.fn();
|
||||
jest.mock('react-redux', () => {
|
||||
const original = jest.requireActual('react-redux');
|
||||
return {
|
||||
...original,
|
||||
useDispatch: () => mockDispatch,
|
||||
};
|
||||
});
|
||||
|
||||
const timestampFieldId = '@timestamp';
|
||||
|
||||
const columnHeaders: ColumnHeaderOptions[] = [
|
||||
|
@ -39,16 +29,18 @@ const columnHeaders: ColumnHeaderOptions[] = [
|
|||
initialWidth: DEFAULT_DATE_COLUMN_MIN_WIDTH,
|
||||
},
|
||||
];
|
||||
const timelineId = 'test';
|
||||
|
||||
const mockOnToggleColumn = jest.fn();
|
||||
|
||||
const defaultProps: FieldTableProps = {
|
||||
selectedCategoryIds: [],
|
||||
columnHeaders: [],
|
||||
filteredBrowserFields: {},
|
||||
searchInput: '',
|
||||
timelineId,
|
||||
filterSelectedEnabled: false,
|
||||
onFilterSelectedChange: jest.fn(),
|
||||
onHide: jest.fn(),
|
||||
onToggleColumn: mockOnToggleColumn,
|
||||
};
|
||||
|
||||
describe('FieldTable', () => {
|
||||
|
@ -56,7 +48,7 @@ describe('FieldTable', () => {
|
|||
const defaultPageSize = 10;
|
||||
|
||||
beforeEach(() => {
|
||||
mockDispatch.mockClear();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should render empty field table', () => {
|
||||
|
@ -156,7 +148,7 @@ describe('FieldTable', () => {
|
|||
});
|
||||
|
||||
describe('selection', () => {
|
||||
it('should dispatch remove column action on field unchecked', () => {
|
||||
it('should call onToggleColumn callback when field unchecked', () => {
|
||||
const result = render(
|
||||
<TestProviders>
|
||||
<FieldTable
|
||||
|
@ -170,13 +162,11 @@ describe('FieldTable', () => {
|
|||
|
||||
result.getByTestId(`field-${timestampFieldId}-checkbox`).click();
|
||||
|
||||
expect(mockDispatch).toHaveBeenCalledTimes(1);
|
||||
expect(mockDispatch).toHaveBeenCalledWith(
|
||||
tGridActions.removeColumn({ id: timelineId, columnId: timestampFieldId })
|
||||
);
|
||||
expect(mockOnToggleColumn).toHaveBeenCalledTimes(1);
|
||||
expect(mockOnToggleColumn).toHaveBeenCalledWith(timestampFieldId);
|
||||
});
|
||||
|
||||
it('should dispatch upsert column action on field checked', () => {
|
||||
it('should call onToggleColumn callback when field checked', () => {
|
||||
const result = render(
|
||||
<TestProviders>
|
||||
<FieldTable
|
||||
|
@ -189,18 +179,8 @@ describe('FieldTable', () => {
|
|||
|
||||
result.getByTestId(`field-${timestampFieldId}-checkbox`).click();
|
||||
|
||||
expect(mockDispatch).toHaveBeenCalledTimes(1);
|
||||
expect(mockDispatch).toHaveBeenCalledWith(
|
||||
tGridActions.upsertColumn({
|
||||
id: timelineId,
|
||||
column: {
|
||||
columnHeaderType: defaultColumnHeaderType,
|
||||
id: timestampFieldId,
|
||||
initialWidth: DEFAULT_COLUMN_MIN_WIDTH,
|
||||
},
|
||||
index: 1,
|
||||
})
|
||||
);
|
||||
expect(mockOnToggleColumn).toHaveBeenCalledTimes(1);
|
||||
expect(mockOnToggleColumn).toHaveBeenCalledWith(timestampFieldId);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -241,7 +221,7 @@ describe('FieldTable', () => {
|
|||
changePage(result);
|
||||
|
||||
result.getAllByRole('checkbox').at(0)?.click();
|
||||
expect(mockDispatch).toHaveBeenCalled(); // assert some field has been selected
|
||||
expect(mockOnToggleColumn).toHaveBeenCalled(); // assert some field has been selected
|
||||
|
||||
expect(isAtFirstPage(result)).toBeFalsy();
|
||||
});
|
||||
|
|
|
@ -8,11 +8,9 @@
|
|||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { EuiInMemoryTable, Pagination, Direction } from '@elastic/eui';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { BrowserFields, ColumnHeaderOptions } from '../../../../../common';
|
||||
import { getColumnHeader, getFieldColumns, getFieldItems, isActionsColumn } from './field_items';
|
||||
import { getFieldColumns, getFieldItems, isActionsColumn } from './field_items';
|
||||
import { CATEGORY_TABLE_CLASS_NAME, TABLE_HEIGHT } from './helpers';
|
||||
import { tGridActions } from '../../../../store/t_grid';
|
||||
import type { GetFieldTableColumns } from '../../../../../common/types/field_browser';
|
||||
import { FieldTableHeader } from './field_table_header';
|
||||
|
||||
|
@ -22,7 +20,6 @@ const DEFAULT_SORTING: { field: string; direction: Direction } = {
|
|||
} as const;
|
||||
|
||||
export interface FieldTableProps {
|
||||
timelineId: string;
|
||||
columnHeaders: ColumnHeaderOptions[];
|
||||
/**
|
||||
* A map of categoryId -> metadata about the fields in that category,
|
||||
|
@ -33,6 +30,7 @@ export interface FieldTableProps {
|
|||
/** when true, show only the the selected field */
|
||||
filterSelectedEnabled: boolean;
|
||||
onFilterSelectedChange: (enabled: boolean) => void;
|
||||
onToggleColumn: (fieldId: string) => void;
|
||||
/**
|
||||
* Optional function to customize field table columns
|
||||
*/
|
||||
|
@ -71,7 +69,7 @@ const FieldTableComponent: React.FC<FieldTableProps> = ({
|
|||
searchInput,
|
||||
selectedCategoryIds,
|
||||
onFilterSelectedChange,
|
||||
timelineId,
|
||||
onToggleColumn,
|
||||
onHide,
|
||||
}) => {
|
||||
const [pageIndex, setPageIndex] = useState(0);
|
||||
|
@ -80,8 +78,6 @@ const FieldTableComponent: React.FC<FieldTableProps> = ({
|
|||
const [sortField, setSortField] = useState<string>(DEFAULT_SORTING.field);
|
||||
const [sortDirection, setSortDirection] = useState<Direction>(DEFAULT_SORTING.direction);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const fieldItems = useMemo(
|
||||
() =>
|
||||
getFieldItems({
|
||||
|
@ -92,28 +88,6 @@ const FieldTableComponent: React.FC<FieldTableProps> = ({
|
|||
[columnHeaders, filteredBrowserFields, selectedCategoryIds]
|
||||
);
|
||||
|
||||
const onToggleColumn = useCallback(
|
||||
(fieldId: string) => {
|
||||
if (columnHeaders.some(({ id }) => id === fieldId)) {
|
||||
dispatch(
|
||||
tGridActions.removeColumn({
|
||||
columnId: fieldId,
|
||||
id: timelineId,
|
||||
})
|
||||
);
|
||||
} else {
|
||||
dispatch(
|
||||
tGridActions.upsertColumn({
|
||||
column: getColumnHeader(timelineId, fieldId),
|
||||
id: timelineId,
|
||||
index: 1,
|
||||
})
|
||||
);
|
||||
}
|
||||
},
|
||||
[columnHeaders, dispatch, timelineId]
|
||||
);
|
||||
|
||||
/**
|
||||
* Pagination controls
|
||||
*/
|
||||
|
|
|
@ -8,9 +8,7 @@
|
|||
import { EuiBadge, EuiLoadingSpinner } from '@elastic/eui';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { TimelineId } from '../../../../types';
|
||||
import type { BrowserField, BrowserFields } from '../../../../../common/search_strategy';
|
||||
import { defaultHeaders } from '../../../../store/t_grid/defaults';
|
||||
import { ColumnHeaderOptions } from '../../../../../common';
|
||||
|
||||
export const LoadingSpinner = styled(EuiLoadingSpinner)`
|
||||
|
@ -149,11 +147,6 @@ export const filterSelectedBrowserFields = ({
|
|||
return result;
|
||||
};
|
||||
|
||||
export const getAlertColumnHeader = (timelineId: string, fieldId: string) =>
|
||||
timelineId === TimelineId.detectionsPage || timelineId === TimelineId.detectionsRulesDetailsPage
|
||||
? defaultHeaders.find((c) => c.id === fieldId) ?? {}
|
||||
: {};
|
||||
|
||||
export const CATEGORY_TABLE_CLASS_NAME = 'category-table';
|
||||
export const CLOSE_BUTTON_CLASS_NAME = 'close-button';
|
||||
export const RESET_FIELDS_CLASS_NAME = 'reset-fields';
|
||||
|
|
|
@ -10,18 +10,11 @@ import React from 'react';
|
|||
import { TestProviders } from '../../../../mock';
|
||||
import { Search } from './search';
|
||||
|
||||
const timelineId = 'test';
|
||||
|
||||
describe('Search', () => {
|
||||
test('it renders the field search input with the expected placeholder text when the searchInput prop is empty', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<Search
|
||||
isSearching={false}
|
||||
onSearchInputChange={jest.fn()}
|
||||
searchInput=""
|
||||
timelineId={timelineId}
|
||||
/>
|
||||
<Search isSearching={false} onSearchInputChange={jest.fn()} searchInput="" />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
|
@ -35,12 +28,7 @@ describe('Search', () => {
|
|||
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<Search
|
||||
isSearching={false}
|
||||
onSearchInputChange={jest.fn()}
|
||||
searchInput={searchInput}
|
||||
timelineId={timelineId}
|
||||
/>
|
||||
<Search isSearching={false} onSearchInputChange={jest.fn()} searchInput={searchInput} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
|
@ -50,12 +38,7 @@ describe('Search', () => {
|
|||
test('it renders the field search input with a spinner when isSearching is true', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<Search
|
||||
isSearching={true}
|
||||
onSearchInputChange={jest.fn()}
|
||||
searchInput=""
|
||||
timelineId={timelineId}
|
||||
/>
|
||||
<Search isSearching={true} onSearchInputChange={jest.fn()} searchInput="" />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
|
@ -67,12 +50,7 @@ describe('Search', () => {
|
|||
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<Search
|
||||
isSearching={false}
|
||||
onSearchInputChange={onSearchInputChange}
|
||||
searchInput=""
|
||||
timelineId={timelineId}
|
||||
/>
|
||||
<Search isSearching={false} onSearchInputChange={onSearchInputChange} searchInput="" />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
|
|
|
@ -12,22 +12,19 @@ interface Props {
|
|||
isSearching: boolean;
|
||||
onSearchInputChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
searchInput: string;
|
||||
timelineId: string;
|
||||
}
|
||||
|
||||
const inputRef = (node: HTMLInputElement | null) => node?.focus();
|
||||
|
||||
export const Search = React.memo<Props>(
|
||||
({ isSearching, onSearchInputChange, searchInput, timelineId }) => (
|
||||
<EuiFieldSearch
|
||||
data-test-subj="field-search"
|
||||
inputRef={inputRef}
|
||||
isLoading={isSearching}
|
||||
onChange={onSearchInputChange}
|
||||
placeholder={i18n.FILTER_PLACEHOLDER}
|
||||
value={searchInput}
|
||||
fullWidth
|
||||
/>
|
||||
)
|
||||
);
|
||||
export const Search = React.memo<Props>(({ isSearching, onSearchInputChange, searchInput }) => (
|
||||
<EuiFieldSearch
|
||||
data-test-subj="field-search"
|
||||
inputRef={inputRef}
|
||||
isLoading={isSearching}
|
||||
onChange={onSearchInputChange}
|
||||
placeholder={i18n.FILTER_PLACEHOLDER}
|
||||
value={searchInput}
|
||||
fullWidth
|
||||
/>
|
||||
));
|
||||
Search.displayName = 'Search';
|
||||
|
|
|
@ -74,10 +74,10 @@ export const getLoadingPanelLazy = (props: LoadingPanelProps) => {
|
|||
};
|
||||
|
||||
const FieldBrowserLazy = lazy(() => import('../components/field_browser'));
|
||||
export const getFieldBrowserLazy = (props: FieldBrowserProps, { store }: { store: Store }) => {
|
||||
export const getFieldBrowserLazy = (props: FieldBrowserProps) => {
|
||||
return (
|
||||
<Suspense fallback={<EuiLoadingSpinner />}>
|
||||
<FieldBrowserLazy {...props} store={store} />
|
||||
<FieldBrowserLazy {...props} />
|
||||
</Suspense>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -75,10 +75,7 @@ export class TimelinesPlugin implements Plugin<void, TimelinesUIStart> {
|
|||
return getLastUpdatedLazy(props);
|
||||
},
|
||||
getFieldBrowser: (props: FieldBrowserProps) => {
|
||||
return getFieldBrowserLazy(props, {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
store: this._store!,
|
||||
});
|
||||
return getFieldBrowserLazy(props);
|
||||
},
|
||||
getUseAddToTimeline: () => {
|
||||
return useAddToTimeline;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue