[RAC] [TGrid] Field browser implemented in EuiDataGrid toolbar (#105207)

* tGid header using EuiDataGrid

* useFetchIndex migrated and column_headers refactor

* removed useless mock

* add badges translations

* i18n translations keys fixed

* code format

* filter default columns not present in field browser

* reset button to initial columns

* cleaning

* dependencies moved

* fix functional test with missing data service

* remove unused code (unrelated)

* fieldBrowser integration with security solutions timeline

* lint and translations cleaned

* timeline toolbar removed for merge & some test fixes

* type fix

* type fixes

* timeline static default colums

* limit size temporary increase

* limit size temporary increase

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Sergi Massaneda 2021-07-20 17:52:52 +02:00 committed by GitHub
parent d91c6d0cfb
commit a8fc9b462c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
58 changed files with 1195 additions and 869 deletions

View file

@ -107,7 +107,7 @@ pageLoadAssetSize:
dataVisualizer: 27530
banners: 17946
mapsEms: 26072
timelines: 251886
timelines: 330000
screenshotMode: 17856
visTypePie: 35583
expressionRevealImage: 25675

View file

@ -10,7 +10,6 @@ import {
FIELDS_BROWSER_FIELDS_COUNT,
FIELDS_BROWSER_HOST_CATEGORIES_COUNT,
FIELDS_BROWSER_HOST_GEO_CITY_NAME_HEADER,
FIELDS_BROWSER_HOST_GEO_COUNTRY_NAME_HEADER,
FIELDS_BROWSER_HEADER_HOST_GEO_CONTINENT_NAME_HEADER,
FIELDS_BROWSER_MESSAGE_HEADER,
FIELDS_BROWSER_SELECTED_CATEGORY_TITLE,
@ -24,7 +23,6 @@ import { cleanKibana } from '../../tasks/common';
import {
addsHostGeoCityNameToTimeline,
addsHostGeoContinentNameToTimeline,
addsHostGeoCountryNameToTimelineDraggingIt,
clearFieldsBrowser,
closeFieldsBrowser,
filterFieldsBrowser,
@ -156,18 +154,6 @@ describe('Fields Browser', () => {
cy.get(FIELDS_BROWSER_HOST_GEO_CITY_NAME_HEADER).should('exist');
});
it('adds a field to the timeline when the user drags and drops a field', () => {
const filterInput = 'host.geo.c';
filterFieldsBrowser(filterInput);
cy.get(FIELDS_BROWSER_HOST_GEO_COUNTRY_NAME_HEADER).should('not.exist');
addsHostGeoCountryNameToTimelineDraggingIt();
cy.get(FIELDS_BROWSER_HOST_GEO_COUNTRY_NAME_HEADER).should('exist');
});
it('resets all fields in the timeline when `Reset Fields` is clicked', () => {
const filterInput = 'host.geo.c';

View file

@ -209,7 +209,7 @@ describe('EventsViewer', () => {
<StatefulEventsViewer {...testProps} />
</TestProviders>
);
expect(wrapper.find(`[data-test-subj="show-field-browser"]`).first().exists()).toBe(true);
expect(wrapper.find(`[data-test-subj="field-browser"]`).first().exists()).toBe(true);
});
test('it renders the footer containing the pagination', () => {

View file

@ -181,11 +181,6 @@ export const getBreadcrumbsForRoute = (
}
if (isAdminRoutes(spyState) && object.navTabs) {
const tempNav: SearchNavTab = { urlKey: 'administration', isDetailPage: false };
let urlStateKeys = [getOr(tempNav, spyState.pageName, object.navTabs)];
if (spyState.tabName != null) {
urlStateKeys = [...urlStateKeys, getOr(tempNav, spyState.tabName, object.navTabs)];
}
return [siemRootBreadcrumb, ...getAdminBreadcrumbs(spyState)];
}

View file

@ -26,6 +26,7 @@ import {
import { ISearchStart } from '../../../../../../../src/plugins/data/public';
import { dataPluginMock } from '../../../../../../../src/plugins/data/public/mocks';
import { getTimelineTemplate } from '../../../timelines/containers/api';
import { defaultHeaders } from '../../../timelines/components/timeline/body/column_headers/default_headers';
jest.mock('../../../timelines/containers/api', () => ({
getTimelineTemplate: jest.fn(),
@ -139,6 +140,7 @@ describe('alert actions', () => {
initialWidth: 180,
},
],
defaultColumns: defaultHeaders,
dataProviders: [],
dateRange: {
end: '2018-11-05T19:03:25.937Z',

View file

@ -5,71 +5,21 @@
* 2.0.
*/
import {
EuiCheckbox,
EuiIcon,
EuiToolTip,
EuiFlexGroup,
EuiFlexItem,
EuiScreenReaderOnly,
} from '@elastic/eui';
import { isEmpty, uniqBy } from 'lodash/fp';
import { isEmpty } from 'lodash/fp';
import React, { useCallback, useRef, useState } from 'react';
import { Draggable } from 'react-beautiful-dnd';
import styled from 'styled-components';
import { DRAGGABLE_KEYBOARD_WRAPPER_CLASS_NAME } from '@kbn/securitysolution-t-grid';
import { BrowserField, BrowserFields } from '../../../common/containers/source';
import { DragEffects } from '../../../common/components/drag_and_drop/draggable_wrapper';
import { DroppableWrapper } from '../../../common/components/drag_and_drop/droppable_wrapper';
import {
DRAG_TYPE_FIELD,
DRAGGABLE_KEYBOARD_WRAPPER_CLASS_NAME,
getDraggableFieldId,
getDroppableId,
} from '../../../common/components/drag_and_drop/helpers';
import { DraggableFieldBadge } from '../../../common/components/draggables/field_badge';
import { getEmptyValue } from '../../../common/components/empty_value';
import {
getColumnsWithTimestamp,
getExampleText,
getIconFromType,
} from '../../../common/components/event_details/helpers';
import { defaultColumnHeaderType } from '../timeline/body/column_headers/default_headers';
import { DEFAULT_COLUMN_MIN_WIDTH } from '../timeline/body/constants';
import { OnUpdateColumns } from '../timeline/events';
import { TruncatableText } from '../../../common/components/truncatable_text';
} from '@kbn/securitysolution-t-grid';
import type { BrowserFields } from '../../../common/containers/source';
import { getColumnsWithTimestamp } from '../../../common/components/event_details/helpers';
import type { OnUpdateColumns } from '../timeline/events';
import { FieldName } from './field_name';
import * as i18n from './translations';
import { getAlertColumnHeader } from './helpers';
import { ColumnHeaderOptions } from '../../../../common';
import type { ColumnHeaderOptions } from '../../../../common';
import { useKibana } from '../../../common/lib/kibana';
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';
/**
* An item rendered in the table
*/
export interface FieldItem {
ariaRowindex?: number;
checkbox: React.ReactNode;
description: React.ReactNode;
field: React.ReactNode;
fieldId: string;
}
const DraggableFieldsBrowserFieldComponent = ({
browserFields,
categoryId,
@ -191,142 +141,3 @@ const DraggableFieldsBrowserFieldComponent = ({
export const DraggableFieldsBrowserField = React.memo(DraggableFieldsBrowserFieldComponent);
DraggableFieldsBrowserField.displayName = 'DraggableFieldsBrowserFieldComponent';
/**
* Returns the draggable fields, values, and descriptions shown when a user expands an event
*/
export const getFieldItems = ({
browserFields,
category,
categoryId,
columnHeaders,
highlight = '',
onUpdateColumns,
timelineId,
toggleColumn,
}: {
browserFields: BrowserFields;
category: Partial<BrowserField>;
categoryId: string;
columnHeaders: ColumnHeaderOptions[];
highlight?: string;
timelineId: string;
toggleColumn: (column: ColumnHeaderOptions) => void;
onUpdateColumns: OnUpdateColumns;
}): FieldItem[] =>
uniqBy('name', [
...Object.values(category != null && category.fields != null ? category.fields : {}),
]).map((field) => ({
checkbox: (
<EuiToolTip content={i18n.VIEW_COLUMN(field.name ?? '')}>
<EuiCheckbox
aria-label={i18n.VIEW_COLUMN(field.name ?? '')}
checked={columnHeaders.findIndex((c) => c.id === field.name) !== -1}
data-test-subj={`field-${field.name}-checkbox`}
data-colindex={1}
id={field.name ?? ''}
onChange={() =>
toggleColumn({
columnHeaderType: defaultColumnHeaderType,
id: field.name ?? '',
initialWidth: DEFAULT_COLUMN_MIN_WIDTH,
...getAlertColumnHeader(timelineId, field.name ?? ''),
})
}
/>
</EuiToolTip>
),
field: (
<EuiFlexGroup alignItems="center" gutterSize="none">
<EuiFlexItem grow={false}>
<EuiToolTip content={field.type}>
<TypeIcon
data-test-subj={`field-${field.name}-icon`}
type={getIconFromType(field.type ?? null)}
/>
</EuiToolTip>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<DroppableWrapper
droppableId={getDroppableId(
`field-browser-field-items-field-droppable-wrapper-${timelineId}-${categoryId}-${field.name}`
)}
key={`field-browser-field-items-field-droppable-wrapper-${timelineId}-${categoryId}-${field.name}`}
isDropDisabled={true}
type={DRAG_TYPE_FIELD}
renderClone={(provided) => (
<div
{...provided.draggableProps}
{...provided.dragHandleProps}
ref={provided.innerRef}
style={{ ...provided.draggableProps.style, zIndex: 9999 }}
tabIndex={-1}
>
<DragEffects>
<DraggableFieldBadge fieldId={field.name ?? ''} />
</DragEffects>
</div>
)}
>
<DraggableFieldsBrowserField
browserFields={browserFields}
categoryId={categoryId}
fieldName={field.name ?? ''}
fieldCategory={field.category ?? ''}
highlight={highlight}
onUpdateColumns={onUpdateColumns}
timelineId={timelineId}
toggleColumn={toggleColumn}
/>
</DroppableWrapper>
</EuiFlexItem>
</EuiFlexGroup>
),
description: (
<div data-colindex={3} tabIndex={0}>
<EuiToolTip content={field.description}>
<>
<EuiScreenReaderOnly data-test-subj="descriptionForScreenReaderOnly">
<p>{i18n.DESCRIPTION_FOR_FIELD(field.name ?? '')}</p>
</EuiScreenReaderOnly>
<TruncatableText>
<Description data-test-subj={`field-${field.name}-description`}>
{`${field.description ?? getEmptyValue()} ${getExampleText(field.example)}`}
</Description>
</TruncatableText>
</>
</EuiToolTip>
</div>
),
fieldId: field.name ?? '',
}));
/**
* Returns a table column template provided to the `EuiInMemoryTable`'s
* `columns` prop
*/
export const getFieldColumns = () => [
{
field: 'checkbox',
name: '',
render: (checkbox: React.ReactNode, _: FieldItem) => checkbox,
sortable: false,
width: '25px',
},
{
field: 'field',
name: i18n.FIELD,
render: (field: React.ReactNode, _: FieldItem) => field,
sortable: false,
width: '225px',
},
{
field: 'description',
name: i18n.DESCRIPTION,
render: (description: React.ReactNode, _: FieldItem) => description,
sortable: false,
truncateText: true,
width: '400px',
},
];

View file

@ -51,6 +51,7 @@ import {
mockTemplate as mockSelectedTemplate,
} from './__mocks__';
import { getTimeline } from '../../containers/api';
import { defaultHeaders } from '../timeline/body/column_headers/default_headers';
jest.mock('../../../common/store/inputs/actions');
jest.mock('../../../common/components/url_state/normalize_time_range.ts');
@ -236,6 +237,49 @@ describe('helpers', () => {
});
describe('#defaultTimelineToTimelineModel', () => {
const columns = [
{
columnHeaderType: 'not-filtered',
id: '@timestamp',
type: 'number',
initialWidth: 190,
},
{
columnHeaderType: 'not-filtered',
id: 'message',
initialWidth: 180,
},
{
columnHeaderType: 'not-filtered',
id: 'event.category',
initialWidth: 180,
},
{
columnHeaderType: 'not-filtered',
id: 'event.action',
initialWidth: 180,
},
{
columnHeaderType: 'not-filtered',
id: 'host.name',
initialWidth: 180,
},
{
columnHeaderType: 'not-filtered',
id: 'source.ip',
initialWidth: 180,
},
{
columnHeaderType: 'not-filtered',
id: 'destination.ip',
initialWidth: 180,
},
{
columnHeaderType: 'not-filtered',
id: 'user.name',
initialWidth: 180,
},
];
test('if title is null, we should get the default title', () => {
const timeline = {
savedObjectId: 'savedObject-1',
@ -247,49 +291,8 @@ describe('helpers', () => {
expect(newTimeline).toEqual({
activeTab: TimelineTabs.query,
prevActiveTab: TimelineTabs.query,
columns: [
{
columnHeaderType: 'not-filtered',
id: '@timestamp',
type: 'number',
initialWidth: 190,
},
{
columnHeaderType: 'not-filtered',
id: 'message',
initialWidth: 180,
},
{
columnHeaderType: 'not-filtered',
id: 'event.category',
initialWidth: 180,
},
{
columnHeaderType: 'not-filtered',
id: 'event.action',
initialWidth: 180,
},
{
columnHeaderType: 'not-filtered',
id: 'host.name',
initialWidth: 180,
},
{
columnHeaderType: 'not-filtered',
id: 'source.ip',
initialWidth: 180,
},
{
columnHeaderType: 'not-filtered',
id: 'destination.ip',
initialWidth: 180,
},
{
columnHeaderType: 'not-filtered',
id: 'user.name',
initialWidth: 180,
},
],
columns,
defaultColumns: defaultHeaders,
dataProviders: [],
dateRange: { start: '2020-07-07T08:20:18.966Z', end: '2020-07-08T08:20:18.966Z' },
description: '',
@ -358,49 +361,8 @@ describe('helpers', () => {
expect(newTimeline).toEqual({
activeTab: TimelineTabs.query,
prevActiveTab: TimelineTabs.query,
columns: [
{
columnHeaderType: 'not-filtered',
id: '@timestamp',
type: 'number',
initialWidth: 190,
},
{
columnHeaderType: 'not-filtered',
id: 'message',
initialWidth: 180,
},
{
columnHeaderType: 'not-filtered',
id: 'event.category',
initialWidth: 180,
},
{
columnHeaderType: 'not-filtered',
id: 'event.action',
initialWidth: 180,
},
{
columnHeaderType: 'not-filtered',
id: 'host.name',
initialWidth: 180,
},
{
columnHeaderType: 'not-filtered',
id: 'source.ip',
initialWidth: 180,
},
{
columnHeaderType: 'not-filtered',
id: 'destination.ip',
initialWidth: 180,
},
{
columnHeaderType: 'not-filtered',
id: 'user.name',
initialWidth: 180,
},
],
columns,
defaultColumns: defaultHeaders,
dataProviders: [],
dateRange: { start: '2020-07-07T08:20:18.966Z', end: '2020-07-08T08:20:18.966Z' },
description: '',
@ -469,49 +431,8 @@ describe('helpers', () => {
expect(newTimeline).toEqual({
activeTab: TimelineTabs.query,
prevActiveTab: TimelineTabs.query,
columns: [
{
columnHeaderType: 'not-filtered',
id: '@timestamp',
type: 'number',
initialWidth: 190,
},
{
columnHeaderType: 'not-filtered',
id: 'message',
initialWidth: 180,
},
{
columnHeaderType: 'not-filtered',
id: 'event.category',
initialWidth: 180,
},
{
columnHeaderType: 'not-filtered',
id: 'event.action',
initialWidth: 180,
},
{
columnHeaderType: 'not-filtered',
id: 'host.name',
initialWidth: 180,
},
{
columnHeaderType: 'not-filtered',
id: 'source.ip',
initialWidth: 180,
},
{
columnHeaderType: 'not-filtered',
id: 'destination.ip',
initialWidth: 180,
},
{
columnHeaderType: 'not-filtered',
id: 'user.name',
initialWidth: 180,
},
],
columns,
defaultColumns: defaultHeaders,
dataProviders: [],
dateRange: { start: '2020-07-07T08:20:18.966Z', end: '2020-07-08T08:20:18.966Z' },
description: '',
@ -578,49 +499,8 @@ describe('helpers', () => {
expect(newTimeline).toEqual({
activeTab: TimelineTabs.query,
prevActiveTab: TimelineTabs.query,
columns: [
{
columnHeaderType: 'not-filtered',
id: '@timestamp',
type: 'number',
initialWidth: 190,
},
{
columnHeaderType: 'not-filtered',
id: 'message',
initialWidth: 180,
},
{
columnHeaderType: 'not-filtered',
id: 'event.category',
initialWidth: 180,
},
{
columnHeaderType: 'not-filtered',
id: 'event.action',
initialWidth: 180,
},
{
columnHeaderType: 'not-filtered',
id: 'host.name',
initialWidth: 180,
},
{
columnHeaderType: 'not-filtered',
id: 'source.ip',
initialWidth: 180,
},
{
columnHeaderType: 'not-filtered',
id: 'destination.ip',
initialWidth: 180,
},
{
columnHeaderType: 'not-filtered',
id: 'user.name',
initialWidth: 180,
},
],
columns,
defaultColumns: defaultHeaders,
dataProviders: [],
dateRange: { start: '2020-07-07T08:20:18.966Z', end: '2020-07-08T08:20:18.966Z' },
description: '',
@ -677,9 +557,12 @@ describe('helpers', () => {
});
test('should merge columns when event.action is deleted without two extra column names of user.name', () => {
const columnsWithoutEventAction = timelineDefaults.columns.filter(
(column) => column.id !== 'event.action'
);
const timeline = {
savedObjectId: 'savedObject-1',
columns: timelineDefaults.columns.filter((column) => column.id !== 'event.action'),
columns: columnsWithoutEventAction,
version: '1',
};
@ -688,85 +571,8 @@ describe('helpers', () => {
activeTab: TimelineTabs.query,
prevActiveTab: TimelineTabs.query,
savedObjectId: 'savedObject-1',
columns: [
{
aggregatable: undefined,
category: undefined,
columnHeaderType: 'not-filtered',
description: undefined,
example: undefined,
id: '@timestamp',
placeholder: undefined,
type: 'number',
initialWidth: 190,
},
{
aggregatable: undefined,
category: undefined,
columnHeaderType: 'not-filtered',
description: undefined,
example: undefined,
id: 'message',
placeholder: undefined,
type: undefined,
initialWidth: 180,
},
{
aggregatable: undefined,
category: undefined,
columnHeaderType: 'not-filtered',
description: undefined,
example: undefined,
id: 'event.category',
placeholder: undefined,
type: undefined,
initialWidth: 180,
},
{
aggregatable: undefined,
category: undefined,
columnHeaderType: 'not-filtered',
description: undefined,
example: undefined,
id: 'host.name',
placeholder: undefined,
type: undefined,
initialWidth: 180,
},
{
aggregatable: undefined,
category: undefined,
columnHeaderType: 'not-filtered',
description: undefined,
example: undefined,
id: 'source.ip',
placeholder: undefined,
type: undefined,
initialWidth: 180,
},
{
aggregatable: undefined,
category: undefined,
columnHeaderType: 'not-filtered',
description: undefined,
example: undefined,
id: 'destination.ip',
placeholder: undefined,
type: undefined,
initialWidth: 180,
},
{
aggregatable: undefined,
category: undefined,
columnHeaderType: 'not-filtered',
description: undefined,
example: undefined,
id: 'user.name',
placeholder: undefined,
type: undefined,
initialWidth: 180,
},
],
columns: columnsWithoutEventAction,
defaultColumns: defaultHeaders,
version: '1',
dataProviders: [],
dateRange: { start: '2020-07-07T08:20:18.966Z', end: '2020-07-08T08:20:18.966Z' },
@ -822,9 +628,12 @@ describe('helpers', () => {
});
test('should merge filters object back with json object', () => {
const columnsWithoutEventAction = timelineDefaults.columns.filter(
(column) => column.id !== 'event.action'
);
const timeline = {
savedObjectId: 'savedObject-1',
columns: timelineDefaults.columns.filter((column) => column.id !== 'event.action'),
columns: columnsWithoutEventAction,
filters: [
{
meta: {
@ -865,44 +674,8 @@ describe('helpers', () => {
activeTab: TimelineTabs.query,
prevActiveTab: TimelineTabs.query,
savedObjectId: 'savedObject-1',
columns: [
{
columnHeaderType: 'not-filtered',
id: '@timestamp',
type: 'number',
initialWidth: 190,
},
{
columnHeaderType: 'not-filtered',
id: 'message',
initialWidth: 180,
},
{
columnHeaderType: 'not-filtered',
id: 'event.category',
initialWidth: 180,
},
{
columnHeaderType: 'not-filtered',
id: 'host.name',
initialWidth: 180,
},
{
columnHeaderType: 'not-filtered',
id: 'source.ip',
initialWidth: 180,
},
{
columnHeaderType: 'not-filtered',
id: 'destination.ip',
initialWidth: 180,
},
{
columnHeaderType: 'not-filtered',
id: 'user.name',
initialWidth: 180,
},
],
columns: columnsWithoutEventAction,
defaultColumns: defaultHeaders,
version: '1',
dateRange: { start: '2020-07-07T08:20:18.966Z', end: '2020-07-08T08:20:18.966Z' },
dataProviders: [],
@ -1013,49 +786,8 @@ describe('helpers', () => {
expect(newTimeline).toEqual({
activeTab: TimelineTabs.query,
prevActiveTab: TimelineTabs.query,
columns: [
{
columnHeaderType: 'not-filtered',
id: '@timestamp',
type: 'number',
initialWidth: 190,
},
{
columnHeaderType: 'not-filtered',
id: 'message',
initialWidth: 180,
},
{
columnHeaderType: 'not-filtered',
id: 'event.category',
initialWidth: 180,
},
{
columnHeaderType: 'not-filtered',
id: 'event.action',
initialWidth: 180,
},
{
columnHeaderType: 'not-filtered',
id: 'host.name',
initialWidth: 180,
},
{
columnHeaderType: 'not-filtered',
id: 'source.ip',
initialWidth: 180,
},
{
columnHeaderType: 'not-filtered',
id: 'destination.ip',
initialWidth: 180,
},
{
columnHeaderType: 'not-filtered',
id: 'user.name',
initialWidth: 180,
},
],
columns,
defaultColumns: defaultHeaders,
dataProviders: [],
dateRange: { end: '2020-10-28T11:37:31.655Z', start: '2020-10-27T11:37:31.655Z' },
description: '',
@ -1124,49 +856,8 @@ describe('helpers', () => {
expect(newTimeline).toEqual({
activeTab: TimelineTabs.query,
prevActiveTab: TimelineTabs.query,
columns: [
{
columnHeaderType: 'not-filtered',
id: '@timestamp',
type: 'number',
initialWidth: 190,
},
{
columnHeaderType: 'not-filtered',
id: 'message',
initialWidth: 180,
},
{
columnHeaderType: 'not-filtered',
id: 'event.category',
initialWidth: 180,
},
{
columnHeaderType: 'not-filtered',
id: 'event.action',
initialWidth: 180,
},
{
columnHeaderType: 'not-filtered',
id: 'host.name',
initialWidth: 180,
},
{
columnHeaderType: 'not-filtered',
id: 'source.ip',
initialWidth: 180,
},
{
columnHeaderType: 'not-filtered',
id: 'destination.ip',
initialWidth: 180,
},
{
columnHeaderType: 'not-filtered',
id: 'user.name',
initialWidth: 180,
},
],
columns,
defaultColumns: defaultHeaders,
dataProviders: [],
dateRange: { end: '2020-07-08T08:20:18.966Z', start: '2020-07-07T08:20:18.966Z' },
description: '',

View file

@ -257,6 +257,7 @@ export const defaultTimelineToTimelineModel = (
const timelineEntries = {
...timeline,
columns: timeline.columns != null ? timeline.columns.map(setTimelineColumn) : defaultHeaders,
defaultColumns: defaultHeaders,
dateRange:
timeline.status === TimelineStatus.immutable &&
timeline.timelineType === TimelineType.template

View file

@ -29,14 +29,13 @@ import {
useTimelineFullScreen,
} from '../../../../../common/containers/use_full_screen';
import { DEFAULT_ICON_BUTTON_WIDTH } from '../../helpers';
import { StatefulFieldsBrowser } from '../../../fields_browser';
import { StatefulRowRenderersBrowser } from '../../../row_renderers_browser';
import { FIELD_BROWSER_HEIGHT, FIELD_BROWSER_WIDTH } from '../../../fields_browser/helpers';
import { EventsTh, EventsThContent } from '../../styles';
import { EventsSelect } from '../column_headers/events_select';
import * as i18n from '../column_headers/translations';
import { timelineActions } from '../../../../store/timeline';
import { isFullScreen } from '../column_headers';
import { useKibana } from '../../../../../common/lib/kibana';
const SortingColumnsContainer = styled.div`
button {
@ -65,6 +64,7 @@ const HeaderActionsComponent: React.FC<HeaderActionProps> = ({
tabType,
timelineId,
}) => {
const { timelines: timelinesUi } = useKibana().services;
const { globalFullScreen, setGlobalFullScreen } = useGlobalFullScreen();
const { timelineFullScreen, setTimelineFullScreen } = useTimelineFullScreen();
const dispatch = useDispatch();
@ -154,14 +154,11 @@ const HeaderActionsComponent: React.FC<HeaderActionProps> = ({
)}
<EventsTh role="button">
<StatefulFieldsBrowser
browserFields={browserFields}
columnHeaders={columnHeaders}
data-test-subj="field-browser"
height={FIELD_BROWSER_HEIGHT}
timelineId={timelineId}
width={FIELD_BROWSER_WIDTH}
/>
{timelinesUi.getFieldBrowser({
browserFields,
columnHeaders,
timelineId,
})}
</EventsTh>
<EventsTh role="button">

View file

@ -23,17 +23,19 @@ export const getColumnHeaders = (
headers: ColumnHeaderOptions[],
browserFields: BrowserFields
): ColumnHeaderOptions[] => {
return headers.map((header) => {
const splitHeader = header.id.split('.'); // source.geo.city_name -> [source, geo, city_name]
return headers
? headers.map((header) => {
const splitHeader = header.id.split('.'); // source.geo.city_name -> [source, geo, city_name]
return {
...header,
...get(
[splitHeader.length > 1 ? splitHeader[0] : 'base', 'fields', header.id],
browserFields
),
};
});
return {
...header,
...get(
[splitHeader.length > 1 ? splitHeader[0] : 'base', 'fields', header.id],
browserFields
),
};
})
: [];
};
export const getColumnWidthFromType = (type: string): number =>

View file

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

View file

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

View file

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

View file

@ -0,0 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`EmptyValue it renders against snapshot 1`] = `
<p>
(Empty String)
</p>
`;

View file

@ -0,0 +1,166 @@
/*
* 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 { mount, shallow } from 'enzyme';
import React from 'react';
import { ThemeProvider } from 'styled-components';
import { mountWithIntl } from '@kbn/test/jest';
import {
defaultToEmptyTag,
getEmptyString,
getEmptyStringTag,
getEmptyTagValue,
getEmptyValue,
getOrEmptyTag,
} from '.';
import { getMockTheme } from '../../mock/kibana_react.mock';
describe('EmptyValue', () => {
const mockTheme = getMockTheme({ eui: { euiColorMediumShade: '#ece' } });
test('it renders against snapshot', () => {
const wrapper = shallow(<p>{getEmptyString()}</p>);
expect(wrapper).toMatchSnapshot();
});
describe('#getEmptyValue', () => {
test('should return an empty value', () => expect(getEmptyValue()).toBe('—'));
});
describe('#getEmptyString', () => {
test('should turn into an empty string place holder', () => {
const wrapper = mountWithIntl(
<ThemeProvider theme={mockTheme}>
<p>{getEmptyString()}</p>
</ThemeProvider>
);
expect(wrapper.text()).toBe('(Empty String)');
});
});
describe('#getEmptyTagValue', () => {
const wrapper = mount(
<ThemeProvider theme={mockTheme}>
<p>{getEmptyTagValue()}</p>
</ThemeProvider>
);
test('should return an empty tag value', () => expect(wrapper.text()).toBe('—'));
});
describe('#getEmptyStringTag', () => {
test('should turn into an span that has length of 1', () => {
const wrapper = mountWithIntl(
<ThemeProvider theme={mockTheme}>
<p>{getEmptyStringTag()}</p>
</ThemeProvider>
);
expect(wrapper.find('span')).toHaveLength(1);
});
test('should turn into an empty string tag place holder', () => {
const wrapper = mountWithIntl(
<ThemeProvider theme={mockTheme}>
<p>{getEmptyStringTag()}</p>
</ThemeProvider>
);
expect(wrapper.text()).toBe(getEmptyString());
});
});
describe('#defaultToEmptyTag', () => {
test('should default to an empty value when a value is null', () => {
const wrapper = mount(
<ThemeProvider theme={mockTheme}>
<p>{defaultToEmptyTag(null)}</p>
</ThemeProvider>
);
expect(wrapper.text()).toBe(getEmptyValue());
});
test('should default to an empty value when a value is undefined', () => {
const wrapper = mount(
<ThemeProvider theme={mockTheme}>
<p>{defaultToEmptyTag(undefined)}</p>
</ThemeProvider>
);
expect(wrapper.text()).toBe(getEmptyValue());
});
test('should return a deep path value', () => {
const test = {
a: {
b: {
c: 1,
},
},
};
const wrapper = mount(<p>{defaultToEmptyTag(test.a.b.c)}</p>);
expect(wrapper.text()).toBe('1');
});
});
describe('#getOrEmptyTag', () => {
test('should default empty value when a deep rooted value is null', () => {
const test = {
a: {
b: {
c: null,
},
},
};
const wrapper = mount(
<ThemeProvider theme={mockTheme}>
<p>{getOrEmptyTag('a.b.c', test)}</p>
</ThemeProvider>
);
expect(wrapper.text()).toBe(getEmptyValue());
});
test('should default empty value when a deep rooted value is undefined', () => {
const test = {
a: {
b: {
c: undefined,
},
},
};
const wrapper = mount(
<ThemeProvider theme={mockTheme}>
<p>{getOrEmptyTag('a.b.c', test)}</p>
</ThemeProvider>
);
expect(wrapper.text()).toBe(getEmptyValue());
});
test('should default empty value when a deep rooted value is missing', () => {
const test = {
a: {
b: {},
},
};
const wrapper = mount(
<ThemeProvider theme={mockTheme}>
<p>{getOrEmptyTag('a.b.c', test)}</p>
</ThemeProvider>
);
expect(wrapper.text()).toBe(getEmptyValue());
});
test('should return a deep path value', () => {
const test = {
a: {
b: {
c: 1,
},
},
};
const wrapper = mount(<p>{getOrEmptyTag('a.b.c', test)}</p>);
expect(wrapper.text()).toBe('1');
});
});
});

View file

@ -0,0 +1,49 @@
/*
* 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 { get, isString } from 'lodash/fp';
import React from 'react';
import styled from 'styled-components';
import * as i18n from './translations';
const EmptyWrapper = styled.span`
color: ${(props) => props.theme.eui.euiColorMediumShade};
`;
EmptyWrapper.displayName = 'EmptyWrapper';
export const getEmptyValue = () => '—';
export const getEmptyString = () => `(${i18n.EMPTY_STRING})`;
export const getEmptyTagValue = () => <EmptyWrapper>{getEmptyValue()}</EmptyWrapper>;
export const getEmptyStringTag = () => <EmptyWrapper>{getEmptyString()}</EmptyWrapper>;
export const defaultToEmptyTag = <T extends unknown>(item: T): JSX.Element => {
if (item == null) {
return getEmptyTagValue();
} else if (isString(item) && item === '') {
return getEmptyStringTag();
} else {
return <>{item}</>;
}
};
export const getOrEmptyTag = (path: string, item: unknown): JSX.Element => {
const text = get(path, item);
return getOrEmptyTagFromValue(text);
};
export const getOrEmptyTagFromValue = (value: string | number | null | undefined): JSX.Element => {
if (value == null) {
return getEmptyTagValue();
} else if (value === '') {
return getEmptyStringTag();
} else {
return <>{value}</>;
}
};

View file

@ -0,0 +1,12 @@
/*
* 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 EMPTY_STRING = i18n.translate('xpack.timelines.emptyString.emptyStringDescription', {
defaultMessage: 'Empty String',
});

View file

@ -0,0 +1,48 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import type { Store } from 'redux';
import { Provider } from 'react-redux';
import { I18nProvider } from '@kbn/i18n/react';
import type { FieldBrowserProps } from '../t_grid/toolbar/fields_browser/types';
import { StatefulFieldsBrowser } from '../t_grid/toolbar/fields_browser';
import {
FIELD_BROWSER_WIDTH,
FIELD_BROWSER_HEIGHT,
} from '../t_grid/toolbar/fields_browser/helpers';
const EMPTY_BROWSER_FIELDS = {};
export type FieldBrowserWrappedProps = Omit<FieldBrowserProps, 'width' | 'height'> & {
width?: FieldBrowserProps['width'];
height?: FieldBrowserProps['height'];
};
export type FieldBrowserWrappedComponentProps = FieldBrowserWrappedProps & {
store: Store;
};
export const FieldBrowserWrappedComponent = (props: FieldBrowserWrappedComponentProps) => {
const { store, ...restProps } = props;
const fieldsBrowseProps = {
width: FIELD_BROWSER_WIDTH,
height: FIELD_BROWSER_HEIGHT,
...restProps,
browserFields: restProps.browserFields ?? EMPTY_BROWSER_FIELDS,
};
return (
<Provider store={store}>
<I18nProvider>
<StatefulFieldsBrowser {...fieldsBrowseProps} />
</I18nProvider>
</Provider>
);
};
FieldBrowserWrappedComponent.displayName = 'FieldBrowserWrappedComponent';
// eslint-disable-next-line import/no-default-export
export { FieldBrowserWrappedComponent as default };

View file

@ -8,17 +8,17 @@
import React from 'react';
import { Provider } from 'react-redux';
import { I18nProvider } from '@kbn/i18n/react';
import { Store } from 'redux';
import type { Store } from 'redux';
import { Storage } from '../../../../../src/plugins/kibana_utils/public';
import { DataPublicPluginStart } from '../../../../../src/plugins/data/public';
import type { DataPublicPluginStart } from '../../../../../src/plugins/data/public';
import { createStore } from '../store/t_grid';
import { TGrid as TGridComponent } from './tgrid';
import { TGridProps } from '../types';
import type { TGridProps } from '../types';
import { DragDropContextWrapper } from './drag_and_drop';
import { initialTGridState } from '../store/t_grid/reducer';
import { TGridIntegratedProps } from './t_grid/integrated';
import type { TGridIntegratedProps } from './t_grid/integrated';
const EMPTY_BROWSER_FIELDS = {};
@ -58,3 +58,4 @@ export * from './drag_and_drop';
export * from './draggables';
export * from './last_updated';
export * from './loading';
export * from './fields_browser';

View file

@ -41,13 +41,12 @@ import { Footer, footerHeight } from '../footer';
import { SELECTOR_TIMELINE_GLOBAL_CONTAINER } from '../styles';
import * as i18n from './translations';
import { InspectButtonContainer } from '../../inspect';
import { useFetchIndex } from '../../../container/source';
export const EVENTS_VIEWER_HEADER_HEIGHT = 90; // px
const UTILITY_BAR_HEIGHT = 19; // px
const COMPACT_HEADER_HEIGHT = EVENTS_VIEWER_HEADER_HEIGHT - UTILITY_BAR_HEIGHT; // px
const STANDALONE_ID = 'standalone-t-grid';
const EMPTY_BROWSER_FIELDS = {};
const EMPTY_INDEX_PATTERN = { title: '', fields: [] };
const EMPTY_DATA_PROVIDERS: DataProvider[] = [];
const UtilityBar = styled.div`
@ -157,6 +156,7 @@ const TGridStandaloneComponent: React.FC<TGridStandaloneProps> = ({
const columnsHeader = isEmpty(columns) ? defaultHeaders : columns;
const { uiSettings } = useKibana<CoreStart>().services;
const [isQueryLoading, setIsQueryLoading] = useState(false);
const [indexPatternsLoading, { browserFields, indexPatterns }] = useFetchIndex(indexNames);
const getTGrid = useMemo(() => tGridSelectors.getTGridByIdSelector(), []);
const {
@ -171,20 +171,24 @@ const TGridStandaloneComponent: React.FC<TGridStandaloneProps> = ({
const justTitle = useMemo(() => <TitleText data-test-subj="title">{title}</TitleText>, [title]);
const combinedQueries = combineQueries({
config: esQuery.getEsQueryConfig(uiSettings),
dataProviders: EMPTY_DATA_PROVIDERS,
indexPattern: EMPTY_INDEX_PATTERN,
browserFields: EMPTY_BROWSER_FIELDS,
filters,
kqlQuery: query,
kqlMode: 'search',
isEventViewer: true,
});
const combinedQueries = useMemo(
() =>
combineQueries({
config: esQuery.getEsQueryConfig(uiSettings),
dataProviders: EMPTY_DATA_PROVIDERS,
indexPattern: indexPatterns,
browserFields,
filters,
kqlQuery: query,
kqlMode: 'search',
isEventViewer: true,
}),
[uiSettings, indexPatterns, browserFields, filters, query]
);
const canQueryTimeline = useMemo(
() => combinedQueries != null && !isEmpty(start) && !isEmpty(end),
[combinedQueries, start, end]
() => !indexPatternsLoading && combinedQueries != null && !isEmpty(start) && !isEmpty(end),
[indexPatternsLoading, combinedQueries, start, end]
);
const fields = useMemo(
@ -280,8 +284,9 @@ const TGridStandaloneComponent: React.FC<TGridStandaloneProps> = ({
);
dispatch(
tGridActions.initializeTGridSettings({
footerText,
id: STANDALONE_ID,
defaultColumns: columns,
footerText,
loadingText,
unit,
})
@ -316,7 +321,7 @@ const TGridStandaloneComponent: React.FC<TGridStandaloneProps> = ({
<ScrollableFlexItem grow={1}>
<StatefulBody
activePage={pageInfo.activePage}
browserFields={EMPTY_BROWSER_FIELDS}
browserFields={browserFields}
data={nonDeletedEvents}
id={STANDALONE_ID}
isEventViewer={true}

View file

@ -8,7 +8,7 @@
import { mount } from 'enzyme';
import React from 'react';
import { mockBrowserFields } from '../../../common/containers/source/mock';
import { mockBrowserFields } from '../../../../mock';
import { CATEGORY_PANE_WIDTH } from './helpers';
import { CategoriesPane } from './categories_pane';

View file

@ -9,14 +9,13 @@ import { EuiInMemoryTable, EuiTitle } from '@elastic/eui';
import { noop } from 'lodash/fp';
import React, { useCallback, useRef } from 'react';
import styled from 'styled-components';
import {
DATA_COLINDEX_ATTRIBUTE,
DATA_ROWINDEX_ATTRIBUTE,
onKeyDownFocusHandler,
} from '../../../../../timelines/public';
import { BrowserFields } from '../../../common/containers/source';
} from '../../../../../common';
// eslint-disable-next-line no-duplicate-imports
import type { BrowserFields } from '../../../../../common';
import { getCategoryColumns } from './category_columns';
import { CATEGORIES_PANE_CLASS_NAME, TABLE_HEIGHT } from './helpers';

View file

@ -7,19 +7,15 @@
import React from 'react';
import '../../../common/mock/match_media';
import { mockBrowserFields } from '../../../common/containers/source/mock';
import { useMountAppended } from '../../../utils/use_mount_appended';
import { Category } from './category';
import { getFieldItems } from './field_items';
import { FIELDS_PANE_WIDTH } from './helpers';
import { TestProviders } from '../../../common/mock';
import { useMountAppended } from '../../../common/utils/use_mount_appended';
import { mockBrowserFields, TestProviders } from '../../../../mock';
import * as i18n from './translations';
jest.mock('../../../common/lib/kibana');
describe('Category', () => {
const timelineId = 'test';
const selectedCategoryId = 'client';
@ -33,12 +29,9 @@ describe('Category', () => {
data-test-subj="category"
filteredBrowserFields={mockBrowserFields}
fieldItems={getFieldItems({
browserFields: mockBrowserFields,
category: mockBrowserFields[selectedCategoryId],
categoryId: selectedCategoryId,
columnHeaders: [],
highlight: '',
onUpdateColumns: jest.fn(),
timelineId,
toggleColumn: jest.fn(),
})}
@ -63,12 +56,9 @@ describe('Category', () => {
data-test-subj="category"
filteredBrowserFields={mockBrowserFields}
fieldItems={getFieldItems({
browserFields: mockBrowserFields,
category: mockBrowserFields[selectedCategoryId],
categoryId: selectedCategoryId,
columnHeaders: [],
highlight: '',
onUpdateColumns: jest.fn(),
timelineId,
toggleColumn: jest.fn(),
})}
@ -91,12 +81,9 @@ describe('Category', () => {
data-test-subj="category"
filteredBrowserFields={mockBrowserFields}
fieldItems={getFieldItems({
browserFields: mockBrowserFields,
category: mockBrowserFields[selectedCategoryId],
categoryId: selectedCategoryId,
columnHeaders: [],
highlight: '',
onUpdateColumns: jest.fn(),
timelineId,
toggleColumn: jest.fn(),
})}

View file

@ -14,13 +14,14 @@ import {
DATA_COLINDEX_ATTRIBUTE,
DATA_ROWINDEX_ATTRIBUTE,
onKeyDownFocusHandler,
} from '../../../../../timelines/public';
import { BrowserFields } from '../../../common/containers/source';
import { OnUpdateColumns } from '../timeline/events';
} from '../../../../../common';
// eslint-disable-next-line no-duplicate-imports
import type { BrowserFields, OnUpdateColumns } from '../../../../../common';
import { CategoryTitle } from './category_title';
import { FieldItem, getFieldColumns } from './field_items';
import { getFieldColumns } from './field_items';
// eslint-disable-next-line no-duplicate-imports
import type { FieldItem } from './field_items';
import { CATEGORY_TABLE_CLASS_NAME, TABLE_HEIGHT } from './helpers';
import * as i18n from './translations';

View file

@ -8,7 +8,7 @@
import { mount } from 'enzyme';
import React from 'react';
import { mockBrowserFields } from '../../../common/containers/source/mock';
import { mockBrowserFields } from '../../../../mock';
import { CATEGORY_PANE_WIDTH, getFieldCount } from './helpers';
import { CategoriesPane } from './categories_pane';

View file

@ -18,19 +18,18 @@ import {
import React, { useCallback, useMemo } from 'react';
import styled from 'styled-components';
import { useDeepEqualSelector } from '../../../common/hooks/use_selector';
import { BrowserFields } from '../../../common/containers/source';
import { getColumnsWithTimestamp } from '../../../common/components/event_details/helpers';
import { CountBadge } from '../../../common/components/page';
import { OnUpdateColumns } from '../timeline/events';
import { useDeepEqualSelector } from '../../../../hooks/use_selector';
import {
LoadingSpinner,
getCategoryPaneCategoryClassName,
getFieldCount,
VIEW_ALL_BUTTON_CLASS_NAME,
CountBadge,
} from './helpers';
import * as i18n from './translations';
import { timelineSelectors } from '../../store/timeline';
import { tGridSelectors } from '../../../../store/t_grid';
import { getColumnsWithTimestamp } from '../../../utils/helpers';
import type { OnUpdateColumns, BrowserFields } from '../../../../../common';
const CategoryName = styled.span<{ bold: boolean }>`
.euiText {
@ -68,7 +67,7 @@ interface ViewAllButtonProps {
export const ViewAllButton = React.memo<ViewAllButtonProps>(
({ categoryId, browserFields, onUpdateColumns, timelineId }) => {
const getManageTimeline = useMemo(() => timelineSelectors.getManageTimelineById(), []);
const getManageTimeline = useMemo(() => tGridSelectors.getManageTimelineById(), []);
const { isLoading } = useDeepEqualSelector((state) =>
getManageTimeline(state, timelineId ?? '')
);

View file

@ -8,8 +8,7 @@
import { mount } from 'enzyme';
import React from 'react';
import { mockBrowserFields } from '../../../common/containers/source/mock';
import { TestProviders } from '../../../common/mock';
import { mockBrowserFields, TestProviders } from '../../../../mock';
import { CategoryTitle } from './category_title';
import { getFieldCount } from './helpers';

View file

@ -8,10 +8,8 @@
import { EuiFlexGroup, EuiFlexItem, EuiScreenReaderOnly, EuiTitle } from '@elastic/eui';
import React from 'react';
import { BrowserFields } from '../../../common/containers/source';
import { getFieldBrowserCategoryTitleClassName, getFieldCount } from './helpers';
import { CountBadge } from '../../../common/components/page';
import { OnUpdateColumns } from '../timeline/events';
import { CountBadge, getFieldBrowserCategoryTitleClassName, getFieldCount } from './helpers';
import type { BrowserFields, OnUpdateColumns } from '../../../../../common';
import { ViewAllButton } from './category_columns';
import * as i18n from './translations';

View file

@ -8,9 +8,7 @@
import { mount } from 'enzyme';
import React from 'react';
import '../../../common/mock/match_media';
import { mockBrowserFields } from '../../../common/containers/source/mock';
import { TestProviders } from '../../../common/mock';
import { TestProviders, mockBrowserFields } from '../../../../mock';
import { FieldsBrowser } from './field_browser';
import { FIELD_BROWSER_HEIGHT, FIELD_BROWSER_WIDTH } from './helpers';

View file

@ -19,8 +19,9 @@ import { noop } from 'lodash/fp';
import styled from 'styled-components';
import { useDispatch } from 'react-redux';
import { isEscape, isTab, stopPropagationAndPreventDefault } from '../../../../../timelines/public';
import { BrowserFields } from '../../../common/containers/source';
import type { BrowserFields, ColumnHeaderOptions } from '../../../../../common';
// eslint-disable-next-line no-duplicate-imports
import { isEscape, isTab, stopPropagationAndPreventDefault } from '../../../../../common';
import { CategoriesPane } from './categories_pane';
import { FieldsPane } from './fields_pane';
import { Header } from './header';
@ -33,11 +34,10 @@ import {
PANES_FLEX_GROUP_WIDTH,
scrollCategoriesPane,
} from './helpers';
import { FieldBrowserProps, OnHideFieldBrowser } from './types';
import { timelineActions } from '../../store/timeline';
import type { FieldBrowserProps, OnHideFieldBrowser } from './types';
import { tGridActions } from '../../../../store/t_grid';
import * as i18n from './translations';
import { ColumnHeaderOptions } from '../../../../common';
const FieldsBrowserContainer = styled.div<{ width: number }>`
background-color: ${({ theme }) => theme.eui.euiColorLightestShade};
@ -116,7 +116,6 @@ type Props = Pick<
* set focus to the search input, scroll to the selected category, etc
*/
const FieldsBrowserComponent: React.FC<Props> = ({
browserFields,
columnHeaders,
filteredBrowserFields,
isSearching,
@ -135,7 +134,7 @@ const FieldsBrowserComponent: React.FC<Props> = ({
const containerElement = useRef<HTMLDivElement | null>(null);
const onUpdateColumns = useCallback(
(columns) => dispatch(timelineActions.updateColumns({ id: timelineId, columns })),
(columns) => dispatch(tGridActions.updateColumns({ id: timelineId, columns })),
[dispatch, timelineId]
);

View file

@ -8,20 +8,15 @@
import { omit } from 'lodash/fp';
import React from 'react';
import { waitFor } from '@testing-library/react';
import { mockBrowserFields } from '../../../common/containers/source/mock';
import { TestProviders } from '../../../common/mock';
import '../../../common/mock/match_media';
import { defaultColumnHeaderType } from '../timeline/body/column_headers/default_headers';
import { DEFAULT_DATE_COLUMN_MIN_WIDTH } from '../timeline/body/constants';
import { mockBrowserFields, TestProviders } from '../../../../mock';
import { defaultColumnHeaderType } from '../../body/column_headers/default_headers';
import { DEFAULT_DATE_COLUMN_MIN_WIDTH } from '../../body/constants';
import { Category } from './category';
import { getFieldColumns, getFieldItems } from './field_items';
import { FIELDS_PANE_WIDTH } from './helpers';
import { useMountAppended } from '../../../common/utils/use_mount_appended';
import { ColumnHeaderOptions } from '../../../../common';
jest.mock('../../../common/lib/kibana');
import { useMountAppended } from '../../../utils/use_mount_appended';
import { ColumnHeaderOptions } from '../../../../../common';
const selectedCategoryId = 'base';
const selectedCategoryFields = mockBrowserFields[selectedCategoryId].fields;
@ -54,12 +49,9 @@ describe('field_items', () => {
data-test-subj="category"
filteredBrowserFields={mockBrowserFields}
fieldItems={getFieldItems({
browserFields: mockBrowserFields,
category: mockBrowserFields[selectedCategoryId],
categoryId: selectedCategoryId,
columnHeaders: [],
highlight: '',
onUpdateColumns: jest.fn(),
timelineId,
toggleColumn: jest.fn(),
})}
@ -86,12 +78,9 @@ describe('field_items', () => {
data-test-subj="category"
filteredBrowserFields={mockBrowserFields}
fieldItems={getFieldItems({
browserFields: mockBrowserFields,
category: mockBrowserFields[selectedCategoryId],
categoryId: selectedCategoryId,
columnHeaders: [],
highlight: '',
onUpdateColumns: jest.fn(),
timelineId,
toggleColumn: jest.fn(),
})}
@ -117,12 +106,9 @@ describe('field_items', () => {
data-test-subj="category"
filteredBrowserFields={mockBrowserFields}
fieldItems={getFieldItems({
browserFields: mockBrowserFields,
category: mockBrowserFields[selectedCategoryId],
categoryId: selectedCategoryId,
columnHeaders,
highlight: '',
onUpdateColumns: jest.fn(),
timelineId,
toggleColumn: jest.fn(),
})}
@ -148,12 +134,9 @@ describe('field_items', () => {
data-test-subj="category"
filteredBrowserFields={mockBrowserFields}
fieldItems={getFieldItems({
browserFields: mockBrowserFields,
category: mockBrowserFields[selectedCategoryId],
categoryId: selectedCategoryId,
columnHeaders: columnHeaders.filter((header) => header.id !== timestampFieldId),
highlight: '',
onUpdateColumns: jest.fn(),
timelineId,
toggleColumn: jest.fn(),
})}
@ -181,12 +164,9 @@ describe('field_items', () => {
data-test-subj="category"
filteredBrowserFields={mockBrowserFields}
fieldItems={getFieldItems({
browserFields: mockBrowserFields,
category: mockBrowserFields[selectedCategoryId],
categoryId: selectedCategoryId,
columnHeaders: [],
highlight: '',
onUpdateColumns: jest.fn(),
timelineId,
toggleColumn,
})}
@ -241,12 +221,9 @@ describe('field_items', () => {
data-test-subj="category"
filteredBrowserFields={mockBrowserFieldsWithSignal}
fieldItems={getFieldItems({
browserFields: mockBrowserFieldsWithSignal,
category: mockBrowserFieldsWithSignal[mockSelectedCategoryId],
categoryId: mockSelectedCategoryId,
columnHeaders,
highlight: '',
onUpdateColumns: jest.fn(),
timelineId,
toggleColumn,
})}
@ -281,12 +258,9 @@ describe('field_items', () => {
data-test-subj="category"
filteredBrowserFields={mockBrowserFields}
fieldItems={getFieldItems({
browserFields: mockBrowserFields,
category: mockBrowserFields[selectedCategoryId],
categoryId: selectedCategoryId,
columnHeaders,
highlight: '',
onUpdateColumns: jest.fn(),
timelineId,
toggleColumn: jest.fn(),
})}
@ -311,12 +285,9 @@ describe('field_items', () => {
data-test-subj="category"
filteredBrowserFields={mockBrowserFields}
fieldItems={getFieldItems({
browserFields: mockBrowserFields,
category: mockBrowserFields[selectedCategoryId],
categoryId: selectedCategoryId,
columnHeaders,
highlight: '',
onUpdateColumns: jest.fn(),
timelineId,
toggleColumn: jest.fn(),
})}

View file

@ -0,0 +1,156 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import {
EuiCheckbox,
EuiIcon,
EuiToolTip,
EuiFlexGroup,
EuiFlexItem,
EuiScreenReaderOnly,
} 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 { ColumnHeaderOptions, BrowserField } from '../../../../../common';
import { defaultColumnHeaderType } from '../../body/column_headers/default_headers';
import { DEFAULT_COLUMN_MIN_WIDTH } from '../../body/constants';
import { TruncatableText } from '../../../truncatable_text';
import { FieldName } from './field_name';
import * as i18n from './translations';
import { getAlertColumnHeader } from './helpers';
const TypeIcon = styled(EuiIcon)`
margin: 0 4px;
position: relative;
top: -1px;
`;
TypeIcon.displayName = 'TypeIcon';
export const Description = styled.span`
user-select: text;
width: 400px;
`;
Description.displayName = 'Description';
/**
* An item rendered in the table
*/
export interface FieldItem {
ariaRowindex?: number;
checkbox: React.ReactNode;
description: React.ReactNode;
field: React.ReactNode;
fieldId: string;
}
/**
* Returns the fields items, values, and descriptions shown when a user expands an event
*/
export const getFieldItems = ({
category,
columnHeaders,
highlight = '',
timelineId,
toggleColumn,
}: {
category: Partial<BrowserField>;
columnHeaders: ColumnHeaderOptions[];
highlight?: string;
timelineId: string;
toggleColumn: (column: ColumnHeaderOptions) => void;
}): FieldItem[] =>
uniqBy('name', [
...Object.values(category != null && category.fields != null ? category.fields : {}),
]).map((field) => ({
checkbox: (
<EuiToolTip content={i18n.VIEW_COLUMN(field.name ?? '')}>
<EuiCheckbox
aria-label={i18n.VIEW_COLUMN(field.name ?? '')}
checked={columnHeaders.findIndex((c) => c.id === field.name) !== -1}
data-test-subj={`field-${field.name}-checkbox`}
data-colindex={1}
id={field.name ?? ''}
onChange={() =>
toggleColumn({
columnHeaderType: defaultColumnHeaderType,
id: field.name ?? '',
initialWidth: DEFAULT_COLUMN_MIN_WIDTH,
...getAlertColumnHeader(timelineId, field.name ?? ''),
})
}
/>
</EuiToolTip>
),
field: (
<EuiFlexGroup alignItems="center" gutterSize="none">
<EuiFlexItem grow={false}>
<EuiToolTip content={field.type}>
<TypeIcon
data-test-subj={`field-${field.name}-icon`}
type={getIconFromType(field.type ?? null)}
/>
</EuiToolTip>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<FieldName data-test-subj="field-name" fieldId={field.name ?? ''} highlight={highlight} />
</EuiFlexItem>
</EuiFlexGroup>
),
description: (
<div data-colindex={3} tabIndex={0}>
<EuiToolTip content={field.description}>
<>
<EuiScreenReaderOnly data-test-subj="descriptionForScreenReaderOnly">
<p>{i18n.DESCRIPTION_FOR_FIELD(field.name ?? '')}</p>
</EuiScreenReaderOnly>
<TruncatableText>
<Description data-test-subj={`field-${field.name}-description`}>
{`${field.description ?? getEmptyValue()} ${getExampleText(field.example)}`}
</Description>
</TruncatableText>
</>
</EuiToolTip>
</div>
),
fieldId: field.name ?? '',
}));
/**
* Returns a table column template provided to the `EuiInMemoryTable`'s
* `columns` prop
*/
export const getFieldColumns = () => [
{
field: 'checkbox',
name: '',
render: (checkbox: React.ReactNode, _: FieldItem) => checkbox,
sortable: false,
width: '25px',
},
{
field: 'field',
name: i18n.FIELD,
render: (field: React.ReactNode, _: FieldItem) => field,
sortable: false,
width: '225px',
},
{
field: 'description',
name: i18n.DESCRIPTION,
render: (description: React.ReactNode, _: FieldItem) => description,
sortable: false,
truncateText: true,
width: '400px',
},
];

View file

@ -0,0 +1,61 @@
/*
* 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 { 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', () => {
beforeEach(() => {
jest.useFakeTimers();
});
test('it renders the field name', () => {
const wrapper = mount(
<TestProviders>
<FieldName {...defaultProps} />
</TestProviders>
);
expect(
wrapper.find(`[data-test-subj="field-name-${timestampFieldId}"]`).first().text()
).toEqual(timestampFieldId);
});
test('it highlights the text specified by the `highlight` prop', () => {
const highlight = 'stamp';
const wrapper = mount(
<TestProviders>
<FieldName {...{ ...defaultProps, highlight }} />
</TestProviders>
);
expect(wrapper.find('mark').first().text()).toEqual(highlight);
});
});

View file

@ -0,0 +1,25 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { EuiHighlight, EuiText } from '@elastic/eui';
/** Renders a field name in it's non-dragging state */
export const FieldName = React.memo<{
fieldId: string;
highlight?: string;
}>(({ fieldId, highlight = '' }) => {
return (
<EuiText size="xs">
<EuiHighlight data-test-subj={`field-name-${fieldId}`} search={highlight}>
{fieldId}
</EuiHighlight>
</EuiText>
);
});
FieldName.displayName = 'FieldName';

View file

@ -7,16 +7,12 @@
import React from 'react';
import '../../../common/mock/match_media';
import { mockBrowserFields } from '../../../common/containers/source/mock';
import { TestProviders } from '../../../common/mock';
import { useMountAppended } from '../../../common/utils/use_mount_appended';
import { useMountAppended } from '../../../utils/use_mount_appended';
import { mockBrowserFields, TestProviders } from '../../../../mock';
import { FIELDS_PANE_WIDTH } from './helpers';
import { FieldsPane } from './fields_pane';
jest.mock('../../../common/lib/kibana');
const timelineId = 'test';
describe('FieldsPane', () => {

View file

@ -10,16 +10,14 @@ import React, { useCallback, useMemo } from 'react';
import styled from 'styled-components';
import { useDispatch } from 'react-redux';
import { BrowserFields } from '../../../common/containers/source';
import { timelineActions } from '../../../timelines/store/timeline';
import { OnUpdateColumns } from '../timeline/events';
import { Category } from './category';
import { FieldBrowserProps } from './types';
import type { FieldBrowserProps } from './types';
import { getFieldItems } from './field_items';
import { FIELDS_PANE_WIDTH, TABLE_HEIGHT } from './helpers';
import * as i18n from './translations';
import { ColumnHeaderOptions } from '../../../../common';
import type { BrowserFields, ColumnHeaderOptions, OnUpdateColumns } from '../../../../../common';
import { tGridActions } from '../../../../store/t_grid';
const NoFieldsPanel = styled.div`
background-color: ${(props) => props.theme.eui.euiColorLightestShade};
@ -76,14 +74,14 @@ export const FieldsPane = React.memo<Props>(
(column: ColumnHeaderOptions) => {
if (columnHeaders.some((c) => c.id === column.id)) {
dispatch(
timelineActions.removeColumn({
tGridActions.removeColumn({
columnId: column.id,
id: timelineId,
})
);
} else {
dispatch(
timelineActions.upsertColumn({
tGridActions.upsertColumn({
column,
id: timelineId,
index: 1,
@ -106,12 +104,9 @@ export const FieldsPane = React.memo<Props>(
data-test-subj="category"
filteredBrowserFields={filteredBrowserFields}
fieldItems={getFieldItems({
browserFields: filteredBrowserFields,
category: filteredBrowserFields[selectedCategoryId],
categoryId: selectedCategoryId,
columnHeaders,
highlight: searchInput,
onUpdateColumns,
timelineId,
toggleColumn,
})}

View file

@ -7,8 +7,7 @@
import { mount } from 'enzyme';
import React from 'react';
import { mockBrowserFields } from '../../../common/containers/source/mock';
import { TestProviders } from '../../../common/mock';
import { mockBrowserFields, TestProviders } from '../../../../mock';
import { Header } from './header';
const timelineId = 'test';
@ -29,9 +28,7 @@ describe('Header', () => {
</TestProviders>
);
expect(wrapper.find('[data-test-subj="field-browser-title"]').first().text()).toEqual(
'Customize Columns'
);
expect(wrapper.find('[data-test-subj="field-browser-title"]').first().text()).toEqual('Fields');
});
test('it renders the Reset Fields button', () => {

View file

@ -15,11 +15,9 @@ import {
} from '@elastic/eui';
import React, { useCallback, useMemo } from 'react';
import styled from 'styled-components';
import { BrowserFields } from '../../../common/containers/source';
import { useDeepEqualSelector } from '../../../common/hooks/use_selector';
import { timelineSelectors } from '../../store/timeline';
import { OnUpdateColumns } from '../timeline/events';
import type { BrowserFields, OnUpdateColumns } from '../../../../../common';
import { useDeepEqualSelector } from '../../../../hooks/use_selector';
import { tGridSelectors } from '../../../../store/t_grid';
import {
getFieldBrowserSearchInputClassName,
@ -102,7 +100,7 @@ const TitleRow = React.memo<{
onOutsideClick: () => void;
onUpdateColumns: OnUpdateColumns;
}>(({ id, onOutsideClick, onUpdateColumns }) => {
const getManageTimeline = useMemo(() => timelineSelectors.getManageTimelineById(), []);
const getManageTimeline = useMemo(() => tGridSelectors.getManageTimelineById(), []);
const { defaultColumns } = useDeepEqualSelector((state) => getManageTimeline(state, id));
const handleResetColumns = useCallback(() => {
@ -119,7 +117,7 @@ const TitleRow = React.memo<{
>
<EuiFlexItem grow={false}>
<EuiTitle data-test-subj="field-browser-title" size="s">
<h2>{i18n.CUSTOMIZE_COLUMNS}</h2>
<h2>{i18n.FIELDS_BROWSER}</h2>
</EuiTitle>
</EuiFlexItem>

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { mockBrowserFields } from '../../../common/containers/source/mock';
import { mockBrowserFields } from '../../../../mock';
import {
categoryHasFields,
@ -16,7 +16,7 @@ import {
getFieldCount,
filterBrowserFieldsByFieldName,
} from './helpers';
import { BrowserFields } from '../../../common/containers/source';
import { BrowserFields } from '../../../../../common';
const timelineId = 'test';
@ -373,5 +373,45 @@ describe('helpers', () => {
})
).toEqual(expectedMatchingFields);
});
test('it combines the specified fields into a virtual category omitting the fields missing in the browser fields', () => {
const expectedMatchingFields = {
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',
},
'client.domain': {
aggregatable: true,
category: 'client',
description: 'Client domain.',
example: null,
format: '',
indexes: ['auditbeat', 'filebeat', 'packetbeat'],
name: 'client.domain',
searchable: true,
type: 'string',
},
},
};
const fieldIds = ['agent.hostname', '@timestamp', 'client.domain'];
const { agent, ...mockBrowserFieldsWithoutAgent } = mockBrowserFields;
expect(
createVirtualCategory({
browserFields: mockBrowserFieldsWithoutAgent,
fieldIds,
})
).toEqual(expectedMatchingFields);
});
});
});

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { EuiLoadingSpinner } from '@elastic/eui';
import { EuiBadge, EuiLoadingSpinner } from '@elastic/eui';
import { filter, get, pickBy } from 'lodash/fp';
import styled from 'styled-components';
@ -13,14 +13,11 @@ import {
elementOrChildrenHasFocus,
skipFocusInContainerTo,
stopPropagationAndPreventDefault,
} from '../../../../../timelines/public';
import { TimelineId } from '../../../../common/types/timeline';
import { BrowserField, BrowserFields } from '../../../common/containers/source';
import { alertsHeaders } from '../../../common/components/alerts_viewer/default_headers';
import {
DEFAULT_CATEGORY_NAME,
defaultHeaders,
} from '../timeline/body/column_headers/default_headers';
} from '../../../../../public';
import { TimelineId } from '../../../../../public/types';
import type { BrowserField, BrowserFields } from '../../../../../common';
import { defaultHeaders } from '../../../../store/t_grid/defaults';
import { DEFAULT_CATEGORY_NAME } from '../../body/column_headers/default_headers';
export const LoadingSpinner = styled(EuiLoadingSpinner)`
cursor: pointer;
@ -126,15 +123,23 @@ export const createVirtualCategory = ({
browserFields: BrowserFields;
fieldIds: string[];
}): Partial<BrowserField> => ({
fields: fieldIds.reduce<Readonly<Record<string, Partial<BrowserField>>>>((fields, fieldId) => {
fields: fieldIds.reduce<Readonly<BrowserFields>>((fields, fieldId) => {
const splitId = fieldId.split('.'); // source.geo.city_name -> [source, geo, city_name]
const browserField = get(
[splitId.length > 1 ? splitId[0] : 'base', 'fields', fieldId],
browserFields
);
return {
...fields,
[fieldId]: {
...get([splitId.length > 1 ? splitId[0] : 'base', 'fields', fieldId], browserFields),
name: fieldId,
},
...(browserField
? {
[fieldId]: {
...browserField,
name: fieldId,
},
}
: {}),
};
}, {}),
});
@ -152,7 +157,7 @@ export const mergeBrowserFieldsWithDefaultCategory = (
export const getAlertColumnHeader = (timelineId: string, fieldId: string) =>
timelineId === TimelineId.detectionsPage || timelineId === TimelineId.detectionsRulesDetailsPage
? alertsHeaders.find((c) => c.id === fieldId) ?? {}
? defaultHeaders.find((c) => c.id === fieldId) ?? {}
: {};
export const CATEGORIES_PANE_CLASS_NAME = 'categories-pane';
@ -393,3 +398,9 @@ export const onFieldsBrowserTabPressed = ({
});
}
};
export const CountBadge = (styled(EuiBadge)`
margin-left: 5px;
` as unknown) as typeof EuiBadge;
CountBadge.displayName = 'CountBadge';

View file

@ -9,17 +9,12 @@ import { mount } from 'enzyme';
import React from 'react';
import { waitFor } from '@testing-library/react';
import '../../../common/mock/match_media';
import '../../../common/mock/react_beautiful_dnd';
import { mockBrowserFields } from '../../../common/containers/source/mock';
import { TestProviders } from '../../../common/mock';
import { mockBrowserFields, TestProviders } from '../../../../mock';
import { FIELD_BROWSER_HEIGHT, FIELD_BROWSER_WIDTH } from './helpers';
import { StatefulFieldsBrowserComponent } from '.';
jest.mock('../../../common/lib/kibana');
describe('StatefulFieldsBrowser', () => {
const timelineId = 'test';

View file

@ -10,12 +10,12 @@ import { noop } from 'lodash/fp';
import React, { useEffect, useRef, useState, useCallback, useMemo } from 'react';
import styled from 'styled-components';
import { BrowserFields } from '../../../common/containers/source';
import { DEFAULT_CATEGORY_NAME } from '../timeline/body/column_headers/default_headers';
import type { BrowserFields } from '../../../../../common/search_strategy/index_fields';
import { DEFAULT_CATEGORY_NAME } from '../../body/column_headers/default_headers';
import { FieldsBrowser } from './field_browser';
import { filterBrowserFieldsByFieldName, mergeBrowserFieldsWithDefaultCategory } from './helpers';
import * as i18n from './translations';
import { FieldBrowserProps } from './types';
import type { FieldBrowserProps } from './types';
const fieldsButtonClassName = 'fields-button';
@ -23,6 +23,7 @@ const fieldsButtonClassName = 'fields-button';
export const INPUT_TIMEOUT = 250;
const FieldsBrowserButtonContainer = styled.div`
display: inline-block;
position: relative;
width: 24px;
`;
@ -125,13 +126,13 @@ export const StatefulFieldsBrowserComponent: React.FC<FieldBrowserProps> = ({
return (
<FieldsBrowserButtonContainer data-test-subj="fields-browser-button-container">
<EuiToolTip content={i18n.CUSTOMIZE_COLUMNS}>
<EuiToolTip content={i18n.FIELDS_BROWSER}>
<EuiButtonIcon
aria-label={i18n.CUSTOMIZE_COLUMNS}
aria-label={i18n.FIELDS_BROWSER}
buttonRef={customizeColumnsButtonRef}
className={fieldsButtonClassName}
data-test-subj="show-field-browser"
iconType="list"
iconType="listAdd"
onClick={toggleShow}
>
{i18n.FIELDS}

View file

@ -7,107 +7,95 @@
import { i18n } from '@kbn/i18n';
export const CATEGORY = i18n.translate('xpack.securitySolution.fieldBrowser.categoryLabel', {
export const CATEGORY = i18n.translate('xpack.timelines.fieldBrowser.categoryLabel', {
defaultMessage: 'Category',
});
export const CATEGORIES = i18n.translate('xpack.securitySolution.fieldBrowser.categoriesTitle', {
export const CATEGORIES = i18n.translate('xpack.timelines.fieldBrowser.categoriesTitle', {
defaultMessage: 'Categories',
});
export const CATEGORIES_COUNT = (totalCount: number) =>
i18n.translate('xpack.securitySolution.fieldBrowser.categoriesCountTitle', {
i18n.translate('xpack.timelines.fieldBrowser.categoriesCountTitle', {
values: { totalCount },
defaultMessage: '{totalCount} {totalCount, plural, =1 {category} other {categories}}',
});
export const CATEGORY_LINK = ({ category, totalCount }: { category: string; totalCount: number }) =>
i18n.translate('xpack.securitySolution.fieldBrowser.categoryLinkAriaLabel', {
i18n.translate('xpack.timelines.fieldBrowser.categoryLinkAriaLabel', {
values: { category, totalCount },
defaultMessage:
'{category} {totalCount} {totalCount, plural, =1 {field} other {fields}}. Click this button to select the {category} category.',
});
export const CATEGORY_FIELDS_TABLE_CAPTION = (categoryId: string) =>
i18n.translate('xpack.securitySolution.fieldBrowser.categoryFieldsTableCaption', {
i18n.translate('xpack.timelines.fieldBrowser.categoryFieldsTableCaption', {
defaultMessage: 'category {categoryId} fields',
values: {
categoryId,
},
});
export const COPY_TO_CLIPBOARD = i18n.translate(
'xpack.securitySolution.fieldBrowser.copyToClipboard',
{
defaultMessage: 'Copy to Clipboard',
}
);
export const COPY_TO_CLIPBOARD = i18n.translate('xpack.timelines.fieldBrowser.copyToClipboard', {
defaultMessage: 'Copy to Clipboard',
});
export const CLOSE = i18n.translate('xpack.securitySolution.fieldBrowser.closeButton', {
export const CLOSE = i18n.translate('xpack.timelines.fieldBrowser.closeButton', {
defaultMessage: 'Close',
});
export const CUSTOMIZE_COLUMNS = i18n.translate(
'xpack.securitySolution.fieldBrowser.customizeColumnsTitle',
{
defaultMessage: 'Customize Columns',
}
);
export const FIELDS_BROWSER = i18n.translate('xpack.timelines.fieldBrowser.fieldBrowserTitle', {
defaultMessage: 'Fields',
});
export const DESCRIPTION = i18n.translate('xpack.securitySolution.fieldBrowser.descriptionLabel', {
export const DESCRIPTION = i18n.translate('xpack.timelines.fieldBrowser.descriptionLabel', {
defaultMessage: 'Description',
});
export const DESCRIPTION_FOR_FIELD = (field: string) =>
i18n.translate('xpack.securitySolution.fieldBrowser.descriptionForScreenReaderOnly', {
i18n.translate('xpack.timelines.fieldBrowser.descriptionForScreenReaderOnly', {
values: {
field,
},
defaultMessage: 'Description for field {field}:',
});
export const FIELD = i18n.translate('xpack.securitySolution.fieldBrowser.fieldLabel', {
export const FIELD = i18n.translate('xpack.timelines.fieldBrowser.fieldLabel', {
defaultMessage: 'Field',
});
export const FIELDS = i18n.translate('xpack.securitySolution.fieldBrowser.fieldsTitle', {
export const FIELDS = i18n.translate('xpack.timelines.fieldBrowser.fieldsTitle', {
defaultMessage: 'Columns',
});
export const FIELDS_COUNT = (totalCount: number) =>
i18n.translate('xpack.securitySolution.fieldBrowser.fieldsCountTitle', {
i18n.translate('xpack.timelines.fieldBrowser.fieldsCountTitle', {
values: { totalCount },
defaultMessage: '{totalCount} {totalCount, plural, =1 {field} other {fields}}',
});
export const FILTER_PLACEHOLDER = i18n.translate(
'xpack.securitySolution.fieldBrowser.filterPlaceholder',
{
defaultMessage: 'Field name',
}
);
export const FILTER_PLACEHOLDER = i18n.translate('xpack.timelines.fieldBrowser.filterPlaceholder', {
defaultMessage: 'Field name',
});
export const NO_FIELDS_MATCH = i18n.translate(
'xpack.securitySolution.fieldBrowser.noFieldsMatchLabel',
{
defaultMessage: 'No fields match',
}
);
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.securitySolution.fieldBrowser.noFieldsMatchInputLabel', {
i18n.translate('xpack.timelines.fieldBrowser.noFieldsMatchInputLabel', {
defaultMessage: 'No fields match {searchInput}',
values: {
searchInput,
},
});
export const RESET_FIELDS = i18n.translate('xpack.securitySolution.fieldBrowser.resetFieldsLink', {
export const RESET_FIELDS = i18n.translate('xpack.timelines.fieldBrowser.resetFieldsLink', {
defaultMessage: 'Reset Fields',
});
export const VIEW_ALL_CATEGORY_FIELDS = (categoryId: string) =>
i18n.translate('xpack.securitySolution.fieldBrowser.viewCategoryTooltip', {
i18n.translate('xpack.timelines.fieldBrowser.viewCategoryTooltip', {
defaultMessage: 'View all {categoryId} fields',
values: {
categoryId,
@ -115,14 +103,14 @@ export const VIEW_ALL_CATEGORY_FIELDS = (categoryId: string) =>
});
export const YOU_ARE_IN_A_POPOVER = i18n.translate(
'xpack.securitySolution.fieldBrowser.youAreInAPopoverScreenReaderOnly',
'xpack.timelines.fieldBrowser.youAreInAPopoverScreenReaderOnly',
{
defaultMessage: 'You are in the Customize Columns popup. To exit this popup, press Escape.',
}
);
export const VIEW_COLUMN = (field: string) =>
i18n.translate('xpack.securitySolution.fieldBrowser.viewColumnCheckboxAriaLabel', {
i18n.translate('xpack.timelines.fieldBrowser.viewColumnCheckboxAriaLabel', {
values: { field },
defaultMessage: 'View {field} column',
});

View file

@ -5,8 +5,8 @@
* 2.0.
*/
import { ColumnHeaderOptions } from '../../../../common';
import { BrowserFields } from '../../../common/containers/source';
import type { BrowserFields } from '../../../../../common/search_strategy/index_fields';
import type { ColumnHeaderOptions } from '../../../../../common/types/timeline/columns';
export type OnFieldSelected = (fieldId: string) => void;
export type OnHideFieldBrowser = () => void;

View file

@ -0,0 +1,78 @@
/*
* 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 { EuiDataGrid } from '@elastic/eui';
import deepEqual from 'fast-deep-equal';
import React, { useState, useEffect, useMemo } from 'react';
import type { ColumnHeaderOptions } from '../../../../common';
import type { BrowserFields } from '../../../../common/search_strategy/index_fields';
import { StatefulFieldsBrowser } from './fields_browser';
import { FIELD_BROWSER_HEIGHT, FIELD_BROWSER_WIDTH } from './fields_browser/helpers';
interface Props {
browserFields: BrowserFields;
columnHeaders: ColumnHeaderOptions[];
timelineId: string;
}
/** Renders the timeline header columns */
export const TimelineToolbarComponent = ({ browserFields, columnHeaders, timelineId }: Props) => {
const [visibleColumns, setVisibleColumns] = useState<string[]>([]);
useEffect(() => setVisibleColumns(columnHeaders.map(({ id }) => id)), [columnHeaders]);
const columns = useMemo(
() =>
columnHeaders.map((column) => ({
...column,
actions: { showHide: false },
})),
[columnHeaders]
);
return (
<EuiDataGrid
aria-label="header-data-grid"
data-test-subj="header-data-grid"
columnVisibility={{
visibleColumns,
setVisibleColumns,
}}
rowCount={0}
renderCellValue={({ rowIndex, columnId }) => `${rowIndex}, ${columnId}`}
columns={columns}
toolbarVisibility={{
showStyleSelector: true,
showSortSelector: true,
showFullScreenSelector: true,
showColumnSelector: {
allowHide: false,
allowReorder: true,
},
additionalControls: (
<StatefulFieldsBrowser
data-test-subj="field-browser"
height={FIELD_BROWSER_HEIGHT}
width={FIELD_BROWSER_WIDTH}
browserFields={browserFields}
timelineId={timelineId}
columnHeaders={columnHeaders}
/>
),
}}
/>
);
};
export const TimelineToolbar = React.memo(
TimelineToolbarComponent,
(prevProps, nextProps) =>
prevProps.timelineId === nextProps.timelineId &&
deepEqual(prevProps.columnHeaders, nextProps.columnHeaders) &&
deepEqual(prevProps.browserFields, nextProps.browserFields)
);

View file

@ -5,6 +5,56 @@
* 2.0.
*/
import { get, getOr, isEmpty, uniqBy } from 'lodash/fp';
import { BrowserField, BrowserFields, ColumnHeaderOptions } from '../../../common';
import { DEFAULT_COLUMN_MIN_WIDTH, DEFAULT_DATE_COLUMN_MIN_WIDTH } from '../t_grid/body/constants';
export const getColumnHeaderFromBrowserField = ({
browserField,
width = DEFAULT_COLUMN_MIN_WIDTH,
}: {
browserField: Partial<BrowserField>;
width?: number;
}): ColumnHeaderOptions => ({
category: browserField.category,
columnHeaderType: 'not-filtered',
description: browserField.description != null ? browserField.description : undefined,
example: browserField.example != null ? `${browserField.example}` : undefined,
id: browserField.name || '',
type: browserField.type,
aggregatable: browserField.aggregatable,
initialWidth: width,
});
/**
* Returns a collection of columns, where the first column in the collection
* is a timestamp, and the remaining columns are all the columns in the
* specified category
*/
export const getColumnsWithTimestamp = ({
browserFields,
category,
}: {
browserFields: BrowserFields;
category: string;
}): ColumnHeaderOptions[] => {
const emptyFields: Record<string, Partial<BrowserField>> = {};
const timestamp = get('base.fields.@timestamp', browserFields);
const categoryFields: Array<Partial<BrowserField>> = [
...Object.values(getOr(emptyFields, `${category}.fields`, browserFields)),
];
return timestamp != null && categoryFields.length
? uniqBy('id', [
getColumnHeaderFromBrowserField({
browserField: timestamp,
width: DEFAULT_DATE_COLUMN_MIN_WIDTH,
}),
...categoryFields.map((f) => getColumnHeaderFromBrowserField({ browserField: f })),
])
: [];
};
export const getIconFromType = (type: string | null) => {
switch (type) {
case 'string': // fall through
@ -26,3 +76,7 @@ export const getIconFromType = (type: string | null) => {
return 'questionInCircle';
}
};
/** 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}` : '';

View file

@ -0,0 +1,187 @@
/*
* 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 { useCallback, useEffect, useRef, useState } from 'react';
import { isEmpty, isEqual, pick } from 'lodash/fp';
import { Subscription } from 'rxjs/internal/Subscription';
import memoizeOne from 'memoize-one';
import {
BrowserField,
BrowserFields,
DocValueFields,
IndexField,
IndexFieldsStrategyRequest,
IndexFieldsStrategyResponse,
} from '../../../common';
import * as i18n from './translations';
import {
IIndexPattern,
DataPublicPluginStart,
isCompleteResponse,
isErrorResponse,
} from '../../../../../../src/plugins/data/public';
import { useKibana } from '../../../../../../src/plugins/kibana_react/public';
import { useAppToasts } from '../../hooks/use_app_toasts';
const DEFAULT_BROWSER_FIELDS = {};
const DEFAULT_INDEX_PATTERNS = { fields: [], title: '' };
const DEFAULT_DOC_VALUE_FIELDS: DocValueFields[] = [];
interface FetchIndexReturn {
browserFields: BrowserFields;
docValueFields: DocValueFields[];
indexes: string[];
indexExists: boolean;
indexPatterns: IIndexPattern;
}
/**
* HOT Code path where the fields can be 16087 in length or larger. This is
* VERY mutatious on purpose to improve the performance of the transform.
*/
export const getBrowserFields = memoizeOne(
(_title: string, fields: IndexField[]): BrowserFields => {
// Adds two dangerous casts to allow for mutations within this function
type DangerCastForMutation = Record<string, {}>;
type DangerCastForBrowserFieldsMutation = Record<
string,
Omit<BrowserField, 'fields'> & { fields: Record<string, BrowserField> }
>;
// We mutate this instead of using lodash/set to keep this as fast as possible
return fields.reduce<DangerCastForBrowserFieldsMutation>((accumulator, field) => {
if (accumulator[field.category] == null) {
(accumulator as DangerCastForMutation)[field.category] = {};
}
if (accumulator[field.category].fields == null) {
accumulator[field.category].fields = {};
}
accumulator[field.category].fields[field.name] = (field as unknown) as BrowserField;
return accumulator;
}, {});
},
// Update the value only if _title has changed
(newArgs, lastArgs) => newArgs[0] === lastArgs[0]
);
export const getDocValueFields = memoizeOne(
(_title: string, fields: IndexField[]): DocValueFields[] =>
fields && fields.length > 0
? fields.reduce<DocValueFields[]>((accumulator: DocValueFields[], field: IndexField) => {
if (field.readFromDocValues && accumulator.length < 100) {
return [
...accumulator,
{
field: field.name,
format: field.format ? field.format : undefined,
},
];
}
return accumulator;
}, [])
: [],
// Update the value only if _title has changed
(newArgs, lastArgs) => newArgs[0] === lastArgs[0]
);
export const getIndexFields = memoizeOne(
(title: string, fields: IndexField[]): IIndexPattern =>
fields && fields.length > 0
? {
fields: fields.map((field) =>
pick(['name', 'searchable', 'type', 'aggregatable', 'esTypes', 'subType'], field)
),
title,
}
: { fields: [], title },
(newArgs, lastArgs) => newArgs[0] === lastArgs[0] && newArgs[1].length === lastArgs[1].length
);
export const useFetchIndex = (
indexNames: string[],
onlyCheckIfIndicesExist: boolean = false
): [boolean, FetchIndexReturn] => {
const { data } = useKibana<{ data: DataPublicPluginStart }>().services;
const abortCtrl = useRef(new AbortController());
const searchSubscription$ = useRef(new Subscription());
const previousIndexesName = useRef<string[]>([]);
const [isLoading, setLoading] = useState(false);
const [state, setState] = useState<FetchIndexReturn>({
browserFields: DEFAULT_BROWSER_FIELDS,
docValueFields: DEFAULT_DOC_VALUE_FIELDS,
indexes: indexNames,
indexExists: true,
indexPatterns: DEFAULT_INDEX_PATTERNS,
});
const { addError, addWarning } = useAppToasts();
const indexFieldsSearch = useCallback(
(iNames) => {
const asyncSearch = async () => {
abortCtrl.current = new AbortController();
setLoading(true);
searchSubscription$.current = data.search
.search<IndexFieldsStrategyRequest, IndexFieldsStrategyResponse>(
{ indices: iNames, onlyCheckIfIndicesExist },
{
abortSignal: abortCtrl.current.signal,
strategy: 'indexFields',
}
)
.subscribe({
next: (response) => {
if (isCompleteResponse(response)) {
const stringifyIndices = response.indicesExist.sort().join();
previousIndexesName.current = response.indicesExist;
setLoading(false);
setState({
browserFields: getBrowserFields(stringifyIndices, response.indexFields),
docValueFields: getDocValueFields(stringifyIndices, response.indexFields),
indexes: response.indicesExist,
indexExists: response.indicesExist.length > 0,
indexPatterns: getIndexFields(stringifyIndices, response.indexFields),
});
searchSubscription$.current.unsubscribe();
} else if (isErrorResponse(response)) {
setLoading(false);
addWarning(i18n.ERROR_BEAT_FIELDS);
searchSubscription$.current.unsubscribe();
}
},
error: (msg) => {
setLoading(false);
addError(msg, {
title: i18n.FAIL_BEAT_FIELDS,
});
searchSubscription$.current.unsubscribe();
},
});
};
searchSubscription$.current.unsubscribe();
abortCtrl.current.abort();
asyncSearch();
},
[data.search, addError, addWarning, onlyCheckIfIndicesExist, setLoading, setState]
);
useEffect(() => {
if (!isEmpty(indexNames) && !isEqual(previousIndexesName.current, indexNames)) {
indexFieldsSearch(indexNames);
}
return () => {
searchSubscription$.current.unsubscribe();
abortCtrl.current.abort();
};
}, [indexNames, indexFieldsSearch, previousIndexesName]);
return [isLoading, state];
};

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 { i18n } from '@kbn/i18n';
export const ERROR_BEAT_FIELDS = i18n.translate(
'xpack.timelines.beatFields.errorSearchDescription',
{
defaultMessage: `An error has occurred on getting beat fields`,
}
);
export const FAIL_BEAT_FIELDS = i18n.translate('xpack.timelines.beatFields.failSearchDescription', {
defaultMessage: `Failed to run search on beat fields`,
});

View file

@ -51,6 +51,7 @@ export {
addFieldToTimelineColumns,
getTimelineIdFromColumnDroppableId,
} from './components/drag_and_drop/helpers';
export { StatefulFieldsBrowser } from './components/t_grid/toolbar/fields_browser';
// This exports static code and TypeScript types,
// as well as, Kibana Platform `plugin()` initializer.

View file

@ -5,13 +5,17 @@
* 2.0.
*/
import { Store } from 'redux';
import React, { lazy, Suspense } from 'react';
import { EuiLoadingSpinner } from '@elastic/eui';
import { Storage } from '../../../../../src/plugins/kibana_utils/public';
import { DataPublicPluginStart } from '../../../../../src/plugins/data/public';
import type { Store } from 'redux';
import type { Storage } from '../../../../../src/plugins/kibana_utils/public';
import type { DataPublicPluginStart } from '../../../../../src/plugins/data/public';
import type { TGridProps } from '../types';
import { LastUpdatedAtProps, LoadingPanelProps } from '../components';
import type {
LastUpdatedAtProps,
LoadingPanelProps,
FieldBrowserWrappedProps,
} from '../components';
const TimelineLazy = lazy(() => import('../components'));
export const getTGridLazy = (
@ -52,3 +56,15 @@ export const getLoadingPanelLazy = (props: LoadingPanelProps) => {
</Suspense>
);
};
const FieldsBrowserLazy = lazy(() => import('../components/fields_browser'));
export const getFieldsBrowserLazy = (
props: FieldBrowserWrappedProps,
{ store }: { store: Store }
) => {
return (
<Suspense fallback={<EuiLoadingSpinner />}>
<FieldsBrowserLazy {...props} store={store} />
</Suspense>
);
};

View file

@ -18,6 +18,8 @@ export const createTGridMocks = () => ({
// eslint-disable-next-line react/display-name
getTGrid: () => <>{'hello grid'}</>,
// eslint-disable-next-line react/display-name
getFieldBrowser: () => <div data-test-subj="field-browser" />,
// eslint-disable-next-line react/display-name
getLastUpdated: (props: LastUpdatedAtProps) => <LastUpdatedAt {...props} />,
// eslint-disable-next-line react/display-name
getLoadingPanel: (props: LoadingPanelProps) => <LoadingPanel {...props} />,

View file

@ -8,16 +8,21 @@
import { Store } from 'redux';
import { Storage } from '../../../../src/plugins/kibana_utils/public';
import { DataPublicPluginStart } from '../../../../src/plugins/data/public';
import {
import type { DataPublicPluginStart } from '../../../../src/plugins/data/public';
import type {
CoreSetup,
Plugin,
PluginInitializerContext,
CoreStart,
} from '../../../../src/core/public';
import type { TimelinesUIStart, TGridProps } from './types';
import { getLastUpdatedLazy, getLoadingPanelLazy, getTGridLazy } from './methods';
import type { LastUpdatedAtProps, LoadingPanelProps } from './components';
import type { LastUpdatedAtProps, LoadingPanelProps, FieldBrowserWrappedProps } from './components';
import {
getLastUpdatedLazy,
getLoadingPanelLazy,
getTGridLazy,
getFieldsBrowserLazy,
} from './methods';
import { tGridReducer } from './store/t_grid/reducer';
import { useDraggableKeyboardWrapper } from './components/drag_and_drop/draggable_keyboard_wrapper_hook';
import { useAddToTimeline, useAddToTimelineSensor } from './hooks/use_add_to_timeline';
@ -55,6 +60,11 @@ export class TimelinesPlugin implements Plugin<void, TimelinesUIStart> {
getLastUpdated: (props: LastUpdatedAtProps) => {
return getLastUpdatedLazy(props);
},
getFieldBrowser: (props: FieldBrowserWrappedProps) => {
return getFieldsBrowserLazy(props, {
store: this._store!,
});
},
getUseAddToTimeline: () => {
return useAddToTimeline;
},

View file

@ -11,6 +11,7 @@ import { Store } from 'redux';
import type {
LastUpdatedAtProps,
LoadingPanelProps,
FieldBrowserWrappedProps,
UseDraggableKeyboardWrapper,
UseDraggableKeyboardWrapperProps,
} from './components';
@ -28,6 +29,7 @@ export interface TimelinesUIStart {
getTGridReducer: () => any;
getLoadingPanel: (props: LoadingPanelProps) => ReactElement<LoadingPanelProps>;
getLastUpdated: (props: LastUpdatedAtProps) => ReactElement<LastUpdatedAtProps>;
getFieldBrowser: (props: FieldBrowserWrappedProps) => ReactElement<FieldBrowserWrappedProps>;
getUseAddToTimeline: () => (props: UseAddToTimelineProps) => UseAddToTimeline;
getUseAddToTimelineSensor: () => (api: SensorAPI) => void;
getUseDraggableKeyboardWrapper: () => (

View file

@ -20248,23 +20248,6 @@
"xpack.securitySolution.featureCatalogueDescription2": "検出して対応します。",
"xpack.securitySolution.featureCatalogueDescription3": "インシデントを調査します。",
"xpack.securitySolution.featureRegistry.linkSecuritySolutionTitle": "セキュリティ",
"xpack.securitySolution.fieldBrowser.categoriesTitle": "カテゴリー",
"xpack.securitySolution.fieldBrowser.categoryFieldsTableCaption": "カテゴリ {categoryId} フィールド",
"xpack.securitySolution.fieldBrowser.categoryLabel": "カテゴリー",
"xpack.securitySolution.fieldBrowser.closeButton": "閉じる",
"xpack.securitySolution.fieldBrowser.copyToClipboard": "クリップボードにコピー",
"xpack.securitySolution.fieldBrowser.customizeColumnsTitle": "列のカスタマイズ",
"xpack.securitySolution.fieldBrowser.descriptionForScreenReaderOnly": "フィールド {field} の説明:",
"xpack.securitySolution.fieldBrowser.descriptionLabel": "説明",
"xpack.securitySolution.fieldBrowser.fieldLabel": "フィールド",
"xpack.securitySolution.fieldBrowser.fieldsTitle": "列",
"xpack.securitySolution.fieldBrowser.filterPlaceholder": "フィールド名",
"xpack.securitySolution.fieldBrowser.noFieldsMatchInputLabel": "{searchInput} に一致するフィールドがありません",
"xpack.securitySolution.fieldBrowser.noFieldsMatchLabel": "一致するフィールドがありません",
"xpack.securitySolution.fieldBrowser.resetFieldsLink": "フィールドをリセット",
"xpack.securitySolution.fieldBrowser.viewCategoryTooltip": "すべての {categoryId} フィールドを表示します",
"xpack.securitySolution.fieldBrowser.viewColumnCheckboxAriaLabel": "{field} 列を表示",
"xpack.securitySolution.fieldBrowser.youAreInAPopoverScreenReaderOnly": "列のカスタマイズポップアップが表示されています。このポップアップを閉じるには、Esc キーを押してください。",
"xpack.securitySolution.fieldRenderers.moreLabel": "詳細",
"xpack.securitySolution.firstLastSeenHost.errorSearchDescription": "最初の前回確認されたホスト検索でエラーが発生しました",
"xpack.securitySolution.firstLastSeenHost.failSearchDescription": "最初の前回確認されたホストで検索を実行できませんでした",

View file

@ -20554,26 +20554,6 @@
"xpack.securitySolution.featureCatalogueDescription2": "检测和响应。",
"xpack.securitySolution.featureCatalogueDescription3": "调查事件。",
"xpack.securitySolution.featureRegistry.linkSecuritySolutionTitle": "安全",
"xpack.securitySolution.fieldBrowser.categoriesCountTitle": "{totalCount} 个{totalCount, plural, other {类别}}",
"xpack.securitySolution.fieldBrowser.categoriesTitle": "类别",
"xpack.securitySolution.fieldBrowser.categoryFieldsTableCaption": "类别 {categoryId} 字段",
"xpack.securitySolution.fieldBrowser.categoryLabel": "类别",
"xpack.securitySolution.fieldBrowser.categoryLinkAriaLabel": "{category} {totalCount} 个{totalCount, plural, other {字段}}。单击此按钮可选择 {category} 类别。",
"xpack.securitySolution.fieldBrowser.closeButton": "关闭",
"xpack.securitySolution.fieldBrowser.copyToClipboard": "复制到剪贴板",
"xpack.securitySolution.fieldBrowser.customizeColumnsTitle": "定制列",
"xpack.securitySolution.fieldBrowser.descriptionForScreenReaderOnly": "{field} 字段的描述:",
"xpack.securitySolution.fieldBrowser.descriptionLabel": "描述",
"xpack.securitySolution.fieldBrowser.fieldLabel": "字段",
"xpack.securitySolution.fieldBrowser.fieldsCountTitle": "{totalCount} 个{totalCount, plural, other {字段}}",
"xpack.securitySolution.fieldBrowser.fieldsTitle": "列",
"xpack.securitySolution.fieldBrowser.filterPlaceholder": "字段名称",
"xpack.securitySolution.fieldBrowser.noFieldsMatchInputLabel": "没有字段匹配“{searchInput}”",
"xpack.securitySolution.fieldBrowser.noFieldsMatchLabel": "没有字段匹配",
"xpack.securitySolution.fieldBrowser.resetFieldsLink": "重置字段",
"xpack.securitySolution.fieldBrowser.viewCategoryTooltip": "查看所有 {categoryId} 字段",
"xpack.securitySolution.fieldBrowser.viewColumnCheckboxAriaLabel": "查看 {field} 列",
"xpack.securitySolution.fieldBrowser.youAreInAPopoverScreenReaderOnly": "您当前位于“定制列”弹出式窗口中。按 Esc 键可退出此弹出式窗口。",
"xpack.securitySolution.fieldRenderers.moreLabel": "更多",
"xpack.securitySolution.firstLastSeenHost.errorSearchDescription": "搜索上次看到的首个主机时发生错误",
"xpack.securitySolution.firstLastSeenHost.failSearchDescription": "无法对上次看到的首个主机执行搜索",

View file

@ -3,10 +3,8 @@
"version": "1.0.0",
"kibanaVersion": "kibana",
"configPath": ["xpack", "timelinesTest"],
"requiredPlugins": ["timelines"],
"requiredBundles": [
"kibanaReact"
],
"requiredPlugins": ["timelines", "data", "dataEnhanced"],
"requiredBundles": ["kibanaReact"],
"server": false,
"ui": true
}

View file

@ -12,12 +12,15 @@ import { AppMountParameters, CoreStart } from 'kibana/public';
import { I18nProvider } from '@kbn/i18n/react';
import { KibanaContextProvider } from '../../../../../../../../src/plugins/kibana_react/public';
import { TimelinesUIStart } from '../../../../../../../plugins/timelines/public';
import { DataPublicPluginStart } from '../../../../../../../../src/plugins/data/public';
type CoreStartTimelines = CoreStart & { data: DataPublicPluginStart };
/**
* Render the Timeline Test app. Returns a cleanup function.
*/
export function renderApp(
coreStart: CoreStart,
coreStart: CoreStartTimelines,
parameters: AppMountParameters,
timelinesPluginSetup: TimelinesUIStart | null
) {
@ -41,7 +44,7 @@ const AppRoot = React.memo(
parameters,
timelinesPluginSetup,
}: {
coreStart: CoreStart;
coreStart: CoreStartTimelines;
parameters: AppMountParameters;
timelinesPluginSetup: TimelinesUIStart | null;
}) => {
@ -50,6 +53,7 @@ const AppRoot = React.memo(
const setRefetch = useCallback((_refetch) => {
refetch.current = _refetch;
}, []);
return (
<I18nProvider>
<Router history={parameters.history}>

View file

@ -9,6 +9,7 @@ import { Plugin, CoreStart, CoreSetup, AppMountParameters } from 'kibana/public'
import { i18n } from '@kbn/i18n';
import { TimelinesUIStart } from '../../../../../plugins/timelines/public';
import { renderApp } from './applications/timelines_test';
import { DataPublicPluginStart } from '../../../../../../src/plugins/data/public';
export type TimelinesTestPluginSetup = void;
export type TimelinesTestPluginStart = void;
@ -17,6 +18,7 @@ export interface TimelinesTestPluginSetupDependencies {}
export interface TimelinesTestPluginStartDependencies {
timelines: TimelinesUIStart;
data: DataPublicPluginStart;
}
export class TimelinesTestPlugin
@ -39,8 +41,8 @@ export class TimelinesTestPlugin
}),
mount: async (params: AppMountParameters<unknown>) => {
const startServices = await core.getStartServices();
const [coreStart] = startServices;
return renderApp(coreStart, params, this.timelinesPlugin);
const [coreStart, { data }] = startServices;
return renderApp({ ...coreStart, data }, params, this.timelinesPlugin);
},
});
}