mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[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:
parent
d91c6d0cfb
commit
a8fc9b462c
58 changed files with 1195 additions and 869 deletions
|
@ -107,7 +107,7 @@ pageLoadAssetSize:
|
|||
dataVisualizer: 27530
|
||||
banners: 17946
|
||||
mapsEms: 26072
|
||||
timelines: 251886
|
||||
timelines: 330000
|
||||
screenshotMode: 17856
|
||||
visTypePie: 35583
|
||||
expressionRevealImage: 25675
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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)];
|
||||
}
|
||||
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
];
|
||||
|
|
|
@ -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: '',
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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 =>
|
||||
|
|
|
@ -60,6 +60,7 @@ jest.mock('../../../../common/lib/kibana', () => {
|
|||
},
|
||||
timelines: {
|
||||
getLastUpdated: jest.fn(),
|
||||
getFieldBrowser: jest.fn(),
|
||||
getUseDraggableKeyboardWrapper: () => mockUseDraggableKeyboardWrapper,
|
||||
},
|
||||
},
|
||||
|
|
|
@ -60,6 +60,7 @@ jest.mock('../../../../common/lib/kibana', () => {
|
|||
},
|
||||
timelines: {
|
||||
getLastUpdated: jest.fn(),
|
||||
getFieldBrowser: jest.fn(),
|
||||
getUseDraggableKeyboardWrapper: () => mockUseDraggableKeyboardWrapper,
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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(),
|
||||
|
|
7
x-pack/plugins/timelines/public/components/empty_value/__snapshots__/empty_value.test.tsx.snap
generated
Normal file
7
x-pack/plugins/timelines/public/components/empty_value/__snapshots__/empty_value.test.tsx.snap
generated
Normal file
|
@ -0,0 +1,7 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`EmptyValue it renders against snapshot 1`] = `
|
||||
<p>
|
||||
(Empty String)
|
||||
</p>
|
||||
`;
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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}</>;
|
||||
}
|
||||
};
|
|
@ -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',
|
||||
});
|
|
@ -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 };
|
|
@ -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';
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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';
|
|
@ -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';
|
||||
|
|
@ -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(),
|
||||
})}
|
|
@ -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';
|
|
@ -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';
|
|
@ -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 ?? '')
|
||||
);
|
|
@ -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';
|
|
@ -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';
|
|
@ -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';
|
|
@ -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]
|
||||
);
|
||||
|
|
@ -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(),
|
||||
})}
|
|
@ -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',
|
||||
},
|
||||
];
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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';
|
|
@ -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', () => {
|
|
@ -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,
|
||||
})}
|
|
@ -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', () => {
|
|
@ -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>
|
||||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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';
|
|
@ -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';
|
||||
|
|
@ -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}
|
|
@ -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',
|
||||
});
|
|
@ -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;
|
|
@ -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)
|
||||
);
|
|
@ -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}` : '';
|
||||
|
|
187
x-pack/plugins/timelines/public/container/source/index.tsx
Normal file
187
x-pack/plugins/timelines/public/container/source/index.tsx
Normal 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];
|
||||
};
|
|
@ -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`,
|
||||
});
|
|
@ -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.
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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} />,
|
||||
|
|
|
@ -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;
|
||||
},
|
||||
|
|
|
@ -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: () => (
|
||||
|
|
|
@ -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": "最初の前回確認されたホストで検索を実行できませんでした",
|
||||
|
|
|
@ -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": "无法对上次看到的首个主机执行搜索",
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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}>
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue