diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/helpers.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/helpers.ts
index c49d088d6241d..760c132cd1824 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/helpers.ts
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/helpers.ts
@@ -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 =>
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/eql_tab_content/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/eql_tab_content/index.test.tsx
index 5f08bf5a016f5..815f8f43d5c14 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/eql_tab_content/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/eql_tab_content/index.test.tsx
@@ -60,6 +60,7 @@ jest.mock('../../../../common/lib/kibana', () => {
},
timelines: {
getLastUpdated: jest.fn(),
+ getFieldBrowser: jest.fn(),
getUseDraggableKeyboardWrapper: () => mockUseDraggableKeyboardWrapper,
},
},
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/pinned_tab_content/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/pinned_tab_content/index.test.tsx
index f4d5570ce40d3..b8e99718fa933 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/pinned_tab_content/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/pinned_tab_content/index.test.tsx
@@ -60,6 +60,7 @@ jest.mock('../../../../common/lib/kibana', () => {
},
timelines: {
getLastUpdated: jest.fn(),
+ getFieldBrowser: jest.fn(),
getUseDraggableKeyboardWrapper: () => mockUseDraggableKeyboardWrapper,
},
},
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.test.tsx
index 9bf7ee28f3934..cd9693313b4f9 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.test.tsx
@@ -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(),
diff --git a/x-pack/plugins/timelines/public/components/empty_value/__snapshots__/empty_value.test.tsx.snap b/x-pack/plugins/timelines/public/components/empty_value/__snapshots__/empty_value.test.tsx.snap
new file mode 100644
index 0000000000000..142ed7a0d7175
--- /dev/null
+++ b/x-pack/plugins/timelines/public/components/empty_value/__snapshots__/empty_value.test.tsx.snap
@@ -0,0 +1,7 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`EmptyValue it renders against snapshot 1`] = `
+
+ (Empty String)
+
+`;
diff --git a/x-pack/plugins/timelines/public/components/empty_value/empty_value.test.tsx b/x-pack/plugins/timelines/public/components/empty_value/empty_value.test.tsx
new file mode 100644
index 0000000000000..be9a086d8dc5b
--- /dev/null
+++ b/x-pack/plugins/timelines/public/components/empty_value/empty_value.test.tsx
@@ -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({getEmptyString()}
);
+ 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(
+
+ {getEmptyString()}
+
+ );
+ expect(wrapper.text()).toBe('(Empty String)');
+ });
+ });
+
+ describe('#getEmptyTagValue', () => {
+ const wrapper = mount(
+
+ {getEmptyTagValue()}
+
+ );
+ 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(
+
+ {getEmptyStringTag()}
+
+ );
+ expect(wrapper.find('span')).toHaveLength(1);
+ });
+
+ test('should turn into an empty string tag place holder', () => {
+ const wrapper = mountWithIntl(
+
+ {getEmptyStringTag()}
+
+ );
+ expect(wrapper.text()).toBe(getEmptyString());
+ });
+ });
+
+ describe('#defaultToEmptyTag', () => {
+ test('should default to an empty value when a value is null', () => {
+ const wrapper = mount(
+
+ {defaultToEmptyTag(null)}
+
+ );
+ expect(wrapper.text()).toBe(getEmptyValue());
+ });
+
+ test('should default to an empty value when a value is undefined', () => {
+ const wrapper = mount(
+
+ {defaultToEmptyTag(undefined)}
+
+ );
+ expect(wrapper.text()).toBe(getEmptyValue());
+ });
+
+ test('should return a deep path value', () => {
+ const test = {
+ a: {
+ b: {
+ c: 1,
+ },
+ },
+ };
+ const wrapper = mount({defaultToEmptyTag(test.a.b.c)}
);
+ 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(
+
+ {getOrEmptyTag('a.b.c', test)}
+
+ );
+ 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(
+
+ {getOrEmptyTag('a.b.c', test)}
+
+ );
+ expect(wrapper.text()).toBe(getEmptyValue());
+ });
+
+ test('should default empty value when a deep rooted value is missing', () => {
+ const test = {
+ a: {
+ b: {},
+ },
+ };
+ const wrapper = mount(
+
+ {getOrEmptyTag('a.b.c', test)}
+
+ );
+ expect(wrapper.text()).toBe(getEmptyValue());
+ });
+
+ test('should return a deep path value', () => {
+ const test = {
+ a: {
+ b: {
+ c: 1,
+ },
+ },
+ };
+ const wrapper = mount({getOrEmptyTag('a.b.c', test)}
);
+ expect(wrapper.text()).toBe('1');
+ });
+ });
+});
diff --git a/x-pack/plugins/timelines/public/components/empty_value/index.tsx b/x-pack/plugins/timelines/public/components/empty_value/index.tsx
new file mode 100644
index 0000000000000..86efb4a78277a
--- /dev/null
+++ b/x-pack/plugins/timelines/public/components/empty_value/index.tsx
@@ -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 = () => {getEmptyValue()};
+export const getEmptyStringTag = () => {getEmptyString()};
+
+export const defaultToEmptyTag = (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}>;
+ }
+};
diff --git a/x-pack/plugins/timelines/public/components/empty_value/translations.ts b/x-pack/plugins/timelines/public/components/empty_value/translations.ts
new file mode 100644
index 0000000000000..20c822c67dfb3
--- /dev/null
+++ b/x-pack/plugins/timelines/public/components/empty_value/translations.ts
@@ -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',
+});
diff --git a/x-pack/plugins/timelines/public/components/fields_browser/index.tsx b/x-pack/plugins/timelines/public/components/fields_browser/index.tsx
new file mode 100644
index 0000000000000..ac121f9afdd58
--- /dev/null
+++ b/x-pack/plugins/timelines/public/components/fields_browser/index.tsx
@@ -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 & {
+ 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 (
+
+
+
+
+
+ );
+};
+
+FieldBrowserWrappedComponent.displayName = 'FieldBrowserWrappedComponent';
+
+// eslint-disable-next-line import/no-default-export
+export { FieldBrowserWrappedComponent as default };
diff --git a/x-pack/plugins/timelines/public/components/index.tsx b/x-pack/plugins/timelines/public/components/index.tsx
index 8bb4e6cb45853..9959574464836 100644
--- a/x-pack/plugins/timelines/public/components/index.tsx
+++ b/x-pack/plugins/timelines/public/components/index.tsx
@@ -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';
diff --git a/x-pack/plugins/timelines/public/components/t_grid/standalone/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/standalone/index.tsx
index c267a0e57dd2c..fd0b0a5eef75d 100644
--- a/x-pack/plugins/timelines/public/components/t_grid/standalone/index.tsx
+++ b/x-pack/plugins/timelines/public/components/t_grid/standalone/index.tsx
@@ -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 = ({
const columnsHeader = isEmpty(columns) ? defaultHeaders : columns;
const { uiSettings } = useKibana().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 = ({
const justTitle = useMemo(() => {title}, [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 = ({
);
dispatch(
tGridActions.initializeTGridSettings({
- footerText,
id: STANDALONE_ID,
+ defaultColumns: columns,
+ footerText,
loadingText,
unit,
})
@@ -316,7 +321,7 @@ const TGridStandaloneComponent: React.FC = ({
{
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(),
})}
diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/category.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/category.tsx
similarity index 91%
rename from x-pack/plugins/security_solution/public/timelines/components/fields_browser/category.tsx
rename to x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/category.tsx
index deafda95ceab2..45f4b5d12c513 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/category.tsx
+++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/category.tsx
@@ -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';
diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/category_columns.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/category_columns.test.tsx
similarity index 98%
rename from x-pack/plugins/security_solution/public/timelines/components/fields_browser/category_columns.test.tsx
rename to x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/category_columns.test.tsx
index 7b00b768b56a0..30a6c44a08a16 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/category_columns.test.tsx
+++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/category_columns.test.tsx
@@ -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';
diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/category_columns.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/category_columns.tsx
similarity index 88%
rename from x-pack/plugins/security_solution/public/timelines/components/fields_browser/category_columns.tsx
rename to x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/category_columns.tsx
index 528791328fdb9..4a2805839024e 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/category_columns.tsx
+++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/category_columns.tsx
@@ -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(
({ categoryId, browserFields, onUpdateColumns, timelineId }) => {
- const getManageTimeline = useMemo(() => timelineSelectors.getManageTimelineById(), []);
+ const getManageTimeline = useMemo(() => tGridSelectors.getManageTimelineById(), []);
const { isLoading } = useDeepEqualSelector((state) =>
getManageTimeline(state, timelineId ?? '')
);
diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/category_title.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/category_title.test.tsx
similarity index 94%
rename from x-pack/plugins/security_solution/public/timelines/components/fields_browser/category_title.test.tsx
rename to x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/category_title.test.tsx
index 6af4b5c5c312e..746668491abb8 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/category_title.test.tsx
+++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/category_title.test.tsx
@@ -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';
diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/category_title.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/category_title.tsx
similarity index 88%
rename from x-pack/plugins/security_solution/public/timelines/components/fields_browser/category_title.tsx
rename to x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/category_title.tsx
index e2599ec250a2d..2296b4c855d42 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/category_title.tsx
+++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/category_title.tsx
@@ -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';
diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_browser.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_browser.test.tsx
similarity index 97%
rename from x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_browser.test.tsx
rename to x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_browser.test.tsx
index 9e374752bd302..96a76df19cd93 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_browser.test.tsx
+++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_browser.test.tsx
@@ -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';
diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_browser.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_browser.tsx
similarity index 95%
rename from x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_browser.tsx
rename to x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_browser.tsx
index 0496b9d7c8886..6529cf8776125 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_browser.tsx
+++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_browser.tsx
@@ -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 = ({
- browserFields,
columnHeaders,
filteredBrowserFields,
isSearching,
@@ -135,7 +134,7 @@ const FieldsBrowserComponent: React.FC = ({
const containerElement = useRef(null);
const onUpdateColumns = useCallback(
- (columns) => dispatch(timelineActions.updateColumns({ id: timelineId, columns })),
+ (columns) => dispatch(tGridActions.updateColumns({ id: timelineId, columns })),
[dispatch, timelineId]
);
diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_items.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_items.test.tsx
similarity index 86%
rename from x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_items.test.tsx
rename to x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_items.test.tsx
index e40807dc85dc7..789aeeeb187fd 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_items.test.tsx
+++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_items.test.tsx
@@ -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(),
})}
diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_items.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_items.tsx
new file mode 100644
index 0000000000000..8015be2bcc857
--- /dev/null
+++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_items.tsx
@@ -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;
+ 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: (
+
+ 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 ?? ''),
+ })
+ }
+ />
+
+ ),
+ field: (
+
+
+
+
+
+
+
+
+
+
+
+ ),
+ description: (
+
+
+ <>
+
+ {i18n.DESCRIPTION_FOR_FIELD(field.name ?? '')}
+
+
+
+ {`${field.description ?? getEmptyValue()} ${getExampleText(field.example)}`}
+
+
+ >
+
+
+ ),
+ 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',
+ },
+];
diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_name.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_name.test.tsx
new file mode 100644
index 0000000000000..05f093eaf1805
--- /dev/null
+++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_name.test.tsx
@@ -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(
+
+
+
+ );
+
+ 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(
+
+
+
+ );
+
+ expect(wrapper.find('mark').first().text()).toEqual(highlight);
+ });
+});
diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_name.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_name.tsx
new file mode 100644
index 0000000000000..5781211058d3c
--- /dev/null
+++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_name.tsx
@@ -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 (
+
+
+ {fieldId}
+
+
+ );
+});
+
+FieldName.displayName = 'FieldName';
diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/fields_pane.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/fields_pane.test.tsx
similarity index 91%
rename from x-pack/plugins/security_solution/public/timelines/components/fields_browser/fields_pane.test.tsx
rename to x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/fields_pane.test.tsx
index 6d17f148aa1dc..275ce4907435f 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/fields_pane.test.tsx
+++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/fields_pane.test.tsx
@@ -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', () => {
diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/fields_pane.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/fields_pane.tsx
similarity index 88%
rename from x-pack/plugins/security_solution/public/timelines/components/fields_browser/fields_pane.tsx
rename to x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/fields_pane.tsx
index dfb4edad17414..633d1c536035a 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/fields_pane.tsx
+++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/fields_pane.tsx
@@ -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(
(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(
data-test-subj="category"
filteredBrowserFields={filteredBrowserFields}
fieldItems={getFieldItems({
- browserFields: filteredBrowserFields,
category: filteredBrowserFields[selectedCategoryId],
- categoryId: selectedCategoryId,
columnHeaders,
highlight: searchInput,
- onUpdateColumns,
timelineId,
toggleColumn,
})}
diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/header.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/header.test.tsx
similarity index 97%
rename from x-pack/plugins/security_solution/public/timelines/components/fields_browser/header.test.tsx
rename to x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/header.test.tsx
index 89b361e86422e..0270540fc491d 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/header.test.tsx
+++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/header.test.tsx
@@ -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', () => {
);
- 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', () => {
diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/header.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/header.tsx
similarity index 91%
rename from x-pack/plugins/security_solution/public/timelines/components/fields_browser/header.tsx
rename to x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/header.tsx
index b52c6cd672ac7..42ea20f1dfab8 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/header.tsx
+++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/header.tsx
@@ -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<{
>
- {i18n.CUSTOMIZE_COLUMNS}
+ {i18n.FIELDS_BROWSER}
diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/helpers.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/helpers.test.tsx
similarity index 88%
rename from x-pack/plugins/security_solution/public/timelines/components/fields_browser/helpers.test.tsx
rename to x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/helpers.test.tsx
index 202020607a3d6..ee7d0a334ed23 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/helpers.test.tsx
+++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/helpers.test.tsx
@@ -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);
+ });
});
});
diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/helpers.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/helpers.tsx
similarity index 92%
rename from x-pack/plugins/security_solution/public/timelines/components/fields_browser/helpers.tsx
rename to x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/helpers.tsx
index 256c172721d35..9a3559c51ef87 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/helpers.tsx
+++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/helpers.tsx
@@ -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 => ({
- fields: fieldIds.reduce>>>((fields, fieldId) => {
+ fields: fieldIds.reduce>((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';
diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/index.test.tsx
similarity index 95%
rename from x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.test.tsx
rename to x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/index.test.tsx
index 381017f7f6260..60c1e8da08b78 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.test.tsx
+++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/index.test.tsx
@@ -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';
diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/index.tsx
similarity index 94%
rename from x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.tsx
rename to x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/index.tsx
index 2bdf6862a8f8c..3f2d8c490d215 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.tsx
+++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/index.tsx
@@ -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 = ({
return (
-
+
{i18n.FIELDS}
diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/translations.ts b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/translations.ts
similarity index 51%
rename from x-pack/plugins/security_solution/public/timelines/components/fields_browser/translations.ts
rename to x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/translations.ts
index a392aa2191422..21bf3a1479fc7 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/translations.ts
+++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/translations.ts
@@ -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',
});
diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/types.ts b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/types.ts
similarity index 86%
rename from x-pack/plugins/security_solution/public/timelines/components/fields_browser/types.ts
rename to x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/types.ts
index ea71a8860ab01..ebd72083a2bfe 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/types.ts
+++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/types.ts
@@ -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;
diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/index.tsx
new file mode 100644
index 0000000000000..d7ac089c17133
--- /dev/null
+++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/index.tsx
@@ -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([]);
+ useEffect(() => setVisibleColumns(columnHeaders.map(({ id }) => id)), [columnHeaders]);
+
+ const columns = useMemo(
+ () =>
+ columnHeaders.map((column) => ({
+ ...column,
+ actions: { showHide: false },
+ })),
+ [columnHeaders]
+ );
+
+ return (
+ `${rowIndex}, ${columnId}`}
+ columns={columns}
+ toolbarVisibility={{
+ showStyleSelector: true,
+ showSortSelector: true,
+ showFullScreenSelector: true,
+ showColumnSelector: {
+ allowHide: false,
+ allowReorder: true,
+ },
+ additionalControls: (
+
+ ),
+ }}
+ />
+ );
+};
+
+export const TimelineToolbar = React.memo(
+ TimelineToolbarComponent,
+ (prevProps, nextProps) =>
+ prevProps.timelineId === nextProps.timelineId &&
+ deepEqual(prevProps.columnHeaders, nextProps.columnHeaders) &&
+ deepEqual(prevProps.browserFields, nextProps.browserFields)
+);
diff --git a/x-pack/plugins/timelines/public/components/utils/helpers.ts b/x-pack/plugins/timelines/public/components/utils/helpers.ts
index 29d83eb1bd7aa..0c531a9c483bc 100644
--- a/x-pack/plugins/timelines/public/components/utils/helpers.ts
+++ b/x-pack/plugins/timelines/public/components/utils/helpers.ts
@@ -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;
+ 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> = {};
+ const timestamp = get('base.fields.@timestamp', browserFields);
+ const categoryFields: Array> = [
+ ...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}` : '';
diff --git a/x-pack/plugins/timelines/public/container/source/index.tsx b/x-pack/plugins/timelines/public/container/source/index.tsx
new file mode 100644
index 0000000000000..94bbd2a874178
--- /dev/null
+++ b/x-pack/plugins/timelines/public/container/source/index.tsx
@@ -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;
+ type DangerCastForBrowserFieldsMutation = Record<
+ string,
+ Omit & { fields: Record }
+ >;
+
+ // We mutate this instead of using lodash/set to keep this as fast as possible
+ return fields.reduce((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((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([]);
+ const [isLoading, setLoading] = useState(false);
+
+ const [state, setState] = useState({
+ 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(
+ { 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];
+};
diff --git a/x-pack/plugins/timelines/public/container/source/translations.ts b/x-pack/plugins/timelines/public/container/source/translations.ts
new file mode 100644
index 0000000000000..f1fa461983116
--- /dev/null
+++ b/x-pack/plugins/timelines/public/container/source/translations.ts
@@ -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`,
+});
diff --git a/x-pack/plugins/timelines/public/index.ts b/x-pack/plugins/timelines/public/index.ts
index 9fe022a670399..6f4de1dd2559e 100644
--- a/x-pack/plugins/timelines/public/index.ts
+++ b/x-pack/plugins/timelines/public/index.ts
@@ -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.
diff --git a/x-pack/plugins/timelines/public/methods/index.tsx b/x-pack/plugins/timelines/public/methods/index.tsx
index a11485f32b6a5..a358e29c39e49 100644
--- a/x-pack/plugins/timelines/public/methods/index.tsx
+++ b/x-pack/plugins/timelines/public/methods/index.tsx
@@ -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) => {
);
};
+
+const FieldsBrowserLazy = lazy(() => import('../components/fields_browser'));
+export const getFieldsBrowserLazy = (
+ props: FieldBrowserWrappedProps,
+ { store }: { store: Store }
+) => {
+ return (
+ }>
+
+
+ );
+};
diff --git a/x-pack/plugins/timelines/public/mock/plugin_mock.tsx b/x-pack/plugins/timelines/public/mock/plugin_mock.tsx
index 8d2141d62f253..64be3306e7aa6 100644
--- a/x-pack/plugins/timelines/public/mock/plugin_mock.tsx
+++ b/x-pack/plugins/timelines/public/mock/plugin_mock.tsx
@@ -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: () => ,
+ // eslint-disable-next-line react/display-name
getLastUpdated: (props: LastUpdatedAtProps) => ,
// eslint-disable-next-line react/display-name
getLoadingPanel: (props: LoadingPanelProps) => ,
diff --git a/x-pack/plugins/timelines/public/plugin.ts b/x-pack/plugins/timelines/public/plugin.ts
index 38fdc6149839b..b1bff6fd85032 100644
--- a/x-pack/plugins/timelines/public/plugin.ts
+++ b/x-pack/plugins/timelines/public/plugin.ts
@@ -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 {
getLastUpdated: (props: LastUpdatedAtProps) => {
return getLastUpdatedLazy(props);
},
+ getFieldBrowser: (props: FieldBrowserWrappedProps) => {
+ return getFieldsBrowserLazy(props, {
+ store: this._store!,
+ });
+ },
getUseAddToTimeline: () => {
return useAddToTimeline;
},
diff --git a/x-pack/plugins/timelines/public/types.ts b/x-pack/plugins/timelines/public/types.ts
index b5a4b54cf5eb5..1342f277b6878 100644
--- a/x-pack/plugins/timelines/public/types.ts
+++ b/x-pack/plugins/timelines/public/types.ts
@@ -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;
getLastUpdated: (props: LastUpdatedAtProps) => ReactElement;
+ getFieldBrowser: (props: FieldBrowserWrappedProps) => ReactElement;
getUseAddToTimeline: () => (props: UseAddToTimelineProps) => UseAddToTimeline;
getUseAddToTimelineSensor: () => (api: SensorAPI) => void;
getUseDraggableKeyboardWrapper: () => (
diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index 7508548906b17..2287d74e199b0 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -20262,23 +20262,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": "最初の前回確認されたホストで検索を実行できませんでした",
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index 5877818a5f102..5fa40af4cb375 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -20568,26 +20568,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": "无法对上次看到的首个主机执行搜索",
diff --git a/x-pack/test/plugin_functional/plugins/timelines_test/kibana.json b/x-pack/test/plugin_functional/plugins/timelines_test/kibana.json
index 85c2639ef7d47..e6465142d4b5d 100644
--- a/x-pack/test/plugin_functional/plugins/timelines_test/kibana.json
+++ b/x-pack/test/plugin_functional/plugins/timelines_test/kibana.json
@@ -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
}
diff --git a/x-pack/test/plugin_functional/plugins/timelines_test/public/applications/timelines_test/index.tsx b/x-pack/test/plugin_functional/plugins/timelines_test/public/applications/timelines_test/index.tsx
index 072944dd9d78e..e21ca4754e6cf 100644
--- a/x-pack/test/plugin_functional/plugins/timelines_test/public/applications/timelines_test/index.tsx
+++ b/x-pack/test/plugin_functional/plugins/timelines_test/public/applications/timelines_test/index.tsx
@@ -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 (
diff --git a/x-pack/test/plugin_functional/plugins/timelines_test/public/plugin.ts b/x-pack/test/plugin_functional/plugins/timelines_test/public/plugin.ts
index b3b9c7ecbf6e0..e5f9aceace3ba 100644
--- a/x-pack/test/plugin_functional/plugins/timelines_test/public/plugin.ts
+++ b/x-pack/test/plugin_functional/plugins/timelines_test/public/plugin.ts
@@ -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) => {
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);
},
});
}