[Security Solution] Migrate Field Browser to TriggersActionsUi plugin (#135231)

* field browser migrated

* fix tests, skip styled-components warnings

* fix types and tests

* more test fixes

* styles migrated to emotion/react

* use eui theme

* cleaning

* rename parameter fieldId to columnId

* move files to components folder

* fix lint error

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Sergi Massaneda 2022-07-07 20:11:00 +02:00 committed by GitHub
parent 5f753ac50c
commit 0573c83ebf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
68 changed files with 1153 additions and 745 deletions

View file

@ -40,6 +40,7 @@ import { MlLocatorDefinition } from '@kbn/ml-plugin/public';
import { EuiTheme } from '@kbn/kibana-react-plugin/common';
import { MockUrlService } from '@kbn/share-plugin/common/mocks';
import { fleetMock } from '@kbn/fleet-plugin/public/mocks';
import { triggersActionsUiMock } from '@kbn/triggers-actions-ui-plugin/public/mocks';
const mockUiSettings: Record<string, unknown> = {
[DEFAULT_TIME_RANGE]: { from: 'now-15m', to: 'now', mode: 'quick' },
@ -97,6 +98,7 @@ export const createStartServicesMock = (
const locator = urlService.locators.create(new MlLocatorDefinition());
const fleet = fleetMock.createStartMock();
const unifiedSearch = unifiedSearchPluginMock.createStartContract();
const triggersActionsUi = triggersActionsUiMock.createStart();
return {
...core,
@ -161,6 +163,7 @@ export const createStartServicesMock = (
getLastUpdated: jest.fn(),
getFieldBrowser: jest.fn(),
},
triggersActionsUi,
} as unknown as StartServices;
};

View file

@ -4,12 +4,10 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
export const mockTimelines = {
getLastUpdated: jest.fn(),
getLoadingPanel: jest.fn(),
getFieldBrowser: jest.fn().mockReturnValue(<div data-test-subj="field-browser" />),
getUseDraggableKeyboardWrapper: () =>
jest.fn().mockReturnValue({
onBlur: jest.fn(),

View file

@ -0,0 +1,11 @@
/*
* 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';
export const mockTriggersActionsUi = {
getFieldBrowser: jest.fn().mockReturnValue(<div data-test-subj="field-browser" />),
};

View file

@ -10,7 +10,7 @@ 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 { mockTriggersActionsUi } from '../../../../../common/mock/mock_triggers_actions_ui_plugin';
import {
ColumnHeaderOptions,
HeaderActionProps,
@ -34,20 +34,20 @@ jest.mock('../../../../../common/hooks/use_selector', () => ({
useShallowEqualSelector: jest.fn(),
}));
const fieldId = 'test-field';
const columnId = 'test-field';
const timelineId = 'test-timeline';
/* eslint-disable jsx-a11y/click-events-have-key-events */
mockTimelines.getFieldBrowser.mockImplementation(
mockTriggersActionsUi.getFieldBrowser.mockImplementation(
({
onToggleColumn,
onResetColumns,
}: {
onToggleColumn: (field: string) => void;
onToggleColumn: (columnId: 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-toggle-button" onClick={() => onToggleColumn(columnId)} />
<div data-test-subj="mock-reset-button" onClick={onResetColumns} />
</div>
)
@ -56,7 +56,7 @@ mockTimelines.getFieldBrowser.mockImplementation(
jest.mock('../../../../../common/lib/kibana', () => ({
useKibana: () => ({
services: {
timelines: { ...mockTimelines },
triggersActionsUi: { ...mockTriggersActionsUi },
},
}),
}));
@ -99,7 +99,7 @@ describe('HeaderActions', () => {
expect(mockDispatch).toHaveBeenCalledWith(
timelineActions.upsertColumn({
column: getColumnHeader(fieldId, []),
column: getColumnHeader(columnId, []),
id: timelineId,
index: 1,
})
@ -111,7 +111,7 @@ describe('HeaderActions', () => {
<TestProviders>
<HeaderActions
{...defaultProps}
columnHeaders={[{ id: fieldId } as unknown as ColumnHeaderOptions]}
columnHeaders={[{ id: columnId } as unknown as ColumnHeaderOptions]}
/>
</TestProviders>
);
@ -119,7 +119,7 @@ describe('HeaderActions', () => {
expect(mockDispatch).toHaveBeenCalledWith(
timelineActions.removeColumn({
columnId: fieldId,
columnId,
id: timelineId,
})
);

View file

@ -90,7 +90,7 @@ const HeaderActionsComponent: React.FC<HeaderActionProps> = ({
timelineId,
fieldBrowserOptions,
}) => {
const { timelines: timelinesUi } = useKibana().services;
const { triggersActionsUi } = useKibana().services;
const { globalFullScreen, setGlobalFullScreen } = useGlobalFullScreen();
const { timelineFullScreen, setTimelineFullScreen } = useTimelineFullScreen();
const dispatch = useDispatch();
@ -179,18 +179,18 @@ const HeaderActionsComponent: React.FC<HeaderActionProps> = ({
}, [defaultColumns, dispatch, timelineId]);
const onToggleColumn = useCallback(
(fieldId: string) => {
if (columnHeaders.some(({ id }) => id === fieldId)) {
(columnId: string) => {
if (columnHeaders.some(({ id }) => id === columnId)) {
dispatch(
timelineActions.removeColumn({
columnId: fieldId,
columnId,
id: timelineId,
})
);
} else {
dispatch(
timelineActions.upsertColumn({
column: getColumnHeader(fieldId, defaultColumns),
column: getColumnHeader(columnId, defaultColumns),
id: timelineId,
index: 1,
})
@ -219,9 +219,9 @@ const HeaderActionsComponent: React.FC<HeaderActionProps> = ({
<EventsTh role="button">
<FieldBrowserContainer>
{timelinesUi.getFieldBrowser({
{triggersActionsUi.getFieldBrowser({
browserFields,
columnHeaders,
columnIds: columnHeaders.map(({ id }) => id),
onResetColumns,
onToggleColumn,
options: fieldBrowserOptions,

View file

@ -10,7 +10,6 @@ import React from 'react';
import { TestProviders, mockTimelineModel, mockTimelineData } from '../../../../../common/mock';
import { Actions, isAlert } from '.';
import { mockTimelines } from '../../../../../common/mock/mock_timelines_plugin';
import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features';
import { mockCasesContract } from '@kbn/cases-plugin/public/mocks';
import { useShallowEqualSelector } from '../../../../../common/hooks/use_selector';
@ -53,7 +52,6 @@ jest.mock('../../../../../common/lib/kibana', () => {
savedObjects: {
client: {},
},
timelines: { ...mockTimelines },
},
}),
useToasts: jest.fn().mockReturnValue({

View file

@ -25,8 +25,17 @@ import { getDefaultControlColumn } from '../control_columns';
import { testTrailingControlColumns } from '../../../../../common/mock/mock_timeline_control_columns';
import { HeaderActions } from '../actions/header_actions';
import { UseFieldBrowserOptionsProps } from '../../../fields_browser';
import { mockTriggersActionsUi } from '../../../../../common/mock/mock_triggers_actions_ui_plugin';
import { mockTimelines } from '../../../../../common/mock/mock_timelines_plugin';
jest.mock('../../../../../common/lib/kibana');
jest.mock('../../../../../common/lib/kibana', () => ({
useKibana: () => ({
services: {
timelines: mockTimelines,
triggersActionsUi: mockTriggersActionsUi,
},
}),
}));
const mockUseFieldBrowserOptions = jest.fn();
jest.mock('../../../fields_browser', () => ({

View file

@ -74,9 +74,11 @@ jest.mock('../../../../common/lib/kibana', () => {
},
timelines: {
getLastUpdated: jest.fn(),
getFieldBrowser: jest.fn(),
getUseDraggableKeyboardWrapper: () => mockUseDraggableKeyboardWrapper,
},
triggersActionsUi: {
getFieldBrowser: jest.fn(),
},
},
}),
useGetUserSavedObjectPermissions: jest.fn(),

View file

@ -71,6 +71,7 @@ jest.mock('../../../../common/lib/kibana', () => {
getFieldBrowser: jest.fn(),
getUseDraggableKeyboardWrapper: () => mockUseDraggableKeyboardWrapper,
},
triggersActionsUi: { getFieldBrowser: jest.fn() },
},
}),
useGetUserSavedObjectPermissions: jest.fn(),

View file

@ -74,10 +74,12 @@ jest.mock('../../../../common/lib/kibana', () => {
savedObjects: {
client: {},
},
triggersActionsUi: {
getFieldBrowser: jest.fn(),
},
timelines: {
getLastUpdated: jest.fn(),
getLoadingPanel: jest.fn(),
getFieldBrowser: jest.fn(),
getUseDraggableKeyboardWrapper: () =>
jest.fn().mockReturnValue({
onBlur: jest.fn(),

View file

@ -61,7 +61,6 @@ jest.mock('../../../../common/lib/kibana', () => {
timelines: {
getLastUpdated: jest.fn(),
getLoadingPanel: jest.fn(),
getFieldBrowser: jest.fn(),
getUseDraggableKeyboardWrapper: () =>
jest.fn().mockReturnValue({
onBlur: jest.fn(),

View file

@ -27,7 +27,6 @@ export type {
ControlColumnProps,
DataProvidersAnd,
DataProvider,
FieldBrowserOptions,
GenericActionRowCellRenderProps,
HeaderActionProps,
HeaderCellRender,

View file

@ -5,5 +5,4 @@
* 2.0.
*/
export * from './field_browser';
export * from './timeline';

View file

@ -7,13 +7,29 @@
import { ComponentType, JSXElementConstructor } from 'react';
import { EuiDataGridControlColumn, EuiDataGridCellValueElementProps } from '@elastic/eui';
// Temporary import from triggers-actions-ui public types, it will not be needed after alerts table migrated
import type {
FieldBrowserOptions,
CreateFieldComponent,
GetFieldTableColumns,
FieldBrowserProps,
BrowserFieldItem,
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
} from '@kbn/triggers-actions-ui-plugin/public/types';
import { OnRowSelected, SortColumnTimeline, TimelineTabs } from '..';
import { BrowserFields } from '../../../search_strategy/index_fields';
import { ColumnHeaderOptions } from '../columns';
import { TimelineItem, TimelineNonEcsData } from '../../../search_strategy';
import { Ecs } from '../../../ecs';
import { FieldBrowserOptions } from '../../field_browser';
export {
FieldBrowserOptions,
CreateFieldComponent,
GetFieldTableColumns,
FieldBrowserProps,
BrowserFieldItem,
};
export interface ActionProps {
action?: RowCellRender;
ariaRowindex: number;
@ -79,6 +95,7 @@ export interface BulkActionsProps {
customBulkActions?: CustomBulkActionProp[];
timelineId?: string;
}
export interface HeaderActionProps {
width: number;
browserFields: BrowserFields;

View file

@ -10,6 +10,6 @@
"extraPublicDirs": ["common"],
"server": true,
"ui": true,
"requiredPlugins": ["alerting", "cases", "data", "kibanaReact", "kibanaUtils"],
"requiredPlugins": ["alerting", "cases", "data", "kibanaReact", "kibanaUtils", "triggersActionsUi"],
"optionalPlugins": ["security"]
}

View file

@ -58,4 +58,3 @@ export { TGrid as default };
export * from './drag_and_drop';
export * from './last_updated';
export * from './loading';
export * from './field_browser';

View file

@ -39,6 +39,20 @@ jest.mock('react-redux', () => {
};
});
jest.mock('@kbn/kibana-react-plugin/public', () => {
const originalModule = jest.requireActual('@kbn/kibana-react-plugin/public');
return {
...originalModule,
useKibana: () => ({
services: {
triggersActionsUi: {
getFieldBrowser: jest.fn(),
},
},
}),
};
});
jest.mock('../../../hooks/use_selector', () => ({
useShallowEqualSelector: () => mockGlobalState.timelineById.test,
useDeepEqualSelector: () => mockGlobalState.timelineById.test,

View file

@ -37,6 +37,8 @@ import styled, { ThemeContext } from 'styled-components';
import { ALERT_RULE_CONSUMER, ALERT_RULE_PRODUCER } from '@kbn/rule-data-utils';
import { Filter } from '@kbn/es-query';
import type { EuiTheme } from '@kbn/kibana-react-plugin/common';
import { FieldBrowserOptions } from '@kbn/triggers-actions-ui-plugin/public';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import {
TGridCellAction,
BulkActionsProp,
@ -65,11 +67,9 @@ import {
import type { BrowserFields } from '../../../../common/search_strategy/index_fields';
import type { OnRowSelected, OnSelectAll } from '../types';
import type { FieldBrowserOptions } from '../../../../common/types';
import type { Refetch } from '../../../store/t_grid/inputs';
import { getPageRowIndex } from '../../../../common/utils/pagination';
import { StatefulEventContext } from '../../stateful_event_context';
import { FieldBrowser } from '../toolbar/field_browser';
import { tGridActions, TGridModel, tGridSelectors, TimelineState } from '../../../store/t_grid';
import { useDeepEqualSelector } from '../../../hooks/use_selector';
import { RowAction } from './row_action';
@ -79,6 +79,7 @@ import { checkBoxControlColumn } from './control_columns';
import { ViewSelection } from '../event_rendered_view/selector';
import { EventRenderedView } from '../event_rendered_view';
import { REMOVE_COLUMN } from './column_headers/translations';
import { TimelinesStartPlugins } from '../../../types';
const StatefulAlertBulkActions = lazy(() => import('../toolbar/bulk_actions/alert_bulk_actions'));
@ -337,6 +338,8 @@ export const BodyComponent = React.memo<StatefulBodyProps>(
trailingControlColumns = EMPTY_CONTROL_COLUMNS,
unit = defaultUnit,
}) => {
const { triggersActionsUi } = useKibana<TimelinesStartPlugins>().services;
const dataGridRef = useRef<EuiDataGridRefProps>(null);
const dispatch = useDispatch();
@ -454,18 +457,18 @@ export const BodyComponent = React.memo<StatefulBodyProps>(
}, [defaultColumns, dispatch, id]);
const onToggleColumn = useCallback(
(fieldId: string) => {
if (columnHeaders.some(({ id: columnId }) => columnId === fieldId)) {
(columnId: string) => {
if (columnHeaders.some(({ id: columnHeaderId }) => columnId === columnHeaderId)) {
dispatch(
tGridActions.removeColumn({
columnId: fieldId,
columnId,
id,
})
);
} else {
dispatch(
tGridActions.upsertColumn({
column: getColumnHeader(fieldId, defaultColumns),
column: getColumnHeader(columnId, defaultColumns),
id,
index: 1,
})
@ -545,14 +548,13 @@ export const BodyComponent = React.memo<StatefulBodyProps>(
) : (
<>
{additionalControls ?? null}
<FieldBrowser
data-test-subj="field-browser"
browserFields={browserFields}
options={fieldBrowserOptions}
columnHeaders={columnHeaders}
onResetColumns={onResetColumns}
onToggleColumn={onToggleColumn}
/>
{triggersActionsUi.getFieldBrowser({
browserFields,
options: fieldBrowserOptions,
columnIds: columnHeaders.map(({ id: columnId }) => columnId),
onResetColumns,
onToggleColumn,
})}
</>
)}
</>
@ -585,6 +587,7 @@ export const BodyComponent = React.memo<StatefulBodyProps>(
onAlertStatusActionFailure,
onResetColumns,
onToggleColumn,
triggersActionsUi,
additionalBulkActions,
refetch,
additionalControls,

View file

@ -23,6 +23,7 @@ import type { DocValueFields } from '../../../../common/search_strategy';
import type { BrowserFields } from '../../../../common/search_strategy/index_fields';
import {
BulkActionsProp,
FieldBrowserOptions,
TGridCellAction,
TimelineId,
TimelineTabs,
@ -42,7 +43,6 @@ import { defaultHeaders } from '../body/column_headers/default_headers';
import { buildCombinedQuery, getCombinedFilterQuery, resolverIsShowing } from '../helpers';
import { tGridActions, tGridSelectors } from '../../../store/t_grid';
import { useTimelineEvents, InspectResponse, Refetch } from '../../../container';
import { FieldBrowserOptions } from '../../field_browser';
import { StatefulBody } from '../body';
import { SELECTOR_TIMELINE_GLOBAL_CONTAINER, UpdatedFlexGroup, UpdatedFlexItem } from '../styles';
import { Sort } from '../body/sort';

View file

@ -1,102 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { i18n } from '@kbn/i18n';
export const CATEGORY = i18n.translate('xpack.timelines.fieldBrowser.categoryLabel', {
defaultMessage: 'Category',
});
export const CATEGORIES = i18n.translate('xpack.timelines.fieldBrowser.categoriesTitle', {
defaultMessage: 'Categories',
});
export const CATEGORIES_COUNT = (totalCount: number) =>
i18n.translate('xpack.timelines.fieldBrowser.categoriesCountTitle', {
values: { totalCount },
defaultMessage: '{totalCount} {totalCount, plural, =1 {category} other {categories}}',
});
export const CLOSE = i18n.translate('xpack.timelines.fieldBrowser.closeButton', {
defaultMessage: 'Close',
});
export const FIELDS_BROWSER = i18n.translate('xpack.timelines.fieldBrowser.fieldBrowserTitle', {
defaultMessage: 'Fields',
});
export const DESCRIPTION = i18n.translate('xpack.timelines.fieldBrowser.descriptionLabel', {
defaultMessage: 'Description',
});
export const DESCRIPTION_FOR_FIELD = (field: string) =>
i18n.translate('xpack.timelines.fieldBrowser.descriptionForScreenReaderOnly', {
values: {
field,
},
defaultMessage: 'Description for field {field}:',
});
export const NAME = i18n.translate('xpack.timelines.fieldBrowser.fieldName', {
defaultMessage: 'Name',
});
export const FIELD = i18n.translate('xpack.timelines.fieldBrowser.fieldLabel', {
defaultMessage: 'Field',
});
export const FIELDS = i18n.translate('xpack.timelines.fieldBrowser.fieldsTitle', {
defaultMessage: 'Fields',
});
export const FIELDS_SHOWING = i18n.translate('xpack.timelines.fieldBrowser.fieldsCountShowing', {
defaultMessage: 'Showing',
});
export const FIELDS_COUNT = (totalCount: number) =>
i18n.translate('xpack.timelines.fieldBrowser.fieldsCountTitle', {
values: { totalCount },
defaultMessage: '{totalCount, plural, =1 {field} other {fields}}',
});
export const FILTER_PLACEHOLDER = i18n.translate('xpack.timelines.fieldBrowser.filterPlaceholder', {
defaultMessage: 'Field name',
});
export const NO_FIELDS_MATCH = i18n.translate('xpack.timelines.fieldBrowser.noFieldsMatchLabel', {
defaultMessage: 'No fields match',
});
export const NO_FIELDS_MATCH_INPUT = (searchInput: string) =>
i18n.translate('xpack.timelines.fieldBrowser.noFieldsMatchInputLabel', {
defaultMessage: 'No fields match {searchInput}',
values: {
searchInput,
},
});
export const RESET_FIELDS = i18n.translate('xpack.timelines.fieldBrowser.resetFieldsLink', {
defaultMessage: 'Reset Fields',
});
export const VIEW_COLUMN = (field: string) =>
i18n.translate('xpack.timelines.fieldBrowser.viewColumnCheckboxAriaLabel', {
values: { field },
defaultMessage: 'View {field} column',
});
export const VIEW_LABEL = i18n.translate('xpack.timelines.fieldBrowser.viewLabel', {
defaultMessage: 'View',
});
export const VIEW_VALUE_SELECTED = i18n.translate('xpack.timelines.fieldBrowser.viewSelected', {
defaultMessage: 'selected',
});
export const VIEW_VALUE_ALL = i18n.translate('xpack.timelines.fieldBrowser.viewAll', {
defaultMessage: 'all',
});

View file

@ -11,7 +11,7 @@ import type { Store } from 'redux';
import type { Storage } from '@kbn/kibana-utils-plugin/public';
import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
import type { TGridProps } from '../types';
import type { LastUpdatedAtProps, LoadingPanelProps, FieldBrowserProps } from '../components';
import type { LastUpdatedAtProps, LoadingPanelProps } from '../components';
import { initialTGridState } from '../store/t_grid/reducer';
import { createStore } from '../store/t_grid';
import { TGridLoading } from '../components/t_grid/shared';
@ -72,12 +72,3 @@ export const getLoadingPanelLazy = (props: LoadingPanelProps) => {
</Suspense>
);
};
const FieldBrowserLazy = lazy(() => import('../components/field_browser'));
export const getFieldBrowserLazy = (props: FieldBrowserProps) => {
return (
<Suspense fallback={<EuiLoadingSpinner />}>
<FieldBrowserLazy {...props} />
</Suspense>
);
};

View file

@ -18,7 +18,6 @@ import { mockHoverActions } from './mock_hover_actions';
export const createTGridMocks = () => ({
getHoverActions: () => mockHoverActions,
getTGrid: () => <>{'hello grid'}</>,
getFieldBrowser: () => <div data-test-subj="field-browser" />,
getLastUpdated: (props: LastUpdatedAtProps) => <LastUpdatedAt {...props} />,
getLoadingPanel: (props: LoadingPanelProps) => <LoadingPanel {...props} />,
getUseAddToTimeline: () => useAddToTimeline,

View file

@ -10,13 +10,8 @@ import { throttle } from 'lodash';
import { Storage } from '@kbn/kibana-utils-plugin/public';
import type { CoreSetup, Plugin, CoreStart } from '@kbn/core/public';
import type { LastUpdatedAtProps, LoadingPanelProps, FieldBrowserProps } from './components';
import {
getLastUpdatedLazy,
getLoadingPanelLazy,
getTGridLazy,
getFieldBrowserLazy,
} from './methods';
import type { LastUpdatedAtProps, LoadingPanelProps } from './components';
import { getLastUpdatedLazy, getLoadingPanelLazy, getTGridLazy } from './methods';
import type { TimelinesUIStart, TGridProps, TimelinesStartPlugins } from './types';
import { tGridReducer } from './store/t_grid/reducer';
import { useDraggableKeyboardWrapper } from './components/drag_and_drop/draggable_keyboard_wrapper_hook';
@ -74,9 +69,6 @@ export class TimelinesPlugin implements Plugin<void, TimelinesUIStart> {
getLastUpdated: (props: LastUpdatedAtProps) => {
return getLastUpdatedLazy(props);
},
getFieldBrowser: (props: FieldBrowserProps) => {
return getFieldBrowserLazy(props);
},
getUseAddToTimeline: () => {
return useAddToTimeline;
},

View file

@ -11,10 +11,10 @@ import { Store } from 'redux';
import { CoreStart } from '@kbn/core/public';
import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
import { CasesUiStart } from '@kbn/cases-plugin/public';
import type { TriggersAndActionsUIPublicPluginStart as TriggersActionsStart } from '@kbn/triggers-actions-ui-plugin/public';
import type {
LastUpdatedAtProps,
LoadingPanelProps,
FieldBrowserProps,
UseDraggableKeyboardWrapper,
UseDraggableKeyboardWrapperProps,
} from './components';
@ -34,7 +34,6 @@ export interface TimelinesUIStart {
getTGridReducer: () => any;
getLoadingPanel: (props: LoadingPanelProps) => ReactElement<LoadingPanelProps>;
getLastUpdated: (props: LastUpdatedAtProps) => ReactElement<LastUpdatedAtProps>;
getFieldBrowser: (props: FieldBrowserProps) => ReactElement<FieldBrowserProps>;
getUseAddToTimeline: () => (props: UseAddToTimelineProps) => UseAddToTimeline;
getUseAddToTimelineSensor: () => (api: SensorAPI) => void;
getUseDraggableKeyboardWrapper: () => (
@ -46,6 +45,7 @@ export interface TimelinesUIStart {
export interface TimelinesStartPlugins {
data: DataPublicPluginStart;
cases: CasesUiStart;
triggersActionsUi: TriggersActionsStart;
}
export type TimelinesStartServices = CoreStart & TimelinesStartPlugins;

View file

@ -29937,26 +29937,6 @@
"xpack.timelines.clipboard.copy.to.the.clipboard": "Copier dans le presse-papiers",
"xpack.timelines.clipboard.to.the.clipboard": "dans le presse-papiers",
"xpack.timelines.copyToClipboardTooltip": "Copier dans le Presse-papiers",
"xpack.timelines.fieldBrowser.categoriesCountTitle": "{totalCount} {totalCount, plural, =1 {catégorie} other {catégories}}",
"xpack.timelines.fieldBrowser.categoriesTitle": "Catégories",
"xpack.timelines.fieldBrowser.categoryLabel": "Catégorie",
"xpack.timelines.fieldBrowser.closeButton": "Fermer",
"xpack.timelines.fieldBrowser.descriptionForScreenReaderOnly": "Description pour le champ {field} :",
"xpack.timelines.fieldBrowser.descriptionLabel": "Description",
"xpack.timelines.fieldBrowser.fieldBrowserTitle": "Champs",
"xpack.timelines.fieldBrowser.fieldLabel": "Champ",
"xpack.timelines.fieldBrowser.fieldName": "Nom",
"xpack.timelines.fieldBrowser.fieldsCountShowing": "Affichage",
"xpack.timelines.fieldBrowser.fieldsCountTitle": "{totalCount, plural, =1 {champ} other {champs}}",
"xpack.timelines.fieldBrowser.fieldsTitle": "Champs",
"xpack.timelines.fieldBrowser.filterPlaceholder": "Nom du champ",
"xpack.timelines.fieldBrowser.noFieldsMatchInputLabel": "Aucun champ ne correspond à {searchInput}",
"xpack.timelines.fieldBrowser.noFieldsMatchLabel": "Aucun champ ne correspond",
"xpack.timelines.fieldBrowser.resetFieldsLink": "Réinitialiser les champs",
"xpack.timelines.fieldBrowser.viewAll": "tous",
"xpack.timelines.fieldBrowser.viewColumnCheckboxAriaLabel": "Afficher la colonne {field}",
"xpack.timelines.fieldBrowser.viewLabel": "Afficher",
"xpack.timelines.fieldBrowser.viewSelected": "sélectionné",
"xpack.timelines.footer.autoRefreshActiveDescription": "Actualisation automatique active",
"xpack.timelines.footer.autoRefreshActiveTooltip": "Lorsque l'actualisation automatique est activée, la chronologie vous montrera les derniers événements {numberOfItems} correspondant à votre recherche.",
"xpack.timelines.footer.events": "Événements",

View file

@ -29922,26 +29922,6 @@
"xpack.timelines.clipboard.copy.to.the.clipboard": "クリップボードにコピー",
"xpack.timelines.clipboard.to.the.clipboard": "クリップボードに",
"xpack.timelines.copyToClipboardTooltip": "クリップボードにコピー",
"xpack.timelines.fieldBrowser.categoriesCountTitle": "{totalCount} {totalCount, plural, other {カテゴリ}}",
"xpack.timelines.fieldBrowser.categoriesTitle": "カテゴリー",
"xpack.timelines.fieldBrowser.categoryLabel": "カテゴリー",
"xpack.timelines.fieldBrowser.closeButton": "閉じる",
"xpack.timelines.fieldBrowser.descriptionForScreenReaderOnly": "フィールド {field} の説明:",
"xpack.timelines.fieldBrowser.descriptionLabel": "説明",
"xpack.timelines.fieldBrowser.fieldBrowserTitle": "フィールド",
"xpack.timelines.fieldBrowser.fieldLabel": "フィールド",
"xpack.timelines.fieldBrowser.fieldName": "名前",
"xpack.timelines.fieldBrowser.fieldsCountShowing": "表示中",
"xpack.timelines.fieldBrowser.fieldsCountTitle": "{totalCount, plural, other {フィールド}}",
"xpack.timelines.fieldBrowser.fieldsTitle": "フィールド",
"xpack.timelines.fieldBrowser.filterPlaceholder": "フィールド名",
"xpack.timelines.fieldBrowser.noFieldsMatchInputLabel": "{searchInput} に一致するフィールドがありません",
"xpack.timelines.fieldBrowser.noFieldsMatchLabel": "一致するフィールドがありません",
"xpack.timelines.fieldBrowser.resetFieldsLink": "フィールドをリセット",
"xpack.timelines.fieldBrowser.viewAll": "すべて",
"xpack.timelines.fieldBrowser.viewColumnCheckboxAriaLabel": "{field} 列を表示",
"xpack.timelines.fieldBrowser.viewLabel": "表示",
"xpack.timelines.fieldBrowser.viewSelected": "選択済み",
"xpack.timelines.footer.autoRefreshActiveDescription": "自動更新アクション",
"xpack.timelines.footer.autoRefreshActiveTooltip": "自動更新が有効な間、タイムラインはクエリに一致する最新の {numberOfItems} 件のイベントを表示します。",
"xpack.timelines.footer.events": "イベント",

View file

@ -29949,26 +29949,6 @@
"xpack.timelines.clipboard.copy.to.the.clipboard": "复制到剪贴板",
"xpack.timelines.clipboard.to.the.clipboard": "至剪贴板",
"xpack.timelines.copyToClipboardTooltip": "复制到剪贴板",
"xpack.timelines.fieldBrowser.categoriesCountTitle": "{totalCount} {totalCount, plural, other {个类别}}",
"xpack.timelines.fieldBrowser.categoriesTitle": "类别",
"xpack.timelines.fieldBrowser.categoryLabel": "类别",
"xpack.timelines.fieldBrowser.closeButton": "关闭",
"xpack.timelines.fieldBrowser.descriptionForScreenReaderOnly": "{field} 字段的描述:",
"xpack.timelines.fieldBrowser.descriptionLabel": "描述",
"xpack.timelines.fieldBrowser.fieldBrowserTitle": "字段",
"xpack.timelines.fieldBrowser.fieldLabel": "字段",
"xpack.timelines.fieldBrowser.fieldName": "名称",
"xpack.timelines.fieldBrowser.fieldsCountShowing": "正在显示",
"xpack.timelines.fieldBrowser.fieldsCountTitle": "{totalCount, plural, other {字段}}",
"xpack.timelines.fieldBrowser.fieldsTitle": "字段",
"xpack.timelines.fieldBrowser.filterPlaceholder": "字段名称",
"xpack.timelines.fieldBrowser.noFieldsMatchInputLabel": "没有字段匹配“{searchInput}”",
"xpack.timelines.fieldBrowser.noFieldsMatchLabel": "没有字段匹配",
"xpack.timelines.fieldBrowser.resetFieldsLink": "重置字段",
"xpack.timelines.fieldBrowser.viewAll": "全部",
"xpack.timelines.fieldBrowser.viewColumnCheckboxAriaLabel": "查看 {field} 列",
"xpack.timelines.fieldBrowser.viewLabel": "查看",
"xpack.timelines.fieldBrowser.viewSelected": "已选定",
"xpack.timelines.footer.autoRefreshActiveDescription": "自动刷新已启用",
"xpack.timelines.footer.autoRefreshActiveTooltip": "自动刷新已启用时,时间线将显示匹配查询的最近 {numberOfItems} 个事件。",
"xpack.timelines.footer.events": "事件",

View file

@ -0,0 +1,15 @@
/*
* 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 { css } from '@emotion/react';
import { UseEuiTheme } from '@elastic/eui';
export const styles = {
badgesGroup: ({ euiTheme }: { euiTheme: UseEuiTheme['euiTheme'] }) => css`
margin-top: ${euiTheme.size.xs};
min-height: 24px;
`,
};

View file

@ -7,9 +7,7 @@
import { render } from '@testing-library/react';
import React from 'react';
import { TestProviders } from '../../../../mock';
import { CategoriesBadges } from './categories_badges';
import { CategoriesBadges, CategoriesBadgesProps } from './categories_badges';
const mockSetSelectedCategoryIds = jest.fn();
const defaultProps = {
@ -17,17 +15,16 @@ const defaultProps = {
selectedCategoryIds: [],
};
const renderComponent = (props: Partial<CategoriesBadgesProps> = {}) =>
render(<CategoriesBadges {...{ ...defaultProps, ...props }} />);
describe('CategoriesBadges', () => {
beforeEach(() => {
mockSetSelectedCategoryIds.mockClear();
});
it('should render empty badges', () => {
const result = render(
<TestProviders>
<CategoriesBadges {...defaultProps} />
</TestProviders>
);
const result = renderComponent();
const badges = result.getByTestId('category-badges');
expect(badges).toBeInTheDocument();
@ -35,11 +32,7 @@ describe('CategoriesBadges', () => {
});
it('should render the selector button with selected categories', () => {
const result = render(
<TestProviders>
<CategoriesBadges {...defaultProps} selectedCategoryIds={['base', 'event']} />
</TestProviders>
);
const result = renderComponent({ selectedCategoryIds: ['base', 'event'] });
const badges = result.getByTestId('category-badges');
expect(badges.childNodes.length).toBe(2);
@ -48,11 +41,7 @@ describe('CategoriesBadges', () => {
});
it('should call the set selected callback when badge unselect button clicked', () => {
const result = render(
<TestProviders>
<CategoriesBadges {...defaultProps} selectedCategoryIds={['base', 'event']} />
</TestProviders>
);
const result = renderComponent({ selectedCategoryIds: ['base', 'event'] });
result.getByTestId('category-badge-unselect-base').click();
expect(mockSetSelectedCategoryIds).toHaveBeenCalledWith(['event']);

View file

@ -4,26 +4,20 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { useCallback } from 'react';
import styled from 'styled-components';
import { EuiBadge, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { EuiBadge, EuiFlexGroup, EuiFlexItem, useEuiTheme } from '@elastic/eui';
import { styles } from './categories_badges.styles';
interface CategoriesBadgesProps {
export interface CategoriesBadgesProps {
setSelectedCategoryIds: (categoryIds: string[]) => void;
selectedCategoryIds: string[];
}
const CategoriesBadgesGroup = styled(EuiFlexGroup)`
margin-top: ${({ theme }) => theme.eui.euiSizeXS};
min-height: 24px;
`;
CategoriesBadgesGroup.displayName = 'CategoriesBadgesGroup';
const CategoriesBadgesComponent: React.FC<CategoriesBadgesProps> = ({
setSelectedCategoryIds,
selectedCategoryIds,
}) => {
const { euiTheme } = useEuiTheme();
const onUnselectCategory = useCallback(
(categoryId: string) => {
setSelectedCategoryIds(
@ -34,7 +28,12 @@ const CategoriesBadgesComponent: React.FC<CategoriesBadgesProps> = ({
);
return (
<CategoriesBadgesGroup data-test-subj="category-badges" gutterSize="xs" wrap>
<EuiFlexGroup
css={styles.badgesGroup({ euiTheme })}
data-test-subj="category-badges"
gutterSize="xs"
wrap
>
{selectedCategoryIds.map((categoryId) => (
<EuiFlexItem grow={false} key={categoryId}>
<EuiBadge
@ -49,7 +48,7 @@ const CategoriesBadgesComponent: React.FC<CategoriesBadgesProps> = ({
</EuiBadge>
</EuiFlexItem>
))}
</CategoriesBadgesGroup>
</EuiFlexGroup>
);
};

View file

@ -0,0 +1,9 @@
/*
* 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.
*/
export { CategoriesBadges } from './categories_badges';
export type { CategoriesBadgesProps } from './categories_badges';

View file

@ -0,0 +1,19 @@
/*
* 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 { css } from '@emotion/react';
export const styles = {
countBadge: css`
margin-left: 5px;
`,
categoryName: ({ bold }: { bold: boolean }) => css`
font-weight: ${bold ? 'bold' : 'normal'};
`,
selectableContainer: css`
width: 300px;
`,
};

View file

@ -4,11 +4,9 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { render } from '@testing-library/react';
import React from 'react';
import { mockBrowserFields, TestProviders } from '../../../../mock';
import { render } from '@testing-library/react';
import { mockBrowserFields } from '../../mock';
import { CategoriesSelector } from './categories_selector';
const mockSetSelectedCategoryIds = jest.fn();
@ -25,11 +23,7 @@ describe('CategoriesSelector', () => {
it('should render the default selector button', () => {
const categoriesCount = Object.keys(mockBrowserFields).length;
const result = render(
<TestProviders>
<CategoriesSelector {...defaultProps} />
</TestProviders>
);
const result = render(<CategoriesSelector {...defaultProps} />);
expect(result.getByTestId('categories-filter-button')).toBeInTheDocument();
expect(result.getByText('Categories')).toBeInTheDocument();
@ -38,9 +32,7 @@ describe('CategoriesSelector', () => {
it('should render the selector button with selected categories', () => {
const result = render(
<TestProviders>
<CategoriesSelector {...defaultProps} selectedCategoryIds={['base', 'event']} />
</TestProviders>
<CategoriesSelector {...defaultProps} selectedCategoryIds={['base', 'event']} />
);
expect(result.getByTestId('categories-filter-button')).toBeInTheDocument();
@ -49,11 +41,7 @@ describe('CategoriesSelector', () => {
});
it('should open the category selector', () => {
const result = render(
<TestProviders>
<CategoriesSelector {...defaultProps} />
</TestProviders>
);
const result = render(<CategoriesSelector {...defaultProps} />);
result.getByTestId('categories-filter-button').click();
@ -63,27 +51,18 @@ describe('CategoriesSelector', () => {
it('should open the category selector with selected categories', () => {
const result = render(
<TestProviders>
<CategoriesSelector {...defaultProps} selectedCategoryIds={['base', 'event']} />
</TestProviders>
<CategoriesSelector {...defaultProps} selectedCategoryIds={['base', 'event']} />
);
result.getByTestId('categories-filter-button').click();
expect(result.getByTestId('categories-selector-search')).toBeInTheDocument();
expect(result.getByTestId(`categories-selector-option-base`)).toBeInTheDocument();
expect(result.getByTestId(`categories-selector-option-name-base`)).toHaveStyleRule(
'font-weight',
'bold'
);
expect(result.getByTestId(`categories-selector-option-name-base`)).toBeInTheDocument();
});
it('should call setSelectedCategoryIds when category selected', () => {
const result = render(
<TestProviders>
<CategoriesSelector {...defaultProps} />
</TestProviders>
);
const result = render(<CategoriesSelector {...defaultProps} />);
result.getByTestId('categories-filter-button').click();
result.getByTestId(`categories-selector-option-base`).click();

View file

@ -4,10 +4,10 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { useCallback, useMemo, useState } from 'react';
import { omit } from 'lodash';
import {
EuiBadge,
EuiFilterButton,
EuiFilterGroup,
EuiFlexGroup,
@ -17,10 +17,10 @@ import {
EuiSelectable,
FilterChecked,
} from '@elastic/eui';
import { BrowserFields } from '../../../../../common';
import * as i18n from './translations';
import { CountBadge, getFieldCount, CategoryName, CategorySelectableContainer } from './helpers';
import { isEscape } from '../../../../../common/utils/accessibility';
import type { BrowserFields } from '../../types';
import * as i18n from '../../translations';
import { getFieldCount, isEscape } from '../../helpers';
import { styles } from './categories_selector.styles';
interface CategoriesSelectorProps {
/**
@ -57,15 +57,15 @@ const renderOption = (option: CategoryOption, searchValue: string) => {
justifyContent="spaceBetween"
>
<EuiFlexItem grow={false}>
<CategoryName
<span
css={styles.categoryName({ bold: checked === 'on' })}
data-test-subj={`categories-selector-option-name-${idAttr}`}
bold={checked === 'on'}
>
<EuiHighlight search={searchValue}>{label}</EuiHighlight>
</CategoryName>
</span>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<CountBadge>{count}</CountBadge>
<EuiBadge css={styles.countBadge}>{count}</EuiBadge>
</EuiFlexItem>
</EuiFlexGroup>
);
@ -143,7 +143,8 @@ const CategoriesSelectorComponent: React.FC<CategoriesSelectorProps> = ({
closePopover={closePopover}
panelPaddingSize="none"
>
<CategorySelectableContainer
<div
css={styles.selectableContainer}
onKeyDown={onKeyDown}
data-test-subj="categories-selector-container"
>
@ -164,7 +165,7 @@ const CategoriesSelectorComponent: React.FC<CategoriesSelectorProps> = ({
</>
)}
</EuiSelectable>
</CategorySelectableContainer>
</div>
</EuiPopover>
</EuiFilterGroup>
);

View file

@ -0,0 +1,8 @@
/*
* 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.
*/
export { CategoriesSelector } from './categories_selector';

View file

@ -0,0 +1,31 @@
/*
* 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 { css } from '@emotion/react';
export const styles = {
icon: css`
margin: 0 4px;
position: relative;
top: -1px;
`,
truncatable: css`
&,
& * {
display: inline-block;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
vertical-align: top;
white-space: nowrap;
}
`,
description: css`
user-select: text;
width: 400px;
`,
};

View file

@ -4,32 +4,16 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { omit } from 'lodash/fp';
import { render } from '@testing-library/react';
import { EuiInMemoryTable } from '@elastic/eui';
import { mockBrowserFields } from '../../../../mock';
import { defaultColumnHeaderType } from '../../body/column_headers/default_headers';
import { DEFAULT_DATE_COLUMN_MIN_WIDTH } from '../../body/constants';
import { mockBrowserFields } from '../../mock';
import { getFieldColumns, getFieldItems } from './field_items';
import { ColumnHeaderOptions } from '../../../../../common/types';
const timestampFieldId = '@timestamp';
const columnHeaders: ColumnHeaderOptions[] = [
{
category: 'base',
columnHeaderType: defaultColumnHeaderType,
description:
'Date/time when the event originated.\nFor log events this is the date/time when the event was generated, and not when it was read.\nRequired field for all events.',
example: '2016-05-23T08:05:34.853Z',
id: timestampFieldId,
type: 'date',
aggregatable: true,
initialWidth: DEFAULT_DATE_COLUMN_MIN_WIDTH,
},
];
const columnIds = [timestampFieldId];
describe('field_items', () => {
describe('getFieldItems', () => {
@ -39,7 +23,7 @@ describe('field_items', () => {
const fieldItems = getFieldItems({
selectedCategoryIds: ['base'],
browserFields: { base: { fields: { [timestampFieldId]: timestampField } } },
columnHeaders: [],
columnIds: [],
});
expect(fieldItems[0]).toEqual({
@ -57,7 +41,7 @@ describe('field_items', () => {
const fieldItems = getFieldItems({
selectedCategoryIds: ['base'],
browserFields: { base: { fields: { [timestampFieldId]: timestampField } } },
columnHeaders,
columnIds,
});
expect(fieldItems[0]).toMatchObject({
@ -78,7 +62,7 @@ describe('field_items', () => {
},
},
},
columnHeaders,
columnIds,
});
expect(fieldItems[0]).toMatchObject({
@ -95,7 +79,7 @@ describe('field_items', () => {
const fieldItems = getFieldItems({
selectedCategoryIds: [],
browserFields: mockBrowserFields,
columnHeaders: [],
columnIds: [],
});
expect(fieldItems.length).toBe(fieldCount);
@ -112,7 +96,7 @@ describe('field_items', () => {
const fieldItems = getFieldItems({
selectedCategoryIds,
browserFields: mockBrowserFields,
columnHeaders: [],
columnIds: [],
});
expect(fieldItems.length).toBe(fieldCount);
@ -195,7 +179,7 @@ describe('field_items', () => {
const fieldItems = getFieldItems({
selectedCategoryIds: ['base'],
browserFields: { base: { fields: { [timestampFieldId]: timestampField } } },
columnHeaders: [],
columnIds: [],
});
const columns = getFieldColumns(getFieldColumnsParams);
@ -218,7 +202,7 @@ describe('field_items', () => {
const fieldItems = getFieldItems({
selectedCategoryIds: ['base'],
browserFields: { base: { fields: { [timestampFieldId]: timestampField } } },
columnHeaders: [],
columnIds: [],
});
const columns = getFieldColumns(getFieldColumnsParams);

View file

@ -4,7 +4,6 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import {
EuiCheckbox,
@ -18,33 +17,17 @@ import {
EuiTableActionsColumnType,
} from '@elastic/eui';
import { uniqBy } from 'lodash/fp';
import styled from 'styled-components';
import { getEmptyValue } from '../../../empty_value';
import { getExampleText, getIconFromType } from '../../../utils/helpers';
import type { BrowserFields } from '../../../../../common/search_strategy';
import { getEmptyValue, getExampleText, getIconFromType } from '../../helpers';
import type {
ColumnHeaderOptions,
BrowserFields,
BrowserFieldItem,
FieldTableColumns,
GetFieldTableColumns,
} from '../../../../../common/types';
import { TruncatableText } from '../../../truncatable_text';
import { FieldName } from './field_name';
import * as i18n from './translations';
const TypeIcon = styled(EuiIcon)`
margin: 0 4px;
position: relative;
top: -1px;
`;
TypeIcon.displayName = 'TypeIcon';
export const Description = styled.span`
user-select: text;
width: 400px;
`;
Description.displayName = 'Description';
} from '../../types';
import { FieldName } from '../field_name';
import * as i18n from '../../translations';
import { styles } from './field_items.style';
/**
* Returns the field items of all categories selected
@ -52,15 +35,15 @@ Description.displayName = 'Description';
export const getFieldItems = ({
browserFields,
selectedCategoryIds,
columnHeaders,
columnIds,
}: {
browserFields: BrowserFields;
selectedCategoryIds: string[];
columnHeaders: ColumnHeaderOptions[];
columnIds: string[];
}): BrowserFieldItem[] => {
const categoryIds =
selectedCategoryIds.length > 0 ? selectedCategoryIds : Object.keys(browserFields);
const selectedFieldIds = new Set(columnHeaders.map(({ id }) => id));
const selectedFieldIds = new Set(columnIds);
return uniqBy(
'name',
@ -93,8 +76,9 @@ const getDefaultFieldTableColumns = (highlight: string): FieldTableColumns => [
<EuiFlexGroup alignItems="center" gutterSize="none">
<EuiFlexItem grow={false}>
<EuiToolTip content={type}>
<TypeIcon
<EuiIcon
data-test-subj={`field-${name}-icon`}
css={styles.icon}
type={getIconFromType(type ?? null)}
/>
</EuiToolTip>
@ -118,11 +102,11 @@ const getDefaultFieldTableColumns = (highlight: string): FieldTableColumns => [
<EuiScreenReaderOnly data-test-subj="descriptionForScreenReaderOnly">
<p>{i18n.DESCRIPTION_FOR_FIELD(name)}</p>
</EuiScreenReaderOnly>
<TruncatableText>
<Description data-test-subj={`field-${name}-description`}>
<span css={styles.truncatable}>
<span css={styles.description} data-test-subj={`field-${name}-description`}>
{`${description ?? getEmptyValue()} ${getExampleText(example)}`}
</Description>
</TruncatableText>
</span>
</span>
</>
</EuiToolTip>
),

View file

@ -0,0 +1,8 @@
/*
* 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.
*/
export { getFieldItems, getFieldColumns, isActionsColumn } from './field_items';

View file

@ -7,27 +7,13 @@
import { mount } from 'enzyme';
import React from 'react';
import { mockBrowserFields, TestProviders } from '../../../../mock';
import { getColumnsWithTimestamp } from '../../../utils/helpers';
import { FieldName } from './field_name';
const categoryId = 'base';
const timestampFieldId = '@timestamp';
const defaultProps = {
categoryId,
categoryColumns: getColumnsWithTimestamp({
browserFields: mockBrowserFields,
category: categoryId,
}),
closePopOverTrigger: false,
fieldId: timestampFieldId,
handleClosePopOverTrigger: jest.fn(),
hoverActionsOwnFocus: false,
onCloseRequested: jest.fn(),
onUpdateColumns: jest.fn(),
setClosePopOverTrigger: jest.fn(),
};
describe('FieldName', () => {
@ -36,11 +22,7 @@ describe('FieldName', () => {
});
test('it renders the field name', () => {
const wrapper = mount(
<TestProviders>
<FieldName {...defaultProps} />
</TestProviders>
);
const wrapper = mount(<FieldName {...defaultProps} />);
expect(
wrapper.find(`[data-test-subj="field-${timestampFieldId}-name"]`).first().text()
@ -50,11 +32,7 @@ describe('FieldName', () => {
test('it highlights the text specified by the `highlight` prop', () => {
const highlight = 'stamp';
const wrapper = mount(
<TestProviders>
<FieldName {...{ ...defaultProps, highlight }} />
</TestProviders>
);
const wrapper = mount(<FieldName {...{ ...defaultProps, highlight }} />);
expect(wrapper.find('mark').first().text()).toEqual(highlight);
});

View file

@ -4,4 +4,5 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export { FieldBrowser } from './field_browser';
export { FieldName } from './field_name';

View file

@ -0,0 +1,23 @@
/*
* 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 { UseEuiTheme } from '@elastic/eui';
import { css } from '@emotion/react';
export const styles = {
tableContainer: ({
height,
euiTheme,
}: {
height: number;
euiTheme: UseEuiTheme['euiTheme'];
}) => css`
margin-top: ${euiTheme.size.xs};
border-top: ${euiTheme.border.thin};
height: ${height}px;
overflow: hidden;
`,
};

View file

@ -7,34 +7,19 @@
import React from 'react';
import { render, RenderResult } from '@testing-library/react';
import { mockBrowserFields, TestProviders } from '../../../../mock';
import { defaultColumnHeaderType } from '../../body/column_headers/default_headers';
import { DEFAULT_DATE_COLUMN_MIN_WIDTH } from '../../body/constants';
import { mockBrowserFields } from '../../mock';
import { ColumnHeaderOptions } from '../../../../../common';
import { FieldTable, FieldTableProps } from './field_table';
const timestampFieldId = '@timestamp';
const columnHeaders: ColumnHeaderOptions[] = [
{
category: 'base',
columnHeaderType: defaultColumnHeaderType,
description:
'Date/time when the event originated.\nFor log events this is the date/time when the event was generated, and not when it was read.\nRequired field for all events.',
example: '2016-05-23T08:05:34.853Z',
id: timestampFieldId,
type: 'date',
aggregatable: true,
initialWidth: DEFAULT_DATE_COLUMN_MIN_WIDTH,
},
];
const columnIds = [timestampFieldId];
const mockOnToggleColumn = jest.fn();
const defaultProps: FieldTableProps = {
selectedCategoryIds: [],
columnHeaders: [],
columnIds: [],
filteredBrowserFields: {},
searchInput: '',
filterSelectedEnabled: false,
@ -43,6 +28,11 @@ const defaultProps: FieldTableProps = {
onToggleColumn: mockOnToggleColumn,
};
const getComponent = (props: Partial<FieldTableProps> = {}) => (
<FieldTable {...{ ...defaultProps, ...props }} />
);
const renderComponent = (props: Partial<FieldTableProps> = {}) => render(getComponent(props));
describe('FieldTable', () => {
const timestampField = mockBrowserFields.base.fields![timestampFieldId];
const defaultPageSize = 10;
@ -52,21 +42,13 @@ describe('FieldTable', () => {
});
it('should render empty field table', () => {
const result = render(
<TestProviders>
<FieldTable {...defaultProps} />
</TestProviders>
);
const result = renderComponent();
expect(result.getByText('No items found')).toBeInTheDocument();
});
it('should render field table with fields of all categories', () => {
const result = render(
<TestProviders>
<FieldTable {...defaultProps} filteredBrowserFields={mockBrowserFields} />
</TestProviders>
);
const result = renderComponent({ filteredBrowserFields: mockBrowserFields });
expect(result.container.getElementsByClassName('euiTableRow').length).toBe(defaultPageSize);
});
@ -80,15 +62,10 @@ describe('FieldTable', () => {
0
);
const result = render(
<TestProviders>
<FieldTable
{...defaultProps}
selectedCategoryIds={selectedCategoryIds}
filteredBrowserFields={mockBrowserFields}
/>
</TestProviders>
);
const result = renderComponent({
selectedCategoryIds,
filteredBrowserFields: mockBrowserFields,
});
expect(result.container.getElementsByClassName('euiTableRow').length).toBe(fieldCount);
});
@ -102,46 +79,31 @@ describe('FieldTable', () => {
},
];
const result = render(
<TestProviders>
<FieldTable
{...defaultProps}
getFieldTableColumns={() => fieldTableColumns}
filteredBrowserFields={mockBrowserFields}
/>
</TestProviders>
);
const result = renderComponent({
getFieldTableColumns: () => fieldTableColumns,
filteredBrowserFields: mockBrowserFields,
});
expect(result.getAllByText('Custom column').length).toBeGreaterThan(0);
expect(result.getAllByTestId('customColumn').length).toEqual(defaultPageSize);
});
it('should render field table with unchecked field', () => {
const result = render(
<TestProviders>
<FieldTable
{...defaultProps}
selectedCategoryIds={['base']}
filteredBrowserFields={{ base: { fields: { [timestampFieldId]: timestampField } } }}
/>
</TestProviders>
);
const result = renderComponent({
selectedCategoryIds: ['base'],
filteredBrowserFields: { base: { fields: { [timestampFieldId]: timestampField } } },
});
const checkbox = result.getByTestId(`field-${timestampFieldId}-checkbox`);
expect(checkbox).not.toHaveAttribute('checked');
});
it('should render field table with checked field', () => {
const result = render(
<TestProviders>
<FieldTable
{...defaultProps}
selectedCategoryIds={['base']}
columnHeaders={columnHeaders}
filteredBrowserFields={{ base: { fields: { [timestampFieldId]: timestampField } } }}
/>
</TestProviders>
);
const result = renderComponent({
selectedCategoryIds: ['base'],
columnIds,
filteredBrowserFields: { base: { fields: { [timestampFieldId]: timestampField } } },
});
const checkbox = result.getByTestId(`field-${timestampFieldId}-checkbox`);
expect(checkbox).toHaveAttribute('checked');
@ -149,16 +111,11 @@ describe('FieldTable', () => {
describe('selection', () => {
it('should call onToggleColumn callback when field unchecked', () => {
const result = render(
<TestProviders>
<FieldTable
{...defaultProps}
selectedCategoryIds={['base']}
columnHeaders={columnHeaders}
filteredBrowserFields={{ base: { fields: { [timestampFieldId]: timestampField } } }}
/>
</TestProviders>
);
const result = renderComponent({
selectedCategoryIds: ['base'],
columnIds,
filteredBrowserFields: { base: { fields: { [timestampFieldId]: timestampField } } },
});
result.getByTestId(`field-${timestampFieldId}-checkbox`).click();
@ -167,15 +124,10 @@ describe('FieldTable', () => {
});
it('should call onToggleColumn callback when field checked', () => {
const result = render(
<TestProviders>
<FieldTable
{...defaultProps}
selectedCategoryIds={['base']}
filteredBrowserFields={{ base: { fields: { [timestampFieldId]: timestampField } } }}
/>
</TestProviders>
);
const result = renderComponent({
selectedCategoryIds: ['base'],
filteredBrowserFields: { base: { fields: { [timestampFieldId]: timestampField } } },
});
result.getByTestId(`field-${timestampFieldId}-checkbox`).click();
@ -192,17 +144,12 @@ describe('FieldTable', () => {
result.getByTestId('pagination-button-1').click();
};
const defaultPaginationProps: FieldTableProps = {
...defaultProps,
const paginationProps = {
filteredBrowserFields: mockBrowserFields,
};
it('should paginate on page clicked', () => {
const result = render(
<TestProviders>
<FieldTable {...defaultPaginationProps} />
</TestProviders>
);
const result = renderComponent(paginationProps);
expect(isAtFirstPage(result)).toBeTruthy();
@ -212,11 +159,7 @@ describe('FieldTable', () => {
});
it('should not reset on field checked', () => {
const result = render(
<TestProviders>
<FieldTable {...defaultPaginationProps} />
</TestProviders>
);
const result = renderComponent(paginationProps);
changePage(result);
@ -227,22 +170,19 @@ describe('FieldTable', () => {
});
it('should reset on filter change', () => {
const result = render(
<FieldTable
{...defaultPaginationProps}
selectedCategoryIds={['destination', 'event', 'client', 'agent', 'host']}
/>,
{ wrapper: TestProviders }
);
const result = renderComponent({
...paginationProps,
selectedCategoryIds: ['destination', 'event', 'client', 'agent', 'host'],
});
changePage(result);
expect(isAtFirstPage(result)).toBeFalsy();
result.rerender(
<FieldTable
{...defaultPaginationProps}
selectedCategoryIds={['destination', 'event', 'client', 'agent']}
/>
getComponent({
...paginationProps,
selectedCategoryIds: ['destination', 'event', 'client', 'agent'],
})
);
expect(isAtFirstPage(result)).toBeTruthy();

View file

@ -4,23 +4,20 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import styled from 'styled-components';
import { EuiInMemoryTable, Pagination, Direction } from '@elastic/eui';
import { BrowserFields, ColumnHeaderOptions } from '../../../../../common';
import { getFieldColumns, getFieldItems, isActionsColumn } from './field_items';
import { CATEGORY_TABLE_CLASS_NAME, TABLE_HEIGHT } from './helpers';
import type { GetFieldTableColumns } from '../../../../../common/types/field_browser';
import { EuiInMemoryTable, Pagination, Direction, useEuiTheme } from '@elastic/eui';
import { getFieldColumns, getFieldItems, isActionsColumn } from '../field_items';
import { CATEGORY_TABLE_CLASS_NAME, TABLE_HEIGHT } from '../../helpers';
import type { BrowserFields, FieldBrowserProps, GetFieldTableColumns } from '../../types';
import { FieldTableHeader } from './field_table_header';
import { styles } from './field_table.styles';
const DEFAULT_SORTING: { field: string; direction: Direction } = {
field: '',
direction: 'asc',
} as const;
export interface FieldTableProps {
columnHeaders: ColumnHeaderOptions[];
export interface FieldTableProps extends Pick<FieldBrowserProps, 'columnIds' | 'onToggleColumn'> {
/**
* A map of categoryId -> metadata about the fields in that category,
* filtered such that the name of every field in the category includes
@ -30,7 +27,6 @@ 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
*/
@ -48,21 +44,8 @@ export interface FieldTableProps {
onHide: () => void;
}
const TableContainer = styled.div<{ height: number }>`
margin-top: ${({ theme }) => theme.eui.euiSizeXS};
border-top: ${({ theme }) => theme.eui.euiBorderThin};
${({ height }) => `height: ${height}px`};
overflow: hidden;
`;
TableContainer.displayName = 'TableContainer';
const Count = styled.span`
font-weight: bold;
`;
Count.displayName = 'Count';
const FieldTableComponent: React.FC<FieldTableProps> = ({
columnHeaders,
columnIds,
filteredBrowserFields,
filterSelectedEnabled,
getFieldTableColumns,
@ -72,6 +55,7 @@ const FieldTableComponent: React.FC<FieldTableProps> = ({
onToggleColumn,
onHide,
}) => {
const { euiTheme } = useEuiTheme();
const [pageIndex, setPageIndex] = useState(0);
const [pageSize, setPageSize] = useState(10);
@ -83,9 +67,9 @@ const FieldTableComponent: React.FC<FieldTableProps> = ({
getFieldItems({
browserFields: filteredBrowserFields,
selectedCategoryIds,
columnHeaders,
columnIds,
}),
[columnHeaders, filteredBrowserFields, selectedCategoryIds]
[columnIds, filteredBrowserFields, selectedCategoryIds]
);
/**
@ -147,7 +131,7 @@ const FieldTableComponent: React.FC<FieldTableProps> = ({
onFilterSelectedChange={onFilterSelectedChange}
/>
<TableContainer height={TABLE_HEIGHT}>
<div css={styles.tableContainer({ height: TABLE_HEIGHT, euiTheme })}>
<EuiInMemoryTable
data-test-subj="field-table"
className={`${CATEGORY_TABLE_CLASS_NAME} eui-yScroll`}
@ -160,9 +144,8 @@ const FieldTableComponent: React.FC<FieldTableProps> = ({
onChange={onTableChange}
compressed
/>
</TableContainer>
</div>
</>
);
};
export const FieldTable = React.memo(FieldTableComponent);

View file

@ -0,0 +1,14 @@
/*
* 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 { css } from '@emotion/react';
export const styles = {
count: css`
font-weight: bold;
`,
};

View file

@ -7,7 +7,6 @@
import React from 'react';
import { render } from '@testing-library/react';
import { TestProviders } from '../../../../mock';
import { FieldTableHeader, FieldTableHeaderProps } from './field_table_header';
const mockOnFilterSelectedChange = jest.fn();
@ -20,30 +19,18 @@ const defaultProps: FieldTableHeaderProps = {
describe('FieldTableHeader', () => {
describe('FieldCount', () => {
it('should render empty field table', () => {
const result = render(
<TestProviders>
<FieldTableHeader {...defaultProps} />
</TestProviders>
);
const result = render(<FieldTableHeader {...defaultProps} />);
expect(result.getByTestId('fields-showing').textContent).toBe('Showing 0 fields');
});
it('should render field table with one singular field count value', () => {
const result = render(
<TestProviders>
<FieldTableHeader {...defaultProps} fieldCount={1} />
</TestProviders>
);
const result = render(<FieldTableHeader {...defaultProps} fieldCount={1} />);
expect(result.getByTestId('fields-showing').textContent).toBe('Showing 1 field');
});
it('should render field table with multiple fields count value', () => {
const result = render(
<TestProviders>
<FieldTableHeader {...defaultProps} fieldCount={4} />
</TestProviders>
);
const result = render(<FieldTableHeader {...defaultProps} fieldCount={4} />);
expect(result.getByTestId('fields-showing').textContent).toBe('Showing 4 fields');
});
@ -55,31 +42,19 @@ describe('FieldTableHeader', () => {
});
it('should render "view all" option when filterSelected is not enabled', () => {
const result = render(
<TestProviders>
<FieldTableHeader {...defaultProps} filterSelectedEnabled={false} />
</TestProviders>
);
const result = render(<FieldTableHeader {...defaultProps} filterSelectedEnabled={false} />);
expect(result.getByTestId('viewSelectorButton').textContent).toBe('View: all');
});
it('should render "view selected" option when filterSelected is not enabled', () => {
const result = render(
<TestProviders>
<FieldTableHeader {...defaultProps} filterSelectedEnabled={true} />
</TestProviders>
);
const result = render(<FieldTableHeader {...defaultProps} filterSelectedEnabled={true} />);
expect(result.getByTestId('viewSelectorButton').textContent).toBe('View: selected');
});
it('should open the view selector with button click', async () => {
const result = render(
<TestProviders>
<FieldTableHeader {...defaultProps} />
</TestProviders>
);
const result = render(<FieldTableHeader {...defaultProps} />);
expect(result.queryByTestId('viewSelectorMenu')).not.toBeInTheDocument();
expect(result.queryByTestId('viewSelectorOption-all')).not.toBeInTheDocument();
@ -93,11 +68,7 @@ describe('FieldTableHeader', () => {
});
it('should callback when "view all" option is clicked', () => {
const result = render(
<TestProviders>
<FieldTableHeader {...defaultProps} filterSelectedEnabled={false} />
</TestProviders>
);
const result = render(<FieldTableHeader {...defaultProps} filterSelectedEnabled={false} />);
result.getByTestId('viewSelectorButton').click();
result.getByTestId('viewSelectorOption-all').click();
@ -105,11 +76,7 @@ describe('FieldTableHeader', () => {
});
it('should callback when "view selected" option is clicked', () => {
const result = render(
<TestProviders>
<FieldTableHeader {...defaultProps} filterSelectedEnabled={false} />
</TestProviders>
);
const result = render(<FieldTableHeader {...defaultProps} filterSelectedEnabled={false} />);
result.getByTestId('viewSelectorButton').click();
result.getByTestId('viewSelectorOption-selected').click();

View file

@ -4,9 +4,7 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { useCallback, useState } from 'react';
import styled from 'styled-components';
import {
EuiText,
EuiPopover,
@ -17,7 +15,8 @@ import {
EuiContextMenuPanel,
EuiHorizontalRule,
} from '@elastic/eui';
import * as i18n from './translations';
import * as i18n from '../../translations';
import { styles } from './field_table_header.styles';
export interface FieldTableHeaderProps {
fieldCount: number;
@ -25,11 +24,6 @@ export interface FieldTableHeaderProps {
onFilterSelectedChange: (enabled: boolean) => void;
}
const Count = styled.span`
font-weight: bold;
`;
Count.displayName = 'Count';
const FieldTableHeaderComponent: React.FC<FieldTableHeaderProps> = ({
fieldCount,
filterSelectedEnabled,
@ -50,7 +44,10 @@ const FieldTableHeaderComponent: React.FC<FieldTableHeaderProps> = ({
<EuiFlexItem>
<EuiText data-test-subj="fields-showing" size="xs">
{i18n.FIELDS_SHOWING}
<Count data-test-subj="fields-count"> {fieldCount} </Count>
<span css={styles.count} data-test-subj="fields-count">
{' '}
{fieldCount}{' '}
</span>
{i18n.FIELDS_COUNT(fieldCount)}
</EuiText>
</EuiFlexItem>

View file

@ -0,0 +1,9 @@
/*
* 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.
*/
export { FieldTable } from './field_table';
export type { FieldTableProps } from './field_table';

View file

@ -0,0 +1,8 @@
/*
* 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.
*/
export { Search } from './search';

View file

@ -7,15 +7,12 @@
import { mount } from 'enzyme';
import React from 'react';
import { TestProviders } from '../../../../mock';
import { Search } from './search';
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="" />
</TestProviders>
<Search isSearching={false} onSearchInputChange={jest.fn()} searchInput="" />
);
expect(wrapper.find('[data-test-subj="field-search"]').first().props().placeholder).toEqual(
@ -27,9 +24,7 @@ describe('Search', () => {
const searchInput = 'aFieldName';
const wrapper = mount(
<TestProviders>
<Search isSearching={false} onSearchInputChange={jest.fn()} searchInput={searchInput} />
</TestProviders>
<Search isSearching={false} onSearchInputChange={jest.fn()} searchInput={searchInput} />
);
expect(wrapper.find('input').props().value).toEqual(searchInput);
@ -37,9 +32,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="" />
</TestProviders>
<Search isSearching={true} onSearchInputChange={jest.fn()} searchInput="" />
);
expect(wrapper.find('.euiLoadingSpinner').first().exists()).toBe(true);
@ -49,9 +42,7 @@ describe('Search', () => {
const onSearchInputChange = jest.fn();
const wrapper = mount(
<TestProviders>
<Search isSearching={false} onSearchInputChange={onSearchInputChange} searchInput="" />
</TestProviders>
<Search isSearching={false} onSearchInputChange={onSearchInputChange} searchInput="" />
);
wrapper

View file

@ -7,7 +7,7 @@
import React from 'react';
import { EuiFieldSearch } from '@elastic/eui';
import * as i18n from './translations';
import * as i18n from '../../translations';
interface Props {
isSearching: boolean;
onSearchInputChange: (event: React.ChangeEvent<HTMLInputElement>) => void;

View file

@ -0,0 +1,15 @@
/*
* 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 { css } from '@emotion/react';
export const styles = {
buttonContainer: css`
display: inline-block;
position: relative;
`,
};

View file

@ -7,29 +7,22 @@
import React from 'react';
import { act, fireEvent, render, waitFor } from '@testing-library/react';
import { mockBrowserFields, TestProviders } from '../../../../mock';
import { mockBrowserFields } from './mock';
import { FIELD_BROWSER_WIDTH } from './helpers';
import { FieldBrowserComponent } from './field_browser';
import { FieldBrowserProps } from '../../../field_browser';
import type { FieldBrowserProps } from './types';
const defaultProps: FieldBrowserProps = {
browserFields: mockBrowserFields,
columnHeaders: [],
columnIds: [],
onToggleColumn: jest.fn(),
onResetColumns: jest.fn(),
};
const renderComponent = (props: Partial<FieldBrowserProps> = {}) =>
render(
<TestProviders>
<FieldBrowserComponent {...{ ...defaultProps, ...props }} />
</TestProviders>
);
render(<FieldBrowserComponent {...{ ...defaultProps, ...props }} />);
describe('StatefulFieldsBrowser', () => {
describe('FieldsBrowser', () => {
it('should render the Fields button, which displays the fields browser on click', () => {
const result = renderComponent();

View file

@ -4,34 +4,26 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { EuiButtonEmpty, EuiToolTip } from '@elastic/eui';
import { debounce } from 'lodash';
import React, { useEffect, useRef, useState, useCallback, useMemo } from 'react';
import styled from 'styled-components';
import type { BrowserFields } from '../../../../../common/search_strategy/index_fields';
import type { FieldBrowserProps } from '../../../../../common/types/field_browser';
import type { FieldBrowserProps, BrowserFields } from './types';
import { FieldBrowserModal } from './field_browser_modal';
import { filterBrowserFieldsByFieldName, filterSelectedBrowserFields } from './helpers';
import * as i18n from './translations';
import { styles } from './field_browser.styles';
const FIELDS_BUTTON_CLASS_NAME = 'fields-button';
/** wait this many ms after the user completes typing before applying the filter input */
export const INPUT_TIMEOUT = 250;
const FieldBrowserButtonContainer = styled.div`
display: inline-block;
position: relative;
`;
FieldBrowserButtonContainer.displayName = 'FieldBrowserButtonContainer';
/**
* Manages the state of the field browser
*/
export const FieldBrowserComponent: React.FC<FieldBrowserProps> = ({
columnHeaders,
columnIds,
browserFields,
onResetColumns,
onToggleColumn,
@ -73,9 +65,9 @@ export const FieldBrowserComponent: React.FC<FieldBrowserProps> = ({
const selectionFilteredBrowserFields = useMemo<BrowserFields>(
() =>
filterSelectedEnabled
? filterSelectedBrowserFields({ browserFields, columnHeaders })
? filterSelectedBrowserFields({ browserFields, columnIds })
: browserFields,
[browserFields, columnHeaders, filterSelectedEnabled]
[browserFields, columnIds, filterSelectedEnabled]
);
useEffect(() => {
@ -123,7 +115,7 @@ export const FieldBrowserComponent: React.FC<FieldBrowserProps> = ({
);
return (
<FieldBrowserButtonContainer data-test-subj="fields-browser-button-container">
<div css={styles.buttonContainer} data-test-subj="fields-browser-button-container">
<EuiToolTip content={i18n.FIELDS_BROWSER}>
<EuiButtonEmpty
aria-label={i18n.FIELDS_BROWSER}
@ -141,7 +133,7 @@ export const FieldBrowserComponent: React.FC<FieldBrowserProps> = ({
{show && (
<FieldBrowserModal
columnHeaders={columnHeaders}
columnIds={columnIds}
filteredBrowserFields={
filteredBrowserFields != null ? filteredBrowserFields : browserFields
}
@ -161,7 +153,7 @@ export const FieldBrowserComponent: React.FC<FieldBrowserProps> = ({
width={width}
/>
)}
</FieldBrowserButtonContainer>
</div>
);
};

View file

@ -8,7 +8,7 @@
import { mount } from 'enzyme';
import React from 'react';
import { TestProviders, mockBrowserFields, defaultHeaders } from '../../../../mock';
import { mockBrowserFields } from './mock';
import { FieldBrowserModal, FieldBrowserModalProps } from './field_browser_modal';
const mockOnHide = jest.fn();
@ -16,7 +16,7 @@ const mockOnToggleColumn = jest.fn();
const mockOnResetColumns = jest.fn();
const testProps: FieldBrowserModalProps = {
columnHeaders: [],
columnIds: [],
filteredBrowserFields: mockBrowserFields,
searchInput: '',
appliedFilterInput: '',
@ -32,59 +32,42 @@ const testProps: FieldBrowserModalProps = {
onFilterSelectedChange: jest.fn(),
};
const mountComponent = (props: Partial<FieldBrowserModalProps> = {}) =>
mount(<FieldBrowserModal {...{ ...testProps, ...props }} />);
describe('FieldBrowserModal', () => {
beforeEach(() => {
jest.clearAllMocks();
});
test('it renders the Close button', () => {
const wrapper = mount(
<TestProviders>
<FieldBrowserModal {...testProps} />
</TestProviders>
);
const wrapper = mountComponent();
expect(wrapper.find('[data-test-subj="close"]').first().text()).toEqual('Close');
});
test('it invokes the Close button', () => {
const wrapper = mount(
<TestProviders>
<FieldBrowserModal {...testProps} />
</TestProviders>
);
const wrapper = mountComponent();
wrapper.find('[data-test-subj="close"]').first().simulate('click');
expect(mockOnHide).toBeCalled();
});
test('it renders the Reset Fields button', () => {
const wrapper = mount(
<TestProviders>
<FieldBrowserModal {...testProps} />
</TestProviders>
);
const wrapper = mountComponent();
expect(wrapper.find('[data-test-subj="reset-fields"]').first().text()).toEqual('Reset Fields');
});
test('it invokes onResetColumns callback when the user clicks the Reset Fields button', () => {
const wrapper = mount(
<TestProviders>
<FieldBrowserModal {...testProps} columnHeaders={defaultHeaders} />
</TestProviders>
);
const wrapper = mountComponent({ columnIds: ['test'] });
wrapper.find('[data-test-subj="reset-fields"]').first().simulate('click');
expect(mockOnResetColumns).toHaveBeenCalled();
});
test('it invokes onHide when the user clicks the Reset Fields button', () => {
const wrapper = mount(
<TestProviders>
<FieldBrowserModal {...testProps} />
</TestProviders>
);
const wrapper = mountComponent();
wrapper.find('[data-test-subj="reset-fields"]').first().simulate('click');
@ -92,41 +75,25 @@ describe('FieldBrowserModal', () => {
});
test('it renders the search', () => {
const wrapper = mount(
<TestProviders>
<FieldBrowserModal {...testProps} />
</TestProviders>
);
const wrapper = mountComponent();
expect(wrapper.find('[data-test-subj="field-search"]').exists()).toBe(true);
});
test('it renders the categories selector', () => {
const wrapper = mount(
<TestProviders>
<FieldBrowserModal {...testProps} />
</TestProviders>
);
const wrapper = mountComponent();
expect(wrapper.find('[data-test-subj="categories-selector"]').exists()).toBe(true);
});
test('it renders the fields table', () => {
const wrapper = mount(
<TestProviders>
<FieldBrowserModal {...testProps} />
</TestProviders>
);
const wrapper = mountComponent();
expect(wrapper.find('[data-test-subj="field-table"]').exists()).toBe(true);
});
test('focuses the search input when the component mounts', () => {
const wrapper = mount(
<TestProviders>
<FieldBrowserModal {...testProps} />
</TestProviders>
);
const wrapper = mountComponent();
expect(
wrapper.find('[data-test-subj="field-search"]').first().getDOMNode().id ===
@ -138,14 +105,9 @@ describe('FieldBrowserModal', () => {
const onSearchInputChange = jest.fn();
const inputText = 'event.category';
const wrapper = mount(
<TestProviders>
<FieldBrowserModal {...testProps} onSearchInputChange={onSearchInputChange} />
</TestProviders>
);
const wrapper = mountComponent({ onSearchInputChange });
const searchField = wrapper.find('[data-test-subj="field-search"]').first();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const changeEvent: any = { target: { value: inputText } };
const onChange = searchField.props().onChange;
@ -158,16 +120,7 @@ describe('FieldBrowserModal', () => {
test('it renders the CreateFieldButton when it is provided', () => {
const MyTestComponent = () => <div>{'test'}</div>;
const wrapper = mount(
<TestProviders>
<FieldBrowserModal
{...testProps}
options={{
createFieldButton: MyTestComponent,
}}
/>
</TestProviders>
);
const wrapper = mountComponent({ options: { createFieldButton: MyTestComponent } });
expect(wrapper.find(MyTestComponent).exists()).toBeTruthy();
});

View file

@ -4,7 +4,6 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import {
EuiFlexGroup,
EuiFlexItem,
@ -19,16 +18,15 @@ import {
} from '@elastic/eui';
import React, { useCallback } from 'react';
import type { BrowserFields } from '../../../../../common/search_strategy';
import type { FieldBrowserProps, ColumnHeaderOptions } from '../../../../../common/types';
import { Search } from './search';
import type { FieldBrowserProps, BrowserFields } from './types';
import { Search } from './components/search';
import { CLOSE_BUTTON_CLASS_NAME, FIELD_BROWSER_WIDTH, RESET_FIELDS_CLASS_NAME } from './helpers';
import * as i18n from './translations';
import { CategoriesSelector } from './categories_selector';
import { FieldTable } from './field_table';
import { CategoriesBadges } from './categories_badges';
import { CategoriesSelector } from './components/categories_selector';
import { CategoriesBadges } from './components/categories_badges';
import { FieldTable } from './components/field_table';
export type FieldBrowserModalProps = Pick<
FieldBrowserProps,
@ -37,7 +35,7 @@ export type FieldBrowserModalProps = Pick<
/**
* The current timeline column headers
*/
columnHeaders: ColumnHeaderOptions[];
columnIds: string[];
/**
* A map of categoryId -> metadata about the fields in that category,
* filtered such that the name of every field in the category includes
@ -88,7 +86,7 @@ export type FieldBrowserModalProps = Pick<
*/
const FieldBrowserModalComponent: React.FC<FieldBrowserModalProps> = ({
appliedFilterInput,
columnHeaders,
columnIds,
filteredBrowserFields,
filterSelectedEnabled,
isSearching,
@ -169,7 +167,7 @@ const FieldBrowserModalComponent: React.FC<FieldBrowserModalProps> = ({
<EuiSpacer size="l" />
<FieldTable
columnHeaders={columnHeaders}
columnIds={columnIds}
filteredBrowserFields={filteredBrowserFields}
filterSelectedEnabled={filterSelectedEnabled}
searchInput={appliedFilterInput}

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { mockBrowserFields } from '../../../../mock';
import { mockBrowserFields } from './mock';
import {
categoryHasFields,
@ -13,8 +13,7 @@ import {
filterBrowserFieldsByFieldName,
filterSelectedBrowserFields,
} from './helpers';
import { BrowserFields } from '../../../../../common/search_strategy';
import { ColumnHeaderOptions } from '../../../../../common';
import type { BrowserFields } from './types';
describe('helpers', () => {
describe('categoryHasFields', () => {
@ -257,25 +256,21 @@ describe('helpers', () => {
});
describe('filterSelectedBrowserFields', () => {
const columnHeaders = [
{ id: 'agent.ephemeral_id' },
{ id: 'agent.id' },
{ id: 'container.id' },
] as ColumnHeaderOptions[];
const columnIds = ['agent.ephemeral_id', 'agent.id', 'container.id'];
test('it returns an empty collection when browserFields is empty', () => {
expect(filterSelectedBrowserFields({ browserFields: {}, columnHeaders: [] })).toEqual({});
expect(filterSelectedBrowserFields({ browserFields: {}, columnIds: [] })).toEqual({});
});
test('it returns an empty collection when browserFields is empty and columnHeaders is non empty', () => {
expect(filterSelectedBrowserFields({ browserFields: {}, columnHeaders })).toEqual({});
test('it returns an empty collection when browserFields is empty and columnIds is non empty', () => {
expect(filterSelectedBrowserFields({ browserFields: {}, columnIds })).toEqual({});
});
test('it returns an empty collection when browserFields is NOT empty and columnHeaders is empty', () => {
test('it returns an empty collection when browserFields is NOT empty and columnIds is empty', () => {
expect(
filterSelectedBrowserFields({
browserFields: mockBrowserFields,
columnHeaders: [],
columnIds: [],
})
).toEqual({});
});
@ -330,7 +325,7 @@ describe('helpers', () => {
expect(
filterSelectedBrowserFields({
browserFields: mockBrowserFields,
columnHeaders,
columnIds,
})
).toEqual(filtered);
});

View file

@ -5,19 +5,8 @@
* 2.0.
*/
import { EuiBadge, EuiLoadingSpinner } from '@elastic/eui';
import styled from 'styled-components';
import type { BrowserField, BrowserFields } from '../../../../../common/search_strategy';
import { ColumnHeaderOptions } from '../../../../../common';
export const LoadingSpinner = styled(EuiLoadingSpinner)`
cursor: pointer;
position: relative;
top: 3px;
`;
LoadingSpinner.displayName = 'LoadingSpinner';
import { isEmpty } from 'lodash/fp';
import { BrowserField, BrowserFields } from './types';
export const FIELD_BROWSER_WIDTH = 925;
export const TABLE_HEIGHT = 260;
@ -50,7 +39,6 @@ export function filterBrowserFieldsByFieldName({
for (const [categoryName, categoryDescriptor] of Object.entries(browserFields)) {
if (!categoryDescriptor.fields) {
// ignore any category that is missing fields. This is not expected to happen.
// eslint-disable-next-line no-continue
continue;
}
@ -67,7 +55,6 @@ export function filterBrowserFieldsByFieldName({
if (!fieldNameFromDescriptor) {
// Ignore any field that is missing a name in its descriptor. This is not expected to happen.
// eslint-disable-next-line no-continue
continue;
}
@ -94,23 +81,22 @@ export function filterBrowserFieldsByFieldName({
/**
* Filters the selected `BrowserFields` to return a new collection where every
* category contains at least one field that is present in the `columnHeaders`.
* category contains at least one field that is present in the `columnIds`.
*/
export const filterSelectedBrowserFields = ({
browserFields,
columnHeaders,
columnIds,
}: {
browserFields: BrowserFields;
columnHeaders: ColumnHeaderOptions[];
columnIds: string[];
}): BrowserFields => {
const selectedFieldIds = new Set(columnHeaders.map(({ id }) => id));
const selectedFieldIds = new Set(columnIds);
const result: Record<string, Partial<BrowserField>> = {};
for (const [categoryName, categoryDescriptor] of Object.entries(browserFields)) {
if (!categoryDescriptor.fields) {
// ignore any category that is missing fields. This is not expected to happen.
// eslint-disable-next-line no-continue
continue;
}
@ -127,7 +113,6 @@ export const filterSelectedBrowserFields = ({
if (!fieldNameFromDescriptor) {
// Ignore any field that is missing a name in its descriptor. This is not expected to happen.
// eslint-disable-next-line no-continue
continue;
}
@ -147,22 +132,37 @@ export const filterSelectedBrowserFields = ({
return result;
};
export const getIconFromType = (type: string | null | undefined) => {
switch (type) {
case 'string': // fall through
case 'keyword':
return 'string';
case 'number': // fall through
case 'long':
return 'number';
case 'date':
return 'clock';
case 'ip':
case 'geo_point':
return 'globe';
case 'object':
return 'questionInCircle';
case 'float':
return 'number';
default:
return 'questionInCircle';
}
};
export const getEmptyValue = () => '—';
/** Returns example text, or an empty string if the field does not have an example */
export const getExampleText = (example: string | number | null | undefined): string =>
!isEmpty(example) ? `Example: ${example}` : '';
/** Returns `true` if the escape key was pressed */
export const isEscape = (event: React.KeyboardEvent): boolean => event.key === 'Escape';
export const CATEGORY_TABLE_CLASS_NAME = 'category-table';
export const CLOSE_BUTTON_CLASS_NAME = 'close-button';
export const RESET_FIELDS_CLASS_NAME = 'reset-fields';
export const CountBadge = styled(EuiBadge)`
margin-left: 5px;
` as unknown as typeof EuiBadge;
CountBadge.displayName = 'CountBadge';
export const CategoryName = styled.span<{ bold: boolean }>`
font-weight: ${({ bold }) => (bold ? 'bold' : 'normal')};
`;
CategoryName.displayName = 'CategoryName';
export const CategorySelectableContainer = styled.div`
width: 300px;
`;
CategorySelectableContainer.displayName = 'CategorySelectableContainer';

View file

@ -4,14 +4,9 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { FieldBrowser } from './field_browser';
export type { FieldBrowserProps } from './types';
import { FieldBrowser } from '../t_grid/toolbar/field_browser';
export type {
CreateFieldComponent,
FieldBrowserOptions,
FieldBrowserProps,
GetFieldTableColumns,
} from '../../../common/types/field_browser';
export { FieldBrowser };
// eslint-disable-next-line import/no-default-export
export { FieldBrowser as default };

View file

@ -0,0 +1,476 @@
/*
* 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 { MappingRuntimeFields } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { BrowserFields } from './types';
const DEFAULT_INDEX_PATTERN = [
'apm-*-transaction*',
'traces-apm*',
'auditbeat-*',
'endgame-*',
'filebeat-*',
'logs-*',
'packetbeat-*',
'winlogbeat-*',
];
export const mockBrowserFields: BrowserFields = {
agent: {
fields: {
'agent.ephemeral_id': {
aggregatable: true,
category: 'agent',
description:
'Ephemeral identifier of this agent (if one exists). This id normally changes across restarts, but `agent.id` does not.',
example: '8a4f500f',
format: '',
indexes: ['auditbeat', 'filebeat', 'packetbeat'],
name: 'agent.ephemeral_id',
searchable: true,
type: 'string',
},
'agent.hostname': {
aggregatable: true,
category: 'agent',
description: null,
example: null,
format: '',
indexes: ['auditbeat', 'filebeat', 'packetbeat'],
name: 'agent.hostname',
searchable: true,
type: 'string',
},
'agent.id': {
aggregatable: true,
category: 'agent',
description:
'Unique identifier of this agent (if one exists). Example: For Beats this would be beat.id.',
example: '8a4f500d',
format: '',
indexes: ['auditbeat', 'filebeat', 'packetbeat'],
name: 'agent.id',
searchable: true,
type: 'string',
},
'agent.name': {
aggregatable: true,
category: 'agent',
description:
'Name of the agent. This is a name that can be given to an agent. This can be helpful if for example two Filebeat instances are running on the same host but a human readable separation is needed on which Filebeat instance data is coming from. If no name is given, the name is often left empty.',
example: 'foo',
format: '',
indexes: ['auditbeat', 'filebeat', 'packetbeat'],
name: 'agent.name',
searchable: true,
type: 'string',
},
},
},
auditd: {
fields: {
'auditd.data.a0': {
aggregatable: true,
category: 'auditd',
description: null,
example: null,
format: '',
indexes: ['auditbeat'],
name: 'auditd.data.a0',
searchable: true,
type: 'string',
},
'auditd.data.a1': {
aggregatable: true,
category: 'auditd',
description: null,
example: null,
format: '',
indexes: ['auditbeat'],
name: 'auditd.data.a1',
searchable: true,
type: 'string',
},
'auditd.data.a2': {
aggregatable: true,
category: 'auditd',
description: null,
example: null,
format: '',
indexes: ['auditbeat'],
name: 'auditd.data.a2',
searchable: true,
type: 'string',
},
},
},
base: {
fields: {
'@timestamp': {
aggregatable: true,
category: 'base',
description:
'Date/time when the event originated. For log events this is the date/time when the event was generated, and not when it was read. Required field for all events.',
example: '2016-05-23T08:05:34.853Z',
format: '',
indexes: ['auditbeat', 'filebeat', 'packetbeat'],
name: '@timestamp',
searchable: true,
type: 'date',
},
_id: {
category: 'base',
description: 'Each document has an _id that uniquely identifies it',
example: 'Y-6TfmcB0WOhS6qyMv3s',
name: '_id',
type: 'string',
searchable: true,
aggregatable: false,
indexes: ['auditbeat', 'filebeat', 'packetbeat'],
},
message: {
category: 'base',
description:
'For log events the message field contains the log message, optimized for viewing in a log viewer. For structured logs without an original message field, other fields can be concatenated to form a human-readable summary of the event. If multiple messages exist, they can be combined into one message.',
example: 'Hello World',
name: 'message',
type: 'string',
searchable: true,
aggregatable: false,
format: 'string',
indexes: ['auditbeat', 'filebeat', 'packetbeat'],
},
},
},
client: {
fields: {
'client.address': {
aggregatable: true,
category: 'client',
description:
'Some event client addresses are defined ambiguously. The event will sometimes list an IP, a domain or a unix socket. You should always store the raw address in the `.address` field. Then it should be duplicated to `.ip` or `.domain`, depending on which one it is.',
example: null,
format: '',
indexes: ['auditbeat', 'filebeat', 'packetbeat'],
name: 'client.address',
searchable: true,
type: 'string',
},
'client.bytes': {
aggregatable: true,
category: 'client',
description: 'Bytes sent from the client to the server.',
example: '184',
format: '',
indexes: ['auditbeat', 'filebeat', 'packetbeat'],
name: 'client.bytes',
searchable: true,
type: 'number',
},
'client.domain': {
aggregatable: true,
category: 'client',
description: 'Client domain.',
example: null,
format: '',
indexes: ['auditbeat', 'filebeat', 'packetbeat'],
name: 'client.domain',
searchable: true,
type: 'string',
},
'client.geo.country_iso_code': {
aggregatable: true,
category: 'client',
description: 'Country ISO code.',
example: 'CA',
format: '',
indexes: ['auditbeat', 'filebeat', 'packetbeat'],
name: 'client.geo.country_iso_code',
searchable: true,
type: 'string',
},
},
},
cloud: {
fields: {
'cloud.account.id': {
aggregatable: true,
category: 'cloud',
description:
'The cloud account or organization id used to identify different entities in a multi-tenant environment. Examples: AWS account id, Google Cloud ORG Id, or other unique identifier.',
example: '666777888999',
format: '',
indexes: ['auditbeat', 'filebeat', 'packetbeat'],
name: 'cloud.account.id',
searchable: true,
type: 'string',
},
'cloud.availability_zone': {
aggregatable: true,
category: 'cloud',
description: 'Availability zone in which this host is running.',
example: 'us-east-1c',
format: '',
indexes: ['auditbeat', 'filebeat', 'packetbeat'],
name: 'cloud.availability_zone',
searchable: true,
type: 'string',
},
},
},
container: {
fields: {
'container.id': {
aggregatable: true,
category: 'container',
description: 'Unique container id.',
example: null,
format: '',
indexes: ['auditbeat', 'filebeat', 'packetbeat'],
name: 'container.id',
searchable: true,
type: 'string',
},
'container.image.name': {
aggregatable: true,
category: 'container',
description: 'Name of the image the container was built on.',
example: null,
format: '',
indexes: ['auditbeat', 'filebeat', 'packetbeat'],
name: 'container.image.name',
searchable: true,
type: 'string',
},
'container.image.tag': {
aggregatable: true,
category: 'container',
description: 'Container image tag.',
example: null,
format: '',
indexes: ['auditbeat', 'filebeat', 'packetbeat'],
name: 'container.image.tag',
searchable: true,
type: 'string',
},
},
},
destination: {
fields: {
'destination.address': {
aggregatable: true,
category: 'destination',
description:
'Some event destination addresses are defined ambiguously. The event will sometimes list an IP, a domain or a unix socket. You should always store the raw address in the `.address` field. Then it should be duplicated to `.ip` or `.domain`, depending on which one it is.',
example: null,
format: '',
indexes: ['auditbeat', 'filebeat', 'packetbeat'],
name: 'destination.address',
searchable: true,
type: 'string',
},
'destination.bytes': {
aggregatable: true,
category: 'destination',
description: 'Bytes sent from the destination to the source.',
example: '184',
format: '',
indexes: ['auditbeat', 'filebeat', 'packetbeat'],
name: 'destination.bytes',
searchable: true,
type: 'number',
},
'destination.domain': {
aggregatable: true,
category: 'destination',
description: 'Destination domain.',
example: null,
format: '',
indexes: ['auditbeat', 'filebeat', 'packetbeat'],
name: 'destination.domain',
searchable: true,
type: 'string',
},
'destination.ip': {
aggregatable: true,
category: 'destination',
description:
'IP address of the destination. Can be one or multiple IPv4 or IPv6 addresses.',
example: '',
format: '',
indexes: ['auditbeat', 'filebeat', 'packetbeat'],
name: 'destination.ip',
searchable: true,
type: 'ip',
},
'destination.port': {
aggregatable: true,
category: 'destination',
description: 'Port of the destination.',
example: '',
format: '',
indexes: ['auditbeat', 'filebeat', 'packetbeat'],
name: 'destination.port',
searchable: true,
type: 'long',
},
},
},
event: {
fields: {
'event.end': {
category: 'event',
description:
'event.end contains the date when the event ended or when the activity was last observed.',
example: null,
format: '',
indexes: DEFAULT_INDEX_PATTERN,
name: 'event.end',
searchable: true,
type: 'date',
aggregatable: true,
},
'event.action': {
category: 'event',
description:
'The action captured by the event. This describes the information in the event. It is more specific than `event.category`. Examples are `group-add`, `process-started`, `file-created`. The value is normally defined by the implementer.',
example: 'user-password-change',
name: 'event.action',
type: 'string',
searchable: true,
aggregatable: true,
format: 'string',
indexes: DEFAULT_INDEX_PATTERN,
},
'event.category': {
category: 'event',
description:
'This is one of four ECS Categorization Fields, and indicates the second level in the ECS category hierarchy. `event.category` represents the "big buckets" of ECS categories. For example, filtering on `event.category:process` yields all events relating to process activity. This field is closely related to `event.type`, which is used as a subcategory. This field is an array. This will allow proper categorization of some events that fall in multiple categories.',
example: 'authentication',
name: 'event.category',
type: 'string',
searchable: true,
aggregatable: true,
format: 'string',
indexes: DEFAULT_INDEX_PATTERN,
},
'event.severity': {
category: 'event',
description:
"The numeric severity of the event according to your event source. What the different severity values mean can be different between sources and use cases. It's up to the implementer to make sure severities are consistent across events from the same source. The Syslog severity belongs in `log.syslog.severity.code`. `event.severity` is meant to represent the severity according to the event source (e.g. firewall, IDS). If the event source does not publish its own severity, you may optionally copy the `log.syslog.severity.code` to `event.severity`.",
example: 7,
name: 'event.severity',
type: 'number',
format: 'number',
searchable: true,
aggregatable: true,
indexes: DEFAULT_INDEX_PATTERN,
},
},
},
host: {
fields: {
'host.name': {
category: 'host',
description:
'Name of the host. It can contain what `hostname` returns on Unix systems, the fully qualified domain name, or a name specified by the user. The sender decides which value to use.',
name: 'host.name',
type: 'string',
searchable: true,
aggregatable: true,
format: 'string',
indexes: DEFAULT_INDEX_PATTERN,
},
},
},
source: {
fields: {
'source.ip': {
aggregatable: true,
category: 'source',
description: 'IP address of the source. Can be one or multiple IPv4 or IPv6 addresses.',
example: '',
format: '',
indexes: ['auditbeat', 'filebeat', 'packetbeat'],
name: 'source.ip',
searchable: true,
type: 'ip',
},
'source.port': {
aggregatable: true,
category: 'source',
description: 'Port of the source.',
example: '',
format: '',
indexes: ['auditbeat', 'filebeat', 'packetbeat'],
name: 'source.port',
searchable: true,
type: 'long',
},
},
},
user: {
fields: {
'user.name': {
category: 'user',
description: 'Short name or login of the user.',
example: 'albert',
name: 'user.name',
type: 'string',
searchable: true,
aggregatable: true,
format: 'string',
indexes: ['auditbeat', 'filebeat', 'packetbeat'],
},
},
},
nestedField: {
fields: {
'nestedField.firstAttributes': {
aggregatable: false,
category: 'nestedField',
description: '',
example: '',
format: '',
indexes: ['auditbeat', 'filebeat', 'packetbeat'],
name: 'nestedField.firstAttributes',
searchable: true,
type: 'string',
subType: {
nested: {
path: 'nestedField',
},
},
},
'nestedField.secondAttributes': {
aggregatable: false,
category: 'nestedField',
description: '',
example: '',
format: '',
indexes: ['auditbeat', 'filebeat', 'packetbeat'],
name: 'nestedField.secondAttributes',
searchable: true,
type: 'string',
subType: {
nested: {
path: 'nestedField',
},
},
},
},
},
};
export const mockRuntimeMappings: MappingRuntimeFields = {
'@a.runtime.field': {
script: {
source: 'emit("Radical dude: " + doc[\'host.name\'].value)',
},
type: 'keyword',
},
};

View file

@ -0,0 +1,117 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { i18n } from '@kbn/i18n';
export const CATEGORY = i18n.translate('xpack.triggersActionsUI.fieldBrowser.categoryLabel', {
defaultMessage: 'Category',
});
export const CATEGORIES = i18n.translate('xpack.triggersActionsUI.fieldBrowser.categoriesTitle', {
defaultMessage: 'Categories',
});
export const CATEGORIES_COUNT = (totalCount: number) =>
i18n.translate('xpack.triggersActionsUI.fieldBrowser.categoriesCountTitle', {
values: { totalCount },
defaultMessage: '{totalCount} {totalCount, plural, =1 {category} other {categories}}',
});
export const CLOSE = i18n.translate('xpack.triggersActionsUI.fieldBrowser.closeButton', {
defaultMessage: 'Close',
});
export const FIELDS_BROWSER = i18n.translate(
'xpack.triggersActionsUI.fieldBrowser.fieldBrowserTitle',
{
defaultMessage: 'Fields',
}
);
export const DESCRIPTION = i18n.translate('xpack.triggersActionsUI.fieldBrowser.descriptionLabel', {
defaultMessage: 'Description',
});
export const DESCRIPTION_FOR_FIELD = (field: string) =>
i18n.translate('xpack.triggersActionsUI.fieldBrowser.descriptionForScreenReaderOnly', {
values: {
field,
},
defaultMessage: 'Description for field {field}:',
});
export const NAME = i18n.translate('xpack.triggersActionsUI.fieldBrowser.fieldName', {
defaultMessage: 'Name',
});
export const FIELD = i18n.translate('xpack.triggersActionsUI.fieldBrowser.fieldLabel', {
defaultMessage: 'Field',
});
export const FIELDS = i18n.translate('xpack.triggersActionsUI.fieldBrowser.fieldsTitle', {
defaultMessage: 'Fields',
});
export const FIELDS_SHOWING = i18n.translate(
'xpack.triggersActionsUI.fieldBrowser.fieldsCountShowing',
{
defaultMessage: 'Showing',
}
);
export const FIELDS_COUNT = (totalCount: number) =>
i18n.translate('xpack.triggersActionsUI.fieldBrowser.fieldsCountTitle', {
values: { totalCount },
defaultMessage: '{totalCount, plural, =1 {field} other {fields}}',
});
export const FILTER_PLACEHOLDER = i18n.translate(
'xpack.triggersActionsUI.fieldBrowser.filterPlaceholder',
{
defaultMessage: 'Field name',
}
);
export const NO_FIELDS_MATCH = i18n.translate(
'xpack.triggersActionsUI.fieldBrowser.noFieldsMatchLabel',
{
defaultMessage: 'No fields match',
}
);
export const NO_FIELDS_MATCH_INPUT = (searchInput: string) =>
i18n.translate('xpack.triggersActionsUI.fieldBrowser.noFieldsMatchInputLabel', {
defaultMessage: 'No fields match {searchInput}',
values: {
searchInput,
},
});
export const RESET_FIELDS = i18n.translate('xpack.triggersActionsUI.fieldBrowser.resetFieldsLink', {
defaultMessage: 'Reset Fields',
});
export const VIEW_COLUMN = (field: string) =>
i18n.translate('xpack.triggersActionsUI.fieldBrowser.viewColumnCheckboxAriaLabel', {
values: { field },
defaultMessage: 'View {field} column',
});
export const VIEW_LABEL = i18n.translate('xpack.triggersActionsUI.fieldBrowser.viewLabel', {
defaultMessage: 'View',
});
export const VIEW_VALUE_SELECTED = i18n.translate(
'xpack.triggersActionsUI.fieldBrowser.viewSelected',
{
defaultMessage: 'selected',
}
);
export const VIEW_VALUE_ALL = i18n.translate('xpack.triggersActionsUI.fieldBrowser.viewAll', {
defaultMessage: 'all',
});

View file

@ -5,10 +5,27 @@
* 2.0.
*/
import { EuiBasicTableColumn } from '@elastic/eui';
import { BrowserFields } from '../../search_strategy';
import { ColumnHeaderOptions } from '../timeline/columns';
import type { EuiBasicTableColumn } from '@elastic/eui';
import type { IFieldSubType } from '@kbn/es-query';
import type { RuntimeField } from '@kbn/data-views-plugin/common';
export interface BrowserField {
aggregatable: boolean;
category: string;
description: string | null;
example: string | number | null;
fields: Readonly<Record<string, Partial<BrowserField>>>;
format: string;
indexes: string[];
name: string;
searchable: boolean;
type: string;
subType?: IFieldSubType;
readFromDocValues: boolean;
runtimeField?: RuntimeField;
}
export type BrowserFields = Readonly<Record<string, Partial<BrowserField>>>;
/**
* An item rendered in the table
*/
@ -39,7 +56,7 @@ export interface FieldBrowserOptions {
export interface FieldBrowserProps {
/** The timeline's current column headers */
columnHeaders: ColumnHeaderOptions[];
columnIds: string[];
/** 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" */
@ -47,7 +64,7 @@ export interface FieldBrowserProps {
/** Callback to reset the default columns */
onResetColumns: () => void;
/** Callback to toggle a field column */
onToggleColumn: (fieldId: string) => void;
onToggleColumn: (columnId: 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 */

View file

@ -0,0 +1,21 @@
/*
* 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, { lazy, Suspense } from 'react';
import { EuiLoadingSpinner } from '@elastic/eui';
import type { FieldBrowserProps } from '../application/sections/field_browser';
const FieldBrowserLazy: React.FC<FieldBrowserProps> = lazy(
() => import('../application/sections/field_browser')
);
export const getFieldBrowserLazy = (props: FieldBrowserProps) => (
<Suspense fallback={<EuiLoadingSpinner />}>
<FieldBrowserLazy {...props} />
</Suspense>
);

View file

@ -39,6 +39,8 @@ export type {
RuleEventLogListProps,
AlertTableFlyoutComponent,
GetRenderCellValue,
FieldBrowserOptions,
FieldBrowserProps,
RuleDefinitionProps,
} from './types';

View file

@ -21,6 +21,7 @@ import {
RuleTypeModel,
AlertsTableProps,
AlertsTableConfigurationRegistry,
FieldBrowserProps,
RuleTagBadgeOptions,
RuleTagBadgeProps,
} from './types';
@ -38,6 +39,7 @@ import { CreateConnectorFlyoutProps } from './application/sections/action_connec
import { EditConnectorFlyoutProps } from './application/sections/action_connector_form/edit_connector_flyout';
import { getActionFormLazy } from './common/get_action_form';
import { ActionAccordionFormProps } from './application/sections/action_connector_form/action_form';
import { getFieldBrowserLazy } from './common/get_field_browser';
import { getRuleDefinitionLazy } from './common/get_rule_definition';
import { getRuleStatusPanelLazy } from './common/get_rule_status_panel';
@ -85,6 +87,9 @@ function createStartMock(): TriggersAndActionsUIPublicPluginStart {
getAlertsTable: (props: AlertsTableProps) => {
return getAlertsTableLazy(props);
},
getFieldBrowser: (props: FieldBrowserProps) => {
return getFieldBrowserLazy(props);
},
getRuleStatusDropdown: (props) => {
return getRuleStatusDropdownLazy(props);
},

View file

@ -31,6 +31,7 @@ import { getEditConnectorFlyoutLazy } from './common/get_edit_connector_flyout';
import { getAddAlertFlyoutLazy } from './common/get_add_alert_flyout';
import { getEditAlertFlyoutLazy } from './common/get_edit_alert_flyout';
import { getAlertsTableLazy } from './common/get_alerts_table';
import { getFieldBrowserLazy } from './common/get_field_browser';
import { getRuleStatusDropdownLazy } from './common/get_rule_status_dropdown';
import { getRuleTagFilterLazy } from './common/get_rule_tag_filter';
import { getRuleStatusFilterLazy } from './common/get_rule_status_filter';
@ -71,6 +72,7 @@ import { PLUGIN_ID } from './common/constants';
import type { AlertsTableStateProps } from './application/sections/alerts_table/alerts_table_state';
import { getAlertsTableStateLazy } from './common/get_alerts_table_state';
import { ActionAccordionFormProps } from './application/sections/action_connector_form/action_form';
import type { FieldBrowserProps } from './application/sections/field_browser/types';
import { getRuleDefinitionLazy } from './common/get_rule_definition';
import { RuleStatusPanelProps } from './application/sections/rule_details/components/rule_status_panel';
@ -101,6 +103,7 @@ export interface TriggersAndActionsUIPublicPluginStart {
) => ReactElement<RuleEditProps>;
getAlertsTable: (props: AlertsTableProps) => ReactElement<AlertsTableProps>;
getAlertsStateTable: (props: AlertsTableStateProps) => ReactElement<AlertsTableStateProps>;
getFieldBrowser: (props: FieldBrowserProps) => ReactElement<FieldBrowserProps>;
getRuleStatusDropdown: (props: RuleStatusDropdownProps) => ReactElement<RuleStatusDropdownProps>;
getRuleTagFilter: (props: RuleTagFilterProps) => ReactElement<RuleTagFilterProps>;
getRuleStatusFilter: (props: RuleStatusFilterProps) => ReactElement<RuleStatusFilterProps>;
@ -308,6 +311,9 @@ export class Plugin
getAlertsTable: (props: AlertsTableProps) => {
return getAlertsTableLazy(props);
},
getFieldBrowser: (props: FieldBrowserProps) => {
return getFieldBrowserLazy(props);
},
getRuleStatusDropdown: (props: RuleStatusDropdownProps) => {
return getRuleStatusDropdownLazy(props);
},

View file

@ -58,6 +58,13 @@ import type { RuleEventLogListProps } from './application/sections/rule_details/
import type { CreateConnectorFlyoutProps } from './application/sections/action_connector_form/create_connector_flyout';
import type { EditConnectorFlyoutProps } from './application/sections/action_connector_form/edit_connector_flyout';
import type { RulesListNotifyBadgeProps } from './application/sections/rules_list/components/rules_list_notify_badge';
import type {
FieldBrowserOptions,
CreateFieldComponent,
GetFieldTableColumns,
FieldBrowserProps,
BrowserFieldItem,
} from './application/sections/field_browser/types';
// In Triggers and Actions we treat all `Alert`s as `SanitizedRule<RuleTypeParams>`
// so the `Params` is a black-box of Record<string, unknown>
@ -98,6 +105,11 @@ export type {
CreateConnectorFlyoutProps,
EditConnectorFlyoutProps,
RulesListNotifyBadgeProps,
FieldBrowserProps,
FieldBrowserOptions,
CreateFieldComponent,
GetFieldTableColumns,
BrowserFieldItem,
};
export type { ActionType, AsApiContract };
export {