From 7db838e0b8ccc379bf1a6f4ca080d2b8a4746f42 Mon Sep 17 00:00:00 2001 From: Constance Date: Fri, 2 Apr 2021 05:49:12 -0700 Subject: [PATCH 01/30] [App Search] API Logs: Add ApiLogsTable and NewApiEventsPrompt components (#96008) * Set up getStatusColor util for upcoming EuiHealth components * Add ApiLogsTable component * Add NewApiEventsPrompt component * Update ApiLogs view with new components + add EuiPageContent wrapper (missed this originally) * Update EngineOverview with new components * PR feedback: Comments + mock FormattedRelative * Fix type error --- .../components/api_logs/api_logs.test.tsx | 5 +- .../components/api_logs/api_logs.tsx | 43 ++++-- .../api_logs/components/api_logs_table.scss | 5 + .../components/api_logs_table.test.tsx | 113 +++++++++++++++ .../api_logs/components/api_logs_table.tsx | 132 ++++++++++++++++++ .../components/api_logs/components/index.ts | 9 ++ .../components/new_api_events_prompt.scss | 6 + .../components/new_api_events_prompt.test.tsx | 51 +++++++ .../components/new_api_events_prompt.tsx | 35 +++++ .../app_search/components/api_logs/index.ts | 1 + .../components/api_logs/utils.test.ts | 11 +- .../app_search/components/api_logs/utils.ts | 9 ++ .../components/recent_api_logs.test.tsx | 4 +- .../components/recent_api_logs.tsx | 20 ++- 14 files changed, 422 insertions(+), 22 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/api_logs_table.scss create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/api_logs_table.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/api_logs_table.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/index.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/new_api_events_prompt.scss create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/new_api_events_prompt.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/new_api_events_prompt.tsx diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_logs.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_logs.test.tsx index 7bdfaf87a2b2f..1945dde84ec45 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_logs.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_logs.test.tsx @@ -17,6 +17,8 @@ import { EuiPageHeader } from '@elastic/eui'; import { Loading } from '../../../shared/loading'; import { LogRetentionCallout, LogRetentionTooltip } from '../log_retention'; +import { ApiLogsTable, NewApiEventsPrompt } from './components'; + import { ApiLogs } from './'; describe('ApiLogs', () => { @@ -41,7 +43,8 @@ describe('ApiLogs', () => { it('renders', () => { expect(wrapper.find(EuiPageHeader).prop('pageTitle')).toEqual('API Logs'); - // TODO: Check for ApiLogsTable + NewApiEventsPrompt when those get added + expect(wrapper.find(ApiLogsTable)).toHaveLength(1); + expect(wrapper.find(NewApiEventsPrompt)).toHaveLength(1); expect(wrapper.find(LogRetentionCallout).prop('type')).toEqual('api'); expect(wrapper.find(LogRetentionTooltip).prop('type')).toEqual('api'); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_logs.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_logs.tsx index 2ffc9ea303b5c..8ca15906783f9 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_logs.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_logs.tsx @@ -9,7 +9,15 @@ import React, { useEffect } from 'react'; import { useValues, useActions } from 'kea'; -import { EuiPageHeader, EuiTitle, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { + EuiPageHeader, + EuiTitle, + EuiPageContent, + EuiPageContentBody, + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, +} from '@elastic/eui'; import { FlashMessages } from '../../../shared/flash_messages'; import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; @@ -18,6 +26,7 @@ import { Loading } from '../../../shared/loading'; import { LogRetentionCallout, LogRetentionTooltip, LogRetentionOptions } from '../log_retention'; +import { ApiLogsTable, NewApiEventsPrompt } from './components'; import { API_LOGS_TITLE, RECENT_API_EVENTS } from './constants'; import { ApiLogsLogic } from './'; @@ -47,19 +56,27 @@ export const ApiLogs: React.FC = ({ engineBreadcrumb }) => { - - - -

{RECENT_API_EVENTS}

-
-
- - - - {/* TODO: NewApiEventsPrompt */} -
+ + + + + +

{RECENT_API_EVENTS}

+
+
+ + + + + + + +
+ - {/* TODO: ApiLogsTable */} + +
+
); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/api_logs_table.scss b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/api_logs_table.scss new file mode 100644 index 0000000000000..44834d81a13c6 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/api_logs_table.scss @@ -0,0 +1,5 @@ +.apiLogDetailButton { + // More closely mimics the regular line height of an EuiLink / + // compresses table rows back to the standard height + height: $euiSizeL !important; +} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/api_logs_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/api_logs_table.test.tsx new file mode 100644 index 0000000000000..99fce81ca348f --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/api_logs_table.test.tsx @@ -0,0 +1,113 @@ +/* + * 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 { setMockValues, setMockActions, mountWithIntl } from '../../../../__mocks__'; + +// NOTE: We're mocking FormattedRelative here because it (currently) has +// console warn issues, and it allows us to skip mocking dates +jest.mock('@kbn/i18n/react', () => ({ + ...(jest.requireActual('@kbn/i18n/react') as object), + FormattedRelative: jest.fn(() => '20 hours ago'), +})); + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { EuiBasicTable, EuiBadge, EuiHealth, EuiButtonEmpty, EuiEmptyPrompt } from '@elastic/eui'; + +import { DEFAULT_META } from '../../../../shared/constants'; + +import { ApiLogsTable } from './'; + +describe('ApiLogsTable', () => { + const apiLogs = [ + { + timestamp: '1970-01-01T00:00:00.000Z', + status: 404, + http_method: 'GET', + full_request_path: '/api/as/v1/test', + }, + { + timestamp: '1970-01-01T00:00:00.000Z', + status: 500, + http_method: 'DELETE', + full_request_path: '/api/as/v1/test', + }, + { + timestamp: '1970-01-01T00:00:00.000Z', + status: 200, + http_method: 'POST', + full_request_path: '/api/as/v1/engines/some-engine/search', + }, + ]; + + const values = { + dataLoading: false, + apiLogs, + meta: DEFAULT_META, + }; + const actions = { + onPaginate: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + setMockValues(values); + setMockActions(actions); + }); + + it('renders', () => { + const wrapper = mountWithIntl(); + const tableContent = wrapper.find(EuiBasicTable).text(); + + expect(tableContent).toContain('Method'); + expect(tableContent).toContain('GET'); + expect(tableContent).toContain('DELETE'); + expect(tableContent).toContain('POST'); + expect(wrapper.find(EuiBadge)).toHaveLength(3); + + expect(tableContent).toContain('Time'); + expect(tableContent).toContain('20 hours ago'); + + expect(tableContent).toContain('Endpoint'); + expect(tableContent).toContain('/api/as/v1/test'); + expect(tableContent).toContain('/api/as/v1/engines/some-engine/search'); + + expect(tableContent).toContain('Status'); + expect(tableContent).toContain('404'); + expect(tableContent).toContain('500'); + expect(tableContent).toContain('200'); + expect(wrapper.find(EuiHealth)).toHaveLength(3); + + expect(wrapper.find(EuiButtonEmpty)).toHaveLength(3); + wrapper.find('[data-test-subj="ApiLogsTableDetailsButton"]').first().simulate('click'); + // TODO: API log details flyout + }); + + it('renders an empty prompt if no items are passed', () => { + setMockValues({ ...values, apiLogs: [] }); + const wrapper = mountWithIntl(); + const promptContent = wrapper.find(EuiEmptyPrompt).text(); + + expect(promptContent).toContain('No recent logs'); + }); + + describe('hasPagination', () => { + it('does not render with pagination by default', () => { + const wrapper = shallow(); + + expect(wrapper.find(EuiBasicTable).prop('pagination')).toBeFalsy(); + }); + + it('renders pagination if hasPagination is true', () => { + const wrapper = shallow(); + + expect(wrapper.find(EuiBasicTable).prop('pagination')).toBeTruthy(); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/api_logs_table.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/api_logs_table.tsx new file mode 100644 index 0000000000000..8ebcc4350f7fc --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/api_logs_table.tsx @@ -0,0 +1,132 @@ +/* + * 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 { useValues, useActions } from 'kea'; + +import { + EuiBasicTable, + EuiBasicTableColumn, + EuiBadge, + EuiHealth, + EuiButtonEmpty, + EuiEmptyPrompt, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedRelative } from '@kbn/i18n/react'; + +import { convertMetaToPagination, handlePageChange } from '../../../../shared/table_pagination'; + +import { ApiLogsLogic } from '../index'; +import { ApiLog } from '../types'; +import { getStatusColor } from '../utils'; + +import './api_logs_table.scss'; + +interface Props { + hasPagination?: boolean; +} +export const ApiLogsTable: React.FC = ({ hasPagination }) => { + const { dataLoading, apiLogs, meta } = useValues(ApiLogsLogic); + const { onPaginate } = useActions(ApiLogsLogic); + + const columns: Array> = [ + { + field: 'http_method', + name: i18n.translate('xpack.enterpriseSearch.appSearch.engine.apiLogs.methodTableHeading', { + defaultMessage: 'Method', + }), + width: '100px', + render: (method: string) => {method}, + }, + { + field: 'timestamp', + name: i18n.translate('xpack.enterpriseSearch.appSearch.engine.apiLogs.timeTableHeading', { + defaultMessage: 'Time', + }), + width: '20%', + render: (dateString: string) => , + }, + { + field: 'full_request_path', + name: i18n.translate('xpack.enterpriseSearch.appSearch.engine.apiLogs.endpointTableHeading', { + defaultMessage: 'Endpoint', + }), + width: '50%', + truncateText: true, + mobileOptions: { + // @ts-ignore - EUI's typing is incorrect here + width: '100%', + }, + }, + { + field: 'status', + name: i18n.translate('xpack.enterpriseSearch.appSearch.engine.apiLogs.statusTableHeading', { + defaultMessage: 'Status', + }), + dataType: 'number', + width: '100px', + render: (status: number) => {status}, + }, + { + width: '100px', + align: 'right', + render: (apiLog: ApiLog) => ( + + {i18n.translate('xpack.enterpriseSearch.appSearch.engine.apiLogs.detailsButtonLabel', { + defaultMessage: 'Details', + })} + + ), + }, + ]; + + const paginationProps = hasPagination + ? { + pagination: { + ...convertMetaToPagination(meta), + hidePerPageOptions: true, + }, + onChange: handlePageChange(onPaginate), + } + : {}; + + return ( + + {i18n.translate('xpack.enterpriseSearch.appSearch.engine.apiLogs.emptyTitle', { + defaultMessage: 'No recent logs', + })} + + } + body={ +

+ {i18n.translate('xpack.enterpriseSearch.appSearch.engine.apiLogs.emptyDescription', { + defaultMessage: "Check back after you've performed some API calls.", + })} +

+ } + /> + } + {...paginationProps} + /> + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/index.ts new file mode 100644 index 0000000000000..c0edc51d06228 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { ApiLogsTable } from './api_logs_table'; +export { NewApiEventsPrompt } from './new_api_events_prompt'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/new_api_events_prompt.scss b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/new_api_events_prompt.scss new file mode 100644 index 0000000000000..0f033bd37c61c --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/new_api_events_prompt.scss @@ -0,0 +1,6 @@ +.newApiEventsPrompt { + padding: $euiSizeXS; + padding-left: $euiSizeS; + display: flex; + align-items: center; +} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/new_api_events_prompt.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/new_api_events_prompt.test.tsx new file mode 100644 index 0000000000000..91d1962cd91db --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/new_api_events_prompt.test.tsx @@ -0,0 +1,51 @@ +/* + * 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 { setMockValues, setMockActions } from '../../../../__mocks__'; + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { EuiButtonEmpty } from '@elastic/eui'; + +import { NewApiEventsPrompt } from './'; + +describe('NewApiEventsPrompt', () => { + const values = { + hasNewData: true, + }; + const actions = { + onUserRefresh: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + setMockValues(values); + setMockActions(actions); + }); + + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.isEmptyRender()).toBe(false); + }); + + it('does not render if no new data has been polled', () => { + setMockValues({ ...values, hasNewData: false }); + const wrapper = shallow(); + + expect(wrapper.isEmptyRender()).toBe(true); + }); + + it('calls onUserRefresh', () => { + const wrapper = shallow(); + + wrapper.find(EuiButtonEmpty).simulate('click'); + expect(actions.onUserRefresh).toHaveBeenCalled(); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/new_api_events_prompt.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/new_api_events_prompt.tsx new file mode 100644 index 0000000000000..1f834e061bd2c --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/new_api_events_prompt.tsx @@ -0,0 +1,35 @@ +/* + * 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 { useValues, useActions } from 'kea'; + +import { EuiPanel, EuiButtonEmpty } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { ApiLogsLogic } from '../'; + +import './new_api_events_prompt.scss'; + +export const NewApiEventsPrompt: React.FC = () => { + const { hasNewData } = useValues(ApiLogsLogic); + const { onUserRefresh } = useActions(ApiLogsLogic); + + return hasNewData ? ( + + {i18n.translate('xpack.enterpriseSearch.appSearch.engines.apiLogs.newEventsMessage', { + defaultMessage: 'New events have been logged.', + })} + + {i18n.translate('xpack.enterpriseSearch.appSearch.engines.apiLogs.newEventsButtonLabel', { + defaultMessage: 'Refresh', + })} + + + ) : null; +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/index.ts index dc05fe3de0d5c..183956e51d8d4 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/index.ts @@ -6,5 +6,6 @@ */ export { API_LOGS_TITLE } from './constants'; +export { ApiLogsTable, NewApiEventsPrompt } from './components'; export { ApiLogs } from './api_logs'; export { ApiLogsLogic } from './api_logs_logic'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/utils.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/utils.test.ts index 53c210d595291..f9b6dcea2cbf3 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/utils.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/utils.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { getDateString } from './utils'; +import { getDateString, getStatusColor } from './utils'; describe('getDateString', () => { const mockDate = jest @@ -23,3 +23,12 @@ describe('getDateString', () => { afterAll(() => mockDate.mockRestore()); }); + +describe('getStatusColor', () => { + it('returns a valid EUI badge color based on the status code', () => { + expect(getStatusColor(200)).toEqual('secondary'); + expect(getStatusColor(301)).toEqual('primary'); + expect(getStatusColor(404)).toEqual('warning'); + expect(getStatusColor(503)).toEqual('danger'); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/utils.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/utils.ts index 4e2dfc2cf701a..3217a1561ce76 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/utils.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/utils.ts @@ -10,3 +10,12 @@ export const getDateString = (offSetDays?: number) => { if (offSetDays) date.setDate(date.getDate() + offSetDays); return date.toISOString(); }; + +export const getStatusColor = (status: number) => { + let color = ''; + if (status >= 100 && status < 300) color = 'secondary'; + if (status >= 300 && status < 400) color = 'primary'; + if (status >= 400 && status < 500) color = 'warning'; + if (status >= 500) color = 'danger'; + return color; +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/recent_api_logs.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/recent_api_logs.test.tsx index 44e125221f674..6f3ec806a438d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/recent_api_logs.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/recent_api_logs.test.tsx @@ -13,6 +13,8 @@ import React from 'react'; import { shallow, ShallowWrapper } from 'enzyme'; +import { ApiLogsTable } from '../../api_logs'; + import { RecentApiLogs } from './recent_api_logs'; describe('RecentApiLogs', () => { @@ -31,7 +33,7 @@ describe('RecentApiLogs', () => { it('renders the recent API logs table', () => { expect(wrapper.prop('title')).toEqual(

Recent API events

); - // TODO: expect(wrapper.find(ApiLogsTable)).toHaveLength(1) + expect(wrapper.find(ApiLogsTable)).toHaveLength(1); }); it('calls fetchApiLogs on page load and starts pollForApiLogs', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/recent_api_logs.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/recent_api_logs.tsx index 1a8e4703d7c2e..3686f380407e2 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/recent_api_logs.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/recent_api_logs.tsx @@ -9,9 +9,11 @@ import React, { useEffect } from 'react'; import { useActions } from 'kea'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; + import { EuiButtonEmptyTo } from '../../../../shared/react_router_helpers'; import { ENGINE_API_LOGS_PATH } from '../../../routes'; -import { ApiLogsLogic } from '../../api_logs'; +import { ApiLogsLogic, ApiLogsTable, NewApiEventsPrompt } from '../../api_logs'; import { RECENT_API_EVENTS } from '../../api_logs/constants'; import { DataPanel } from '../../data_panel'; import { generateEnginePath } from '../../engine'; @@ -30,14 +32,20 @@ export const RecentApiLogs: React.FC = () => { {RECENT_API_EVENTS}} action={ - - {VIEW_API_LOGS} - + + + + + + + {VIEW_API_LOGS} + + + } hasBorder > - TODO: API Logs Table - {/* */} + ); }; From d2a484c5bd9801e3e6b89c3073bab241b82bb869 Mon Sep 17 00:00:00 2001 From: Stratoula Kalafateli Date: Fri, 2 Apr 2021 15:51:09 +0300 Subject: [PATCH 02/30] [TSVB] Enables url drilldowns for range selection (#95296) * Temp research code * Make it work * Cleanup * Convert series to datatable * Remove unecessary log * Minor * Fix types problem * Add unit tests * Take under consideration the override index pattern setting * Implement brush event for dual mode * Move indexpatterns fetch outside the loop Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../vis_type_timeseries/common/types.ts | 3 + .../lib/convert_series_to_datatable.test.ts | 185 ++++++++++++++++++ .../lib/convert_series_to_datatable.ts | 113 +++++++++++ .../components/timeseries_visualization.tsx | 42 ++-- .../application/components/vis_types/index.ts | 4 +- .../constants/{chart.js => chart.ts} | 0 .../constants/{icons.js => icons.ts} | 3 +- .../constants/{index.js => index.ts} | 0 .../visualizations/views/timeseries/index.js | 2 +- .../public/metrics_type.ts | 2 +- .../server/lib/vis_data/helpers/get_splits.js | 1 + .../lib/vis_data/helpers/get_splits.test.js | 2 + 12 files changed, 335 insertions(+), 22 deletions(-) create mode 100644 src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_datatable.test.ts create mode 100644 src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_datatable.ts rename src/plugins/vis_type_timeseries/public/application/visualizations/constants/{chart.js => chart.ts} (100%) rename src/plugins/vis_type_timeseries/public/application/visualizations/constants/{icons.js => icons.ts} (97%) rename src/plugins/vis_type_timeseries/public/application/visualizations/constants/{index.js => index.ts} (100%) diff --git a/src/plugins/vis_type_timeseries/common/types.ts b/src/plugins/vis_type_timeseries/common/types.ts index 4aa69be346608..74e247b7af06d 100644 --- a/src/plugins/vis_type_timeseries/common/types.ts +++ b/src/plugins/vis_type_timeseries/common/types.ts @@ -63,6 +63,9 @@ export interface PanelData { id: string; label: string; data: Array<[number, number]>; + seriesId: string; + splitByLabel: string; + isSplitByTerms: boolean; } export const isVisTableData = (data: TimeseriesVisData): data is TableData => diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_datatable.test.ts b/src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_datatable.test.ts new file mode 100644 index 0000000000000..df0874fdd73ec --- /dev/null +++ b/src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_datatable.test.ts @@ -0,0 +1,185 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import { IndexPattern, IndexPatternField } from 'src/plugins/data/public'; +import { PanelData } from '../../../../common/types'; +import { TimeseriesVisParams } from '../../../types'; +import { convertSeriesToDataTable, addMetaToColumns } from './convert_series_to_datatable'; + +jest.mock('../../../services', () => { + return { + getDataStart: jest.fn(() => { + return { + indexPatterns: jest.fn(), + }; + }), + }; +}); + +describe('convert series to datatables', () => { + let indexPattern: IndexPattern; + + beforeEach(() => { + const fieldMap: Record = { + test1: { name: 'test1', spec: { type: 'date' } } as IndexPatternField, + test2: { name: 'test2' } as IndexPatternField, + test3: { name: 'test3', spec: { type: 'boolean' } } as IndexPatternField, + }; + + const getFieldByName = (name: string): IndexPatternField | undefined => fieldMap[name]; + indexPattern = { + id: 'index1', + title: 'index1', + timeFieldName: 'timestamp', + getFieldByName, + } as IndexPattern; + }); + + describe('addMetaColumns()', () => { + test('adds the correct meta to a date column', () => { + const columns = [{ id: 0, name: 'test1', isSplit: false }]; + const columnsWithMeta = addMetaToColumns(columns, indexPattern, 'count'); + expect(columnsWithMeta).toEqual([ + { + id: '0', + meta: { + field: 'test1', + index: 'index1', + source: 'esaggs', + sourceParams: { + enabled: true, + indexPatternId: 'index1', + type: 'date_histogram', + }, + type: 'date', + }, + name: 'test1', + }, + ]); + }); + + test('adds the correct meta to a non date column', () => { + const columns = [{ id: 1, name: 'Average of test2', isSplit: false }]; + const columnsWithMeta = addMetaToColumns(columns, indexPattern, 'avg'); + expect(columnsWithMeta).toEqual([ + { + id: '1', + meta: { + field: 'Average of test2', + index: 'index1', + source: 'esaggs', + sourceParams: { + enabled: true, + indexPatternId: 'index1', + type: 'avg', + }, + type: 'number', + }, + name: 'Average of test2', + }, + ]); + }); + + test('adds the correct meta for a split column', () => { + const columns = [{ id: 2, name: 'test3', isSplit: true }]; + const columnsWithMeta = addMetaToColumns(columns, indexPattern, 'avg'); + expect(columnsWithMeta).toEqual([ + { + id: '2', + meta: { + field: 'test3', + index: 'index1', + source: 'esaggs', + sourceParams: { + enabled: true, + indexPatternId: 'index1', + type: 'terms', + }, + type: 'boolean', + }, + name: 'test3', + }, + ]); + }); + }); + + describe('convertSeriesToDataTable()', () => { + const model = { + series: [ + { + formatter: 'number', + id: 'series1', + label: '', + line_width: 1, + metrics: [ + { + field: 'test2', + id: 'series1', + type: 'avg', + }, + ], + split_mode: 'terms', + terms_field: 'Cancelled', + type: 'timeseries', + }, + ], + } as TimeseriesVisParams; + const series = ([ + { + id: 'series1:0', + label: 0, + splitByLabel: 'Average of test2', + labelFormatted: 'false', + data: [ + [1616454000000, 0], + [1616457600000, 5], + [1616461200000, 7], + [1616464800000, 8], + ], + seriesId: 'series1', + isSplitByTerms: true, + }, + { + id: 'series1:1', + label: 1, + splitByLabel: 'Average of test2', + labelFormatted: 'true', + data: [ + [1616454000000, 10], + [1616457600000, 12], + [1616461200000, 1], + [1616464800000, 14], + ], + seriesId: 'series1', + isSplitByTerms: true, + }, + ] as unknown) as PanelData[]; + test('creates one table for one layer series with the correct columns', async () => { + const tables = await convertSeriesToDataTable(model, series, indexPattern); + expect(Object.keys(tables).sort()).toEqual([model.series[0].id].sort()); + + expect(tables.series1.columns.length).toEqual(3); + expect(tables.series1.rows.length).toEqual(8); + }); + + test('the table rows for a series with term aggregation should be a combination of the different terms', async () => { + const tables = await convertSeriesToDataTable(model, series, indexPattern); + expect(Object.keys(tables).sort()).toEqual([model.series[0].id].sort()); + + expect(tables.series1.rows.length).toEqual(8); + const expected1 = series[0].data.map((d) => { + d.push(parseInt(series[0].label, 10)); + return d; + }); + const expected2 = series[1].data.map((d) => { + d.push(parseInt(series[1].label, 10)); + return d; + }); + expect(tables.series1.rows).toEqual([...expected1, ...expected2]); + }); + }); +}); diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_datatable.ts b/src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_datatable.ts new file mode 100644 index 0000000000000..164d93e490db1 --- /dev/null +++ b/src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_datatable.ts @@ -0,0 +1,113 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import { IndexPattern } from 'src/plugins/data/public'; +import { + Datatable, + DatatableRow, + DatatableColumn, + DatatableColumnType, +} from 'src/plugins/expressions/public'; +import { TimeseriesVisParams } from '../../../types'; +import { PanelData } from '../../../../common/types'; +import { fetchIndexPattern } from '../../../../common/index_patterns_utils'; +import { getDataStart } from '../../../services'; +import { X_ACCESSOR_INDEX } from '../../visualizations/constants'; + +interface TSVBTables { + [key: string]: Datatable; +} + +interface TSVBColumns { + id: number; + name: string; + isSplit: boolean; +} + +export const addMetaToColumns = ( + columns: TSVBColumns[], + indexPattern: IndexPattern, + metricsType: string +): DatatableColumn[] => { + return columns.map((column) => { + const field = indexPattern.getFieldByName(column.name); + const type = (field?.spec.type as DatatableColumnType) || 'number'; + const cleanedColumn = { + id: column.id.toString(), + name: column.name, + meta: { + type, + field: column.name, + index: indexPattern.title, + source: 'esaggs', + sourceParams: { + enabled: true, + indexPatternId: indexPattern?.id, + type: type === 'date' ? 'date_histogram' : column.isSplit ? 'terms' : metricsType, + }, + }, + }; + return cleanedColumn; + }); +}; + +export const convertSeriesToDataTable = async ( + model: TimeseriesVisParams, + series: PanelData[], + initialIndexPattern: IndexPattern +) => { + const tables: TSVBTables = {}; + const { indexPatterns } = getDataStart(); + for (let layerIdx = 0; layerIdx < model.series.length; layerIdx++) { + const layer = model.series[layerIdx]; + let usedIndexPattern = initialIndexPattern; + // The user can overwrite the index pattern of a layer. + // In that case, the index pattern should be fetched again. + if (layer.override_index_pattern) { + const { indexPattern } = await fetchIndexPattern(layer.series_index_pattern, indexPatterns); + if (indexPattern) { + usedIndexPattern = indexPattern; + } + } + const isGroupedByTerms = layer.split_mode === 'terms'; + const seriesPerLayer = series.filter((s) => s.seriesId === layer.id); + let id = X_ACCESSOR_INDEX; + + const columns: TSVBColumns[] = [ + { id, name: usedIndexPattern.timeFieldName || '', isSplit: false }, + ]; + if (seriesPerLayer.length) { + id++; + columns.push({ id, name: seriesPerLayer[0].splitByLabel, isSplit: false }); + // Adds an extra column, if the layer is split by terms aggregation + if (isGroupedByTerms) { + id++; + columns.push({ id, name: layer.terms_field || '', isSplit: true }); + } + } + const columnsWithMeta = addMetaToColumns(columns, usedIndexPattern, layer.metrics[0].type); + + let rows: DatatableRow[] = []; + for (let j = 0; j < seriesPerLayer.length; j++) { + const data = seriesPerLayer[j].data.map((rowData) => { + const row: DatatableRow = [rowData[0], rowData[1]]; + // If the layer is split by terms aggregation, the data array should also contain the split value. + if (isGroupedByTerms) { + row.push(seriesPerLayer[j].label); + } + return row; + }); + rows = [...rows, ...data]; + } + tables[layer.id] = { + type: 'datatable', + rows, + columns: columnsWithMeta, + }; + } + return tables; +}; diff --git a/src/plugins/vis_type_timeseries/public/application/components/timeseries_visualization.tsx b/src/plugins/vis_type_timeseries/public/application/components/timeseries_visualization.tsx index ad4949259cfaf..7fba2e1cb701f 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/timeseries_visualization.tsx +++ b/src/plugins/vis_type_timeseries/public/application/components/timeseries_visualization.tsx @@ -21,8 +21,12 @@ import { PaletteRegistry } from 'src/plugins/charts/public'; // @ts-expect-error import { ErrorComponent } from './error'; import { TimeseriesVisTypes } from './vis_types'; +import { TimeseriesVisData, PanelData, isVisSeriesData } from '../../../common/types'; +import { fetchIndexPattern } from '../../../common/index_patterns_utils'; import { TimeseriesVisParams } from '../../types'; -import { isVisSeriesData, TimeseriesVisData } from '../../../common/types'; +import { getDataStart } from '../../services'; +import { convertSeriesToDataTable } from './lib/convert_series_to_datatable'; +import { X_ACCESSOR_INDEX } from '../visualizations/constants'; import { LastValueModeIndicator } from './last_value_mode_indicator'; import { getInterval } from './lib/get_interval'; import { AUTO_INTERVAL } from '../../../common/constants'; @@ -51,25 +55,29 @@ function TimeseriesVisualization({ palettesService, }: TimeseriesVisualizationProps) { const onBrush = useCallback( - (gte: string, lte: string) => { - handlers.event({ - name: 'applyFilter', + async (gte: string, lte: string, series: PanelData[]) => { + const indexPatternValue = model.index_pattern || ''; + const { indexPatterns } = getDataStart(); + const { indexPattern } = await fetchIndexPattern(indexPatternValue, indexPatterns); + + const tables = indexPattern + ? await convertSeriesToDataTable(model, series, indexPattern) + : null; + const table = tables?.[model.series[0].id]; + + const range: [number, number] = [parseInt(gte, 10), parseInt(lte, 10)]; + const event = { data: { - timeFieldName: '*', - filters: [ - { - range: { - '*': { - gte, - lte, - }, - }, - }, - ], + table, + column: X_ACCESSOR_INDEX, + range, + timeFieldName: indexPattern?.timeFieldName, }, - }); + name: 'brush', + }; + handlers.event(event); }, - [handlers] + [handlers, model] ); const handleUiState = useCallback( diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/index.ts b/src/plugins/vis_type_timeseries/public/application/components/vis_types/index.ts index 0e169c50e4db6..3447641352468 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_types/index.ts +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_types/index.ts @@ -13,7 +13,7 @@ import { PersistedState } from 'src/plugins/visualizations/public'; import { PaletteRegistry } from 'src/plugins/charts/public'; import { TimeseriesVisParams } from '../../../types'; -import { TimeseriesVisData } from '../../../../common/types'; +import { TimeseriesVisData, PanelData } from '../../../../common/types'; /** * Lazy load each visualization type, since the only one is presented on the screen at the same time. @@ -44,7 +44,7 @@ export const TimeseriesVisTypes: Record void; + onBrush: (gte: string, lte: string, series: PanelData[]) => Promise; onUiState: ( field: string, value: { diff --git a/src/plugins/vis_type_timeseries/public/application/visualizations/constants/chart.js b/src/plugins/vis_type_timeseries/public/application/visualizations/constants/chart.ts similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/visualizations/constants/chart.js rename to src/plugins/vis_type_timeseries/public/application/visualizations/constants/chart.ts diff --git a/src/plugins/vis_type_timeseries/public/application/visualizations/constants/icons.js b/src/plugins/vis_type_timeseries/public/application/visualizations/constants/icons.ts similarity index 97% rename from src/plugins/vis_type_timeseries/public/application/visualizations/constants/icons.js rename to src/plugins/vis_type_timeseries/public/application/visualizations/constants/icons.ts index 1bc98c6c2a722..5fd6933fcef01 100644 --- a/src/plugins/vis_type_timeseries/public/application/visualizations/constants/icons.js +++ b/src/plugins/vis_type_timeseries/public/application/visualizations/constants/icons.ts @@ -5,8 +5,9 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ - +// @ts-expect-error import { bombIcon } from '../../components/svg/bomb_icon'; +// @ts-expect-error import { fireIcon } from '../../components/svg/fire_icon'; export const ICON_NAMES = { diff --git a/src/plugins/vis_type_timeseries/public/application/visualizations/constants/index.js b/src/plugins/vis_type_timeseries/public/application/visualizations/constants/index.ts similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/visualizations/constants/index.js rename to src/plugins/vis_type_timeseries/public/application/visualizations/constants/index.ts diff --git a/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/index.js b/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/index.js index 537344a6da39a..a90faea50f22a 100644 --- a/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/index.js +++ b/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/index.js @@ -100,7 +100,7 @@ export const TimeSeries = ({ return; } const [min, max] = x; - onBrush(min, max); + onBrush(min, max, series); }; const getSeriesColor = useCallback( diff --git a/src/plugins/vis_type_timeseries/public/metrics_type.ts b/src/plugins/vis_type_timeseries/public/metrics_type.ts index 5d5e082b2b7bb..4e45ddf434771 100644 --- a/src/plugins/vis_type_timeseries/public/metrics_type.ts +++ b/src/plugins/vis_type_timeseries/public/metrics_type.ts @@ -74,7 +74,7 @@ export const metricsVisDefinition = { }, toExpressionAst, getSupportedTriggers: () => { - return [VIS_EVENT_TO_TRIGGER.applyFilter]; + return [VIS_EVENT_TO_TRIGGER.brush]; }, inspectorAdapters: {}, getUsedIndexPattern: async (params: VisParams) => { diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_splits.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_splits.js index f22226e03a5aa..268c26115233e 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_splits.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_splits.js @@ -45,6 +45,7 @@ export async function getSplits(resp, panel, series, meta, extractFields) { const bucket = _.get(resp, `aggregations.${series.id}.buckets.${filter.id}`); bucket.id = `${series.id}:${filter.id}`; bucket.key = filter.id; + bucket.splitByLabel = splitByLabel; bucket.color = filter.color; bucket.label = filter.label || filter.filter.query || '*'; bucket.meta = meta; diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_splits.test.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_splits.test.js index e2ae404d98970..d26bfa9be893e 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_splits.test.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_splits.test.js @@ -257,6 +257,7 @@ describe('getSplits(resp, panel, series)', () => { key: 'filter-1', label: '200s', meta: { bucketSize: 10 }, + splitByLabel: 'Count', color: '#F00', timeseries: { buckets: [] }, }, @@ -264,6 +265,7 @@ describe('getSplits(resp, panel, series)', () => { id: 'SERIES:filter-2', key: 'filter-2', label: '300s', + splitByLabel: 'Count', meta: { bucketSize: 10 }, color: '#0F0', timeseries: { buckets: [] }, From f042ec8945b523d327ff6629e74d282933bad5a9 Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> Date: Fri, 2 Apr 2021 09:00:51 -0400 Subject: [PATCH 03/30] [SECURITY SOLUTIONS][Timeline] correlation bug (#96099) * fix pagination bug with correlation * fix close resolver to go back to prev tab --- .../public/common/mock/global_state.ts | 1 + .../public/common/mock/timeline_results.ts | 2 + .../components/alerts_table/actions.test.tsx | 1 + .../components/open_timeline/helpers.test.ts | 8 +++ .../timelines/containers/index.test.tsx | 31 ++++++++++ .../public/timelines/containers/index.tsx | 9 ++- .../timelines/store/timeline/defaults.ts | 1 + .../timelines/store/timeline/epic.test.ts | 1 + .../timelines/store/timeline/helpers.ts | 3 + .../public/timelines/store/timeline/model.ts | 2 + .../timelines/store/timeline/reducer.test.ts | 60 ++++++++++++++++++- .../timelines/store/timeline/reducer.ts | 1 + 12 files changed, 114 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/mock/global_state.ts b/x-pack/plugins/security_solution/public/common/mock/global_state.ts index c933afc98856b..eac8fb7f6813e 100644 --- a/x-pack/plugins/security_solution/public/common/mock/global_state.ts +++ b/x-pack/plugins/security_solution/public/common/mock/global_state.ts @@ -204,6 +204,7 @@ export const mockGlobalState: State = { timelineById: { test: { activeTab: TimelineTabs.query, + prevActiveTab: TimelineTabs.notes, deletedEventIds: [], id: 'test', savedObjectId: null, diff --git a/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts b/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts index a9214eed60b36..5aef3b97c81b7 100644 --- a/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts +++ b/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts @@ -2062,6 +2062,7 @@ export const mockTimelineResults: OpenTimelineResult[] = [ export const mockTimelineModel: TimelineModel = { activeTab: TimelineTabs.query, + prevActiveTab: TimelineTabs.notes, columns: [ { columnHeaderType: 'not-filtered', @@ -2209,6 +2210,7 @@ export const defaultTimelineProps: CreateTimelineProps = { from: '2018-11-05T18:58:25.937Z', timeline: { activeTab: TimelineTabs.query, + prevActiveTab: TimelineTabs.query, columns: [ { columnHeaderType: 'not-filtered', id: '@timestamp', type: 'number', width: 190 }, { columnHeaderType: 'not-filtered', id: 'message', width: 180 }, diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx index a8aa42a3a59ff..6eccba954a175 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx @@ -108,6 +108,7 @@ describe('alert actions', () => { notes: null, timeline: { activeTab: TimelineTabs.query, + prevActiveTab: TimelineTabs.query, columns: [ { columnHeaderType: 'not-filtered', diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts index 705ddd62470a7..4d1c9e8037455 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts @@ -240,6 +240,7 @@ describe('helpers', () => { const newTimeline = defaultTimelineToTimelineModel(timeline, false); expect(newTimeline).toEqual({ activeTab: TimelineTabs.query, + prevActiveTab: TimelineTabs.query, columns: [ { columnHeaderType: 'not-filtered', @@ -350,6 +351,7 @@ describe('helpers', () => { const newTimeline = defaultTimelineToTimelineModel(timeline, false, TimelineType.template); expect(newTimeline).toEqual({ activeTab: TimelineTabs.query, + prevActiveTab: TimelineTabs.query, columns: [ { columnHeaderType: 'not-filtered', @@ -460,6 +462,7 @@ describe('helpers', () => { const newTimeline = defaultTimelineToTimelineModel(timeline, false, TimelineType.default); expect(newTimeline).toEqual({ activeTab: TimelineTabs.query, + prevActiveTab: TimelineTabs.query, columns: [ { columnHeaderType: 'not-filtered', @@ -568,6 +571,7 @@ describe('helpers', () => { const newTimeline = defaultTimelineToTimelineModel(timeline, false); expect(newTimeline).toEqual({ activeTab: TimelineTabs.query, + prevActiveTab: TimelineTabs.query, columns: [ { columnHeaderType: 'not-filtered', @@ -676,6 +680,7 @@ describe('helpers', () => { const newTimeline = defaultTimelineToTimelineModel(timeline, false); expect(newTimeline).toEqual({ activeTab: TimelineTabs.query, + prevActiveTab: TimelineTabs.query, savedObjectId: 'savedObject-1', columns: [ { @@ -852,6 +857,7 @@ describe('helpers', () => { const newTimeline = defaultTimelineToTimelineModel(timeline, false); expect(newTimeline).toEqual({ activeTab: TimelineTabs.query, + prevActiveTab: TimelineTabs.query, savedObjectId: 'savedObject-1', columns: [ { @@ -1000,6 +1006,7 @@ describe('helpers', () => { const newTimeline = defaultTimelineToTimelineModel(timeline, false, TimelineType.template); expect(newTimeline).toEqual({ activeTab: TimelineTabs.query, + prevActiveTab: TimelineTabs.query, columns: [ { columnHeaderType: 'not-filtered', @@ -1110,6 +1117,7 @@ describe('helpers', () => { const newTimeline = defaultTimelineToTimelineModel(timeline, false, TimelineType.default); expect(newTimeline).toEqual({ activeTab: TimelineTabs.query, + prevActiveTab: TimelineTabs.query, columns: [ { columnHeaderType: 'not-filtered', diff --git a/x-pack/plugins/security_solution/public/timelines/containers/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/containers/index.test.tsx index 7f38de0cebbd5..b24a50a516325 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/containers/index.test.tsx @@ -208,4 +208,35 @@ describe('useTimelineEvents', () => { ]); }); }); + + test('Correlation pagination is calling search strategy when switching page', async () => { + await act(async () => { + const { result, waitForNextUpdate, rerender } = renderHook< + UseTimelineEventsProps, + [boolean, TimelineArgs] + >((args) => useTimelineEvents(args), { + initialProps: { + ...props, + language: 'eql', + eqlOptions: { + eventCategoryField: 'category', + tiebreakerField: '', + timestampField: '@timestamp', + query: 'find it EQL', + size: 100, + }, + }, + }); + + // useEffect on params request + await waitForNextUpdate(); + rerender({ ...props, startDate, endDate }); + // useEffect on params request + await waitForNextUpdate(); + expect(mockSearch).toHaveBeenCalledTimes(2); + result.current[1].loadPage(4); + await waitForNextUpdate(); + expect(mockSearch).toHaveBeenCalledTimes(3); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/containers/index.tsx b/x-pack/plugins/security_solution/public/timelines/containers/index.tsx index 38fa81a4fb7c2..ab4b4358fd326 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/containers/index.tsx @@ -143,7 +143,6 @@ export const useTimelineEvents = ({ activeTimeline.setExpandedDetail({}); activeTimeline.setActivePage(newActivePage); } - setActivePage(newActivePage); }, [clearSignalsState, id] @@ -294,22 +293,22 @@ export const useTimelineEvents = ({ querySize: prevRequest?.pagination.querySize ?? 0, sort: prevRequest?.sort ?? initSortDefault, timerange: prevRequest?.timerange ?? {}, - ...(prevEqlRequest?.eventCategoryField + ...(!isEmpty(prevEqlRequest?.eventCategoryField) ? { eventCategoryField: prevEqlRequest?.eventCategoryField, } : {}), - ...(prevEqlRequest?.size + ...(!isEmpty(prevEqlRequest?.size) ? { size: prevEqlRequest?.size, } : {}), - ...(prevEqlRequest?.tiebreakerField + ...(!isEmpty(prevEqlRequest?.tiebreakerField) ? { tiebreakerField: prevEqlRequest?.tiebreakerField, } : {}), - ...(prevEqlRequest?.timestampField + ...(!isEmpty(prevEqlRequest?.timestampField) ? { timestampField: prevEqlRequest?.timestampField, } diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/defaults.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/defaults.ts index 5f9e64843573f..df79ff1d2b309 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/defaults.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/defaults.ts @@ -18,6 +18,7 @@ const { from: start, to: end } = normalizeTimeRange({ from: '', to: '' }, false) export const timelineDefaults: SubsetTimelineModel & Pick = { activeTab: TimelineTabs.query, + prevActiveTab: TimelineTabs.query, columns: defaultHeaders, dataProviders: [], dateRange: { start, end }, diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.test.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.test.ts index 57fa86f853c8d..0bc1c5d57fa33 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.test.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.test.ts @@ -16,6 +16,7 @@ describe('Epic Timeline', () => { test('should return a TimelineInput instead of TimelineModel ', () => { const timelineModel: TimelineModel = { activeTab: TimelineTabs.query, + prevActiveTab: TimelineTabs.notes, columns: [ { columnHeaderType: 'not-filtered', diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts index 864e52fc377a0..135cbb3f73281 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts @@ -305,6 +305,9 @@ export const updateGraphEventId = ({ [id]: { ...timeline, graphEventId, + ...(graphEventId === '' && id === TimelineId.active + ? { activeTab: timeline.prevActiveTab, prevActiveTab: timeline.activeTab } + : {}), }, }; }; diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts index b1ff4a1e89729..a899994ad4aab 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts @@ -51,6 +51,7 @@ export interface ColumnHeaderOptions { export interface TimelineModel { /** The selected tab to displayed in the timeline */ activeTab: TimelineTabs; + prevActiveTab: TimelineTabs; /** The columns displayed in the timeline */ columns: ColumnHeaderOptions[]; /** Timeline saved object owner */ @@ -142,6 +143,7 @@ export type SubsetTimelineModel = Readonly< Pick< TimelineModel, | 'activeTab' + | 'prevActiveTab' | 'columns' | 'dataProviders' | 'deletedEventIds' diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.test.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.test.ts index acdf064c2355f..e464637c469f8 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.test.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.test.ts @@ -6,7 +6,12 @@ */ import { cloneDeep } from 'lodash/fp'; -import { TimelineType, TimelineStatus, TimelineTabs } from '../../../../common/types/timeline'; +import { + TimelineType, + TimelineStatus, + TimelineTabs, + TimelineId, +} from '../../../../common/types/timeline'; import { IS_OPERATOR, @@ -39,6 +44,7 @@ import { updateTimelineSort, updateTimelineTitleAndDescription, upsertTimelineColumn, + updateGraphEventId, } from './helpers'; import { ColumnHeaderOptions, TimelineModel } from './model'; import { timelineDefaults } from './defaults'; @@ -69,6 +75,7 @@ const basicDataProvider: DataProvider = { }; const basicTimeline: TimelineModel = { activeTab: TimelineTabs.query, + prevActiveTab: TimelineTabs.graph, columns: [], dataProviders: [{ ...basicDataProvider }], dateRange: { @@ -1757,4 +1764,55 @@ describe('Timeline', () => { ]); }); }); + + describe('#updateGraphEventId', () => { + test('should return a new reference and not the same reference', () => { + const update = updateGraphEventId({ + id: 'foo', + graphEventId: '123', + timelineById: timelineByIdMock, + }); + expect(update).not.toBe(timelineByIdMock); + }); + + test('should empty graphEventId', () => { + const update = updateGraphEventId({ + id: 'foo', + graphEventId: '', + timelineById: timelineByIdMock, + }); + expect(update.foo.graphEventId).toEqual(''); + }); + + test('should empty graphEventId and not change activeTab and prevActiveTab because TimelineId !== TimelineId.active', () => { + const update = updateGraphEventId({ + id: 'foo', + graphEventId: '', + timelineById: timelineByIdMock, + }); + expect(update.foo.graphEventId).toEqual(''); + expect(update.foo.activeTab).toEqual(timelineByIdMock.foo.activeTab); + expect(update.foo.prevActiveTab).toEqual(timelineByIdMock.foo.prevActiveTab); + }); + + test('should empty graphEventId and return to the previous tab if TimelineId === TimelineId.active', () => { + const mock = cloneDeep(timelineByIdMock); + mock[TimelineId.active] = { + ...timelineByIdMock.foo, + activeTab: TimelineTabs.graph, + prevActiveTab: TimelineTabs.eql, + }; + delete mock.foo; + + const update = updateGraphEventId({ + id: TimelineId.active, + graphEventId: '', + timelineById: mock, + }); + + expect(update[TimelineId.active].graphEventId).toEqual(''); + expect(update[TimelineId.active].activeTab).toEqual(TimelineTabs.eql); + expect(update[TimelineId.active].prevActiveTab).toEqual(TimelineTabs.graph); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts index 332d9ad4ba91b..80c6d83075719 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts @@ -526,6 +526,7 @@ export const timelineReducer = reducerWithInitialState(initialTimelineState) [id]: { ...state.timelineById[id], activeTab, + prevActiveTab: state.timelineById[id].activeTab, }, }, })) From 3237fd063765bc95a9b3b5db0f7fc672bcf495f1 Mon Sep 17 00:00:00 2001 From: Constance Date: Fri, 2 Apr 2021 06:05:53 -0700 Subject: [PATCH 04/30] [Enterprise Search] Fix eslint import rule not ordering sibling mocks with parent mocks (#96070) * Enterprise Search eslint import order rule fix - mocks in current folder should be grouped with mocks from parent folders * Run --fix/update instances of importing ./__mocks__ --- .eslintrc.js | 2 +- .../search_experience/search_experience_content.test.tsx | 2 +- .../shared/role_mapping/role_mappings_table.test.tsx | 4 ++-- .../workplace_search/views/groups/group_logic.test.ts | 2 +- .../workplace_search/views/groups/groups_logic.test.ts | 2 +- .../workplace_search/views/overview/onboarding_steps.test.tsx | 3 +-- .../views/overview/organization_stats.test.tsx | 2 +- .../workplace_search/views/overview/overview.test.tsx | 2 +- .../workplace_search/views/overview/overview_logic.test.ts | 2 +- .../workplace_search/views/overview/recent_activity.test.tsx | 3 +-- .../views/overview_mvp/onboarding_steps.test.tsx | 3 +-- .../views/overview_mvp/organization_stats.test.tsx | 2 +- .../workplace_search/views/overview_mvp/overview.test.tsx | 2 +- .../views/overview_mvp/overview_logic.test.ts | 2 +- .../views/overview_mvp/recent_activity.test.tsx | 3 +-- 15 files changed, 16 insertions(+), 20 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index a7b45534391c0..65c8e8ee2e694 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1180,7 +1180,7 @@ module.exports = { pathGroups: [ { pattern: - '{../../../../../../,../../../../../,../../../../,../../../,../../,../}{common/,*}__mocks__{*,/**}', + '{../../../../../../,../../../../../,../../../../,../../../,../../,../,./}{common/,*}__mocks__{*,/**}', group: 'unknown', }, { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience_content.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience_content.test.tsx index 49f51c2010e3a..96fcd8997f674 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience_content.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience_content.test.tsx @@ -6,6 +6,7 @@ */ import { setMockValues } from '../../../../__mocks__/kea.mock'; +import { setMockSearchContextState } from './__mocks__/hooks.mock'; import React from 'react'; @@ -16,7 +17,6 @@ import { Results } from '@elastic/react-search-ui'; import { SchemaTypes } from '../../../../shared/types'; -import { setMockSearchContextState } from './__mocks__/hooks.mock'; import { Pagination } from './pagination'; import { SearchExperienceContent } from './search_experience_content'; import { ResultView } from './views'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_table.test.tsx index 5589309d00ef8..e1c43dca581fe 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_table.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_table.test.tsx @@ -5,14 +5,14 @@ * 2.0. */ +import { wsRoleMapping, asRoleMapping } from './__mocks__/roles'; + import React from 'react'; import { shallow } from 'enzyme'; import { EuiFieldSearch, EuiTableRow } from '@elastic/eui'; -import { wsRoleMapping, asRoleMapping } from './__mocks__/roles'; - import { ALL_LABEL, ANY_AUTH_PROVIDER_OPTION_LABEL } from './constants'; import { RoleMappingsTable } from './role_mappings_table'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/group_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/group_logic.test.ts index 836efa82995fc..9f12e8f202d50 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/group_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/group_logic.test.ts @@ -12,12 +12,12 @@ import { mockHttpValues, } from '../../../__mocks__'; import { groups } from '../../__mocks__/groups.mock'; +import { mockGroupValues } from './__mocks__/group_logic.mock'; import { nextTick } from '@kbn/test/jest'; import { GROUPS_PATH } from '../../routes'; -import { mockGroupValues } from './__mocks__/group_logic.mock'; import { GroupLogic } from './group_logic'; describe('GroupLogic', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups_logic.test.ts index 806c6e1c69f84..bb6e7c0c76faf 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups_logic.test.ts @@ -9,13 +9,13 @@ import { LogicMounter, mockFlashMessageHelpers, mockHttpValues } from '../../../ import { contentSources } from '../../__mocks__/content_sources.mock'; import { groups } from '../../__mocks__/groups.mock'; import { users } from '../../__mocks__/users.mock'; +import { mockGroupsValues } from './__mocks__/groups_logic.mock'; import { nextTick } from '@kbn/test/jest'; import { JSON_HEADER as headers } from '../../../../../common/constants'; import { DEFAULT_META } from '../../../shared/constants'; -import { mockGroupsValues } from './__mocks__/groups_logic.mock'; import { GroupsLogic } from './groups_logic'; // We need to mock out the debounced functionality diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_steps.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_steps.test.tsx index 7a368e7d384ea..5059533519a6f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_steps.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_steps.test.tsx @@ -6,7 +6,7 @@ */ import { mockTelemetryActions } from '../../../__mocks__'; - +import { setMockValues } from './__mocks__'; import './__mocks__/overview_logic.mock'; import React from 'react'; @@ -15,7 +15,6 @@ import { shallow } from 'enzyme'; import { SOURCES_PATH, USERS_PATH } from '../../routes'; -import { setMockValues } from './__mocks__'; import { OnboardingCard } from './onboarding_card'; import { OnboardingSteps, OrgNameOnboarding } from './onboarding_steps'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/organization_stats.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/organization_stats.test.tsx index 412977f18fadf..110557ac4087a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/organization_stats.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/organization_stats.test.tsx @@ -5,6 +5,7 @@ * 2.0. */ +import { setMockValues } from './__mocks__'; import './__mocks__/overview_logic.mock'; import React from 'react'; @@ -13,7 +14,6 @@ import { shallow } from 'enzyme'; import { EuiFlexGrid } from '@elastic/eui'; -import { setMockValues } from './__mocks__'; import { OrganizationStats } from './organization_stats'; import { StatisticCard } from './statistic_card'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview.test.tsx index 2ec2d949ff491..19c893bec81ea 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview.test.tsx @@ -7,6 +7,7 @@ import '../../../__mocks__/react_router_history.mock'; import './__mocks__/overview_logic.mock'; +import { mockActions, setMockValues } from './__mocks__'; import React from 'react'; @@ -15,7 +16,6 @@ import { shallow, mount } from 'enzyme'; import { Loading } from '../../../shared/loading'; import { ViewContentHeader } from '../../components/shared/view_content_header'; -import { mockActions, setMockValues } from './__mocks__'; import { OnboardingSteps } from './onboarding_steps'; import { OrganizationStats } from './organization_stats'; import { Overview } from './overview'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview_logic.test.ts index 0e84315104343..75a41216ffbb7 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview_logic.test.ts @@ -6,8 +6,8 @@ */ import { LogicMounter, mockHttpValues } from '../../../__mocks__'; - import { mockOverviewValues } from './__mocks__'; + import { OverviewLogic } from './overview_logic'; describe('OverviewLogic', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/recent_activity.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/recent_activity.test.tsx index 9ab7b908ad3cd..3a925f011cc18 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/recent_activity.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/recent_activity.test.tsx @@ -6,7 +6,7 @@ */ import { mockTelemetryActions } from '../../../__mocks__'; - +import { setMockValues } from './__mocks__'; import './__mocks__/overview_logic.mock'; import React from 'react'; @@ -18,7 +18,6 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { EuiLinkTo } from '../../../shared/react_router_helpers'; -import { setMockValues } from './__mocks__'; import { RecentActivity, RecentActivityItem } from './recent_activity'; const organization = { name: 'foo', defaultOrgName: 'bar' }; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview_mvp/onboarding_steps.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview_mvp/onboarding_steps.test.tsx index 7a368e7d384ea..5059533519a6f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview_mvp/onboarding_steps.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview_mvp/onboarding_steps.test.tsx @@ -6,7 +6,7 @@ */ import { mockTelemetryActions } from '../../../__mocks__'; - +import { setMockValues } from './__mocks__'; import './__mocks__/overview_logic.mock'; import React from 'react'; @@ -15,7 +15,6 @@ import { shallow } from 'enzyme'; import { SOURCES_PATH, USERS_PATH } from '../../routes'; -import { setMockValues } from './__mocks__'; import { OnboardingCard } from './onboarding_card'; import { OnboardingSteps, OrgNameOnboarding } from './onboarding_steps'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview_mvp/organization_stats.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview_mvp/organization_stats.test.tsx index 412977f18fadf..110557ac4087a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview_mvp/organization_stats.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview_mvp/organization_stats.test.tsx @@ -5,6 +5,7 @@ * 2.0. */ +import { setMockValues } from './__mocks__'; import './__mocks__/overview_logic.mock'; import React from 'react'; @@ -13,7 +14,6 @@ import { shallow } from 'enzyme'; import { EuiFlexGrid } from '@elastic/eui'; -import { setMockValues } from './__mocks__'; import { OrganizationStats } from './organization_stats'; import { StatisticCard } from './statistic_card'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview_mvp/overview.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview_mvp/overview.test.tsx index 2ec2d949ff491..19c893bec81ea 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview_mvp/overview.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview_mvp/overview.test.tsx @@ -7,6 +7,7 @@ import '../../../__mocks__/react_router_history.mock'; import './__mocks__/overview_logic.mock'; +import { mockActions, setMockValues } from './__mocks__'; import React from 'react'; @@ -15,7 +16,6 @@ import { shallow, mount } from 'enzyme'; import { Loading } from '../../../shared/loading'; import { ViewContentHeader } from '../../components/shared/view_content_header'; -import { mockActions, setMockValues } from './__mocks__'; import { OnboardingSteps } from './onboarding_steps'; import { OrganizationStats } from './organization_stats'; import { Overview } from './overview'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview_mvp/overview_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview_mvp/overview_logic.test.ts index 0e84315104343..75a41216ffbb7 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview_mvp/overview_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview_mvp/overview_logic.test.ts @@ -6,8 +6,8 @@ */ import { LogicMounter, mockHttpValues } from '../../../__mocks__'; - import { mockOverviewValues } from './__mocks__'; + import { OverviewLogic } from './overview_logic'; describe('OverviewLogic', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview_mvp/recent_activity.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview_mvp/recent_activity.test.tsx index 0b62207afc520..7213526c8864a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview_mvp/recent_activity.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview_mvp/recent_activity.test.tsx @@ -6,7 +6,7 @@ */ import { mockTelemetryActions } from '../../../__mocks__'; - +import { setMockValues } from './__mocks__'; import './__mocks__/overview_logic.mock'; import React from 'react'; @@ -16,7 +16,6 @@ import { shallow } from 'enzyme'; import { EuiEmptyPrompt, EuiLink } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { setMockValues } from './__mocks__'; import { RecentActivity, RecentActivityItem } from './recent_activity'; const organization = { name: 'foo', defaultOrgName: 'bar' }; From fe3ec69f9e62f95ba370360603988591f74344d2 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 2 Apr 2021 16:01:06 +0200 Subject: [PATCH 05/30] Update dependency @elastic/charts to v27 (#95963) Co-authored-by: Renovate Bot --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index deb8bc24319f6..34e044140d297 100644 --- a/package.json +++ b/package.json @@ -97,7 +97,7 @@ "dependencies": { "@elastic/apm-rum": "^5.6.1", "@elastic/apm-rum-react": "^1.2.5", - "@elastic/charts": "26.1.0", + "@elastic/charts": "27.0.0", "@elastic/datemath": "link:bazel-bin/packages/elastic-datemath/npm_module", "@elastic/elasticsearch": "npm:@elastic/elasticsearch-canary@^8.0.0-canary.4", "@elastic/ems-client": "7.12.0", diff --git a/yarn.lock b/yarn.lock index a7e5b0bbfe4dd..832a8561bd71c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1359,10 +1359,10 @@ dependencies: object-hash "^1.3.0" -"@elastic/charts@26.1.0": - version "26.1.0" - resolved "https://registry.yarnpkg.com/@elastic/charts/-/charts-26.1.0.tgz#3c8677d84e52ac7209aee19484fb2b7e2a22e5cc" - integrity sha512-RiidG+9QIn17o5AW8cntrznH+MaOO8gIAwrkJW1EMInntZgEA66WhVs4Kg2Negp6hsPMMeArQVWbDhXE9ST3qg== +"@elastic/charts@27.0.0": + version "27.0.0" + resolved "https://registry.yarnpkg.com/@elastic/charts/-/charts-27.0.0.tgz#cc6ea80dc90d07cfad0a932200cad2f6b217f7b8" + integrity sha512-gnLT+htGgcYzPUpa3NTBQyD8bw7t+0aAxdpVnBL7fZ0TdbX0xQ7u1yPEI9ljMbGguiVJMKoI1KMVLI49E3f1bg== dependencies: "@popperjs/core" "^2.4.0" chroma-js "^2.1.0" From 8fef5fd9e1289b7e383f8ab7e39faa5f4126386b Mon Sep 17 00:00:00 2001 From: Melissa Alvarez Date: Fri, 2 Apr 2021 11:06:31 -0400 Subject: [PATCH 06/30] [ML] Data Frame Analytics: adds support for runtime fields (#95734) * add runtime mapping editor in wizard * ensure depVar is updated correctly with RF changes * remove old RF from includes * ensure cloning works with RF as depVar * ensure indexPattern RF work * scatterplot supports RTF. depVar options have indexPattern RTF on first load * remove unnecessary types * ensure supported fields included by default * update types in editor * use isRuntimeMappings * fix translations. ensure runtimeMappings persist when going back to step 1 * ensure histograms support runtime fields * update types --- .../ml/common/types/data_frame_analytics.ts | 3 + x-pack/plugins/ml/common/types/fields.ts | 4 +- .../components/data_grid/common.ts | 51 +-- .../application/components/data_grid/index.ts | 2 +- .../scatterplot_matrix/scatterplot_matrix.tsx | 14 + .../configuration_step/configuration_step.tsx | 14 +- .../configuration_step_form.tsx | 332 ++++++++++++------ .../form_options_validation.ts | 67 +++- .../components/runtime_mappings/index.ts | 8 + .../runtime_mappings/runtime_mappings.tsx | 237 +++++++++++++ .../runtime_mappings_editor.tsx | 82 +++++ .../hooks/use_index_data.ts | 83 ++++- .../pages/analytics_creation/page.tsx | 1 + .../action_clone/clone_action_name.tsx | 5 + .../use_create_analytics_form/reducer.ts | 8 +- .../hooks/use_create_analytics_form/state.ts | 12 + .../index_based/data_loader/data_loader.ts | 5 +- .../routes/schemas/data_analytics_schema.ts | 3 + .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 2 - 20 files changed, 767 insertions(+), 167 deletions(-) create mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/runtime_mappings/index.ts create mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/runtime_mappings/runtime_mappings.tsx create mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/runtime_mappings/runtime_mappings_editor.tsx diff --git a/x-pack/plugins/ml/common/types/data_frame_analytics.ts b/x-pack/plugins/ml/common/types/data_frame_analytics.ts index 8686e3d64037e..d9632f4d4a83b 100644 --- a/x-pack/plugins/ml/common/types/data_frame_analytics.ts +++ b/x-pack/plugins/ml/common/types/data_frame_analytics.ts @@ -6,6 +6,8 @@ */ import Boom from '@hapi/boom'; +import { RuntimeMappings } from './fields'; + import { EsErrorBody } from '../util/errors'; import { ANALYSIS_CONFIG_TYPE } from '../constants/data_frame_analytics'; import { DATA_FRAME_TASK_STATE } from '../constants/data_frame_analytics'; @@ -74,6 +76,7 @@ export interface DataFrameAnalyticsConfig { source: { index: IndexName | IndexName[]; query?: any; + runtime_mappings?: RuntimeMappings; }; analysis: AnalysisConfig; analyzed_fields: { diff --git a/x-pack/plugins/ml/common/types/fields.ts b/x-pack/plugins/ml/common/types/fields.ts index f9f7f8fc7ead6..8dfe9d111ed38 100644 --- a/x-pack/plugins/ml/common/types/fields.ts +++ b/x-pack/plugins/ml/common/types/fields.ts @@ -109,8 +109,8 @@ export interface AggCardinality { export type RollupFields = Record]>; // Replace this with import once #88995 is merged -const RUNTIME_FIELD_TYPES = ['keyword', 'long', 'double', 'date', 'ip', 'boolean'] as const; -type RuntimeType = typeof RUNTIME_FIELD_TYPES[number]; +export const RUNTIME_FIELD_TYPES = ['keyword', 'long', 'double', 'date', 'ip', 'boolean'] as const; +export type RuntimeType = typeof RUNTIME_FIELD_TYPES[number]; export interface RuntimeField { type: RuntimeType; diff --git a/x-pack/plugins/ml/public/application/components/data_grid/common.ts b/x-pack/plugins/ml/public/application/components/data_grid/common.ts index 312776f0d6a07..d3e58c4d7bb0d 100644 --- a/x-pack/plugins/ml/public/application/components/data_grid/common.ts +++ b/x-pack/plugins/ml/public/application/components/data_grid/common.ts @@ -49,9 +49,8 @@ import { getNestedProperty } from '../../util/object_utils'; import { mlFieldFormatService } from '../../services/field_format_service'; import { DataGridItem, IndexPagination, RenderCellValue } from './types'; -import type { RuntimeField } from '../../../../../../../src/plugins/data/common/index_patterns'; -import { RuntimeMappings } from '../../../../common/types/fields'; -import { isPopulatedObject } from '../../../../common/util/object_utils'; +import { RuntimeMappings, RuntimeField } from '../../../../common/types/fields'; +import { isRuntimeMappings } from '../../../../common/util/runtime_field_utils'; export const INIT_MAX_COLUMNS = 10; export const COLUMN_CHART_DEFAULT_VISIBILITY_ROWS_THRESHOLED = 10000; @@ -94,34 +93,36 @@ export const getFieldsFromKibanaIndexPattern = (indexPattern: IndexPattern): str /** * Return a map of runtime_mappings for each of the index pattern field provided * to provide in ES search queries - * @param indexPatternFields * @param indexPattern - * @param clonedRuntimeMappings + * @param RuntimeMappings */ -export const getRuntimeFieldsMapping = ( - indexPatternFields: string[] | undefined, +export function getCombinedRuntimeMappings( indexPattern: IndexPattern | undefined, - clonedRuntimeMappings?: RuntimeMappings -) => { - if (!Array.isArray(indexPatternFields) || indexPattern === undefined) return {}; - const ipRuntimeMappings = indexPattern.getComputedFields().runtimeFields; - let combinedRuntimeMappings: RuntimeMappings = {}; - - if (isPopulatedObject(ipRuntimeMappings)) { - indexPatternFields.forEach((ipField) => { - if (ipRuntimeMappings.hasOwnProperty(ipField)) { - // @ts-expect-error - combinedRuntimeMappings[ipField] = ipRuntimeMappings[ipField]; + runtimeMappings?: RuntimeMappings +): RuntimeMappings | undefined { + let combinedRuntimeMappings = {}; + + // And runtime field mappings defined by index pattern + if (indexPattern) { + const computedFields = indexPattern?.getComputedFields(); + if (computedFields?.runtimeFields !== undefined) { + const indexPatternRuntimeMappings = computedFields.runtimeFields; + if (isRuntimeMappings(indexPatternRuntimeMappings)) { + combinedRuntimeMappings = { ...combinedRuntimeMappings, ...indexPatternRuntimeMappings }; } - }); + } } - if (isPopulatedObject(clonedRuntimeMappings)) { - combinedRuntimeMappings = { ...combinedRuntimeMappings, ...clonedRuntimeMappings }; + + // Use runtime field mappings defined inline from API + // and override fields with same name from index pattern + if (isRuntimeMappings(runtimeMappings)) { + combinedRuntimeMappings = { ...combinedRuntimeMappings, ...runtimeMappings }; } - return Object.keys(combinedRuntimeMappings).length > 0 - ? { runtime_mappings: combinedRuntimeMappings } - : {}; -}; + + if (isRuntimeMappings(combinedRuntimeMappings)) { + return combinedRuntimeMappings; + } +} export interface FieldTypes { [key: string]: ES_FIELD_TYPES; diff --git a/x-pack/plugins/ml/public/application/components/data_grid/index.ts b/x-pack/plugins/ml/public/application/components/data_grid/index.ts index be37e381d1bae..481ff432e0156 100644 --- a/x-pack/plugins/ml/public/application/components/data_grid/index.ts +++ b/x-pack/plugins/ml/public/application/components/data_grid/index.ts @@ -10,7 +10,7 @@ export { getDataGridSchemaFromESFieldType, getDataGridSchemaFromKibanaFieldType, getFieldsFromKibanaIndexPattern, - getRuntimeFieldsMapping, + getCombinedRuntimeMappings, multiColumnSortFactory, showDataGridColumnChartErrorMessageToast, useRenderCellValue, diff --git a/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix.tsx b/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix.tsx index 842d5fc1ae87a..bc76020d19649 100644 --- a/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix.tsx +++ b/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix.tsx @@ -24,9 +24,13 @@ import { import { i18n } from '@kbn/i18n'; +import { IndexPattern } from '../../../../../../../src/plugins/data/public'; import { extractErrorMessage } from '../../../../common'; +import { isRuntimeMappings } from '../../../../common/util/runtime_field_utils'; import { stringHash } from '../../../../common/util/string_utils'; +import { RuntimeMappings } from '../../../../common/types/fields'; import type { ResultsSearchQuery } from '../../data_frame_analytics/common/analytics'; +import { getCombinedRuntimeMappings } from '../../components/data_grid'; import { useMlApiContext } from '../../contexts/kibana'; @@ -84,6 +88,8 @@ export interface ScatterplotMatrixProps { color?: string; legendType?: LegendType; searchQuery?: ResultsSearchQuery; + runtimeMappings?: RuntimeMappings; + indexPattern?: IndexPattern; } export const ScatterplotMatrix: FC = ({ @@ -93,6 +99,8 @@ export const ScatterplotMatrix: FC = ({ color, legendType, searchQuery, + runtimeMappings, + indexPattern, }) => { const { esSearch } = useMlApiContext(); @@ -185,6 +193,9 @@ export const ScatterplotMatrix: FC = ({ } : searchQuery; + const combinedRuntimeMappings = + indexPattern && getCombinedRuntimeMappings(indexPattern, runtimeMappings); + const resp: estypes.SearchResponse = await esSearch({ index, body: { @@ -193,6 +204,9 @@ export const ScatterplotMatrix: FC = ({ query, from: 0, size: fetchSize, + ...(isRuntimeMappings(combinedRuntimeMappings) + ? { runtime_mappings: combinedRuntimeMappings } + : {}), }, }); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step.tsx index 3b9c84e2fa51a..710fd49f72fb6 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step.tsx @@ -13,12 +13,17 @@ import { ConfigurationStepDetails } from './configuration_step_details'; import { ConfigurationStepForm } from './configuration_step_form'; import { ANALYTICS_STEPS } from '../../page'; -export const ConfigurationStep: FC = ({ +export interface ConfigurationStepProps extends CreateAnalyticsStepProps { + isClone: boolean; +} + +export const ConfigurationStep: FC = ({ actions, state, setCurrentStep, step, stepActivated, + isClone, }) => { const showForm = step === ANALYTICS_STEPS.CONFIGURATION; const showDetails = step !== ANALYTICS_STEPS.CONFIGURATION && stepActivated === true; @@ -30,7 +35,12 @@ export const ConfigurationStep: FC = ({ return ( {showForm && ( - + )} {showDetails && } diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_form.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_form.tsx index 36d3de1376373..1046f1a8c3e92 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_form.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_form.tsx @@ -5,10 +5,9 @@ * 2.0. */ -import React, { FC, Fragment, useEffect, useMemo, useRef, useState } from 'react'; +import React, { FC, Fragment, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { EuiBadge, - EuiCallOut, EuiComboBox, EuiComboBoxOptionOption, EuiFormRow, @@ -18,11 +17,11 @@ import { EuiText, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { debounce } from 'lodash'; +import { debounce, cloneDeep } from 'lodash'; -import { FormattedMessage } from '@kbn/i18n/react'; import { newJobCapsService } from '../../../../../services/new_job_capabilities_service'; import { useMlContext } from '../../../../../contexts/ml'; +import { getCombinedRuntimeMappings } from '../../../../../components/data_grid/common'; import { ANALYSIS_CONFIG_TYPE, @@ -31,13 +30,18 @@ import { FieldSelectionItem, } from '../../../../common/analytics'; import { getScatterplotMatrixLegendType } from '../../../../common/get_scatterplot_matrix_legend_type'; -import { CreateAnalyticsStepProps } from '../../../analytics_management/hooks/use_create_analytics_form'; +import { RuntimeMappings as RuntimeMappingsType } from '../../../../../../../common/types/fields'; +import { + isRuntimeMappings, + isRuntimeField, +} from '../../../../../../../common/util/runtime_field_utils'; +import { AnalyticsJobType } from '../../../analytics_management/hooks/use_create_analytics_form/state'; import { Messages } from '../shared'; import { DEFAULT_MODEL_MEMORY_LIMIT, State, } from '../../../analytics_management/hooks/use_create_analytics_form/state'; -import { shouldAddAsDepVarOption } from './form_options_validation'; +import { handleExplainErrorMessage, shouldAddAsDepVarOption } from './form_options_validation'; import { getToastNotifications } from '../../../../../util/dependency_cache'; import { ANALYTICS_STEPS } from '../../page'; @@ -55,6 +59,18 @@ import { ExplorationQueryBarProps } from '../../../analytics_exploration/compone import { Query } from '../../../../../../../../../../src/plugins/data/common/query'; import { ScatterplotMatrix } from '../../../../../components/scatterplot_matrix'; +import { RuntimeMappings } from '../runtime_mappings'; +import { ConfigurationStepProps } from './configuration_step'; + +const runtimeMappingKey = 'runtime_mapping'; +const notIncludedReason = 'field not in includes list'; +const requiredFieldsErrorText = i18n.translate( + 'xpack.ml.dataframe.analytics.createWizard.requiredFieldsErrorMessage', + { + defaultMessage: + 'At least one field must be included in the analysis in addition to the dependent variable.', + } +); function getIndexDataQuery(savedSearchQuery: SavedSearchQuery, jobConfigQuery: any) { // Return `undefined` if savedSearchQuery itself is `undefined`, meaning it hasn't been initialized yet. @@ -65,18 +81,23 @@ function getIndexDataQuery(savedSearchQuery: SavedSearchQuery, jobConfigQuery: a return savedSearchQuery !== null ? savedSearchQuery : jobConfigQuery; } -const requiredFieldsErrorText = i18n.translate( - 'xpack.ml.dataframe.analytics.createWizard.requiredFieldsErrorMessage', - { - defaultMessage: - 'At least one field must be included in the analysis in addition to the dependent variable.', - } -); - -const maxRuntimeFieldsDisplayCount = 5; +function getRuntimeDepVarOptions(jobType: AnalyticsJobType, runtimeMappings: RuntimeMappingsType) { + const runtimeOptions: EuiComboBoxOptionOption[] = []; + Object.keys(runtimeMappings).forEach((id) => { + const field = runtimeMappings[id]; + if (isRuntimeField(field) && shouldAddAsDepVarOption(id, field.type, jobType)) { + runtimeOptions.push({ + label: id, + key: `runtime_mapping_${id}`, + }); + } + }); + return runtimeOptions; +} -export const ConfigurationStepForm: FC = ({ +export const ConfigurationStepForm: FC = ({ actions, + isClone, state, setCurrentStep, }) => { @@ -100,7 +121,7 @@ export const ConfigurationStepForm: FC = ({ >(); const { setEstimatedModelMemoryLimit, setFormState } = actions; - const { estimatedModelMemoryLimit, form, isJobCreated, requestMessages } = state; + const { cloneJob, estimatedModelMemoryLimit, form, isJobCreated, requestMessages } = state; const firstUpdate = useRef(true); const { dependentVariable, @@ -111,10 +132,22 @@ export const ConfigurationStepForm: FC = ({ modelMemoryLimit, previousJobType, requiredFieldsError, + runtimeMappings, + previousRuntimeMapping, + runtimeMappingsUpdated, sourceIndex, trainingPercent, useEstimatedMml, } = form; + + const isJobTypeWithDepVar = + jobType === ANALYSIS_CONFIG_TYPE.REGRESSION || jobType === ANALYSIS_CONFIG_TYPE.CLASSIFICATION; + const dependentVariableEmpty = isJobTypeWithDepVar && dependentVariable === ''; + const hasBasicRequiredFields = jobType !== undefined; + const hasRequiredAnalysisFields = + (isJobTypeWithDepVar && dependentVariable !== '') || + jobType === ANALYSIS_CONFIG_TYPE.OUTLIER_DETECTION; + const [query, setQuery] = useState({ query: jobConfigQueryString ?? '', language: SEARCH_QUERY_LANGUAGE.KUERY, @@ -132,7 +165,8 @@ export const ConfigurationStepForm: FC = ({ const indexData = useIndexData( currentIndexPattern, getIndexDataQuery(savedSearchQuery, jobConfigQuery), - toastNotifications + toastNotifications, + runtimeMappings ); const indexPreviewProps = { @@ -141,11 +175,6 @@ export const ConfigurationStepForm: FC = ({ toastNotifications, }; - const isJobTypeWithDepVar = - jobType === ANALYSIS_CONFIG_TYPE.REGRESSION || jobType === ANALYSIS_CONFIG_TYPE.CLASSIFICATION; - - const dependentVariableEmpty = isJobTypeWithDepVar && dependentVariable === ''; - const isStepInvalid = dependentVariableEmpty || jobType === undefined || @@ -155,20 +184,23 @@ export const ConfigurationStepForm: FC = ({ unsupportedFieldsError !== undefined || fetchingExplainData; - const loadDepVarOptions = async (formState: State['form']) => { + const loadDepVarOptions = async ( + formState: State['form'], + runtimeOptions: EuiComboBoxOptionOption[] = [] + ) => { setLoadingDepVarOptions(true); setMaxDistinctValuesError(undefined); try { if (currentIndexPattern !== undefined) { const depVarOptions = []; - let depVarUpdate = dependentVariable; + let depVarUpdate = formState.dependentVariable; // Get fields and filter for supported types for job type const { fields } = newJobCapsService; let resetDependentVariable = true; for (const field of fields) { - if (shouldAddAsDepVarOption(field, jobType)) { + if (shouldAddAsDepVarOption(field.id, field.type, jobType)) { depVarOptions.push({ label: field.id, }); @@ -179,10 +211,21 @@ export const ConfigurationStepForm: FC = ({ } } + if ( + isRuntimeMappings(formState.runtimeMappings) && + Object.keys(formState.runtimeMappings).includes(form.dependentVariable) + ) { + resetDependentVariable = false; + depVarOptions.push({ + label: form.dependentVariable, + key: `runtime_mapping_${form.dependentVariable}`, + }); + } + if (resetDependentVariable) { depVarUpdate = ''; } - setDependentVariableOptions(depVarOptions); + setDependentVariableOptions([...runtimeOptions, ...depVarOptions]); setLoadingDepVarOptions(false); setDependentVariableFetchFail(false); setFormState({ dependentVariable: depVarUpdate }); @@ -209,8 +252,23 @@ export const ConfigurationStepForm: FC = ({ if (jobTypeChanged) { setLoadingFieldOptions(true); } + // Ensure runtime field is in 'includes' table if it is set as dependent variable + const depVarIsRuntimeField = + isJobTypeWithDepVar && + runtimeMappings && + Object.keys(runtimeMappings).includes(dependentVariable) && + includes.length > 0 && + includes.includes(dependentVariable) === false; + let formToUse = form; + + if (depVarIsRuntimeField) { + formToUse = cloneDeep(form); + formToUse.includes = [...includes, dependentVariable]; + } - const { success, expectedMemory, fieldSelection, errorMessage } = await fetchExplainData(form); + const { success, expectedMemory, fieldSelection, errorMessage } = await fetchExplainData( + formToUse + ); if (success) { if (shouldUpdateEstimatedMml) { @@ -226,53 +284,33 @@ export const ConfigurationStepForm: FC = ({ setFieldOptionsFetchFail(false); setMaxDistinctValuesError(undefined); setUnsupportedFieldsError(undefined); - setIncludesTableItems(fieldSelection ? fieldSelection : []); setFormState({ ...(shouldUpdateModelMemoryLimit ? { modelMemoryLimit: expectedMemory } : {}), requiredFieldsError: !hasRequiredFields ? requiredFieldsErrorText : undefined, + includes: formToUse.includes, }); + setIncludesTableItems(fieldSelection ? fieldSelection : []); } else { setFormState({ ...(shouldUpdateModelMemoryLimit ? { modelMemoryLimit: expectedMemory } : {}), requiredFieldsError: !hasRequiredFields ? requiredFieldsErrorText : undefined, + includes: formToUse.includes, }); } setFetchingExplainData(false); } else { - let maxDistinctValuesErrorMessage; - let unsupportedFieldsErrorMessage; - if ( - jobType === ANALYSIS_CONFIG_TYPE.CLASSIFICATION && - (errorMessage.includes('must have at most') || errorMessage.includes('must have at least')) - ) { - maxDistinctValuesErrorMessage = errorMessage; - } else if ( - errorMessage.includes('status_exception') && - errorMessage.includes('unsupported type') - ) { - unsupportedFieldsErrorMessage = errorMessage; - } else if ( - errorMessage.includes('status_exception') && - errorMessage.includes('Unable to estimate memory usage as no documents') - ) { - toastNotifications.addWarning( - i18n.translate('xpack.ml.dataframe.analytics.create.allDocsMissingFieldsErrorMessage', { - defaultMessage: `Unable to estimate memory usage. There are mapped fields for source index [{index}] that do not exist in any indexed documents. You will have to switch to the JSON editor for explicit field selection and include only fields that exist in indexed documents.`, - values: { - index: sourceIndex, - }, - }) - ); - } else { - toastNotifications.addDanger({ - title: i18n.translate( - 'xpack.ml.dataframe.analytics.create.unableToFetchExplainDataMessage', - { - defaultMessage: 'An error occurred fetching analysis fields data.', - } - ), - text: errorMessage, - }); + const { + maxDistinctValuesErrorMessage, + unsupportedFieldsErrorMessage, + toastNotificationDanger, + toastNotificationWarning, + } = handleExplainErrorMessage(errorMessage, sourceIndex, jobType); + + if (toastNotificationDanger) { + toastNotifications.addDanger(toastNotificationDanger); + } + if (toastNotificationWarning) { + toastNotifications.addWarning(toastNotificationWarning); } const fallbackModelMemoryLimit = @@ -304,17 +342,126 @@ export const ConfigurationStepForm: FC = ({ useEffect(() => { if (isJobTypeWithDepVar) { - loadDepVarOptions(form); + const indexPatternRuntimeFields = getCombinedRuntimeMappings(currentIndexPattern); + let runtimeOptions; + + if (indexPatternRuntimeFields) { + runtimeOptions = getRuntimeDepVarOptions(jobType, indexPatternRuntimeFields); + } + + loadDepVarOptions(form, runtimeOptions); } }, [jobType]); - useEffect(() => { - const hasBasicRequiredFields = jobType !== undefined; + const handleRuntimeUpdate = useCallback(async () => { + if (runtimeMappingsUpdated) { + // Update dependent variable options + let resetDepVar = false; + if (isJobTypeWithDepVar) { + const filteredOptions = dependentVariableOptions.filter((option) => { + if (option.label === dependentVariable && option.key?.includes(runtimeMappingKey)) { + resetDepVar = true; + } + return !option.key?.includes(runtimeMappingKey); + }); + // Runtime mappings have been removed + if (runtimeMappings === undefined && runtimeMappingsUpdated === true) { + setDependentVariableOptions(filteredOptions); + } else if (runtimeMappings) { + // add to filteredOptions if it's the type supported + const runtimeOptions = getRuntimeDepVarOptions(jobType, runtimeMappings); + setDependentVariableOptions([...filteredOptions, ...runtimeOptions]); + } + } - const hasRequiredAnalysisFields = - (isJobTypeWithDepVar && dependentVariable !== '') || - jobType === ANALYSIS_CONFIG_TYPE.OUTLIER_DETECTION; + // Update includes - remove previous runtime mappings then add supported runtime fields to includes + const updatedIncludes = includes.filter((field) => { + const isRemovedRuntimeField = previousRuntimeMapping && previousRuntimeMapping[field]; + return !isRemovedRuntimeField; + }); + if (resetDepVar) { + setFormState({ + dependentVariable: '', + includes: updatedIncludes, + }); + setIncludesTableItems( + includesTableItems.filter(({ name }) => { + const isRemovedRuntimeField = previousRuntimeMapping && previousRuntimeMapping[name]; + return !isRemovedRuntimeField; + }) + ); + } + + if (!resetDepVar && hasBasicRequiredFields && hasRequiredAnalysisFields) { + const formCopy = cloneDeep(form); + // When switching back to step ensure runtime field is in 'includes' table if it is set as dependent variable + const depVarIsRuntimeField = + isJobTypeWithDepVar && + runtimeMappings && + Object.keys(runtimeMappings).includes(dependentVariable) && + formCopy.includes.length > 0 && + formCopy.includes.includes(dependentVariable) === false; + + formCopy.includes = depVarIsRuntimeField + ? [...updatedIncludes, dependentVariable] + : updatedIncludes; + + const { success, fieldSelection, errorMessage } = await fetchExplainData(formCopy); + if (success) { + // update the field selection table + const hasRequiredFields = fieldSelection.some( + (field) => field.is_included === true && field.is_required === false + ); + let updatedFieldSelection; + // Update field selection to select supported runtime fields by default. Add those fields to 'includes'. + if (isRuntimeMappings(runtimeMappings)) { + updatedFieldSelection = fieldSelection.map((field) => { + if ( + runtimeMappings[field.name] !== undefined && + field.is_included === false && + field.reason?.includes(notIncludedReason) + ) { + updatedIncludes.push(field.name); + field.is_included = true; + } + return field; + }); + } + setIncludesTableItems(updatedFieldSelection ? updatedFieldSelection : fieldSelection); + setMaxDistinctValuesError(undefined); + setUnsupportedFieldsError(undefined); + setFormState({ + includes: updatedIncludes, + requiredFieldsError: !hasRequiredFields ? requiredFieldsErrorText : undefined, + }); + } else { + const { + maxDistinctValuesErrorMessage, + unsupportedFieldsErrorMessage, + toastNotificationDanger, + toastNotificationWarning, + } = handleExplainErrorMessage(errorMessage, sourceIndex, jobType); + + if (toastNotificationDanger) { + toastNotifications.addDanger(toastNotificationDanger); + } + if (toastNotificationWarning) { + toastNotifications.addWarning(toastNotificationWarning); + } + + setMaxDistinctValuesError(maxDistinctValuesErrorMessage); + setUnsupportedFieldsError(unsupportedFieldsErrorMessage); + } + } + } + }, [JSON.stringify(runtimeMappings)]); + + useEffect(() => { + handleRuntimeUpdate(); + }, [JSON.stringify(runtimeMappings)]); + + useEffect(() => { if (hasBasicRequiredFields && hasRequiredAnalysisFields) { debouncedGetExplainData(); } @@ -324,15 +471,6 @@ export const ConfigurationStepForm: FC = ({ }; }, [jobType, dependentVariable, trainingPercent, JSON.stringify(includes), jobConfigQueryString]); - const unsupportedRuntimeFields = useMemo( - () => - currentIndexPattern.fields - .getAll() - .filter((f) => f.runtimeField) - .map((f) => `'${f.displayName}'`), - [currentIndexPattern.fields] - ); - const scatterplotMatrixProps = useMemo( () => ({ color: isJobTypeWithDepVar ? dependentVariable : undefined, @@ -342,6 +480,8 @@ export const ConfigurationStepForm: FC = ({ index: currentIndexPattern.title, legendType: getScatterplotMatrixLegendType(jobType), searchQuery: jobConfigQuery, + runtimeMappings, + indexPattern: currentIndexPattern, }), [ currentIndexPattern.title, @@ -388,6 +528,7 @@ export const ConfigurationStepForm: FC = ({ /> )} + {((isClone && cloneJob) || !isClone) && } @@ -476,11 +617,11 @@ export const ConfigurationStepForm: FC = ({ singleSelection={true} options={dependentVariableOptions} selectedOptions={dependentVariable ? [{ label: dependentVariable }] : []} - onChange={(selectedOptions) => + onChange={(selectedOptions) => { setFormState({ dependentVariable: selectedOptions[0].label || '', - }) - } + }); + }} isClearable={false} isInvalid={dependentVariable === ''} data-test-subj={`mlAnalyticsCreateJobWizardDependentVariableSelect${ @@ -500,35 +641,6 @@ export const ConfigurationStepForm: FC = ({ > - {Array.isArray(unsupportedRuntimeFields) && unsupportedRuntimeFields.length > 0 && ( - <> - - 0 ? ( - - ) : ( - '' - ), - unsupportedRuntimeFields: unsupportedRuntimeFields - .slice(0, maxRuntimeFieldsDisplayCount) - .join(', '), - }} - /> - - - - )} { - if (field.id === EVENT_RATE_FIELD_ID) return false; +export const shouldAddAsDepVarOption = ( + fieldId: string, + fieldType: ES_FIELD_TYPES | RuntimeType, + jobType: AnalyticsJobType +) => { + if (fieldId === EVENT_RATE_FIELD_ID) return false; - const isBasicNumerical = BASIC_NUMERICAL_TYPES.has(field.type); + const isBasicNumerical = BASIC_NUMERICAL_TYPES.has(fieldType as ES_FIELD_TYPES); const isSupportedByClassification = - isBasicNumerical || CATEGORICAL_TYPES.has(field.type) || field.type === ES_FIELD_TYPES.BOOLEAN; + isBasicNumerical || CATEGORICAL_TYPES.has(fieldType) || fieldType === ES_FIELD_TYPES.BOOLEAN; if (jobType === ANALYSIS_CONFIG_TYPE.REGRESSION) { - return isBasicNumerical || EXTENDED_NUMERICAL_TYPES.has(field.type); + return isBasicNumerical || EXTENDED_NUMERICAL_TYPES.has(fieldType as ES_FIELD_TYPES); } if (jobType === ANALYSIS_CONFIG_TYPE.CLASSIFICATION) return isSupportedByClassification; }; + +export const handleExplainErrorMessage = ( + errorMessage: string, + sourceIndex: string, + jobType: AnalyticsJobType +) => { + let maxDistinctValuesErrorMessage; + let unsupportedFieldsErrorMessage; + let toastNotificationWarning; + let toastNotificationDanger; + if ( + jobType === ANALYSIS_CONFIG_TYPE.CLASSIFICATION && + (errorMessage.includes('must have at most') || errorMessage.includes('must have at least')) + ) { + maxDistinctValuesErrorMessage = errorMessage; + } else if ( + errorMessage.includes('status_exception') && + errorMessage.includes('unsupported type') + ) { + unsupportedFieldsErrorMessage = errorMessage; + } else if ( + errorMessage.includes('status_exception') && + errorMessage.includes('Unable to estimate memory usage as no documents') + ) { + toastNotificationWarning = i18n.translate( + 'xpack.ml.dataframe.analytics.create.allDocsMissingFieldsErrorMessage', + { + defaultMessage: `Unable to estimate memory usage. There are mapped fields for source index [{index}] that do not exist in any indexed documents. You will have to switch to the JSON editor for explicit field selection and include only fields that exist in indexed documents.`, + values: { + index: sourceIndex, + }, + } + ); + } else { + toastNotificationDanger = { + title: i18n.translate('xpack.ml.dataframe.analytics.create.unableToFetchExplainDataMessage', { + defaultMessage: 'An error occurred fetching analysis fields data.', + }), + text: errorMessage, + }; + } + + return { + maxDistinctValuesErrorMessage, + unsupportedFieldsErrorMessage, + toastNotificationDanger, + toastNotificationWarning, + }; +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/runtime_mappings/index.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/runtime_mappings/index.ts new file mode 100644 index 0000000000000..8b93ddaa4a26a --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/runtime_mappings/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { RuntimeMappings } from './runtime_mappings'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/runtime_mappings/runtime_mappings.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/runtime_mappings/runtime_mappings.tsx new file mode 100644 index 0000000000000..d9f1d78c302fd --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/runtime_mappings/runtime_mappings.tsx @@ -0,0 +1,237 @@ +/* + * 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, { FC, useState, useEffect } from 'react'; +import { + EuiButton, + EuiButtonIcon, + EuiCopy, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiSpacer, + EuiSwitch, + EuiText, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { XJsonMode } from '@kbn/ace'; +import { RuntimeField } from '../../../../../../../../../../src/plugins/data/common/index_patterns'; +import { useMlContext } from '../../../../../contexts/ml'; +import { CreateAnalyticsFormProps } from '../../../analytics_management/hooks/use_create_analytics_form'; +import { XJson } from '../../../../../../../../../../src/plugins/es_ui_shared/public'; +import { getCombinedRuntimeMappings } from '../../../../../components/data_grid/common'; +import { isPopulatedObject } from '../../../../../../../common/util/object_utils'; +import { RuntimeMappingsEditor } from './runtime_mappings_editor'; + +const advancedEditorsSidebarWidth = '220px'; +const COPY_TO_CLIPBOARD_RUNTIME_MAPPINGS = i18n.translate( + 'xpack.ml.dataframe.analytics.createWizard.indexPreview.copyRuntimeMappingsClipboardTooltip', + { + defaultMessage: 'Copy Dev Console statement of the runtime mappings to the clipboard.', + } +); + +const { useXJsonMode } = XJson; +const xJsonMode = new XJsonMode(); + +interface Props { + actions: CreateAnalyticsFormProps['actions']; + state: CreateAnalyticsFormProps['state']; +} + +type RuntimeMappings = Record; + +export const RuntimeMappings: FC = ({ actions, state }) => { + const [isRuntimeMappingsEditorEnabled, setIsRuntimeMappingsEditorEnabled] = useState( + false + ); + const [ + isRuntimeMappingsEditorApplyButtonEnabled, + setIsRuntimeMappingsEditorApplyButtonEnabled, + ] = useState(false); + const [ + advancedEditorRuntimeMappingsLastApplied, + setAdvancedEditorRuntimeMappingsLastApplied, + ] = useState(); + const [advancedEditorRuntimeMappings, setAdvancedEditorRuntimeMappings] = useState(); + + const { setFormState } = actions; + const { jobType, previousRuntimeMapping, runtimeMappings } = state.form; + + const { + convertToJson, + setXJson: setAdvancedRuntimeMappingsConfig, + xJson: advancedRuntimeMappingsConfig, + } = useXJsonMode(runtimeMappings || ''); + + const mlContext = useMlContext(); + const { currentIndexPattern } = mlContext; + + const applyChanges = () => { + const removeRuntimeMappings = advancedRuntimeMappingsConfig === ''; + const parsedRuntimeMappings = removeRuntimeMappings + ? undefined + : JSON.parse(advancedRuntimeMappingsConfig); + const prettySourceConfig = removeRuntimeMappings + ? '' + : JSON.stringify(parsedRuntimeMappings, null, 2); + const previous = + previousRuntimeMapping === undefined && runtimeMappings === undefined + ? parsedRuntimeMappings + : runtimeMappings; + setFormState({ + runtimeMappings: parsedRuntimeMappings, + runtimeMappingsUpdated: true, + previousRuntimeMapping: previous, + }); + setAdvancedEditorRuntimeMappings(prettySourceConfig); + setAdvancedEditorRuntimeMappingsLastApplied(prettySourceConfig); + setIsRuntimeMappingsEditorApplyButtonEnabled(false); + }; + + // If switching to KQL after updating via editor - reset search + const toggleEditorHandler = (reset = false) => { + if (reset === true) { + setFormState({ runtimeMappingsUpdated: false }); + } + if (isRuntimeMappingsEditorEnabled === false) { + setAdvancedEditorRuntimeMappingsLastApplied(advancedEditorRuntimeMappings); + } + + setIsRuntimeMappingsEditorEnabled(!isRuntimeMappingsEditorEnabled); + setIsRuntimeMappingsEditorApplyButtonEnabled(false); + }; + + useEffect(function getInitialRuntimeMappings() { + const combinedRuntimeMappings = getCombinedRuntimeMappings( + currentIndexPattern, + runtimeMappings + ); + + if (combinedRuntimeMappings) { + setAdvancedRuntimeMappingsConfig(JSON.stringify(combinedRuntimeMappings, null, 2)); + setFormState({ + runtimeMappings: combinedRuntimeMappings, + }); + } + }, []); + + return ( + <> + + + + + {isPopulatedObject(runtimeMappings) ? ( + + {Object.keys(runtimeMappings).join(',')} + + ) : ( + + )} + + {isRuntimeMappingsEditorEnabled && ( + <> + + + + )} + + + + + + + + toggleEditorHandler()} + data-test-subj="mlDataFrameAnalyticsRuntimeMappingsEditorSwitch" + /> + + + + {(copy: () => void) => ( + + )} + + + + + + {isRuntimeMappingsEditorEnabled && ( + + + + {i18n.translate( + 'xpack.ml.dataframe.analytics.createWizard.advancedRuntimeMappingsEditorHelpText', + { + defaultMessage: + 'The advanced editor allows you to edit the runtime mappings of the source.', + } + )} + + + + {i18n.translate( + 'xpack.ml.dataframe.analytics.createWizard.advancedSourceEditorApplyButtonText', + { + defaultMessage: 'Apply changes', + } + )} + + + )} + + + + + + + ); +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/runtime_mappings/runtime_mappings_editor.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/runtime_mappings/runtime_mappings_editor.tsx new file mode 100644 index 0000000000000..70544cc14ba08 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/runtime_mappings/runtime_mappings_editor.tsx @@ -0,0 +1,82 @@ +/* + * 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 { isEqual } from 'lodash'; +import React, { memo, FC } from 'react'; +import { EuiCodeEditor } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { isRuntimeMappings } from '../../../../../../../common/util/runtime_field_utils'; + +interface Props { + convertToJson: (data: string) => string; + setAdvancedRuntimeMappingsConfig: React.Dispatch; + setIsRuntimeMappingsEditorApplyButtonEnabled: React.Dispatch>; + advancedEditorRuntimeMappingsLastApplied: string | undefined; + advancedRuntimeMappingsConfig: string; + xJsonMode: any; +} + +export const RuntimeMappingsEditor: FC = memo( + ({ + convertToJson, + xJsonMode, + setAdvancedRuntimeMappingsConfig, + setIsRuntimeMappingsEditorApplyButtonEnabled, + advancedEditorRuntimeMappingsLastApplied, + advancedRuntimeMappingsConfig, + }) => { + return ( + { + setAdvancedRuntimeMappingsConfig(d); + + // Disable the "Apply"-Button if the config hasn't changed. + if (advancedEditorRuntimeMappingsLastApplied === d) { + setIsRuntimeMappingsEditorApplyButtonEnabled(false); + return; + } + + // Enable Apply button so user can remove previously created runtime field + if (d === '') { + setIsRuntimeMappingsEditorApplyButtonEnabled(true); + return; + } + + // Try to parse the string passed on from the editor. + // If parsing fails, the "Apply"-Button will be disabled + try { + const parsedJson = JSON.parse(convertToJson(d)); + setIsRuntimeMappingsEditorApplyButtonEnabled(isRuntimeMappings(parsedJson)); + } catch (e) { + setIsRuntimeMappingsEditorApplyButtonEnabled(false); + } + }} + setOptions={{ + fontSize: '12px', + }} + theme="textmate" + aria-label={i18n.translate( + 'xpack.ml.dataframe.analytics.createWizard.runtimeMappings.advancedEditorAriaLabel', + { + defaultMessage: 'Advanced runtime editor', + } + )} + /> + ); + }, + (prevProps, nextProps) => isEqual(pickProps(prevProps), pickProps(nextProps)) +); + +function pickProps(props: Props) { + return [props.advancedEditorRuntimeMappingsLastApplied, props.advancedRuntimeMappingsConfig]; +} diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/hooks/use_index_data.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/hooks/use_index_data.ts index 4552ca34ebbae..f48f4a62f5a7d 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/hooks/use_index_data.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/hooks/use_index_data.ts @@ -5,20 +5,23 @@ * 2.0. */ -import { useEffect, useMemo } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import { estypes } from '@elastic/elasticsearch'; import { EuiDataGridColumn } from '@elastic/eui'; - import { CoreSetup } from 'src/core/public'; import { IndexPattern } from '../../../../../../../../../src/plugins/data/public'; +import { isRuntimeMappings } from '../../../../../../common/util/runtime_field_utils'; +import { RuntimeMappings, RuntimeField } from '../../../../../../common/types/fields'; +import { DEFAULT_SAMPLER_SHARD_SIZE } from '../../../../../../common/constants/field_histograms'; import { DataLoader } from '../../../../datavisualizer/index_based/data_loader'; import { getFieldType, getDataGridSchemaFromKibanaFieldType, + getDataGridSchemaFromESFieldType, getFieldsFromKibanaIndexPattern, showDataGridColumnChartErrorMessageToast, useDataGrid, @@ -26,31 +29,51 @@ import { EsSorting, UseIndexDataReturnType, getProcessedFields, + getCombinedRuntimeMappings, } from '../../../../components/data_grid'; import { extractErrorMessage } from '../../../../../../common/util/errors'; import { INDEX_STATUS } from '../../../common/analytics'; import { ml } from '../../../../services/ml_api_service'; -import { getRuntimeFieldsMapping } from '../../../../components/data_grid/common'; type IndexSearchResponse = estypes.SearchResponse; +interface MLEuiDataGridColumn extends EuiDataGridColumn { + isRuntimeFieldColumn?: boolean; +} + +function getRuntimeFieldColumns(runtimeMappings: RuntimeMappings) { + return Object.keys(runtimeMappings).map((id) => { + const field = runtimeMappings[id]; + const schema = getDataGridSchemaFromESFieldType(field.type as RuntimeField['type']); + return { id, schema, isExpandable: schema !== 'boolean', isRuntimeFieldColumn: true }; + }); +} + export const useIndexData = ( indexPattern: IndexPattern, query: Record | undefined, - toastNotifications: CoreSetup['notifications']['toasts'] + toastNotifications: CoreSetup['notifications']['toasts'], + runtimeMappings?: RuntimeMappings ): UseIndexDataReturnType => { const indexPatternFields = useMemo(() => getFieldsFromKibanaIndexPattern(indexPattern), [ indexPattern, ]); - // EuiDataGrid State - const columns: EuiDataGridColumn[] = [ + const [columns, setColumns] = useState([ ...indexPatternFields.map((id) => { const field = indexPattern.fields.getByName(id); - const schema = getDataGridSchemaFromKibanaFieldType(field); - return { id, schema, isExpandable: schema !== 'boolean' }; + const isRuntimeFieldColumn = field?.runtimeField !== undefined; + const schema = isRuntimeFieldColumn + ? getDataGridSchemaFromESFieldType(field?.type as RuntimeField['type']) + : getDataGridSchemaFromKibanaFieldType(field); + return { + id, + schema, + isExpandable: schema !== 'boolean', + isRuntimeFieldColumn, + }; }), - ]; + ]); const dataGrid = useDataGrid(columns); @@ -75,6 +98,8 @@ export const useIndexData = ( setErrorMessage(''); setStatus(INDEX_STATUS.LOADING); + const combinedRuntimeMappings = getCombinedRuntimeMappings(indexPattern, runtimeMappings); + const sort: EsSorting = sortingColumns.reduce((s, column) => { s[column.id] = { order: column.direction }; return s; @@ -88,14 +113,37 @@ export const useIndexData = ( fields: ['*'], _source: false, ...(Object.keys(sort).length > 0 ? { sort } : {}), - ...getRuntimeFieldsMapping(indexPatternFields, indexPattern), + ...(isRuntimeMappings(combinedRuntimeMappings) + ? { runtime_mappings: combinedRuntimeMappings } + : {}), }, }; try { const resp: IndexSearchResponse = await ml.esSearch(esSearchRequest); - const docs = resp.hits.hits.map((d) => getProcessedFields(d.fields ?? {})); + + if (isRuntimeMappings(runtimeMappings)) { + // remove old runtime field from columns + const updatedColumns = columns.filter((col) => col.isRuntimeFieldColumn === false); + setColumns([ + ...updatedColumns, + ...(combinedRuntimeMappings ? getRuntimeFieldColumns(combinedRuntimeMappings) : []), + ]); + } else { + setColumns([ + ...indexPatternFields.map((id) => { + const field = indexPattern.fields.getByName(id); + const schema = getDataGridSchemaFromKibanaFieldType(field); + return { + id, + schema, + isExpandable: schema !== 'boolean', + isRuntimeFieldColumn: field?.runtimeField !== undefined, + }; + }), + ]); + } setRowCount(typeof resp.hits.total === 'number' ? resp.hits.total : resp.hits.total.value); setRowCountRelation( typeof resp.hits.total === 'number' @@ -115,13 +163,18 @@ export const useIndexData = ( getIndexData(); } // custom comparison - }, [indexPattern.title, indexPatternFields, JSON.stringify([query, pagination, sortingColumns])]); + }, [ + indexPattern.title, + indexPatternFields, + JSON.stringify([query, pagination, sortingColumns, runtimeMappings]), + ]); const dataLoader = useMemo(() => new DataLoader(indexPattern, toastNotifications), [ indexPattern, ]); const fetchColumnChartsData = async function (fieldHistogramsQuery: Record) { + const combinedRuntimeMappings = getCombinedRuntimeMappings(indexPattern, runtimeMappings); try { const columnChartsData = await dataLoader.loadFieldHistograms( columns @@ -130,7 +183,9 @@ export const useIndexData = ( fieldName: cT.id, type: getFieldType(cT.schema), })), - fieldHistogramsQuery + fieldHistogramsQuery, + DEFAULT_SAMPLER_SHARD_SIZE, + combinedRuntimeMappings ); dataGrid.setColumnCharts(columnChartsData); } catch (e) { @@ -146,7 +201,7 @@ export const useIndexData = ( }, [ dataGrid.chartsVisible, indexPattern.title, - JSON.stringify([query, dataGrid.visibleColumns]), + JSON.stringify([query, dataGrid.visibleColumns, runtimeMappings]), ]); const renderCellValue = useRenderCellValue(indexPattern, pagination, tableItems); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/page.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/page.tsx index 8fd0ae86d240c..830870cf1ca74 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/page.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/page.tsx @@ -104,6 +104,7 @@ export const Page: FC = ({ jobId }) => { children: ( ({ requiredFieldsError: undefined, randomizeSeed: undefined, resultsField: undefined, + runtimeMappings: undefined, + runtimeMappingsUpdated: false, + previousRuntimeMapping: undefined, softTreeDepthLimit: undefined, softTreeDepthTolerance: undefined, sourceIndex: '', @@ -212,6 +220,9 @@ export const getJobConfigFromFormState = ( ? formState.sourceIndex.split(',').map((d) => d.trim()) : formState.sourceIndex, query: formState.jobConfigQuery, + ...(isRuntimeMappings(formState.runtimeMappings) + ? { runtime_mappings: formState.runtimeMappings } + : {}), }, dest: { index: formState.destinationIndex, @@ -340,6 +351,7 @@ export function getFormStateFromJobConfig( sourceIndex: Array.isArray(analyticsJobConfig.source.index) ? analyticsJobConfig.source.index.join(',') : analyticsJobConfig.source.index, + runtimeMappings: analyticsJobConfig.source.runtime_mappings, modelMemoryLimit: analyticsJobConfig.model_memory_limit, maxNumThreads: analyticsJobConfig.max_num_threads, includes: analyticsJobConfig.analyzed_fields?.includes ?? [], diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/data_loader/data_loader.ts b/x-pack/plugins/ml/public/application/datavisualizer/index_based/data_loader/data_loader.ts index 38b9aa2ce29f2..0da7d3d6b63d8 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/index_based/data_loader/data_loader.ts +++ b/x-pack/plugins/ml/public/application/datavisualizer/index_based/data_loader/data_loader.ts @@ -110,14 +110,15 @@ export class DataLoader { async loadFieldHistograms( fields: FieldHistogramRequestConfig[], query: string | SavedSearchQuery, - samplerShardSize = DEFAULT_SAMPLER_SHARD_SIZE + samplerShardSize = DEFAULT_SAMPLER_SHARD_SIZE, + editorRuntimeMappings?: RuntimeMappings ): Promise { const stats = await ml.getVisualizerFieldHistograms({ indexPatternTitle: this._indexPatternTitle, query, fields, samplerShardSize, - runtimeMappings: this._runtimeMappings, + runtimeMappings: editorRuntimeMappings || this._runtimeMappings, }); return stats; diff --git a/x-pack/plugins/ml/server/routes/schemas/data_analytics_schema.ts b/x-pack/plugins/ml/server/routes/schemas/data_analytics_schema.ts index 70ffecd11c96c..1f5bcbc23423a 100644 --- a/x-pack/plugins/ml/server/routes/schemas/data_analytics_schema.ts +++ b/x-pack/plugins/ml/server/routes/schemas/data_analytics_schema.ts @@ -6,6 +6,7 @@ */ import { schema } from '@kbn/config-schema'; +import { runtimeMappingsSchema } from './runtime_mappings_schema'; export const dataAnalyticsJobConfigSchema = schema.object({ description: schema.maybe(schema.string()), @@ -16,6 +17,7 @@ export const dataAnalyticsJobConfigSchema = schema.object({ source: schema.object({ index: schema.oneOf([schema.string(), schema.arrayOf(schema.string())]), query: schema.maybe(schema.any()), + runtime_mappings: runtimeMappingsSchema, _source: schema.maybe( schema.object({ /** Fields to include in results */ @@ -51,6 +53,7 @@ export const dataAnalyticsExplainSchema = schema.object({ source: schema.object({ index: schema.oneOf([schema.string(), schema.arrayOf(schema.string())]), query: schema.maybe(schema.any()), + runtime_mappings: runtimeMappingsSchema, }), analysis: schema.any(), analyzed_fields: schema.maybe(schema.any()), diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 9602a324e5d51..133b4d0b6aaa8 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -13329,7 +13329,6 @@ "xpack.ml.dataframe.analytics.create.etaInputAriaLabel": "縮小が重みに適用されました。", "xpack.ml.dataframe.analytics.create.etaLabel": "Eta", "xpack.ml.dataframe.analytics.create.etaText": "縮小が重みに適用されました。0.001から1の範囲でなければなりません。", - "xpack.ml.dataframe.analytics.create.extraUnsupportedRuntimeFieldsMsg": "{count}以上", "xpack.ml.dataframe.analytics.create.featureBagFractionInputAriaLabel": "各候補分割のランダムなbagを選択したときに使用される特徴量の割合", "xpack.ml.dataframe.analytics.create.featureBagFractionLabel": "特徴量bag割合", "xpack.ml.dataframe.analytics.create.featureBagFractionText": "各候補分割のランダムなbagを選択したときに使用される特徴量の割合。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 12a3c8925cfc6..0f9d8b90a2578 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -13500,7 +13500,6 @@ "xpack.ml.dataframe.analytics.create.etaInputAriaLabel": "缩小量已应用于权重。", "xpack.ml.dataframe.analytics.create.etaLabel": "Eta", "xpack.ml.dataframe.analytics.create.etaText": "缩小量已应用于权重。必须介于 0.001 和 1 之间。", - "xpack.ml.dataframe.analytics.create.extraUnsupportedRuntimeFieldsMsg": "及另外 {count} 个", "xpack.ml.dataframe.analytics.create.featureBagFractionInputAriaLabel": "选择为每个候选拆分选择随机袋时使用的特征比例", "xpack.ml.dataframe.analytics.create.featureBagFractionLabel": "特征袋比例", "xpack.ml.dataframe.analytics.create.featureBagFractionText": "选择为每个候选拆分选择随机袋时使用的特征比例。", @@ -13604,7 +13603,6 @@ "xpack.ml.dataframe.analytics.create.trainingPercentLabel": "训练百分比", "xpack.ml.dataframe.analytics.create.unableToFetchExplainDataMessage": "提取分析字段数据时发生错误。", "xpack.ml.dataframe.analytics.create.unsupportedFieldsError": "无效。{message}", - "xpack.ml.dataframe.analytics.create.unsupportedRuntimeFieldsCallout": "不支持分析运行时{runtimeFieldsCount, plural, other {字段}} {unsupportedRuntimeFields} {extraCountMsg}。", "xpack.ml.dataframe.analytics.create.useEstimatedMmlLabel": "使用估计的模型内存限制", "xpack.ml.dataframe.analytics.create.UseResultsFieldDefaultLabel": "使用结果字段默认值“{defaultValue}”", "xpack.ml.dataframe.analytics.create.viewResultsCardDescription": "查看分析作业的结果。", From 59100b562686e7ac5fb3243c09ee6c8ba6a6e83c Mon Sep 17 00:00:00 2001 From: Lukas Olson Date: Fri, 2 Apr 2021 08:13:14 -0700 Subject: [PATCH 07/30] Fix autocomplete telemetry (#95724) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../ui/query_string_input/query_string_input.tsx | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/plugins/data/public/ui/query_string_input/query_string_input.tsx b/src/plugins/data/public/ui/query_string_input/query_string_input.tsx index 71ff09e81c567..16e1325b2b56b 100644 --- a/src/plugins/data/public/ui/query_string_input/query_string_input.tsx +++ b/src/plugins/data/public/ui/query_string_input/query_string_input.tsx @@ -379,14 +379,12 @@ export default class QueryStringInputUI extends Component { const newQueryString = value.substr(0, start) + text + value.substr(end); this.reportUiCounter?.( - METRIC_TYPE.LOADED, - `query_string:${type}:suggestions_select_position`, - listIndex + METRIC_TYPE.CLICK, + `query_string:${type}:suggestions_select_position_${listIndex}` ); this.reportUiCounter?.( - METRIC_TYPE.LOADED, - `query_string:${type}:suggestions_select_q_length`, - end - start + METRIC_TYPE.CLICK, + `query_string:${type}:suggestions_select_q_length_${end - start}` ); this.onQueryStringChange(newQueryString); From fb681d906241789f54889a6a3920108cde9400d7 Mon Sep 17 00:00:00 2001 From: Scotty Bollinger Date: Fri, 2 Apr 2021 11:41:27 -0500 Subject: [PATCH 08/30] [Workplace Search] Fix broken reauthenticate URLs and fix spelling (#96140) * Remove kebabCase Kibana routes are snake case, which matches the existing serviceType, so this is no longer needed * Fix route segment The word reauthenticate is not hyphenated * Fix all misspelling of reauthenticate Renames files too --- .../components/shared/source_row/source_row.tsx | 6 +----- .../components/add_source/add_source.test.tsx | 8 ++++---- .../content_sources/components/add_source/add_source.tsx | 6 +++--- .../components/add_source/add_source_logic.test.ts | 2 +- .../components/add_source/add_source_logic.ts | 4 ++-- .../{re_authenticate.test.tsx => reauthenticate.test.tsx} | 8 ++++---- .../{re_authenticate.tsx => reauthenticate.tsx} | 8 ++++---- .../views/content_sources/sources_router.tsx | 2 +- 8 files changed, 20 insertions(+), 24 deletions(-) rename x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/{re_authenticate.test.tsx => reauthenticate.test.tsx} (87%) rename x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/{re_authenticate.tsx => reauthenticate.tsx} (91%) diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_row/source_row.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_row/source_row.tsx index 5d15196fba5a6..f9679bd42c07d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_row/source_row.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_row/source_row.tsx @@ -7,10 +7,6 @@ import React from 'react'; -// Prefer importing entire lodash library, e.g. import { get } from "lodash" -// eslint-disable-next-line no-restricted-imports -import _kebabCase from 'lodash/kebabCase'; - import { EuiFlexGroup, EuiFlexItem, @@ -72,7 +68,7 @@ export const SourceRow: React.FC = ({ const fixLink = ( diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.test.tsx index 41f53523bca4e..0ee872f7cfe8a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.test.tsx @@ -22,7 +22,7 @@ import { ConfigurationIntro } from './configuration_intro'; import { ConfigureCustom } from './configure_custom'; import { ConfigureOauth } from './configure_oauth'; import { ConnectInstance } from './connect_instance'; -import { ReAuthenticate } from './re_authenticate'; +import { Reauthenticate } from './reauthenticate'; import { SaveConfig } from './save_config'; import { SaveCustom } from './save_custom'; @@ -142,13 +142,13 @@ describe('AddSourceList', () => { expect(wrapper.find(SaveCustom)).toHaveLength(1); }); - it('renders ReAuthenticate step', () => { + it('renders Reauthenticate step', () => { setMockValues({ ...mockValues, - addSourceCurrentStep: AddSourceSteps.ReAuthenticateStep, + addSourceCurrentStep: AddSourceSteps.ReauthenticateStep, }); const wrapper = shallow(); - expect(wrapper.find(ReAuthenticate)).toHaveLength(1); + expect(wrapper.find(Reauthenticate)).toHaveLength(1); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.tsx index 30f5009ac0b3c..8186c43efef49 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.tsx @@ -27,7 +27,7 @@ import { ConfigurationIntro } from './configuration_intro'; import { ConfigureCustom } from './configure_custom'; import { ConfigureOauth } from './configure_oauth'; import { ConnectInstance } from './connect_instance'; -import { ReAuthenticate } from './re_authenticate'; +import { Reauthenticate } from './reauthenticate'; import { SaveConfig } from './save_config'; import { SaveCustom } from './save_custom'; @@ -150,8 +150,8 @@ export const AddSource: React.FC = (props) => { header={header} /> )} - {addSourceCurrentStep === AddSourceSteps.ReAuthenticateStep && ( - + {addSourceCurrentStep === AddSourceSteps.ReauthenticateStep && ( + )} ); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_logic.test.ts index 8ced90e7d7729..b52b354a6b115 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_logic.test.ts @@ -276,7 +276,7 @@ describe('AddSourceLogic', () => { const addSourceProps = { sourceIndex: 1, reAuthenticate: true }; AddSourceLogic.actions.initializeAddSource(addSourceProps); - expect(setAddSourceStepSpy).toHaveBeenCalledWith(AddSourceSteps.ReAuthenticateStep); + expect(setAddSourceStepSpy).toHaveBeenCalledWith(AddSourceSteps.ReauthenticateStep); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_logic.ts index 6ca7f6fa72e24..0bd37aed81c32 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_logic.ts @@ -42,7 +42,7 @@ export enum AddSourceSteps { ConfigureCustomStep = 'Configure Custom', ConfigureOauthStep = 'Configure Oauth', SaveCustomStep = 'Save Custom', - ReAuthenticateStep = 'ReAuthenticate', + ReauthenticateStep = 'Reauthenticate', } export interface OauthParams { @@ -577,6 +577,6 @@ const getFirstStep = (props: AddSourceProps): AddSourceSteps => { if (isCustom) return AddSourceSteps.ConfigureCustomStep; if (connect) return AddSourceSteps.ConnectInstanceStep; if (configure) return AddSourceSteps.ConfigureOauthStep; - if (reAuthenticate) return AddSourceSteps.ReAuthenticateStep; + if (reAuthenticate) return AddSourceSteps.ReauthenticateStep; return AddSourceSteps.ConfigIntroStep; }; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/re_authenticate.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/reauthenticate.test.tsx similarity index 87% rename from x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/re_authenticate.test.tsx rename to x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/reauthenticate.test.tsx index 38b6925008181..c38ab167b18de 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/re_authenticate.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/reauthenticate.test.tsx @@ -12,9 +12,9 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { ReAuthenticate } from './re_authenticate'; +import { Reauthenticate } from './reauthenticate'; -describe('ReAuthenticate', () => { +describe('Reauthenticate', () => { // Needed to mock redirect window.location.replace(oauthUrl) const mockReplace = jest.fn(); const mockWindow = { @@ -44,14 +44,14 @@ describe('ReAuthenticate', () => { }); it('renders', () => { - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper.find('form')).toHaveLength(1); }); it('handles form submission', () => { jest.spyOn(window.location, 'replace').mockImplementationOnce(mockReplace); - const wrapper = shallow(); + const wrapper = shallow(); const preventDefault = jest.fn(); wrapper.find('form').simulate('submit', { preventDefault }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/re_authenticate.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/reauthenticate.tsx similarity index 91% rename from x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/re_authenticate.tsx rename to x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/reauthenticate.tsx index f57118b952eac..fa604ef758a44 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/re_authenticate.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/reauthenticate.tsx @@ -22,12 +22,12 @@ interface SourceQueryParams { sourceId: string; } -interface ReAuthenticateProps { +interface ReauthenticateProps { name: string; header: React.ReactNode; } -export const ReAuthenticate: React.FC = ({ name, header }) => { +export const Reauthenticate: React.FC = ({ name, header }) => { const { search } = useLocation() as Location; const { sourceId } = (parseQueryParams(search) as unknown) as SourceQueryParams; @@ -66,7 +66,7 @@ export const ReAuthenticate: React.FC = ({ name, header }) 'xpack.enterpriseSearch.workplaceSearch.contentSource.reAuthenticate.body', { defaultMessage: - 'Your {name} credentials are no longer valid. Please re-authenticate with the original credentials to resume content syncing.', + 'Your {name} credentials are no longer valid. Please reauthenticate with the original credentials to resume content syncing.', values: { name }, } )} @@ -79,7 +79,7 @@ export const ReAuthenticate: React.FC = ({ name, header }) {i18n.translate( 'xpack.enterpriseSearch.workplaceSearch.contentSource.reAuthenticate.button', { - defaultMessage: 'Re-authenticate {name}', + defaultMessage: 'Reauthenticate {name}', values: { name }, } )} diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_router.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_router.tsx index f4a56c8a0beaa..84bff65e62cef 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_router.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_router.tsx @@ -82,7 +82,7 @@ export const SourcesRouter: React.FC = () => { ))} {staticSourceData.map(({ addPath, name }, i) => ( - + From 1d5826655973be81a313c50c2f3784a5b8dffc9a Mon Sep 17 00:00:00 2001 From: Constance Date: Fri, 2 Apr 2021 09:50:39 -0700 Subject: [PATCH 09/30] [Enterprise Search] Add toasts support to FlashMessages component (#95981) * Add toasts to FlashMessagesLogic + Tests cleanup: - Group actions by their reducer blocks (since flashMessages has such specific logic) - recommend viewing with whitespace changes off for this - Do not reset context between each test, but instead by mount(), which allows tests to maintain state between adding/removing/resetting - Remove '()' from test names (feedback from previous PRs) * Add toast message helpers + refactor FLASH_MESSAGE_TYPES to constants, so that both callouts & toasts can use it effectively * Update FlashMessages to display toasts as well as callouts - This means we can automatically use toasts alongside callouts in all views that already have FlashMessages + a11y enhancement! update callouts to also announce new messages to screenreaders * [Example] Update ApiLogsLogic to flash an error toast on poll + update copy to better match EUI guidelines (shorter) * Fix test caused by new FlashMessages structure * PR suggestion - destructure Co-authored-by: Scotty Bollinger * PR feedback: implicit return * Fix color types - adding our own string enum fixes the typescript errors that both EuiCallout & EuiToast emit when passing color props to the base EUI types * PR feedback: Update flashToast API to match callout helper API - accepts a string title with optional args, creates a unique ID automatically if missing Co-authored-by: Scotty Bollinger --- .../__mocks__/flash_messages_logic.mock.ts | 2 + .../api_logs/api_logs_logic.test.ts | 9 +- .../components/api_logs/api_logs_logic.ts | 15 ++- .../components/api_logs/constants.ts | 11 +- .../shared/flash_messages/constants.ts | 19 +++ .../flash_messages/flash_messages.test.tsx | 106 +++++++++------ .../shared/flash_messages/flash_messages.tsx | 39 ++++-- .../flash_messages_logic.test.ts | 121 ++++++++++++------ .../flash_messages/flash_messages_logic.ts | 18 +++ .../shared/flash_messages/index.ts | 2 + .../set_message_helpers.test.ts | 80 ++++++++++++ .../flash_messages/set_message_helpers.ts | 20 +++ .../shared/flash_messages/types.ts | 14 +- .../views/groups/groups.test.tsx | 2 +- 14 files changed, 355 insertions(+), 103 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/constants.ts diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/flash_messages_logic.mock.ts b/x-pack/plugins/enterprise_search/public/applications/__mocks__/flash_messages_logic.mock.ts index ac2f4ba50d7f9..17e22e6f23daf 100644 --- a/x-pack/plugins/enterprise_search/public/applications/__mocks__/flash_messages_logic.mock.ts +++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/flash_messages_logic.mock.ts @@ -24,6 +24,8 @@ export const mockFlashMessageHelpers = { setQueuedSuccessMessage: jest.fn(), setQueuedErrorMessage: jest.fn(), clearFlashMessages: jest.fn(), + flashSuccessToast: jest.fn(), + flashErrorToast: jest.fn(), }; jest.mock('../shared/flash_messages', () => ({ diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_logs_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_logs_logic.test.ts index e7f3124a48e8c..7b3ee80668ac7 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_logs_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_logs_logic.test.ts @@ -12,14 +12,12 @@ import { nextTick } from '@kbn/test/jest'; import { DEFAULT_META } from '../../../shared/constants'; -import { POLLING_ERROR_MESSAGE } from './constants'; - import { ApiLogsLogic } from './'; describe('ApiLogsLogic', () => { const { mount, unmount } = new LogicMounter(ApiLogsLogic); const { http } = mockHttpValues; - const { flashAPIErrors, setErrorMessage } = mockFlashMessageHelpers; + const { flashAPIErrors, flashErrorToast } = mockFlashMessageHelpers; const DEFAULT_VALUES = { dataLoading: true, @@ -213,7 +211,10 @@ describe('ApiLogsLogic', () => { ApiLogsLogic.actions.fetchApiLogs({ isPoll: true }); await nextTick(); - expect(setErrorMessage).toHaveBeenCalledWith(POLLING_ERROR_MESSAGE); + expect(flashErrorToast).toHaveBeenCalledWith('Could not refresh API log data', { + text: expect.stringContaining('Please check your connection'), + toastLifeTimeMs: 3750, + }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_logs_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_logs_logic.ts index 2a2f55a0c8033..4d0d80cf2c24b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_logs_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_logs_logic.ts @@ -8,12 +8,12 @@ import { kea, MakeLogicType } from 'kea'; import { DEFAULT_META } from '../../../shared/constants'; -import { flashAPIErrors, setErrorMessage } from '../../../shared/flash_messages'; +import { flashAPIErrors, flashErrorToast } from '../../../shared/flash_messages'; import { HttpLogic } from '../../../shared/http'; import { updateMetaPageIndex } from '../../../shared/table_pagination'; import { EngineLogic } from '../engine'; -import { POLLING_DURATION, POLLING_ERROR_MESSAGE } from './constants'; +import { POLLING_DURATION, POLLING_ERROR_TITLE, POLLING_ERROR_TEXT } from './constants'; import { ApiLogsData, ApiLog } from './types'; import { getDateString } from './utils'; @@ -117,14 +117,21 @@ export const ApiLogsLogic = kea>({ // while polls are stored in-state until the user manually triggers the 'Refresh' action if (isPoll) { actions.onPollInterval(response); + flashErrorToast(POLLING_ERROR_TITLE, { + text: POLLING_ERROR_TEXT, + toastLifeTimeMs: POLLING_DURATION * 0.75, + }); } else { actions.updateView(response); } } catch (e) { if (isPoll) { - // If polling fails, it will typically be due due to http connection - + // If polling fails, it will typically be due to http connection - // we should send a more human-readable message if so - setErrorMessage(POLLING_ERROR_MESSAGE); + flashErrorToast(POLLING_ERROR_TITLE, { + text: POLLING_ERROR_TEXT, + toastLifeTimeMs: POLLING_DURATION * 0.75, + }); } else { flashAPIErrors(e); } diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/constants.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/constants.ts index 9f64ec44e8b13..ac1fbff150723 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/constants.ts @@ -19,10 +19,11 @@ export const RECENT_API_EVENTS = i18n.translate( export const POLLING_DURATION = 5000; -export const POLLING_ERROR_MESSAGE = i18n.translate( +export const POLLING_ERROR_TITLE = i18n.translate( 'xpack.enterpriseSearch.appSearch.engine.apiLogs.pollingErrorMessage', - { - defaultMessage: - 'Could not automatically refresh API logs data. Please check your connection or manually refresh the page.', - } + { defaultMessage: 'Could not refresh API log data' } +); +export const POLLING_ERROR_TEXT = i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.apiLogs.pollingErrorDescription', + { defaultMessage: 'Please check your connection or manually reload the page.' } ); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/constants.ts b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/constants.ts new file mode 100644 index 0000000000000..35e1942bdc3de --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/constants.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 { FlashMessageColors } from './types'; + +export const FLASH_MESSAGE_TYPES = { + success: { color: 'success' as FlashMessageColors, iconType: 'check' }, + info: { color: 'primary' as FlashMessageColors, iconType: 'iInCircle' }, + warning: { color: 'warning' as FlashMessageColors, iconType: 'alert' }, + error: { color: 'danger' as FlashMessageColors, iconType: 'alert' }, +}; + +// This is the default amount of time (5 seconds) a toast will last before disappearing +// It can be overridden per-toast by passing the `toastLifetimeMs` property - @see types.ts +export const DEFAULT_TOAST_TIMEOUT = 5000; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages.test.tsx index aa45ce58af86a..289dcc0137cb8 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages.test.tsx @@ -5,62 +5,96 @@ * 2.0. */ -import { setMockValues } from '../../__mocks__/kea.mock'; +import { setMockValues, setMockActions } from '../../__mocks__/kea.mock'; import React from 'react'; import { shallow } from 'enzyme'; -import { EuiCallOut } from '@elastic/eui'; +import { EuiCallOut, EuiGlobalToastList } from '@elastic/eui'; -import { FlashMessages } from './flash_messages'; +import { FlashMessages, Callouts, Toasts } from './flash_messages'; describe('FlashMessages', () => { beforeEach(() => { jest.clearAllMocks(); }); - it('does not render if no messages exist', () => { - setMockValues({ messages: [] }); - + it('renders callout and toast flash messages', () => { const wrapper = shallow(); - - expect(wrapper.isEmptyRender()).toBe(true); + expect(wrapper.find(Callouts)).toHaveLength(1); + expect(wrapper.find(Toasts)).toHaveLength(1); }); - it('renders an array of flash messages & types', () => { - const mockMessages = [ - { type: 'success', message: 'Hello world!!' }, - { - type: 'error', - message: 'Whoa nelly!', - description:
Something went wrong
, - }, - { type: 'info', message: 'Everything is fine, nothing is ruined' }, - { type: 'warning', message: 'Uh oh' }, - { type: 'info', message: 'Testing multiples of same type' }, - ]; - setMockValues({ messages: mockMessages }); + describe('callouts', () => { + it('renders an array of flash messages & types', () => { + const mockMessages = [ + { type: 'success', message: 'Hello world!!' }, + { + type: 'error', + message: 'Whoa nelly!', + description:
Something went wrong
, + }, + { type: 'info', message: 'Everything is fine, nothing is ruined' }, + { type: 'warning', message: 'Uh oh' }, + { type: 'info', message: 'Testing multiples of same type' }, + ]; + setMockValues({ messages: mockMessages }); - const wrapper = shallow(); + const wrapper = shallow(); + + expect(wrapper.find(EuiCallOut)).toHaveLength(5); + expect(wrapper.find(EuiCallOut).first().prop('color')).toEqual('success'); + expect(wrapper.find('[data-test-subj="error"]')).toHaveLength(1); + expect(wrapper.find(EuiCallOut).last().prop('iconType')).toEqual('iInCircle'); + }); - expect(wrapper.find(EuiCallOut)).toHaveLength(5); - expect(wrapper.find(EuiCallOut).first().prop('color')).toEqual('success'); - expect(wrapper.find('[data-test-subj="error"]')).toHaveLength(1); - expect(wrapper.find(EuiCallOut).last().prop('iconType')).toEqual('iInCircle'); + it('renders any children', () => { + setMockValues({ messages: [{ type: 'success' }] }); + + const wrapper = shallow( + + + + ); + + expect(wrapper.find('[data-test-subj="testing"]').text()).toContain('Some action'); + }); }); - it('renders any children', () => { - setMockValues({ messages: [{ type: 'success' }] }); + describe('toasts', () => { + const actions = { dismissToastMessage: jest.fn() }; + beforeAll(() => setMockActions(actions)); + + it('renders an EUI toast list', () => { + const mockToasts = [ + { id: 'test', title: 'Hello world!!' }, + { + color: 'success', + iconType: 'check', + title: 'Success!', + toastLifeTimeMs: 500, + id: 'successToastId', + }, + { + color: 'danger', + iconType: 'alert', + title: 'Oh no!', + text:
Something went wrong
, + id: 'errorToastId', + }, + ]; + setMockValues({ toastMessages: mockToasts }); - const wrapper = shallow( - - - - ); + const wrapper = shallow(); + const euiToastList = wrapper.find(EuiGlobalToastList); - expect(wrapper.find('[data-test-subj="testing"]').text()).toContain('Some action'); + expect(euiToastList).toHaveLength(1); + expect(euiToastList.prop('toasts')).toEqual(mockToasts); + expect(euiToastList.prop('dismissToast')).toEqual(actions.dismissToastMessage); + expect(euiToastList.prop('toastLifeTimeMs')).toEqual(5000); + }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages.tsx index ef1a4a2d0be86..10f80d9a6345a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages.tsx @@ -7,32 +7,30 @@ import React, { Fragment } from 'react'; -import { useValues } from 'kea'; +import { useValues, useActions } from 'kea'; -import { EuiCallOut, EuiCallOutProps, EuiSpacer } from '@elastic/eui'; +import { EuiCallOut, EuiSpacer, EuiGlobalToastList } from '@elastic/eui'; +import { FLASH_MESSAGE_TYPES, DEFAULT_TOAST_TIMEOUT } from './constants'; import { FlashMessagesLogic } from './flash_messages_logic'; -const FLASH_MESSAGE_TYPES = { - success: { color: 'success' as EuiCallOutProps['color'], icon: 'check' }, - info: { color: 'primary' as EuiCallOutProps['color'], icon: 'iInCircle' }, - warning: { color: 'warning' as EuiCallOutProps['color'], icon: 'alert' }, - error: { color: 'danger' as EuiCallOutProps['color'], icon: 'alert' }, -}; +export const FlashMessages: React.FC = ({ children }) => ( + <> + {children} + + +); -export const FlashMessages: React.FC = ({ children }) => { +export const Callouts: React.FC = ({ children }) => { const { messages } = useValues(FlashMessagesLogic); - // If we have no messages to display, do not render the element at all - if (!messages.length) return null; - return ( -
+
{messages.map(({ type, message, description }, index) => ( {description} @@ -44,3 +42,16 @@ export const FlashMessages: React.FC = ({ children }) => {
); }; + +export const Toasts: React.FC = () => { + const { toastMessages } = useValues(FlashMessagesLogic); + const { dismissToastMessage } = useActions(FlashMessagesLogic); + + return ( + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_logic.test.ts index 7fc78c99fb242..c7dc658dada74 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_logic.test.ts @@ -15,11 +15,13 @@ import { FlashMessagesLogic, mountFlashMessagesLogic } from './flash_messages_lo import { IFlashMessage } from './types'; describe('FlashMessagesLogic', () => { - const mount = () => mountFlashMessagesLogic(); + const mount = () => { + resetContext({}); + return mountFlashMessagesLogic(); + }; beforeEach(() => { jest.clearAllMocks(); - resetContext({}); }); it('has default values', () => { @@ -27,67 +29,112 @@ describe('FlashMessagesLogic', () => { expect(FlashMessagesLogic.values).toEqual({ messages: [], queuedMessages: [], + toastMessages: [], historyListener: expect.any(Function), }); }); - describe('setFlashMessages()', () => { - it('sets an array of messages', () => { - const messages: IFlashMessage[] = [ - { type: 'success', message: 'Hello world!!' }, - { type: 'error', message: 'Whoa nelly!', description: 'Uh oh' }, - { type: 'info', message: 'Everything is fine, nothing is ruined' }, - ]; - + describe('messages', () => { + beforeAll(() => { mount(); - FlashMessagesLogic.actions.setFlashMessages(messages); - - expect(FlashMessagesLogic.values.messages).toEqual(messages); }); - it('automatically converts to an array if a single message obj is passed in', () => { - const message = { type: 'success', message: 'I turn into an array!' } as IFlashMessage; + describe('setFlashMessages', () => { + it('sets an array of messages', () => { + const messages: IFlashMessage[] = [ + { type: 'success', message: 'Hello world!!' }, + { type: 'error', message: 'Whoa nelly!', description: 'Uh oh' }, + { type: 'info', message: 'Everything is fine, nothing is ruined' }, + ]; - mount(); - FlashMessagesLogic.actions.setFlashMessages(message); + FlashMessagesLogic.actions.setFlashMessages(messages); + + expect(FlashMessagesLogic.values.messages).toEqual(messages); + }); + + it('automatically converts to an array if a single message obj is passed in', () => { + const message = { type: 'success', message: 'I turn into an array!' } as IFlashMessage; + + FlashMessagesLogic.actions.setFlashMessages(message); - expect(FlashMessagesLogic.values.messages).toEqual([message]); + expect(FlashMessagesLogic.values.messages).toEqual([message]); + }); }); - }); - describe('clearFlashMessages()', () => { - it('sets messages back to an empty array', () => { - mount(); - FlashMessagesLogic.actions.setFlashMessages('test' as any); - FlashMessagesLogic.actions.clearFlashMessages(); + describe('clearFlashMessages', () => { + it('resets messages back to an empty array', () => { + FlashMessagesLogic.actions.clearFlashMessages(); - expect(FlashMessagesLogic.values.messages).toEqual([]); + expect(FlashMessagesLogic.values.messages).toEqual([]); + }); }); }); - describe('setQueuedMessages()', () => { - it('sets an array of messages', () => { - const queuedMessage: IFlashMessage = { type: 'error', message: 'You deleted a thing' }; - + describe('queuedMessages', () => { + beforeAll(() => { mount(); - FlashMessagesLogic.actions.setQueuedMessages(queuedMessage); + }); + + describe('setQueuedMessages', () => { + it('sets an array of messages', () => { + const queuedMessage: IFlashMessage = { type: 'error', message: 'You deleted a thing' }; - expect(FlashMessagesLogic.values.queuedMessages).toEqual([queuedMessage]); + FlashMessagesLogic.actions.setQueuedMessages(queuedMessage); + + expect(FlashMessagesLogic.values.queuedMessages).toEqual([queuedMessage]); + }); + }); + + describe('clearQueuedMessages', () => { + it('resets queued messages back to an empty array', () => { + FlashMessagesLogic.actions.clearQueuedMessages(); + + expect(FlashMessagesLogic.values.queuedMessages).toEqual([]); + }); }); }); - describe('clearQueuedMessages()', () => { - it('sets queued messages back to an empty array', () => { + describe('toastMessages', () => { + beforeAll(() => { mount(); - FlashMessagesLogic.actions.setQueuedMessages('test' as any); - FlashMessagesLogic.actions.clearQueuedMessages(); + }); - expect(FlashMessagesLogic.values.queuedMessages).toEqual([]); + describe('addToastMessage', () => { + it('appends a toast message to the current toasts array', () => { + FlashMessagesLogic.actions.addToastMessage({ id: 'hello' }); + FlashMessagesLogic.actions.addToastMessage({ id: 'world' }); + FlashMessagesLogic.actions.addToastMessage({ id: 'lorem ipsum' }); + + expect(FlashMessagesLogic.values.toastMessages).toEqual([ + { id: 'hello' }, + { id: 'world' }, + { id: 'lorem ipsum' }, + ]); + }); + }); + + describe('dismissToastMessage', () => { + it('removes a specific toast ID from the current toasts array', () => { + FlashMessagesLogic.actions.dismissToastMessage({ id: 'world' }); + + expect(FlashMessagesLogic.values.toastMessages).toEqual([ + { id: 'hello' }, + { id: 'lorem ipsum' }, + ]); + }); + }); + + describe('clearToastMessages', () => { + it('resets toast messages back to an empty array', () => { + FlashMessagesLogic.actions.clearToastMessages(); + + expect(FlashMessagesLogic.values.toastMessages).toEqual([]); + }); }); }); describe('history listener logic', () => { - describe('setHistoryListener()', () => { + describe('setHistoryListener', () => { it('sets the historyListener value', () => { mount(); FlashMessagesLogic.actions.setHistoryListener('test' as any); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_logic.ts b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_logic.ts index 5993e67b28a39..f71897cc5a1d7 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_logic.ts @@ -7,6 +7,8 @@ import { kea, MakeLogicType } from 'kea'; +import { EuiGlobalToastListToast as IToast } from '@elastic/eui'; + import { KibanaLogic } from '../kibana'; import { IFlashMessage } from './types'; @@ -14,6 +16,7 @@ import { IFlashMessage } from './types'; interface FlashMessagesValues { messages: IFlashMessage[]; queuedMessages: IFlashMessage[]; + toastMessages: IToast[]; historyListener: Function | null; } interface FlashMessagesActions { @@ -21,6 +24,9 @@ interface FlashMessagesActions { clearFlashMessages(): void; setQueuedMessages(messages: IFlashMessage | IFlashMessage[]): { messages: IFlashMessage[] }; clearQueuedMessages(): void; + addToastMessage(newToast: IToast): { newToast: IToast }; + dismissToastMessage(removedToast: IToast): { removedToast: IToast }; + clearToastMessages(): void; setHistoryListener(historyListener: Function): { historyListener: Function }; } @@ -34,6 +40,9 @@ export const FlashMessagesLogic = kea null, setQueuedMessages: (messages) => ({ messages: convertToArray(messages) }), clearQueuedMessages: () => null, + addToastMessage: (newToast) => ({ newToast }), + dismissToastMessage: (removedToast) => ({ removedToast }), + clearToastMessages: () => null, setHistoryListener: (historyListener) => ({ historyListener }), }, reducers: { @@ -51,6 +60,15 @@ export const FlashMessagesLogic = kea [], }, ], + toastMessages: [ + [], + { + addToastMessage: (toasts, { newToast }) => [...toasts, newToast], + dismissToastMessage: (toasts, { removedToast }) => + toasts.filter(({ id }) => id !== removedToast.id), + clearToastMessages: () => [], + }, + ], historyListener: [ null, { diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/index.ts b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/index.ts index 40317eb390547..f08ac493f20b3 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/index.ts @@ -14,5 +14,7 @@ export { setErrorMessage, setQueuedSuccessMessage, setQueuedErrorMessage, + flashSuccessToast, + flashErrorToast, clearFlashMessages, } from './set_message_helpers'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/set_message_helpers.test.ts b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/set_message_helpers.test.ts index 0261a5556a404..d22be32e038cb 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/set_message_helpers.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/set_message_helpers.test.ts @@ -14,6 +14,8 @@ import { setQueuedSuccessMessage, setQueuedErrorMessage, clearFlashMessages, + flashSuccessToast, + flashErrorToast, } from './set_message_helpers'; describe('Flash Message Helpers', () => { @@ -72,4 +74,82 @@ describe('Flash Message Helpers', () => { expect(FlashMessagesLogic.values.messages).toEqual([]); }); + + describe('toast helpers', () => { + afterEach(() => { + FlashMessagesLogic.actions.clearToastMessages(); + }); + + describe('without optional args', () => { + beforeEach(() => { + jest.spyOn(global.Date, 'now').mockReturnValueOnce(1234567890); + }); + + it('flashSuccessToast', () => { + flashSuccessToast('You did a thing!'); + + expect(FlashMessagesLogic.values.toastMessages).toEqual([ + { + color: 'success', + iconType: 'check', + title: 'You did a thing!', + id: 'successToast-1234567890', + }, + ]); + }); + + it('flashErrorToast', () => { + flashErrorToast('Something went wrong'); + + expect(FlashMessagesLogic.values.toastMessages).toEqual([ + { + color: 'danger', + iconType: 'alert', + title: 'Something went wrong', + id: 'errorToast-1234567890', + }, + ]); + }); + }); + + describe('with optional args', () => { + it('flashSuccessToast', () => { + flashSuccessToast('You did a thing!', { + text: '', + toastLifeTimeMs: 50, + id: 'customId', + }); + + expect(FlashMessagesLogic.values.toastMessages).toEqual([ + { + color: 'success', + iconType: 'check', + title: 'You did a thing!', + text: '', + toastLifeTimeMs: 50, + id: 'customId', + }, + ]); + }); + + it('flashErrorToast', () => { + flashErrorToast('Something went wrong', { + text: "Here's some helpful advice on what to do", + toastLifeTimeMs: 50000, + id: 'specificErrorId', + }); + + expect(FlashMessagesLogic.values.toastMessages).toEqual([ + { + color: 'danger', + iconType: 'alert', + title: 'Something went wrong', + text: "Here's some helpful advice on what to do", + toastLifeTimeMs: 50000, + id: 'specificErrorId', + }, + ]); + }); + }); + }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/set_message_helpers.ts b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/set_message_helpers.ts index 1f06d8cd95930..37f7256ad44a9 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/set_message_helpers.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/set_message_helpers.ts @@ -5,7 +5,9 @@ * 2.0. */ +import { FLASH_MESSAGE_TYPES } from './constants'; import { FlashMessagesLogic } from './flash_messages_logic'; +import { ToastOptions } from './types'; export const setSuccessMessage = (message: string) => { FlashMessagesLogic.actions.setFlashMessages({ @@ -38,3 +40,21 @@ export const setQueuedErrorMessage = (message: string) => { export const clearFlashMessages = () => { FlashMessagesLogic.actions.clearFlashMessages(); }; + +export const flashSuccessToast = (message: string, toastOptions: ToastOptions = {}) => { + FlashMessagesLogic.actions.addToastMessage({ + ...FLASH_MESSAGE_TYPES.success, + ...toastOptions, + title: message, + id: toastOptions?.id || `successToast-${Date.now()}`, + }); +}; + +export const flashErrorToast = (message: string, toastOptions: ToastOptions = {}) => { + FlashMessagesLogic.actions.addToastMessage({ + ...FLASH_MESSAGE_TYPES.error, + ...toastOptions, + title: message, + id: toastOptions?.id || `errorToast-${Date.now()}`, + }); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/types.ts b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/types.ts index c1d2f8420198d..4c1b613bbc57f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/types.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/types.ts @@ -5,10 +5,20 @@ * 2.0. */ -import { ReactNode } from 'react'; +import { ReactNode, ReactChild } from 'react'; + +export type FlashMessageTypes = 'success' | 'info' | 'warning' | 'error'; +export type FlashMessageColors = 'success' | 'primary' | 'warning' | 'danger'; export interface IFlashMessage { - type: 'success' | 'info' | 'warning' | 'error'; + type: FlashMessageTypes; message: ReactNode; description?: ReactNode; } + +// @see EuiGlobalToastListToast for more props +export interface ToastOptions { + text?: ReactChild; // Additional text below the message/title, same as IFlashMessage['description'] + toastLifeTimeMs?: number; // Allows customing per-toast timeout + id?: string; +} diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups.test.tsx index 8470c5d3e0f66..54f8580a8eab9 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups.test.tsx @@ -98,7 +98,7 @@ describe('GroupOverview', () => { messages: [mockSuccessMessage], }); const wrapper = shallow(); - const flashMessages = wrapper.find(FlashMessages).dive().shallow(); + const flashMessages = wrapper.find(FlashMessages).dive().childAt(0).dive(); expect(flashMessages.find('[data-test-subj="NewGroupManageButton"]')).toHaveLength(1); }); From 82f7b2127e233b6a6438c7021250f6a1a66943c5 Mon Sep 17 00:00:00 2001 From: Jason Stoltzfus Date: Fri, 2 Apr 2021 13:24:04 -0400 Subject: [PATCH 10/30] Move `tr`s to be under `tbody` in ResultSettings (#96132) This was causing console errors. I factored out the column headers to their own component, and moved all table rows to be under a tbody. This alleviates the console warnings. --- .../column_headers.test.tsx | 21 ++++++++ .../result_settings_table/column_headers.tsx | 52 +++++++++++++++++++ .../disabled_fields_body.tsx | 6 +-- .../disabled_fields_header.tsx | 2 +- .../non_text_fields_body.tsx | 12 ++--- .../non_text_fields_header.tsx | 2 +- .../result_settings_table.tsx | 16 +++--- .../text_fields_body.tsx | 12 ++--- .../text_fields_header.tsx | 38 +------------- 9 files changed, 94 insertions(+), 67 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/column_headers.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/column_headers.tsx diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/column_headers.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/column_headers.test.tsx new file mode 100644 index 0000000000000..a2ef43908776e --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/column_headers.test.tsx @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { EuiTableHeaderCell } from '@elastic/eui'; + +import { ColumnHeaders } from './column_headers'; + +describe('ColumnHeaders', () => { + it('renders', () => { + const wrapper = shallow(); + expect(wrapper.find(EuiTableHeaderCell).length).toBe(3); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/column_headers.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/column_headers.tsx new file mode 100644 index 0000000000000..b36d71a49de13 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/column_headers.tsx @@ -0,0 +1,52 @@ +/* + * 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 { EuiIconTip, EuiTableHeader, EuiTableHeaderCell } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +export const ColumnHeaders: React.FC = () => { + return ( + + + + {i18n.translate('xpack.enterpriseSearch.appSearch.engine.resultSettings.table.rawTitle', { + defaultMessage: 'Raw', + })} + + + + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.resultSettings.table.highlightingTitle', + { + defaultMessage: 'Highlighting', + } + )} + tags for highlighting. Fallback will look for a snippet match, but fallback to an escaped raw value if none is found. Range is between 20-1000. Defaults to 100.', + } + )} + /> + + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/disabled_fields_body.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/disabled_fields_body.tsx index fd4646bf9a9f7..2f4ba0892784d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/disabled_fields_body.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/disabled_fields_body.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { useValues } from 'kea'; -import { EuiTableBody, EuiTableRow, EuiTableRowCell, EuiText, EuiHealth } from '@elastic/eui'; +import { EuiTableRow, EuiTableRowCell, EuiText, EuiHealth } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { ResultSettingsLogic } from '..'; @@ -17,7 +17,7 @@ import { ResultSettingsLogic } from '..'; export const DisabledFieldsBody: React.FC = () => { const { schemaConflicts } = useValues(ResultSettingsLogic); return ( - + <> {Object.keys(schemaConflicts).map((fieldName) => ( @@ -35,6 +35,6 @@ export const DisabledFieldsBody: React.FC = () => { ))} - + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/disabled_fields_header.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/disabled_fields_header.tsx index 0c82477814dab..1c0c1da3e4ef2 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/disabled_fields_header.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/disabled_fields_header.tsx @@ -12,7 +12,7 @@ import { i18n } from '@kbn/i18n'; export const DisabledFieldsHeader: React.FC = () => { return ( - + {i18n.translate( 'xpack.enterpriseSearch.appSearch.engine.resultSettings.table.column.disabledFieldsTitle', diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/non_text_fields_body.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/non_text_fields_body.tsx index 57dd2d5fdb974..145654be20461 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/non_text_fields_body.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/non_text_fields_body.tsx @@ -9,13 +9,7 @@ import React, { useMemo } from 'react'; import { useValues, useActions } from 'kea'; -import { - EuiTableBody, - EuiTableRow, - EuiTableRowCell, - EuiCheckbox, - EuiTableRowCellCheckbox, -} from '@elastic/eui'; +import { EuiTableRow, EuiTableRowCell, EuiCheckbox, EuiTableRowCellCheckbox } from '@elastic/eui'; import { ResultSettingsLogic } from '..'; import { FieldResultSetting } from '../types'; @@ -31,7 +25,7 @@ export const NonTextFieldsBody: React.FC = () => { }, [nonTextResultFields]); return ( - + <> {resultSettingsArray.map(([fieldName, fieldSettings]) => ( @@ -50,6 +44,6 @@ export const NonTextFieldsBody: React.FC = () => { ))} - + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/non_text_fields_header.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/non_text_fields_header.tsx index 6024f736899de..b929187780e10 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/non_text_fields_header.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/non_text_fields_header.tsx @@ -12,7 +12,7 @@ import { i18n } from '@kbn/i18n'; export const NonTextFieldsHeader: React.FC = () => { return ( - + {i18n.translate( 'xpack.enterpriseSearch.appSearch.engine.resultSettings.table.column.nonTextFieldsTitle', diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/result_settings_table.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/result_settings_table.tsx index 2da334e1f2ae2..092a4beee0c8e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/result_settings_table.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/result_settings_table.tsx @@ -8,10 +8,11 @@ import React from 'react'; import { useValues } from 'kea'; -import { EuiTable } from '@elastic/eui'; +import { EuiTable, EuiTableBody } from '@elastic/eui'; import { ResultSettingsLogic } from '..'; +import { ColumnHeaders } from './column_headers'; import { DisabledFieldsBody } from './disabled_fields_body'; import { DisabledFieldsHeader } from './disabled_fields_header'; import { NonTextFieldsBody } from './non_text_fields_body'; @@ -28,23 +29,24 @@ export const ResultSettingsTable: React.FC = () => { // to alleviate the issue. return ( + {!!Object.keys(textResultFields).length && ( - <> + - + )} {!!Object.keys(nonTextResultFields).length && ( - <> + - + )} {!!Object.keys(schemaConflicts).length && ( - <> + - + )} ); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/text_fields_body.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/text_fields_body.tsx index af01ced81f7dd..0f7e6f5e0eb1d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/text_fields_body.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/text_fields_body.tsx @@ -9,13 +9,7 @@ import React, { useMemo } from 'react'; import { useValues, useActions } from 'kea'; -import { - EuiTableBody, - EuiTableRow, - EuiTableRowCell, - EuiTableRowCellCheckbox, - EuiCheckbox, -} from '@elastic/eui'; +import { EuiTableRow, EuiTableRowCell, EuiTableRowCellCheckbox, EuiCheckbox } from '@elastic/eui'; import { ResultSettingsLogic } from '../result_settings_logic'; import { FieldResultSetting } from '../types'; @@ -41,7 +35,7 @@ export const TextFieldsBody: React.FC = () => { }, [textResultFields]); return ( - + <> {resultSettingsArray.map(([fieldName, fieldSettings]) => ( @@ -100,6 +94,6 @@ export const TextFieldsBody: React.FC = () => { ))} - + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/text_fields_header.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/text_fields_header.tsx index 3810570b3e3a2..cf4dfa9462781 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/text_fields_header.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/text_fields_header.tsx @@ -7,49 +7,13 @@ import React from 'react'; -import { EuiTableRow, EuiTableHeader, EuiTableHeaderCell, EuiIconTip } from '@elastic/eui'; +import { EuiTableRow, EuiTableHeaderCell } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; export const TextFieldsHeader: React.FC = () => { return ( <> - - - - {i18n.translate('xpack.enterpriseSearch.appSearch.engine.resultSettings.table.rawTitle', { - defaultMessage: 'Raw', - })} - - - - {i18n.translate( - 'xpack.enterpriseSearch.appSearch.engine.resultSettings.table.highlightingTitle', - { - defaultMessage: 'Highlighting', - } - )} - tags for highlighting. Fallback will look for a snippet match, but fallback to an escaped raw value if none is found. Range is between 20-1000. Defaults to 100.', - } - )} - /> - - {i18n.translate( From a0828c6797ad1eb82490f002a35998047d7021a0 Mon Sep 17 00:00:00 2001 From: James Rodewig <40268737+jrodewig@users.noreply.github.com> Date: Fri, 2 Apr 2021 16:15:16 -0400 Subject: [PATCH 11/30] [DOCS] Fix docker run code snippet (#96157) The current docker run code snippet is malformed and renders incorrectly. This fixes the rendering. --- docs/setup/docker.asciidoc | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/setup/docker.asciidoc b/docs/setup/docker.asciidoc index 25883307e69f0..31e7b25eb66b1 100644 --- a/docs/setup/docker.asciidoc +++ b/docs/setup/docker.asciidoc @@ -39,11 +39,13 @@ docker pull {docker-repo}:{version} === Run Kibana on Docker for development Kibana can be quickly started and connected to a local Elasticsearch container for development or testing use with the following command: --------------------------------------------- + +[source,sh,subs="attributes"] +---- docker run --link YOUR_ELASTICSEARCH_CONTAINER_NAME_OR_ID:elasticsearch -p 5601:5601 {docker-repo}:{version} --------------------------------------------- -endif::[] +---- +endif::[] [float] [[configuring-kibana-docker]] === Configure Kibana on Docker From c97c51876e4bcf1abd8a8ea60e771ec66e7049be Mon Sep 17 00:00:00 2001 From: Constance Date: Fri, 2 Apr 2021 14:28:18 -0700 Subject: [PATCH 12/30] Remove incorrect error toast on successful poll (#96159) --- .../app_search/components/api_logs/api_logs_logic.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_logs_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_logs_logic.ts index 4d0d80cf2c24b..a9186bd4d66cf 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_logs_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_logs_logic.ts @@ -117,10 +117,6 @@ export const ApiLogsLogic = kea>({ // while polls are stored in-state until the user manually triggers the 'Refresh' action if (isPoll) { actions.onPollInterval(response); - flashErrorToast(POLLING_ERROR_TITLE, { - text: POLLING_ERROR_TEXT, - toastLifeTimeMs: POLLING_DURATION * 0.75, - }); } else { actions.updateView(response); } From 8ccb0d4ca37b4ca655c399ab72b9f2cb51d608d4 Mon Sep 17 00:00:00 2001 From: Jonathan Budzenski Date: Fri, 2 Apr 2021 18:02:54 -0500 Subject: [PATCH 13/30] [docker] Consistent timestamps across builds (#96081) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- src/dev/build/tasks/os_packages/create_os_package_tasks.ts | 7 +++++++ src/dev/build/tasks/os_packages/docker_generator/run.ts | 3 ++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/dev/build/tasks/os_packages/create_os_package_tasks.ts b/src/dev/build/tasks/os_packages/create_os_package_tasks.ts index e37a61582c6a8..2ae882000cae0 100644 --- a/src/dev/build/tasks/os_packages/create_os_package_tasks.ts +++ b/src/dev/build/tasks/os_packages/create_os_package_tasks.ts @@ -49,6 +49,7 @@ export const CreateRpmPackage: Task = { }, }; +const dockerBuildDate = new Date().toISOString(); export const CreateDockerCentOS: Task = { description: 'Creating Docker CentOS image', @@ -57,11 +58,13 @@ export const CreateDockerCentOS: Task = { architecture: 'x64', context: false, image: true, + dockerBuildDate, }); await runDockerGenerator(config, log, build, { architecture: 'aarch64', context: false, image: true, + dockerBuildDate, }); }, }; @@ -76,6 +79,7 @@ export const CreateDockerUBI: Task = { context: false, ubi: true, image: true, + dockerBuildDate, }); } }, @@ -88,6 +92,7 @@ export const CreateDockerContexts: Task = { await runDockerGenerator(config, log, build, { context: true, image: false, + dockerBuildDate, }); if (!build.isOss()) { @@ -95,11 +100,13 @@ export const CreateDockerContexts: Task = { ubi: true, context: true, image: false, + dockerBuildDate, }); await runDockerGenerator(config, log, build, { ironbank: true, context: true, image: false, + dockerBuildDate, }); } }, diff --git a/src/dev/build/tasks/os_packages/docker_generator/run.ts b/src/dev/build/tasks/os_packages/docker_generator/run.ts index 8bf876b558431..c72112b7b6b03 100644 --- a/src/dev/build/tasks/os_packages/docker_generator/run.ts +++ b/src/dev/build/tasks/os_packages/docker_generator/run.ts @@ -33,6 +33,7 @@ export async function runDockerGenerator( image: boolean; ubi?: boolean; ironbank?: boolean; + dockerBuildDate?: string; } ) { // UBI var config @@ -53,7 +54,7 @@ export async function runDockerGenerator( const artifactPrefix = `kibana${artifactFlavor}-${version}-linux`; const artifactTarball = `${artifactPrefix}-${artifactArchitecture}.tar.gz`; const artifactsDir = config.resolveFromTarget('.'); - const dockerBuildDate = new Date().toISOString(); + const dockerBuildDate = flags.dockerBuildDate || new Date().toISOString(); // That would produce oss, default and default-ubi7 const dockerBuildDir = config.resolveFromRepo( 'build', From 7d0920bbfa244c20c05ac94f9f9fb6ac1c19867b Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Fri, 2 Apr 2021 23:26:35 -0400 Subject: [PATCH 14/30] Replace lodash templates with static react renderer for field formatters (#96048) --- .../field_formats/converters/color.test.ts | 14 ++--- .../converters/{color.ts => color.tsx} | 23 +++++--- .../field_formats/converters/source.test.ts | 16 ++++++ .../converters/{source.ts => source.tsx} | 52 +++++++++---------- 4 files changed, 62 insertions(+), 43 deletions(-) rename src/plugins/data/common/field_formats/converters/{color.ts => color.tsx} (79%) rename src/plugins/data/common/field_formats/converters/{source.ts => source.tsx} (62%) diff --git a/src/plugins/data/common/field_formats/converters/color.test.ts b/src/plugins/data/common/field_formats/converters/color.test.ts index 9ce00db10b28d..4b7f2733f56fc 100644 --- a/src/plugins/data/common/field_formats/converters/color.test.ts +++ b/src/plugins/data/common/field_formats/converters/color.test.ts @@ -28,10 +28,10 @@ describe('Color Format', () => { expect(colorer.convert(99, HTML_CONTEXT_TYPE)).toBe('99'); expect(colorer.convert(100, HTML_CONTEXT_TYPE)).toBe( - '100' + '100' ); expect(colorer.convert(150, HTML_CONTEXT_TYPE)).toBe( - '150' + '150' ); expect(colorer.convert(151, HTML_CONTEXT_TYPE)).toBe('151'); }); @@ -74,22 +74,22 @@ describe('Color Format', () => { expect(converter('B', HTML_CONTEXT_TYPE)).toBe('B'); expect(converter('AAA', HTML_CONTEXT_TYPE)).toBe( - 'AAA' + 'AAA' ); expect(converter('AB', HTML_CONTEXT_TYPE)).toBe( - 'AB' + 'AB' ); expect(converter('a', HTML_CONTEXT_TYPE)).toBe('a'); expect(converter('B', HTML_CONTEXT_TYPE)).toBe('B'); expect(converter('AAA', HTML_CONTEXT_TYPE)).toBe( - 'AAA' + 'AAA' ); expect(converter('AB', HTML_CONTEXT_TYPE)).toBe( - 'AB' + 'AB' ); expect(converter('AB <', HTML_CONTEXT_TYPE)).toBe( - 'AB <' + 'AB <' ); expect(converter('a', HTML_CONTEXT_TYPE)).toBe('a'); }); diff --git a/src/plugins/data/common/field_formats/converters/color.ts b/src/plugins/data/common/field_formats/converters/color.tsx similarity index 79% rename from src/plugins/data/common/field_formats/converters/color.ts rename to src/plugins/data/common/field_formats/converters/color.tsx index f4603f32acc15..98f25fdf81811 100644 --- a/src/plugins/data/common/field_formats/converters/color.ts +++ b/src/plugins/data/common/field_formats/converters/color.tsx @@ -7,15 +7,15 @@ */ import { i18n } from '@kbn/i18n'; -import { findLast, cloneDeep, template, escape } from 'lodash'; +import React from 'react'; +import ReactDOM from 'react-dom/server'; +import { findLast, cloneDeep, escape } from 'lodash'; import { KBN_FIELD_TYPES } from '../../kbn_field_types/types'; import { FieldFormat } from '../field_format'; import { HtmlContextTypeConvert, FIELD_FORMAT_IDS } from '../types'; import { asPrettyString } from '../utils'; import { DEFAULT_CONVERTER_COLOR } from '../constants/color_default'; -const convertTemplate = template('<%- val %>'); - export class ColorFormat extends FieldFormat { static id = FIELD_FORMAT_IDS.COLOR; static title = i18n.translate('data.fieldFormats.color.title', { @@ -51,11 +51,18 @@ export class ColorFormat extends FieldFormat { htmlConvert: HtmlContextTypeConvert = (val) => { const color = this.findColorRuleForVal(val) as typeof DEFAULT_CONVERTER_COLOR; - if (!color) return escape(asPrettyString(val)); - let style = ''; - if (color.text) style += `color: ${color.text};`; - if (color.background) style += `background-color: ${color.background};`; - return convertTemplate({ val, style }); + const displayVal = escape(asPrettyString(val)); + if (!color) return displayVal; + + return ReactDOM.renderToStaticMarkup( + + ); }; } diff --git a/src/plugins/data/common/field_formats/converters/source.test.ts b/src/plugins/data/common/field_formats/converters/source.test.ts index f0576142892e2..655cf315a05a4 100644 --- a/src/plugins/data/common/field_formats/converters/source.test.ts +++ b/src/plugins/data/common/field_formats/converters/source.test.ts @@ -9,6 +9,7 @@ import { SourceFormat } from './source'; import { HtmlContextTypeConvert } from '../types'; import { HTML_CONTEXT_TYPE } from '../content_types'; +import { stubIndexPatternWithFields } from '../../index_patterns/index_pattern.stub'; describe('Source Format', () => { let convertHtml: Function; @@ -31,4 +32,19 @@ describe('Source Format', () => { '{"foo":"bar","number":42,"hello":"<h1>World</h1>","also":"with \\"quotes\\" or 'single quotes'"}' ); }); + + test('should render a description list if a field is passed', () => { + const hit = { + foo: 'bar', + number: 42, + hello: '

World

', + also: 'with "quotes" or \'single quotes\'', + }; + + const indexPattern = { ...stubIndexPatternWithFields, formatHit: (h: string) => h }; + + expect(convertHtml(hit, { field: 'field', indexPattern, hit })).toMatchInlineSnapshot( + `"
foo:
bar
number:
42
hello:

World

also:
with \\"quotes\\" or 'single quotes'
"` + ); + }); }); diff --git a/src/plugins/data/common/field_formats/converters/source.ts b/src/plugins/data/common/field_formats/converters/source.tsx similarity index 62% rename from src/plugins/data/common/field_formats/converters/source.ts rename to src/plugins/data/common/field_formats/converters/source.tsx index bacfc1ab4c737..d6176b321f3f3 100644 --- a/src/plugins/data/common/field_formats/converters/source.ts +++ b/src/plugins/data/common/field_formats/converters/source.tsx @@ -6,40 +6,34 @@ * Side Public License, v 1. */ -import { template, escape, keys } from 'lodash'; +import React, { Fragment } from 'react'; +import ReactDOM from 'react-dom/server'; +import { escape, keys } from 'lodash'; import { shortenDottedString } from '../../utils'; import { KBN_FIELD_TYPES } from '../../kbn_field_types/types'; import { FieldFormat } from '../field_format'; import { TextContextTypeConvert, HtmlContextTypeConvert, FIELD_FORMAT_IDS } from '../types'; import { UI_SETTINGS } from '../../constants'; -/** - * Remove all of the whitespace between html tags - * so that inline elements don't have extra spaces. - * - * If you have inline elements (span, a, em, etc.) and any - * amount of whitespace around them in your markup, then the - * browser will push them apart. This is ugly in certain - * scenarios and is only fixed by removing the whitespace - * from the html in the first place (or ugly css hacks). - * - * @param {string} html - the html to modify - * @return {string} - modified html - */ -function noWhiteSpace(html: string) { - const TAGS_WITH_WS = />\s+<'); +interface Props { + defPairs: Array<[string, string]>; } - -const templateHtml = ` -
- <% defPairs.forEach(function (def) { %> -
<%- def[0] %>:
-
<%= def[1] %>
- <%= ' ' %> - <% }); %> -
`; -const doTemplate = template(noWhiteSpace(templateHtml)); +const TemplateComponent = ({ defPairs }: Props) => { + return ( +
+ {defPairs.map((pair, idx) => ( + +
+
{' '} + + ))} +
+ ); +}; export class SourceFormat extends FieldFormat { static id = FIELD_FORMAT_IDS._SOURCE; @@ -70,6 +64,8 @@ export class SourceFormat extends FieldFormat { pairs.push([newField, val]); }, []); - return doTemplate({ defPairs: highlightPairs.concat(sourcePairs) }); + return ReactDOM.renderToStaticMarkup( + + ); }; } From f8a6ba223ac5d44c627458176859050ab3f4d200 Mon Sep 17 00:00:00 2001 From: Spencer Date: Sun, 4 Apr 2021 09:15:14 -0700 Subject: [PATCH 15/30] stop wrapping steps in runbld (#96195) Co-authored-by: spalger --- vars/runbld.groovy | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vars/runbld.groovy b/vars/runbld.groovy index e52bc244c65cb..1a8ebec18d485 100644 --- a/vars/runbld.groovy +++ b/vars/runbld.groovy @@ -1,8 +1,8 @@ def call(script, label, enableJunitProcessing = false) { - def extraConfig = enableJunitProcessing ? "" : "--config ${env.WORKSPACE}/kibana/.ci/runbld_no_junit.yml" + // def extraConfig = enableJunitProcessing ? "" : "--config ${env.WORKSPACE}/kibana/.ci/runbld_no_junit.yml" sh( - script: "/usr/local/bin/runbld -d '${pwd()}' ${extraConfig} ${script}", + script: script, label: label ?: script ) } From 763c0038ae965ce8633b8c247999469b321e3371 Mon Sep 17 00:00:00 2001 From: spalger Date: Sun, 4 Apr 2021 09:34:58 -0700 Subject: [PATCH 16/30] Revert "stop wrapping steps in runbld (#96195)" This reverts commit f8a6ba223ac5d44c627458176859050ab3f4d200. --- vars/runbld.groovy | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vars/runbld.groovy b/vars/runbld.groovy index 1a8ebec18d485..e52bc244c65cb 100644 --- a/vars/runbld.groovy +++ b/vars/runbld.groovy @@ -1,8 +1,8 @@ def call(script, label, enableJunitProcessing = false) { - // def extraConfig = enableJunitProcessing ? "" : "--config ${env.WORKSPACE}/kibana/.ci/runbld_no_junit.yml" + def extraConfig = enableJunitProcessing ? "" : "--config ${env.WORKSPACE}/kibana/.ci/runbld_no_junit.yml" sh( - script: script, + script: "/usr/local/bin/runbld -d '${pwd()}' ${extraConfig} ${script}", label: label ?: script ) } From cf22394807f0112dbe415acfdfdc75e3dc636e2e Mon Sep 17 00:00:00 2001 From: Spencer Date: Sun, 4 Apr 2021 11:40:42 -0700 Subject: [PATCH 17/30] pass script to bash to support scripts which aren't actually executable (#96198) Co-authored-by: spalger --- vars/runbld.groovy | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vars/runbld.groovy b/vars/runbld.groovy index e52bc244c65cb..80416d4fa9a41 100644 --- a/vars/runbld.groovy +++ b/vars/runbld.groovy @@ -1,8 +1,8 @@ def call(script, label, enableJunitProcessing = false) { - def extraConfig = enableJunitProcessing ? "" : "--config ${env.WORKSPACE}/kibana/.ci/runbld_no_junit.yml" + // def extraConfig = enableJunitProcessing ? "" : "--config ${env.WORKSPACE}/kibana/.ci/runbld_no_junit.yml" sh( - script: "/usr/local/bin/runbld -d '${pwd()}' ${extraConfig} ${script}", + script: "bash ${script}", label: label ?: script ) } From 8c8323abfd5863825a001081ed090581b223d5b7 Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Mon, 5 Apr 2021 14:01:24 +0200 Subject: [PATCH 18/30] [Search Sessions] fix updating deleting sessions from non-default space (#96123) * add spaces test * fix updating and deleting sessions in non-default space * revert back to batch update * Add space tests Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Liza K --- .../session/check_running_sessions.test.ts | 90 ++++++++++- .../search/session/check_running_sessions.ts | 43 ++++- .../api_integration/apis/search/session.ts | 153 ++++++++++++++++++ 3 files changed, 278 insertions(+), 8 deletions(-) diff --git a/x-pack/plugins/data_enhanced/server/search/session/check_running_sessions.test.ts b/x-pack/plugins/data_enhanced/server/search/session/check_running_sessions.test.ts index f9c62069154b6..2611f6c9da19f 100644 --- a/x-pack/plugins/data_enhanced/server/search/session/check_running_sessions.test.ts +++ b/x-pack/plugins/data_enhanced/server/search/session/check_running_sessions.test.ts @@ -13,9 +13,13 @@ import { EQL_SEARCH_STRATEGY, } from '../../../common'; import { savedObjectsClientMock } from '../../../../../../src/core/server/mocks'; -import type { SavedObjectsClientContract } from 'kibana/server'; import { SearchSessionsConfig, SearchStatus } from './types'; import moment from 'moment'; +import { + SavedObjectsBulkUpdateObject, + SavedObjectsDeleteOptions, + SavedObjectsClientContract, +} from '../../../../../../src/core/server'; describe('getSearchStatus', () => { let mockClient: any; @@ -263,6 +267,45 @@ describe('getSearchStatus', () => { expect(savedObjectsClient.delete).not.toBeCalled(); }); + test('deletes in space', async () => { + savedObjectsClient.find.mockResolvedValue({ + saved_objects: [ + { + id: '123', + namespaces: ['awesome'], + attributes: { + persisted: false, + status: SearchSessionStatus.IN_PROGRESS, + created: moment().subtract(moment.duration(3, 'm')), + touched: moment().subtract(moment.duration(2, 'm')), + idMapping: { + 'map-key': { + strategy: ENHANCED_ES_SEARCH_STRATEGY, + id: 'async-id', + }, + }, + }, + }, + ], + total: 1, + } as any); + + await checkRunningSessions( + { + savedObjectsClient, + client: mockClient, + logger: mockLogger, + }, + config + ); + + expect(savedObjectsClient.delete).toBeCalled(); + + const [, id, opts] = savedObjectsClient.delete.mock.calls[0]; + expect(id).toBe('123'); + expect((opts as SavedObjectsDeleteOptions).namespace).toBe('awesome'); + }); + test('deletes a non persisted, abandoned session', async () => { savedObjectsClient.find.mockResolvedValue({ saved_objects: [ @@ -479,6 +522,50 @@ describe('getSearchStatus', () => { expect(savedObjectsClient.delete).not.toBeCalled(); }); + test('updates in space', async () => { + savedObjectsClient.bulkUpdate = jest.fn(); + const so = { + namespaces: ['awesome'], + attributes: { + status: SearchSessionStatus.IN_PROGRESS, + touched: '123', + idMapping: { + 'search-hash': { + id: 'search-id', + strategy: 'cool', + status: SearchStatus.IN_PROGRESS, + }, + }, + }, + }; + savedObjectsClient.find.mockResolvedValue({ + saved_objects: [so], + total: 1, + } as any); + + mockClient.asyncSearch.status.mockResolvedValue({ + body: { + is_partial: false, + is_running: false, + completion_status: 200, + }, + }); + + await checkRunningSessions( + { + savedObjectsClient, + client: mockClient, + logger: mockLogger, + }, + config + ); + + expect(mockClient.asyncSearch.status).toBeCalledWith({ id: 'search-id' }); + const [updateInput] = savedObjectsClient.bulkUpdate.mock.calls[0]; + const updatedAttributes = updateInput[0] as SavedObjectsBulkUpdateObject; + expect(updatedAttributes.namespace).toBe('awesome'); + }); + test('updates to complete if the search is done', async () => { savedObjectsClient.bulkUpdate = jest.fn(); const so = { @@ -563,7 +650,6 @@ describe('getSearchStatus', () => { config ); const [updateInput] = savedObjectsClient.bulkUpdate.mock.calls[0]; - const updatedAttributes = updateInput[0].attributes as SearchSessionSavedObjectAttributes; expect(updatedAttributes.status).toBe(SearchSessionStatus.ERROR); expect(updatedAttributes.idMapping['search-hash'].status).toBe(SearchStatus.ERROR); diff --git a/x-pack/plugins/data_enhanced/server/search/session/check_running_sessions.ts b/x-pack/plugins/data_enhanced/server/search/session/check_running_sessions.ts index e521c39d7cfd3..6e52b17f36803 100644 --- a/x-pack/plugins/data_enhanced/server/search/session/check_running_sessions.ts +++ b/x-pack/plugins/data_enhanced/server/search/session/check_running_sessions.ts @@ -10,6 +10,7 @@ import { Logger, SavedObjectsClientContract, SavedObjectsFindResult, + SavedObjectsUpdateResponse, } from 'kibana/server'; import moment from 'moment'; import { EMPTY, from } from 'rxjs'; @@ -169,12 +170,20 @@ export async function checkRunningSessions( if (!session.attributes.persisted) { if (isSessionStale(session, config, logger)) { - deleted = true; // delete saved object to free up memory // TODO: there's a potential rare edge case of deleting an object and then receiving a new trackId for that same session! // Maybe we want to change state to deleted and cleanup later? logger.debug(`Deleting stale session | ${session.id}`); - await savedObjectsClient.delete(SEARCH_SESSION_TYPE, session.id); + try { + await savedObjectsClient.delete(SEARCH_SESSION_TYPE, session.id, { + namespace: session.namespaces?.[0], + }); + deleted = true; + } catch (e) { + logger.error( + `Error while deleting stale search session ${session.id}: ${e.message}` + ); + } // Send a delete request for each async search to ES Object.keys(session.attributes.idMapping).map(async (searchKey: string) => { @@ -183,8 +192,8 @@ export async function checkRunningSessions( try { await client.asyncSearch.delete({ id: searchInfo.id }); } catch (e) { - logger.debug( - `Error ignored while deleting async_search ${searchInfo.id}: ${e.message}` + logger.error( + `Error while deleting async_search ${searchInfo.id}: ${e.message}` ); } } @@ -202,9 +211,31 @@ export async function checkRunningSessions( if (updatedSessions.length) { // If there's an error, we'll try again in the next iteration, so there's no need to check the output. const updatedResponse = await savedObjectsClient.bulkUpdate( - updatedSessions + updatedSessions.map((session) => ({ + ...session, + namespace: session.namespaces?.[0], + })) + ); + + const success: Array< + SavedObjectsUpdateResponse + > = []; + const fail: Array> = []; + + updatedResponse.saved_objects.forEach((savedObjectResponse) => { + if ('error' in savedObjectResponse) { + fail.push(savedObjectResponse); + logger.error( + `Error while updating search session ${savedObjectResponse?.id}: ${savedObjectResponse.error?.message}` + ); + } else { + success.push(savedObjectResponse); + } + }); + + logger.debug( + `Updating search sessions: success: ${success.length}, fail: ${fail.length}` ); - logger.debug(`Updated ${updatedResponse.saved_objects.length} search sessions`); } }) ) diff --git a/x-pack/test/api_integration/apis/search/session.ts b/x-pack/test/api_integration/apis/search/session.ts index 50bc85ed1e793..63a6a842fd9f7 100644 --- a/x-pack/test/api_integration/apis/search/session.ts +++ b/x-pack/test/api_integration/apis/search/session.ts @@ -14,6 +14,7 @@ export default function ({ getService }: FtrProviderContext) { const supertestWithoutAuth = getService('supertestWithoutAuth'); const security = getService('security'); const retry = getService('retry'); + const spacesService = getService('spaces'); describe('search session', () => { describe('session management', () => { @@ -596,5 +597,157 @@ export default function ({ getService }: FtrProviderContext) { .expect(403); }); }); + + describe('in non-default space', () => { + const spaceId = 'foo-space'; + before(async () => { + try { + await spacesService.create({ + id: spaceId, + name: 'Foo Space', + }); + } catch { + // might already be created + } + }); + + after(async () => { + await spacesService.delete(spaceId); + }); + + it('should complete and delete non-persistent sessions', async () => { + const sessionId = `my-session-${Math.random()}`; + + // run search + const searchRes = await supertest + .post(`/s/${spaceId}/internal/search/ese`) + .set('kbn-xsrf', 'foo') + .send({ + sessionId, + params: { + body: { + query: { + term: { + agent: '1', + }, + }, + }, + wait_for_completion_timeout: '1ms', + }, + }) + .expect(200); + + const { id } = searchRes.body; + + await retry.waitForWithTimeout('searches persisted into session', 5000, async () => { + const resp = await supertest + .get(`/s/${spaceId}/internal/session/${sessionId}`) + .set('kbn-xsrf', 'foo') + .expect(200); + + const { touched, created, persisted, idMapping } = resp.body.attributes; + expect(persisted).to.be(false); + expect(touched).not.to.be(undefined); + expect(created).not.to.be(undefined); + + const idMappings = Object.values(idMapping).map((value: any) => value.id); + expect(idMappings).to.contain(id); + return true; + }); + + // not touched timeout in tests is 15s, wait to give a chance for status to update + await new Promise((resolve) => + setTimeout(() => { + resolve(void 0); + }, 15_000) + ); + + await retry.waitForWithTimeout( + 'searches eventually complete and session gets into the complete state', + 30_000, + async () => { + await supertest + .get(`/s/${spaceId}/internal/session/${sessionId}`) + .set('kbn-xsrf', 'foo') + .expect(404); + + return true; + } + ); + }); + + it('should complete persisten session', async () => { + const sessionId = `my-session-${Math.random()}`; + + // run search + const searchRes = await supertest + .post(`/s/${spaceId}/internal/search/ese`) + .set('kbn-xsrf', 'foo') + .send({ + sessionId, + params: { + body: { + query: { + term: { + agent: '1', + }, + }, + }, + wait_for_completion_timeout: '1ms', + }, + }) + .expect(200); + + const { id } = searchRes.body; + + // persist session + await supertest + .post(`/s/${spaceId}/internal/session`) + .set('kbn-xsrf', 'foo') + .send({ + sessionId, + name: 'My Session', + appId: 'discover', + expires: '123', + urlGeneratorId: 'discover', + }) + .expect(200); + + await retry.waitForWithTimeout('searches persisted into session', 5000, async () => { + const resp = await supertest + .get(`/s/${spaceId}/internal/session/${sessionId}`) + .set('kbn-xsrf', 'foo') + .expect(200); + + const { touched, created, persisted, idMapping } = resp.body.attributes; + expect(persisted).to.be(true); + expect(touched).not.to.be(undefined); + expect(created).not.to.be(undefined); + + const idMappings = Object.values(idMapping).map((value: any) => value.id); + expect(idMappings).to.contain(id); + return true; + }); + + // session refresh interval is 5 seconds, wait to give a chance for status to update + await new Promise((resolve) => setTimeout(resolve, 5000)); + + await retry.waitForWithTimeout( + 'searches eventually complete and session gets into the complete state', + 5000, + async () => { + const resp = await supertest + .get(`/s/${spaceId}/internal/session/${sessionId}`) + .set('kbn-xsrf', 'foo') + .expect(200); + + const { status } = resp.body.attributes; + + expect(status).to.be(SearchSessionStatus.COMPLETE); + return true; + } + ); + }); + }); }); } From 413477b788eeaf83edf6036603c120ffb3b2f4b7 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Mon, 5 Apr 2021 13:36:28 +0100 Subject: [PATCH 19/30] skip flaky suite (#96113) --- test/functional/apps/discover/_huge_fields.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/functional/apps/discover/_huge_fields.ts b/test/functional/apps/discover/_huge_fields.ts index 8cb39feb2e6bb..b3e63e482e734 100644 --- a/test/functional/apps/discover/_huge_fields.ts +++ b/test/functional/apps/discover/_huge_fields.ts @@ -15,7 +15,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const kibanaServer = getService('kibanaServer'); const testSubjects = getService('testSubjects'); - describe('test large number of fields in sidebar', function () { + // FLAKY: https://github.com/elastic/kibana/issues/96113 + describe.skip('test large number of fields in sidebar', function () { before(async function () { await security.testUser.setRoles(['kibana_admin', 'test_testhuge_reader'], false); await esArchiver.loadIfNeeded('large_fields'); From b670ef29ae80f77d3bc2fa7dcc652d173efa985f Mon Sep 17 00:00:00 2001 From: Alexey Antonov Date: Mon, 5 Apr 2021 15:57:42 +0300 Subject: [PATCH 20/30] [TSVB] fix wrong field list on overriding index pattern for series (#96204) --- .../public/application/components/aggs/agg.tsx | 6 +++--- .../public/application/components/aggs/filter_ratio.js | 5 +++-- .../public/application/components/aggs/percentile.js | 5 +++-- .../public/application/components/aggs/positive_rate.js | 6 +++--- .../public/application/components/aggs/std_agg.js | 5 +++-- .../public/application/components/aggs/std_deviation.js | 5 +++-- .../public/application/components/aggs/top_hit.js | 5 +++-- .../public/application/components/series_config.js | 7 +++---- .../public/application/components/split.js | 5 +++-- .../application/components/vis_types/timeseries/config.js | 7 +++---- 10 files changed, 30 insertions(+), 26 deletions(-) diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/agg.tsx b/src/plugins/vis_type_timeseries/public/application/components/aggs/agg.tsx index 25965d796e651..d02565717b247 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/agg.tsx +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/agg.tsx @@ -48,9 +48,9 @@ export function Agg(props: AggProps) { ...props.style, }; - const indexPattern = - (props.series.override_index_pattern && props.series.series_index_pattern) || - props.panel.index_pattern; + const indexPattern = props.series.override_index_pattern + ? props.series.series_index_pattern + : props.panel.index_pattern; return (
diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/filter_ratio.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/filter_ratio.js index 19d2b88c8d123..7f93567980b2d 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/filter_ratio.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/filter_ratio.js @@ -51,8 +51,9 @@ export const FilterRatioAgg = (props) => { (query) => handleChange({ denominator: query }), [handleChange] ); - const indexPattern = - (series.override_index_pattern && series.series_index_pattern) || panel.index_pattern; + const indexPattern = series.override_index_pattern + ? series.series_index_pattern + : panel.index_pattern; const defaults = { numerator: getDataStart().query.queryString.getDefaultQuery(), diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile.js index 505a2ff4f3c78..77b2e2f020307 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile.js @@ -39,8 +39,9 @@ export function PercentileAgg(props) { const handleSelectChange = createSelectHandler(handleChange); const handleNumberChange = createNumberHandler(handleChange); - const indexPattern = - (series.override_index_pattern && series.series_index_pattern) || panel.index_pattern; + const indexPattern = series.override_index_pattern + ? series.series_index_pattern + : panel.index_pattern; useEffect(() => { if (!checkModel(model)) { diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/positive_rate.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/positive_rate.js index a80194f72b7b2..4b1528ca27081 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/positive_rate.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/positive_rate.js @@ -65,9 +65,9 @@ export const PositiveRateAgg = (props) => { const handleSelectChange = createSelectHandler(handleChange); const htmlId = htmlIdGenerator(); - const indexPattern = - (props.series.override_index_pattern && props.series.series_index_pattern) || - props.panel.index_pattern; + const indexPattern = props.series.override_index_pattern + ? props.series.series_index_pattern + : props.panel.index_pattern; const selectedUnitOptions = UNIT_OPTIONS.filter((o) => o.value === model.unit); diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/std_agg.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/std_agg.js index 4a4114f70f06a..74b441f446308 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/std_agg.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/std_agg.js @@ -31,8 +31,9 @@ export function StandardAgg(props) { const handleSelectChange = createSelectHandler(handleChange); const restrictFields = getSupportedFieldsByMetricType(model.type); - const indexPattern = - (series.override_index_pattern && series.series_index_pattern) || panel.index_pattern; + const indexPattern = series.override_index_pattern + ? series.series_index_pattern + : panel.index_pattern; const htmlId = htmlIdGenerator(); return ( diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/std_deviation.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/std_deviation.js index c28cb294c3308..749a97fa79f28 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/std_deviation.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/std_deviation.js @@ -72,8 +72,9 @@ const StandardDeviationAggUi = (props) => { const handleSelectChange = createSelectHandler(handleChange); const handleTextChange = createTextHandler(handleChange); - const indexPattern = - (series.override_index_pattern && series.series_index_pattern) || panel.index_pattern; + const indexPattern = series.override_index_pattern + ? series.series_index_pattern + : panel.index_pattern; const htmlId = htmlIdGenerator(); const selectedModeOption = modeOptions.find((option) => { return model.mode === option.value; diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/top_hit.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/top_hit.js index 9bea32b7cbd5b..92e754c1dcdaf 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/top_hit.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/top_hit.js @@ -100,8 +100,9 @@ const TopHitAggUi = (props) => { order: 'desc', }; const model = { ...defaults, ...props.model }; - const indexPattern = - (series.override_index_pattern && series.series_index_pattern) || panel.index_pattern; + const indexPattern = series.override_index_pattern + ? series.series_index_pattern + : panel.index_pattern; const aggWithOptionsRestrictFields = [ PANEL_TYPES.TABLE, diff --git a/src/plugins/vis_type_timeseries/public/application/components/series_config.js b/src/plugins/vis_type_timeseries/public/application/components/series_config.js index 3185503acb569..8f3893feb89bd 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/series_config.js +++ b/src/plugins/vis_type_timeseries/public/application/components/series_config.js @@ -33,10 +33,9 @@ export const SeriesConfig = (props) => { const handleSelectChange = createSelectHandler(props.onChange); const handleTextChange = createTextHandler(props.onChange); const htmlId = htmlIdGenerator(); - const seriesIndexPattern = - props.model.override_index_pattern && props.model.series_index_pattern - ? props.model.series_index_pattern - : props.indexPatternForQuery; + const seriesIndexPattern = props.model.override_index_pattern + ? props.model.series_index_pattern + : props.indexPatternForQuery; return (
diff --git a/src/plugins/vis_type_timeseries/public/application/components/split.js b/src/plugins/vis_type_timeseries/public/application/components/split.js index 63aa717174a04..4990800acf6db 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/split.js +++ b/src/plugins/vis_type_timeseries/public/application/components/split.js @@ -63,8 +63,9 @@ export class Split extends Component { render() { const { model, panel, uiRestrictions, seriesQuantity } = this.props; - const indexPattern = - (model.override_index_pattern && model.series_index_pattern) || panel.index_pattern; + const indexPattern = model.override_index_pattern + ? model.series_index_pattern + : panel.index_pattern; const splitMode = get(this.props, 'model.split_mode', SPLIT_MODES.EVERYTHING); const Component = this.getComponent(splitMode, uiRestrictions); diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/config.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/config.js index 22bf2fa4ca708..1c3a0411998b0 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/config.js +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/config.js @@ -327,10 +327,9 @@ export const TimeseriesConfig = injectI18n(function (props) { const disableSeparateYaxis = model.separate_axis ? false : true; - const seriesIndexPattern = - props.model.override_index_pattern && props.model.series_index_pattern - ? props.model.series_index_pattern - : props.indexPatternForQuery; + const seriesIndexPattern = props.model.override_index_pattern + ? props.model.series_index_pattern + : props.indexPatternForQuery; const initialPalette = { ...model.palette, From 76acef0c2ceeeb38e76ac6979ee6bcbda099561c Mon Sep 17 00:00:00 2001 From: Caroline Horn <549577+cchaos@users.noreply.github.com> Date: Mon, 5 Apr 2021 09:05:30 -0400 Subject: [PATCH 21/30] [KQL] Fixed styles of KQL textarea for the K8 theme (#96190) * Fixed style of KQL textarea for K8 theme and some general heights and borders * Fix popover paddings --- .../ui/filter_bar/filter_editor/index.tsx | 2 +- .../public/ui/filter_bar/filter_options.tsx | 2 +- .../ui/query_string_input/_query_bar.scss | 17 +++++++++++++++-- .../ui/query_string_input/language_switcher.tsx | 2 +- .../saved_query_management_component.tsx | 4 ++-- 5 files changed, 20 insertions(+), 7 deletions(-) diff --git a/src/plugins/data/public/ui/filter_bar/filter_editor/index.tsx b/src/plugins/data/public/ui/filter_bar/filter_editor/index.tsx index 5639229e1ff31..d2f04228ed396 100644 --- a/src/plugins/data/public/ui/filter_bar/filter_editor/index.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_editor/index.tsx @@ -85,7 +85,7 @@ class FilterEditorUI extends Component { public render() { return (
- + { panelPaddingSize="none" repositionOnScroll > - +
- +

- + {savedQueryPopoverTitleText} {savedQueries.length > 0 ? ( @@ -234,7 +234,7 @@ export function SavedQueryManagementComponent({ )} - + Date: Mon, 5 Apr 2021 15:26:48 +0200 Subject: [PATCH 22/30] [Observability] Exploratory View initial skeleton (#94426) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- tsconfig.json | 1 + x-pack/plugins/observability/kibana.json | 2 +- .../public/assets/kibana_dashboard_dark.svg | 116 ++++++ .../public/assets/kibana_dashboard_light.svg | 116 ++++++ .../public/components/app/header/index.tsx | 4 +- .../components/empty_view.tsx | 32 ++ .../components/filter_label.test.tsx | 138 +++++++ .../components/filter_label.tsx | 90 ++++ .../configurations/constants.ts | 73 ++++ .../configurations/cpu_usage_config.ts | 42 ++ .../data/elasticsearch_fieldnames.ts | 144 +++++++ .../configurations/data/sample_attribute.ts | 74 ++++ .../data/test_index_pattern.json | 11 + .../configurations/default_configs.ts | 52 +++ .../configurations/kpi_trends_config.ts | 73 ++++ .../configurations/lens_attributes.test.ts | 387 ++++++++++++++++++ .../configurations/lens_attributes.ts | 273 ++++++++++++ .../configurations/logs_frequency_config.ts | 39 ++ .../configurations/memory_usage_config.ts | 42 ++ .../configurations/monitor_duration_config.ts | 48 +++ .../configurations/monitor_pings_config.ts | 43 ++ .../configurations/network_activity_config.ts | 41 ++ .../configurations/performance_dist_config.ts | 86 ++++ .../configurations/service_latency_config.ts | 52 +++ .../service_throughput_config.ts | 55 +++ .../configurations/url_constants.ts | 15 + .../exploratory_view/configurations/utils.ts | 54 +++ .../exploratory_view.test.tsx | 93 +++++ .../exploratory_view/exploratory_view.tsx | 87 ++++ .../exploratory_view/header/header.test.tsx | 53 +++ .../shared/exploratory_view/header/header.tsx | 63 +++ .../hooks/use_default_index_pattern.tsx | 61 +++ .../hooks/use_init_exploratory_view.ts | 44 ++ .../hooks/use_lens_attributes.ts | 88 ++++ .../hooks/use_series_filters.ts | 100 +++++ .../hooks/use_url_strorage.tsx | 103 +++++ .../shared/exploratory_view/index.tsx | 64 +++ .../shared/exploratory_view/rtl_helpers.tsx | 318 ++++++++++++++ .../columns/data_types_col.test.tsx | 59 +++ .../series_builder/columns/data_types_col.tsx | 56 +++ .../columns/report_breakdowns.test.tsx | 75 ++++ .../columns/report_breakdowns.tsx | 15 + .../columns/report_definition_col.test.tsx | 75 ++++ .../columns/report_definition_col.tsx | 95 +++++ .../columns/report_filters.test.tsx | 28 ++ .../series_builder/columns/report_filters.tsx | 22 + .../columns/report_types_col.test.tsx | 65 +++ .../columns/report_types_col.tsx | 61 +++ .../series_builder/custom_report_field.tsx | 47 +++ .../series_builder/series_builder.tsx | 201 +++++++++ .../series_date_picker/index.tsx | 55 +++ .../series_date_picker.test.tsx | 76 ++++ .../series_editor/columns/actions_col.tsx | 31 ++ .../series_editor/columns/breakdowns.test.tsx | 49 +++ .../series_editor/columns/breakdowns.tsx | 65 +++ .../columns/chart_types.test.tsx | 56 +++ .../series_editor/columns/chart_types.tsx | 149 +++++++ .../series_editor/columns/date_picker_col.tsx | 20 + .../columns/filter_expanded.test.tsx | 93 +++++ .../series_editor/columns/filter_expanded.tsx | 100 +++++ .../columns/filter_value_btn.test.tsx | 238 +++++++++++ .../columns/filter_value_btn.tsx | 117 ++++++ .../columns/metric_selection.test.tsx | 112 +++++ .../columns/metric_selection.tsx | 86 ++++ .../series_editor/columns/remove_series.tsx | 35 ++ .../series_editor/columns/series_filter.tsx | 139 +++++++ .../series_editor/selected_filters.test.tsx | 33 ++ .../series_editor/selected_filters.tsx | 96 +++++ .../series_editor/series_editor.tsx | 139 +++++++ .../shared/exploratory_view/types.ts | 89 ++++ .../field_value_selection.tsx | 19 +- .../shared/field_value_suggestions/index.tsx | 24 +- .../public/context/has_data_context.test.tsx | 9 +- .../public/context/has_data_context.tsx | 46 ++- .../public/hooks/use_breadcrumbs.ts | 71 ++++ .../public/hooks/use_quick_time_ranges.tsx | 22 + .../public/hooks/use_values_list.ts | 46 ++- x-pack/plugins/observability/public/index.ts | 1 + .../observability/public/routes/index.tsx | 21 + .../utils/observability_index_patterns.ts | 64 +++ x-pack/plugins/observability/tsconfig.json | 9 +- x-pack/plugins/uptime/public/apps/plugin.ts | 27 +- 82 files changed, 6058 insertions(+), 55 deletions(-) create mode 100644 x-pack/plugins/observability/public/assets/kibana_dashboard_dark.svg create mode 100644 x-pack/plugins/observability/public/assets/kibana_dashboard_light.svg create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/components/empty_view.tsx create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.test.tsx create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.tsx create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants.ts create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/cpu_usage_config.ts create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/data/elasticsearch_fieldnames.ts create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/data/sample_attribute.ts create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/data/test_index_pattern.json create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/default_configs.ts create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/kpi_trends_config.ts create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.test.ts create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/logs_frequency_config.ts create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/memory_usage_config.ts create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/monitor_duration_config.ts create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/monitor_pings_config.ts create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/network_activity_config.ts create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/performance_dist_config.ts create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/service_latency_config.ts create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/service_throughput_config.ts create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/url_constants.ts create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.test.tsx create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.tsx create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.test.tsx create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.tsx create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_default_index_pattern.tsx create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_init_exploratory_view.ts create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.ts create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_filters.ts create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_url_strorage.tsx create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/index.tsx create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/rtl_helpers.tsx create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.test.tsx create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.tsx create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.test.tsx create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.tsx create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.test.tsx create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.tsx create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.test.tsx create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.tsx create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.test.tsx create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.tsx create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/custom_report_field.tsx create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/series_builder.tsx create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/index.tsx create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/series_date_picker.test.tsx create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/actions_col.tsx create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.test.tsx create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.tsx create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_types.test.tsx create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_types.tsx create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/date_picker_col.tsx create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.test.tsx create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.tsx create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.test.tsx create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.tsx create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/metric_selection.test.tsx create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/metric_selection.tsx create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/remove_series.tsx create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_filter.tsx create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.test.tsx create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.tsx create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/series_editor.tsx create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts create mode 100644 x-pack/plugins/observability/public/hooks/use_breadcrumbs.ts create mode 100644 x-pack/plugins/observability/public/hooks/use_quick_time_ranges.tsx create mode 100644 x-pack/plugins/observability/public/utils/observability_index_patterns.ts diff --git a/tsconfig.json b/tsconfig.json index 03597114333ca..30944ac71fcc8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -83,6 +83,7 @@ "x-pack/plugins/uptime/server/lib/requests/helper.ts", "x-pack/plugins/uptime/public/lib/helper/rtl_helpers.tsx", "x-pack/plugins/uptime/public/lib/helper/enzyme_helpers.tsx", + "x-pack/plugins/observability/public/components/shared/exploratory_view/rtl_helpers.tsx", "x-pack/plugins/apm/server/utils/test_helpers.tsx", "x-pack/plugins/apm/public/utils/testHelpers.tsx", diff --git a/x-pack/plugins/observability/kibana.json b/x-pack/plugins/observability/kibana.json index 84aa1be9a8d87..5c47d0376581a 100644 --- a/x-pack/plugins/observability/kibana.json +++ b/x-pack/plugins/observability/kibana.json @@ -3,7 +3,7 @@ "version": "8.0.0", "kibanaVersion": "kibana", "configPath": ["xpack", "observability"], - "optionalPlugins": ["licensing", "home", "usageCollection"], + "optionalPlugins": ["licensing", "home", "usageCollection","lens"], "requiredPlugins": ["data"], "ui": true, "server": true, diff --git a/x-pack/plugins/observability/public/assets/kibana_dashboard_dark.svg b/x-pack/plugins/observability/public/assets/kibana_dashboard_dark.svg new file mode 100644 index 0000000000000..834dd98d60e4c --- /dev/null +++ b/x-pack/plugins/observability/public/assets/kibana_dashboard_dark.svg @@ -0,0 +1,116 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/x-pack/plugins/observability/public/assets/kibana_dashboard_light.svg b/x-pack/plugins/observability/public/assets/kibana_dashboard_light.svg new file mode 100644 index 0000000000000..958d25362c439 --- /dev/null +++ b/x-pack/plugins/observability/public/assets/kibana_dashboard_light.svg @@ -0,0 +1,116 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/x-pack/plugins/observability/public/components/app/header/index.tsx b/x-pack/plugins/observability/public/components/app/header/index.tsx index a41e3364d22b6..8b86e0b25379b 100644 --- a/x-pack/plugins/observability/public/components/app/header/index.tsx +++ b/x-pack/plugins/observability/public/components/app/header/index.tsx @@ -59,13 +59,13 @@ export function Header({ color, datePicker = null, restrictWidth }: Props) { - + - +

{i18n.translate('xpack.observability.home.title', { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/empty_view.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/empty_view.tsx new file mode 100644 index 0000000000000..17f1b039667d0 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/empty_view.tsx @@ -0,0 +1,32 @@ +/* + * 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 { EuiImage } from '@elastic/eui'; +import styled from 'styled-components'; +import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; + +export function EmptyView() { + const { + services: { http }, + } = useKibana(); + + return ( + + + + ); +} + +const Wrapper = styled.div` + text-align: center; + opacity: 0.4; + height: 550px; +`; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.test.tsx new file mode 100644 index 0000000000000..37597e0ce513f --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.test.tsx @@ -0,0 +1,138 @@ +/* + * 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 { fireEvent, screen, waitFor } from '@testing-library/react'; +import { mockIndexPattern, render } from '../rtl_helpers'; +import { buildFilterLabel, FilterLabel } from './filter_label'; +import * as useSeriesHook from '../hooks/use_series_filters'; + +describe('FilterLabel', function () { + const invertFilter = jest.fn(); + jest.spyOn(useSeriesHook, 'useSeriesFilters').mockReturnValue({ + invertFilter, + } as any); + + it('should render properly', async function () { + render( + + ); + + await waitFor(() => { + screen.getByText('elastic-co'); + screen.getByText(/web application:/i); + screen.getByTitle('Delete Web Application: elastic-co'); + screen.getByRole('button', { + name: /delete web application: elastic-co/i, + }); + }); + }); + + it.skip('should delete filter', async function () { + const removeFilter = jest.fn(); + render( + + ); + + await waitFor(() => { + fireEvent.click(screen.getByLabelText('Filter actions')); + }); + + fireEvent.click(screen.getByTestId('deleteFilter')); + expect(removeFilter).toHaveBeenCalledTimes(1); + expect(removeFilter).toHaveBeenCalledWith('service.name', 'elastic-co', false); + }); + + it.skip('should invert filter', async function () { + const removeFilter = jest.fn(); + render( + + ); + + await waitFor(() => { + fireEvent.click(screen.getByLabelText('Filter actions')); + }); + + fireEvent.click(screen.getByTestId('negateFilter')); + expect(invertFilter).toHaveBeenCalledTimes(1); + expect(invertFilter).toHaveBeenCalledWith({ + field: 'service.name', + negate: false, + value: 'elastic-co', + }); + }); + + it('should display invert filter', async function () { + render( + + ); + + await waitFor(() => { + screen.getByText('elastic-co'); + screen.getByText(/web application:/i); + screen.getByTitle('Delete NOT Web Application: elastic-co'); + screen.getByRole('button', { + name: /delete not web application: elastic-co/i, + }); + }); + }); + + it('should build filter meta', function () { + expect( + buildFilterLabel({ + field: 'user_agent.name', + label: 'Browser family', + indexPattern: mockIndexPattern, + value: 'Firefox', + negate: false, + }) + ).toEqual({ + meta: { + alias: null, + disabled: false, + index: 'apm-*', + key: 'Browser family', + negate: false, + type: 'phrase', + value: 'Firefox', + }, + query: { + match_phrase: { + 'user_agent.name': 'Firefox', + }, + }, + }); + }); +}); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.tsx new file mode 100644 index 0000000000000..3d6dc5b3f2bf5 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.tsx @@ -0,0 +1,90 @@ +/* + * 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 { injectI18n } from '@kbn/i18n/react'; +import { esFilters, Filter, IndexPattern } from '../../../../../../../../src/plugins/data/public'; +import { useIndexPatternContext } from '../hooks/use_default_index_pattern'; +import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; +import { useSeriesFilters } from '../hooks/use_series_filters'; + +interface Props { + field: string; + label: string; + value: string; + seriesId: string; + negate: boolean; + definitionFilter?: boolean; + removeFilter: (field: string, value: string, notVal: boolean) => void; +} +export function buildFilterLabel({ + field, + value, + label, + indexPattern, + negate, +}: { + label: string; + value: string; + negate: boolean; + field: string; + indexPattern: IndexPattern; +}) { + const indexField = indexPattern.getFieldByName(field)!; + + const filter = esFilters.buildPhraseFilter(indexField, value, indexPattern); + + filter.meta.value = value; + filter.meta.key = label; + filter.meta.alias = null; + filter.meta.negate = negate; + filter.meta.disabled = false; + filter.meta.type = 'phrase'; + + return filter; +} +export function FilterLabel({ + label, + seriesId, + field, + value, + negate, + removeFilter, + definitionFilter, +}: Props) { + const FilterItem = injectI18n(esFilters.FilterItem); + + const { indexPattern } = useIndexPatternContext(); + + const filter = buildFilterLabel({ field, value, label, indexPattern, negate }); + + const { invertFilter } = useSeriesFilters({ seriesId }); + + const { + services: { uiSettings }, + } = useKibana(); + + return indexPattern ? ( + { + removeFilter(field, value, false); + }} + onUpdate={(filterN: Filter) => { + if (definitionFilter) { + // FIXME handle this use case + } else if (filterN.meta.negate !== negate) { + invertFilter({ field, value, negate }); + } + }} + uiSettings={uiSettings!} + hiddenPanelOptions={['pinFilter', 'editFilter', 'disableFilter']} + /> + ) : null; +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants.ts new file mode 100644 index 0000000000000..aa3ac2fa64317 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants.ts @@ -0,0 +1,73 @@ +/* + * 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 { AppDataType, ReportViewTypeId } from '../types'; +import { + CLS_FIELD, + FCP_FIELD, + FID_FIELD, + LCP_FIELD, + TBT_FIELD, +} from './data/elasticsearch_fieldnames'; + +export const FieldLabels: Record = { + 'user_agent.name': 'Browser family', + 'user_agent.version': 'Browser version', + 'user_agent.os.name': 'Operating system', + 'client.geo.country_name': 'Location', + 'user_agent.device.name': 'Device', + 'observer.geo.name': 'Observer location', + 'service.name': 'Service Name', + 'service.environment': 'Environment', + + [LCP_FIELD]: 'Largest contentful paint', + [FCP_FIELD]: 'First contentful paint', + [TBT_FIELD]: 'Total blocking time', + [FID_FIELD]: 'First input delay', + [CLS_FIELD]: 'Cumulative layout shift', + + 'monitor.id': 'Monitor Id', + 'monitor.status': 'Monitor Status', + + 'agent.hostname': 'Agent host', + 'host.hostname': 'Host name', + 'monitor.name': 'Monitor name', + 'monitor.type': 'Monitor Type', + 'url.port': 'Port', + tags: 'Tags', + + // custom + + 'performance.metric': 'Metric', + 'Business.KPI': 'KPI', +}; + +export const DataViewLabels: Record = { + pld: 'Performance Distribution', + upd: 'Uptime monitor duration', + upp: 'Uptime pings', + svl: 'APM Service latency', + kpi: 'KPI over time', + tpt: 'APM Service throughput', + cpu: 'System CPU Usage', + logs: 'Logs Frequency', + mem: 'System Memory Usage', + nwk: 'Network Activity', +}; + +export const ReportToDataTypeMap: Record = { + upd: 'synthetics', + upp: 'synthetics', + tpt: 'apm', + svl: 'apm', + kpi: 'rum', + pld: 'rum', + nwk: 'metrics', + mem: 'metrics', + logs: 'logs', + cpu: 'metrics', +}; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/cpu_usage_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/cpu_usage_config.ts new file mode 100644 index 0000000000000..5a4fb2aa3a6a5 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/cpu_usage_config.ts @@ -0,0 +1,42 @@ +/* + * 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 { DataSeries } from '../types'; +import { FieldLabels } from './constants'; +import { OperationType } from '../../../../../../lens/public'; + +interface Props { + seriesId: string; +} + +export function getCPUUsageLensConfig({ seriesId }: Props): DataSeries { + return { + id: seriesId, + reportType: 'cpu-usage', + defaultSeriesType: 'line', + seriesTypes: ['line', 'bar'], + xAxisColumn: { + sourceField: '@timestamp', + }, + yAxisColumn: { + operationType: 'avg' as OperationType, + sourceField: 'system.cpu.user.pct', + label: 'CPU Usage %', + }, + hasMetricType: true, + defaultFilters: [], + breakdowns: ['host.hostname'], + filters: [], + labels: { ...FieldLabels, 'host.hostname': 'Host name' }, + reportDefinitions: [ + { + field: 'agent.hostname', + required: true, + }, + ], + }; +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/data/elasticsearch_fieldnames.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/data/elasticsearch_fieldnames.ts new file mode 100644 index 0000000000000..3faf54fff3140 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/data/elasticsearch_fieldnames.ts @@ -0,0 +1,144 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const CLOUD = 'cloud'; +export const CLOUD_AVAILABILITY_ZONE = 'cloud.availability_zone'; +export const CLOUD_PROVIDER = 'cloud.provider'; +export const CLOUD_REGION = 'cloud.region'; +export const CLOUD_MACHINE_TYPE = 'cloud.machine.type'; + +export const SERVICE = 'service'; +export const SERVICE_NAME = 'service.name'; +export const SERVICE_ENVIRONMENT = 'service.environment'; +export const SERVICE_FRAMEWORK_NAME = 'service.framework.name'; +export const SERVICE_FRAMEWORK_VERSION = 'service.framework.version'; +export const SERVICE_LANGUAGE_NAME = 'service.language.name'; +export const SERVICE_LANGUAGE_VERSION = 'service.language.version'; +export const SERVICE_RUNTIME_NAME = 'service.runtime.name'; +export const SERVICE_RUNTIME_VERSION = 'service.runtime.version'; +export const SERVICE_NODE_NAME = 'service.node.name'; +export const SERVICE_VERSION = 'service.version'; + +export const AGENT = 'agent'; +export const AGENT_NAME = 'agent.name'; +export const AGENT_VERSION = 'agent.version'; + +export const URL_FULL = 'url.full'; +export const HTTP_REQUEST_METHOD = 'http.request.method'; +export const HTTP_RESPONSE_STATUS_CODE = 'http.response.status_code'; +export const USER_ID = 'user.id'; +export const USER_AGENT_ORIGINAL = 'user_agent.original'; +export const USER_AGENT_NAME = 'user_agent.name'; +export const USER_AGENT_VERSION = 'user_agent.version'; + +export const DESTINATION_ADDRESS = 'destination.address'; + +export const OBSERVER_HOSTNAME = 'observer.hostname'; +export const OBSERVER_VERSION_MAJOR = 'observer.version_major'; +export const OBSERVER_LISTENING = 'observer.listening'; +export const PROCESSOR_EVENT = 'processor.event'; + +export const TRANSACTION_DURATION = 'transaction.duration.us'; +export const TRANSACTION_DURATION_HISTOGRAM = 'transaction.duration.histogram'; +export const TRANSACTION_TYPE = 'transaction.type'; +export const TRANSACTION_RESULT = 'transaction.result'; +export const TRANSACTION_NAME = 'transaction.name'; +export const TRANSACTION_ID = 'transaction.id'; +export const TRANSACTION_SAMPLED = 'transaction.sampled'; +export const TRANSACTION_BREAKDOWN_COUNT = 'transaction.breakdown.count'; +export const TRANSACTION_PAGE_URL = 'transaction.page.url'; +// for transaction metrics +export const TRANSACTION_ROOT = 'transaction.root'; + +export const EVENT_OUTCOME = 'event.outcome'; + +export const TRACE_ID = 'trace.id'; + +export const SPAN_DURATION = 'span.duration.us'; +export const SPAN_TYPE = 'span.type'; +export const SPAN_SUBTYPE = 'span.subtype'; +export const SPAN_SELF_TIME_SUM = 'span.self_time.sum.us'; +export const SPAN_ACTION = 'span.action'; +export const SPAN_NAME = 'span.name'; +export const SPAN_ID = 'span.id'; +export const SPAN_DESTINATION_SERVICE_RESOURCE = 'span.destination.service.resource'; +export const SPAN_DESTINATION_SERVICE_RESPONSE_TIME_COUNT = + 'span.destination.service.response_time.count'; + +export const SPAN_DESTINATION_SERVICE_RESPONSE_TIME_SUM = + 'span.destination.service.response_time.sum.us'; + +// Parent ID for a transaction or span +export const PARENT_ID = 'parent.id'; + +export const ERROR_GROUP_ID = 'error.grouping_key'; +export const ERROR_CULPRIT = 'error.culprit'; +export const ERROR_LOG_LEVEL = 'error.log.level'; +export const ERROR_LOG_MESSAGE = 'error.log.message'; +export const ERROR_EXC_MESSAGE = 'error.exception.message'; // only to be used in es queries, since error.exception is now an array +export const ERROR_EXC_HANDLED = 'error.exception.handled'; // only to be used in es queries, since error.exception is now an array +export const ERROR_EXC_TYPE = 'error.exception.type'; +export const ERROR_PAGE_URL = 'error.page.url'; + +// METRICS +export const METRIC_SYSTEM_FREE_MEMORY = 'system.memory.actual.free'; +export const METRIC_SYSTEM_TOTAL_MEMORY = 'system.memory.total'; +export const METRIC_SYSTEM_CPU_PERCENT = 'system.cpu.total.norm.pct'; +export const METRIC_PROCESS_CPU_PERCENT = 'system.process.cpu.total.norm.pct'; +export const METRIC_CGROUP_MEMORY_LIMIT_BYTES = 'system.process.cgroup.memory.mem.limit.bytes'; +export const METRIC_CGROUP_MEMORY_USAGE_BYTES = 'system.process.cgroup.memory.mem.usage.bytes'; + +export const METRIC_JAVA_HEAP_MEMORY_MAX = 'jvm.memory.heap.max'; +export const METRIC_JAVA_HEAP_MEMORY_COMMITTED = 'jvm.memory.heap.committed'; +export const METRIC_JAVA_HEAP_MEMORY_USED = 'jvm.memory.heap.used'; +export const METRIC_JAVA_NON_HEAP_MEMORY_MAX = 'jvm.memory.non_heap.max'; +export const METRIC_JAVA_NON_HEAP_MEMORY_COMMITTED = 'jvm.memory.non_heap.committed'; +export const METRIC_JAVA_NON_HEAP_MEMORY_USED = 'jvm.memory.non_heap.used'; +export const METRIC_JAVA_THREAD_COUNT = 'jvm.thread.count'; +export const METRIC_JAVA_GC_COUNT = 'jvm.gc.count'; +export const METRIC_JAVA_GC_TIME = 'jvm.gc.time'; + +export const LABEL_NAME = 'labels.name'; + +export const HOST = 'host'; +export const HOST_NAME = 'host.hostname'; +export const HOST_OS_PLATFORM = 'host.os.platform'; +export const CONTAINER_ID = 'container.id'; +export const KUBERNETES = 'kubernetes'; +export const POD_NAME = 'kubernetes.pod.name'; + +export const CLIENT_GEO_COUNTRY_ISO_CODE = 'client.geo.country_iso_code'; +export const CLIENT_GEO_COUNTRY_NAME = 'client.geo.country_name'; + +// RUM Labels +export const TRANSACTION_URL = 'url.full'; +export const CLIENT_GEO = 'client.geo'; +export const USER_AGENT_DEVICE = 'user_agent.device.name'; +export const USER_AGENT_OS = 'user_agent.os.name'; + +export const TRANSACTION_TIME_TO_FIRST_BYTE = 'transaction.marks.agent.timeToFirstByte'; +export const TRANSACTION_DOM_INTERACTIVE = 'transaction.marks.agent.domInteractive'; + +export const FCP_FIELD = 'transaction.marks.agent.firstContentfulPaint'; +export const LCP_FIELD = 'transaction.marks.agent.largestContentfulPaint'; +export const TBT_FIELD = 'transaction.experience.tbt'; +export const FID_FIELD = 'transaction.experience.fid'; +export const CLS_FIELD = 'transaction.experience.cls'; + +export const PROFILE_ID = 'profile.id'; +export const PROFILE_DURATION = 'profile.duration'; +export const PROFILE_TOP_ID = 'profile.top.id'; +export const PROFILE_STACK = 'profile.stack'; + +export const PROFILE_SAMPLES_COUNT = 'profile.samples.count'; +export const PROFILE_CPU_NS = 'profile.cpu.ns'; +export const PROFILE_WALL_US = 'profile.wall.us'; + +export const PROFILE_ALLOC_OBJECTS = 'profile.alloc_objects.count'; +export const PROFILE_ALLOC_SPACE = 'profile.alloc_space.bytes'; +export const PROFILE_INUSE_OBJECTS = 'profile.inuse_objects.count'; +export const PROFILE_INUSE_SPACE = 'profile.inuse_space.bytes'; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/data/sample_attribute.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/data/sample_attribute.ts new file mode 100644 index 0000000000000..9b299e7d70bcc --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/data/sample_attribute.ts @@ -0,0 +1,74 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +export const sampleAttribute = { + title: 'Prefilled from exploratory view app', + description: '', + visualizationType: 'lnsXY', + references: [ + { id: 'apm-*', name: 'indexpattern-datasource-current-indexpattern', type: 'index-pattern' }, + { id: 'apm-*', name: 'indexpattern-datasource-layer-layer1', type: 'index-pattern' }, + ], + state: { + datasourceStates: { + indexpattern: { + layers: { + layer1: { + columnOrder: ['x-axis-column', 'y-axis-column'], + columns: { + 'x-axis-column': { + sourceField: 'transaction.duration.us', + label: 'Page load time', + dataType: 'number', + operationType: 'range', + isBucketed: true, + scale: 'interval', + params: { + type: 'histogram', + ranges: [{ from: 0, to: 1000, label: '' }], + maxBars: 'auto', + }, + }, + 'y-axis-column': { + dataType: 'number', + isBucketed: false, + label: 'Pages loaded', + operationType: 'count', + scale: 'ratio', + sourceField: 'Records', + }, + }, + incompleteColumns: {}, + }, + }, + }, + }, + visualization: { + legend: { isVisible: true, position: 'right' }, + valueLabels: 'hide', + fittingFunction: 'Linear', + curveType: 'CURVE_MONOTONE_X', + axisTitlesVisibilitySettings: { x: true, yLeft: true, yRight: true }, + tickLabelsVisibilitySettings: { x: true, yLeft: true, yRight: true }, + gridlinesVisibilitySettings: { x: true, yLeft: true, yRight: true }, + preferredSeriesType: 'line', + layers: [ + { + accessors: ['y-axis-column'], + layerId: 'layer1', + seriesType: 'line', + yConfig: [{ forAccessor: 'y-axis-column', color: 'green' }], + xAccessor: 'x-axis-column', + }, + ], + }, + query: { query: '', language: 'kuery' }, + filters: [ + { meta: { index: 'apm-*' }, query: { match_phrase: { 'transaction.type': 'page-load' } } }, + { meta: { index: 'apm-*' }, query: { match_phrase: { 'processor.event': 'transaction' } } }, + ], + }, +}; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/data/test_index_pattern.json b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/data/test_index_pattern.json new file mode 100644 index 0000000000000..31fec1fe8d4f4 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/data/test_index_pattern.json @@ -0,0 +1,11 @@ +{ + "attributes": { + "fieldFormatMap": "{\"client.bytes\":{\"id\":\"bytes\"},\"client.nat.port\":{\"id\":\"string\"},\"client.port\":{\"id\":\"string\"},\"destination.bytes\":{\"id\":\"bytes\"},\"destination.nat.port\":{\"id\":\"string\"},\"destination.port\":{\"id\":\"string\"},\"event.duration\":{\"id\":\"duration\",\"params\":{\"inputFormat\":\"nanoseconds\",\"outputFormat\":\"asMilliseconds\",\"outputPrecision\":1}},\"event.sequence\":{\"id\":\"string\"},\"event.severity\":{\"id\":\"string\"},\"http.request.body.bytes\":{\"id\":\"bytes\"},\"http.request.bytes\":{\"id\":\"bytes\"},\"http.response.body.bytes\":{\"id\":\"bytes\"},\"http.response.bytes\":{\"id\":\"bytes\"},\"http.response.status_code\":{\"id\":\"string\"},\"log.syslog.facility.code\":{\"id\":\"string\"},\"log.syslog.priority\":{\"id\":\"string\"},\"network.bytes\":{\"id\":\"bytes\"},\"package.size\":{\"id\":\"string\"},\"process.parent.pgid\":{\"id\":\"string\"},\"process.parent.pid\":{\"id\":\"string\"},\"process.parent.ppid\":{\"id\":\"string\"},\"process.parent.thread.id\":{\"id\":\"string\"},\"process.pgid\":{\"id\":\"string\"},\"process.pid\":{\"id\":\"string\"},\"process.ppid\":{\"id\":\"string\"},\"process.thread.id\":{\"id\":\"string\"},\"server.bytes\":{\"id\":\"bytes\"},\"server.nat.port\":{\"id\":\"string\"},\"server.port\":{\"id\":\"string\"},\"source.bytes\":{\"id\":\"bytes\"},\"source.nat.port\":{\"id\":\"string\"},\"source.port\":{\"id\":\"string\"},\"system.cpu.total.norm.pct\":{\"id\":\"percent\"},\"system.memory.actual.free\":{\"id\":\"bytes\"},\"system.memory.total\":{\"id\":\"bytes\"},\"system.process.cgroup.memory.mem.limit.bytes\":{\"id\":\"bytes\"},\"system.process.cgroup.memory.mem.usage.bytes\":{\"id\":\"bytes\"},\"system.process.cpu.total.norm.pct\":{\"id\":\"percent\"},\"system.process.memory.rss.bytes\":{\"id\":\"bytes\"},\"system.process.memory.size\":{\"id\":\"bytes\"},\"url.port\":{\"id\":\"string\"}}", + "fields": "[{\"name\":\"@timestamp\",\"type\":\"date\",\"esTypes\":[\"date\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"_id\",\"type\":\"string\",\"esTypes\":[\"_id\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"esTypes\":[\"_index\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"esTypes\":[\"_source\"],\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"agent.build.original\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"agent.ephemeral_id\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"agent.hostname\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"agent.id\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"agent.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"agent.type\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"agent.version\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"as.number\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"as.organization.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"as.organization.name.text\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false,\"subType\":{\"multi\":{\"parent\":\"as.organization.name\"}}},{\"name\":\"child.id\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"client.address\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"client.as.number\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"client.as.organization.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"client.as.organization.name.text\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false,\"subType\":{\"multi\":{\"parent\":\"client.as.organization.name\"}}},{\"name\":\"client.bytes\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"client.domain\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"client.geo.city_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"client.geo.continent_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"client.geo.country_iso_code\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"client.geo.country_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"client.geo.location\",\"type\":\"geo_point\",\"esTypes\":[\"geo_point\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"client.geo.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"client.geo.region_iso_code\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"client.geo.region_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"client.ip\",\"type\":\"ip\",\"esTypes\":[\"ip\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"client.mac\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"client.nat.ip\",\"type\":\"ip\",\"esTypes\":[\"ip\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"client.nat.port\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"client.packets\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"client.port\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"client.registered_domain\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"client.subdomain\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"client.top_level_domain\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"client.user.domain\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"client.user.email\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"client.user.full_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"client.user.full_name.text\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false,\"subType\":{\"multi\":{\"parent\":\"client.user.full_name\"}}},{\"name\":\"client.user.group.domain\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"client.user.group.id\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"client.user.group.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"client.user.hash\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"client.user.id\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"client.user.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"client.user.name.text\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false,\"subType\":{\"multi\":{\"parent\":\"client.user.name\"}}},{\"name\":\"client.user.roles\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"cloud.account.id\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"cloud.account.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"cloud.availability_zone\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"cloud.image.id\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"cloud.instance.id\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"cloud.instance.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"cloud.machine.type\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"cloud.project.id\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"cloud.project.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"cloud.provider\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"cloud.region\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"cloud.service.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"clr.gc.count\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"clr.gc.gen0size\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"clr.gc.gen1size\",\"type\":\"number\",\"esTypes\":[\"float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"clr.gc.gen2size\",\"type\":\"number\",\"esTypes\":[\"float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"clr.gc.gen3size\",\"type\":\"number\",\"esTypes\":[\"float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"code_signature.exists\",\"type\":\"boolean\",\"esTypes\":[\"boolean\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"code_signature.status\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"code_signature.subject_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"code_signature.trusted\",\"type\":\"boolean\",\"esTypes\":[\"boolean\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"code_signature.valid\",\"type\":\"boolean\",\"esTypes\":[\"boolean\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"container.id\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"container.image.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"container.image.tag\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"container.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"container.runtime\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"destination.address\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"destination.as.number\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"destination.as.organization.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"destination.as.organization.name.text\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false,\"subType\":{\"multi\":{\"parent\":\"destination.as.organization.name\"}}},{\"name\":\"destination.bytes\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"destination.domain\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"destination.geo.city_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"destination.geo.continent_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"destination.geo.country_iso_code\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"destination.geo.country_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"destination.geo.location\",\"type\":\"geo_point\",\"esTypes\":[\"geo_point\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"destination.geo.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"destination.geo.region_iso_code\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"destination.geo.region_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"destination.ip\",\"type\":\"ip\",\"esTypes\":[\"ip\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"destination.mac\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"destination.nat.ip\",\"type\":\"ip\",\"esTypes\":[\"ip\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"destination.nat.port\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"destination.packets\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"destination.port\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"destination.registered_domain\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"destination.subdomain\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"destination.top_level_domain\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"destination.user.domain\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"destination.user.email\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"destination.user.full_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"destination.user.full_name.text\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false,\"subType\":{\"multi\":{\"parent\":\"destination.user.full_name\"}}},{\"name\":\"destination.user.group.domain\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"destination.user.group.id\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"destination.user.group.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"destination.user.hash\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"destination.user.id\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"destination.user.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"destination.user.name.text\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false,\"subType\":{\"multi\":{\"parent\":\"destination.user.name\"}}},{\"name\":\"destination.user.roles\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"dll.code_signature.exists\",\"type\":\"boolean\",\"esTypes\":[\"boolean\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"dll.code_signature.status\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"dll.code_signature.subject_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"dll.code_signature.trusted\",\"type\":\"boolean\",\"esTypes\":[\"boolean\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"dll.code_signature.valid\",\"type\":\"boolean\",\"esTypes\":[\"boolean\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"dll.hash.md5\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"dll.hash.sha1\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"dll.hash.sha256\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"dll.hash.sha512\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"dll.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"dll.path\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"dll.pe.architecture\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"dll.pe.company\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"dll.pe.description\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"dll.pe.file_version\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"dll.pe.imphash\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"dll.pe.original_file_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"dll.pe.product\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"dns.answers.class\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"dns.answers.data\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"dns.answers.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"dns.answers.ttl\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"dns.answers.type\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"dns.header_flags\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"dns.id\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"dns.op_code\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"dns.question.class\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"dns.question.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"dns.question.registered_domain\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"dns.question.subdomain\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"dns.question.top_level_domain\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"dns.question.type\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"dns.resolved_ip\",\"type\":\"ip\",\"esTypes\":[\"ip\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"dns.response_code\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"dns.type\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"ecs.version\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"error.code\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"error.culprit\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"error.exception.code\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"error.exception.handled\",\"type\":\"boolean\",\"esTypes\":[\"boolean\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"error.exception.message\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"error.exception.module\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"error.exception.type\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"error.grouping_key\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"error.id\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"error.log.level\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"error.log.logger_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"error.log.message\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"error.log.param_message\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"error.message\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"error.stack_trace\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":false,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"error.stack_trace.text\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false,\"subType\":{\"multi\":{\"parent\":\"error.stack_trace\"}}},{\"name\":\"error.type\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"event.action\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"event.category\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"event.code\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"event.created\",\"type\":\"date\",\"esTypes\":[\"date\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"event.dataset\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"event.duration\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"event.end\",\"type\":\"date\",\"esTypes\":[\"date\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"event.hash\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"event.id\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"event.ingested\",\"type\":\"date\",\"esTypes\":[\"date\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"event.kind\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"event.module\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"event.original\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":false,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"event.outcome\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"event.provider\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"event.reason\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"event.reference\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"event.risk_score\",\"type\":\"number\",\"esTypes\":[\"float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"event.risk_score_norm\",\"type\":\"number\",\"esTypes\":[\"float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"event.sequence\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"event.severity\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"event.start\",\"type\":\"date\",\"esTypes\":[\"date\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"event.timezone\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"event.type\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"event.url\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.accessed\",\"type\":\"date\",\"esTypes\":[\"date\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.attributes\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.code_signature.exists\",\"type\":\"boolean\",\"esTypes\":[\"boolean\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.code_signature.status\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.code_signature.subject_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.code_signature.trusted\",\"type\":\"boolean\",\"esTypes\":[\"boolean\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.code_signature.valid\",\"type\":\"boolean\",\"esTypes\":[\"boolean\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.created\",\"type\":\"date\",\"esTypes\":[\"date\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.ctime\",\"type\":\"date\",\"esTypes\":[\"date\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.device\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.directory\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.drive_letter\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.extension\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.gid\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.group\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.hash.md5\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.hash.sha1\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.hash.sha256\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.hash.sha512\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.inode\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.mime_type\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.mode\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.mtime\",\"type\":\"date\",\"esTypes\":[\"date\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.owner\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.path\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.path.text\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false,\"subType\":{\"multi\":{\"parent\":\"file.path\"}}},{\"name\":\"file.pe.architecture\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.pe.company\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.pe.description\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.pe.file_version\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.pe.imphash\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.pe.original_file_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.pe.product\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.size\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.target_path\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.target_path.text\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false,\"subType\":{\"multi\":{\"parent\":\"file.target_path\"}}},{\"name\":\"file.type\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.uid\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.x509.alternative_names\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.x509.issuer.common_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.x509.issuer.country\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.x509.issuer.distinguished_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.x509.issuer.locality\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.x509.issuer.organization\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.x509.issuer.organizational_unit\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.x509.issuer.state_or_province\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.x509.not_after\",\"type\":\"date\",\"esTypes\":[\"date\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.x509.not_before\",\"type\":\"date\",\"esTypes\":[\"date\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.x509.public_key_algorithm\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.x509.public_key_curve\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.x509.public_key_exponent\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":false,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.x509.public_key_size\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.x509.serial_number\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.x509.signature_algorithm\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.x509.subject.common_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.x509.subject.country\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.x509.subject.distinguished_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.x509.subject.locality\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.x509.subject.organization\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.x509.subject.organizational_unit\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.x509.subject.state_or_province\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.x509.version_number\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.city_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.continent_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.country_iso_code\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.country_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.location\",\"type\":\"geo_point\",\"esTypes\":[\"geo_point\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.region_iso_code\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.region_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"golang.goroutines\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"golang.heap.allocations.active\",\"type\":\"number\",\"esTypes\":[\"float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"golang.heap.allocations.allocated\",\"type\":\"number\",\"esTypes\":[\"float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"golang.heap.allocations.frees\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"golang.heap.allocations.idle\",\"type\":\"number\",\"esTypes\":[\"float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"golang.heap.allocations.mallocs\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"golang.heap.allocations.objects\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"golang.heap.allocations.total\",\"type\":\"number\",\"esTypes\":[\"float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"golang.heap.gc.cpu_fraction\",\"type\":\"number\",\"esTypes\":[\"float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"golang.heap.gc.next_gc_limit\",\"type\":\"number\",\"esTypes\":[\"float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"golang.heap.gc.total_count\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"golang.heap.gc.total_pause.ns\",\"type\":\"number\",\"esTypes\":[\"float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"golang.heap.system.obtained\",\"type\":\"number\",\"esTypes\":[\"float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"golang.heap.system.released\",\"type\":\"number\",\"esTypes\":[\"float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"golang.heap.system.stack\",\"type\":\"number\",\"esTypes\":[\"float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"golang.heap.system.total\",\"type\":\"number\",\"esTypes\":[\"float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"group.domain\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"group.id\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"group.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"hash.md5\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"hash.sha1\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"hash.sha256\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"hash.sha512\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.architecture\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.containerized\",\"type\":\"boolean\",\"esTypes\":[\"boolean\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.domain\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.geo.city_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.geo.continent_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.geo.country_iso_code\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.geo.country_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.geo.location\",\"type\":\"geo_point\",\"esTypes\":[\"geo_point\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.geo.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.geo.region_iso_code\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.geo.region_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.hostname\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.id\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.ip\",\"type\":\"ip\",\"esTypes\":[\"ip\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.mac\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.os.build\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.os.codename\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.os.family\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.os.full\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.os.full.text\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false,\"subType\":{\"multi\":{\"parent\":\"host.os.full\"}}},{\"name\":\"host.os.kernel\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.os.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.os.name.text\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false,\"subType\":{\"multi\":{\"parent\":\"host.os.name\"}}},{\"name\":\"host.os.platform\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.os.version\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.type\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.uptime\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.user.domain\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.user.email\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.user.full_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.user.full_name.text\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false,\"subType\":{\"multi\":{\"parent\":\"host.user.full_name\"}}},{\"name\":\"host.user.group.domain\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.user.group.id\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.user.group.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.user.hash\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.user.id\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.user.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.user.name.text\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false,\"subType\":{\"multi\":{\"parent\":\"host.user.name\"}}},{\"name\":\"host.user.roles\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"http.request.body.bytes\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"http.request.body.content\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"http.request.body.content.text\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false,\"subType\":{\"multi\":{\"parent\":\"http.request.body.content\"}}},{\"name\":\"http.request.bytes\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"http.request.method\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"http.request.mime_type\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"http.request.referrer\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"http.response.body.bytes\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"http.response.body.content\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"http.response.body.content.text\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false,\"subType\":{\"multi\":{\"parent\":\"http.response.body.content\"}}},{\"name\":\"http.response.bytes\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"http.response.finished\",\"type\":\"boolean\",\"esTypes\":[\"boolean\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"http.response.mime_type\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"http.response.status_code\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"http.version\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"interface.alias\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"interface.id\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"interface.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"jvm.gc.alloc\",\"type\":\"number\",\"esTypes\":[\"float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"jvm.gc.count\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"jvm.gc.time\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"jvm.memory.heap.committed\",\"type\":\"number\",\"esTypes\":[\"float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"jvm.memory.heap.max\",\"type\":\"number\",\"esTypes\":[\"float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"jvm.memory.heap.pool.committed\",\"type\":\"number\",\"esTypes\":[\"float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"jvm.memory.heap.pool.max\",\"type\":\"number\",\"esTypes\":[\"float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"jvm.memory.heap.pool.used\",\"type\":\"number\",\"esTypes\":[\"float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"jvm.memory.heap.used\",\"type\":\"number\",\"esTypes\":[\"float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"jvm.memory.non_heap.committed\",\"type\":\"number\",\"esTypes\":[\"float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"jvm.memory.non_heap.max\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"jvm.memory.non_heap.used\",\"type\":\"number\",\"esTypes\":[\"float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"jvm.thread.count\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"kubernetes.container.image\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"kubernetes.container.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"kubernetes.deployment.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"kubernetes.namespace\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"kubernetes.node.hostname\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"kubernetes.node.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"kubernetes.pod.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"kubernetes.pod.uid\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"kubernetes.replicaset.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"kubernetes.statefulset.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"labels.city\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"labels.company\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"labels.country_code\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"labels.customer_email\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"labels.customer_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"labels.customer_tier\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"labels.env\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"labels.events_encoded\",\"type\":\"number\",\"esTypes\":[\"scaled_float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"labels.events_failed\",\"type\":\"number\",\"esTypes\":[\"scaled_float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"labels.events_original\",\"type\":\"number\",\"esTypes\":[\"scaled_float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"labels.events_published\",\"type\":\"number\",\"esTypes\":[\"scaled_float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"labels.foo\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"labels.git_rev\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"labels.hostname\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"labels.in_eu\",\"type\":\"boolean\",\"esTypes\":[\"boolean\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"labels.ip\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"labels.kibana_uuid\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"labels.lang\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"labels.lorem\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"labels.multi-line\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"labels.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"labels.plugin\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"labels.productId\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"labels.request_id\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"labels.served_from_cache\",\"type\":\"conflict\",\"esTypes\":[\"boolean\",\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false,\"conflictDescriptions\":{\"boolean\":[\"apm-8.0.0-transaction-000001\"],\"keyword\":[\"apm-8.0.0-transaction-000002\"]}},{\"name\":\"labels.taskType\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"labels.this-is-a-very-long-tag-name-without-any-spaces\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"labels.u\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"labels.worker\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"log.file.path\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"log.level\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"log.logger\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"log.origin.file.line\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"log.origin.file.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"log.origin.function\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"log.original\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":false,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"log.syslog.facility.code\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"log.syslog.facility.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"log.syslog.priority\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"log.syslog.severity.code\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"log.syslog.severity.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"message\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"metricset.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"metricset.period\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"network.application\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"network.bytes\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"network.community_id\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"network.direction\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"network.forwarded_ip\",\"type\":\"ip\",\"esTypes\":[\"ip\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"network.iana_number\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"network.inner.vlan.id\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"network.inner.vlan.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"network.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"network.packets\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"network.protocol\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"network.transport\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"network.type\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"network.vlan.id\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"network.vlan.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"nodejs.eventloop.delay.avg.ms\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"nodejs.eventloop.delay.ns\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"nodejs.handles.active\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"nodejs.memory.arrayBuffers.bytes\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"nodejs.memory.external.bytes\",\"type\":\"number\",\"esTypes\":[\"float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"nodejs.memory.heap.allocated.bytes\",\"type\":\"number\",\"esTypes\":[\"float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"nodejs.memory.heap.used.bytes\",\"type\":\"number\",\"esTypes\":[\"float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"nodejs.requests.active\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"observer.egress.interface.alias\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"observer.egress.interface.id\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"observer.egress.interface.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"observer.egress.vlan.id\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"observer.egress.vlan.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"observer.egress.zone\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"observer.geo.city_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"observer.geo.continent_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"observer.geo.country_iso_code\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"observer.geo.country_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"observer.geo.location\",\"type\":\"geo_point\",\"esTypes\":[\"geo_point\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"observer.geo.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"observer.geo.region_iso_code\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"observer.geo.region_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"observer.hostname\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"observer.ingress.interface.alias\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"observer.ingress.interface.id\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"observer.ingress.interface.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"observer.ingress.vlan.id\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"observer.ingress.vlan.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"observer.ingress.zone\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"observer.ip\",\"type\":\"ip\",\"esTypes\":[\"ip\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"observer.listening\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"observer.mac\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"observer.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"observer.os.family\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"observer.os.full\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"observer.os.full.text\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false,\"subType\":{\"multi\":{\"parent\":\"observer.os.full\"}}},{\"name\":\"observer.os.kernel\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"observer.os.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"observer.os.name.text\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false,\"subType\":{\"multi\":{\"parent\":\"observer.os.name\"}}},{\"name\":\"observer.os.platform\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"observer.os.version\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"observer.product\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"observer.serial_number\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"observer.type\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"observer.vendor\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"observer.version\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"observer.version_major\",\"type\":\"number\",\"esTypes\":[\"byte\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"organization.id\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"organization.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"organization.name.text\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false,\"subType\":{\"multi\":{\"parent\":\"organization.name\"}}},{\"name\":\"os.family\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"os.full\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"os.full.text\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false,\"subType\":{\"multi\":{\"parent\":\"os.full\"}}},{\"name\":\"os.kernel\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"os.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"os.name.text\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false,\"subType\":{\"multi\":{\"parent\":\"os.name\"}}},{\"name\":\"os.platform\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"os.version\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"package.architecture\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"package.build_version\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"package.checksum\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"package.description\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"package.install_scope\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"package.installed\",\"type\":\"date\",\"esTypes\":[\"date\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"package.license\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"package.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"package.path\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"package.reference\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"package.size\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"package.type\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"package.version\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"parent.id\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"pe.architecture\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"pe.company\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"pe.description\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"pe.file_version\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"pe.imphash\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"pe.original_file_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"pe.product\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.args\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.args_count\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.code_signature.exists\",\"type\":\"boolean\",\"esTypes\":[\"boolean\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.code_signature.status\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.code_signature.subject_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.code_signature.trusted\",\"type\":\"boolean\",\"esTypes\":[\"boolean\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.code_signature.valid\",\"type\":\"boolean\",\"esTypes\":[\"boolean\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.command_line\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.command_line.text\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false,\"subType\":{\"multi\":{\"parent\":\"process.command_line\"}}},{\"name\":\"process.entity_id\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.executable\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.executable.text\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false,\"subType\":{\"multi\":{\"parent\":\"process.executable\"}}},{\"name\":\"process.exit_code\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.hash.md5\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.hash.sha1\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.hash.sha256\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.hash.sha512\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.name.text\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false,\"subType\":{\"multi\":{\"parent\":\"process.name\"}}},{\"name\":\"process.parent.args\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.args_count\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.code_signature.exists\",\"type\":\"boolean\",\"esTypes\":[\"boolean\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.code_signature.status\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.code_signature.subject_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.code_signature.trusted\",\"type\":\"boolean\",\"esTypes\":[\"boolean\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.code_signature.valid\",\"type\":\"boolean\",\"esTypes\":[\"boolean\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.command_line\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.command_line.text\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false,\"subType\":{\"multi\":{\"parent\":\"process.parent.command_line\"}}},{\"name\":\"process.parent.entity_id\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.executable\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.executable.text\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false,\"subType\":{\"multi\":{\"parent\":\"process.parent.executable\"}}},{\"name\":\"process.parent.exit_code\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.hash.md5\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.hash.sha1\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.hash.sha256\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.hash.sha512\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.name.text\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false,\"subType\":{\"multi\":{\"parent\":\"process.parent.name\"}}},{\"name\":\"process.parent.pe.architecture\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.pe.company\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.pe.description\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.pe.file_version\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.pe.imphash\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.pe.original_file_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.pe.product\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.pgid\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.pid\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.ppid\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.start\",\"type\":\"date\",\"esTypes\":[\"date\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.thread.id\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.thread.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.title\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.title.text\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false,\"subType\":{\"multi\":{\"parent\":\"process.parent.title\"}}},{\"name\":\"process.parent.uptime\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.working_directory\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.working_directory.text\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false,\"subType\":{\"multi\":{\"parent\":\"process.parent.working_directory\"}}},{\"name\":\"process.pe.architecture\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.pe.company\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.pe.description\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.pe.file_version\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.pe.imphash\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.pe.original_file_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.pe.product\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.pgid\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.pid\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.ppid\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.start\",\"type\":\"date\",\"esTypes\":[\"date\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.thread.id\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.thread.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.title\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.uptime\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.working_directory\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.working_directory.text\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false,\"subType\":{\"multi\":{\"parent\":\"process.working_directory\"}}},{\"name\":\"processor.event\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"processor.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"profile.alloc_objects.count\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"profile.alloc_space.bytes\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"profile.cpu.ns\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"profile.duration\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"profile.id\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"profile.inuse_objects.count\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"profile.inuse_space.bytes\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"profile.samples.count\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"profile.stack.filename\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"profile.stack.function\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"profile.stack.id\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"profile.stack.line\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"profile.top.filename\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"profile.top.function\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"profile.top.id\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"profile.top.line\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"registry.data.bytes\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"registry.data.strings\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"registry.data.type\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"registry.hive\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"registry.key\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"registry.path\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"registry.value\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"related.hash\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"related.hosts\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"related.ip\",\"type\":\"ip\",\"esTypes\":[\"ip\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"related.user\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"ruby.gc.count\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"ruby.heap.allocations.total\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"ruby.heap.slots.free\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"ruby.heap.slots.live\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"ruby.threads\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"rule.author\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"rule.category\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"rule.description\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"rule.id\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"rule.license\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"rule.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"rule.reference\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"rule.ruleset\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"rule.uuid\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"rule.version\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"server.address\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"server.as.number\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"server.as.organization.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"server.as.organization.name.text\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false,\"subType\":{\"multi\":{\"parent\":\"server.as.organization.name\"}}},{\"name\":\"server.bytes\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"server.domain\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"server.geo.city_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"server.geo.continent_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"server.geo.country_iso_code\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"server.geo.country_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"server.geo.location\",\"type\":\"geo_point\",\"esTypes\":[\"geo_point\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"server.geo.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"server.geo.region_iso_code\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"server.geo.region_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"server.ip\",\"type\":\"ip\",\"esTypes\":[\"ip\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"server.mac\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"server.nat.ip\",\"type\":\"ip\",\"esTypes\":[\"ip\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"server.nat.port\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"server.packets\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"server.port\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"server.registered_domain\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"server.subdomain\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"server.top_level_domain\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"server.user.domain\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"server.user.email\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"server.user.full_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"server.user.full_name.text\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false,\"subType\":{\"multi\":{\"parent\":\"server.user.full_name\"}}},{\"name\":\"server.user.group.domain\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"server.user.group.id\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"server.user.group.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"server.user.hash\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"server.user.id\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"server.user.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"server.user.name.text\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false,\"subType\":{\"multi\":{\"parent\":\"server.user.name\"}}},{\"name\":\"server.user.roles\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"service.environment\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"service.ephemeral_id\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"service.framework.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"service.framework.version\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"service.id\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"service.language.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"service.language.version\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"service.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"service.node.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"service.runtime.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"service.runtime.version\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"service.state\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"service.type\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"service.version\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"source.address\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"source.as.number\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"source.as.organization.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"source.as.organization.name.text\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false,\"subType\":{\"multi\":{\"parent\":\"source.as.organization.name\"}}},{\"name\":\"source.bytes\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"source.domain\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"source.geo.city_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"source.geo.continent_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"source.geo.country_iso_code\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"source.geo.country_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"source.geo.location\",\"type\":\"geo_point\",\"esTypes\":[\"geo_point\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"source.geo.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"source.geo.region_iso_code\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"source.geo.region_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"source.ip\",\"type\":\"ip\",\"esTypes\":[\"ip\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"source.mac\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"source.nat.ip\",\"type\":\"ip\",\"esTypes\":[\"ip\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"source.nat.port\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"source.packets\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"source.port\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"source.registered_domain\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"source.subdomain\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"source.top_level_domain\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"source.user.domain\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"source.user.email\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"source.user.full_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"source.user.full_name.text\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false,\"subType\":{\"multi\":{\"parent\":\"source.user.full_name\"}}},{\"name\":\"source.user.group.domain\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"source.user.group.id\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"source.user.group.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"source.user.hash\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"source.user.id\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"source.user.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"source.user.name.text\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false,\"subType\":{\"multi\":{\"parent\":\"source.user.name\"}}},{\"name\":\"source.user.roles\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"sourcemap.bundle_filepath\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"sourcemap.service.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"sourcemap.service.version\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"span.action\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"span.db.link\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"span.db.rows_affected\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"span.destination.service.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"span.destination.service.resource\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"span.destination.service.response_time.count\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"span.destination.service.response_time.sum.us\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"span.destination.service.type\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"span.duration.us\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"span.id\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"span.message.age.ms\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"span.message.queue.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"span.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"span.self_time.count\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"span.self_time.sum.us\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"span.start.us\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"span.subtype\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"span.sync\",\"type\":\"boolean\",\"esTypes\":[\"boolean\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"span.type\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"system.cpu.total.norm.pct\",\"type\":\"number\",\"esTypes\":[\"scaled_float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"system.memory.actual.free\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"system.memory.total\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"system.process.cgroup.memory.mem.limit.bytes\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"system.process.cgroup.memory.mem.usage.bytes\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"system.process.cgroup.memory.stats.inactive_file.bytes\",\"type\":\"number\",\"esTypes\":[\"float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"system.process.cpu.system.norm.pct\",\"type\":\"number\",\"esTypes\":[\"float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"system.process.cpu.total.norm.pct\",\"type\":\"number\",\"esTypes\":[\"scaled_float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"system.process.cpu.user.norm.pct\",\"type\":\"number\",\"esTypes\":[\"float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"system.process.memory.rss.bytes\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"system.process.memory.size\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tags\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"threat.framework\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"threat.tactic.id\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"threat.tactic.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"threat.tactic.reference\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"threat.technique.id\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"threat.technique.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"threat.technique.name.text\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false,\"subType\":{\"multi\":{\"parent\":\"threat.technique.name\"}}},{\"name\":\"threat.technique.reference\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"threat.technique.subtechnique.id\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"threat.technique.subtechnique.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"threat.technique.subtechnique.name.text\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false,\"subType\":{\"multi\":{\"parent\":\"threat.technique.subtechnique.name\"}}},{\"name\":\"threat.technique.subtechnique.reference\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"timeseries.instance\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"timestamp.us\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.cipher\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.client.certificate\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.client.certificate_chain\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.client.hash.md5\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.client.hash.sha1\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.client.hash.sha256\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.client.issuer\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.client.ja3\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.client.not_after\",\"type\":\"date\",\"esTypes\":[\"date\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.client.not_before\",\"type\":\"date\",\"esTypes\":[\"date\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.client.server_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.client.subject\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.client.supported_ciphers\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.client.x509.alternative_names\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.client.x509.issuer.common_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.client.x509.issuer.country\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.client.x509.issuer.distinguished_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.client.x509.issuer.locality\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.client.x509.issuer.organization\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.client.x509.issuer.organizational_unit\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.client.x509.issuer.state_or_province\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.client.x509.not_after\",\"type\":\"date\",\"esTypes\":[\"date\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.client.x509.not_before\",\"type\":\"date\",\"esTypes\":[\"date\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.client.x509.public_key_algorithm\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.client.x509.public_key_curve\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.client.x509.public_key_exponent\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":false,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.client.x509.public_key_size\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.client.x509.serial_number\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.client.x509.signature_algorithm\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.client.x509.subject.common_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.client.x509.subject.country\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.client.x509.subject.distinguished_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.client.x509.subject.locality\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.client.x509.subject.organization\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.client.x509.subject.organizational_unit\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.client.x509.subject.state_or_province\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.client.x509.version_number\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.curve\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.established\",\"type\":\"boolean\",\"esTypes\":[\"boolean\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.next_protocol\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.resumed\",\"type\":\"boolean\",\"esTypes\":[\"boolean\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.server.certificate\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.server.certificate_chain\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.server.hash.md5\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.server.hash.sha1\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.server.hash.sha256\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.server.issuer\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.server.ja3s\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.server.not_after\",\"type\":\"date\",\"esTypes\":[\"date\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.server.not_before\",\"type\":\"date\",\"esTypes\":[\"date\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.server.subject\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.server.x509.alternative_names\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.server.x509.issuer.common_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.server.x509.issuer.country\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.server.x509.issuer.distinguished_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.server.x509.issuer.locality\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.server.x509.issuer.organization\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.server.x509.issuer.organizational_unit\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.server.x509.issuer.state_or_province\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.server.x509.not_after\",\"type\":\"date\",\"esTypes\":[\"date\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.server.x509.not_before\",\"type\":\"date\",\"esTypes\":[\"date\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.server.x509.public_key_algorithm\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.server.x509.public_key_curve\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.server.x509.public_key_exponent\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":false,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.server.x509.public_key_size\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.server.x509.serial_number\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.server.x509.signature_algorithm\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.server.x509.subject.common_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.server.x509.subject.country\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.server.x509.subject.distinguished_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.server.x509.subject.locality\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.server.x509.subject.organization\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.server.x509.subject.organizational_unit\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.server.x509.subject.state_or_province\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.server.x509.version_number\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.version\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.version_protocol\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"trace.id\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"transaction.breakdown.count\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"transaction.duration.count\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"transaction.duration.histogram\",\"type\":\"histogram\",\"esTypes\":[\"histogram\"],\"searchable\":false,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"transaction.duration.sum.us\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"transaction.duration.us\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"transaction.experience.cls\",\"type\":\"number\",\"esTypes\":[\"scaled_float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"transaction.experience.fid\",\"type\":\"number\",\"esTypes\":[\"scaled_float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"transaction.experience.longtask.count\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"transaction.experience.longtask.max\",\"type\":\"number\",\"esTypes\":[\"scaled_float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"transaction.experience.longtask.sum\",\"type\":\"number\",\"esTypes\":[\"scaled_float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"transaction.experience.tbt\",\"type\":\"number\",\"esTypes\":[\"scaled_float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"transaction.id\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"transaction.marks.agent.domComplete\",\"type\":\"number\",\"esTypes\":[\"scaled_float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"transaction.marks.agent.domInteractive\",\"type\":\"number\",\"esTypes\":[\"scaled_float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"transaction.marks.agent.firstContentfulPaint\",\"type\":\"number\",\"esTypes\":[\"scaled_float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"transaction.marks.agent.largestContentfulPaint\",\"type\":\"number\",\"esTypes\":[\"scaled_float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"transaction.marks.agent.timeToFirstByte\",\"type\":\"number\",\"esTypes\":[\"scaled_float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"transaction.marks.navigationTiming.connectEnd\",\"type\":\"number\",\"esTypes\":[\"scaled_float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"transaction.marks.navigationTiming.connectStart\",\"type\":\"number\",\"esTypes\":[\"scaled_float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"transaction.marks.navigationTiming.domComplete\",\"type\":\"number\",\"esTypes\":[\"scaled_float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"transaction.marks.navigationTiming.domContentLoadedEventEnd\",\"type\":\"number\",\"esTypes\":[\"scaled_float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"transaction.marks.navigationTiming.domContentLoadedEventStart\",\"type\":\"number\",\"esTypes\":[\"scaled_float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"transaction.marks.navigationTiming.domInteractive\",\"type\":\"number\",\"esTypes\":[\"scaled_float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"transaction.marks.navigationTiming.domLoading\",\"type\":\"number\",\"esTypes\":[\"scaled_float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"transaction.marks.navigationTiming.domainLookupEnd\",\"type\":\"number\",\"esTypes\":[\"scaled_float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"transaction.marks.navigationTiming.domainLookupStart\",\"type\":\"number\",\"esTypes\":[\"scaled_float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"transaction.marks.navigationTiming.fetchStart\",\"type\":\"number\",\"esTypes\":[\"scaled_float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"transaction.marks.navigationTiming.loadEventEnd\",\"type\":\"number\",\"esTypes\":[\"scaled_float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"transaction.marks.navigationTiming.loadEventStart\",\"type\":\"number\",\"esTypes\":[\"scaled_float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"transaction.marks.navigationTiming.requestStart\",\"type\":\"number\",\"esTypes\":[\"scaled_float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"transaction.marks.navigationTiming.responseEnd\",\"type\":\"number\",\"esTypes\":[\"scaled_float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"transaction.marks.navigationTiming.responseStart\",\"type\":\"number\",\"esTypes\":[\"scaled_float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"transaction.message.age.ms\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"transaction.message.queue.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"transaction.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"transaction.name.text\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false,\"subType\":{\"multi\":{\"parent\":\"transaction.name\"}}},{\"name\":\"transaction.result\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"transaction.root\",\"type\":\"boolean\",\"esTypes\":[\"boolean\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"transaction.sampled\",\"type\":\"boolean\",\"esTypes\":[\"boolean\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"transaction.self_time.count\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"transaction.self_time.sum.us\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"transaction.span_count.dropped\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"transaction.type\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"url.domain\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"url.extension\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"url.fragment\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"url.full\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"url.original\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"url.original.text\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false,\"subType\":{\"multi\":{\"parent\":\"url.original\"}}},{\"name\":\"url.password\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"url.path\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"url.port\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"url.query\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"url.registered_domain\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"url.scheme\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"url.subdomain\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"url.top_level_domain\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"url.username\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"user.domain\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"user.email\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"user.full_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"user.full_name.text\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false,\"subType\":{\"multi\":{\"parent\":\"user.full_name\"}}},{\"name\":\"user.group.domain\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"user.group.id\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"user.group.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"user.hash\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"user.id\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"user.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"user.roles\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"user_agent.device.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"user_agent.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"user_agent.original\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"user_agent.original.text\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false,\"subType\":{\"multi\":{\"parent\":\"user_agent.original\"}}},{\"name\":\"user_agent.os.family\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"user_agent.os.full\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"user_agent.os.kernel\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"user_agent.os.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"user_agent.os.platform\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"user_agent.os.version\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"user_agent.version\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"vlan.id\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"vlan.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"vulnerability.category\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"vulnerability.classification\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"vulnerability.description\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"vulnerability.description.text\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false,\"subType\":{\"multi\":{\"parent\":\"vulnerability.description\"}}},{\"name\":\"vulnerability.enumeration\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"vulnerability.id\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"vulnerability.reference\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"vulnerability.report_id\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"vulnerability.scanner.vendor\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"vulnerability.score.base\",\"type\":\"number\",\"esTypes\":[\"float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"vulnerability.score.environmental\",\"type\":\"number\",\"esTypes\":[\"float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"vulnerability.score.temporal\",\"type\":\"number\",\"esTypes\":[\"float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"vulnerability.score.version\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"vulnerability.severity\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"x509.alternative_names\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"x509.issuer.common_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"x509.issuer.country\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"x509.issuer.distinguished_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"x509.issuer.locality\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"x509.issuer.organization\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"x509.issuer.organizational_unit\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"x509.issuer.state_or_province\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"x509.not_after\",\"type\":\"date\",\"esTypes\":[\"date\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"x509.not_before\",\"type\":\"date\",\"esTypes\":[\"date\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"x509.public_key_algorithm\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"x509.public_key_curve\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"x509.public_key_exponent\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":false,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"x509.public_key_size\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"x509.serial_number\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"x509.signature_algorithm\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"x509.subject.common_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"x509.subject.country\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"x509.subject.distinguished_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"x509.subject.locality\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"x509.subject.organization\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"x509.subject.organizational_unit\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"x509.subject.state_or_province\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"x509.version_number\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true}]", + "sourceFilters": "[{\"value\":\"sourcemap.sourcemap\"}]", + "timeFieldName": "@timestamp" + }, + "id": "apm-*", + "type": "index-pattern", + "version": "1" +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/default_configs.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/default_configs.ts new file mode 100644 index 0000000000000..85d48ef638d44 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/default_configs.ts @@ -0,0 +1,52 @@ +/* + * 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 { ReportViewTypes } from '../types'; +import { getPerformanceDistLensConfig } from './performance_dist_config'; +import { getMonitorDurationConfig } from './monitor_duration_config'; +import { getServiceLatencyLensConfig } from './service_latency_config'; +import { getMonitorPingsConfig } from './monitor_pings_config'; +import { getServiceThroughputLensConfig } from './service_throughput_config'; +import { getKPITrendsLensConfig } from './kpi_trends_config'; +import { getCPUUsageLensConfig } from './cpu_usage_config'; +import { getMemoryUsageLensConfig } from './memory_usage_config'; +import { getNetworkActivityLensConfig } from './network_activity_config'; +import { getLogsFrequencyLensConfig } from './logs_frequency_config'; +import { IIndexPattern } from '../../../../../../../../src/plugins/data/common/index_patterns'; + +interface Props { + reportType: keyof typeof ReportViewTypes; + seriesId: string; + indexPattern: IIndexPattern; +} + +export const getDefaultConfigs = ({ reportType, seriesId, indexPattern }: Props) => { + switch (ReportViewTypes[reportType]) { + case 'page-load-dist': + return getPerformanceDistLensConfig({ seriesId, indexPattern }); + case 'kpi-trends': + return getKPITrendsLensConfig({ seriesId, indexPattern }); + case 'uptime-duration': + return getMonitorDurationConfig({ seriesId }); + case 'uptime-pings': + return getMonitorPingsConfig({ seriesId }); + case 'service-latency': + return getServiceLatencyLensConfig({ seriesId, indexPattern }); + case 'service-throughput': + return getServiceThroughputLensConfig({ seriesId, indexPattern }); + case 'cpu-usage': + return getCPUUsageLensConfig({ seriesId }); + case 'memory-usage': + return getMemoryUsageLensConfig({ seriesId }); + case 'network-activity': + return getNetworkActivityLensConfig({ seriesId }); + case 'logs-frequency': + return getLogsFrequencyLensConfig({ seriesId }); + default: + return getKPITrendsLensConfig({ seriesId, indexPattern }); + } +}; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/kpi_trends_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/kpi_trends_config.ts new file mode 100644 index 0000000000000..a967a8824bca7 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/kpi_trends_config.ts @@ -0,0 +1,73 @@ +/* + * 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 { ConfigProps, DataSeries } from '../types'; +import { FieldLabels } from './constants'; +import { buildPhraseFilter } from './utils'; +import { + CLIENT_GEO_COUNTRY_NAME, + PROCESSOR_EVENT, + SERVICE_ENVIRONMENT, + SERVICE_NAME, + TRANSACTION_TYPE, + USER_AGENT_DEVICE, + USER_AGENT_NAME, + USER_AGENT_OS, + USER_AGENT_VERSION, +} from './data/elasticsearch_fieldnames'; + +export function getKPITrendsLensConfig({ seriesId, indexPattern }: ConfigProps): DataSeries { + return { + id: seriesId, + defaultSeriesType: 'bar_stacked', + reportType: 'kpi-trends', + seriesTypes: ['bar', 'bar_stacked'], + xAxisColumn: { + sourceField: '@timestamp', + }, + yAxisColumn: { + operationType: 'count', + label: 'Page views', + }, + hasMetricType: false, + defaultFilters: [ + USER_AGENT_OS, + CLIENT_GEO_COUNTRY_NAME, + USER_AGENT_DEVICE, + { + field: USER_AGENT_NAME, + nested: USER_AGENT_VERSION, + }, + ], + breakdowns: [USER_AGENT_NAME, USER_AGENT_OS, CLIENT_GEO_COUNTRY_NAME, USER_AGENT_DEVICE], + filters: [ + buildPhraseFilter(TRANSACTION_TYPE, 'page-load', indexPattern), + buildPhraseFilter(PROCESSOR_EVENT, 'transaction', indexPattern), + ], + labels: { ...FieldLabels, SERVICE_NAME: 'Web Application' }, + reportDefinitions: [ + { + field: SERVICE_NAME, + required: true, + }, + { + field: SERVICE_ENVIRONMENT, + }, + { + field: 'Business.KPI', + custom: true, + defaultValue: 'Records', + options: [ + { + field: 'Records', + label: 'Page views', + }, + ], + }, + ], + }; +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.test.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.test.ts new file mode 100644 index 0000000000000..dcfaed938cc0f --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.test.ts @@ -0,0 +1,387 @@ +/* + * 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 { LensAttributes } from './lens_attributes'; +import { mockIndexPattern } from '../rtl_helpers'; +import { getDefaultConfigs } from './default_configs'; +import { sampleAttribute } from './data/sample_attribute'; +import { LCP_FIELD, SERVICE_NAME } from './data/elasticsearch_fieldnames'; +import { USER_AGENT_NAME } from './data/elasticsearch_fieldnames'; + +describe('Lens Attribute', () => { + const reportViewConfig = getDefaultConfigs({ + reportType: 'pld', + indexPattern: mockIndexPattern, + seriesId: 'series-id', + }); + + let lnsAttr: LensAttributes; + + beforeEach(() => { + lnsAttr = new LensAttributes(mockIndexPattern, reportViewConfig, 'line', [], 'count', {}); + }); + + it('should return expected json', function () { + expect(lnsAttr.getJSON()).toEqual(sampleAttribute); + }); + + it('should return main y axis', function () { + expect(lnsAttr.getMainYAxis()).toEqual({ + dataType: 'number', + isBucketed: false, + label: 'Pages loaded', + operationType: 'count', + scale: 'ratio', + sourceField: 'Records', + }); + }); + + it('should return expected field type', function () { + expect(JSON.stringify(lnsAttr.getFieldMeta('transaction.type'))).toEqual( + JSON.stringify({ + count: 0, + name: 'transaction.type', + type: 'string', + esTypes: ['keyword'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }) + ); + }); + + it('should return expected field type for custom field with default value', function () { + expect(JSON.stringify(lnsAttr.getFieldMeta('performance.metric'))).toEqual( + JSON.stringify({ + count: 0, + name: 'transaction.duration.us', + type: 'number', + esTypes: ['long'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }) + ); + }); + + it('should return expected field type for custom field with passed value', function () { + lnsAttr = new LensAttributes(mockIndexPattern, reportViewConfig, 'line', [], 'count', { + 'performance.metric': LCP_FIELD, + }); + + expect(JSON.stringify(lnsAttr.getFieldMeta('performance.metric'))).toEqual( + JSON.stringify({ + count: 0, + name: LCP_FIELD, + type: 'number', + esTypes: ['scaled_float'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }) + ); + }); + + it('should return expected number column', function () { + expect(lnsAttr.getNumberColumn('transaction.duration.us')).toEqual({ + dataType: 'number', + isBucketed: true, + label: 'Page load time', + operationType: 'range', + params: { + maxBars: 'auto', + ranges: [ + { + from: 0, + label: '', + to: 1000, + }, + ], + type: 'histogram', + }, + scale: 'interval', + sourceField: 'transaction.duration.us', + }); + }); + + it('should return expected date histogram column', function () { + expect(lnsAttr.getDateHistogramColumn('@timestamp')).toEqual({ + dataType: 'date', + isBucketed: true, + label: '@timestamp', + operationType: 'date_histogram', + params: { + interval: 'auto', + }, + scale: 'interval', + sourceField: '@timestamp', + }); + }); + + it('should return main x axis', function () { + expect(lnsAttr.getXAxis()).toEqual({ + dataType: 'number', + isBucketed: true, + label: 'Page load time', + operationType: 'range', + params: { + maxBars: 'auto', + ranges: [ + { + from: 0, + label: '', + to: 1000, + }, + ], + type: 'histogram', + }, + scale: 'interval', + sourceField: 'transaction.duration.us', + }); + }); + + it('should return first layer', function () { + expect(lnsAttr.getLayer()).toEqual({ + columnOrder: ['x-axis-column', 'y-axis-column'], + columns: { + 'x-axis-column': { + dataType: 'number', + isBucketed: true, + label: 'Page load time', + operationType: 'range', + params: { + maxBars: 'auto', + ranges: [ + { + from: 0, + label: '', + to: 1000, + }, + ], + type: 'histogram', + }, + scale: 'interval', + sourceField: 'transaction.duration.us', + }, + 'y-axis-column': { + dataType: 'number', + isBucketed: false, + label: 'Pages loaded', + operationType: 'count', + scale: 'ratio', + sourceField: 'Records', + }, + }, + incompleteColumns: {}, + }); + }); + + it('should return expected XYState', function () { + expect(lnsAttr.getXyState()).toEqual({ + axisTitlesVisibilitySettings: { x: true, yLeft: true, yRight: true }, + curveType: 'CURVE_MONOTONE_X', + fittingFunction: 'Linear', + gridlinesVisibilitySettings: { x: true, yLeft: true, yRight: true }, + layers: [ + { + accessors: ['y-axis-column'], + layerId: 'layer1', + palette: undefined, + seriesType: 'line', + xAccessor: 'x-axis-column', + yConfig: [{ color: 'green', forAccessor: 'y-axis-column' }], + }, + ], + legend: { isVisible: true, position: 'right' }, + preferredSeriesType: 'line', + tickLabelsVisibilitySettings: { x: true, yLeft: true, yRight: true }, + valueLabels: 'hide', + }); + }); + + describe('ParseFilters function', function () { + it('should parse default filters', function () { + expect(lnsAttr.parseFilters()).toEqual([ + { meta: { index: 'apm-*' }, query: { match_phrase: { 'transaction.type': 'page-load' } } }, + { meta: { index: 'apm-*' }, query: { match_phrase: { 'processor.event': 'transaction' } } }, + ]); + }); + + it('should parse default and ui filters', function () { + lnsAttr = new LensAttributes( + mockIndexPattern, + reportViewConfig, + 'line', + [ + { field: SERVICE_NAME, values: ['elastic-co', 'kibana-front'] }, + { field: USER_AGENT_NAME, values: ['Firefox'], notValues: ['Chrome'] }, + ], + 'count', + {} + ); + + expect(lnsAttr.parseFilters()).toEqual([ + { meta: { index: 'apm-*' }, query: { match_phrase: { 'transaction.type': 'page-load' } } }, + { meta: { index: 'apm-*' }, query: { match_phrase: { 'processor.event': 'transaction' } } }, + { + meta: { + index: 'apm-*', + key: 'service.name', + params: ['elastic-co', 'kibana-front'], + type: 'phrases', + value: 'elastic-co, kibana-front', + }, + query: { + bool: { + minimum_should_match: 1, + should: [ + { + match_phrase: { + 'service.name': 'elastic-co', + }, + }, + { + match_phrase: { + 'service.name': 'kibana-front', + }, + }, + ], + }, + }, + }, + { + meta: { + index: 'apm-*', + }, + query: { + match_phrase: { + 'user_agent.name': 'Firefox', + }, + }, + }, + { + meta: { + index: 'apm-*', + negate: true, + }, + query: { + match_phrase: { + 'user_agent.name': 'Chrome', + }, + }, + }, + ]); + }); + }); + + describe('Layer breakdowns', function () { + it('should add breakdown column', function () { + lnsAttr.addBreakdown(USER_AGENT_NAME); + + expect(lnsAttr.visualization.layers).toEqual([ + { + accessors: ['y-axis-column'], + layerId: 'layer1', + palette: undefined, + seriesType: 'line', + splitAccessor: 'break-down-column', + xAccessor: 'x-axis-column', + yConfig: [{ color: 'green', forAccessor: 'y-axis-column' }], + }, + ]); + + expect(lnsAttr.layers.layer1).toEqual({ + columnOrder: ['x-axis-column', 'break-down-column', 'y-axis-column'], + columns: { + 'break-down-column': { + dataType: 'string', + isBucketed: true, + label: 'Top values of Browser family', + operationType: 'terms', + params: { + missingBucket: false, + orderBy: { columnId: 'y-axis-column', type: 'column' }, + orderDirection: 'desc', + otherBucket: true, + size: 3, + }, + scale: 'ordinal', + sourceField: 'user_agent.name', + }, + 'x-axis-column': { + dataType: 'number', + isBucketed: true, + label: 'Page load time', + operationType: 'range', + params: { + maxBars: 'auto', + ranges: [{ from: 0, label: '', to: 1000 }], + type: 'histogram', + }, + scale: 'interval', + sourceField: 'transaction.duration.us', + }, + 'y-axis-column': { + dataType: 'number', + isBucketed: false, + label: 'Pages loaded', + operationType: 'count', + scale: 'ratio', + sourceField: 'Records', + }, + }, + incompleteColumns: {}, + }); + }); + + it('should remove breakdown column', function () { + lnsAttr.addBreakdown(USER_AGENT_NAME); + + lnsAttr.removeBreakdown(); + + expect(lnsAttr.visualization.layers).toEqual([ + { + accessors: ['y-axis-column'], + layerId: 'layer1', + palette: undefined, + seriesType: 'line', + xAccessor: 'x-axis-column', + yConfig: [{ color: 'green', forAccessor: 'y-axis-column' }], + }, + ]); + + expect(lnsAttr.layers.layer1.columnOrder).toEqual(['x-axis-column', 'y-axis-column']); + + expect(lnsAttr.layers.layer1.columns).toEqual({ + 'x-axis-column': { + dataType: 'number', + isBucketed: true, + label: 'Page load time', + operationType: 'range', + params: { + maxBars: 'auto', + ranges: [{ from: 0, label: '', to: 1000 }], + type: 'histogram', + }, + scale: 'interval', + sourceField: 'transaction.duration.us', + }, + 'y-axis-column': { + dataType: 'number', + isBucketed: false, + label: 'Pages loaded', + operationType: 'count', + scale: 'ratio', + sourceField: 'Records', + }, + }); + }); + }); +}); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts new file mode 100644 index 0000000000000..589a93d160068 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts @@ -0,0 +1,273 @@ +/* + * 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 { + CountIndexPatternColumn, + DateHistogramIndexPatternColumn, + LastValueIndexPatternColumn, + OperationType, + PersistedIndexPatternLayer, + RangeIndexPatternColumn, + SeriesType, + TypedLensByValueInput, + XYState, + XYCurveType, + DataType, +} from '../../../../../../lens/public'; +import { + buildPhraseFilter, + buildPhrasesFilter, + IndexPattern, +} from '../../../../../../../../src/plugins/data/common'; +import { FieldLabels } from './constants'; +import { DataSeries, UrlFilter } from '../types'; + +function getLayerReferenceName(layerId: string) { + return `indexpattern-datasource-layer-${layerId}`; +} + +export class LensAttributes { + indexPattern: IndexPattern; + layers: Record; + visualization: XYState; + filters: UrlFilter[]; + seriesType: SeriesType; + reportViewConfig: DataSeries; + reportDefinitions: Record; + + constructor( + indexPattern: IndexPattern, + reportViewConfig: DataSeries, + seriesType?: SeriesType, + filters?: UrlFilter[], + metricType?: OperationType, + reportDefinitions?: Record + ) { + this.indexPattern = indexPattern; + this.layers = {}; + this.filters = filters ?? []; + this.reportDefinitions = reportDefinitions ?? {}; + + if (typeof reportViewConfig.yAxisColumn.operationType !== undefined && metricType) { + reportViewConfig.yAxisColumn.operationType = metricType; + } + this.seriesType = seriesType ?? reportViewConfig.defaultSeriesType; + this.reportViewConfig = reportViewConfig; + this.layers.layer1 = this.getLayer(); + this.visualization = this.getXyState(); + } + + addBreakdown(sourceField: string) { + const fieldMeta = this.indexPattern.getFieldByName(sourceField); + + this.layers.layer1.columns['break-down-column'] = { + sourceField, + label: `Top values of ${FieldLabels[sourceField]}`, + dataType: fieldMeta?.type as DataType, + operationType: 'terms', + scale: 'ordinal', + isBucketed: true, + params: { + size: 3, + orderBy: { type: 'column', columnId: 'y-axis-column' }, + orderDirection: 'desc', + otherBucket: true, + missingBucket: false, + }, + }; + + this.layers.layer1.columnOrder = ['x-axis-column', 'break-down-column', 'y-axis-column']; + + this.visualization.layers[0].splitAccessor = 'break-down-column'; + } + + removeBreakdown() { + delete this.layers.layer1.columns['break-down-column']; + + this.layers.layer1.columnOrder = ['x-axis-column', 'y-axis-column']; + + this.visualization.layers[0].splitAccessor = undefined; + } + + getNumberColumn(sourceField: string): RangeIndexPatternColumn { + return { + sourceField, + label: this.reportViewConfig.labels[sourceField], + dataType: 'number', + operationType: 'range', + isBucketed: true, + scale: 'interval', + params: { + type: 'histogram', + ranges: [{ from: 0, to: 1000, label: '' }], + maxBars: 'auto', + }, + }; + } + + getDateHistogramColumn(sourceField: string): DateHistogramIndexPatternColumn { + return { + sourceField, + dataType: 'date', + isBucketed: true, + label: '@timestamp', + operationType: 'date_histogram', + params: { interval: 'auto' }, + scale: 'interval', + }; + } + + getXAxis(): + | LastValueIndexPatternColumn + | DateHistogramIndexPatternColumn + | RangeIndexPatternColumn { + const { xAxisColumn } = this.reportViewConfig; + + const { type: fieldType, name: fieldName } = this.getFieldMeta(xAxisColumn.sourceField)!; + + if (fieldType === 'date') { + return this.getDateHistogramColumn(fieldName); + } + if (fieldType === 'number') { + return this.getNumberColumn(fieldName); + } + + // FIXME review my approach again + return this.getDateHistogramColumn(fieldName); + } + + getFieldMeta(sourceField?: string) { + let xAxisField = sourceField; + + if (xAxisField) { + const rdf = this.reportViewConfig.reportDefinitions ?? []; + + const customField = rdf.find(({ field }) => field === xAxisField); + + if (customField) { + if (this.reportDefinitions[xAxisField]) { + xAxisField = this.reportDefinitions[xAxisField]; + } else if (customField.defaultValue) { + xAxisField = customField.defaultValue; + } else if (customField.options?.[0].field) { + xAxisField = customField.options?.[0].field; + } + } + + return this.indexPattern.getFieldByName(xAxisField); + } + } + + getMainYAxis() { + return { + dataType: 'number', + isBucketed: false, + label: 'Count of records', + operationType: 'count', + scale: 'ratio', + sourceField: 'Records', + ...this.reportViewConfig.yAxisColumn, + } as CountIndexPatternColumn; + } + + getLayer() { + return { + columnOrder: ['x-axis-column', 'y-axis-column'], + columns: { + 'x-axis-column': this.getXAxis(), + 'y-axis-column': this.getMainYAxis(), + }, + incompleteColumns: {}, + }; + } + + getXyState(): XYState { + return { + legend: { isVisible: true, position: 'right' }, + valueLabels: 'hide', + fittingFunction: 'Linear', + curveType: 'CURVE_MONOTONE_X' as XYCurveType, + axisTitlesVisibilitySettings: { x: true, yLeft: true, yRight: true }, + tickLabelsVisibilitySettings: { x: true, yLeft: true, yRight: true }, + gridlinesVisibilitySettings: { x: true, yLeft: true, yRight: true }, + preferredSeriesType: 'line', + layers: [ + { + accessors: ['y-axis-column'], + layerId: 'layer1', + seriesType: this.seriesType ?? 'line', + palette: this.reportViewConfig.palette, + yConfig: [{ forAccessor: 'y-axis-column', color: 'green' }], + xAccessor: 'x-axis-column', + }, + ], + }; + } + + parseFilters() { + const defaultFilters = this.reportViewConfig.filters ?? []; + const parsedFilters = this.reportViewConfig.filters ? [...defaultFilters] : []; + + this.filters.forEach(({ field, values = [], notValues = [] }) => { + const fieldMeta = this.indexPattern.fields.find((fieldT) => fieldT.name === field)!; + + if (values?.length > 0) { + if (values?.length > 1) { + const multiFilter = buildPhrasesFilter(fieldMeta, values, this.indexPattern); + parsedFilters.push(multiFilter); + } else { + const filter = buildPhraseFilter(fieldMeta, values[0], this.indexPattern); + parsedFilters.push(filter); + } + } + + if (notValues?.length > 0) { + if (notValues?.length > 1) { + const multiFilter = buildPhrasesFilter(fieldMeta, notValues, this.indexPattern); + multiFilter.meta.negate = true; + parsedFilters.push(multiFilter); + } else { + const filter = buildPhraseFilter(fieldMeta, notValues[0], this.indexPattern); + filter.meta.negate = true; + parsedFilters.push(filter); + } + } + }); + + return parsedFilters; + } + + getJSON(): TypedLensByValueInput['attributes'] { + return { + title: 'Prefilled from exploratory view app', + description: '', + visualizationType: 'lnsXY', + references: [ + { + id: this.indexPattern.id!, + name: 'indexpattern-datasource-current-indexpattern', + type: 'index-pattern', + }, + { + id: this.indexPattern.id!, + name: getLayerReferenceName('layer1'), + type: 'index-pattern', + }, + ], + state: { + datasourceStates: { + indexpattern: { + layers: this.layers, + }, + }, + visualization: this.visualization, + query: { query: '', language: 'kuery' }, + filters: this.parseFilters(), + }, + }; + } +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/logs_frequency_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/logs_frequency_config.ts new file mode 100644 index 0000000000000..68e5e697d2f9d --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/logs_frequency_config.ts @@ -0,0 +1,39 @@ +/* + * 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 { DataSeries } from '../types'; +import { FieldLabels } from './constants'; + +interface Props { + seriesId: string; +} + +export function getLogsFrequencyLensConfig({ seriesId }: Props): DataSeries { + return { + id: seriesId, + reportType: 'logs-frequency', + defaultSeriesType: 'line', + seriesTypes: ['line', 'bar'], + xAxisColumn: { + sourceField: '@timestamp', + }, + yAxisColumn: { + operationType: 'count', + }, + hasMetricType: false, + defaultFilters: [], + breakdowns: ['agent.hostname'], + filters: [], + labels: { ...FieldLabels }, + reportDefinitions: [ + { + field: 'agent.hostname', + required: true, + }, + ], + }; +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/memory_usage_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/memory_usage_config.ts new file mode 100644 index 0000000000000..579372ed86fa7 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/memory_usage_config.ts @@ -0,0 +1,42 @@ +/* + * 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 { DataSeries } from '../types'; +import { FieldLabels } from './constants'; +import { OperationType } from '../../../../../../lens/public'; + +interface Props { + seriesId: string; +} + +export function getMemoryUsageLensConfig({ seriesId }: Props): DataSeries { + return { + id: seriesId, + reportType: 'memory-usage', + defaultSeriesType: 'line', + seriesTypes: ['line', 'bar'], + xAxisColumn: { + sourceField: '@timestamp', + }, + yAxisColumn: { + operationType: 'avg' as OperationType, + sourceField: 'system.memory.used.pct', + label: 'Memory Usage %', + }, + hasMetricType: true, + defaultFilters: [], + breakdowns: ['host.hostname'], + filters: [], + labels: { ...FieldLabels, 'host.hostname': 'Host name' }, + reportDefinitions: [ + { + field: 'host.hostname', + required: true, + }, + ], + }; +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/monitor_duration_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/monitor_duration_config.ts new file mode 100644 index 0000000000000..aa9b8b94c6d86 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/monitor_duration_config.ts @@ -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 { DataSeries } from '../types'; +import { FieldLabels } from './constants'; +import { OperationType } from '../../../../../../lens/public'; + +interface Props { + seriesId: string; +} + +export function getMonitorDurationConfig({ seriesId }: Props): DataSeries { + return { + id: seriesId, + reportType: 'uptime-duration', + defaultSeriesType: 'line', + seriesTypes: ['line', 'bar_stacked'], + xAxisColumn: { + sourceField: '@timestamp', + }, + yAxisColumn: { + operationType: 'avg' as OperationType, + sourceField: 'monitor.duration.us', + label: 'Monitor duration (ms)', + }, + hasMetricType: true, + defaultFilters: ['monitor.type', 'observer.geo.name', 'tags'], + breakdowns: [ + 'observer.geo.name', + 'monitor.name', + 'monitor.id', + 'monitor.type', + 'tags', + 'url.port', + ], + filters: [], + reportDefinitions: [ + { + field: 'monitor.id', + }, + ], + labels: { ...FieldLabels }, + }; +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/monitor_pings_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/monitor_pings_config.ts new file mode 100644 index 0000000000000..72968626e934b --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/monitor_pings_config.ts @@ -0,0 +1,43 @@ +/* + * 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 { DataSeries } from '../types'; +import { FieldLabels } from './constants'; + +interface Props { + seriesId: string; +} + +export function getMonitorPingsConfig({ seriesId }: Props): DataSeries { + return { + id: seriesId, + reportType: 'uptime-pings', + defaultSeriesType: 'bar_stacked', + seriesTypes: ['bar_stacked', 'bar'], + xAxisColumn: { + sourceField: '@timestamp', + }, + yAxisColumn: { + operationType: 'count', + label: 'Monitor pings', + }, + hasMetricType: false, + defaultFilters: ['observer.geo.name'], + breakdowns: ['monitor.status', 'observer.geo.name', 'monitor.type'], + filters: [], + palette: { type: 'palette', name: 'status' }, + reportDefinitions: [ + { + field: 'monitor.id', + }, + { + field: 'url.full', + }, + ], + labels: { ...FieldLabels }, + }; +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/network_activity_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/network_activity_config.ts new file mode 100644 index 0000000000000..63cdd0ec8bd60 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/network_activity_config.ts @@ -0,0 +1,41 @@ +/* + * 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 { DataSeries } from '../types'; +import { FieldLabels } from './constants'; +import { OperationType } from '../../../../../../lens/public'; + +interface Props { + seriesId: string; +} + +export function getNetworkActivityLensConfig({ seriesId }: Props): DataSeries { + return { + id: seriesId, + reportType: 'network-activity', + defaultSeriesType: 'line', + seriesTypes: ['line', 'bar'], + xAxisColumn: { + sourceField: '@timestamp', + }, + yAxisColumn: { + operationType: 'avg' as OperationType, + sourceField: 'system.memory.used.pct', + }, + hasMetricType: true, + defaultFilters: [], + breakdowns: ['host.hostname'], + filters: [], + labels: { ...FieldLabels, 'host.hostname': 'Host name' }, + reportDefinitions: [ + { + field: 'host.hostname', + required: true, + }, + ], + }; +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/performance_dist_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/performance_dist_config.ts new file mode 100644 index 0000000000000..41617304c9f3d --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/performance_dist_config.ts @@ -0,0 +1,86 @@ +/* + * 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 { ConfigProps, DataSeries } from '../types'; +import { FieldLabels } from './constants'; +import { buildPhraseFilter } from './utils'; +import { + CLIENT_GEO_COUNTRY_NAME, + CLS_FIELD, + FCP_FIELD, + FID_FIELD, + LCP_FIELD, + PROCESSOR_EVENT, + SERVICE_ENVIRONMENT, + SERVICE_NAME, + TBT_FIELD, + TRANSACTION_DURATION, + TRANSACTION_TYPE, + USER_AGENT_DEVICE, + USER_AGENT_NAME, + USER_AGENT_OS, + USER_AGENT_VERSION, +} from './data/elasticsearch_fieldnames'; + +export function getPerformanceDistLensConfig({ seriesId, indexPattern }: ConfigProps): DataSeries { + return { + id: seriesId ?? 'unique-key', + reportType: 'page-load-dist', + defaultSeriesType: 'line', + seriesTypes: ['line', 'bar'], + xAxisColumn: { + sourceField: 'performance.metric', + }, + yAxisColumn: { + operationType: 'count', + label: 'Pages loaded', + }, + hasMetricType: false, + defaultFilters: [ + USER_AGENT_OS, + CLIENT_GEO_COUNTRY_NAME, + USER_AGENT_DEVICE, + { + field: USER_AGENT_NAME, + nested: USER_AGENT_VERSION, + }, + ], + breakdowns: [USER_AGENT_NAME, USER_AGENT_OS, CLIENT_GEO_COUNTRY_NAME, USER_AGENT_DEVICE], + reportDefinitions: [ + { + field: SERVICE_NAME, + required: true, + }, + { + field: SERVICE_ENVIRONMENT, + }, + { + field: 'performance.metric', + custom: true, + defaultValue: TRANSACTION_DURATION, + options: [ + { label: 'Page load time', field: TRANSACTION_DURATION }, + { label: 'First contentful paint', field: FCP_FIELD }, + { label: 'Total blocking time', field: TBT_FIELD }, + // FIXME, review if we need these descriptions + { label: 'Largest contentful paint', field: LCP_FIELD, description: 'Core web vital' }, + { label: 'First input delay', field: FID_FIELD, description: 'Core web vital' }, + { label: 'Cumulative layout shift', field: CLS_FIELD, description: 'Core web vital' }, + ], + }, + ], + filters: [ + buildPhraseFilter(TRANSACTION_TYPE, 'page-load', indexPattern), + buildPhraseFilter(PROCESSOR_EVENT, 'transaction', indexPattern), + ], + labels: { + ...FieldLabels, + [SERVICE_NAME]: 'Web Application', + [TRANSACTION_DURATION]: 'Page load time', + }, + }; +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/service_latency_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/service_latency_config.ts new file mode 100644 index 0000000000000..a31679c61a4ab --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/service_latency_config.ts @@ -0,0 +1,52 @@ +/* + * 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 { ConfigProps, DataSeries } from '../types'; +import { FieldLabels } from './constants'; +import { buildPhraseFilter } from './utils'; +import { OperationType } from '../../../../../../lens/public'; + +export function getServiceLatencyLensConfig({ seriesId, indexPattern }: ConfigProps): DataSeries { + return { + id: seriesId, + reportType: 'service-latency', + defaultSeriesType: 'line', + seriesTypes: ['line', 'bar'], + xAxisColumn: { + sourceField: '@timestamp', + }, + yAxisColumn: { + operationType: 'avg' as OperationType, + sourceField: 'transaction.duration.us', + label: 'Latency', + }, + hasMetricType: true, + defaultFilters: [ + 'user_agent.name', + 'user_agent.os.name', + 'client.geo.country_name', + 'user_agent.device.name', + ], + breakdowns: [ + 'user_agent.name', + 'user_agent.os.name', + 'client.geo.country_name', + 'user_agent.device.name', + ], + filters: [buildPhraseFilter('transaction.type', 'request', indexPattern)], + labels: { ...FieldLabels }, + reportDefinitions: [ + { + field: 'service.name', + required: true, + }, + { + field: 'service.environment', + }, + ], + }; +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/service_throughput_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/service_throughput_config.ts new file mode 100644 index 0000000000000..32cae2167ddf0 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/service_throughput_config.ts @@ -0,0 +1,55 @@ +/* + * 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 { ConfigProps, DataSeries } from '../types'; +import { FieldLabels } from './constants'; +import { buildPhraseFilter } from './utils'; +import { OperationType } from '../../../../../../lens/public'; + +export function getServiceThroughputLensConfig({ + seriesId, + indexPattern, +}: ConfigProps): DataSeries { + return { + id: seriesId, + reportType: 'service-latency', + defaultSeriesType: 'line', + seriesTypes: ['line', 'bar'], + xAxisColumn: { + sourceField: '@timestamp', + }, + yAxisColumn: { + operationType: 'avg' as OperationType, + sourceField: 'transaction.duration.us', + label: 'Throughput', + }, + hasMetricType: true, + defaultFilters: [ + 'user_agent.name', + 'user_agent.os.name', + 'client.geo.country_name', + 'user_agent.device.name', + ], + breakdowns: [ + 'user_agent.name', + 'user_agent.os.name', + 'client.geo.country_name', + 'user_agent.device.name', + ], + filters: [buildPhraseFilter('transaction.type', 'request', indexPattern)], + labels: { ...FieldLabels }, + reportDefinitions: [ + { + field: 'service.name', + required: true, + }, + { + field: 'service.environment', + }, + ], + }; +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/url_constants.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/url_constants.ts new file mode 100644 index 0000000000000..5b99c19dbabb7 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/url_constants.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export enum URL_KEYS { + METRIC_TYPE = 'mt', + REPORT_TYPE = 'rt', + SERIES_TYPE = 'st', + BREAK_DOWN = 'bd', + FILTERS = 'ft', + REPORT_DEFINITIONS = 'rdf', +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts new file mode 100644 index 0000000000000..38b8ce81b2acd --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts @@ -0,0 +1,54 @@ +/* + * 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 rison, { RisonValue } from 'rison-node'; +import type { AllSeries, AllShortSeries } from '../hooks/use_url_strorage'; +import type { SeriesUrl } from '../types'; +import { IIndexPattern } from '../../../../../../../../src/plugins/data/common/index_patterns'; +import { esFilters } from '../../../../../../../../src/plugins/data/public'; +import { URL_KEYS } from './url_constants'; + +export function convertToShortUrl(series: SeriesUrl) { + const { + metric, + seriesType, + reportType, + breakdown, + filters, + reportDefinitions, + ...restSeries + } = series; + + return { + [URL_KEYS.METRIC_TYPE]: metric, + [URL_KEYS.REPORT_TYPE]: reportType, + [URL_KEYS.SERIES_TYPE]: seriesType, + [URL_KEYS.BREAK_DOWN]: breakdown, + [URL_KEYS.FILTERS]: filters, + [URL_KEYS.REPORT_DEFINITIONS]: reportDefinitions, + ...restSeries, + }; +} + +export function createExploratoryViewUrl(allSeries: AllSeries, baseHref = '') { + const allSeriesIds = Object.keys(allSeries); + + const allShortSeries: AllShortSeries = {}; + + allSeriesIds.forEach((seriesKey) => { + allShortSeries[seriesKey] = convertToShortUrl(allSeries[seriesKey]); + }); + + return ( + baseHref + + `/app/observability/exploratory-view#?sr=${rison.encode(allShortSeries as RisonValue)}` + ); +} + +export function buildPhraseFilter(field: string, value: any, indexPattern: IIndexPattern) { + const fieldMeta = indexPattern.fields.find((fieldT) => fieldT.name === field)!; + return esFilters.buildPhraseFilter(fieldMeta, value, indexPattern); +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.test.tsx new file mode 100644 index 0000000000000..7e99874f557b3 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.test.tsx @@ -0,0 +1,93 @@ +/* + * 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 { within } from '@testing-library/react'; +import { fireEvent, screen, waitFor } from '@testing-library/dom'; +import { render, mockUrlStorage, mockCore } from './rtl_helpers'; +import { ExploratoryView } from './exploratory_view'; +import { getStubIndexPattern } from '../../../../../../../src/plugins/data/public/test_utils'; +import * as obsvInd from '../../../utils/observability_index_patterns'; + +describe('ExploratoryView', () => { + beforeEach(() => { + const indexPattern = getStubIndexPattern( + 'apm-*', + () => {}, + '@timestamp', + [ + { + name: '@timestamp', + type: 'date', + esTypes: ['date'], + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + ], + mockCore() as any + ); + + jest.spyOn(obsvInd, 'ObservabilityIndexPatterns').mockReturnValue({ + getIndexPattern: jest.fn().mockReturnValue(indexPattern), + } as any); + }); + + it('renders exploratory view', async () => { + render(); + + await waitFor(() => { + screen.getByText(/open in lens/i); + screen.getByRole('heading', { name: /exploratory view/i }); + screen.getByRole('img', { name: /visulization/i }); + screen.getByText(/add series/i); + screen.getByText(/no series found, please add a series\./i); + }); + }); + + it('can add, cancel new series', async () => { + render(); + + await fireEvent.click(screen.getByText(/add series/i)); + + await waitFor(() => { + screen.getByText(/open in lens/i); + screen.getByText(/select a data type to start building a series\./i); + screen.getByRole('table', { name: /this table contains 1 rows\./i }); + const button = screen.getByRole('button', { name: /add/i }); + within(button).getByText(/add/i); + }); + + await fireEvent.click(screen.getByText(/cancel/i)); + + await waitFor(() => { + screen.getByText(/add series/i); + }); + }); + + it('renders lens component when there is series', async () => { + mockUrlStorage({ + data: { + 'uptime-pings-histogram': { + reportType: 'upp', + breakdown: 'monitor.status', + time: { from: 'now-15m', to: 'now' }, + }, + }, + }); + + render(); + + await waitFor(() => { + screen.getByText(/open in lens/i); + screen.getByRole('heading', { name: /uptime pings/i }); + screen.getByText(/uptime-pings-histogram/i); + screen.getByText(/Lens Embeddable Component/i); + screen.getByRole('table', { name: /this table contains 1 rows\./i }); + }); + }); +}); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.tsx new file mode 100644 index 0000000000000..b3ad107bbe0e2 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.tsx @@ -0,0 +1,87 @@ +/* + * 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'; +import React, { useEffect, useState } from 'react'; +import styled from 'styled-components'; +import { EuiLoadingSpinner, EuiPanel, EuiTitle } from '@elastic/eui'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; +import { ObservabilityPublicPluginsStart } from '../../../plugin'; +import { ExploratoryViewHeader } from './header/header'; +import { SeriesEditor } from './series_editor/series_editor'; +import { useUrlStorage } from './hooks/use_url_strorage'; +import { useLensAttributes } from './hooks/use_lens_attributes'; +import { EmptyView } from './components/empty_view'; +import { useIndexPatternContext } from './hooks/use_default_index_pattern'; +import { TypedLensByValueInput } from '../../../../../lens/public'; + +export function ExploratoryView() { + const { + services: { lens }, + } = useKibana(); + + const [lensAttributes, setLensAttributes] = useState( + null + ); + + const { indexPattern } = useIndexPatternContext(); + + const LensComponent = lens?.EmbeddableComponent; + + const { firstSeriesId: seriesId, firstSeries: series } = useUrlStorage(); + + const lensAttributesT = useLensAttributes({ + seriesId, + indexPattern, + }); + + useEffect(() => { + setLensAttributes(lensAttributesT); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [JSON.stringify(lensAttributesT ?? {}), series?.reportType, series?.time?.from]); + + return ( + + {lens ? ( + <> + + {!indexPattern && ( + + + + )} + {lensAttributes && seriesId && series?.reportType && series?.time ? ( + + ) : ( + + )} + + + ) : ( + +

+ {i18n.translate('xpack.observability.overview.exploratoryView.lensDisabled', { + defaultMessage: + 'Lens app is not available, please enable Lens to use exploratory view.', + })} +

+
+ )} +
+ ); +} + +const SpinnerWrap = styled.div` + height: 100vh; + display: flex; + justify-content: center; + align-items: center; +`; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.test.tsx new file mode 100644 index 0000000000000..de6912f256be7 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.test.tsx @@ -0,0 +1,53 @@ +/* + * 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 { mockUrlStorage, render } from '../rtl_helpers'; +import { ExploratoryViewHeader } from './header'; +import { fireEvent } from '@testing-library/dom'; + +describe('ExploratoryViewHeader', function () { + it('should render properly', function () { + const { getByText } = render( + + ); + getByText('Open in Lens'); + }); + + it('should be able to click open in lens', function () { + mockUrlStorage({ + data: { + 'uptime-pings-histogram': { + reportType: 'upp', + breakdown: 'monitor.status', + time: { from: 'now-15m', to: 'now' }, + }, + }, + }); + + const { getByText, core } = render( + + ); + fireEvent.click(getByText('Open in Lens')); + + expect(core?.lens?.navigateToPrefilledEditor).toHaveBeenCalledTimes(1); + expect(core?.lens?.navigateToPrefilledEditor).toHaveBeenCalledWith({ + attributes: { title: 'Performance distribution' }, + id: '', + timeRange: { + from: 'now-15m', + to: 'now', + }, + }); + }); +}); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.tsx new file mode 100644 index 0000000000000..bda3566c76602 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.tsx @@ -0,0 +1,63 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; +import { TypedLensByValueInput } from '../../../../../../lens/public'; +import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; +import { ObservabilityPublicPluginsStart } from '../../../../plugin'; +import { DataViewLabels } from '../configurations/constants'; +import { useUrlStorage } from '../hooks/use_url_strorage'; + +interface Props { + seriesId: string; + lensAttributes: TypedLensByValueInput['attributes'] | null; +} + +export function ExploratoryViewHeader({ seriesId, lensAttributes }: Props) { + const { + services: { lens }, + } = useKibana(); + + const { series } = useUrlStorage(seriesId); + + return ( + + + +

+ {DataViewLabels[series.reportType] ?? + i18n.translate('xpack.observability.expView.heading.label', { + defaultMessage: 'Exploratory view', + })} +

+
+
+ + { + if (lensAttributes) { + lens.navigateToPrefilledEditor({ + id: '', + timeRange: series.time, + attributes: lensAttributes, + }); + } + }} + > + {i18n.translate('xpack.observability.expView.heading.openInLens', { + defaultMessage: 'Open in Lens', + })} + + +
+ ); +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_default_index_pattern.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_default_index_pattern.tsx new file mode 100644 index 0000000000000..04cbb4a4ddb18 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_default_index_pattern.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 React, { createContext, useContext, Context, useState, useEffect } from 'react'; +import { IndexPattern } from '../../../../../../../../src/plugins/data/common'; +import { AppDataType } from '../types'; +import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; +import { ObservabilityPublicPluginsStart } from '../../../../plugin'; +import { ObservabilityIndexPatterns } from '../../../../utils/observability_index_patterns'; + +export interface IIndexPatternContext { + indexPattern: IndexPattern; + loadIndexPattern: (dataType: AppDataType) => void; +} + +export const IndexPatternContext = createContext>({}); + +interface ProviderProps { + indexPattern?: IndexPattern; + children: JSX.Element; +} + +export function IndexPatternContextProvider({ + children, + indexPattern: initialIndexPattern, +}: ProviderProps) { + const [indexPattern, setIndexPattern] = useState(initialIndexPattern); + + useEffect(() => { + setIndexPattern(initialIndexPattern); + }, [initialIndexPattern]); + + const { + services: { data }, + } = useKibana(); + + const loadIndexPattern = async (dataType: AppDataType) => { + const obsvIndexP = new ObservabilityIndexPatterns(data); + const indPattern = await obsvIndexP.getIndexPattern(dataType); + setIndexPattern(indPattern!); + }; + + return ( + + {children} + + ); +} + +export const useIndexPatternContext = () => { + return useContext((IndexPatternContext as unknown) as Context); +}; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_init_exploratory_view.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_init_exploratory_view.ts new file mode 100644 index 0000000000000..9f462790e8d37 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_init_exploratory_view.ts @@ -0,0 +1,44 @@ +/* + * 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 { useFetcher } from '../../../..'; +import { IKbnUrlStateStorage } from '../../../../../../../../src/plugins/kibana_utils/public'; +import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; +import { ObservabilityPublicPluginsStart } from '../../../../plugin'; +import { AllShortSeries } from './use_url_strorage'; +import { ReportToDataTypeMap } from '../configurations/constants'; +import { + DataType, + ObservabilityIndexPatterns, +} from '../../../../utils/observability_index_patterns'; + +export const useInitExploratoryView = (storage: IKbnUrlStateStorage) => { + const { + services: { data }, + } = useKibana(); + + const allSeriesKey = 'sr'; + + const allSeries = storage.get(allSeriesKey) ?? {}; + + const allSeriesIds = Object.keys(allSeries); + + const firstSeriesId = allSeriesIds?.[0]; + + const firstSeries = allSeries[firstSeriesId]; + + const { data: indexPattern } = useFetcher(() => { + const obsvIndexP = new ObservabilityIndexPatterns(data); + let reportType: DataType = 'apm'; + if (firstSeries?.rt) { + reportType = ReportToDataTypeMap[firstSeries?.rt]; + } + + return obsvIndexP.getIndexPattern(reportType); + }, [firstSeries?.rt, data]); + + return indexPattern; +}; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.ts new file mode 100644 index 0000000000000..1c735009f66f9 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.ts @@ -0,0 +1,88 @@ +/* + * 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 { useMemo } from 'react'; +import { TypedLensByValueInput } from '../../../../../../lens/public'; +import { LensAttributes } from '../configurations/lens_attributes'; +import { useUrlStorage } from './use_url_strorage'; +import { getDefaultConfigs } from '../configurations/default_configs'; + +import { IndexPattern } from '../../../../../../../../src/plugins/data/common'; +import { DataSeries, SeriesUrl, UrlFilter } from '../types'; + +interface Props { + seriesId: string; + indexPattern?: IndexPattern | null; +} + +export const getFiltersFromDefs = ( + reportDefinitions: SeriesUrl['reportDefinitions'], + dataViewConfig: DataSeries +) => { + const rdfFilters = Object.entries(reportDefinitions ?? {}).map(([field, value]) => { + return { + field, + values: [value], + }; + }) as UrlFilter[]; + + // let's filter out custom fields + return rdfFilters.filter(({ field }) => { + const rdf = dataViewConfig.reportDefinitions.find(({ field: fd }) => field === fd); + return !rdf?.custom; + }); +}; + +export const useLensAttributes = ({ + seriesId, + indexPattern, +}: Props): TypedLensByValueInput['attributes'] | null => { + const { series } = useUrlStorage(seriesId); + + const { breakdown, seriesType, metric: metricType, reportType, reportDefinitions = {} } = + series ?? {}; + + return useMemo(() => { + if (!indexPattern || !reportType) { + return null; + } + + const dataViewConfig = getDefaultConfigs({ + seriesId, + reportType, + indexPattern, + }); + + const filters: UrlFilter[] = (series.filters ?? []).concat( + getFiltersFromDefs(reportDefinitions, dataViewConfig) + ); + + const lensAttributes = new LensAttributes( + indexPattern, + dataViewConfig, + seriesType, + filters, + metricType, + reportDefinitions + ); + + if (breakdown) { + lensAttributes.addBreakdown(breakdown); + } + + return lensAttributes.getJSON(); + }, [ + indexPattern, + breakdown, + seriesType, + metricType, + reportType, + reportDefinitions, + seriesId, + series.filters, + ]); +}; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_filters.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_filters.ts new file mode 100644 index 0000000000000..35247180c2ee5 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_filters.ts @@ -0,0 +1,100 @@ +/* + * 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 { useUrlStorage } from './use_url_strorage'; +import { UrlFilter } from '../types'; + +export interface UpdateFilter { + field: string; + value: string; + negate?: boolean; +} + +export const useSeriesFilters = ({ seriesId }: { seriesId: string }) => { + const { series, setSeries } = useUrlStorage(seriesId); + + const filters = series.filters ?? []; + + const removeFilter = ({ field, value, negate }: UpdateFilter) => { + const filtersN = filters.map((filter) => { + if (filter.field === field) { + if (negate) { + const notValuesN = filter.notValues?.filter((val) => val !== value); + return { ...filter, notValues: notValuesN }; + } else { + const valuesN = filter.values?.filter((val) => val !== value); + return { ...filter, values: valuesN }; + } + } + + return filter; + }); + setSeries(seriesId, { ...series, filters: filtersN }); + }; + + const addFilter = ({ field, value, negate }: UpdateFilter) => { + const currFilter: UrlFilter = { field }; + if (negate) { + currFilter.notValues = [value]; + } else { + currFilter.values = [value]; + } + if (filters.length === 0) { + setSeries(seriesId, { ...series, filters: [currFilter] }); + } else { + setSeries(seriesId, { + ...series, + filters: [currFilter, ...filters.filter((ft) => ft.field !== field)], + }); + } + }; + + const updateFilter = ({ field, value, negate }: UpdateFilter) => { + const currFilter: UrlFilter | undefined = filters.find(({ field: fd }) => field === fd) ?? { + field, + }; + + const currNotValues = currFilter.notValues ?? []; + const currValues = currFilter.values ?? []; + + const notValues = currNotValues.filter((val) => val !== value); + const values = currValues.filter((val) => val !== value); + + if (negate) { + notValues.push(value); + } else { + values.push(value); + } + + currFilter.notValues = notValues.length > 0 ? notValues : undefined; + currFilter.values = values.length > 0 ? values : undefined; + + const otherFilters = filters.filter(({ field: fd }) => fd !== field); + + if (notValues.length > 0 || values.length > 0) { + setSeries(seriesId, { ...series, filters: [...otherFilters, currFilter] }); + } else { + setSeries(seriesId, { ...series, filters: otherFilters }); + } + }; + + const setFilter = ({ field, value, negate }: UpdateFilter) => { + const currFilter: UrlFilter | undefined = filters.find(({ field: fd }) => field === fd); + + if (!currFilter) { + addFilter({ field, value, negate }); + } else { + updateFilter({ field, value, negate }); + } + }; + + const invertFilter = ({ field, value, negate }: UpdateFilter) => { + updateFilter({ field, value, negate: !negate }); + }; + + return { invertFilter, setFilter, removeFilter }; +}; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_url_strorage.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_url_strorage.tsx new file mode 100644 index 0000000000000..d38429703b709 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_url_strorage.tsx @@ -0,0 +1,103 @@ +/* + * 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, { createContext, useContext, Context } from 'react'; +import { IKbnUrlStateStorage } from '../../../../../../../../src/plugins/kibana_utils/public'; +import type { AppDataType, ReportViewTypeId, SeriesUrl, UrlFilter } from '../types'; +import { convertToShortUrl } from '../configurations/utils'; +import { OperationType, SeriesType } from '../../../../../../lens/public'; +import { URL_KEYS } from '../configurations/url_constants'; + +export const UrlStorageContext = createContext(null); + +interface ProviderProps { + storage: IKbnUrlStateStorage; +} + +export function UrlStorageContextProvider({ + children, + storage, +}: ProviderProps & { children: JSX.Element }) { + return {children}; +} + +function convertFromShortUrl(newValue: ShortUrlSeries): SeriesUrl { + const { mt, st, rt, bd, ft, time, rdf, ...restSeries } = newValue; + return { + metric: mt, + reportType: rt!, + seriesType: st, + breakdown: bd, + filters: ft!, + time: time!, + reportDefinitions: rdf, + ...restSeries, + }; +} + +interface ShortUrlSeries { + [URL_KEYS.METRIC_TYPE]?: OperationType; + [URL_KEYS.REPORT_TYPE]?: ReportViewTypeId; + [URL_KEYS.SERIES_TYPE]?: SeriesType; + [URL_KEYS.BREAK_DOWN]?: string; + [URL_KEYS.FILTERS]?: UrlFilter[]; + [URL_KEYS.REPORT_DEFINITIONS]?: Record; + time?: { + to: string; + from: string; + }; + dataType?: AppDataType; +} + +export type AllShortSeries = Record; +export type AllSeries = Record; + +export const NEW_SERIES_KEY = 'newSeriesKey'; + +export function useUrlStorage(seriesId?: string) { + const allSeriesKey = 'sr'; + const storage = useContext((UrlStorageContext as unknown) as Context); + let series: SeriesUrl = {} as SeriesUrl; + const allShortSeries = storage.get(allSeriesKey) ?? {}; + + const allSeriesIds = Object.keys(allShortSeries); + + const allSeries: AllSeries = {}; + + allSeriesIds.forEach((seriesKey) => { + allSeries[seriesKey] = convertFromShortUrl(allShortSeries[seriesKey]); + }); + + if (seriesId) { + series = allSeries?.[seriesId] ?? ({} as SeriesUrl); + } + + const setSeries = async (seriesIdN: string, newValue: SeriesUrl) => { + allShortSeries[seriesIdN] = convertToShortUrl(newValue); + allSeries[seriesIdN] = newValue; + return storage.set(allSeriesKey, allShortSeries); + }; + + const removeSeries = (seriesIdN: string) => { + delete allShortSeries[seriesIdN]; + delete allSeries[seriesIdN]; + storage.set(allSeriesKey, allShortSeries); + }; + + const firstSeriesId = allSeriesIds?.[0]; + + return { + storage, + setSeries, + removeSeries, + series, + firstSeriesId, + allSeries, + allSeriesIds, + firstSeries: allSeries?.[firstSeriesId], + }; +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/index.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/index.tsx new file mode 100644 index 0000000000000..dc47a0f075fe6 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/index.tsx @@ -0,0 +1,64 @@ +/* + * 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, { useContext } from 'react'; +import { i18n } from '@kbn/i18n'; +import { useHistory } from 'react-router-dom'; +import { ThemeContext } from 'styled-components'; +import { ExploratoryView } from './exploratory_view'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; +import { ObservabilityPublicPluginsStart } from '../../../plugin'; +import { useBreadcrumbs } from '../../../hooks/use_breadcrumbs'; +import { IndexPatternContextProvider } from './hooks/use_default_index_pattern'; +import { + createKbnUrlStateStorage, + withNotifyOnErrors, +} from '../../../../../../../src/plugins/kibana_utils/public/'; +import { UrlStorageContextProvider } from './hooks/use_url_strorage'; +import { useInitExploratoryView } from './hooks/use_init_exploratory_view'; +import { WithHeaderLayout } from '../../app/layout/with_header'; + +export function ExploratoryViewPage() { + useBreadcrumbs([ + { + text: i18n.translate('xpack.observability.overview.exploratoryView', { + defaultMessage: 'Exploratory view', + }), + }, + ]); + + const theme = useContext(ThemeContext); + + const { + services: { uiSettings, notifications }, + } = useKibana(); + + const history = useHistory(); + + const kbnUrlStateStorage = createKbnUrlStateStorage({ + history, + useHash: uiSettings!.get('state:storeInSessionStorage'), + ...withNotifyOnErrors(notifications!.toasts), + }); + + const indexPattern = useInitExploratoryView(kbnUrlStateStorage); + + return ( + + {indexPattern ? ( + + + + + + ) : null} + + ); +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/rtl_helpers.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/rtl_helpers.tsx new file mode 100644 index 0000000000000..112bfcc3ccb58 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/rtl_helpers.tsx @@ -0,0 +1,318 @@ +/* + * 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 { of } from 'rxjs'; +import React, { ReactElement } from 'react'; +import { stringify } from 'query-string'; +// eslint-disable-next-line import/no-extraneous-dependencies +import { render as reactTestLibRender, RenderOptions } from '@testing-library/react'; +import { Router } from 'react-router-dom'; +import { createMemoryHistory, History } from 'history'; +import { CoreStart } from 'kibana/public'; +import { I18nProvider } from '@kbn/i18n/react'; +import { coreMock } from 'src/core/public/mocks'; +import { + KibanaServices, + KibanaContextProvider, +} from '../../../../../../../src/plugins/kibana_react/public'; +import { ObservabilityPublicPluginsStart } from '../../../plugin'; +import { EuiThemeProvider } from '../../../../../../../src/plugins/kibana_react/common'; +import { lensPluginMock } from '../../../../../lens/public/mocks'; +import { IndexPatternContextProvider } from './hooks/use_default_index_pattern'; +import { AllSeries, UrlStorageContextProvider } from './hooks/use_url_strorage'; +import { + withNotifyOnErrors, + createKbnUrlStateStorage, +} from '../../../../../../../src/plugins/kibana_utils/public'; +import * as fetcherHook from '../../../hooks/use_fetcher'; +import * as useUrlHook from './hooks/use_url_strorage'; +import * as useSeriesFilterHook from './hooks/use_series_filters'; +import * as useHasDataHook from '../../../hooks/use_has_data'; +import * as useValuesListHook from '../../../hooks/use_values_list'; + +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { getStubIndexPattern } from '../../../../../../../src/plugins/data/public/index_patterns/index_pattern.stub'; +import indexPatternData from './configurations/data/test_index_pattern.json'; + +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { setIndexPatterns } from '../../../../../../../src/plugins/data/public/services'; +import { IndexPatternsContract } from '../../../../../../../src/plugins/data/common/index_patterns/index_patterns'; +import { UrlFilter } from './types'; +import { dataPluginMock } from '../../../../../../../src/plugins/data/public/mocks'; + +interface KibanaProps { + services?: KibanaServices; +} + +export interface KibanaProviderOptions { + core?: ExtraCore & Partial; + kibanaProps?: KibanaProps; +} + +interface MockKibanaProviderProps> + extends KibanaProviderOptions { + children: ReactElement; + history: History; +} + +type MockRouterProps> = MockKibanaProviderProps; + +type Url = + | string + | { + path: string; + queryParams: Record; + }; + +interface RenderRouterOptions extends KibanaProviderOptions { + history?: History; + renderOptions?: Omit; + url?: Url; +} + +function getSetting(key: string): T { + if (key === 'timepicker:quickRanges') { + return ([ + { + display: 'Today', + from: 'now/d', + to: 'now/d', + }, + ] as unknown) as T; + } + return ('MMM D, YYYY @ HH:mm:ss.SSS' as unknown) as T; +} + +function setSetting$(key: string): T { + return (of('MMM D, YYYY @ HH:mm:ss.SSS') as unknown) as T; +} + +/* default mock core */ +const defaultCore = coreMock.createStart(); +export const mockCore: () => Partial = () => { + const core: Partial = { + ...defaultCore, + application: { + ...defaultCore.application, + getUrlForApp: () => '/app/observability', + navigateToUrl: jest.fn(), + capabilities: { + ...defaultCore.application.capabilities, + observability: { + 'alerting:save': true, + configureSettings: true, + save: true, + show: true, + }, + }, + }, + uiSettings: { + ...defaultCore.uiSettings, + get: getSetting, + get$: setSetting$, + }, + lens: lensPluginMock.createStartContract(), + data: dataPluginMock.createStartContract(), + }; + + return core; +}; + +/* Mock Provider Components */ +export function MockKibanaProvider>({ + children, + core, + history, + kibanaProps, +}: MockKibanaProviderProps) { + const { notifications } = core!; + + const kbnUrlStateStorage = createKbnUrlStateStorage({ + history, + useHash: false, + ...withNotifyOnErrors(notifications!.toasts), + }); + + const indexPattern = mockIndexPattern; + + setIndexPatterns(({ + ...[indexPattern], + get: async () => indexPattern, + } as unknown) as IndexPatternsContract); + + return ( + + + + + + {children} + + + + + + ); +} + +export function MockRouter({ + children, + core, + history = createMemoryHistory(), + kibanaProps, +}: MockRouterProps) { + return ( + + + {children} + + + ); +} + +/* Custom react testing library render */ +export function render( + ui: ReactElement, + { + history = createMemoryHistory(), + core: customCore, + kibanaProps, + renderOptions, + url, + }: RenderRouterOptions = {} +) { + if (url) { + history = getHistoryFromUrl(url); + } + + const core = { + ...mockCore(), + ...customCore, + }; + + return { + ...reactTestLibRender( + + {ui} + , + renderOptions + ), + history, + core, + }; +} + +const getHistoryFromUrl = (url: Url) => { + if (typeof url === 'string') { + return createMemoryHistory({ + initialEntries: [url], + }); + } + + return createMemoryHistory({ + initialEntries: [url.path + stringify(url.queryParams)], + }); +}; + +export const mockFetcher = (data: any) => { + return jest.spyOn(fetcherHook, 'useFetcher').mockReturnValue({ + data, + status: fetcherHook.FETCH_STATUS.SUCCESS, + refetch: jest.fn(), + }); +}; + +export const mockUseHasData = () => { + const onRefreshTimeRange = jest.fn(); + const spy = jest.spyOn(useHasDataHook, 'useHasData').mockReturnValue({ + onRefreshTimeRange, + } as any); + return { spy, onRefreshTimeRange }; +}; + +export const mockUseValuesList = (values?: string[]) => { + const onRefreshTimeRange = jest.fn(); + const spy = jest.spyOn(useValuesListHook, 'useValuesList').mockReturnValue({ + values: values ?? [], + } as any); + return { spy, onRefreshTimeRange }; +}; + +export const mockUrlStorage = ({ + data, + filters, + breakdown, +}: { + data?: AllSeries; + filters?: UrlFilter[]; + breakdown?: string; +}) => { + const mockDataSeries = data || { + 'performance-distribution': { + reportType: 'pld', + breakdown: breakdown || 'user_agent.name', + time: { from: 'now-15m', to: 'now' }, + ...(filters ? { filters } : {}), + }, + }; + const allSeriesIds = Object.keys(mockDataSeries); + const firstSeriesId = allSeriesIds?.[0]; + + const series = mockDataSeries[firstSeriesId]; + + const removeSeries = jest.fn(); + const setSeries = jest.fn(); + + const spy = jest.spyOn(useUrlHook, 'useUrlStorage').mockReturnValue({ + firstSeriesId, + allSeriesIds, + removeSeries, + setSeries, + series, + firstSeries: mockDataSeries[firstSeriesId], + allSeries: mockDataSeries, + } as any); + + return { spy, removeSeries, setSeries }; +}; + +export function mockUseSeriesFilter() { + const removeFilter = jest.fn(); + const invertFilter = jest.fn(); + const setFilter = jest.fn(); + const spy = jest.spyOn(useSeriesFilterHook, 'useSeriesFilters').mockReturnValue({ + removeFilter, + invertFilter, + setFilter, + }); + + return { + spy, + removeFilter, + invertFilter, + setFilter, + }; +} + +const hist = createMemoryHistory(); +export const mockHistory = { + ...hist, + createHref: jest.fn(({ pathname }) => `/observability${pathname}`), + push: jest.fn(), + location: { + ...hist.location, + pathname: '/current-path', + }, +}; + +export const mockIndexPattern = getStubIndexPattern( + 'apm-*', + () => {}, + '@timestamp', + JSON.parse(indexPatternData.attributes.fields), + mockCore() as any +); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.test.tsx new file mode 100644 index 0000000000000..d33d8515d3bee --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.test.tsx @@ -0,0 +1,59 @@ +/* + * 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 { fireEvent, screen } from '@testing-library/react'; +import { mockUrlStorage, render } from '../../rtl_helpers'; +import { dataTypes, DataTypesCol } from './data_types_col'; +import { NEW_SERIES_KEY } from '../../hooks/use_url_strorage'; + +describe('DataTypesCol', function () { + it('should render properly', function () { + const { getByText } = render(); + + dataTypes.forEach(({ label }) => { + getByText(label); + }); + }); + + it('should set series on change', function () { + const { setSeries } = mockUrlStorage({}); + + render(); + + fireEvent.click(screen.getByText(/user experience\(rum\)/i)); + + expect(setSeries).toHaveBeenCalledTimes(1); + expect(setSeries).toHaveBeenCalledWith('newSeriesKey', { dataType: 'rum' }); + }); + + it('should set series on change on already selected', function () { + const { setSeries } = mockUrlStorage({ + data: { + [NEW_SERIES_KEY]: { + dataType: 'synthetics', + reportType: 'upp', + breakdown: 'monitor.status', + time: { from: 'now-15m', to: 'now' }, + }, + }, + }); + + render(); + + const button = screen.getByRole('button', { + name: /Synthetic Monitoring/i, + }); + + expect(button.classList).toContain('euiButton--fill'); + + fireEvent.click(button); + + // undefined on click selected + expect(setSeries).toHaveBeenCalledWith('newSeriesKey', { dataType: undefined }); + }); +}); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.tsx new file mode 100644 index 0000000000000..7ea44e66a721a --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.tsx @@ -0,0 +1,56 @@ +/* + * 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 { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { AppDataType } from '../../types'; +import { useIndexPatternContext } from '../../hooks/use_default_index_pattern'; +import { NEW_SERIES_KEY, useUrlStorage } from '../../hooks/use_url_strorage'; + +export const dataTypes: Array<{ id: AppDataType; label: string }> = [ + { id: 'synthetics', label: 'Synthetic Monitoring' }, + { id: 'rum', label: 'User Experience(RUM)' }, + { id: 'logs', label: 'Logs' }, + { id: 'metrics', label: 'Metrics' }, + { id: 'apm', label: 'APM' }, +]; + +export function DataTypesCol() { + const { series, setSeries } = useUrlStorage(NEW_SERIES_KEY); + + const { loadIndexPattern } = useIndexPatternContext(); + + const onDataTypeChange = (dataType?: AppDataType) => { + if (dataType) { + loadIndexPattern(dataType); + } + setSeries(NEW_SERIES_KEY, { dataType } as any); + }; + + const selectedDataType = series.dataType; + + return ( + + {dataTypes.map(({ id: dataTypeId, label }) => ( + + { + onDataTypeChange(dataTypeId === selectedDataType ? undefined : dataTypeId); + }} + > + {label} + + + ))} + + ); +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.test.tsx new file mode 100644 index 0000000000000..dba660fff9c36 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.test.tsx @@ -0,0 +1,75 @@ +/* + * 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 { fireEvent, screen } from '@testing-library/react'; +import { render } from '../../../../../utils/test_helper'; +import { getDefaultConfigs } from '../../configurations/default_configs'; +import { mockIndexPattern, mockUrlStorage } from '../../rtl_helpers'; +import { NEW_SERIES_KEY } from '../../hooks/use_url_strorage'; +import { ReportBreakdowns } from './report_breakdowns'; +import { USER_AGENT_OS } from '../../configurations/data/elasticsearch_fieldnames'; + +describe('Series Builder ReportBreakdowns', function () { + const dataViewSeries = getDefaultConfigs({ + reportType: 'pld', + indexPattern: mockIndexPattern, + seriesId: NEW_SERIES_KEY, + }); + + it('should render properly', function () { + mockUrlStorage({}); + + render(); + + screen.getByText('Select an option: , is selected'); + screen.getAllByText('Browser family'); + }); + + it('should set new series breakdown on change', function () { + const { setSeries } = mockUrlStorage({}); + + render(); + + const btn = screen.getByRole('button', { + name: /select an option: Browser family , is selected/i, + hidden: true, + }); + + fireEvent.click(btn); + + fireEvent.click(screen.getByText(/operating system/i)); + + expect(setSeries).toHaveBeenCalledTimes(1); + expect(setSeries).toHaveBeenCalledWith('newSeriesKey', { + breakdown: USER_AGENT_OS, + reportType: 'pld', + time: { from: 'now-15m', to: 'now' }, + }); + }); + it('should set undefined on new series on no select breakdown', function () { + const { setSeries } = mockUrlStorage({}); + + render(); + + const btn = screen.getByRole('button', { + name: /select an option: Browser family , is selected/i, + hidden: true, + }); + + fireEvent.click(btn); + + fireEvent.click(screen.getByText(/no breakdown/i)); + + expect(setSeries).toHaveBeenCalledTimes(1); + expect(setSeries).toHaveBeenCalledWith('newSeriesKey', { + breakdown: undefined, + reportType: 'pld', + time: { from: 'now-15m', to: 'now' }, + }); + }); +}); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.tsx new file mode 100644 index 0000000000000..7667cea417a52 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.tsx @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { Breakdowns } from '../../series_editor/columns/breakdowns'; +import { NEW_SERIES_KEY } from '../../hooks/use_url_strorage'; +import { DataSeries } from '../../types'; + +export function ReportBreakdowns({ dataViewSeries }: { dataViewSeries: DataSeries }) { + return ; +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.test.tsx new file mode 100644 index 0000000000000..2fda581154166 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.test.tsx @@ -0,0 +1,75 @@ +/* + * 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 { fireEvent, screen } from '@testing-library/react'; +import { getDefaultConfigs } from '../../configurations/default_configs'; +import { mockIndexPattern, mockUrlStorage, mockUseValuesList, render } from '../../rtl_helpers'; +import { NEW_SERIES_KEY } from '../../hooks/use_url_strorage'; +import { ReportDefinitionCol } from './report_definition_col'; +import { SERVICE_NAME } from '../../configurations/data/elasticsearch_fieldnames'; + +describe('Series Builder ReportDefinitionCol', function () { + const dataViewSeries = getDefaultConfigs({ + reportType: 'pld', + indexPattern: mockIndexPattern, + seriesId: NEW_SERIES_KEY, + }); + + const { setSeries } = mockUrlStorage({ + data: { + 'performance-dist': { + dataType: 'rum', + reportType: 'pld', + time: { from: 'now-30d', to: 'now' }, + reportDefinitions: { [SERVICE_NAME]: 'elastic-co' }, + }, + }, + }); + + it('should render properly', async function () { + render(); + + screen.getByText('Web Application'); + screen.getByText('Environment'); + screen.getByText('Select an option: Page load time, is selected'); + screen.getByText('Page load time'); + }); + + it('should render selected report definitions', function () { + render(); + + screen.getByText('elastic-co'); + }); + + it('should be able to remove selected definition', function () { + render(); + + const removeBtn = screen.getByText(/elastic-co/i); + + fireEvent.click(removeBtn); + + expect(setSeries).toHaveBeenCalledTimes(1); + expect(setSeries).toHaveBeenCalledWith('newSeriesKey', { + dataType: 'rum', + reportDefinitions: {}, + reportType: 'pld', + time: { from: 'now-30d', to: 'now' }, + }); + }); + + it('should be able to unselected selected definition', async function () { + mockUseValuesList(['elastic-co']); + render(); + + const definitionBtn = screen.getByText(/web application/i); + + fireEvent.click(definitionBtn); + + screen.getByText('Apply'); + }); +}); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.tsx new file mode 100644 index 0000000000000..ce11c869de0ab --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.tsx @@ -0,0 +1,95 @@ +/* + * 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 { EuiBadge, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { useIndexPatternContext } from '../../hooks/use_default_index_pattern'; +import { NEW_SERIES_KEY, useUrlStorage } from '../../hooks/use_url_strorage'; +import { CustomReportField } from '../custom_report_field'; +import FieldValueSuggestions from '../../../field_value_suggestions'; +import { DataSeries } from '../../types'; + +export function ReportDefinitionCol({ dataViewSeries }: { dataViewSeries: DataSeries }) { + const { indexPattern } = useIndexPatternContext(); + + const { series, setSeries } = useUrlStorage(NEW_SERIES_KEY); + + const { reportDefinitions: rtd = {} } = series; + + const { reportDefinitions, labels, filters } = dataViewSeries; + + const onChange = (field: string, value?: string) => { + if (!value) { + delete rtd[field]; + setSeries(NEW_SERIES_KEY, { + ...series, + reportDefinitions: { ...rtd }, + }); + } else { + setSeries(NEW_SERIES_KEY, { + ...series, + reportDefinitions: { ...rtd, [field]: value }, + }); + } + }; + + const onRemove = (field: string) => { + delete rtd[field]; + setSeries(NEW_SERIES_KEY, { + ...series, + reportDefinitions: rtd, + }); + }; + + return ( + + {indexPattern && + reportDefinitions.map(({ field, custom, options, defaultValue }) => ( + + {!custom ? ( + + + onChange(field, val)} + filters={(filters ?? []).map(({ query }) => query)} + time={series.time} + width={200} + /> + + {rtd?.[field] && ( + + onRemove(field)} + iconOnClick={() => onRemove(field)} + iconOnClickAriaLabel={'Click to remove'} + onClickAriaLabel={'Click to remove'} + > + {rtd?.[field]} + + + )} + + ) : ( + + )} + + ))} + + ); +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.test.tsx new file mode 100644 index 0000000000000..674f5e6f49bde --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.test.tsx @@ -0,0 +1,28 @@ +/* + * 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 { screen } from '@testing-library/react'; +import { render } from '../../../../../utils/test_helper'; +import { ReportFilters } from './report_filters'; +import { getDefaultConfigs } from '../../configurations/default_configs'; +import { mockIndexPattern, mockUrlStorage } from '../../rtl_helpers'; +import { NEW_SERIES_KEY } from '../../hooks/use_url_strorage'; + +describe('Series Builder ReportFilters', function () { + const dataViewSeries = getDefaultConfigs({ + reportType: 'pld', + indexPattern: mockIndexPattern, + seriesId: NEW_SERIES_KEY, + }); + mockUrlStorage({}); + it('should render properly', function () { + render(); + + screen.getByText('Add filter'); + }); +}); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.tsx new file mode 100644 index 0000000000000..903dda549aeee --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.tsx @@ -0,0 +1,22 @@ +/* + * 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 { SeriesFilter } from '../../series_editor/columns/series_filter'; +import { NEW_SERIES_KEY } from '../../hooks/use_url_strorage'; +import { DataSeries } from '../../types'; + +export function ReportFilters({ dataViewSeries }: { dataViewSeries: DataSeries }) { + return ( + + ); +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.test.tsx new file mode 100644 index 0000000000000..567e2654130e8 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.test.tsx @@ -0,0 +1,65 @@ +/* + * 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 { fireEvent, screen } from '@testing-library/react'; +import { mockUrlStorage, render } from '../../rtl_helpers'; +import { ReportTypesCol, SELECTED_DATA_TYPE_FOR_REPORT } from './report_types_col'; +import { ReportTypes } from '../series_builder'; + +describe('ReportTypesCol', function () { + it('should render properly', function () { + render(); + screen.getByText('Performance distribution'); + screen.getByText('KPI over time'); + }); + + it('should display empty message', function () { + render(); + screen.getByText(SELECTED_DATA_TYPE_FOR_REPORT); + }); + + it('should set series on change', function () { + const { setSeries } = mockUrlStorage({}); + render(); + + fireEvent.click(screen.getByText(/monitor duration/i)); + + expect(setSeries).toHaveBeenCalledWith('newSeriesKey', { + breakdown: 'user_agent.name', + reportDefinitions: {}, + reportType: 'upd', + time: { from: 'now-15m', to: 'now' }, + }); + expect(setSeries).toHaveBeenCalledTimes(1); + }); + + it('should set selected as filled', function () { + const { setSeries } = mockUrlStorage({ + data: { + newSeriesKey: { + dataType: 'synthetics', + reportType: 'upp', + breakdown: 'monitor.status', + time: { from: 'now-15m', to: 'now' }, + }, + }, + }); + + render(); + + const button = screen.getByRole('button', { + name: /pings histogram/i, + }); + + expect(button.classList).toContain('euiButton--fill'); + fireEvent.click(button); + + // undefined on click selected + expect(setSeries).toHaveBeenCalledWith('newSeriesKey', { dataType: 'synthetics' }); + }); +}); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.tsx new file mode 100644 index 0000000000000..5c94a5bca60f8 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.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 React from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; +import { ReportViewTypeId, SeriesUrl } from '../../types'; +import { NEW_SERIES_KEY, useUrlStorage } from '../../hooks/use_url_strorage'; + +interface Props { + reportTypes: Array<{ id: ReportViewTypeId; label: string }>; +} + +export function ReportTypesCol({ reportTypes }: Props) { + const { + series: { reportType: selectedReportType, ...restSeries }, + setSeries, + } = useUrlStorage(NEW_SERIES_KEY); + + return reportTypes?.length > 0 ? ( + + {reportTypes.map(({ id: reportType, label }) => ( + + { + if (reportType === selectedReportType) { + setSeries(NEW_SERIES_KEY, { + dataType: restSeries.dataType, + } as SeriesUrl); + } else { + setSeries(NEW_SERIES_KEY, { + ...restSeries, + reportType, + reportDefinitions: {}, + }); + } + }} + > + {label} + + + ))} + + ) : ( + {SELECTED_DATA_TYPE_FOR_REPORT} + ); +} + +export const SELECTED_DATA_TYPE_FOR_REPORT = i18n.translate( + 'xpack.observability.expView.reportType.noDataType', + { defaultMessage: 'Select a data type to start building a series.' } +); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/custom_report_field.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/custom_report_field.tsx new file mode 100644 index 0000000000000..6039fd4cba280 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/custom_report_field.tsx @@ -0,0 +1,47 @@ +/* + * 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 { EuiSuperSelect } from '@elastic/eui'; +import { useUrlStorage } from '../hooks/use_url_strorage'; +import { ReportDefinition } from '../types'; + +interface Props { + field: string; + seriesId: string; + defaultValue?: string; + options: ReportDefinition['options']; +} + +export function CustomReportField({ field, seriesId, options: opts, defaultValue }: Props) { + const { series, setSeries } = useUrlStorage(seriesId); + + const { reportDefinitions: rtd = {} } = series; + + const onChange = (value: string) => { + setSeries(seriesId, { ...series, reportDefinitions: { ...rtd, [field]: value } }); + }; + + const { reportDefinitions } = series; + + const NO_SELECT = 'no_select'; + + const options = [{ label: 'Select metric', field: NO_SELECT }, ...(opts ?? [])]; + + return ( +
+ ({ + value: fd, + inputDisplay: label, + }))} + valueOfSelected={reportDefinitions?.[field] || defaultValue || NO_SELECT} + onChange={(value) => onChange(value)} + /> +
+ ); +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/series_builder.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/series_builder.tsx new file mode 100644 index 0000000000000..983c18af031d0 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/series_builder.tsx @@ -0,0 +1,201 @@ +/* + * 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, { useState } from 'react'; + +import { i18n } from '@kbn/i18n'; +import { EuiButton, EuiBasicTable, EuiSpacer, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import styled from 'styled-components'; +import { AppDataType, ReportViewTypeId, ReportViewTypes, SeriesUrl } from '../types'; +import { DataTypesCol } from './columns/data_types_col'; +import { ReportTypesCol } from './columns/report_types_col'; +import { ReportDefinitionCol } from './columns/report_definition_col'; +import { ReportFilters } from './columns/report_filters'; +import { ReportBreakdowns } from './columns/report_breakdowns'; +import { NEW_SERIES_KEY, useUrlStorage } from '../hooks/use_url_strorage'; +import { useIndexPatternContext } from '../hooks/use_default_index_pattern'; +import { getDefaultConfigs } from '../configurations/default_configs'; + +export const ReportTypes: Record> = { + synthetics: [ + { id: 'upd', label: 'Monitor duration' }, + { id: 'upp', label: 'Pings histogram' }, + ], + rum: [ + { id: 'pld', label: 'Performance distribution' }, + { id: 'kpi', label: 'KPI over time' }, + ], + apm: [ + { id: 'svl', label: 'Latency' }, + { id: 'tpt', label: 'Throughput' }, + ], + logs: [ + { + id: 'logs', + label: 'Logs Frequency', + }, + ], + metrics: [ + { id: 'cpu', label: 'CPU usage' }, + { id: 'mem', label: 'Memory usage' }, + { id: 'nwk', label: 'Network activity' }, + ], +}; + +export function SeriesBuilder() { + const { series, setSeries, allSeriesIds, removeSeries } = useUrlStorage(NEW_SERIES_KEY); + + const { dataType, reportType, reportDefinitions = {}, filters = [] } = series; + + const [isFlyoutVisible, setIsFlyoutVisible] = useState(!!series.dataType); + + const { indexPattern } = useIndexPatternContext(); + + const getDataViewSeries = () => { + return getDefaultConfigs({ + indexPattern, + reportType: reportType!, + seriesId: NEW_SERIES_KEY, + }); + }; + + const columns = [ + { + name: i18n.translate('xpack.observability.expView.seriesBuilder.dataType', { + defaultMessage: 'Data Type', + }), + width: '20%', + render: (val: string) => , + }, + { + name: i18n.translate('xpack.observability.expView.seriesBuilder.report', { + defaultMessage: 'Report', + }), + width: '20%', + render: (val: string) => ( + + ), + }, + { + name: i18n.translate('xpack.observability.expView.seriesBuilder.definition', { + defaultMessage: 'Definition', + }), + width: '30%', + render: (val: string) => + reportType && indexPattern ? ( + + ) : null, + }, + { + name: i18n.translate('xpack.observability.expView.seriesBuilder.filters', { + defaultMessage: 'Filters', + }), + width: '25%', + render: (val: string) => + reportType && indexPattern ? : null, + }, + { + name: i18n.translate('xpack.observability.expView.seriesBuilder.breakdown', { + defaultMessage: 'Breakdowns', + }), + width: '25%', + field: 'id', + render: (val: string) => + reportType && indexPattern ? ( + + ) : null, + }, + ]; + + const addSeries = () => { + if (reportType) { + const newSeriesId = `${ + reportDefinitions?.['service.name'] || + reportDefinitions?.['monitor.id'] || + ReportViewTypes[reportType] + }`; + + const newSeriesN = { + reportType, + time: { from: 'now-30m', to: 'now' }, + filters, + reportDefinitions, + } as SeriesUrl; + + setSeries(newSeriesId, newSeriesN).then(() => { + removeSeries(NEW_SERIES_KEY); + setIsFlyoutVisible(false); + }); + } + }; + + const items = [{ id: NEW_SERIES_KEY }]; + + let flyout; + + if (isFlyoutVisible) { + flyout = ( + + + + + + + {i18n.translate('xpack.observability.expView.seriesBuilder.add', { + defaultMessage: 'Add', + })} + + + + { + removeSeries(NEW_SERIES_KEY); + setIsFlyoutVisible(false); + }} + > + {i18n.translate('xpack.observability.expView.seriesBuilder.cancel', { + defaultMessage: 'Cancel', + })} + + + + + ); + } + + return ( +
+ {!isFlyoutVisible && ( + <> + setIsFlyoutVisible((prevState) => !prevState)} + disabled={allSeriesIds.length > 0} + > + {i18n.translate('xpack.observability.expView.seriesBuilder.addSeries', { + defaultMessage: 'Add series', + })} + + + + )} + {flyout} +
+ ); +} + +const BottomFlyout = styled.div` + height: 300px; +`; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/index.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/index.tsx new file mode 100644 index 0000000000000..71e3317ad6db8 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/index.tsx @@ -0,0 +1,55 @@ +/* + * 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 { EuiSuperDatePicker } from '@elastic/eui'; +import React, { useEffect } from 'react'; +import { useHasData } from '../../../../hooks/use_has_data'; +import { useUrlStorage } from '../hooks/use_url_strorage'; +import { useQuickTimeRanges } from '../../../../hooks/use_quick_time_ranges'; + +export interface TimePickerTime { + from: string; + to: string; +} + +export interface TimePickerQuickRange extends TimePickerTime { + display: string; +} + +interface Props { + seriesId: string; +} + +export function SeriesDatePicker({ seriesId }: Props) { + const { onRefreshTimeRange } = useHasData(); + + const commonlyUsedRanges = useQuickTimeRanges(); + + const { series, setSeries } = useUrlStorage(seriesId); + + function onTimeChange({ start, end }: { start: string; end: string }) { + onRefreshTimeRange(); + setSeries(seriesId, { ...series, time: { from: start, to: end } }); + } + + useEffect(() => { + if (!series || !series.time) { + setSeries(seriesId, { ...series, time: { from: 'now-5h', to: 'now' } }); + } + }, [seriesId, series, setSeries]); + + return ( + + ); +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/series_date_picker.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/series_date_picker.test.tsx new file mode 100644 index 0000000000000..acc9ba9658a08 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/series_date_picker.test.tsx @@ -0,0 +1,76 @@ +/* + * 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 { mockUrlStorage, mockUseHasData, render } from '../rtl_helpers'; +import { fireEvent, waitFor } from '@testing-library/react'; +import { SeriesDatePicker } from './index'; + +describe('SeriesDatePicker', function () { + it('should render properly', function () { + mockUrlStorage({ + data: { + 'uptime-pings-histogram': { + reportType: 'upp', + breakdown: 'monitor.status', + time: { from: 'now-30m', to: 'now' }, + }, + }, + }); + const { getByText } = render(); + + getByText('Last 30 minutes'); + }); + + it('should set defaults', async function () { + const { setSeries: setSeries1 } = mockUrlStorage({ + data: { + 'uptime-pings-histogram': { + reportType: 'upp', + breakdown: 'monitor.status', + }, + }, + } as any); + render(); + expect(setSeries1).toHaveBeenCalledTimes(1); + expect(setSeries1).toHaveBeenCalledWith('uptime-pings-histogram', { + breakdown: 'monitor.status', + reportType: 'upp', + time: { from: 'now-5h', to: 'now' }, + }); + }); + + it('should set series data', async function () { + const { setSeries } = mockUrlStorage({ + data: { + 'uptime-pings-histogram': { + reportType: 'upp', + breakdown: 'monitor.status', + time: { from: 'now-30m', to: 'now' }, + }, + }, + }); + + const { onRefreshTimeRange } = mockUseHasData(); + const { getByTestId } = render(); + + await waitFor(function () { + fireEvent.click(getByTestId('superDatePickerToggleQuickMenuButton')); + }); + + fireEvent.click(getByTestId('superDatePickerCommonlyUsed_Today')); + + expect(onRefreshTimeRange).toHaveBeenCalledTimes(1); + + expect(setSeries).toHaveBeenCalledWith('series-id', { + breakdown: 'monitor.status', + reportType: 'upp', + time: { from: 'now/d', to: 'now/d' }, + }); + expect(setSeries).toHaveBeenCalledTimes(1); + }); +}); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/actions_col.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/actions_col.tsx new file mode 100644 index 0000000000000..c6209381a4da1 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/actions_col.tsx @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { DataSeries } from '../../types'; +import { SeriesChartTypes } from './chart_types'; +import { MetricSelection } from './metric_selection'; + +interface Props { + series: DataSeries; +} + +export function ActionsCol({ series }: Props) { + return ( + + + + + {series.hasMetricType && ( + + + + )} + + ); +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.test.tsx new file mode 100644 index 0000000000000..654a93a08a7c8 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.test.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 React from 'react'; +import { fireEvent, screen } from '@testing-library/react'; +import { Breakdowns } from './breakdowns'; +import { mockIndexPattern, mockUrlStorage, render } from '../../rtl_helpers'; +import { NEW_SERIES_KEY } from '../../hooks/use_url_strorage'; +import { getDefaultConfigs } from '../../configurations/default_configs'; +import { USER_AGENT_OS } from '../../configurations/data/elasticsearch_fieldnames'; + +describe('Breakdowns', function () { + const dataViewSeries = getDefaultConfigs({ + reportType: 'pld', + indexPattern: mockIndexPattern, + seriesId: NEW_SERIES_KEY, + }); + + it('should render properly', async function () { + mockUrlStorage({}); + + render(); + + screen.getAllByText('Browser family'); + }); + + it('should call set series on change', function () { + const { setSeries } = mockUrlStorage({ breakdown: USER_AGENT_OS }); + + render(); + + screen.getAllByText('Operating system'); + + fireEvent.click(screen.getByTestId('seriesBreakdown')); + + fireEvent.click(screen.getByText('Browser family')); + + expect(setSeries).toHaveBeenCalledWith('series-id', { + breakdown: 'user_agent.name', + reportType: 'pld', + time: { from: 'now-15m', to: 'now' }, + }); + expect(setSeries).toHaveBeenCalledTimes(1); + }); +}); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.tsx new file mode 100644 index 0000000000000..0d34d7245725a --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.tsx @@ -0,0 +1,65 @@ +/* + * 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 { EuiSuperSelect } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FieldLabels } from '../../configurations/constants'; +import { useUrlStorage } from '../../hooks/use_url_strorage'; + +interface Props { + seriesId: string; + breakdowns: string[]; +} + +export function Breakdowns({ seriesId, breakdowns = [] }: Props) { + const { setSeries, series } = useUrlStorage(seriesId); + + const selectedBreakdown = series.breakdown; + const NO_BREAKDOWN = 'no_breakdown'; + + const onOptionChange = (optionId: string) => { + if (optionId === NO_BREAKDOWN) { + setSeries(seriesId, { + ...series, + breakdown: undefined, + }); + } else { + setSeries(seriesId, { + ...series, + breakdown: selectedBreakdown === optionId ? undefined : optionId, + }); + } + }; + + const items = breakdowns.map((breakdown) => ({ id: breakdown, label: FieldLabels[breakdown] })); + items.push({ + id: NO_BREAKDOWN, + label: i18n.translate('xpack.observability.exp.breakDownFilter.noBreakdown', { + defaultMessage: 'No breakdown', + }), + }); + + const options = items.map(({ id, label }) => ({ + inputDisplay: id === NO_BREAKDOWN ? label : {label}, + value: id, + dropdownDisplay: label, + })); + + return ( +
+ onOptionChange(value)} + data-test-subj={'seriesBreakdown'} + /> +
+ ); +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_types.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_types.test.tsx new file mode 100644 index 0000000000000..f291d0de4dac0 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_types.test.tsx @@ -0,0 +1,56 @@ +/* + * 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 { fireEvent, screen, waitFor } from '@testing-library/react'; +import { SeriesChartTypes, XYChartTypes } from './chart_types'; +import { mockUrlStorage, render } from '../../rtl_helpers'; + +describe.skip('SeriesChartTypes', function () { + it('should render properly', async function () { + mockUrlStorage({}); + + render(); + + await waitFor(() => { + screen.getByText(/chart type/i); + }); + }); + + it('should call set series on change', async function () { + const { setSeries } = mockUrlStorage({}); + + render(); + + await waitFor(() => { + screen.getByText(/chart type/i); + }); + + fireEvent.click(screen.getByText(/chart type/i)); + fireEvent.click(screen.getByTestId('lnsXY_seriesType-bar_stacked')); + + expect(setSeries).toHaveBeenNthCalledWith(1, 'performance-distribution', { + breakdown: 'user_agent.name', + reportType: 'pld', + seriesType: 'bar_stacked', + time: { from: 'now-15m', to: 'now' }, + }); + expect(setSeries).toHaveBeenCalledTimes(3); + }); + + describe('XYChartTypes', function () { + it('should render properly', async function () { + mockUrlStorage({}); + + render(); + + await waitFor(() => { + screen.getByText(/chart type/i); + }); + }); + }); +}); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_types.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_types.tsx new file mode 100644 index 0000000000000..017655053eef2 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_types.tsx @@ -0,0 +1,149 @@ +/* + * 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, { useState } from 'react'; + +import { + EuiButton, + EuiButtonGroup, + EuiButtonIcon, + EuiLoadingSpinner, + EuiPopover, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import styled from 'styled-components'; +import { useKibana } from '../../../../../../../../../src/plugins/kibana_react/public'; +import { ObservabilityPublicPluginsStart } from '../../../../../plugin'; +import { useFetcher } from '../../../../..'; +import { useUrlStorage } from '../../hooks/use_url_strorage'; +import { SeriesType } from '../../../../../../../lens/public'; + +export function SeriesChartTypes({ + seriesId, + defaultChartType, +}: { + seriesId: string; + defaultChartType: SeriesType; +}) { + const { series, setSeries, allSeries } = useUrlStorage(seriesId); + + const seriesType = series?.seriesType ?? defaultChartType; + + const onChange = (value: SeriesType) => { + Object.keys(allSeries).forEach((seriesKey) => { + const seriesN = allSeries[seriesKey]; + + setSeries(seriesKey, { ...seriesN, seriesType: value }); + }); + }; + + return ( + + ); +} + +export interface XYChartTypesProps { + onChange: (value: SeriesType) => void; + value: SeriesType; + label?: string; + includeChartTypes?: string[]; + excludeChartTypes?: string[]; +} + +export function XYChartTypes({ + onChange, + value, + label, + includeChartTypes, + excludeChartTypes, +}: XYChartTypesProps) { + const [isOpen, setIsOpen] = useState(false); + + const { + services: { lens }, + } = useKibana(); + + const { data = [], loading } = useFetcher(() => lens.getXyVisTypes(), [lens]); + + let vizTypes = data ?? []; + + if ((excludeChartTypes ?? []).length > 0) { + vizTypes = vizTypes.filter(({ id }) => !excludeChartTypes?.includes(id)); + } + + if ((includeChartTypes ?? []).length > 0) { + vizTypes = vizTypes.filter(({ id }) => includeChartTypes?.includes(id)); + } + + return loading ? ( + + ) : ( + id === value)?.icon} + onClick={() => { + setIsOpen((prevState) => !prevState); + }} + > + {label} + + ) : ( + id === value)?.label} + iconType={vizTypes.find(({ id }) => id === value)?.icon!} + onClick={() => { + setIsOpen((prevState) => !prevState); + }} + /> + ) + } + closePopover={() => setIsOpen(false)} + > + ({ + id: t.id, + label: t.label, + title: t.label, + iconType: t.icon || 'empty', + 'data-test-subj': `lnsXY_seriesType-${t.id}`, + }))} + idSelected={value} + onChange={(valueN: string) => { + onChange(valueN as SeriesType); + }} + /> + + ); +} + +const ButtonGroup = styled(EuiButtonGroup)` + &&& { + .euiButtonGroupButton-isSelected { + background-color: #a5a9b1 !important; + } + } +`; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/date_picker_col.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/date_picker_col.tsx new file mode 100644 index 0000000000000..8c99de51978a7 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/date_picker_col.tsx @@ -0,0 +1,20 @@ +/* + * 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 { SeriesDatePicker } from '../../series_date_picker'; + +interface Props { + seriesId: string; +} +export function DatePickerCol({ seriesId }: Props) { + return ( +
+ +
+ ); +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.test.tsx new file mode 100644 index 0000000000000..edd5546f13940 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.test.tsx @@ -0,0 +1,93 @@ +/* + * 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 { fireEvent, screen } from '@testing-library/react'; +import { FilterExpanded } from './filter_expanded'; +import { mockUrlStorage, mockUseValuesList, render } from '../../rtl_helpers'; +import { USER_AGENT_NAME } from '../../configurations/data/elasticsearch_fieldnames'; + +describe('FilterExpanded', function () { + it('should render properly', async function () { + mockUrlStorage({ filters: [{ field: USER_AGENT_NAME, values: ['Chrome'] }] }); + + render( + + ); + + screen.getByText('Browser Family'); + }); + it('should call go back on click', async function () { + mockUrlStorage({ filters: [{ field: USER_AGENT_NAME, values: ['Chrome'] }] }); + const goBack = jest.fn(); + + render( + + ); + + fireEvent.click(screen.getByText('Browser Family')); + + expect(goBack).toHaveBeenCalledTimes(1); + expect(goBack).toHaveBeenCalledWith(); + }); + + it('should call useValuesList on load', async function () { + mockUrlStorage({ filters: [{ field: USER_AGENT_NAME, values: ['Chrome'] }] }); + + const { spy } = mockUseValuesList(['Chrome', 'Firefox']); + + const goBack = jest.fn(); + + render( + + ); + + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toBeCalledWith( + expect.objectContaining({ + time: { from: 'now-15m', to: 'now' }, + sourceField: USER_AGENT_NAME, + }) + ); + }); + it('should filter display values', async function () { + mockUrlStorage({ filters: [{ field: USER_AGENT_NAME, values: ['Chrome'] }] }); + + mockUseValuesList(['Chrome', 'Firefox']); + + render( + + ); + + expect(screen.queryByText('Firefox')).toBeTruthy(); + + fireEvent.input(screen.getByRole('searchbox'), { target: { value: 'ch' } }); + + expect(screen.queryByText('Firefox')).toBeFalsy(); + expect(screen.getByText('Chrome')).toBeTruthy(); + }); +}); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.tsx new file mode 100644 index 0000000000000..280912dd0902f --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.tsx @@ -0,0 +1,100 @@ +/* + * 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, { useState, Fragment } from 'react'; +import { + EuiFieldSearch, + EuiSpacer, + EuiButtonEmpty, + EuiLoadingSpinner, + EuiFilterGroup, +} from '@elastic/eui'; +import { useIndexPatternContext } from '../../hooks/use_default_index_pattern'; +import { useUrlStorage } from '../../hooks/use_url_strorage'; +import { UrlFilter } from '../../types'; +import { FilterValueButton } from './filter_value_btn'; +import { useValuesList } from '../../../../../hooks/use_values_list'; + +interface Props { + seriesId: string; + label: string; + field: string; + goBack: () => void; + nestedField?: string; +} + +export function FilterExpanded({ seriesId, field, label, goBack, nestedField }: Props) { + const { indexPattern } = useIndexPatternContext(); + + const [value, setValue] = useState(''); + + const [isOpen, setIsOpen] = useState({ value: '', negate: false }); + + const { series } = useUrlStorage(seriesId); + + const { values, loading } = useValuesList({ + sourceField: field, + time: series.time, + indexPattern, + }); + + const filters = series?.filters ?? []; + + const currFilter: UrlFilter | undefined = filters.find(({ field: fd }) => field === fd); + + const displayValues = (values || []).filter((opt) => + opt.toLowerCase().includes(value.toLowerCase()) + ); + + return ( + <> + goBack()}> + {label} + + { + setValue(evt.target.value); + }} + /> + + {loading && ( +
+ +
+ )} + {displayValues.map((opt) => ( + + + + + + + + ))} + + ); +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.test.tsx new file mode 100644 index 0000000000000..7f76c9ea999ee --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.test.tsx @@ -0,0 +1,238 @@ +/* + * 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 { fireEvent, screen } from '@testing-library/react'; +import { FilterValueButton } from './filter_value_btn'; +import { mockUrlStorage, mockUseSeriesFilter, mockUseValuesList, render } from '../../rtl_helpers'; +import { + USER_AGENT_NAME, + USER_AGENT_VERSION, +} from '../../configurations/data/elasticsearch_fieldnames'; + +describe('FilterValueButton', function () { + it('should render properly', async function () { + render( + + ); + + screen.getByText('Chrome'); + }); + + it('should render display negate state', async function () { + render( + + ); + + screen.getByText('Not Chrome'); + screen.getByTitle('Not Chrome'); + const btn = screen.getByRole('button'); + expect(btn.classList).toContain('euiButtonEmpty--danger'); + }); + + it('should call set filter on click', async function () { + const { setFilter, removeFilter } = mockUseSeriesFilter(); + + render( + + ); + + fireEvent.click(screen.getByText('Not Chrome')); + + expect(removeFilter).toHaveBeenCalledTimes(0); + expect(setFilter).toHaveBeenCalledTimes(1); + + expect(setFilter).toHaveBeenCalledWith({ + field: 'user_agent.name', + negate: true, + value: 'Chrome', + }); + }); + it('should remove filter on click if already selected', async function () { + mockUrlStorage({}); + const { removeFilter } = mockUseSeriesFilter(); + + render( + + ); + + fireEvent.click(screen.getByText('Chrome')); + + expect(removeFilter).toHaveBeenCalledWith({ + field: 'user_agent.name', + negate: false, + value: 'Chrome', + }); + }); + + it('should change filter on negated one', async function () { + const { removeFilter } = mockUseSeriesFilter(); + + render( + + ); + + fireEvent.click(screen.getByText('Not Chrome')); + + expect(removeFilter).toHaveBeenCalledWith({ + field: 'user_agent.name', + negate: true, + value: 'Chrome', + }); + }); + + it('should force open nested', async function () { + mockUseSeriesFilter(); + const { spy } = mockUseValuesList(); + + render( + + ); + + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toBeCalledWith( + expect.objectContaining({ + filters: [ + { + term: { + [USER_AGENT_NAME]: 'Chrome', + }, + }, + ], + sourceField: 'user_agent.version', + }) + ); + }); + it('should set isNestedOpen on click', async function () { + mockUseSeriesFilter(); + const { spy } = mockUseValuesList(); + + render( + + ); + + expect(spy).toHaveBeenCalledTimes(2); + expect(spy).toBeCalledWith( + expect.objectContaining({ + filters: [ + { + term: { + [USER_AGENT_NAME]: 'Chrome', + }, + }, + ], + sourceField: USER_AGENT_VERSION, + }) + ); + }); + + it('should set call setIsNestedOpen on click selected', async function () { + mockUseSeriesFilter(); + mockUseValuesList(); + + const setIsNestedOpen = jest.fn(); + + render( + + ); + + fireEvent.click(screen.getByText('Chrome')); + + expect(setIsNestedOpen).toHaveBeenCalledTimes(1); + expect(setIsNestedOpen).toHaveBeenCalledWith({ negate: false, value: '' }); + }); + + it('should set call setIsNestedOpen on click not selected', async function () { + mockUseSeriesFilter(); + mockUseValuesList(); + + const setIsNestedOpen = jest.fn(); + + render( + + ); + + fireEvent.click(screen.getByText('Not Chrome')); + + expect(setIsNestedOpen).toHaveBeenCalledTimes(1); + expect(setIsNestedOpen).toHaveBeenCalledWith({ negate: true, value: 'Chrome' }); + }); +}); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.tsx new file mode 100644 index 0000000000000..42cdfd595e66b --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.tsx @@ -0,0 +1,117 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { i18n } from '@kbn/i18n'; +import React, { useMemo } from 'react'; +import { EuiFilterButton, hexToRgb } from '@elastic/eui'; +import { useIndexPatternContext } from '../../hooks/use_default_index_pattern'; +import { useUrlStorage } from '../../hooks/use_url_strorage'; +import { useSeriesFilters } from '../../hooks/use_series_filters'; +import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common'; +import FieldValueSuggestions from '../../../field_value_suggestions'; + +interface Props { + value: string; + field: string; + allSelectedValues?: string[]; + negate: boolean; + nestedField?: string; + seriesId: string; + isNestedOpen: { + value: string; + negate: boolean; + }; + setIsNestedOpen: (val: { value: string; negate: boolean }) => void; +} + +export function FilterValueButton({ + isNestedOpen, + setIsNestedOpen, + value, + field, + negate, + seriesId, + nestedField, + allSelectedValues, +}: Props) { + const { series } = useUrlStorage(seriesId); + + const { indexPattern } = useIndexPatternContext(); + + const { setFilter, removeFilter } = useSeriesFilters({ seriesId }); + + const hasActiveFilters = (allSelectedValues ?? []).includes(value); + + const button = ( + { + if (hasActiveFilters) { + removeFilter({ field, value, negate }); + } else { + setFilter({ field, value, negate }); + } + if (!hasActiveFilters) { + setIsNestedOpen({ value, negate }); + } else { + setIsNestedOpen({ value: '', negate }); + } + }} + > + {negate + ? i18n.translate('xpack.observability.expView.filterValueButton.negate', { + defaultMessage: 'Not {value}', + values: { value }, + }) + : value} + + ); + + const onNestedChange = (val?: string) => { + setFilter({ field: nestedField!, value: val! }); + setIsNestedOpen({ value: '', negate }); + }; + + const forceOpenNested = isNestedOpen?.value === value && isNestedOpen.negate === negate; + + const filters = useMemo(() => { + return [ + { + term: { + [field]: value, + }, + }, + ]; + }, [field, value]); + + return nestedField && forceOpenNested ? ( + + ) : ( + button + ); +} + +const FilterButton = euiStyled(EuiFilterButton)` + background-color: rgba(${(props) => { + const color = props.hasActiveFilters + ? props.color === 'danger' + ? hexToRgb(props.theme.eui.euiColorDanger) + : hexToRgb(props.theme.eui.euiColorPrimary) + : 'initial'; + return `${color[0]}, ${color[1]}, ${color[2]}, 0.1`; + }}); +`; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/metric_selection.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/metric_selection.test.tsx new file mode 100644 index 0000000000000..ced04f0a59c8c --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/metric_selection.test.tsx @@ -0,0 +1,112 @@ +/* + * 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 { fireEvent, screen } from '@testing-library/react'; +import { mockUrlStorage, render } from '../../rtl_helpers'; +import { MetricSelection } from './metric_selection'; + +describe('MetricSelection', function () { + it('should render properly', function () { + render(); + + screen.getByText('Average'); + }); + + it('should display selected value', function () { + mockUrlStorage({ + data: { + 'performance-distribution': { + reportType: 'kpi', + metric: 'median', + time: { from: 'now-15m', to: 'now' }, + }, + }, + }); + + render(); + + screen.getByText('Median'); + }); + + it('should be disabled on disabled state', function () { + render(); + + const btn = screen.getByRole('button'); + + expect(btn.classList).toContain('euiButton-isDisabled'); + }); + + it('should call set series on change', function () { + const { setSeries } = mockUrlStorage({ + data: { + 'performance-distribution': { + reportType: 'kpi', + metric: 'median', + time: { from: 'now-15m', to: 'now' }, + }, + }, + }); + + render(); + + fireEvent.click(screen.getByText('Median')); + + screen.getByText('Chart metric group'); + + fireEvent.click(screen.getByText('95th Percentile')); + + expect(setSeries).toHaveBeenNthCalledWith(1, 'performance-distribution', { + metric: '95th', + reportType: 'kpi', + time: { from: 'now-15m', to: 'now' }, + }); + // FIXME This is a bug in EUI EuiButtonGroup calls on change multiple times + // This should be one https://github.com/elastic/eui/issues/4629 + expect(setSeries).toHaveBeenCalledTimes(3); + }); + + it('should call set series on change for all series', function () { + const { setSeries } = mockUrlStorage({ + data: { + 'page-views': { + reportType: 'kpi', + metric: 'median', + time: { from: 'now-15m', to: 'now' }, + }, + 'performance-distribution': { + reportType: 'kpi', + metric: 'median', + time: { from: 'now-15m', to: 'now' }, + }, + }, + }); + + render(); + + fireEvent.click(screen.getByText('Median')); + + screen.getByText('Chart metric group'); + + fireEvent.click(screen.getByText('95th Percentile')); + + expect(setSeries).toHaveBeenNthCalledWith(1, 'page-views', { + metric: '95th', + reportType: 'kpi', + time: { from: 'now-15m', to: 'now' }, + }); + + expect(setSeries).toHaveBeenNthCalledWith(2, 'performance-distribution', { + metric: '95th', + reportType: 'kpi', + time: { from: 'now-15m', to: 'now' }, + }); + // FIXME This is a bug in EUI EuiButtonGroup calls on change multiple times + // This should be one https://github.com/elastic/eui/issues/4629 + expect(setSeries).toHaveBeenCalledTimes(6); + }); +}); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/metric_selection.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/metric_selection.tsx new file mode 100644 index 0000000000000..e01e371b5eeeb --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/metric_selection.tsx @@ -0,0 +1,86 @@ +/* + * 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, { useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiButton, EuiButtonGroup, EuiPopover } from '@elastic/eui'; +import { useUrlStorage } from '../../hooks/use_url_strorage'; +import { OperationType } from '../../../../../../../lens/public'; + +const toggleButtons = [ + { + id: `avg`, + label: i18n.translate('xpack.observability.expView.metricsSelect.average', { + defaultMessage: 'Average', + }), + }, + { + id: `median`, + label: i18n.translate('xpack.observability.expView.metricsSelect.median', { + defaultMessage: 'Median', + }), + }, + { + id: `95th`, + label: i18n.translate('xpack.observability.expView.metricsSelect.9thPercentile', { + defaultMessage: '95th Percentile', + }), + }, + { + id: `99th`, + label: i18n.translate('xpack.observability.expView.metricsSelect.99thPercentile', { + defaultMessage: '99th Percentile', + }), + }, +]; + +export function MetricSelection({ + seriesId, + isDisabled, +}: { + seriesId: string; + isDisabled: boolean; +}) { + const { series, setSeries, allSeries } = useUrlStorage(seriesId); + + const [isOpen, setIsOpen] = useState(false); + + const [toggleIdSelected, setToggleIdSelected] = useState(series?.metric ?? 'avg'); + + const onChange = (optionId: OperationType) => { + setToggleIdSelected(optionId); + + Object.keys(allSeries).forEach((seriesKey) => { + const seriesN = allSeries[seriesKey]; + + setSeries(seriesKey, { ...seriesN, metric: optionId }); + }); + }; + const button = ( + setIsOpen((prevState) => !prevState)} + size="s" + color="text" + isDisabled={isDisabled} + > + {toggleButtons.find(({ id }) => id === toggleIdSelected)!.label} + + ); + + return ( + setIsOpen(false)}> + onChange(id as OperationType)} + /> + + ); +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/remove_series.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/remove_series.tsx new file mode 100644 index 0000000000000..67aebed943326 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/remove_series.tsx @@ -0,0 +1,35 @@ +/* + * 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'; +import React from 'react'; +import { EuiButtonIcon } from '@elastic/eui'; +import { DataSeries } from '../../types'; +import { useUrlStorage } from '../../hooks/use_url_strorage'; + +interface Props { + series: DataSeries; +} + +export function RemoveSeries({ series }: Props) { + const { removeSeries } = useUrlStorage(); + + const onClick = () => { + removeSeries(series.id); + }; + return ( + + ); +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_filter.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_filter.tsx new file mode 100644 index 0000000000000..24b65d2adb38e --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_filter.tsx @@ -0,0 +1,139 @@ +/* + * 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'; +import React, { useState, Fragment } from 'react'; +import { + EuiButton, + EuiPopover, + EuiSpacer, + EuiButtonEmpty, + EuiFlexItem, + EuiFlexGroup, +} from '@elastic/eui'; +import { FilterExpanded } from './filter_expanded'; +import { DataSeries } from '../../types'; +import { FieldLabels } from '../../configurations/constants'; +import { SelectedFilters } from '../selected_filters'; +import { NEW_SERIES_KEY, useUrlStorage } from '../../hooks/use_url_strorage'; + +interface Props { + seriesId: string; + defaultFilters: DataSeries['defaultFilters']; + series: DataSeries; + isNew?: boolean; +} + +export interface Field { + label: string; + field: string; + nested?: string; +} + +export function SeriesFilter({ series, isNew, seriesId, defaultFilters = [] }: Props) { + const [isPopoverVisible, setIsPopoverVisible] = useState(false); + + const [selectedField, setSelectedField] = useState(); + + const options = defaultFilters.map((field) => { + if (typeof field === 'string') { + return { label: FieldLabels[field], field }; + } + return { label: FieldLabels[field.field], field: field.field, nested: field.nested }; + }); + const disabled = seriesId === NEW_SERIES_KEY && !isNew; + + const { setSeries, series: urlSeries } = useUrlStorage(seriesId); + + const button = ( + { + setIsPopoverVisible(true); + }} + isDisabled={disabled} + size="s" + > + {i18n.translate('xpack.observability.expView.seriesEditor.addFilter', { + defaultMessage: 'Add filter', + })} + + ); + + const mainPanel = ( + <> + + {options.map((opt) => ( + + { + setSelectedField(opt); + }} + > + {opt.label} + + + + ))} + + ); + + const childPanel = selectedField ? ( + { + setSelectedField(undefined); + }} + /> + ) : null; + + const closePopover = () => { + setIsPopoverVisible(false); + setSelectedField(undefined); + }; + + return ( + + {!disabled && } + + + {!selectedField ? mainPanel : childPanel} + + + {(urlSeries.filters ?? []).length > 0 && ( + + { + setSeries(seriesId, { ...urlSeries, filters: undefined }); + }} + isDisabled={disabled} + size="s" + > + {i18n.translate('xpack.observability.expView.seriesEditor.clearFilter', { + defaultMessage: 'Clear filters', + })} + + + )} + + ); +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.test.tsx new file mode 100644 index 0000000000000..5770a7e209f06 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.test.tsx @@ -0,0 +1,33 @@ +/* + * 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 { screen, waitFor } from '@testing-library/react'; +import { mockIndexPattern, mockUrlStorage, render } from '../rtl_helpers'; +import { SelectedFilters } from './selected_filters'; +import { getDefaultConfigs } from '../configurations/default_configs'; +import { NEW_SERIES_KEY } from '../hooks/use_url_strorage'; +import { USER_AGENT_NAME } from '../configurations/data/elasticsearch_fieldnames'; + +describe('SelectedFilters', function () { + const dataViewSeries = getDefaultConfigs({ + reportType: 'pld', + indexPattern: mockIndexPattern, + seriesId: NEW_SERIES_KEY, + }); + + it('should render properly', async function () { + mockUrlStorage({ filters: [{ field: USER_AGENT_NAME, values: ['Chrome'] }] }); + + render(); + + await waitFor(() => { + screen.getByText('Chrome'); + screen.getByTitle('Filter: Browser family: Chrome. Select for more filter actions.'); + }); + }); +}); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.tsx new file mode 100644 index 0000000000000..be8b1feb4d723 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.tsx @@ -0,0 +1,96 @@ +/* + * 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, { Fragment } from 'react'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { NEW_SERIES_KEY, useUrlStorage } from '../hooks/use_url_strorage'; +import { FilterLabel } from '../components/filter_label'; +import { DataSeries, UrlFilter } from '../types'; +import { useIndexPatternContext } from '../hooks/use_default_index_pattern'; +import { useSeriesFilters } from '../hooks/use_series_filters'; +import { getFiltersFromDefs } from '../hooks/use_lens_attributes'; + +interface Props { + seriesId: string; + series: DataSeries; + isNew?: boolean; +} +export function SelectedFilters({ seriesId, isNew, series: dataSeries }: Props) { + const { series } = useUrlStorage(seriesId); + + const { reportDefinitions = {} } = series; + + const { labels } = dataSeries; + + const filters: UrlFilter[] = series.filters ?? []; + + let definitionFilters: UrlFilter[] = getFiltersFromDefs(reportDefinitions, dataSeries); + + // we don't want to display report definition filters in new series view + if (seriesId === NEW_SERIES_KEY && isNew) { + definitionFilters = []; + } + + const { removeFilter } = useSeriesFilters({ seriesId }); + + const { indexPattern } = useIndexPatternContext(); + + return (filters.length > 0 || definitionFilters.length > 0) && indexPattern ? ( + + + {filters.map(({ field, values, notValues }) => ( + + {(values ?? []).map((val) => ( + + removeFilter({ field, value: val, negate: false })} + negate={false} + /> + + ))} + {(notValues ?? []).map((val) => ( + + removeFilter({ field, value: val, negate: true })} + /> + + ))} + + ))} + + {definitionFilters.map(({ field, values }) => ( + + {(values ?? []).map((val) => ( + + { + // FIXME handle this use case + }} + negate={false} + definitionFilter={true} + /> + + ))} + + ))} + + + ) : null; +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/series_editor.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/series_editor.tsx new file mode 100644 index 0000000000000..2d423c9aee3fc --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/series_editor.tsx @@ -0,0 +1,139 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { EuiBasicTable, EuiIcon, EuiSpacer, EuiText } from '@elastic/eui'; +import { SeriesFilter } from './columns/series_filter'; +import { ActionsCol } from './columns/actions_col'; +import { Breakdowns } from './columns/breakdowns'; +import { DataSeries } from '../types'; +import { SeriesBuilder } from '../series_builder/series_builder'; +import { NEW_SERIES_KEY, useUrlStorage } from '../hooks/use_url_strorage'; +import { getDefaultConfigs } from '../configurations/default_configs'; +import { DatePickerCol } from './columns/date_picker_col'; +import { RemoveSeries } from './columns/remove_series'; +import { useIndexPatternContext } from '../hooks/use_default_index_pattern'; + +export function SeriesEditor() { + const { allSeries, firstSeriesId } = useUrlStorage(); + + const columns = [ + { + name: i18n.translate('xpack.observability.expView.seriesEditor.name', { + defaultMessage: 'Name', + }), + field: 'id', + width: '15%', + render: (val: string) => ( + + {' '} + {val === NEW_SERIES_KEY ? 'new-series-preview' : val} + + ), + }, + ...(firstSeriesId !== NEW_SERIES_KEY + ? [ + { + name: i18n.translate('xpack.observability.expView.seriesEditor.filters', { + defaultMessage: 'Filters', + }), + field: 'defaultFilters', + width: '25%', + render: (defaultFilters: string[], series: DataSeries) => ( + + ), + }, + { + name: i18n.translate('xpack.observability.expView.seriesEditor.breakdowns', { + defaultMessage: 'Breakdowns', + }), + field: 'breakdowns', + width: '15%', + render: (val: string[], item: DataSeries) => ( + + ), + }, + { + name: '', + align: 'center' as const, + width: '15%', + field: 'id', + render: (val: string, item: DataSeries) => , + }, + ] + : []), + { + name: ( +
+ {i18n.translate('xpack.observability.expView.seriesEditor.time', { + defaultMessage: 'Time', + })} +
+ ), + width: '20%', + field: 'id', + align: 'right' as const, + render: (val: string, item: DataSeries) => , + }, + + ...(firstSeriesId !== NEW_SERIES_KEY + ? [ + { + name: i18n.translate('xpack.observability.expView.seriesEditor.actions', { + defaultMessage: 'Actions', + }), + align: 'center' as const, + width: '5%', + field: 'id', + render: (val: string, item: DataSeries) => , + }, + ] + : []), + ]; + + const allSeriesKeys = Object.keys(allSeries); + + const items: DataSeries[] = []; + + const { indexPattern } = useIndexPatternContext(); + + allSeriesKeys.forEach((seriesKey) => { + const series = allSeries[seriesKey]; + if (series.reportType && indexPattern) { + items.push( + getDefaultConfigs({ + indexPattern, + reportType: series.reportType, + seriesId: seriesKey, + }) + ); + } + }); + + return ( + <> + + (firstSeriesId === NEW_SERIES_KEY ? {} : { height: 100 })} + noItemsMessage={i18n.translate('xpack.observability.expView.seriesEditor.notFound', { + defaultMessage: 'No series found, please add a series.', + })} + cellProps={{ + style: { + verticalAlign: 'top', + }, + }} + /> + + + + ); +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts new file mode 100644 index 0000000000000..444e0ddaecb4a --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts @@ -0,0 +1,89 @@ +/* + * 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 { PaletteOutput } from 'src/plugins/charts/public'; +import { + LastValueIndexPatternColumn, + DateHistogramIndexPatternColumn, + SeriesType, + OperationType, + IndexPatternColumn, +} from '../../../../../lens/public'; + +import { PersistableFilter } from '../../../../../lens/common'; +import { IIndexPattern } from '../../../../../../../src/plugins/data/common/index_patterns'; + +export const ReportViewTypes = { + pld: 'page-load-dist', + kpi: 'kpi-trends', + upd: 'uptime-duration', + upp: 'uptime-pings', + svl: 'service-latency', + tpt: 'service-throughput', + logs: 'logs-frequency', + cpu: 'cpu-usage', + mem: 'memory-usage', + nwk: 'network-activity', +} as const; + +type ValueOf = T[keyof T]; + +export type ReportViewTypeId = keyof typeof ReportViewTypes; + +export type ReportViewType = ValueOf; + +export interface ReportDefinition { + field: string; + required?: boolean; + custom?: boolean; + defaultValue?: string; + options?: Array<{ field: string; label: string; description?: string }>; +} + +export interface DataSeries { + reportType: ReportViewType; + id: string; + xAxisColumn: Partial | Partial; + yAxisColumn: Partial; + + breakdowns: string[]; + defaultSeriesType: SeriesType; + defaultFilters: Array; + seriesTypes: SeriesType[]; + filters?: PersistableFilter[]; + reportDefinitions: ReportDefinition[]; + labels: Record; + hasMetricType: boolean; + palette?: PaletteOutput; +} + +export interface SeriesUrl { + time: { + to: string; + from: string; + }; + breakdown?: string; + filters?: UrlFilter[]; + seriesType?: SeriesType; + reportType: ReportViewTypeId; + metric?: OperationType; + dataType?: AppDataType; + reportDefinitions?: Record; +} + +export interface UrlFilter { + field: string; + values?: string[]; + notValues?: string[]; +} + +export interface ConfigProps { + seriesId: string; + indexPattern: IIndexPattern; +} + +export type AppDataType = 'synthetics' | 'rum' | 'logs' | 'metrics' | 'apm'; diff --git a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/field_value_selection.tsx b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/field_value_selection.tsx index b2c682dc58937..a44aab2da85be 100644 --- a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/field_value_selection.tsx +++ b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/field_value_selection.tsx @@ -15,14 +15,19 @@ import { EuiSelectableOption, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { PopoverAnchorPosition } from '@elastic/eui/src/components/popover/popover'; export interface FieldValueSelectionProps { value?: string; label: string; - loading: boolean; + loading?: boolean; onChange: (val?: string) => void; values?: string[]; setQuery: Dispatch>; + anchorPosition?: PopoverAnchorPosition; + forceOpen?: boolean; + button?: JSX.Element; + width?: number; } const formatOptions = (values?: string[], value?: string): EuiSelectableOption[] => { @@ -38,6 +43,10 @@ export function FieldValueSelection({ loading, values, setQuery, + button, + width, + forceOpen, + anchorPosition, onChange: onSelectionChange, }: FieldValueSelectionProps) { const [options, setOptions] = useState(formatOptions(values, value)); @@ -63,8 +72,9 @@ export function FieldValueSelection({ setQuery((evt.target as HTMLInputElement).value); }; - const button = ( + const anchorButton = ( void; + filters: ESFilter[]; + anchorPosition?: PopoverAnchorPosition; + time?: { from: string; to: string }; + forceOpen?: boolean; + button?: JSX.Element; + width?: number; } export function FieldValueSuggestions({ @@ -25,12 +33,18 @@ export function FieldValueSuggestions({ label, indexPattern, value, + filters, + button, + time, + width, + forceOpen, + anchorPosition, onChange: onSelectionChange, }: FieldValueSuggestionsProps) { const [query, setQuery] = useState(''); const [debouncedValue, setDebouncedValue] = useState(''); - const { values, loading } = useValuesList({ indexPattern, query, sourceField }); + const { values, loading } = useValuesList({ indexPattern, query, sourceField, filters, time }); useDebounce( () => { @@ -48,6 +62,10 @@ export function FieldValueSuggestions({ setQuery={setDebouncedValue} loading={loading} value={value} + button={button} + forceOpen={forceOpen} + anchorPosition={anchorPosition} + width={width} /> ); } diff --git a/x-pack/plugins/observability/public/context/has_data_context.test.tsx b/x-pack/plugins/observability/public/context/has_data_context.test.tsx index 5e48860a9b049..01655c0d7b2d7 100644 --- a/x-pack/plugins/observability/public/context/has_data_context.test.tsx +++ b/x-pack/plugins/observability/public/context/has_data_context.test.tsx @@ -17,12 +17,19 @@ import { HasData, ObservabilityFetchDataPlugins } from '../typings/fetch_overvie import { HasDataContextProvider } from './has_data_context'; import * as pluginContext from '../hooks/use_plugin_context'; import { PluginContextValue } from './plugin_context'; +import { Router } from 'react-router-dom'; +import { createMemoryHistory } from 'history'; const relativeStart = '2020-10-08T06:00:00.000Z'; const relativeEnd = '2020-10-08T07:00:00.000Z'; function wrapper({ children }: { children: React.ReactElement }) { - return {children}; + const history = createMemoryHistory(); + return ( + + {children} + + ); } function unregisterAll() { diff --git a/x-pack/plugins/observability/public/context/has_data_context.tsx b/x-pack/plugins/observability/public/context/has_data_context.tsx index 085b7fd7ba028..a2628d37828a4 100644 --- a/x-pack/plugins/observability/public/context/has_data_context.tsx +++ b/x-pack/plugins/observability/public/context/has_data_context.tsx @@ -7,6 +7,7 @@ import { uniqueId } from 'lodash'; import React, { createContext, useEffect, useState } from 'react'; +import { useRouteMatch } from 'react-router-dom'; import { Alert } from '../../../alerting/common'; import { getDataHandler } from '../data_handler'; import { FETCH_STATUS } from '../hooks/use_fetcher'; @@ -41,35 +42,38 @@ export function HasDataContextProvider({ children }: { children: React.ReactNode const [hasData, setHasData] = useState({}); + const isExploratoryView = useRouteMatch('/exploratory-view'); + useEffect( () => { - apps.forEach(async (app) => { - try { - if (app !== 'alert') { - const params = - app === 'ux' - ? { absoluteTime: { start: absoluteStart, end: absoluteEnd } } - : undefined; - - const result = await getDataHandler(app)?.hasData(params); + if (!isExploratoryView) + apps.forEach(async (app) => { + try { + if (app !== 'alert') { + const params = + app === 'ux' + ? { absoluteTime: { start: absoluteStart, end: absoluteEnd } } + : undefined; + + const result = await getDataHandler(app)?.hasData(params); + setHasData((prevState) => ({ + ...prevState, + [app]: { + hasData: result, + status: FETCH_STATUS.SUCCESS, + }, + })); + } + } catch (e) { setHasData((prevState) => ({ ...prevState, [app]: { - hasData: result, - status: FETCH_STATUS.SUCCESS, + hasData: undefined, + status: FETCH_STATUS.FAILURE, }, })); } - } catch (e) { - setHasData((prevState) => ({ - ...prevState, - [app]: { - hasData: undefined, - status: FETCH_STATUS.FAILURE, - }, - })); - } - }); + }); }, // eslint-disable-next-line react-hooks/exhaustive-deps [] diff --git a/x-pack/plugins/observability/public/hooks/use_breadcrumbs.ts b/x-pack/plugins/observability/public/hooks/use_breadcrumbs.ts new file mode 100644 index 0000000000000..a354ac8a07f05 --- /dev/null +++ b/x-pack/plugins/observability/public/hooks/use_breadcrumbs.ts @@ -0,0 +1,71 @@ +/* + * 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 { ChromeBreadcrumb } from 'kibana/public'; +import { i18n } from '@kbn/i18n'; +import { MouseEvent, useEffect } from 'react'; +import { EuiBreadcrumb } from '@elastic/eui'; +import { stringify } from 'query-string'; +import { useKibana } from '../../../../../src/plugins/kibana_react/public'; +import { useQueryParams } from './use_query_params'; + +const EMPTY_QUERY = '?'; + +function handleBreadcrumbClick( + breadcrumbs: ChromeBreadcrumb[], + navigateToHref?: (url: string) => Promise +) { + return breadcrumbs.map((bc) => ({ + ...bc, + ...(bc.href + ? { + onClick: (event: MouseEvent) => { + if (navigateToHref && bc.href) { + event.preventDefault(); + navigateToHref(bc.href); + } + }, + } + : {}), + })); +} + +export const makeBaseBreadcrumb = (href: string, params?: any): EuiBreadcrumb => { + if (params) { + const crumbParams = { ...params }; + + delete crumbParams.statusFilter; + const query = stringify(crumbParams, { skipEmptyString: true, skipNull: true }); + href += query === EMPTY_QUERY ? '' : query; + } + return { + text: i18n.translate('xpack.observability.breadcrumbs.observability', { + defaultMessage: 'Observability', + }), + href, + }; +}; + +export const useBreadcrumbs = (extraCrumbs: ChromeBreadcrumb[]) => { + const params = useQueryParams(); + + const { + services: { chrome, application }, + } = useKibana(); + + const setBreadcrumbs = chrome?.setBreadcrumbs; + const appPath = application?.getUrlForApp('observability-overview') ?? ''; + const navigate = application?.navigateToUrl; + + useEffect(() => { + if (setBreadcrumbs) { + setBreadcrumbs( + handleBreadcrumbClick([makeBaseBreadcrumb(appPath, params)].concat(extraCrumbs), navigate) + ); + } + }, [appPath, extraCrumbs, navigate, params, setBreadcrumbs]); +}; diff --git a/x-pack/plugins/observability/public/hooks/use_quick_time_ranges.tsx b/x-pack/plugins/observability/public/hooks/use_quick_time_ranges.tsx new file mode 100644 index 0000000000000..82a0fc39b8519 --- /dev/null +++ b/x-pack/plugins/observability/public/hooks/use_quick_time_ranges.tsx @@ -0,0 +1,22 @@ +/* + * 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 { useUiSetting } from '../../../../../src/plugins/kibana_react/public'; +import { UI_SETTINGS } from '../../../../../src/plugins/data/common'; +import { TimePickerQuickRange } from '../components/shared/exploratory_view/series_date_picker'; + +export function useQuickTimeRanges() { + const timePickerQuickRanges = useUiSetting( + UI_SETTINGS.TIMEPICKER_QUICK_RANGES + ); + + return timePickerQuickRanges.map(({ from, to, display }) => ({ + start: from, + end: to, + label: display, + })); +} diff --git a/x-pack/plugins/observability/public/hooks/use_values_list.ts b/x-pack/plugins/observability/public/hooks/use_values_list.ts index 25a12ab4a9ebd..e17f515ed6cb9 100644 --- a/x-pack/plugins/observability/public/hooks/use_values_list.ts +++ b/x-pack/plugins/observability/public/hooks/use_values_list.ts @@ -5,32 +5,58 @@ * 2.0. */ -import { IIndexPattern } from '../../../../../src/plugins/data/common'; +import { IndexPattern } from '../../../../../src/plugins/data/common'; import { useKibana } from '../../../../../src/plugins/kibana_react/public'; +import { DataPublicPluginStart } from '../../../../../src/plugins/data/public'; import { useFetcher } from './use_fetcher'; import { ESFilter } from '../../../../../typings/elasticsearch'; -import { DataPublicPluginStart } from '../../../../../src/plugins/data/public'; -interface Props { +export interface Props { sourceField: string; query?: string; - indexPattern: IIndexPattern; + indexPattern: IndexPattern; filters?: ESFilter[]; + time?: { from: string; to: string }; } -export const useValuesList = ({ sourceField, indexPattern, query, filters }: Props) => { +export const useValuesList = ({ + sourceField, + indexPattern, + query = '', + filters, + time, +}: Props): { values: string[]; loading?: boolean } => { const { services: { data }, } = useKibana<{ data: DataPublicPluginStart }>(); - const { data: values, status } = useFetcher(() => { + const { from, to } = time ?? {}; + + const { data: values, loading } = useFetcher(() => { + if (!sourceField || !indexPattern) { + return []; + } return data.autocomplete.getValueSuggestions({ indexPattern, query: query || '', - field: indexPattern.fields.find(({ name }) => name === sourceField)!, - boolFilter: filters ?? [], + useTimeRange: !(from && to), + field: indexPattern.getFieldByName(sourceField)!, + boolFilter: + from && to + ? [ + ...(filters || []), + { + range: { + '@timestamp': { + gte: from, + lte: to, + }, + }, + }, + ] + : filters || [], }); - }, [sourceField, query, data.autocomplete, indexPattern, filters]); + }, [query, sourceField, data.autocomplete, indexPattern, from, to, filters]); - return { values, loading: status === 'loading' || status === 'pending' }; + return { values: values as string[], loading }; }; diff --git a/x-pack/plugins/observability/public/index.ts b/x-pack/plugins/observability/public/index.ts index 35443ca090077..837404d273ee4 100644 --- a/x-pack/plugins/observability/public/index.ts +++ b/x-pack/plugins/observability/public/index.ts @@ -55,3 +55,4 @@ export * from './typings'; export { useChartTheme } from './hooks/use_chart_theme'; export { useTheme } from './hooks/use_theme'; export { getApmTraceUrl } from './utils/get_apm_trace_url'; +export { createExploratoryViewUrl } from './components/shared/exploratory_view/configurations/utils'; diff --git a/x-pack/plugins/observability/public/routes/index.tsx b/x-pack/plugins/observability/public/routes/index.tsx index 20817901dab82..49cc55832dcf2 100644 --- a/x-pack/plugins/observability/public/routes/index.tsx +++ b/x-pack/plugins/observability/public/routes/index.tsx @@ -14,6 +14,7 @@ import { OverviewPage } from '../pages/overview'; import { jsonRt } from './json_rt'; import { AlertsPage } from '../pages/alerts'; import { CasesPage } from '../pages/cases'; +import { ExploratoryViewPage } from '../components/shared/exploratory_view'; export type RouteParams = DecodeParams; @@ -115,4 +116,24 @@ export const routes = { }, ], }, + '/exploratory-view': { + handler: () => { + return ; + }, + params: { + query: t.partial({ + rangeFrom: t.string, + rangeTo: t.string, + refreshPaused: jsonRt.pipe(t.boolean), + refreshInterval: jsonRt.pipe(t.number), + }), + }, + breadcrumb: [ + { + text: i18n.translate('xpack.observability.overview.exploratoryView', { + defaultMessage: 'Exploratory view', + }), + }, + ], + }, }; diff --git a/x-pack/plugins/observability/public/utils/observability_index_patterns.ts b/x-pack/plugins/observability/public/utils/observability_index_patterns.ts new file mode 100644 index 0000000000000..b23a246105544 --- /dev/null +++ b/x-pack/plugins/observability/public/utils/observability_index_patterns.ts @@ -0,0 +1,64 @@ +/* + * 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 { DataPublicPluginStart, IndexPattern } from '../../../../../src/plugins/data/public'; + +export type DataType = 'synthetics' | 'apm' | 'logs' | 'metrics' | 'rum'; + +const indexPatternList: Record = { + synthetics: 'synthetics_static_index_pattern_id', + apm: 'apm_static_index_pattern_id', + rum: 'apm_static_index_pattern_id', + logs: 'logs_static_index_pattern_id', + metrics: 'metrics_static_index_pattern_id', +}; + +const appToPatternMap: Record = { + synthetics: 'heartbeat-*', + apm: 'apm-*', + rum: 'apm-*', + logs: 'logs-*,filebeat-*', + metrics: 'metrics-*,metricbeat-*', +}; + +export class ObservabilityIndexPatterns { + data?: DataPublicPluginStart; + + constructor(data: DataPublicPluginStart) { + this.data = data; + } + + async createIndexPattern(app: DataType) { + if (!this.data) { + throw new Error('data is not defined'); + } + + const pattern = appToPatternMap[app]; + + const fields = await this.data.indexPatterns.getFieldsForWildcard({ + pattern, + }); + + return await this.data.indexPatterns.createAndSave({ + fields, + title: pattern, + id: indexPatternList[app], + timeFieldName: '@timestamp', + }); + } + + async getIndexPattern(app: DataType): Promise { + if (!this.data) { + throw new Error('data is not defined'); + } + try { + return await this.data?.indexPatterns.get(indexPatternList[app]); + } catch (e) { + return await this.createIndexPattern(app || 'apm'); + } + } +} diff --git a/x-pack/plugins/observability/tsconfig.json b/x-pack/plugins/observability/tsconfig.json index 083c35a26c20b..cc6e298795e4a 100644 --- a/x-pack/plugins/observability/tsconfig.json +++ b/x-pack/plugins/observability/tsconfig.json @@ -7,7 +7,14 @@ "declaration": true, "declarationMap": true }, - "include": ["common/**/*", "public/**/*", "server/**/*", "typings/**/*"], + "include": [ + "common/**/*", + "public/**/*", + "public/**/*.json", + "server/**/*", + "typings/**/*", + "../../../typings/**/*" + ], "references": [ { "path": "../../../src/core/tsconfig.json" }, { "path": "../../../src/plugins/data/tsconfig.json" }, diff --git a/x-pack/plugins/uptime/public/apps/plugin.ts b/x-pack/plugins/uptime/public/apps/plugin.ts index e3457884594a9..7ea6b72547386 100644 --- a/x-pack/plugins/uptime/public/apps/plugin.ts +++ b/x-pack/plugins/uptime/public/apps/plugin.ts @@ -68,18 +68,21 @@ export class UptimePlugin return UptimeDataHelper(coreStart); }; - plugins.observability.dashboard.register({ - appName: 'uptime', - hasData: async () => { - const dataHelper = await getUptimeDataHelper(); - const status = await dataHelper.indexStatus(); - return status.docCount > 0; - }, - fetchData: async (params: FetchDataParams) => { - const dataHelper = await getUptimeDataHelper(); - return await dataHelper.overviewData(params); - }, - }); + + if (plugins.observability) { + plugins.observability.dashboard.register({ + appName: 'uptime', + hasData: async () => { + const dataHelper = await getUptimeDataHelper(); + const status = await dataHelper.indexStatus(); + return status.docCount > 0; + }, + fetchData: async (params: FetchDataParams) => { + const dataHelper = await getUptimeDataHelper(); + return await dataHelper.overviewData(params); + }, + }); + } core.application.register({ id: PLUGIN.ID, From 9cebff12980d7afe2e4b13fcb15c26c627f38498 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Marcondes?= <55978943+cauemarcondes@users.noreply.github.com> Date: Mon, 5 Apr 2021 10:51:56 -0400 Subject: [PATCH 23/30] [OBS]home page is showing incorrect value of APM throughput (tpm) (#95991) * fixing obs transaction per minute value * addressing PR comments * fixing unit test * addressing PR comments Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- ...pm_observability_overview_fetchers.test.ts | 20 ++-- .../apm_observability_overview_fetchers.ts | 12 +-- .../get_transaction_coordinates.ts | 64 ------------- .../get_transactions_per_minute.ts | 95 +++++++++++++++++++ .../server/routes/observability_overview.ts | 8 +- .../components/app/section/apm/index.test.tsx | 26 +++++ .../components/app/section/apm/index.tsx | 15 ++- .../typings/fetch_overview_data/index.ts | 2 +- .../observability_overview.ts | 19 ++-- 9 files changed, 166 insertions(+), 95 deletions(-) delete mode 100644 x-pack/plugins/apm/server/lib/observability_overview/get_transaction_coordinates.ts create mode 100644 x-pack/plugins/apm/server/lib/observability_overview/get_transactions_per_minute.ts diff --git a/x-pack/plugins/apm/public/services/rest/apm_observability_overview_fetchers.test.ts b/x-pack/plugins/apm/public/services/rest/apm_observability_overview_fetchers.test.ts index 1821e92ee5a78..29fabc51fd582 100644 --- a/x-pack/plugins/apm/public/services/rest/apm_observability_overview_fetchers.test.ts +++ b/x-pack/plugins/apm/public/services/rest/apm_observability_overview_fetchers.test.ts @@ -46,11 +46,14 @@ describe('Observability dashboard data', () => { callApmApiMock.mockImplementation(() => Promise.resolve({ serviceCount: 10, - transactionCoordinates: [ - { x: 1, y: 1 }, - { x: 2, y: 2 }, - { x: 3, y: 3 }, - ], + transactionPerMinute: { + value: 2, + timeseries: [ + { x: 1, y: 1 }, + { x: 2, y: 2 }, + { x: 3, y: 3 }, + ], + }, }) ); const response = await fetchObservabilityOverviewPageData(params); @@ -81,7 +84,7 @@ describe('Observability dashboard data', () => { callApmApiMock.mockImplementation(() => Promise.resolve({ serviceCount: 0, - transactionCoordinates: [], + transactionPerMinute: { value: null, timeseries: [] }, }) ); const response = await fetchObservabilityOverviewPageData(params); @@ -108,7 +111,10 @@ describe('Observability dashboard data', () => { callApmApiMock.mockImplementation(() => Promise.resolve({ serviceCount: 0, - transactionCoordinates: [{ x: 1 }, { x: 2 }, { x: 3 }], + transactionPerMinute: { + value: 0, + timeseries: [{ x: 1 }, { x: 2 }, { x: 3 }], + }, }) ); const response = await fetchObservabilityOverviewPageData(params); diff --git a/x-pack/plugins/apm/public/services/rest/apm_observability_overview_fetchers.ts b/x-pack/plugins/apm/public/services/rest/apm_observability_overview_fetchers.ts index 55ead8d942aca..3a02efd05e5a5 100644 --- a/x-pack/plugins/apm/public/services/rest/apm_observability_overview_fetchers.ts +++ b/x-pack/plugins/apm/public/services/rest/apm_observability_overview_fetchers.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { mean } from 'lodash'; import { ApmFetchDataResponse, FetchDataParams, @@ -31,7 +30,7 @@ export const fetchObservabilityOverviewPageData = async ({ }, }); - const { serviceCount, transactionCoordinates } = data; + const { serviceCount, transactionPerMinute } = data; return { appLink: `/app/apm/services?rangeFrom=${relativeTime.start}&rangeTo=${relativeTime.end}`, @@ -42,17 +41,12 @@ export const fetchObservabilityOverviewPageData = async ({ }, transactions: { type: 'number', - value: - mean( - transactionCoordinates - .map(({ y }) => y) - .filter((y) => y && isFinite(y)) - ) || 0, + value: transactionPerMinute.value || 0, }, }, series: { transactions: { - coordinates: transactionCoordinates, + coordinates: transactionPerMinute.timeseries, }, }, }; diff --git a/x-pack/plugins/apm/server/lib/observability_overview/get_transaction_coordinates.ts b/x-pack/plugins/apm/server/lib/observability_overview/get_transaction_coordinates.ts deleted file mode 100644 index aac18e2bdfe4c..0000000000000 --- a/x-pack/plugins/apm/server/lib/observability_overview/get_transaction_coordinates.ts +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { rangeQuery } from '../../../server/utils/queries'; -import { Coordinates } from '../../../../observability/typings/common'; -import { Setup, SetupTimeRange } from '../helpers/setup_request'; -import { getProcessorEventForAggregatedTransactions } from '../helpers/aggregated_transactions'; -import { calculateThroughput } from '../helpers/calculate_throughput'; -import { withApmSpan } from '../../utils/with_apm_span'; - -export function getTransactionCoordinates({ - setup, - bucketSize, - searchAggregatedTransactions, -}: { - setup: Setup & SetupTimeRange; - bucketSize: string; - searchAggregatedTransactions: boolean; -}): Promise { - return withApmSpan( - 'observability_overview_get_transaction_distribution', - async () => { - const { apmEventClient, start, end } = setup; - - const { aggregations } = await apmEventClient.search({ - apm: { - events: [ - getProcessorEventForAggregatedTransactions( - searchAggregatedTransactions - ), - ], - }, - body: { - size: 0, - query: { - bool: { - filter: rangeQuery(start, end), - }, - }, - aggs: { - distribution: { - date_histogram: { - field: '@timestamp', - fixed_interval: bucketSize, - min_doc_count: 0, - }, - }, - }, - }, - }); - - return ( - aggregations?.distribution.buckets.map((bucket) => ({ - x: bucket.key, - y: calculateThroughput({ start, end, value: bucket.doc_count }), - })) || [] - ); - } - ); -} diff --git a/x-pack/plugins/apm/server/lib/observability_overview/get_transactions_per_minute.ts b/x-pack/plugins/apm/server/lib/observability_overview/get_transactions_per_minute.ts new file mode 100644 index 0000000000000..da8ac7c50b594 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/observability_overview/get_transactions_per_minute.ts @@ -0,0 +1,95 @@ +/* + * 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 { + TRANSACTION_PAGE_LOAD, + TRANSACTION_REQUEST, +} from '../../../common/transaction_types'; +import { TRANSACTION_TYPE } from '../../../common/elasticsearch_fieldnames'; +import { rangeQuery } from '../../../server/utils/queries'; +import { Setup, SetupTimeRange } from '../helpers/setup_request'; +import { getProcessorEventForAggregatedTransactions } from '../helpers/aggregated_transactions'; +import { calculateThroughput } from '../helpers/calculate_throughput'; +import { withApmSpan } from '../../utils/with_apm_span'; + +export function getTransactionsPerMinute({ + setup, + bucketSize, + searchAggregatedTransactions, +}: { + setup: Setup & SetupTimeRange; + bucketSize: string; + searchAggregatedTransactions: boolean; +}) { + return withApmSpan( + 'observability_overview_get_transactions_per_minute', + async () => { + const { apmEventClient, start, end } = setup; + + const { aggregations } = await apmEventClient.search({ + apm: { + events: [ + getProcessorEventForAggregatedTransactions( + searchAggregatedTransactions + ), + ], + }, + body: { + size: 0, + query: { + bool: { + filter: rangeQuery(start, end), + }, + }, + aggs: { + transactionType: { + terms: { + field: TRANSACTION_TYPE, + }, + aggs: { + timeseries: { + date_histogram: { + field: '@timestamp', + fixed_interval: bucketSize, + min_doc_count: 0, + }, + aggs: { + throughput: { rate: { unit: 'minute' as const } }, + }, + }, + }, + }, + }, + }, + }); + + if (!aggregations || !aggregations.transactionType.buckets) { + return { value: undefined, timeseries: [] }; + } + + const topTransactionTypeBucket = + aggregations.transactionType.buckets.find( + ({ key: transactionType }) => + transactionType === TRANSACTION_REQUEST || + transactionType === TRANSACTION_PAGE_LOAD + ) || aggregations.transactionType.buckets[0]; + + return { + value: calculateThroughput({ + start, + end, + value: topTransactionTypeBucket?.doc_count || 0, + }), + timeseries: + topTransactionTypeBucket?.timeseries.buckets.map((bucket) => ({ + x: bucket.key, + y: bucket.throughput.value, + })) || [], + }; + } + ); +} diff --git a/x-pack/plugins/apm/server/routes/observability_overview.ts b/x-pack/plugins/apm/server/routes/observability_overview.ts index b9c0a76b6fb90..1aac2c09d01c5 100644 --- a/x-pack/plugins/apm/server/routes/observability_overview.ts +++ b/x-pack/plugins/apm/server/routes/observability_overview.ts @@ -8,7 +8,7 @@ import * as t from 'io-ts'; import { setupRequest } from '../lib/helpers/setup_request'; import { getServiceCount } from '../lib/observability_overview/get_service_count'; -import { getTransactionCoordinates } from '../lib/observability_overview/get_transaction_coordinates'; +import { getTransactionsPerMinute } from '../lib/observability_overview/get_transactions_per_minute'; import { getHasData } from '../lib/observability_overview/has_data'; import { createRoute } from './create_route'; import { rangeRt } from './default_api_types'; @@ -39,18 +39,18 @@ export const observabilityOverviewRoute = createRoute({ ); return withApmSpan('observability_overview', async () => { - const [serviceCount, transactionCoordinates] = await Promise.all([ + const [serviceCount, transactionPerMinute] = await Promise.all([ getServiceCount({ setup, searchAggregatedTransactions, }), - getTransactionCoordinates({ + getTransactionsPerMinute({ setup, bucketSize, searchAggregatedTransactions, }), ]); - return { serviceCount, transactionCoordinates }; + return { serviceCount, transactionPerMinute }; }); }, }); diff --git a/x-pack/plugins/observability/public/components/app/section/apm/index.test.tsx b/x-pack/plugins/observability/public/components/app/section/apm/index.test.tsx index e5f100be285e1..d29481a39eb72 100644 --- a/x-pack/plugins/observability/public/components/app/section/apm/index.test.tsx +++ b/x-pack/plugins/observability/public/components/app/section/apm/index.test.tsx @@ -56,6 +56,32 @@ describe('APMSection', () => { } as unknown) as ObservabilityPublicPluginsStart, })); }); + + it('renders transaction stat less then 1k', () => { + const resp = { + appLink: '/app/apm', + stats: { + services: { value: 11, type: 'number' }, + transactions: { value: 900, type: 'number' }, + }, + series: { + transactions: { coordinates: [] }, + }, + }; + jest.spyOn(fetcherHook, 'useFetcher').mockReturnValue({ + data: resp, + status: fetcherHook.FETCH_STATUS.SUCCESS, + refetch: jest.fn(), + }); + const { getByText, queryAllByTestId } = render(); + + expect(getByText('APM')).toBeInTheDocument(); + expect(getByText('View in app')).toBeInTheDocument(); + expect(getByText('Services 11')).toBeInTheDocument(); + expect(getByText('Throughput 900.0 tpm')).toBeInTheDocument(); + expect(queryAllByTestId('loading')).toEqual([]); + }); + it('renders with transaction series and stats', () => { jest.spyOn(fetcherHook, 'useFetcher').mockReturnValue({ data: response, diff --git a/x-pack/plugins/observability/public/components/app/section/apm/index.tsx b/x-pack/plugins/observability/public/components/app/section/apm/index.tsx index 91a536840ecbd..e71468d3b028c 100644 --- a/x-pack/plugins/observability/public/components/app/section/apm/index.tsx +++ b/x-pack/plugins/observability/public/components/app/section/apm/index.tsx @@ -31,6 +31,19 @@ function formatTpm(value?: number) { return numeral(value).format('0.00a'); } +function formatTpmStat(value?: number) { + if (!value || value === 0) { + return '0'; + } + if (value <= 0.1) { + return '< 0.1'; + } + if (value > 1000) { + return numeral(value).format('0.00a'); + } + return numeral(value).format('0,0.0'); +} + export function APMSection({ bucketSize }: Props) { const theme = useContext(ThemeContext); const chartTheme = useChartTheme(); @@ -93,7 +106,7 @@ export function APMSection({ bucketSize }: Props) { ({ x: new Date(x).toISOString(), @@ -67,23 +68,23 @@ export default function ApiTest({ getService }: FtrProviderContext) { Array [ Object { "x": "2020-12-08T13:57:00.000Z", - "y": 0.166666666666667, + "y": 2, }, Object { "x": "2020-12-08T13:58:00.000Z", - "y": 5.23333333333333, + "y": 61, }, Object { "x": "2020-12-08T13:59:00.000Z", - "y": 4.4, + "y": 36, }, Object { "x": "2020-12-08T14:00:00.000Z", - "y": 5.73333333333333, + "y": 75, }, Object { "x": "2020-12-08T14:01:00.000Z", - "y": 4.33333333333333, + "y": 36, }, ] `); From 123f3400a82f89a7be5a6c2d9526e62102530c47 Mon Sep 17 00:00:00 2001 From: Scotty Bollinger Date: Mon, 5 Apr 2021 10:19:32 -0500 Subject: [PATCH 24/30] [Workplace Search] Add sub nav and fix rendering bugs in Personal dashboard (#96100) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix route for private deferated source summary * Make schema types nullable Federated sources don’t have counts and the server returns null so our routes have to expect that sometimes these values will be null * Add SourceSubNav to Personal dashboard We are able to leverage the existing component with a couple a small change; the existing componet is a subnav in the larger Enterprise Search shared navigation component and does not include its styles. This caused the list items to render with bullet points next to them. Adding this class and displaying the nav items as block elements fixes this issue. Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../views/content_sources/components/source_sub_nav.tsx | 4 ++-- .../views/content_sources/private_sources_layout.test.tsx | 3 +++ .../views/content_sources/private_sources_layout.tsx | 3 +++ .../views/content_sources/source_logic.test.ts | 2 +- .../workplace_search/views/content_sources/source_logic.ts | 2 +- .../workplace_search/views/content_sources/sources.scss | 6 ++++++ .../server/routes/workplace_search/sources.ts | 6 +++--- 7 files changed, 19 insertions(+), 7 deletions(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_sub_nav.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_sub_nav.tsx index 99cebd5ded585..bf0c5471f7b57 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_sub_nav.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_sub_nav.tsx @@ -33,7 +33,7 @@ export const SourceSubNav: React.FC = () => { const isCustom = serviceType === CUSTOM_SERVICE_TYPE; return ( - <> +
{NAV.OVERVIEW} @@ -53,6 +53,6 @@ export const SourceSubNav: React.FC = () => { {NAV.SETTINGS} - +
); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources_layout.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources_layout.test.tsx index 488eb4b49853b..9e3b50ea083eb 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources_layout.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources_layout.test.tsx @@ -17,6 +17,8 @@ import { EuiCallOut } from '@elastic/eui'; import { ViewContentHeader } from '../../components/shared/view_content_header'; +import { SourceSubNav } from './components/source_sub_nav'; + import { PRIVATE_CAN_CREATE_PAGE_TITLE, PRIVATE_VIEW_ONLY_PAGE_TITLE, @@ -40,6 +42,7 @@ describe('PrivateSourcesLayout', () => { const wrapper = shallow({children}); expect(wrapper.find('[data-test-subj="TestChildren"]')).toHaveLength(1); + expect(wrapper.find(SourceSubNav)).toHaveLength(1); }); it('uses correct title and description when private sources are enabled', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources_layout.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources_layout.tsx index bdc2421432c8a..2a6281075dc40 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources_layout.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources_layout.tsx @@ -14,6 +14,8 @@ import { EuiPage, EuiPageSideBar, EuiPageBody, EuiCallOut } from '@elastic/eui'; import { AppLogic } from '../../app_logic'; import { ViewContentHeader } from '../../components/shared/view_content_header'; +import { SourceSubNav } from './components/source_sub_nav'; + import { PRIVATE_DASHBOARD_READ_ONLY_MODE_WARNING, PRIVATE_CAN_CREATE_PAGE_TITLE, @@ -49,6 +51,7 @@ export const PrivateSourcesLayout: React.FC = ({ + {readOnlyMode && ( diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.test.ts index d20d0576d11ce..a9712cc4e1dc0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.test.ts @@ -214,7 +214,7 @@ describe('SourceLogic', () => { SourceLogic.actions.initializeFederatedSummary(contentSource.id); expect(http.get).toHaveBeenCalledWith( - '/api/workplace_search/org/sources/123/federated_summary' + '/api/workplace_search/account/sources/123/federated_summary' ); await promise; expect(onUpdateSummarySpy).toHaveBeenCalledWith(contentSource.summary); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.ts index 72700ce42c75d..3da90c4fc7739 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.ts @@ -156,7 +156,7 @@ export const SourceLogic = kea>({ } }, initializeFederatedSummary: async ({ sourceId }) => { - const route = `/api/workplace_search/org/sources/${sourceId}/federated_summary`; + const route = `/api/workplace_search/account/sources/${sourceId}/federated_summary`; try { const response = await HttpLogic.values.http.get(route); actions.onUpdateSummary(response.summary); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources.scss b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources.scss index f142567fb621f..abab139e32369 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources.scss +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources.scss @@ -30,3 +30,9 @@ margin-left: -$sideBarWidth; } } + +.sourcesSubNav { + li { + display: block; + } +} diff --git a/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.ts b/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.ts index 8257dd0dc52b0..1dd6d859d88ad 100644 --- a/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.ts +++ b/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.ts @@ -22,9 +22,9 @@ const schemaValuesSchema = schema.recordOf( ); const pageSchema = schema.object({ - current: schema.number(), - size: schema.number(), - total_pages: schema.number(), + current: schema.nullable(schema.number()), + size: schema.nullable(schema.number()), + total_pages: schema.nullable(schema.number()), total_results: schema.number(), }); From ea03eb1bab086b6e6e526d8c1eb11ffa877d7d52 Mon Sep 17 00:00:00 2001 From: Scotty Bollinger Date: Mon, 5 Apr 2021 10:27:05 -0500 Subject: [PATCH 25/30] [Enterprise Search] Expose core.chrome.setIsVisible for use in Workplace Search (#95984) * Hide chrome for Workplace Search by default The Workplace Search Personal dashboard needs the chrome hidden. We hide it globally here first to prevent a flash of chrome on the Personal dashboard and unhide it for admin routes, which will be in a future commit * Add core.chrome.setIsVisible to KibanaLogic * Toggle chrome visibility for Workplace Search * Add test * Refactor to set context and chrome when pathname changes Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../applications/__mocks__/kibana_logic.mock.ts | 1 + .../enterprise_search/public/applications/index.tsx | 1 + .../applications/shared/kibana/kibana_logic.ts | 2 ++ .../applications/workplace_search/index.test.tsx | 4 +++- .../public/applications/workplace_search/index.tsx | 12 +++++++----- x-pack/plugins/enterprise_search/public/plugin.ts | 3 +++ 6 files changed, 17 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/kibana_logic.mock.ts b/x-pack/plugins/enterprise_search/public/applications/__mocks__/kibana_logic.mock.ts index 133f704fd59a9..2325ddcf2b270 100644 --- a/x-pack/plugins/enterprise_search/public/applications/__mocks__/kibana_logic.mock.ts +++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/kibana_logic.mock.ts @@ -19,6 +19,7 @@ export const mockKibanaValues = { history: mockHistory, navigateToUrl: jest.fn(), setBreadcrumbs: jest.fn(), + setChromeIsVisible: jest.fn(), setDocTitle: jest.fn(), renderHeaderActions: jest.fn(), }; diff --git a/x-pack/plugins/enterprise_search/public/applications/index.tsx b/x-pack/plugins/enterprise_search/public/applications/index.tsx index 155ff5b92ba27..c2bf77751528a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/index.tsx @@ -49,6 +49,7 @@ export const renderApp = ( history: params.history, navigateToUrl: core.application.navigateToUrl, setBreadcrumbs: core.chrome.setBreadcrumbs, + setChromeIsVisible: core.chrome.setIsVisible, setDocTitle: core.chrome.docTitle.change, renderHeaderActions: (HeaderActions) => params.setHeaderActionMenu((el) => renderHeaderActions(HeaderActions, store, el)), diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/kibana/kibana_logic.ts b/x-pack/plugins/enterprise_search/public/applications/shared/kibana/kibana_logic.ts index 8015d22f7c44a..2bef7d373f160 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/kibana/kibana_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/kibana/kibana_logic.ts @@ -24,6 +24,7 @@ interface KibanaLogicProps { charts: ChartsPluginStart; navigateToUrl: ApplicationStart['navigateToUrl']; setBreadcrumbs(crumbs: ChromeBreadcrumb[]): void; + setChromeIsVisible(isVisible: boolean): void; setDocTitle(title: string): void; renderHeaderActions(HeaderActions: FC): void; } @@ -47,6 +48,7 @@ export const KibanaLogic = kea>({ {}, ], setBreadcrumbs: [props.setBreadcrumbs, {}], + setChromeIsVisible: [props.setChromeIsVisible, {}], setDocTitle: [props.setDocTitle, {}], renderHeaderActions: [props.renderHeaderActions, {}], }), diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.test.tsx index 48bdcd6551b65..a2c0ec18def4b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.test.tsx @@ -57,11 +57,13 @@ describe('WorkplaceSearchConfigured', () => { setMockActions({ initializeAppData, setContext }); }); - it('renders layout and header actions', () => { + it('renders layout, chrome, and header actions', () => { const wrapper = shallow(); expect(wrapper.find(Layout).first().prop('readOnlyMode')).toBeFalsy(); expect(wrapper.find(OverviewMVP)).toHaveLength(1); + + expect(mockKibanaValues.setChromeIsVisible).toHaveBeenCalledWith(true); expect(mockKibanaValues.renderHeaderActions).toHaveBeenCalledWith(WorkplaceSearchHeaderActions); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx index c269a987dc092..7a76de43be41b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx @@ -53,7 +53,7 @@ export const WorkplaceSearch: React.FC = (props) => { export const WorkplaceSearchConfigured: React.FC = (props) => { const { hasInitialized } = useValues(AppLogic); const { initializeAppData, setContext } = useActions(AppLogic); - const { renderHeaderActions } = useValues(KibanaLogic); + const { renderHeaderActions, setChromeIsVisible } = useValues(KibanaLogic); const { errorConnecting, readOnlyMode } = useValues(HttpLogic); const { pathname } = useLocation(); @@ -66,11 +66,13 @@ export const WorkplaceSearchConfigured: React.FC = (props) => { * Personal dashboard urls begin with /p/ * EX: http://localhost:5601/app/enterprise_search/workplace_search/p/sources */ - const personalSourceUrlRegex = /^\/p\//g; // matches '/p/*' + useEffect(() => { + const personalSourceUrlRegex = /^\/p\//g; // matches '/p/*' + const isOrganization = !pathname.match(personalSourceUrlRegex); // TODO: Once auth is figured out, we need to have a check for the equivilent of `isAdmin`. - // TODO: Once auth is figured out, we need to have a check for the equivilent of `isAdmin`. - const isOrganization = !pathname.match(personalSourceUrlRegex); - setContext(isOrganization); + setContext(isOrganization); + setChromeIsVisible(isOrganization); + }, [pathname]); useEffect(() => { if (!hasInitialized) { diff --git a/x-pack/plugins/enterprise_search/public/plugin.ts b/x-pack/plugins/enterprise_search/public/plugin.ts index f00e81a5accf7..dd1a62d243d03 100644 --- a/x-pack/plugins/enterprise_search/public/plugin.ts +++ b/x-pack/plugins/enterprise_search/public/plugin.ts @@ -114,6 +114,9 @@ export class EnterpriseSearchPlugin implements Plugin { const { chrome, http } = kibanaDeps.core; chrome.docTitle.change(WORKPLACE_SEARCH_PLUGIN.NAME); + // The Workplace Search Personal dashboard needs the chrome hidden. We hide it globally + // here first to prevent a flash of chrome on the Personal dashboard and unhide it for admin routes. + chrome.setIsVisible(false); await this.getInitialData(http); const pluginData = this.getPluginData(); From 95e45ddebc6e86bb3d63f04bd7c4e56ad8ef55bb Mon Sep 17 00:00:00 2001 From: Pierre Gayvallet Date: Mon, 5 Apr 2021 18:00:13 +0200 Subject: [PATCH 26/30] Use plugin version in its publicPath (#95945) * Use plugin version in its publicPath * remove useless comment * fix types * update generated doc --- .../register_bundle_routes.test.ts | 15 +++++++------ .../bundle_routes/register_bundle_routes.ts | 8 +++---- src/core/server/legacy/legacy_service.test.ts | 1 + .../server/plugins/plugins_service.test.ts | 14 ++++++++++--- src/core/server/plugins/plugins_service.ts | 1 + src/core/server/plugins/plugins_system.ts | 1 + src/core/server/plugins/types.ts | 13 +++++++++--- .../bootstrap/get_plugin_bundle_paths.test.ts | 21 ++++++++++++------- .../bootstrap/get_plugin_bundle_paths.ts | 10 +++++++-- src/core/server/server.api.md | 8 +++---- 10 files changed, 63 insertions(+), 29 deletions(-) diff --git a/src/core/server/core_app/bundle_routes/register_bundle_routes.test.ts b/src/core/server/core_app/bundle_routes/register_bundle_routes.test.ts index d51c369146957..830f4a9a94364 100644 --- a/src/core/server/core_app/bundle_routes/register_bundle_routes.test.ts +++ b/src/core/server/core_app/bundle_routes/register_bundle_routes.test.ts @@ -10,7 +10,7 @@ import { registerRouteForBundleMock } from './register_bundle_routes.test.mocks' import { PackageInfo } from '@kbn/config'; import { httpServiceMock } from '../../http/http_service.mock'; -import { UiPlugins } from '../../plugins'; +import { InternalPluginInfo, UiPlugins } from '../../plugins'; import { registerBundleRoutes } from './register_bundle_routes'; import { FileHashCache } from './file_hash_cache'; @@ -29,9 +29,12 @@ const createUiPlugins = (...ids: string[]): UiPlugins => ({ internal: ids.reduce((map, id) => { map.set(id, { publicTargetDir: `/plugins/${id}/public-target-dir`, + publicAssetsDir: `/plugins/${id}/public-assets-dir`, + version: '8.0.0', + requiredBundles: [], }); return map; - }, new Map()), + }, new Map()), }); describe('registerBundleRoutes', () => { @@ -86,16 +89,16 @@ describe('registerBundleRoutes', () => { fileHashCache: expect.any(FileHashCache), isDist: true, bundlesPath: '/plugins/plugin-a/public-target-dir', - publicPath: '/server-base-path/42/bundles/plugin/plugin-a/', - routePath: '/42/bundles/plugin/plugin-a/', + publicPath: '/server-base-path/42/bundles/plugin/plugin-a/8.0.0/', + routePath: '/42/bundles/plugin/plugin-a/8.0.0/', }); expect(registerRouteForBundleMock).toHaveBeenCalledWith(router, { fileHashCache: expect.any(FileHashCache), isDist: true, bundlesPath: '/plugins/plugin-b/public-target-dir', - publicPath: '/server-base-path/42/bundles/plugin/plugin-b/', - routePath: '/42/bundles/plugin/plugin-b/', + publicPath: '/server-base-path/42/bundles/plugin/plugin-b/8.0.0/', + routePath: '/42/bundles/plugin/plugin-b/8.0.0/', }); }); }); diff --git a/src/core/server/core_app/bundle_routes/register_bundle_routes.ts b/src/core/server/core_app/bundle_routes/register_bundle_routes.ts index ee54f8ef34622..df46753747f5b 100644 --- a/src/core/server/core_app/bundle_routes/register_bundle_routes.ts +++ b/src/core/server/core_app/bundle_routes/register_bundle_routes.ts @@ -27,7 +27,7 @@ import { registerRouteForBundle } from './bundles_route'; */ export function registerBundleRoutes({ router, - serverBasePath, // serverBasePath + serverBasePath, uiPlugins, packageInfo, }: { @@ -57,10 +57,10 @@ export function registerBundleRoutes({ isDist, }); - [...uiPlugins.internal.entries()].forEach(([id, { publicTargetDir }]) => { + [...uiPlugins.internal.entries()].forEach(([id, { publicTargetDir, version }]) => { registerRouteForBundle(router, { - publicPath: `${serverBasePath}/${buildNum}/bundles/plugin/${id}/`, - routePath: `/${buildNum}/bundles/plugin/${id}/`, + publicPath: `${serverBasePath}/${buildNum}/bundles/plugin/${id}/${version}/`, + routePath: `/${buildNum}/bundles/plugin/${id}/${version}/`, bundlesPath: publicTargetDir, fileHashCache, isDist, diff --git a/src/core/server/legacy/legacy_service.test.ts b/src/core/server/legacy/legacy_service.test.ts index d0a02b9859960..67b5393f0b838 100644 --- a/src/core/server/legacy/legacy_service.test.ts +++ b/src/core/server/legacy/legacy_service.test.ts @@ -91,6 +91,7 @@ beforeEach(() => { 'plugin-id', { requiredBundles: [], + version: '8.0.0', publicTargetDir: 'path/to/target/public', publicAssetsDir: '/plugins/name/assets/', }, diff --git a/src/core/server/plugins/plugins_service.test.ts b/src/core/server/plugins/plugins_service.test.ts index 2d54648d22950..6bf7a1fadb4d3 100644 --- a/src/core/server/plugins/plugins_service.test.ts +++ b/src/core/server/plugins/plugins_service.test.ts @@ -562,12 +562,12 @@ describe('PluginsService', () => { plugin$: from([ createPlugin('plugin-1', { path: 'path-1', - version: 'some-version', + version: 'version-1', configPath: 'plugin1', }), createPlugin('plugin-2', { path: 'path-2', - version: 'some-version', + version: 'version-2', configPath: 'plugin2', }), ]), @@ -577,7 +577,7 @@ describe('PluginsService', () => { }); describe('uiPlugins.internal', () => { - it('includes disabled plugins', async () => { + it('contains internal properties for plugins', async () => { config$.next({ plugins: { initialize: true }, plugin1: { enabled: false } }); const { uiPlugins } = await pluginsService.discover({ environment: environmentSetup }); expect(uiPlugins.internal).toMatchInlineSnapshot(` @@ -586,15 +586,23 @@ describe('PluginsService', () => { "publicAssetsDir": /path-1/public/assets, "publicTargetDir": /path-1/target/public, "requiredBundles": Array [], + "version": "version-1", }, "plugin-2" => Object { "publicAssetsDir": /path-2/public/assets, "publicTargetDir": /path-2/target/public, "requiredBundles": Array [], + "version": "version-2", }, } `); }); + + it('includes disabled plugins', async () => { + config$.next({ plugins: { initialize: true }, plugin1: { enabled: false } }); + const { uiPlugins } = await pluginsService.discover({ environment: environmentSetup }); + expect([...uiPlugins.internal.keys()].sort()).toEqual(['plugin-1', 'plugin-2']); + }); }); describe('plugin initialization', () => { diff --git a/src/core/server/plugins/plugins_service.ts b/src/core/server/plugins/plugins_service.ts index 8b33e2cf4cc6b..09be40ecaf2a2 100644 --- a/src/core/server/plugins/plugins_service.ts +++ b/src/core/server/plugins/plugins_service.ts @@ -222,6 +222,7 @@ export class PluginsService implements CoreService(); diff --git a/src/core/server/plugins/types.ts b/src/core/server/plugins/types.ts index a6086bd6f17e8..3a01049c5e1fe 100644 --- a/src/core/server/plugins/types.ts +++ b/src/core/server/plugins/types.ts @@ -224,12 +224,15 @@ export interface DiscoveredPlugin { */ export interface InternalPluginInfo { /** - * Bundles that must be loaded for this plugoin + * Version of the plugin + */ + readonly version: string; + /** + * Bundles that must be loaded for this plugin */ readonly requiredBundles: readonly string[]; /** - * Path to the target/public directory of the plugin which should be - * served + * Path to the target/public directory of the plugin which should be served */ readonly publicTargetDir: string; /** @@ -250,7 +253,9 @@ export interface Plugin< TPluginsStart extends object = object > { setup(core: CoreSetup, plugins: TPluginsSetup): TSetup; + start(core: CoreStart, plugins: TPluginsStart): TStart; + stop?(): void; } @@ -267,7 +272,9 @@ export interface AsyncPlugin< TPluginsStart extends object = object > { setup(core: CoreSetup, plugins: TPluginsSetup): TSetup | Promise; + start(core: CoreStart, plugins: TPluginsStart): TStart | Promise; + stop?(): void; } diff --git a/src/core/server/rendering/bootstrap/get_plugin_bundle_paths.test.ts b/src/core/server/rendering/bootstrap/get_plugin_bundle_paths.test.ts index ea3843884df31..0abd8fd5a0057 100644 --- a/src/core/server/rendering/bootstrap/get_plugin_bundle_paths.test.ts +++ b/src/core/server/rendering/bootstrap/get_plugin_bundle_paths.test.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { UiPlugins } from '../../plugins'; +import { InternalPluginInfo, UiPlugins } from '../../plugins'; import { getPluginsBundlePaths } from './get_plugin_bundle_paths'; const createUiPlugins = (pluginDeps: Record) => { @@ -16,12 +16,13 @@ const createUiPlugins = (pluginDeps: Record) => { browserConfigs: new Map(), }; - Object.entries(pluginDeps).forEach(([pluginId, deps]) => { + const addPlugin = (pluginId: string, deps: string[]) => { uiPlugins.internal.set(pluginId, { requiredBundles: deps, + version: '8.0.0', publicTargetDir: '', publicAssetsDir: '', - } as any); + } as InternalPluginInfo); uiPlugins.public.set(pluginId, { id: pluginId, configPath: 'config-path', @@ -29,6 +30,12 @@ const createUiPlugins = (pluginDeps: Record) => { requiredPlugins: [], requiredBundles: deps, }); + + deps.forEach((dep) => addPlugin(dep, [])); + }; + + Object.entries(pluginDeps).forEach(([pluginId, deps]) => { + addPlugin(pluginId, deps); }); return uiPlugins; @@ -56,13 +63,13 @@ describe('getPluginsBundlePaths', () => { }); expect(pluginBundlePaths.get('a')).toEqual({ - bundlePath: '/regular-bundle-path/plugin/a/a.plugin.js', - publicPath: '/regular-bundle-path/plugin/a/', + bundlePath: '/regular-bundle-path/plugin/a/8.0.0/a.plugin.js', + publicPath: '/regular-bundle-path/plugin/a/8.0.0/', }); expect(pluginBundlePaths.get('b')).toEqual({ - bundlePath: '/regular-bundle-path/plugin/b/b.plugin.js', - publicPath: '/regular-bundle-path/plugin/b/', + bundlePath: '/regular-bundle-path/plugin/b/8.0.0/b.plugin.js', + publicPath: '/regular-bundle-path/plugin/b/8.0.0/', }); }); }); diff --git a/src/core/server/rendering/bootstrap/get_plugin_bundle_paths.ts b/src/core/server/rendering/bootstrap/get_plugin_bundle_paths.ts index c8291b2720a92..86ffdcf835f7b 100644 --- a/src/core/server/rendering/bootstrap/get_plugin_bundle_paths.ts +++ b/src/core/server/rendering/bootstrap/get_plugin_bundle_paths.ts @@ -25,9 +25,15 @@ export const getPluginsBundlePaths = ({ while (pluginsToProcess.length > 0) { const pluginId = pluginsToProcess.pop() as string; + const plugin = uiPlugins.internal.get(pluginId); + if (!plugin) { + continue; + } + const { version } = plugin; + pluginBundlePaths.set(pluginId, { - publicPath: `${regularBundlePath}/plugin/${pluginId}/`, - bundlePath: `${regularBundlePath}/plugin/${pluginId}/${pluginId}.plugin.js`, + publicPath: `${regularBundlePath}/plugin/${pluginId}/${version}/`, + bundlePath: `${regularBundlePath}/plugin/${pluginId}/${version}/${pluginId}.plugin.js`, }); const pluginBundleIds = uiPlugins.internal.get(pluginId)?.requiredBundles ?? []; diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index de96c5ccfb81e..fb5fe3efd3e06 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -3259,9 +3259,9 @@ export const validBodyOutput: readonly ["data", "stream"]; // // src/core/server/elasticsearch/client/types.ts:94:7 - (ae-forgotten-export) The symbol "Explanation" needs to be exported by the entry point index.d.ts // src/core/server/http/router/response.ts:297:3 - (ae-forgotten-export) The symbol "KibanaResponse" needs to be exported by the entry point index.d.ts -// src/core/server/plugins/types.ts:286:3 - (ae-forgotten-export) The symbol "KibanaConfigType" needs to be exported by the entry point index.d.ts -// src/core/server/plugins/types.ts:286:3 - (ae-forgotten-export) The symbol "SharedGlobalConfigKeys" needs to be exported by the entry point index.d.ts -// src/core/server/plugins/types.ts:289:3 - (ae-forgotten-export) The symbol "SavedObjectsConfigType" needs to be exported by the entry point index.d.ts -// src/core/server/plugins/types.ts:394:5 - (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "create" +// src/core/server/plugins/types.ts:293:3 - (ae-forgotten-export) The symbol "KibanaConfigType" needs to be exported by the entry point index.d.ts +// src/core/server/plugins/types.ts:293:3 - (ae-forgotten-export) The symbol "SharedGlobalConfigKeys" needs to be exported by the entry point index.d.ts +// src/core/server/plugins/types.ts:296:3 - (ae-forgotten-export) The symbol "SavedObjectsConfigType" needs to be exported by the entry point index.d.ts +// src/core/server/plugins/types.ts:401:5 - (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "create" ``` From 8e11e2598e874d603e99bcfac407ef9e09784102 Mon Sep 17 00:00:00 2001 From: Thomas Neirynck Date: Mon, 5 Apr 2021 12:04:20 -0400 Subject: [PATCH 27/30] [Maps] Enable all zoom levels for all users (#96093) --- .github/CODEOWNERS | 1 - docs/developer/plugin-list.asciidoc | 4 - packages/kbn-optimizer/limits.yml | 1 - .../service_settings/service_settings.test.js | 50 ++------- .../service_settings/service_settings.ts | 17 +-- .../service_settings_types.ts | 2 - src/plugins/maps_legacy/kibana.json | 2 +- .../public/map/base_maps_visualization.js | 3 +- .../maps_legacy/public/map/kibana_map.js | 23 ---- .../maps_legacy/public/map/map_messages.js | 105 ------------------ test/functional/apps/visualize/_tile_map.ts | 59 ---------- tsconfig.json | 1 - tsconfig.refs.json | 1 - .../plugins/maps_legacy_licensing/README.md | 4 - .../plugins/maps_legacy_licensing/kibana.json | 8 -- .../maps_legacy_licensing/public/index.ts | 12 -- .../maps_legacy_licensing/public/plugin.ts | 48 -------- .../maps_legacy_licensing/tsconfig.json | 15 --- .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - 20 files changed, 12 insertions(+), 346 deletions(-) delete mode 100644 src/plugins/maps_legacy/public/map/map_messages.js delete mode 100644 x-pack/plugins/maps_legacy_licensing/README.md delete mode 100644 x-pack/plugins/maps_legacy_licensing/kibana.json delete mode 100644 x-pack/plugins/maps_legacy_licensing/public/index.ts delete mode 100644 x-pack/plugins/maps_legacy_licensing/public/plugin.ts delete mode 100644 x-pack/plugins/maps_legacy_licensing/tsconfig.json diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index d14556ea1dabf..b9afc197bac9c 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -146,7 +146,6 @@ /x-pack/test/visual_regression/tests/maps/index.js @elastic/kibana-gis #CC# /src/plugins/maps_legacy/ @elastic/kibana-gis #CC# /x-pack/plugins/file_upload @elastic/kibana-gis -#CC# /x-pack/plugins/maps_legacy_licensing @elastic/kibana-gis /src/plugins/tile_map/ @elastic/kibana-gis /src/plugins/region_map/ @elastic/kibana-gis diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index bcf74936077ec..691d7fb82f3bc 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -452,10 +452,6 @@ using the CURL scripts in the scripts folder. |Visualize geo data from Elasticsearch or 3rd party geo-services. -|{kib-repo}blob/{branch}/x-pack/plugins/maps_legacy_licensing/README.md[mapsLegacyLicensing] -|This plugin provides access to the detailed tile map services from Elastic. - - |{kib-repo}blob/{branch}/x-pack/plugins/ml/readme.md[ml] |This plugin provides access to the machine learning features provided by Elastic. diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index 3c9fd4f59a406..a027768ad66a0 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -51,7 +51,6 @@ pageLoadAssetSize: management: 46112 maps: 80000 mapsLegacy: 87859 - mapsLegacyLicensing: 20214 ml: 82187 monitoring: 80000 navigation: 37269 diff --git a/src/plugins/maps_ems/public/service_settings/service_settings.test.js b/src/plugins/maps_ems/public/service_settings/service_settings.test.js index 5bd371aace79b..eb67997c253b9 100644 --- a/src/plugins/maps_ems/public/service_settings/service_settings.test.js +++ b/src/plugins/maps_ems/public/service_settings/service_settings.test.js @@ -103,43 +103,8 @@ describe('service_settings (FKA tile_map test)', function () { expect(tmsService.attribution.includes('OpenStreetMap')).toEqual(true); }); - describe('modify - url', function () { - let tilemapServices; - + describe('tms mods', function () { let serviceSettings; - async function assertQuery(expected) { - const attrs = await serviceSettings.getAttributesForTMSLayer(tilemapServices[0]); - const urlObject = url.parse(attrs.url, true); - Object.keys(expected).forEach((key) => { - expect(urlObject.query[key]).toEqual(expected[key]); - }); - } - - it('accepts an object', async () => { - serviceSettings = makeServiceSettings(); - serviceSettings.setQueryParams({ foo: 'bar' }); - tilemapServices = await serviceSettings.getTMSServices(); - await assertQuery({ foo: 'bar' }); - }); - - it('merged additions with previous values', async () => { - // ensure that changes are always additive - serviceSettings = makeServiceSettings(); - serviceSettings.setQueryParams({ foo: 'bar' }); - serviceSettings.setQueryParams({ bar: 'stool' }); - tilemapServices = await serviceSettings.getTMSServices(); - await assertQuery({ foo: 'bar', bar: 'stool' }); - }); - - it('overwrites conflicting previous values', async () => { - serviceSettings = makeServiceSettings(); - // ensure that conflicts are overwritten - serviceSettings.setQueryParams({ foo: 'bar' }); - serviceSettings.setQueryParams({ bar: 'stool' }); - serviceSettings.setQueryParams({ foo: 'tstool' }); - tilemapServices = await serviceSettings.getTMSServices(); - await assertQuery({ foo: 'tstool', bar: 'stool' }); - }); it('should merge in tilemap url', async () => { serviceSettings = makeServiceSettings( @@ -161,7 +126,7 @@ describe('service_settings (FKA tile_map test)', function () { id: 'road_map', name: 'Road Map - Bright', url: - 'https://tiles.foobar/raster/styles/osm-bright/{z}/{x}/{y}.png?elastic_tile_service_tos=agree&my_app_name=kibana&my_app_version=1.2.3', + 'https://tiles.foobar/raster/styles/osm-bright/{z}/{x}/{y}.png?elastic_tile_service_tos=agree&my_app_name=kibana&my_app_version=1.2.3&license=sspl', minZoom: 0, maxZoom: 10, attribution: @@ -208,19 +173,19 @@ describe('service_settings (FKA tile_map test)', function () { ); expect(desaturationFalse.url).toEqual( - 'https://tiles.foobar/raster/styles/osm-bright/{z}/{x}/{y}.png?elastic_tile_service_tos=agree&my_app_name=kibana&my_app_version=1.2.3' + 'https://tiles.foobar/raster/styles/osm-bright/{z}/{x}/{y}.png?elastic_tile_service_tos=agree&my_app_name=kibana&my_app_version=1.2.3&license=sspl' ); expect(desaturationFalse.maxZoom).toEqual(10); expect(desaturationTrue.url).toEqual( - 'https://tiles.foobar/raster/styles/osm-bright-desaturated/{z}/{x}/{y}.png?elastic_tile_service_tos=agree&my_app_name=kibana&my_app_version=1.2.3' + 'https://tiles.foobar/raster/styles/osm-bright-desaturated/{z}/{x}/{y}.png?elastic_tile_service_tos=agree&my_app_name=kibana&my_app_version=1.2.3&license=sspl' ); expect(desaturationTrue.maxZoom).toEqual(18); expect(darkThemeDesaturationFalse.url).toEqual( - 'https://tiles.foobar/raster/styles/dark-matter/{z}/{x}/{y}.png?elastic_tile_service_tos=agree&my_app_name=kibana&my_app_version=1.2.3' + 'https://tiles.foobar/raster/styles/dark-matter/{z}/{x}/{y}.png?elastic_tile_service_tos=agree&my_app_name=kibana&my_app_version=1.2.3&license=sspl' ); expect(darkThemeDesaturationFalse.maxZoom).toEqual(22); expect(darkThemeDesaturationTrue.url).toEqual( - 'https://tiles.foobar/raster/styles/dark-matter/{z}/{x}/{y}.png?elastic_tile_service_tos=agree&my_app_name=kibana&my_app_version=1.2.3' + 'https://tiles.foobar/raster/styles/dark-matter/{z}/{x}/{y}.png?elastic_tile_service_tos=agree&my_app_name=kibana&my_app_version=1.2.3&license=sspl' ); expect(darkThemeDesaturationTrue.maxZoom).toEqual(22); }); @@ -264,14 +229,13 @@ describe('service_settings (FKA tile_map test)', function () { describe('File layers', function () { it('should load manifest (all props)', async function () { const serviceSettings = makeServiceSettings(); - serviceSettings.setQueryParams({ foo: 'bar' }); const fileLayers = await serviceSettings.getFileLayers(); expect(fileLayers.length).toEqual(19); const assertions = fileLayers.map(async function (fileLayer) { expect(fileLayer.origin).toEqual(ORIGIN.EMS); const fileUrl = await serviceSettings.getUrlForRegionLayer(fileLayer); const urlObject = url.parse(fileUrl, true); - Object.keys({ foo: 'bar', elastic_tile_service_tos: 'agree' }).forEach((key) => { + Object.keys({ elastic_tile_service_tos: 'agree' }).forEach((key) => { expect(typeof urlObject.query[key]).toEqual('string'); }); }); diff --git a/src/plugins/maps_ems/public/service_settings/service_settings.ts b/src/plugins/maps_ems/public/service_settings/service_settings.ts index f7c735b6c3037..412db42a1570c 100644 --- a/src/plugins/maps_ems/public/service_settings/service_settings.ts +++ b/src/plugins/maps_ems/public/service_settings/service_settings.ts @@ -22,7 +22,6 @@ export class ServiceSettings implements IServiceSettings { private readonly _mapConfig: MapsEmsConfig; private readonly _tilemapsConfig: TileMapConfig; private readonly _hasTmsConfigured: boolean; - private _showZoomMessage: boolean; private readonly _emsClient: EMSClient; private readonly tmsOptionsFromConfig: any; @@ -31,7 +30,6 @@ export class ServiceSettings implements IServiceSettings { this._tilemapsConfig = tilemapsConfig; this._hasTmsConfigured = typeof tilemapsConfig.url === 'string' && tilemapsConfig.url !== ''; - this._showZoomMessage = true; this._emsClient = new EMSClient({ language: i18n.getLocale(), appVersion: getKibanaVersion(), @@ -45,6 +43,9 @@ export class ServiceSettings implements IServiceSettings { return fetch(...args); }, }); + // any kibana user, regardless of distribution, should get all zoom levels + // use `sspl` license to indicate this + this._emsClient.addQueryParams({ license: 'sspl' }); const markdownIt = new MarkdownIt({ html: false, @@ -58,18 +59,6 @@ export class ServiceSettings implements IServiceSettings { }); } - shouldShowZoomMessage({ origin }: { origin: string }): boolean { - return origin === ORIGIN.EMS && this._showZoomMessage; - } - - enableZoomMessage(): void { - this._showZoomMessage = true; - } - - disableZoomMessage(): void { - this._showZoomMessage = false; - } - __debugStubManifestCalls(manifestRetrieval: () => Promise): { removeStub: () => void } { const oldGetManifest = this._emsClient.getManifest; diff --git a/src/plugins/maps_ems/public/service_settings/service_settings_types.ts b/src/plugins/maps_ems/public/service_settings/service_settings_types.ts index 80a9aae835844..6b04bd200eba8 100644 --- a/src/plugins/maps_ems/public/service_settings/service_settings_types.ts +++ b/src/plugins/maps_ems/public/service_settings/service_settings_types.ts @@ -46,8 +46,6 @@ export interface IServiceSettings { getFileLayers(): Promise; getUrlForRegionLayer(layer: FileLayer): Promise; setQueryParams(params: { [p: string]: string }): void; - enableZoomMessage(): void; - disableZoomMessage(): void; getAttributesForTMSLayer( tmsServiceConfig: TmsLayer, isDesaturated: boolean, diff --git a/src/plugins/maps_legacy/kibana.json b/src/plugins/maps_legacy/kibana.json index 8e283288e34b2..f321274791a3b 100644 --- a/src/plugins/maps_legacy/kibana.json +++ b/src/plugins/maps_legacy/kibana.json @@ -5,5 +5,5 @@ "ui": true, "server": true, "requiredPlugins": ["mapsEms"], - "requiredBundles": ["kibanaReact", "visDefaultEditor", "mapsEms"] + "requiredBundles": ["visDefaultEditor", "mapsEms"] } diff --git a/src/plugins/maps_legacy/public/map/base_maps_visualization.js b/src/plugins/maps_legacy/public/map/base_maps_visualization.js index 9cd574c5246e8..a261bcf6edd80 100644 --- a/src/plugins/maps_legacy/public/map/base_maps_visualization.js +++ b/src/plugins/maps_legacy/public/map/base_maps_visualization.js @@ -193,13 +193,12 @@ export function BaseMapsVisualizationProvider() { isDesaturated, isDarkMode ); - const showZoomMessage = serviceSettings.shouldShowZoomMessage(tmsLayer); const options = { ...tmsLayer }; delete options.id; delete options.subdomains; this._kibanaMap.setBaseLayer({ baseLayerType: 'tms', - options: { ...options, showZoomMessage, ...meta }, + options: { ...options, ...meta }, }); } diff --git a/src/plugins/maps_legacy/public/map/kibana_map.js b/src/plugins/maps_legacy/public/map/kibana_map.js index eea8315419289..62dbbda2588a5 100644 --- a/src/plugins/maps_legacy/public/map/kibana_map.js +++ b/src/plugins/maps_legacy/public/map/kibana_map.js @@ -7,13 +7,11 @@ */ import { EventEmitter } from 'events'; -import { createZoomWarningMsg } from './map_messages'; import $ from 'jquery'; import { get, isEqual, escape } from 'lodash'; import { zoomToPrecision } from './zoom_to_precision'; import { i18n } from '@kbn/i18n'; import { ORIGIN } from '../../../maps_ems/common'; -import { getToasts } from '../kibana_services'; import { L } from '../leaflet'; function makeFitControl(fitContainer, kibanaMap) { @@ -479,22 +477,6 @@ export class KibanaMap extends EventEmitter { this._updateLegend(); } - _addMaxZoomMessage = (layer) => { - const zoomWarningMsg = createZoomWarningMsg( - getToasts(), - this.getZoomLevel, - this.getMaxZoomLevel - ); - - this._leafletMap.on('zoomend', zoomWarningMsg); - this._containerNode.setAttribute('data-test-subj', 'zoomWarningEnabled'); - - layer.on('remove', () => { - this._leafletMap.off('zoomend', zoomWarningMsg); - this._containerNode.removeAttribute('data-test-subj'); - }); - }; - setLegendPosition(position) { if (this._legendPosition === position) { if (!this._leafletLegendControl) { @@ -572,11 +554,6 @@ export class KibanaMap extends EventEmitter { }); this._leafletBaseLayer = baseLayer; - if (settings.options.showZoomMessage) { - baseLayer.on('add', () => { - this._addMaxZoomMessage(baseLayer); - }); - } this._leafletBaseLayer.addTo(this._leafletMap); this._leafletBaseLayer.bringToBack(); if (settings.options.minZoom > this._leafletMap.getZoom()) { diff --git a/src/plugins/maps_legacy/public/map/map_messages.js b/src/plugins/maps_legacy/public/map/map_messages.js deleted file mode 100644 index f60d819f0b390..0000000000000 --- a/src/plugins/maps_legacy/public/map/map_messages.js +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React from 'react'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiSpacer, EuiButtonEmpty } from '@elastic/eui'; -import { toMountPoint } from '../../../kibana_react/public'; - -export const createZoomWarningMsg = (function () { - let disableZoomMsg = false; - const setZoomMsg = (boolDisableMsg) => (disableZoomMsg = boolDisableMsg); - - class ZoomWarning extends React.Component { - constructor(props) { - super(props); - this.state = { - disabled: false, - }; - } - - render() { - return ( -
-

- - {`default distribution `} - - ), - ems: ( - - {`Elastic Maps Service`} - - ), - wms: ( - - {`Custom WMS Configuration`} - - ), - configSettings: ( - - {`Custom TMS Using Config Settings`} - - ), - }} - /> -

- - { - this.setState( - { - disabled: true, - }, - () => this.props.onChange(this.state.disabled) - ); - }} - data-test-subj="suppressZoomWarnings" - > - {`Don't show again`} - -
- ); - } - } - - const zoomToast = { - title: 'No additional zoom levels', - text: toMountPoint(), - 'data-test-subj': 'maxZoomWarning', - }; - - return (toastService, getZoomLevel, getMaxZoomLevel) => { - return () => { - const zoomLevel = getZoomLevel(); - const maxMapZoom = getMaxZoomLevel(); - if (!disableZoomMsg && zoomLevel === maxMapZoom) { - toastService.addDanger(zoomToast); - } - }; - }; -})(); diff --git a/test/functional/apps/visualize/_tile_map.ts b/test/functional/apps/visualize/_tile_map.ts index 668aec6ac5783..3af467affa1fb 100644 --- a/test/functional/apps/visualize/_tile_map.ts +++ b/test/functional/apps/visualize/_tile_map.ts @@ -15,7 +15,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const retry = getService('retry'); const inspector = getService('inspector'); const filterBar = getService('filterBar'); - const testSubjects = getService('testSubjects'); const browser = getService('browser'); const PageObjects = getPageObjects([ 'common', @@ -221,63 +220,5 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); }); }); - - describe('zoom warning behavior', function describeIndexTests() { - // Zoom warning is only applicable to OSS - this.tags(['skipCloud', 'skipFirefox']); - - const waitForLoading = false; - let zoomWarningEnabled; - let last = false; - const toastDefaultLife = 6000; - - before(async function () { - await browser.setWindowSize(1280, 1000); - - log.debug('navigateToApp visualize'); - await PageObjects.visualize.navigateToNewAggBasedVisualization(); - log.debug('clickTileMap'); - await PageObjects.visualize.clickTileMap(); - await PageObjects.visualize.clickNewSearch(); - - zoomWarningEnabled = await testSubjects.exists('zoomWarningEnabled'); - log.debug(`Zoom warning enabled: ${zoomWarningEnabled}`); - - const zoomLevel = 9; - for (let i = 0; i < zoomLevel; i++) { - await PageObjects.tileMap.clickMapZoomIn(); - } - }); - - beforeEach(async function () { - await PageObjects.tileMap.clickMapZoomIn(waitForLoading); - }); - - afterEach(async function () { - if (!last) { - await PageObjects.common.sleep(toastDefaultLife); - await PageObjects.tileMap.clickMapZoomOut(waitForLoading); - } - }); - - it('should show warning at zoom 10', async () => { - await testSubjects.existOrFail('maxZoomWarning'); - }); - - it('should continue providing zoom warning if left alone', async () => { - await testSubjects.existOrFail('maxZoomWarning'); - }); - - it('should suppress zoom warning if suppress warnings button clicked', async () => { - last = true; - await PageObjects.visChart.waitForVisualization(); - await testSubjects.click('suppressZoomWarnings'); - await PageObjects.tileMap.clickMapZoomOut(waitForLoading); - await testSubjects.waitForDeleted('suppressZoomWarnings'); - await PageObjects.tileMap.clickMapZoomIn(waitForLoading); - - await testSubjects.missingOrFail('maxZoomWarning'); - }); - }); }); } diff --git a/tsconfig.json b/tsconfig.json index 30944ac71fcc8..7c06e80858640 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -181,7 +181,6 @@ { "path": "./x-pack/plugins/license_management/tsconfig.json" }, { "path": "./x-pack/plugins/licensing/tsconfig.json" }, { "path": "./x-pack/plugins/logstash/tsconfig.json" }, - { "path": "./x-pack/plugins/maps_legacy_licensing/tsconfig.json" }, { "path": "./x-pack/plugins/maps/tsconfig.json" }, { "path": "./x-pack/plugins/ml/tsconfig.json" }, { "path": "./x-pack/plugins/monitoring/tsconfig.json" }, diff --git a/tsconfig.refs.json b/tsconfig.refs.json index 2d9ddc1b9e568..f13455a14b4df 100644 --- a/tsconfig.refs.json +++ b/tsconfig.refs.json @@ -85,7 +85,6 @@ { "path": "./x-pack/plugins/license_management/tsconfig.json" }, { "path": "./x-pack/plugins/licensing/tsconfig.json" }, { "path": "./x-pack/plugins/logstash/tsconfig.json" }, - { "path": "./x-pack/plugins/maps_legacy_licensing/tsconfig.json" }, { "path": "./x-pack/plugins/maps/tsconfig.json" }, { "path": "./x-pack/plugins/ml/tsconfig.json" }, { "path": "./x-pack/plugins/monitoring/tsconfig.json" }, diff --git a/x-pack/plugins/maps_legacy_licensing/README.md b/x-pack/plugins/maps_legacy_licensing/README.md deleted file mode 100644 index 7c2ce84d848d4..0000000000000 --- a/x-pack/plugins/maps_legacy_licensing/README.md +++ /dev/null @@ -1,4 +0,0 @@ -# Tile Map Plugin - -This plugin provides access to the detailed tile map services from Elastic. - diff --git a/x-pack/plugins/maps_legacy_licensing/kibana.json b/x-pack/plugins/maps_legacy_licensing/kibana.json deleted file mode 100644 index 7a49e0aaa7be1..0000000000000 --- a/x-pack/plugins/maps_legacy_licensing/kibana.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "id": "mapsLegacyLicensing", - "version": "8.0.0", - "kibanaVersion": "kibana", - "server": false, - "ui": true, - "requiredPlugins": ["licensing", "mapsEms"] -} diff --git a/x-pack/plugins/maps_legacy_licensing/public/index.ts b/x-pack/plugins/maps_legacy_licensing/public/index.ts deleted file mode 100644 index 9105919eaa635..0000000000000 --- a/x-pack/plugins/maps_legacy_licensing/public/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { MapsLegacyLicensing } from './plugin'; - -export function plugin() { - return new MapsLegacyLicensing(); -} diff --git a/x-pack/plugins/maps_legacy_licensing/public/plugin.ts b/x-pack/plugins/maps_legacy_licensing/public/plugin.ts deleted file mode 100644 index f8118575cd6a2..0000000000000 --- a/x-pack/plugins/maps_legacy_licensing/public/plugin.ts +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { CoreSetup, CoreStart, Plugin } from 'kibana/public'; -import { LicensingPluginSetup, ILicense } from '../../licensing/public'; -import { IServiceSettings, MapsEmsPluginSetup } from '../../../../src/plugins/maps_ems/public'; - -/** - * These are the interfaces with your public contracts. You should export these - * for other plugins to use in _their_ `SetupDeps`/`StartDeps` interfaces. - * @public - */ - -export interface MapsLegacyLicensingSetupDependencies { - licensing: LicensingPluginSetup; - mapsEms: MapsEmsPluginSetup; -} -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface MapsLegacyLicensingStartDependencies {} - -export type MapsLegacyLicensingSetup = ReturnType; -export type MapsLegacyLicensingStart = ReturnType; - -export class MapsLegacyLicensing - implements Plugin { - public setup(core: CoreSetup, plugins: MapsLegacyLicensingSetupDependencies) { - const { licensing, mapsEms } = plugins; - if (licensing) { - licensing.license$.subscribe(async (license: ILicense) => { - const serviceSettings: IServiceSettings = await mapsEms.getServiceSettings(); - const { uid, isActive } = license; - if (isActive && license.hasAtLeast('basic')) { - serviceSettings.setQueryParams({ license: uid || '' }); - serviceSettings.disableZoomMessage(); - } else { - serviceSettings.setQueryParams({ license: '' }); - serviceSettings.enableZoomMessage(); - } - }); - } - } - - public start(core: CoreStart, plugins: MapsLegacyLicensingStartDependencies) {} -} diff --git a/x-pack/plugins/maps_legacy_licensing/tsconfig.json b/x-pack/plugins/maps_legacy_licensing/tsconfig.json deleted file mode 100644 index 3b8102b5205a8..0000000000000 --- a/x-pack/plugins/maps_legacy_licensing/tsconfig.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "extends": "../../../tsconfig.project.json", - "compilerOptions": { - "composite": true, - "outDir": "./target/types", - "emitDeclarationOnly": true, - "declaration": true, - "declarationMap": true - }, - "include": ["public/**/*"], - "references": [ - { "path": "../licensing/tsconfig.json" }, - { "path": "../../../src/plugins/maps_ems/tsconfig.json" } - ] -} diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 133b4d0b6aaa8..6dc490b4ffc53 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -3080,7 +3080,6 @@ "maps_legacy.baseMapsVisualization.childShouldImplementMethodErrorMessage": "子はdata-updateに対応できるようこのメソッドを導入する必要があります", "maps_legacy.defaultDistributionMessage": "Mapsを入手するには、ElasticsearchとKibanaの{defaultDistribution}にアップグレードしてください。", "maps_legacy.kibanaMap.leaflet.fitDataBoundsAriaLabel": "データバウンドを合わせる", - "maps_legacy.kibanaMap.zoomWarning": "ズームレベルが最大に達しました。完全にズームインするには、ElasticsearchとKibanaの{defaultDistribution}にアップグレードしてください。{ems}ではより多くのズームレベルを無料で利用できます。または、独自のマップサーバーを構成できます。詳細は、{ wms }または{ configSettings}をご覧ください。", "maps_legacy.legacyMapDeprecationMessage": "Mapsを使用すると、複数のレイヤーとインデックスを追加する、個別のドキュメントをプロットする、データ値から特徴を表現する、ヒートマップ、グリッド、クラスターを追加するなど、さまざまなことが可能です。{getMapsMessage}", "maps_legacy.legacyMapDeprecationTitle": "{label}は8.0でMapsに移行されます。", "maps_legacy.openInMapsButtonLabel": "Mapsで表示", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 0f9d8b90a2578..32574690b13f2 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -3101,7 +3101,6 @@ "maps_legacy.baseMapsVisualization.childShouldImplementMethodErrorMessage": "子对象应实现此方法以响应数据更新", "maps_legacy.defaultDistributionMessage": "要获取 Maps,请升级到 {defaultDistribution} 版的 Elasticsearch 和 Kibana。", "maps_legacy.kibanaMap.leaflet.fitDataBoundsAriaLabel": "适应数据边界", - "maps_legacy.kibanaMap.zoomWarning": "已达到缩放级别数目上限。要一直放大,请升级到 Elasticsearch 和 Kibana 的{defaultDistribution}。您可以通过 {ems} 免费使用其他缩放级别。或者,您可以配置自己的地图服务器。请前往 { wms } 或 { configSettings} 以获取详细信息。", "maps_legacy.legacyMapDeprecationMessage": "使用 Maps,可以添加多个图层和索引,绘制单个文档,使用数据值表示特征,添加热图、网格和集群,等等。{getMapsMessage}", "maps_legacy.legacyMapDeprecationTitle": "在 8.0 中,{label} 将迁移到 Maps。", "maps_legacy.openInMapsButtonLabel": "在 Maps 中查看", From bcb72c596a438f6a223e875395380afe1efc291c Mon Sep 17 00:00:00 2001 From: Andrew Goldstein Date: Mon, 5 Apr 2021 11:39:09 -0600 Subject: [PATCH 28/30] [RAC][Alert Triage][TGrid] Update the Alerts Table (TGrid) API to implement `renderCellValue` (#96098) ### [RAC][Alert Triage][TGrid] Update the Alerts Table (TGrid) API to implement `renderCellValue` - This PR implements a superset of the `renderCellValue` API from [EuiDataGrid](https://elastic.github.io/eui/#/tabular-content/data-grid) in the `TGrid` (Timeline grid) API - The TGrid API was also updated to accept a collection of `RowRenderer`s as a prop The API changes are summarized by the following screenshot: render-cell-value The following screenshot shows the `signal.rule.risk_score` column in the Alerts table being rendered with a green background color, using the same technique illustrated by `EuiDataGrid`'s [codesandbox example](https://codesandbox.io/s/nsmzs): alerts Note: In the screenshot above, the values in the Alerts table are also _not_ rendered as draggables. Related (RAC) issue: https://github.com/elastic/kibana/issues/94520 ### Details The `StatefulEventsViewer` has been updated to accept `renderCellValue` as a (required) prop: ``` renderCellValue: (props: CellValueElementProps) => React.ReactNode; ``` The type definition of `CellValueElementProps` is: ``` export type CellValueElementProps = EuiDataGridCellValueElementProps & { data: TimelineNonEcsData[]; eventId: string; // _id header: ColumnHeaderOptions; linkValues: string[] | undefined; timelineId: string; }; ``` The `CellValueElementProps` type above is a _superset_ of `EuiDataGridCellValueElementProps`. The additional properties above include the `data` returned by the TGrid when it performs IO to retrieve alerts and events. ### Using `renderCellValue` to control rendering The internal implementation of TGrid's cell rendering didn't change with this PR; it moved to `x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/default_cell_renderer.tsx` as shown below: ``` export const DefaultCellRenderer: React.FC = ({ columnId, data, eventId, header, linkValues, setCellProps, timelineId, }) => ( <> {getColumnRenderer(header.id, columnRenderers, data).renderColumn({ columnName: header.id, eventId, field: header, linkValues, timelineId, truncate: true, values: getMappedNonEcsValue({ data, fieldName: header.id, }), })} ); ``` Any usages of TGrid were updated to pass `DefaultCellRenderer` as the value of the `renderCellValue` prop, as shown in the screenshot below: render-cell-value The `EuiDataGrid` [codesandbox example](https://codesandbox.io/s/nsmzs) provides the following example `renderCellValue` implementation, which highlights a cell green based on it's numeric value: ``` const renderCellValue = useMemo(() => { return ({ rowIndex, columnId, setCellProps }) => { const data = useContext(DataContext); useEffect(() => { if (columnId === 'amount') { if (data.hasOwnProperty(rowIndex)) { const numeric = parseFloat( data[rowIndex][columnId].match(/\d+\.\d+/)[0], 10 ); setCellProps({ style: { backgroundColor: `rgba(0, 255, 0, ${numeric * 0.0002})`, }, }); } } }, [rowIndex, columnId, setCellProps, data]); function getFormatted() { return data[rowIndex][columnId].formatted ? data[rowIndex][columnId].formatted : data[rowIndex][columnId]; } return data.hasOwnProperty(rowIndex) ? getFormatted(rowIndex, columnId) : null; }; }, []); ``` The sample code above formats the `amount` column in the example `EuiDataGrid` with a green `backgroundColor` based on the value of the data, as shown in the screenshot below: datagrid-cell-formatting To demonstrate that similar styling can be applied to TGrid using the same technique illustrated by `EuiDataGrid`'s [codesandbox example](https://codesandbox.io/s/nsmzs), we can update the `DefaultCellRenderer` in `x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/default_cell_renderer.tsx` to apply a similar technique: ``` export const DefaultCellRenderer: React.FC = ({ columnId, data, eventId, header, linkValues, setCellProps, timelineId, }) => { useEffect(() => { if (columnId === 'signal.rule.risk_score') { const value = getMappedNonEcsValue({ data, fieldName: columnId, }); if (Array.isArray(value) && value.length > 0) { const numeric = parseFloat(value[0]); setCellProps({ style: { backgroundColor: `rgba(0, 255, 0, ${numeric * 0.002})`, }, }); } } }, [columnId, data, setCellProps]); return ( <> {getMappedNonEcsValue({ data, fieldName: columnId, })} ); }; ``` The example code above renders the `signal.rule.risk_score` column in the Alerts table with a green `backgroundColor` based on the value of the data, as shown in the screenshot below: alerts Note: In the screenshot above, the values in the Alerts table are not rendered as draggables. --- .../components/alerts_viewer/alerts_table.tsx | 4 + .../events_viewer/events_viewer.test.tsx | 6 + .../events_viewer/events_viewer.tsx | 14 +- .../components/events_viewer/index.test.tsx | 4 + .../common/components/events_viewer/index.tsx | 13 +- .../components/alerts_table/index.tsx | 4 + .../navigation/events_query_tab_body.tsx | 4 + .../components/flyout/pane/index.tsx | 8 +- .../__snapshots__/index.test.tsx.snap | 553 ++++++++-- .../body/data_driven_columns/index.test.tsx | 4 +- .../body/data_driven_columns/index.tsx | 30 +- .../stateful_cell.test.tsx | 171 +++ .../data_driven_columns/stateful_cell.tsx | 63 ++ .../body/events/event_column_view.test.tsx | 2 + .../body/events/event_column_view.tsx | 8 +- .../components/timeline/body/events/index.tsx | 8 +- .../timeline/body/events/stateful_event.tsx | 10 +- .../components/timeline/body/index.test.tsx | 8 +- .../components/timeline/body/index.tsx | 13 +- .../body/renderers/get_row_renderer.test.tsx | 16 +- .../timeline/body/renderers/index.ts | 2 +- .../default_cell_renderer.test.tsx | 107 ++ .../cell_rendering/default_cell_renderer.tsx | 39 + .../timeline/cell_rendering/index.tsx | 20 + .../__snapshots__/index.test.tsx.snap | 980 ++++++++++++++++++ .../timeline/eql_tab_content/index.test.tsx | 4 + .../timeline/eql_tab_content/index.tsx | 8 + .../components/timeline/index.test.tsx | 4 + .../timelines/components/timeline/index.tsx | 14 +- .../__snapshots__/index.test.tsx.snap | 980 ++++++++++++++++++ .../pinned_tab_content/index.test.tsx | 5 +- .../timeline/pinned_tab_content/index.tsx | 8 + .../__snapshots__/index.test.tsx.snap | 980 ++++++++++++++++++ .../timeline/query_tab_content/index.test.tsx | 4 + .../timeline/query_tab_content/index.tsx | 8 + .../timeline/tabs_content/index.tsx | 64 +- .../timeline/epic_local_storage.test.tsx | 5 +- 37 files changed, 4047 insertions(+), 128 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/stateful_cell.test.tsx create mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/stateful_cell.tsx create mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/default_cell_renderer.test.tsx create mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/default_cell_renderer.tsx create mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/index.tsx diff --git a/x-pack/plugins/security_solution/public/common/components/alerts_viewer/alerts_table.tsx b/x-pack/plugins/security_solution/public/common/components/alerts_viewer/alerts_table.tsx index af90d17fe62b8..43d5c66655808 100644 --- a/x-pack/plugins/security_solution/public/common/components/alerts_viewer/alerts_table.tsx +++ b/x-pack/plugins/security_solution/public/common/components/alerts_viewer/alerts_table.tsx @@ -12,6 +12,8 @@ import { TimelineIdLiteral } from '../../../../common/types/timeline'; import { StatefulEventsViewer } from '../events_viewer'; import { alertsDefaultModel } from './default_headers'; import { useManageTimeline } from '../../../timelines/components/manage_timeline'; +import { defaultRowRenderers } from '../../../timelines/components/timeline/body/renderers'; +import { DefaultCellRenderer } from '../../../timelines/components/timeline/cell_rendering/default_cell_renderer'; import * as i18n from './translations'; import { useKibana } from '../../lib/kibana'; import { SourcererScopeName } from '../../store/sourcerer/model'; @@ -91,6 +93,8 @@ const AlertsTableComponent: React.FC = ({ defaultModel={alertsDefaultModel} end={endDate} id={timelineId} + renderCellValue={DefaultCellRenderer} + rowRenderers={defaultRowRenderers} scopeId={SourcererScopeName.default} start={startDate} /> diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx index 3ecc17589fe08..8962f5e6c5146 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx @@ -26,6 +26,8 @@ import { KqlMode } from '../../../timelines/store/timeline/model'; import { SortDirection } from '../../../timelines/components/timeline/body/sort'; import { AlertsTableFilterGroup } from '../../../detections/components/alerts_table/alerts_filter_group'; import { SourcererScopeName } from '../../store/sourcerer/model'; +import { defaultRowRenderers } from '../../../timelines/components/timeline/body/renderers'; +import { DefaultCellRenderer } from '../../../timelines/components/timeline/cell_rendering/default_cell_renderer'; import { useTimelineEvents } from '../../../timelines/containers'; jest.mock('../../../timelines/components/graph_overlay', () => ({ @@ -99,6 +101,8 @@ const eventsViewerDefaultProps = { query: '', language: 'kql', }, + renderCellValue: DefaultCellRenderer, + rowRenderers: defaultRowRenderers, start: from, sort: [ { @@ -118,6 +122,8 @@ describe('EventsViewer', () => { defaultModel: eventsDefaultModel, end: to, id: TimelineId.test, + renderCellValue: DefaultCellRenderer, + rowRenderers: defaultRowRenderers, start: from, scopeId: SourcererScopeName.timeline, }; diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx index 050cd92b0556e..e6e868f1a7365 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx @@ -4,7 +4,6 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - import { EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui'; import { isEmpty } from 'lodash/fp'; import React, { useEffect, useMemo, useState } from 'react'; @@ -41,7 +40,9 @@ import { useManageTimeline } from '../../../timelines/components/manage_timeline import { ExitFullScreen } from '../exit_full_screen'; import { useGlobalFullScreen } from '../../containers/use_full_screen'; import { TimelineId, TimelineTabs } from '../../../../common/types/timeline'; +import { RowRenderer } from '../../../timelines/components/timeline/body/renderers/row_renderer'; import { GraphOverlay } from '../../../timelines/components/graph_overlay'; +import { CellValueElementProps } from '../../../timelines/components/timeline/cell_rendering'; import { SELECTOR_TIMELINE_GLOBAL_CONTAINER } from '../../../timelines/components/timeline/styles'; export const EVENTS_VIEWER_HEADER_HEIGHT = 90; // px @@ -122,6 +123,8 @@ interface Props { kqlMode: KqlMode; query: Query; onRuleChange?: () => void; + renderCellValue: (props: CellValueElementProps) => React.ReactNode; + rowRenderers: RowRenderer[]; start: string; sort: Sort[]; utilityBar?: (refetch: inputsModel.Refetch, totalCount: number) => React.ReactNode; @@ -146,8 +149,10 @@ const EventsViewerComponent: React.FC = ({ itemsPerPage, itemsPerPageOptions, kqlMode, - query, onRuleChange, + query, + renderCellValue, + rowRenderers, start, sort, utilityBar, @@ -310,6 +315,8 @@ const EventsViewerComponent: React.FC = ({ isEventViewer={true} onRuleChange={onRuleChange} refetch={refetch} + renderCellValue={renderCellValue} + rowRenderers={rowRenderers} sort={sort} tabType={TimelineTabs.query} totalPages={calculateTotalPages({ @@ -343,6 +350,7 @@ const EventsViewerComponent: React.FC = ({ export const EventsViewer = React.memo( EventsViewerComponent, + // eslint-disable-next-line complexity (prevProps, nextProps) => deepEqual(prevProps.browserFields, nextProps.browserFields) && prevProps.columns === nextProps.columns && @@ -359,6 +367,8 @@ export const EventsViewer = React.memo( prevProps.itemsPerPageOptions === nextProps.itemsPerPageOptions && prevProps.kqlMode === nextProps.kqlMode && deepEqual(prevProps.query, nextProps.query) && + prevProps.renderCellValue === nextProps.renderCellValue && + prevProps.rowRenderers === nextProps.rowRenderers && prevProps.start === nextProps.start && deepEqual(prevProps.sort, nextProps.sort) && prevProps.utilityBar === nextProps.utilityBar && diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.test.tsx index 5004c23f9111c..cd27177643b44 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.test.tsx @@ -18,7 +18,9 @@ import { StatefulEventsViewer } from '.'; import { eventsDefaultModel } from './default_model'; import { TimelineId } from '../../../../common/types/timeline'; import { SourcererScopeName } from '../../store/sourcerer/model'; +import { DefaultCellRenderer } from '../../../timelines/components/timeline/cell_rendering/default_cell_renderer'; import { useTimelineEvents } from '../../../timelines/containers'; +import { defaultRowRenderers } from '../../../timelines/components/timeline/body/renderers'; jest.mock('../../../timelines/containers', () => ({ useTimelineEvents: jest.fn(), @@ -38,6 +40,8 @@ const testProps = { end: to, indexNames: [], id: TimelineId.test, + renderCellValue: DefaultCellRenderer, + rowRenderers: defaultRowRenderers, scopeId: SourcererScopeName.default, start: from, }; diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx index 59dc756bb2b3e..b58aa2236d292 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx @@ -22,6 +22,8 @@ import { useGlobalFullScreen } from '../../containers/use_full_screen'; import { SourcererScopeName } from '../../store/sourcerer/model'; import { useSourcererScope } from '../../containers/sourcerer'; import { DetailsPanel } from '../../../timelines/components/side_panel'; +import { RowRenderer } from '../../../timelines/components/timeline/body/renderers/row_renderer'; +import { CellValueElementProps } from '../../../timelines/components/timeline/cell_rendering'; const DEFAULT_EVENTS_VIEWER_HEIGHT = 652; @@ -41,6 +43,8 @@ export interface OwnProps { headerFilterGroup?: React.ReactNode; pageFilters?: Filter[]; onRuleChange?: () => void; + renderCellValue: (props: CellValueElementProps) => React.ReactNode; + rowRenderers: RowRenderer[]; utilityBar?: (refetch: inputsModel.Refetch, totalCount: number) => React.ReactNode; } @@ -67,8 +71,10 @@ const StatefulEventsViewerComponent: React.FC = ({ itemsPerPageOptions, kqlMode, pageFilters, - query, onRuleChange, + query, + renderCellValue, + rowRenderers, start, scopeId, showCheckboxes, @@ -129,6 +135,8 @@ const StatefulEventsViewerComponent: React.FC = ({ kqlMode={kqlMode} query={query} onRuleChange={onRuleChange} + renderCellValue={renderCellValue} + rowRenderers={rowRenderers} start={start} sort={sort} utilityBar={utilityBar} @@ -201,6 +209,7 @@ type PropsFromRedux = ConnectedProps; export const StatefulEventsViewer = connector( React.memo( StatefulEventsViewerComponent, + // eslint-disable-next-line complexity (prevProps, nextProps) => prevProps.id === nextProps.id && prevProps.scopeId === nextProps.scopeId && @@ -215,6 +224,8 @@ export const StatefulEventsViewer = connector( deepEqual(prevProps.itemsPerPageOptions, nextProps.itemsPerPageOptions) && prevProps.kqlMode === nextProps.kqlMode && deepEqual(prevProps.query, nextProps.query) && + prevProps.renderCellValue === nextProps.renderCellValue && + prevProps.rowRenderers === nextProps.rowRenderers && deepEqual(prevProps.sort, nextProps.sort) && prevProps.start === nextProps.start && deepEqual(prevProps.pageFilters, nextProps.pageFilters) && diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx index 6c88b8e29800b..cf6db52d0cece 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx @@ -48,6 +48,8 @@ import { import { SourcererScopeName } from '../../../common/store/sourcerer/model'; import { useSourcererScope } from '../../../common/containers/sourcerer'; import { buildTimeRangeFilter } from './helpers'; +import { DefaultCellRenderer } from '../../../timelines/components/timeline/cell_rendering/default_cell_renderer'; +import { defaultRowRenderers } from '../../../timelines/components/timeline/body/renderers'; interface OwnProps { timelineId: TimelineIdLiteral; @@ -336,6 +338,8 @@ export const AlertsTableComponent: React.FC = ({ headerFilterGroup={headerFilterGroup} id={timelineId} onRuleChange={onRuleChange} + renderCellValue={DefaultCellRenderer} + rowRenderers={defaultRowRenderers} scopeId={SourcererScopeName.detections} start={from} utilityBar={utilityBarCallback} diff --git a/x-pack/plugins/security_solution/public/hosts/pages/navigation/events_query_tab_body.tsx b/x-pack/plugins/security_solution/public/hosts/pages/navigation/events_query_tab_body.tsx index 922d52b6cfe5a..f88709e6e95ac 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/navigation/events_query_tab_body.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/navigation/events_query_tab_body.tsx @@ -21,6 +21,8 @@ import { useGlobalFullScreen } from '../../../common/containers/use_full_screen' import * as i18n from '../translations'; import { MatrixHistogramType } from '../../../../common/search_strategy/security_solution'; import { useManageTimeline } from '../../../timelines/components/manage_timeline'; +import { defaultRowRenderers } from '../../../timelines/components/timeline/body/renderers'; +import { DefaultCellRenderer } from '../../../timelines/components/timeline/cell_rendering/default_cell_renderer'; import { SourcererScopeName } from '../../../common/store/sourcerer/model'; const EVENTS_HISTOGRAM_ID = 'eventsHistogramQuery'; @@ -96,6 +98,8 @@ const EventsQueryTabBodyComponent: React.FC = ({ defaultModel={eventsDefaultModel} end={endDate} id={TimelineId.hostsPageEvents} + renderCellValue={DefaultCellRenderer} + rowRenderers={defaultRowRenderers} scopeId={SourcererScopeName.default} start={startDate} pageFilters={pageFilters} diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/index.tsx index e63ffedf3da7c..459706de36569 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/index.tsx @@ -14,6 +14,8 @@ import { StatefulTimeline } from '../../timeline'; import { TimelineId } from '../../../../../common/types/timeline'; import * as i18n from './translations'; import { timelineActions } from '../../../store/timeline'; +import { defaultRowRenderers } from '../../timeline/body/renderers'; +import { DefaultCellRenderer } from '../../timeline/cell_rendering/default_cell_renderer'; import { focusActiveTimelineButton } from '../../timeline/helpers'; interface FlyoutPaneComponentProps { @@ -46,7 +48,11 @@ const FlyoutPaneComponent: React.FC = ({ timelineId }) onClose={handleClose} size="l" > - + ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/__snapshots__/index.test.tsx.snap index 72d2956bd4086..91d039a19495c 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/__snapshots__/index.test.tsx.snap @@ -22,26 +22,77 @@ exports[`Columns it renders the expected columns 1`] = ` You are in a table cell. row: 2, column: 2

- @@ -63,15 +114,77 @@ exports[`Columns it renders the expected columns 1`] = ` You are in a table cell. row: 2, column: 3

- @@ -93,15 +206,77 @@ exports[`Columns it renders the expected columns 1`] = ` You are in a table cell. row: 2, column: 4

- @@ -123,15 +298,77 @@ exports[`Columns it renders the expected columns 1`] = ` You are in a table cell. row: 2, column: 5

- @@ -153,15 +390,77 @@ exports[`Columns it renders the expected columns 1`] = ` You are in a table cell. row: 2, column: 6

- @@ -183,15 +482,77 @@ exports[`Columns it renders the expected columns 1`] = ` You are in a table cell. row: 2, column: 7

- @@ -213,15 +574,77 @@ exports[`Columns it renders the expected columns 1`] = ` You are in a table cell. row: 2, column: 8

- diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/index.test.tsx index f20978c6ba726..234e28e6231c5 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/index.test.tsx @@ -9,10 +9,10 @@ import { shallow } from 'enzyme'; import React from 'react'; +import { DefaultCellRenderer } from '../../cell_rendering/default_cell_renderer'; import '../../../../../common/mock/match_media'; import { mockTimelineData } from '../../../../../common/mock'; import { defaultHeaders } from '../column_headers/default_headers'; -import { columnRenderers } from '../renderers'; import { DataDrivenColumns } from '.'; @@ -25,11 +25,11 @@ describe('Columns', () => { ariaRowindex={2} _id={mockTimelineData[0]._id} columnHeaders={headersSansTimestamp} - columnRenderers={columnRenderers} data={mockTimelineData[0].data} ecsData={mockTimelineData[0].ecs} hasRowRenderers={false} notesCount={0} + renderCellValue={DefaultCellRenderer} timelineId="test" /> ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/index.tsx index 5aba562749f01..aeb9af46ea2ec 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/index.tsx @@ -9,6 +9,7 @@ import { EuiScreenReaderOnly } from '@elastic/eui'; import React from 'react'; import { getOr } from 'lodash/fp'; +import { CellValueElementProps } from '../../cell_rendering'; import { DRAGGABLE_KEYBOARD_WRAPPER_CLASS_NAME } from '../../../../../common/components/drag_and_drop/helpers'; import { Ecs } from '../../../../../../common/ecs'; import { TimelineNonEcsData } from '../../../../../../common/search_strategy/timeline'; @@ -16,20 +17,19 @@ import { TimelineTabs } from '../../../../../../common/types/timeline'; import { ColumnHeaderOptions } from '../../../../../timelines/store/timeline/model'; import { ARIA_COLUMN_INDEX_OFFSET } from '../../helpers'; import { EventsTd, EVENTS_TD_CLASS_NAME, EventsTdContent, EventsTdGroupData } from '../../styles'; -import { ColumnRenderer } from '../renderers/column_renderer'; -import { getColumnRenderer } from '../renderers/get_column_renderer'; +import { StatefulCell } from './stateful_cell'; import * as i18n from './translations'; interface Props { _id: string; ariaRowindex: number; columnHeaders: ColumnHeaderOptions[]; - columnRenderers: ColumnRenderer[]; data: TimelineNonEcsData[]; ecsData: Ecs; hasRowRenderers: boolean; notesCount: number; + renderCellValue: (props: CellValueElementProps) => React.ReactNode; tabType?: TimelineTabs; timelineId: string; } @@ -82,11 +82,11 @@ export const DataDrivenColumns = React.memo( _id, ariaRowindex, columnHeaders, - columnRenderers, data, ecsData, hasRowRenderers, notesCount, + renderCellValue, tabType, timelineId, }) => ( @@ -105,18 +105,16 @@ export const DataDrivenColumns = React.memo(

{i18n.YOU_ARE_IN_A_TABLE_CELL({ row: ariaRowindex, column: i + 2 })}

- {getColumnRenderer(header.id, columnRenderers, data).renderColumn({ - columnName: header.id, - eventId: _id, - field: header, - linkValues: getOr([], header.linkField ?? '', ecsData), - timelineId: tabType != null ? `${timelineId}-${tabType}` : timelineId, - truncate: true, - values: getMappedNonEcsValue({ - data, - fieldName: header.id, - }), - })} + {hasRowRenderers ? ( diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/stateful_cell.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/stateful_cell.test.tsx new file mode 100644 index 0000000000000..3c75bc7fb2649 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/stateful_cell.test.tsx @@ -0,0 +1,171 @@ +/* + * 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 { cloneDeep } from 'lodash/fp'; +import React, { useEffect } from 'react'; + +import { CellValueElementProps } from '../../cell_rendering'; +import { defaultHeaders, mockTimelineData } from '../../../../../common/mock'; +import { TimelineNonEcsData } from '../../../../../../common/search_strategy/timeline'; +import { TimelineTabs } from '../../../../../../common/types/timeline'; +import { ColumnHeaderOptions } from '../../../../store/timeline/model'; + +import { StatefulCell } from './stateful_cell'; +import { getMappedNonEcsValue } from '.'; + +/** + * This (test) component implement's `EuiDataGrid`'s `renderCellValue` interface, + * as documented here: https://elastic.github.io/eui/#/tabular-content/data-grid + * + * Its `CellValueElementProps` props are a superset of `EuiDataGridCellValueElementProps`. + * The `setCellProps` function, defined by the `EuiDataGridCellValueElementProps` interface, + * is typically called in a `useEffect`, as illustrated by `EuiDataGrid`'s code sandbox example: + * https://codesandbox.io/s/zhxmo + */ +const RenderCellValue: React.FC = ({ columnId, data, setCellProps }) => { + useEffect(() => { + // branching logic that conditionally renders a specific cell green: + if (columnId === defaultHeaders[0].id) { + const value = getMappedNonEcsValue({ + data, + fieldName: columnId, + }); + + if (value?.length) { + setCellProps({ + style: { + backgroundColor: 'green', + }, + }); + } + } + }, [columnId, data, setCellProps]); + + return ( +
+ {getMappedNonEcsValue({ + data, + fieldName: columnId, + })} +
+ ); +}; + +describe('StatefulCell', () => { + const ariaRowindex = 123; + const eventId = '_id-123'; + const linkValues = ['foo', 'bar', '@baz']; + const tabType = TimelineTabs.query; + const timelineId = 'test'; + + let header: ColumnHeaderOptions; + let data: TimelineNonEcsData[]; + beforeEach(() => { + data = cloneDeep(mockTimelineData[0].data); + header = cloneDeep(defaultHeaders[0]); + }); + + test('it invokes renderCellValue with the expected arguments when tabType is specified', () => { + const renderCellValue = jest.fn(); + + mount( + + ); + + expect(renderCellValue).toBeCalledWith( + expect.objectContaining({ + columnId: header.id, + eventId, + data, + header, + isExpandable: true, + isExpanded: false, + isDetails: false, + linkValues, + rowIndex: ariaRowindex - 1, + timelineId: `${timelineId}-${tabType}`, + }) + ); + }); + + test('it invokes renderCellValue with the expected arguments when tabType is NOT specified', () => { + const renderCellValue = jest.fn(); + + mount( + + ); + + expect(renderCellValue).toBeCalledWith( + expect.objectContaining({ + columnId: header.id, + eventId, + data, + header, + isExpandable: true, + isExpanded: false, + isDetails: false, + linkValues, + rowIndex: ariaRowindex - 1, + timelineId, + }) + ); + }); + + test('it renders the React.Node returned by renderCellValue', () => { + const renderCellValue = () =>
; + + const wrapper = mount( + + ); + + expect(wrapper.find('[data-test-subj="renderCellValue"]').exists()).toBe(true); + }); + + test("it renders a div with the styles set by `renderCellValue`'s `setCellProps` argument", () => { + const wrapper = mount( + + ); + + expect( + wrapper.find('[data-test-subj="statefulCell"]').getDOMNode().getAttribute('style') + ).toEqual('background-color: green;'); + }); +}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/stateful_cell.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/stateful_cell.tsx new file mode 100644 index 0000000000000..83f603364ba8c --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/stateful_cell.tsx @@ -0,0 +1,63 @@ +/* + * 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, { HTMLAttributes, useState } from 'react'; + +import { CellValueElementProps } from '../../cell_rendering'; +import { TimelineNonEcsData } from '../../../../../../common/search_strategy/timeline'; +import { TimelineTabs } from '../../../../../../common/types/timeline'; +import { ColumnHeaderOptions } from '../../../../../timelines/store/timeline/model'; + +export interface CommonProps { + className?: string; + 'aria-label'?: string; + 'data-test-subj'?: string; +} + +const StatefulCellComponent = ({ + ariaRowindex, + data, + header, + eventId, + linkValues, + renderCellValue, + tabType, + timelineId, +}: { + ariaRowindex: number; + data: TimelineNonEcsData[]; + header: ColumnHeaderOptions; + eventId: string; + linkValues: string[] | undefined; + renderCellValue: (props: CellValueElementProps) => React.ReactNode; + tabType?: TimelineTabs; + timelineId: string; +}) => { + const [cellProps, setCellProps] = useState>({}); + + return ( +
+ {renderCellValue({ + columnId: header.id, + eventId, + data, + header, + isExpandable: true, + isExpanded: false, + isDetails: false, + linkValues, + rowIndex: ariaRowindex - 1, + setCellProps, + timelineId: tabType != null ? `${timelineId}-${tabType}` : timelineId, + })} +
+ ); +}; + +StatefulCellComponent.displayName = 'StatefulCellComponent'; + +export const StatefulCell = React.memo(StatefulCellComponent); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.test.tsx index abdfda3272d6a..74724dedf4d11 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.test.tsx @@ -14,6 +14,7 @@ import { DEFAULT_ACTIONS_COLUMN_WIDTH } from '../constants'; import * as i18n from '../translations'; import { EventColumnView } from './event_column_view'; +import { DefaultCellRenderer } from '../../cell_rendering/default_cell_renderer'; import { TimelineTabs, TimelineType, TimelineId } from '../../../../../../common/types/timeline'; import { useShallowEqualSelector } from '../../../../../common/hooks/use_selector'; @@ -56,6 +57,7 @@ describe('EventColumnView', () => { onRowSelected: jest.fn(), onUnPinEvent: jest.fn(), refetch: jest.fn(), + renderCellValue: DefaultCellRenderer, selectedEventIds: {}, showCheckboxes: false, showNotes: false, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx index c6caf0a7b5b15..a0a0aeb23e8f7 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx @@ -7,6 +7,7 @@ import React, { useCallback, useMemo } from 'react'; +import { CellValueElementProps } from '../../cell_rendering'; import { useShallowEqualSelector } from '../../../../../common/hooks/use_selector'; import { Ecs } from '../../../../../../common/ecs'; import { TimelineNonEcsData } from '../../../../../../common/search_strategy/timeline'; @@ -21,7 +22,6 @@ import { getPinOnClick, InvestigateInResolverAction, } from '../helpers'; -import { ColumnRenderer } from '../renderers/column_renderer'; import { AlertContextMenu } from '../../../../../detections/components/alerts_table/timeline_actions/alert_context_menu'; import { InvestigateInTimelineAction } from '../../../../../detections/components/alerts_table/timeline_actions/investigate_in_timeline_action'; import { AddEventNoteAction } from '../actions/add_note_icon_item'; @@ -38,7 +38,6 @@ interface Props { actionsColumnWidth: number; ariaRowindex: number; columnHeaders: ColumnHeaderOptions[]; - columnRenderers: ColumnRenderer[]; data: TimelineNonEcsData[]; ecsData: Ecs; eventIdToNoteIds: Readonly>; @@ -51,6 +50,7 @@ interface Props { onRowSelected: OnRowSelected; onUnPinEvent: OnUnPinEvent; refetch: inputsModel.Refetch; + renderCellValue: (props: CellValueElementProps) => React.ReactNode; onRuleChange?: () => void; hasRowRenderers: boolean; selectedEventIds: Readonly>; @@ -69,7 +69,6 @@ export const EventColumnView = React.memo( actionsColumnWidth, ariaRowindex, columnHeaders, - columnRenderers, data, ecsData, eventIdToNoteIds, @@ -84,6 +83,7 @@ export const EventColumnView = React.memo( refetch, hasRowRenderers, onRuleChange, + renderCellValue, selectedEventIds, showCheckboxes, showNotes, @@ -227,11 +227,11 @@ export const EventColumnView = React.memo( _id={id} ariaRowindex={ariaRowindex} columnHeaders={columnHeaders} - columnRenderers={columnRenderers} data={data} ecsData={ecsData} hasRowRenderers={hasRowRenderers} notesCount={notesCount} + renderCellValue={renderCellValue} tabType={tabType} timelineId={timelineId} /> diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/index.tsx index d76b5834c233e..7f8a3a92fb5ba 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/index.tsx @@ -8,6 +8,7 @@ import React from 'react'; import { isEmpty } from 'lodash'; +import { CellValueElementProps } from '../../cell_rendering'; import { inputsModel } from '../../../../../common/store'; import { BrowserFields } from '../../../../../common/containers/source'; import { @@ -18,7 +19,6 @@ import { TimelineTabs } from '../../../../../../common/types/timeline'; import { ColumnHeaderOptions } from '../../../../../timelines/store/timeline/model'; import { OnRowSelected } from '../../events'; import { EventsTbody } from '../../styles'; -import { ColumnRenderer } from '../renderers/column_renderer'; import { RowRenderer } from '../renderers/row_renderer'; import { StatefulEvent } from './stateful_event'; import { eventIsPinned } from '../helpers'; @@ -30,7 +30,6 @@ interface Props { actionsColumnWidth: number; browserFields: BrowserFields; columnHeaders: ColumnHeaderOptions[]; - columnRenderers: ColumnRenderer[]; containerRef: React.MutableRefObject; data: TimelineItem[]; eventIdToNoteIds: Readonly>; @@ -41,6 +40,7 @@ interface Props { onRowSelected: OnRowSelected; pinnedEventIds: Readonly>; refetch: inputsModel.Refetch; + renderCellValue: (props: CellValueElementProps) => React.ReactNode; onRuleChange?: () => void; rowRenderers: RowRenderer[]; selectedEventIds: Readonly>; @@ -52,7 +52,6 @@ const EventsComponent: React.FC = ({ actionsColumnWidth, browserFields, columnHeaders, - columnRenderers, containerRef, data, eventIdToNoteIds, @@ -64,6 +63,7 @@ const EventsComponent: React.FC = ({ pinnedEventIds, refetch, onRuleChange, + renderCellValue, rowRenderers, selectedEventIds, showCheckboxes, @@ -76,7 +76,6 @@ const EventsComponent: React.FC = ({ ariaRowindex={i + ARIA_ROW_INDEX_OFFSET} browserFields={browserFields} columnHeaders={columnHeaders} - columnRenderers={columnRenderers} containerRef={containerRef} event={event} eventIdToNoteIds={eventIdToNoteIds} @@ -88,6 +87,7 @@ const EventsComponent: React.FC = ({ lastFocusedAriaColindex={lastFocusedAriaColindex} loadingEventIds={loadingEventIds} onRowSelected={onRowSelected} + renderCellValue={renderCellValue} refetch={refetch} rowRenderers={rowRenderers} onRuleChange={onRuleChange} diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx index 4191badd6b03f..97ab088b61583 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx @@ -8,6 +8,7 @@ import React, { useCallback, useMemo, useRef, useState } from 'react'; import { useDispatch } from 'react-redux'; +import { CellValueElementProps } from '../../cell_rendering'; import { useDeepEqualSelector } from '../../../../../common/hooks/use_selector'; import { TimelineExpandedDetailType, @@ -23,7 +24,6 @@ import { ColumnHeaderOptions } from '../../../../../timelines/store/timeline/mod import { OnPinEvent, OnRowSelected } from '../../events'; import { STATEFUL_EVENT_CSS_CLASS_NAME } from '../../helpers'; import { EventsTrGroup, EventsTrSupplement, EventsTrSupplementContainer } from '../../styles'; -import { ColumnRenderer } from '../renderers/column_renderer'; import { RowRenderer } from '../renderers/row_renderer'; import { isEventBuildingBlockType, getEventType, isEvenEqlSequence } from '../helpers'; import { NoteCards } from '../../../notes/note_cards'; @@ -45,7 +45,6 @@ interface Props { containerRef: React.MutableRefObject; browserFields: BrowserFields; columnHeaders: ColumnHeaderOptions[]; - columnRenderers: ColumnRenderer[]; event: TimelineItem; eventIdToNoteIds: Readonly>; isEventViewer?: boolean; @@ -56,6 +55,7 @@ interface Props { refetch: inputsModel.Refetch; ariaRowindex: number; onRuleChange?: () => void; + renderCellValue: (props: CellValueElementProps) => React.ReactNode; rowRenderers: RowRenderer[]; selectedEventIds: Readonly>; showCheckboxes: boolean; @@ -77,7 +77,6 @@ const StatefulEventComponent: React.FC = ({ browserFields, containerRef, columnHeaders, - columnRenderers, event, eventIdToNoteIds, isEventViewer = false, @@ -86,8 +85,9 @@ const StatefulEventComponent: React.FC = ({ loadingEventIds, onRowSelected, refetch, - onRuleChange, + renderCellValue, rowRenderers, + onRuleChange, ariaRowindex, selectedEventIds, showCheckboxes, @@ -259,7 +259,6 @@ const StatefulEventComponent: React.FC = ({ actionsColumnWidth={actionsColumnWidth} ariaRowindex={ariaRowindex} columnHeaders={columnHeaders} - columnRenderers={columnRenderers} data={event.data} ecsData={event.ecs} eventIdToNoteIds={eventIdToNoteIds} @@ -273,6 +272,7 @@ const StatefulEventComponent: React.FC = ({ onRowSelected={onRowSelected} onUnPinEvent={onUnPinEvent} refetch={refetch} + renderCellValue={renderCellValue} onRuleChange={onRuleChange} selectedEventIds={selectedEventIds} showCheckboxes={showCheckboxes} diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx index 723e4c3de5c27..76dbfc553d228 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx @@ -8,6 +8,7 @@ import React from 'react'; import { waitFor } from '@testing-library/react'; +import { DefaultCellRenderer } from '../cell_rendering/default_cell_renderer'; import '../../../../common/mock/match_media'; import { mockBrowserFields } from '../../../../common/containers/source/mock'; import { Direction } from '../../../../../common/search_strategy'; @@ -19,6 +20,7 @@ import { Sort } from './sort'; import { useMountAppended } from '../../../../common/utils/use_mount_appended'; import { timelineActions } from '../../../store/timeline'; import { TimelineTabs } from '../../../../../common/types/timeline'; +import { defaultRowRenderers } from './renderers'; const mockSort: Sort[] = [ { @@ -39,8 +41,8 @@ jest.mock('react-redux', () => { }); jest.mock('../../../../common/hooks/use_selector', () => ({ - useShallowEqualSelector: jest.fn().mockReturnValue(mockTimelineModel), - useDeepEqualSelector: jest.fn().mockReturnValue(mockTimelineModel), + useShallowEqualSelector: () => mockTimelineModel, + useDeepEqualSelector: () => mockTimelineModel, })); jest.mock('../../../../common/components/link_to'); @@ -76,6 +78,8 @@ describe('Body', () => { loadingEventIds: [], pinnedEventIds: {}, refetch: jest.fn(), + renderCellValue: DefaultCellRenderer, + rowRenderers: defaultRowRenderers, selectedEventIds: {}, setSelected: (jest.fn() as unknown) as StatefulBodyProps['setSelected'], sort: mockSort, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx index 4df6eb16ccb62..59c0610c544e9 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx @@ -11,6 +11,7 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { connect, ConnectedProps } from 'react-redux'; import deepEqual from 'fast-deep-equal'; +import { CellValueElementProps } from '../cell_rendering'; import { RowRendererId, TimelineId, TimelineTabs } from '../../../../../common/types/timeline'; import { FIRST_ARIA_INDEX, @@ -28,9 +29,9 @@ import { timelineActions, timelineSelectors } from '../../../store/timeline'; import { OnRowSelected, OnSelectAll } from '../events'; import { getActionsColumnWidth, getColumnHeaders } from './column_headers/helpers'; import { getEventIdToDataMapping } from './helpers'; -import { columnRenderers, rowRenderers } from './renderers'; import { Sort } from './sort'; import { plainRowRenderer } from './renderers/plain_row_renderer'; +import { RowRenderer } from './renderers/row_renderer'; import { EventsTable, TimelineBody, TimelineBodyGlobalStyle } from '../styles'; import { ColumnHeaders } from './column_headers'; import { Events } from './events'; @@ -44,6 +45,8 @@ interface OwnProps { isEventViewer?: boolean; sort: Sort[]; refetch: inputsModel.Refetch; + renderCellValue: (props: CellValueElementProps) => React.ReactNode; + rowRenderers: RowRenderer[]; tabType: TimelineTabs; totalPages: number; onRuleChange?: () => void; @@ -83,6 +86,8 @@ export const BodyComponent = React.memo( onRuleChange, showCheckboxes, refetch, + renderCellValue, + rowRenderers, sort, tabType, totalPages, @@ -141,7 +146,7 @@ export const BodyComponent = React.memo( if (!excludedRowRendererIds) return rowRenderers; return rowRenderers.filter((rowRenderer) => !excludedRowRendererIds.includes(rowRenderer.id)); - }, [excludedRowRendererIds]); + }, [excludedRowRendererIds, rowRenderers]); const actionsColumnWidth = useMemo( () => @@ -209,7 +214,6 @@ export const BodyComponent = React.memo( actionsColumnWidth={actionsColumnWidth} browserFields={browserFields} columnHeaders={columnHeaders} - columnRenderers={columnRenderers} data={data} eventIdToNoteIds={eventIdToNoteIds} id={id} @@ -219,6 +223,7 @@ export const BodyComponent = React.memo( onRowSelected={onRowSelected} pinnedEventIds={pinnedEventIds} refetch={refetch} + renderCellValue={renderCellValue} rowRenderers={enabledRowRenderers} onRuleChange={onRuleChange} selectedEventIds={selectedEventIds} @@ -244,6 +249,8 @@ export const BodyComponent = React.memo( prevProps.id === nextProps.id && prevProps.isEventViewer === nextProps.isEventViewer && prevProps.isSelectAllChecked === nextProps.isSelectAllChecked && + prevProps.renderCellValue === nextProps.renderCellValue && + prevProps.rowRenderers === nextProps.rowRenderers && prevProps.showCheckboxes === nextProps.showCheckboxes && prevProps.tabType === nextProps.tabType ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/get_row_renderer.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/get_row_renderer.test.tsx index 6e36102da2de9..b92a4381d837b 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/get_row_renderer.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/get_row_renderer.test.tsx @@ -17,7 +17,7 @@ import { mockTimelineData } from '../../../../../common/mock'; import { TestProviders } from '../../../../../common/mock/test_providers'; import { useMountAppended } from '../../../../../common/utils/use_mount_appended'; -import { rowRenderers } from '.'; +import { defaultRowRenderers } from '.'; import { getRowRenderer } from './get_row_renderer'; jest.mock('@elastic/eui', () => { @@ -48,7 +48,7 @@ describe('get_column_renderer', () => { }); test('renders correctly against snapshot', () => { - const rowRenderer = getRowRenderer(nonSuricata, rowRenderers); + const rowRenderer = getRowRenderer(nonSuricata, defaultRowRenderers); const row = rowRenderer?.renderRow({ browserFields: mockBrowserFields, data: nonSuricata, @@ -60,7 +60,7 @@ describe('get_column_renderer', () => { }); test('should render plain row data when it is a non suricata row', () => { - const rowRenderer = getRowRenderer(nonSuricata, rowRenderers); + const rowRenderer = getRowRenderer(nonSuricata, defaultRowRenderers); const row = rowRenderer?.renderRow({ browserFields: mockBrowserFields, data: nonSuricata, @@ -75,7 +75,7 @@ describe('get_column_renderer', () => { }); test('should render a suricata row data when it is a suricata row', () => { - const rowRenderer = getRowRenderer(suricata, rowRenderers); + const rowRenderer = getRowRenderer(suricata, defaultRowRenderers); const row = rowRenderer?.renderRow({ browserFields: mockBrowserFields, data: suricata, @@ -93,7 +93,7 @@ describe('get_column_renderer', () => { test('should render a suricata row data if event.category is network_traffic', () => { suricata.event = { ...suricata.event, ...{ category: ['network_traffic'] } }; - const rowRenderer = getRowRenderer(suricata, rowRenderers); + const rowRenderer = getRowRenderer(suricata, defaultRowRenderers); const row = rowRenderer?.renderRow({ browserFields: mockBrowserFields, data: suricata, @@ -111,7 +111,7 @@ describe('get_column_renderer', () => { test('should render a zeek row data if event.category is network_traffic', () => { zeek.event = { ...zeek.event, ...{ category: ['network_traffic'] } }; - const rowRenderer = getRowRenderer(zeek, rowRenderers); + const rowRenderer = getRowRenderer(zeek, defaultRowRenderers); const row = rowRenderer?.renderRow({ browserFields: mockBrowserFields, data: zeek, @@ -129,7 +129,7 @@ describe('get_column_renderer', () => { test('should render a system row data if event.category is network_traffic', () => { system.event = { ...system.event, ...{ category: ['network_traffic'] } }; - const rowRenderer = getRowRenderer(system, rowRenderers); + const rowRenderer = getRowRenderer(system, defaultRowRenderers); const row = rowRenderer?.renderRow({ browserFields: mockBrowserFields, data: system, @@ -147,7 +147,7 @@ describe('get_column_renderer', () => { test('should render a auditd row data if event.category is network_traffic', () => { auditd.event = { ...auditd.event, ...{ category: ['network_traffic'] } }; - const rowRenderer = getRowRenderer(auditd, rowRenderers); + const rowRenderer = getRowRenderer(auditd, defaultRowRenderers); const row = rowRenderer?.renderRow({ browserFields: mockBrowserFields, data: auditd, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/index.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/index.ts index 671d183c62e6d..209a9414f62f1 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/index.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/index.ts @@ -23,7 +23,7 @@ import { systemRowRenderers } from './system/generic_row_renderer'; // Suricata and Zeek which is why Suricata and Zeek are above it. The // plainRowRenderer always returns true to everything which is why it always // should be last. -export const rowRenderers: RowRenderer[] = [ +export const defaultRowRenderers: RowRenderer[] = [ ...auditdRowRenderers, ...systemRowRenderers, suricataRowRenderer, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/default_cell_renderer.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/default_cell_renderer.test.tsx new file mode 100644 index 0000000000000..5ac1dcf8805cf --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/default_cell_renderer.test.tsx @@ -0,0 +1,107 @@ +/* + * 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 { cloneDeep } from 'lodash/fp'; +import React from 'react'; + +import { columnRenderers } from '../body/renderers'; +import { getColumnRenderer } from '../body/renderers/get_column_renderer'; +import { DragDropContextWrapper } from '../../../../common/components/drag_and_drop/drag_drop_context_wrapper'; +import { DroppableWrapper } from '../../../../common/components/drag_and_drop/droppable_wrapper'; +import { mockBrowserFields } from '../../../../common/containers/source/mock'; +import { defaultHeaders, mockTimelineData, TestProviders } from '../../../../common/mock'; +import { DefaultCellRenderer } from './default_cell_renderer'; + +jest.mock('../body/renderers/get_column_renderer'); +const getColumnRendererMock = getColumnRenderer as jest.Mock; +const mockImplementation = { + renderColumn: jest.fn(), +}; + +describe('DefaultCellRenderer', () => { + const columnId = 'signal.rule.risk_score'; + const eventId = '_id-123'; + const isDetails = true; + const isExpandable = true; + const isExpanded = true; + const linkValues = ['foo', 'bar', '@baz']; + const rowIndex = 3; + const setCellProps = jest.fn(); + const timelineId = 'test'; + + beforeEach(() => { + jest.clearAllMocks(); + getColumnRendererMock.mockImplementation(() => mockImplementation); + }); + + test('it invokes `getColumnRenderer` with the expected arguments', () => { + const data = cloneDeep(mockTimelineData[0].data); + const header = cloneDeep(defaultHeaders[0]); + + mount( + + + + + + + + ); + + expect(getColumnRenderer).toBeCalledWith(header.id, columnRenderers, data); + }); + + test('it invokes `renderColumn` with the expected arguments', () => { + const data = cloneDeep(mockTimelineData[0].data); + const header = cloneDeep(defaultHeaders[0]); + + mount( + + + + + + + + ); + + expect(mockImplementation.renderColumn).toBeCalledWith({ + columnName: header.id, + eventId, + field: header, + linkValues, + timelineId, + truncate: true, + values: ['2018-11-05T19:03:25.937Z'], + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/default_cell_renderer.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/default_cell_renderer.tsx new file mode 100644 index 0000000000000..8d8f821107e7b --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/default_cell_renderer.tsx @@ -0,0 +1,39 @@ +/* + * 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 { getMappedNonEcsValue } from '../body/data_driven_columns'; +import { columnRenderers } from '../body/renderers'; +import { getColumnRenderer } from '../body/renderers/get_column_renderer'; + +import { CellValueElementProps } from '.'; + +export const DefaultCellRenderer: React.FC = ({ + columnId, + data, + eventId, + header, + linkValues, + setCellProps, + timelineId, +}) => ( + <> + {getColumnRenderer(header.id, columnRenderers, data).renderColumn({ + columnName: header.id, + eventId, + field: header, + linkValues, + timelineId, + truncate: true, + values: getMappedNonEcsValue({ + data, + fieldName: header.id, + }), + })} + +); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/index.tsx new file mode 100644 index 0000000000000..03e444e3a9afd --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/index.tsx @@ -0,0 +1,20 @@ +/* + * 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 { EuiDataGridCellValueElementProps } from '@elastic/eui'; + +import { TimelineNonEcsData } from '../../../../../common/search_strategy/timeline'; +import { ColumnHeaderOptions } from '../../../store/timeline/model'; + +/** The following props are provided to the function called by `renderCellValue` */ +export type CellValueElementProps = EuiDataGridCellValueElementProps & { + data: TimelineNonEcsData[]; + eventId: string; // _id + header: ColumnHeaderOptions; + linkValues: string[] | undefined; + timelineId: string; +}; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/eql_tab_content/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/eql_tab_content/__snapshots__/index.test.tsx.snap index 2595f29144b80..7d237ecaf92df 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/eql_tab_content/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/eql_tab_content/__snapshots__/index.test.tsx.snap @@ -140,6 +140,986 @@ In other use cases the message field can be used to concatenate different values ] } onEventClosed={[MockFunction]} + renderCellValue={[Function]} + rowRenderers={ + Array [ + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_dns", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_security_event", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_security_event", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_fim", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_fim", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_fim", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_fim", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_fim", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_fim", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "alerts", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "alerts", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "alerts", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "alerts", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "alerts", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "alerts", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "alerts", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "alerts", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "alerts", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "alerts", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "library", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_fim", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "registry", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_socket", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_socket", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_socket", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_socket", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_socket", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_socket", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_socket", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_security_event", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_security_event", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_security_event", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_fim", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_fim", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_security_event", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_socket", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_socket", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "suricata", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "zeek", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "netflow", + "isInstance": [Function], + "renderRow": [Function], + }, + ] + } showExpandedDetails={false} start="2018-03-23T18:49:23.132Z" timelineId="test" 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 7b77a915f2f05..e13bed1e2eff6 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 @@ -9,6 +9,8 @@ import { shallow } from 'enzyme'; import React from 'react'; import useResizeObserver from 'use-resize-observer/polyfilled'; +import { defaultRowRenderers } from '../body/renderers'; +import { DefaultCellRenderer } from '../cell_rendering/default_cell_renderer'; import { defaultHeaders, mockTimelineData } from '../../../../common/mock'; import '../../../../common/mock/match_media'; import { TestProviders } from '../../../../common/mock/test_providers'; @@ -94,6 +96,8 @@ describe('Timeline', () => { itemsPerPage: 5, itemsPerPageOptions: [5, 10, 20], onEventClosed: jest.fn(), + renderCellValue: DefaultCellRenderer, + rowRenderers: defaultRowRenderers, showExpandedDetails: false, start: startDate, timerangeKind: 'absolute', diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/eql_tab_content/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/eql_tab_content/index.tsx index 51f8db4e796e5..6bb19ce5a6852 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/eql_tab_content/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/eql_tab_content/index.tsx @@ -22,10 +22,12 @@ import deepEqual from 'fast-deep-equal'; import { InPortal } from 'react-reverse-portal'; import { timelineActions, timelineSelectors } from '../../../store/timeline'; +import { CellValueElementProps } from '../cell_rendering'; import { TimelineItem } from '../../../../../common/search_strategy'; import { useTimelineEvents } from '../../../containers/index'; import { defaultHeaders } from '../body/column_headers/default_headers'; import { StatefulBody } from '../body'; +import { RowRenderer } from '../body/renderers/row_renderer'; import { Footer, footerHeight } from '../footer'; import { calculateTotalPages } from '../helpers'; import { TimelineRefetch } from '../refetch_timeline'; @@ -133,6 +135,8 @@ const isTimerangeSame = (prevProps: Props, nextProps: Props) => prevProps.timerangeKind === nextProps.timerangeKind; interface OwnProps { + renderCellValue: (props: CellValueElementProps) => React.ReactNode; + rowRenderers: RowRenderer[]; timelineId: string; } @@ -154,6 +158,8 @@ export const EqlTabContentComponent: React.FC = ({ itemsPerPage, itemsPerPageOptions, onEventClosed, + renderCellValue, + rowRenderers, showExpandedDetails, start, timerangeKind, @@ -284,6 +290,8 @@ export const EqlTabContentComponent: React.FC = ({ data={isBlankTimeline ? EMPTY_EVENTS : events} id={timelineId} refetch={refetch} + renderCellValue={renderCellValue} + rowRenderers={rowRenderers} sort={NO_SORTING} tabType={TimelineTabs.eql} totalPages={calculateTotalPages({ diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.test.tsx index ee2ce8cf8103b..db7a3cc3c9900 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.test.tsx @@ -17,7 +17,9 @@ import { mockIndexNames, mockIndexPattern, TestProviders } from '../../../common import { StatefulTimeline, Props as StatefulTimelineOwnProps } from './index'; import { useTimelineEvents } from '../../containers/index'; +import { DefaultCellRenderer } from './cell_rendering/default_cell_renderer'; import { SELECTOR_TIMELINE_GLOBAL_CONTAINER } from './styles'; +import { defaultRowRenderers } from './body/renderers'; jest.mock('../../containers/index', () => ({ useTimelineEvents: jest.fn(), @@ -63,6 +65,8 @@ jest.mock('../../../common/containers/sourcerer', () => { }); describe('StatefulTimeline', () => { const props: StatefulTimelineOwnProps = { + renderCellValue: DefaultCellRenderer, + rowRenderers: defaultRowRenderers, timelineId: TimelineId.test, }; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx index 6d2374dd8eef7..367357511c9c8 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx @@ -14,6 +14,8 @@ import styled from 'styled-components'; import { timelineActions, timelineSelectors } from '../../store/timeline'; import { timelineDefaults } from '../../../timelines/store/timeline/defaults'; import { defaultHeaders } from './body/column_headers/default_headers'; +import { RowRenderer } from './body/renderers/row_renderer'; +import { CellValueElementProps } from './cell_rendering'; import { isTab } from '../../../common/components/accessibility/helpers'; import { useSourcererScope } from '../../../common/containers/sourcerer'; import { SourcererScopeName } from '../../../common/store/sourcerer/model'; @@ -36,10 +38,12 @@ const TimelineTemplateBadge = styled.div` `; export interface Props { + renderCellValue: (props: CellValueElementProps) => React.ReactNode; + rowRenderers: RowRenderer[]; timelineId: TimelineId; } -const TimelineSavingProgressComponent: React.FC = ({ timelineId }) => { +const TimelineSavingProgressComponent: React.FC<{ timelineId: TimelineId }> = ({ timelineId }) => { const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); const isSaving = useShallowEqualSelector( (state) => (getTimeline(state, timelineId) ?? timelineDefaults).isSaving @@ -50,7 +54,11 @@ const TimelineSavingProgressComponent: React.FC = ({ timelineId }) => { const TimelineSavingProgress = React.memo(TimelineSavingProgressComponent); -const StatefulTimelineComponent: React.FC = ({ timelineId }) => { +const StatefulTimelineComponent: React.FC = ({ + renderCellValue, + rowRenderers, + timelineId, +}) => { const dispatch = useDispatch(); const containerElement = useRef(null); const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); @@ -131,6 +139,8 @@ const StatefulTimelineComponent: React.FC = ({ timelineId }) => { { timelineId: TimelineId.test, itemsPerPage: 5, itemsPerPageOptions: [5, 10, 20], + renderCellValue: DefaultCellRenderer, + rowRenderers: defaultRowRenderers, sort, pinnedEventIds: {}, showExpandedDetails: false, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/pinned_tab_content/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/pinned_tab_content/index.tsx index a19a61d8268ff..dfc14747dacf3 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/pinned_tab_content/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/pinned_tab_content/index.tsx @@ -14,10 +14,12 @@ import { connect, ConnectedProps } from 'react-redux'; import deepEqual from 'fast-deep-equal'; import { timelineActions, timelineSelectors } from '../../../store/timeline'; +import { CellValueElementProps } from '../cell_rendering'; import { Direction } from '../../../../../common/search_strategy'; import { useTimelineEvents } from '../../../containers/index'; import { defaultHeaders } from '../body/column_headers/default_headers'; import { StatefulBody } from '../body'; +import { RowRenderer } from '../body/renderers/row_renderer'; import { Footer, footerHeight } from '../footer'; import { requiredFieldsForActions } from '../../../../detections/components/alerts_table/default_config'; import { EventDetailsWidthProvider } from '../../../../common/components/events_viewer/event_details_width_context'; @@ -87,6 +89,8 @@ const VerticalRule = styled.div` VerticalRule.displayName = 'VerticalRule'; interface OwnProps { + renderCellValue: (props: CellValueElementProps) => React.ReactNode; + rowRenderers: RowRenderer[]; timelineId: string; } @@ -106,6 +110,8 @@ export const PinnedTabContentComponent: React.FC = ({ itemsPerPageOptions, pinnedEventIds, onEventClosed, + renderCellValue, + rowRenderers, showExpandedDetails, sort, }) => { @@ -217,6 +223,8 @@ export const PinnedTabContentComponent: React.FC = ({ data={events} id={timelineId} refetch={refetch} + renderCellValue={renderCellValue} + rowRenderers={rowRenderers} sort={sort} tabType={TimelineTabs.pinned} totalPages={calculateTotalPages({ diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/__snapshots__/index.test.tsx.snap index 0688a10b31eef..46c85f634ff6b 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/__snapshots__/index.test.tsx.snap @@ -276,6 +276,986 @@ In other use cases the message field can be used to concatenate different values kqlMode="search" kqlQueryExpression="" onEventClosed={[MockFunction]} + renderCellValue={[Function]} + rowRenderers={ + Array [ + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_dns", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_security_event", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_security_event", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_fim", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_fim", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_fim", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_fim", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_fim", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_fim", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "alerts", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "alerts", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "alerts", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "alerts", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "alerts", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "alerts", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "alerts", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "alerts", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "alerts", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "alerts", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "library", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_fim", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "registry", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_socket", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_socket", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_socket", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_socket", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_socket", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_socket", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_socket", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_security_event", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_security_event", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_security_event", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_fim", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_fim", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_security_event", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_socket", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_socket", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "suricata", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "zeek", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "netflow", + "isInstance": [Function], + "renderRow": [Function], + }, + ] + } show={true} showCallOutUnauthorizedMsg={false} showExpandedDetails={false} 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 c7d27da64c650..ede473acbfb2a 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 @@ -10,11 +10,13 @@ import React from 'react'; import useResizeObserver from 'use-resize-observer/polyfilled'; import { Direction } from '../../../../graphql/types'; +import { DefaultCellRenderer } from '../cell_rendering/default_cell_renderer'; import { defaultHeaders, mockTimelineData } from '../../../../common/mock'; import '../../../../common/mock/match_media'; import { TestProviders } from '../../../../common/mock/test_providers'; import { QueryTabContentComponent, Props as QueryTabContentComponentProps } from './index'; +import { defaultRowRenderers } from '../body/renderers'; import { Sort } from '../body/sort'; import { mockDataProviders } from '../data_providers/mock/mock_data_providers'; import { useMountAppended } from '../../../../common/utils/use_mount_appended'; @@ -106,6 +108,8 @@ describe('Timeline', () => { kqlMode: 'search' as QueryTabContentComponentProps['kqlMode'], kqlQueryExpression: '', onEventClosed: jest.fn(), + renderCellValue: DefaultCellRenderer, + rowRenderers: defaultRowRenderers, showCallOutUnauthorizedMsg: false, showExpandedDetails: false, sort, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.tsx index 28fec7ded9ca2..74a0f02354219 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.tsx @@ -22,6 +22,8 @@ import deepEqual from 'fast-deep-equal'; import { InPortal } from 'react-reverse-portal'; import { timelineActions, timelineSelectors } from '../../../store/timeline'; +import { RowRenderer } from '../body/renderers/row_renderer'; +import { CellValueElementProps } from '../cell_rendering'; import { Direction, TimelineItem } from '../../../../../common/search_strategy'; import { useTimelineEvents } from '../../../containers/index'; import { useKibana } from '../../../../common/lib/kibana'; @@ -142,6 +144,8 @@ const compareQueryProps = (prevProps: Props, nextProps: Props) => deepEqual(prevProps.filters, nextProps.filters); interface OwnProps { + renderCellValue: (props: CellValueElementProps) => React.ReactNode; + rowRenderers: RowRenderer[]; timelineId: string; } @@ -164,6 +168,8 @@ export const QueryTabContentComponent: React.FC = ({ kqlMode, kqlQueryExpression, onEventClosed, + renderCellValue, + rowRenderers, show, showCallOutUnauthorizedMsg, showExpandedDetails, @@ -330,6 +336,8 @@ export const QueryTabContentComponent: React.FC = ({ data={isBlankTimeline ? EMPTY_EVENTS : events} id={timelineId} refetch={refetch} + renderCellValue={renderCellValue} + rowRenderers={rowRenderers} sort={sort} tabType={TimelineTabs.query} totalPages={calculateTotalPages({ diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs_content/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs_content/index.tsx index f29211d519841..76a2ad0960322 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs_content/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs_content/index.tsx @@ -20,6 +20,8 @@ import { TimelineEventsCountBadge, } from '../../../../common/hooks/use_timeline_events_count'; import { timelineActions } from '../../../store/timeline'; +import { RowRenderer } from '../body/renderers/row_renderer'; +import { CellValueElementProps } from '../cell_rendering'; import { getActiveTabSelector, getNoteIdsSelector, @@ -46,6 +48,8 @@ const NotesTabContent = lazy(() => import('../notes_tab_content')); const PinnedTabContent = lazy(() => import('../pinned_tab_content')); interface BasicTimelineTab { + renderCellValue: (props: CellValueElementProps) => React.ReactNode; + rowRenderers: RowRenderer[]; setTimelineFullScreen?: (fullScreen: boolean) => void; timelineFullScreen?: boolean; timelineId: TimelineId; @@ -53,16 +57,32 @@ interface BasicTimelineTab { graphEventId?: string; } -const QueryTab: React.FC<{ timelineId: TimelineId }> = memo(({ timelineId }) => ( +const QueryTab: React.FC<{ + renderCellValue: (props: CellValueElementProps) => React.ReactNode; + rowRenderers: RowRenderer[]; + timelineId: TimelineId; +}> = memo(({ renderCellValue, rowRenderers, timelineId }) => ( }> - + )); QueryTab.displayName = 'QueryTab'; -const EqlTab: React.FC<{ timelineId: TimelineId }> = memo(({ timelineId }) => ( +const EqlTab: React.FC<{ + renderCellValue: (props: CellValueElementProps) => React.ReactNode; + rowRenderers: RowRenderer[]; + timelineId: TimelineId; +}> = memo(({ renderCellValue, rowRenderers, timelineId }) => ( }> - + )); EqlTab.displayName = 'EqlTab'; @@ -81,9 +101,17 @@ const NotesTab: React.FC<{ timelineId: TimelineId }> = memo(({ timelineId }) => )); NotesTab.displayName = 'NotesTab'; -const PinnedTab: React.FC<{ timelineId: TimelineId }> = memo(({ timelineId }) => ( +const PinnedTab: React.FC<{ + renderCellValue: (props: CellValueElementProps) => React.ReactNode; + rowRenderers: RowRenderer[]; + timelineId: TimelineId; +}> = memo(({ renderCellValue, rowRenderers, timelineId }) => ( }> - + )); PinnedTab.displayName = 'PinnedTab'; @@ -91,7 +119,7 @@ PinnedTab.displayName = 'PinnedTab'; type ActiveTimelineTabProps = BasicTimelineTab & { activeTimelineTab: TimelineTabs }; const ActiveTimelineTab = memo( - ({ activeTimelineTab, timelineId, timelineType }) => { + ({ activeTimelineTab, renderCellValue, rowRenderers, timelineId, timelineType }) => { const getTab = useCallback( (tab: TimelineTabs) => { switch (tab) { @@ -119,14 +147,26 @@ const ActiveTimelineTab = memo( return ( <> - + - + {timelineType === TimelineType.default && ( - + )} @@ -160,6 +200,8 @@ const StyledEuiTab = styled(EuiTab)` `; const TabsContentComponent: React.FC = ({ + renderCellValue, + rowRenderers, timelineId, timelineFullScreen, timelineType, @@ -300,6 +342,8 @@ const TabsContentComponent: React.FC = ({ diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.test.tsx b/x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.test.tsx index 3d92397f4ab50..0b70ba8991686 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.test.tsx @@ -30,11 +30,12 @@ import { updateItemsPerPage, updateSort, } from './actions'; - +import { DefaultCellRenderer } from '../../components/timeline/cell_rendering/default_cell_renderer'; import { QueryTabContentComponent, Props as QueryTabContentComponentProps, } from '../../components/timeline/query_tab_content'; +import { defaultRowRenderers } from '../../components/timeline/body/renderers'; import { mockDataProviders } from '../../components/timeline/data_providers/mock/mock_data_providers'; import { Sort } from '../../components/timeline/body/sort'; import { Direction } from '../../../graphql/types'; @@ -90,6 +91,8 @@ describe('epicLocalStorage', () => { kqlMode: 'search' as QueryTabContentComponentProps['kqlMode'], kqlQueryExpression: '', onEventClosed: jest.fn(), + renderCellValue: DefaultCellRenderer, + rowRenderers: defaultRowRenderers, showCallOutUnauthorizedMsg: false, showExpandedDetails: false, start: startDate, From 1fad3175f9526882f4eab02829d73b75194e6b4e Mon Sep 17 00:00:00 2001 From: Thomas Neirynck Date: Mon, 5 Apr 2021 13:42:46 -0400 Subject: [PATCH 29/30] [Maps] Safe-erase text-field (#94873) --- .../properties/dynamic_text_property.test.tsx | 109 ++++++++++++++++++ .../properties/dynamic_text_property.ts | 4 +- .../properties/static_text_property.test.ts | 70 +++++++++++ .../vector/properties/static_text_property.ts | 4 +- 4 files changed, 185 insertions(+), 2 deletions(-) create mode 100644 x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_text_property.test.tsx create mode 100644 x-pack/plugins/maps/public/classes/styles/vector/properties/static_text_property.test.ts diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_text_property.test.tsx b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_text_property.test.tsx new file mode 100644 index 0000000000000..4550a27ac2d9a --- /dev/null +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_text_property.test.tsx @@ -0,0 +1,109 @@ +/* + * 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. + */ + +jest.mock('../components/vector_style_editor', () => ({ + VectorStyleEditor: () => { + return
mockVectorStyleEditor
; + }, +})); + +import React from 'react'; + +// @ts-ignore +import { DynamicTextProperty } from './dynamic_text_property'; +import { RawValue, VECTOR_STYLES } from '../../../../../common/constants'; +import { IField } from '../../../fields/field'; +import { Map as MbMap } from 'mapbox-gl'; +import { mockField, MockLayer, MockStyle } from './test_helpers/test_util'; +import { IVectorLayer } from '../../../layers/vector_layer'; + +export class MockMbMap { + _paintPropertyCalls: unknown[]; + _lastTextFieldValue: unknown | undefined; + + constructor(lastTextFieldValue?: unknown) { + this._paintPropertyCalls = []; + this._lastTextFieldValue = lastTextFieldValue; + } + setLayoutProperty(layerId: string, propName: string, value: undefined | 'string') { + if (propName !== 'text-field') { + throw new Error('should only use to test `text-field`'); + } + this._lastTextFieldValue = value; + this._paintPropertyCalls.push([layerId, value]); + } + + getLayoutProperty(layername: string, propName: string): unknown | undefined { + if (propName !== 'text-field') { + throw new Error('should only use to test `text-field`'); + } + return this._lastTextFieldValue; + } + + getPaintPropertyCalls(): unknown[] { + return this._paintPropertyCalls; + } +} + +const makeProperty = (mockStyle: MockStyle, field: IField | null) => { + return new DynamicTextProperty( + {}, + VECTOR_STYLES.LABEL_TEXT, + field, + (new MockLayer(mockStyle) as unknown) as IVectorLayer, + () => { + return (value: RawValue) => value + '_format'; + } + ); +}; + +describe('syncTextFieldWithMb', () => { + describe('with field', () => { + test('Should set', async () => { + const dynamicTextProperty = makeProperty(new MockStyle({ min: 0, max: 100 }), mockField); + const mockMbMap = (new MockMbMap() as unknown) as MbMap; + + dynamicTextProperty.syncTextFieldWithMb('foobar', mockMbMap); + + // @ts-expect-error + expect(mockMbMap.getPaintPropertyCalls()).toEqual([ + ['foobar', ['coalesce', ['get', '__kbn__dynamic__foobar__labelText'], '']], + ]); + }); + }); + + describe('without field', () => { + test('Should clear', async () => { + const dynamicTextProperty = makeProperty(new MockStyle({ min: 0, max: 100 }), null); + const mockMbMap = (new MockMbMap([ + 'foobar', + ['coalesce', ['get', '__kbn__dynamic__foobar__labelText'], ''], + ]) as unknown) as MbMap; + + dynamicTextProperty.syncTextFieldWithMb('foobar', mockMbMap); + + // @ts-expect-error + expect(mockMbMap.getPaintPropertyCalls()).toEqual([['foobar', undefined]]); + }); + + test('Should not clear when already cleared', async () => { + // This verifies a weird edge-case in mapbox-gl, where setting the `text-field` layout-property to null causes tiles to be invalidated. + // This triggers a refetch of the tile during panning and zooming + // This affects vector-tile rendering in tiled_vector_layers with custom vector_styles + // It does _not_ affect EMS, since that does not have a code-path where a `text-field` need to be resynced. + // Do not remove this logic without verifying that mapbox-gl does not re-issue tile-requests for previously requested tiles + + const dynamicTextProperty = makeProperty(new MockStyle({ min: 0, max: 100 }), null); + const mockMbMap = (new MockMbMap(undefined) as unknown) as MbMap; + + dynamicTextProperty.syncTextFieldWithMb('foobar', mockMbMap); + + // @ts-expect-error + expect(mockMbMap.getPaintPropertyCalls()).toEqual([]); + }); + }); +}); diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_text_property.ts b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_text_property.ts index 22ea3067b1748..e8612388a5ae1 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_text_property.ts +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_text_property.ts @@ -20,7 +20,9 @@ export class DynamicTextProperty extends DynamicStyleProperty { + return new StaticTextProperty({ value }, VECTOR_STYLES.LABEL_TEXT); +}; + +describe('syncTextFieldWithMb', () => { + test('Should set with value', async () => { + const dynamicTextProperty = makeProperty('foo'); + const mockMbMap = (new MockMbMap() as unknown) as MbMap; + + dynamicTextProperty.syncTextFieldWithMb('foobar', mockMbMap); + + // @ts-expect-error + expect(mockMbMap.getPaintPropertyCalls()).toEqual([['foobar', 'foo']]); + }); + + test('Should not clear when already cleared', async () => { + // This verifies a weird edge-case in mapbox-gl, where setting the `text-field` layout-property to null causes tiles to be invalidated. + // This triggers a refetch of the tile during panning and zooming + // This affects vector-tile rendering in tiled_vector_layers with custom vector_styles + // It does _not_ affect EMS, since that does not have a code-path where a `text-field` need to be resynced. + // Do not remove this logic without verifying that mapbox-gl does not re-issue tile-requests for previously requested tiles + + const dynamicTextProperty = makeProperty(''); + const mockMbMap = (new MockMbMap(undefined) as unknown) as MbMap; + + dynamicTextProperty.syncTextFieldWithMb('foobar', mockMbMap); + + // @ts-expect-error + expect(mockMbMap.getPaintPropertyCalls()).toEqual([]); + }); +}); diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/static_text_property.ts b/x-pack/plugins/maps/public/classes/styles/vector/properties/static_text_property.ts index b0016106b8c31..fb05fa052db21 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/static_text_property.ts +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/static_text_property.ts @@ -18,7 +18,9 @@ export class StaticTextProperty extends StaticStyleProperty if (this.getOptions().value.length) { mbMap.setLayoutProperty(mbLayerId, 'text-field', this.getOptions().value); } else { - mbMap.setLayoutProperty(mbLayerId, 'text-field', null); + if (typeof mbMap.getLayoutProperty(mbLayerId, 'text-field') !== 'undefined') { + mbMap.setLayoutProperty(mbLayerId, 'text-field', undefined); + } } } } From ad5f83a36230abeff79d01bfff0a104f5fd615d2 Mon Sep 17 00:00:00 2001 From: Jason Stoltzfus Date: Mon, 5 Apr 2021 13:49:54 -0400 Subject: [PATCH 30/30] [App Search] Added Sample Response section to Result Settings (#95971) --- .../result_settings/result_settings.tsx | 4 +- .../non_text_fields_body.tsx | 5 + .../text_fields_body.tsx | 13 ++ .../result_settings/sample_response/index.ts | 8 + .../sample_response/sample_response.test.tsx | 75 ++++++ .../sample_response/sample_response.tsx | 72 ++++++ .../sample_response_logic.test.ts | 214 ++++++++++++++++++ .../sample_response/sample_response_logic.ts | 100 ++++++++ .../components/result_settings/types.ts | 4 + .../routes/app_search/result_settings.test.ts | 44 ++++ .../routes/app_search/result_settings.ts | 18 ++ 11 files changed, 556 insertions(+), 1 deletion(-) create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/sample_response/index.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/sample_response/sample_response.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/sample_response/sample_response.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/sample_response/sample_response_logic.test.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/sample_response/sample_response_logic.ts diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings.tsx index 38db5c60e98a9..7f4373835f8d5 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings.tsx @@ -17,6 +17,8 @@ import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chro import { RESULT_SETTINGS_TITLE } from './constants'; import { ResultSettingsTable } from './result_settings_table'; +import { SampleResponse } from './sample_response'; + import { ResultSettingsLogic } from '.'; interface Props { @@ -40,7 +42,7 @@ export const ResultSettings: React.FC = ({ engineBreadcrumb }) => { -
TODO
+
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/non_text_fields_body.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/non_text_fields_body.tsx index 145654be20461..dc91b5039a3c9 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/non_text_fields_body.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/non_text_fields_body.tsx @@ -10,6 +10,7 @@ import React, { useMemo } from 'react'; import { useValues, useActions } from 'kea'; import { EuiTableRow, EuiTableRowCell, EuiCheckbox, EuiTableRowCellCheckbox } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { ResultSettingsLogic } from '..'; import { FieldResultSetting } from '../types'; @@ -33,6 +34,10 @@ export const NonTextFieldsBody: React.FC = () => { { { { { + const actions = { + queryChanged: jest.fn(), + getSearchResults: jest.fn(), + }; + + const values = { + reducedServerResultFields: {}, + query: 'foo', + response: { + bar: 'baz', + }, + }; + + beforeEach(() => { + jest.clearAllMocks(); + setMockActions(actions); + setMockValues(values); + }); + + it('renders a text box with the current user "query" value from state', () => { + const wrapper = shallow(); + expect(wrapper.find(EuiFieldSearch).prop('value')).toEqual('foo'); + }); + + it('updates the "query" value in state when a user updates the text in the text box', () => { + const wrapper = shallow(); + wrapper.find(EuiFieldSearch).simulate('change', { target: { value: 'bar' } }); + expect(actions.queryChanged).toHaveBeenCalledWith('bar'); + }); + + it('will call getSearchResults with the current value of query and reducedServerResultFields in a useEffect, which updates the displayed response', () => { + const wrapper = shallow(); + expect(wrapper.find(EuiFieldSearch).prop('value')).toEqual('foo'); + }); + + it('renders the response from the given user "query" in a code block', () => { + const wrapper = shallow(); + expect(wrapper.find(EuiCodeBlock).prop('children')).toEqual('{\n "bar": "baz"\n}'); + }); + + it('renders a plain old string in the code block if the response is a string', () => { + setMockValues({ + response: 'No results.', + }); + const wrapper = shallow(); + expect(wrapper.find(EuiCodeBlock).prop('children')).toEqual('No results.'); + }); + + it('will not render a code block at all if there is no response yet', () => { + setMockValues({ + response: null, + }); + const wrapper = shallow(); + expect(wrapper.find(EuiCodeBlock).exists()).toEqual(false); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/sample_response/sample_response.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/sample_response/sample_response.tsx new file mode 100644 index 0000000000000..ae91b9648356c --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/sample_response/sample_response.tsx @@ -0,0 +1,72 @@ +/* + * 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, { useEffect } from 'react'; + +import { useActions, useValues } from 'kea'; + +import { + EuiCodeBlock, + EuiFieldSearch, + EuiFlexGroup, + EuiFlexItem, + EuiPanel, + EuiSpacer, + EuiTitle, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { ResultSettingsLogic } from '../result_settings_logic'; + +import { SampleResponseLogic } from './sample_response_logic'; + +export const SampleResponse: React.FC = () => { + const { reducedServerResultFields } = useValues(ResultSettingsLogic); + + const { query, response } = useValues(SampleResponseLogic); + const { queryChanged, getSearchResults } = useActions(SampleResponseLogic); + + useEffect(() => { + getSearchResults(query, reducedServerResultFields); + }, [query, reducedServerResultFields]); + + return ( + + + + +

+ {i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.resultSettings.sampleResponseTitle', + { defaultMessage: 'Sample response' } + )} +

+
+
+ + {/* TODO */} + +
+ + queryChanged(e.target.value)} + placeholder={i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.resultSettings.sampleResponse.inputPlaceholder', + { defaultMessage: 'Type a search query to test a response...' } + )} + data-test-subj="ResultSettingsQuerySampleResponse" + /> + + {!!response && ( + + {typeof response === 'string' ? response : JSON.stringify(response, null, 2)} + + )} +
+ ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/sample_response/sample_response_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/sample_response/sample_response_logic.test.ts new file mode 100644 index 0000000000000..79379306c1618 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/sample_response/sample_response_logic.test.ts @@ -0,0 +1,214 @@ +/* + * 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 { LogicMounter, mockHttpValues } from '../../../../__mocks__'; +import '../../../__mocks__/engine_logic.mock'; + +import { nextTick } from '@kbn/test/jest'; + +import { flashAPIErrors } from '../../../../shared/flash_messages'; + +import { SampleResponseLogic } from './sample_response_logic'; + +describe('SampleResponseLogic', () => { + const { mount } = new LogicMounter(SampleResponseLogic); + const { http } = mockHttpValues; + + const DEFAULT_VALUES = { + query: '', + response: null, + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('has expected default values', () => { + mount(); + expect(SampleResponseLogic.values).toEqual({ + ...DEFAULT_VALUES, + }); + }); + + describe('actions', () => { + describe('queryChanged', () => { + it('updates the query', () => { + mount({ + query: '', + }); + + SampleResponseLogic.actions.queryChanged('foo'); + + expect(SampleResponseLogic.values).toEqual({ + ...DEFAULT_VALUES, + query: 'foo', + }); + }); + }); + + describe('getSearchResultsSuccess', () => { + it('sets the response from a search API request', () => { + mount({ + response: null, + }); + + SampleResponseLogic.actions.getSearchResultsSuccess({}); + + expect(SampleResponseLogic.values).toEqual({ + ...DEFAULT_VALUES, + response: {}, + }); + }); + }); + + describe('getSearchResultsFailure', () => { + it('sets a string response from a search API request', () => { + mount({ + response: null, + }); + + SampleResponseLogic.actions.getSearchResultsFailure('An error occured.'); + + expect(SampleResponseLogic.values).toEqual({ + ...DEFAULT_VALUES, + response: 'An error occured.', + }); + }); + }); + }); + + describe('listeners', () => { + describe('getSearchResults', () => { + beforeAll(() => jest.useFakeTimers()); + afterAll(() => jest.useRealTimers()); + + it('makes a search API request and calls getSearchResultsSuccess with the first result of the response', async () => { + mount(); + jest.spyOn(SampleResponseLogic.actions, 'getSearchResultsSuccess'); + + http.post.mockReturnValue( + Promise.resolve({ + results: [ + { id: { raw: 'foo' }, _meta: {} }, + { id: { raw: 'bar' }, _meta: {} }, + { id: { raw: 'baz' }, _meta: {} }, + ], + }) + ); + + SampleResponseLogic.actions.getSearchResults('foo', { foo: { raw: true } }); + jest.runAllTimers(); + await nextTick(); + + expect(SampleResponseLogic.actions.getSearchResultsSuccess).toHaveBeenCalledWith({ + // Note that the _meta field was stripped from the result + id: { raw: 'foo' }, + }); + }); + + it('calls getSearchResultsSuccess with a "No Results." message if there are no results', async () => { + mount(); + jest.spyOn(SampleResponseLogic.actions, 'getSearchResultsSuccess'); + + http.post.mockReturnValue( + Promise.resolve({ + results: [], + }) + ); + + SampleResponseLogic.actions.getSearchResults('foo', { foo: { raw: true } }); + jest.runAllTimers(); + await nextTick(); + + expect(SampleResponseLogic.actions.getSearchResultsSuccess).toHaveBeenCalledWith( + 'No results.' + ); + }); + + it('handles 500 errors by setting a generic error response and showing a flash message error', async () => { + mount(); + jest.spyOn(SampleResponseLogic.actions, 'getSearchResultsFailure'); + + const error = { + response: { + status: 500, + }, + }; + + http.post.mockReturnValueOnce(Promise.reject(error)); + + SampleResponseLogic.actions.getSearchResults('foo', { foo: { raw: true } }); + jest.runAllTimers(); + await nextTick(); + + expect(flashAPIErrors).toHaveBeenCalledWith(error); + expect(SampleResponseLogic.actions.getSearchResultsFailure).toHaveBeenCalledWith( + 'An error occured.' + ); + }); + + it('handles 400 errors by setting the response, but does not show a flash error message', async () => { + mount(); + jest.spyOn(SampleResponseLogic.actions, 'getSearchResultsFailure'); + + http.post.mockReturnValueOnce( + Promise.reject({ + response: { + status: 400, + }, + body: { + attributes: { + errors: ['A validation error occurred.'], + }, + }, + }) + ); + + SampleResponseLogic.actions.getSearchResults('foo', { foo: { raw: true } }); + jest.runAllTimers(); + await nextTick(); + + expect(SampleResponseLogic.actions.getSearchResultsFailure).toHaveBeenCalledWith({ + errors: ['A validation error occurred.'], + }); + }); + + it('sets a generic message on a 400 error if no custom message is provided in the response', async () => { + mount(); + jest.spyOn(SampleResponseLogic.actions, 'getSearchResultsFailure'); + + http.post.mockReturnValueOnce( + Promise.reject({ + response: { + status: 400, + }, + }) + ); + + SampleResponseLogic.actions.getSearchResults('foo', { foo: { raw: true } }); + jest.runAllTimers(); + await nextTick(); + + expect(SampleResponseLogic.actions.getSearchResultsFailure).toHaveBeenCalledWith( + 'An error occured.' + ); + }); + + it('does nothing if an empty object is passed for the resultFields parameter', async () => { + mount(); + jest.spyOn(SampleResponseLogic.actions, 'getSearchResultsSuccess'); + + SampleResponseLogic.actions.getSearchResults('foo', {}); + + jest.runAllTimers(); + await nextTick(); + + expect(SampleResponseLogic.actions.getSearchResultsSuccess).not.toHaveBeenCalled(); + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/sample_response/sample_response_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/sample_response/sample_response_logic.ts new file mode 100644 index 0000000000000..808a7ec9c65dc --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/sample_response/sample_response_logic.ts @@ -0,0 +1,100 @@ +/* + * 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 { kea, MakeLogicType } from 'kea'; + +import { i18n } from '@kbn/i18n'; + +import { flashAPIErrors } from '../../../../shared/flash_messages'; + +import { HttpLogic } from '../../../../shared/http'; +import { EngineLogic } from '../../engine'; + +import { SampleSearchResponse, ServerFieldResultSettingObject } from '../types'; + +const NO_RESULTS_MESSAGE = i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.resultSettings.sampleResponse.noResultsMessage', + { defaultMessage: 'No results.' } +); + +const ERROR_MESSAGE = i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.resultSettings.sampleResponse.errorMessage', + { defaultMessage: 'An error occured.' } +); + +interface SampleResponseValues { + query: string; + response: SampleSearchResponse | string | null; +} + +interface SampleResponseActions { + queryChanged: (query: string) => { query: string }; + getSearchResultsSuccess: ( + response: SampleSearchResponse | string + ) => { response: SampleSearchResponse | string }; + getSearchResultsFailure: (response: string) => { response: string }; + getSearchResults: ( + query: string, + resultFields: ServerFieldResultSettingObject + ) => { query: string; resultFields: ServerFieldResultSettingObject }; +} + +export const SampleResponseLogic = kea>({ + path: ['enterprise_search', 'app_search', 'sample_response_logic'], + actions: { + queryChanged: (query) => ({ query }), + getSearchResultsSuccess: (response) => ({ response }), + getSearchResultsFailure: (response) => ({ response }), + getSearchResults: (query, resultFields) => ({ query, resultFields }), + }, + reducers: { + query: ['', { queryChanged: (_, { query }) => query }], + response: [ + null, + { + getSearchResultsSuccess: (_, { response }) => response, + getSearchResultsFailure: (_, { response }) => response, + }, + ], + }, + listeners: ({ actions }) => ({ + getSearchResults: async ({ query, resultFields }, breakpoint) => { + if (Object.keys(resultFields).length < 1) return; + await breakpoint(250); + + const { http } = HttpLogic.values; + const { engineName } = EngineLogic.values; + + const url = `/api/app_search/engines/${engineName}/sample_response_search`; + + try { + const response = await http.post(url, { + body: JSON.stringify({ + query, + result_fields: resultFields, + }), + }); + + const result = response.results?.[0]; + actions.getSearchResultsSuccess( + result ? { ...result, _meta: undefined } : NO_RESULTS_MESSAGE + ); + } catch (e) { + if (e.response.status >= 500) { + // 4XX Validation errors are expected, as a user could enter something like 2 as a size, which is out of valid range. + // In this case, we simply render the message from the server as the response. + // + // 5xx Server errors are unexpected, and need to be reported in a flash message. + flashAPIErrors(e); + actions.getSearchResultsFailure(ERROR_MESSAGE); + } else { + actions.getSearchResultsFailure(e.body?.attributes || ERROR_MESSAGE); + } + } + }, + }), +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/types.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/types.ts index 96bf277314a7b..18843112f46bf 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/types.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/types.ts @@ -5,6 +5,8 @@ * 2.0. */ +import { FieldValue } from '../result/types'; + export enum OpenModal { None, ConfirmResetModal, @@ -35,3 +37,5 @@ export interface FieldResultSetting { } export type FieldResultSettingObject = Record; + +export type SampleSearchResponse = Record; diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/result_settings.test.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/result_settings.test.ts index 8d1a7e3ead37b..e38380d60c6e9 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/result_settings.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/result_settings.test.ts @@ -88,4 +88,48 @@ describe('result settings routes', () => { }); }); }); + + describe('POST /api/app_search/engines/{name}/sample_response_search', () => { + const mockRouter = new MockRouter({ + method: 'post', + path: '/api/app_search/engines/{engineName}/sample_response_search', + }); + + beforeEach(() => { + registerResultSettingsRoutes({ + ...mockDependencies, + router: mockRouter.router, + }); + }); + + it('creates a request to enterprise search', () => { + mockRouter.callRoute({ + params: { engineName: 'some-engine' }, + body: { + query: 'test', + result_fields: resultFields, + }, + }); + + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/as/engines/:engineName/sample_response_search', + }); + }); + + describe('validates', () => { + it('correctly', () => { + const request = { + body: { + query: 'test', + result_fields: resultFields, + }, + }; + mockRouter.shouldValidate(request); + }); + it('missing required fields', () => { + const request = { body: {} }; + mockRouter.shouldThrow(request); + }); + }); + }); }); diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/result_settings.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/result_settings.ts index 38cb4aa922738..b091ae7a539c2 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/result_settings.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/result_settings.ts @@ -45,4 +45,22 @@ export function registerResultSettingsRoutes({ path: '/as/engines/:engineName/result_settings', }) ); + + router.post( + { + path: '/api/app_search/engines/{engineName}/sample_response_search', + validate: { + params: schema.object({ + engineName: schema.string(), + }), + body: schema.object({ + query: schema.string(), + result_fields: schema.recordOf(schema.string(), schema.object({}, { unknowns: 'allow' })), + }), + }, + }, + enterpriseSearchRequestHandler.createRequest({ + path: '/as/engines/:engineName/sample_response_search', + }) + ); }