diff --git a/common/constants/explorer.ts b/common/constants/explorer.ts index 504d06b9fe..d6fffd0196 100644 --- a/common/constants/explorer.ts +++ b/common/constants/explorer.ts @@ -324,3 +324,23 @@ export const TYPE_TAB_MAPPING = { [SAVED_QUERY]: TAB_EVENT_ID, [SAVED_VISUALIZATION]: TAB_CHART_ID, }; + +export const DEFAULT_EMPTY_EXPLORER_FIELDS = [ + { name: 'timestamp', type: 'timestamp' }, + { name: '_source', type: 'string' }, +]; + +export const DEFAULT_TIMESTAMP_COLUMN = { + id: 'timestamp', + isSortable: true, + display: 'Time', + schema: 'datetime', + initialWidth: 200, +}; + +export const DEFAULT_SOURCE_COLUMN = { + id: '_source', + isSortable: false, + display: 'Source', + schema: '_source', +}; diff --git a/common/types/explorer.ts b/common/types/explorer.ts index ceb95bb9e2..af94da8026 100644 --- a/common/types/explorer.ts +++ b/common/types/explorer.ts @@ -401,3 +401,8 @@ export type MOMENT_UNIT_OF_TIME = | 's' | 'milliseconds' | 'ms'; + +export interface GridSortingColumn { + id: string; + direction: 'asc' | 'desc'; +} diff --git a/public/components/event_analytics/explorer/__tests__/__snapshots__/data_grid.test.tsx.snap b/public/components/event_analytics/explorer/__tests__/__snapshots__/data_grid.test.tsx.snap index ef9d2d221a..a0e63ed20c 100644 --- a/public/components/event_analytics/explorer/__tests__/__snapshots__/data_grid.test.tsx.snap +++ b/public/components/event_analytics/explorer/__tests__/__snapshots__/data_grid.test.tsx.snap @@ -1,695 +1,436 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Datagrid component Renders data grid component 1`] = ` - + -
- - - - - - - - - - - - - - - - - - + - - - - - -
- - double_per_ip_bytes - - host - - ip_count - - per_ip_bytes - - resp_code - - sum_bytes -
- - - -
- - - -
-
-
- +
+
+ +
+ + + + + +
+ + `; diff --git a/public/components/event_analytics/explorer/__tests__/data_grid.test.tsx b/public/components/event_analytics/explorer/__tests__/data_grid.test.tsx index f659f32435..5a1729adf7 100644 --- a/public/components/event_analytics/explorer/__tests__/data_grid.test.tsx +++ b/public/components/event_analytics/explorer/__tests__/data_grid.test.tsx @@ -8,17 +8,27 @@ import Adapter from 'enzyme-adapter-react-16'; import React from 'react'; import { waitFor } from '@testing-library/react'; import { DataGrid } from '../events_views/data_grid'; -import { - SELECTED_FIELDS, +import { + SELECTED_FIELDS, AVAILABLE_FIELDS, UNSELECTED_FIELDS, - QUERIED_FIELDS + QUERIED_FIELDS, + DEFAULT_EMPTY_EXPLORER_FIELDS, } from '../../../../../common/constants/explorer'; -import { +import { AVAILABLE_FIELDS as SIDEBAR_AVAILABLE_FIELDS, QUERY_FIELDS, - DATA_GRID_ROWS + DATA_GRID_ROWS, + EXPLORER_DATA_GRID_QUERY, } from '../../../../../test/event_analytics_constants'; +import httpClientMock from '../../../../../test/__mocks__/httpClientMock'; +import { sampleEmptyPanel } from '../../../../../test/panels_constants'; +import { HttpResponse } from '../../../../../../../src/core/public'; +import PPLService from '../../../../../public/services/requests/ppl'; +import { applyMiddleware, createStore } from 'redux'; +import { rootReducer } from '../../../../../public/framework/redux/reducers'; +import thunk from 'redux-thunk'; +import { Provider } from 'react-redux'; describe('Datagrid component', () => { configure({ adapter: new Adapter() }); @@ -28,21 +38,42 @@ describe('Datagrid component', () => { [SELECTED_FIELDS]: [], [UNSELECTED_FIELDS]: [], [AVAILABLE_FIELDS]: SIDEBAR_AVAILABLE_FIELDS, - [QUERIED_FIELDS]: QUERY_FIELDS + [QUERIED_FIELDS]: QUERY_FIELDS, }; - + + httpClientMock.get = jest.fn(() => + Promise.resolve((sampleEmptyPanel as unknown) as HttpResponse) + ); + + const http = httpClientMock; + const pplService = new PPLService(httpClientMock); + const store = createStore(rootReducer, applyMiddleware(thunk)); + const wrapper = mount( - + + + ); - + wrapper.update(); await waitFor(() => { expect(wrapper).toMatchSnapshot(); }); }); -}); \ No newline at end of file +}); diff --git a/public/components/event_analytics/explorer/events_views/__tests__/__snapshots__/doc_viewer_row.test.tsx.snap b/public/components/event_analytics/explorer/events_views/__tests__/__snapshots__/doc_viewer_row.test.tsx.snap deleted file mode 100644 index 7449753dd1..0000000000 --- a/public/components/event_analytics/explorer/events_views/__tests__/__snapshots__/doc_viewer_row.test.tsx.snap +++ /dev/null @@ -1,78 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Datagrid Doc viewer row component Renders Doc viewer row component 1`] = ` - - - - - - - - - 45.957544288332315 - - - -`; diff --git a/public/components/event_analytics/explorer/events_views/__tests__/__snapshots__/flyout_button.test.tsx.snap b/public/components/event_analytics/explorer/events_views/__tests__/__snapshots__/flyout_button.test.tsx.snap new file mode 100644 index 0000000000..be38d974b6 --- /dev/null +++ b/public/components/event_analytics/explorer/events_views/__tests__/__snapshots__/flyout_button.test.tsx.snap @@ -0,0 +1,62 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Datagrid Doc viewer row component Renders Doc viewer row component 1`] = ` + + + + + +`; diff --git a/public/components/event_analytics/explorer/events_views/__tests__/doc_viewer_row.test.tsx b/public/components/event_analytics/explorer/events_views/__tests__/flyout_button.test.tsx similarity index 60% rename from public/components/event_analytics/explorer/events_views/__tests__/doc_viewer_row.test.tsx rename to public/components/event_analytics/explorer/events_views/__tests__/flyout_button.test.tsx index 684ede48c7..079a938420 100644 --- a/public/components/event_analytics/explorer/events_views/__tests__/doc_viewer_row.test.tsx +++ b/public/components/event_analytics/explorer/events_views/__tests__/flyout_button.test.tsx @@ -7,33 +7,29 @@ import { configure, mount } from 'enzyme'; import Adapter from 'enzyme-adapter-react-16'; import React from 'react'; import { waitFor } from '@testing-library/react'; -import { DocViewRow } from '../docViewRow'; +import { FlyoutButton } from '../docViewRow'; describe('Datagrid Doc viewer row component', () => { configure({ adapter: new Adapter() }); it('Renders Doc viewer row component', async () => { - const hit = { - 'Carrier': 'JetBeats', - 'avg(FlightDelayMin)': '45.957544288332315' + Carrier: 'JetBeats', + 'avg(FlightDelayMin)': '45.957544288332315', }; - const selectedCols = [{ - name: 'avg(FlightDelayMin)', - type: 'double' - }]; + const selectedCols = [ + { + name: 'avg(FlightDelayMin)', + type: 'double', + }, + ]; + + const wrapper = mount(); - const wrapper = mount( - - ); - wrapper.update(); await waitFor(() => { expect(wrapper).toMatchSnapshot(); }); }); -}); \ No newline at end of file +}); diff --git a/public/components/event_analytics/explorer/events_views/data_grid.scss b/public/components/event_analytics/explorer/events_views/data_grid.scss index f0dcbfbbba..bf5392d3b3 100644 --- a/public/components/event_analytics/explorer/events_views/data_grid.scss +++ b/public/components/event_analytics/explorer/events_views/data_grid.scss @@ -488,4 +488,8 @@ .osdDocViewer__warning { margin-right: $euiSizeS; } + + .euiDescriptionList.euiDescriptionList--inline .euiDescriptionList__title.osdDescriptionListFieldTitle { + background-color: tintOrShade($euiColorPrimary, 90%, 70%); + } } diff --git a/public/components/event_analytics/explorer/events_views/data_grid.tsx b/public/components/event_analytics/explorer/events_views/data_grid.tsx index fb15d0694c..8ac9c7aa92 100644 --- a/public/components/event_analytics/explorer/events_views/data_grid.tsx +++ b/public/components/event_analytics/explorer/events_views/data_grid.tsx @@ -3,148 +3,251 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { useMemo, useState, useEffect, useRef, RefObject } from 'react'; -import { IExplorerFields } from '../../../../../common/types/explorer'; -import { DEFAULT_COLUMNS, PAGE_SIZE } from '../../../../../common/constants/explorer'; -import { getHeaders, getTrs, populateDataGrid } from '../../utils'; +import React, { useMemo, useState, useRef, RefObject, Fragment, useCallback } from 'react'; +import { + EuiDataGrid, + EuiDescriptionList, + EuiDescriptionListDescription, + EuiDescriptionListTitle, + EuiDataGridColumn, + EuiDataGridSorting, +} from '@elastic/eui'; +import moment from 'moment'; +import dompurify from 'dompurify'; +import datemath from '@elastic/datemath'; +import { MutableRefObject } from 'react'; +import { GridSortingColumn, IExplorerFields, IField } from '../../../../../common/types/explorer'; +import { + DATE_DISPLAY_FORMAT, + DATE_PICKER_FORMAT, + DEFAULT_SOURCE_COLUMN, + DEFAULT_TIMESTAMP_COLUMN, +} from '../../../../../common/constants/explorer'; import { HttpSetup } from '../../../../../../../src/core/public'; import PPLService from '../../../../services/requests/ppl'; +import { FlyoutButton, IDocType } from './docViewRow'; +import { useFetchEvents } from '../../hooks'; +import { + PPL_INDEX_INSERT_POINT_REGEX, + PPL_NEWLINE_REGEX, +} from '../../../../../common/constants/shared'; +import { redoQuery } from '../../utils/utils'; interface DataGridProps { http: HttpSetup; pplService: PPLService; - rows: Array; - rowsAll: Array; + rows: any[]; + rowsAll: any[]; explorerFields: IExplorerFields; timeStampField: string; rawQuery: string; + totalHits: number; + requestParams: any; + startTime: string; + endTime: string; + storedSelectedColumns: IField[]; } export function DataGrid(props: DataGridProps) { - const { http, pplService, rows, rowsAll, explorerFields, timeStampField, rawQuery } = props; - const [limit, setLimit] = useState(PAGE_SIZE); - const loader = useRef(null); - const [rowRefs, setRowRefs] = useState[]>([]); + const { + http, + pplService, + rows, + rowsAll, + explorerFields, + timeStampField, + rawQuery, + totalHits, + requestParams, + startTime, + endTime, + storedSelectedColumns, + } = props; + const { getEvents } = useFetchEvents({ + pplService, + requestParams, + }); + // useRef instead of useState somehow solves the issue of user triggered sorting not + // having any delays + const sortingFields: MutableRefObject = useRef([]); + const pageFields = useRef([0, 100]); - useEffect(() => { - if (!loader.current) return; + // setSort and setPage are used to change the query and send a direct request to get data + const setSort = (sort: EuiDataGridSorting['columns']) => { + sortingFields.current = sort; + redoQuery(startTime, endTime, rawQuery, timeStampField, sortingFields, pageFields, getEvents); + }; - const observer = new IntersectionObserver( - (entries) => { - if (entries[0].isIntersecting) setLimit((limit) => limit + PAGE_SIZE); - }, - { - root: null, - rootMargin: '500px', - threshold: 0, - } - ); - observer.observe(loader.current); + const setPage = (page: number[]) => { + pageFields.current = page; + redoQuery(startTime, endTime, rawQuery, timeStampField, sortingFields, pageFields, getEvents); + }; - return () => observer.disconnect(); - }, [loader]); + // creates the header for each column listing what that column is + const dataGridColumns = useMemo(() => { + if (storedSelectedColumns.length > 0) { + const columns: EuiDataGridColumn[] = []; + storedSelectedColumns.map(({ name, type }) => { + if (name === 'timestamp') { + columns.push(DEFAULT_TIMESTAMP_COLUMN); + } else if (name === '_source') { + columns.push(DEFAULT_SOURCE_COLUMN); + } else { + columns.push({ + id: name, + display: name, + isSortable: true, // TODO: add functionality here based on type + }); + } + }); + return columns; + } + return []; + }, [storedSelectedColumns]); - const onFlyoutOpen = (docId: string) => { - rowRefs.forEach((rowRef) => { - rowRef.current?.closeAllFlyouts(docId); - }); - }; + // used for which columns are visible and their order + const dataGridColumnVisibility = useMemo(() => { + if (storedSelectedColumns.length > 0) { + const columns: string[] = []; + storedSelectedColumns.map(({ name }) => { + columns.push(name); + }); + return { + visibleColumns: columns, + setVisibleColumns: (visibleColumns: string[]) => { + // TODO: implement with sidebar field order (dragability) changes + }, + }; + } + // default shown fields + throw new Error('explorer data grid stored columns empty'); + }, [storedSelectedColumns]); + + // sets the very first column, which is the button used for the flyout of each row + const dataGridLeadingColumns = useMemo(() => { + return [ + { + id: 'inspectCollapseColumn', + headerCellRender: () => null, + rowCellRender: ({ rowIndex }: { rowIndex: number }) => { + return ( + {}} + /> + ); + }, + width: 40, + }, + ]; + }, [rows, http, explorerFields, pplService, rawQuery, timeStampField]); + + // renders what is shown in each cell, i.e. the content of each row + const dataGridCellRender = useCallback( + ({ rowIndex, columnId }: { rowIndex: number; columnId: string }) => { + const trueIndex = rowIndex % pageFields.current[1]; + if (trueIndex < rows.length) { + if (columnId === '_source') { + return ( + + {Object.keys(rows[trueIndex]).map((key) => ( + + + {key} + + + {rows[trueIndex][key]} + + + ))} + + ); + } + if (columnId === 'timestamp') { + return `${moment(rows[trueIndex][columnId]).format(DATE_DISPLAY_FORMAT)}`; + } + return `${rows[trueIndex][columnId]}`; + } + return null; + }, + [rows, pageFields, explorerFields] + ); - const Queriedheaders = useMemo(() => getHeaders(explorerFields.queriedFields, DEFAULT_COLUMNS), [ - explorerFields.queriedFields, - ]); - const [QueriedtableRows, setQueriedtableRows] = useState([]); - useEffect(() => { - setQueriedtableRows( - getTrs( - http, - explorerFields.queriedFields, - limit, - setLimit, - PAGE_SIZE, - timeStampField, - explorerFields, - pplService, - rawQuery, - rowRefs, - setRowRefs, - onFlyoutOpen, - rows - ) - ); - }, [rows, explorerFields.queriedFields]); + // ** Pagination config + const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 100 }); + // changing the number of items per page, reset index and modify page size + const onChangeItemsPerPage = useCallback( + (pageSize) => + setPagination(() => { + setPage([0, pageSize]); + return { pageIndex: 0, pageSize }; + }), + [setPagination, setPage] + ); + // changing the page index, keep page size constant + const onChangePage = useCallback( + (pageIndex) => { + setPagination(({ pageSize }) => { + setPage([pageIndex, pageSize]); + return { pageSize, pageIndex }; + }); + }, + [setPagination, setPage] + ); - const headers = useMemo(() => getHeaders(explorerFields.selectedFields, DEFAULT_COLUMNS), [ - explorerFields.selectedFields, - ]); - const [tableRows, setTableRows] = useState([]); - useEffect(() => { - const dataToRender = - explorerFields?.queriedFields && explorerFields.queriedFields.length > 0 ? rowsAll : rows; - setTableRows( - getTrs( - http, - explorerFields.selectedFields, - limit, - setLimit, - PAGE_SIZE, - timeStampField, - explorerFields, - pplService, - rawQuery, - rowRefs, - setRowRefs, - onFlyoutOpen, - dataToRender - ) - ); - }, [rows, explorerFields.selectedFields]); + const rowHeightsOptions = useMemo( + () => ({ + defaultHeight: { + // if source is listed as a column, add extra space + lineCount: storedSelectedColumns.some((obj) => obj.name === '_source') ? 3 : 1, + }, + }), + [storedSelectedColumns] + ); - useEffect(() => { - setQueriedtableRows((prev) => - getTrs( - http, - explorerFields.queriedFields, - limit, - setLimit, - PAGE_SIZE, - timeStampField, - explorerFields, - pplService, - rawQuery, - rowRefs, - setRowRefs, - onFlyoutOpen, - rows, - prev - ) - ); - const dataToRender = - explorerFields?.queriedFields && explorerFields.queriedFields.length > 0 ? rowsAll : rows; - setTableRows((prev) => - getTrs( - http, - explorerFields.selectedFields, - limit, - setLimit, - PAGE_SIZE, - timeStampField, - explorerFields, - pplService, - rawQuery, - rowRefs, - setRowRefs, - onFlyoutOpen, - dataToRender, - prev - ) - ); - }, [limit]); + // TODO: memoize the expensive table below return ( <> - {populateDataGrid(explorerFields, Queriedheaders, QueriedtableRows, headers, tableRows)} -
+
+ +
); } diff --git a/public/components/event_analytics/explorer/events_views/docViewRow.tsx b/public/components/event_analytics/explorer/events_views/docViewRow.tsx index 41182fb4bb..bd0a69e887 100644 --- a/public/components/event_analytics/explorer/events_views/docViewRow.tsx +++ b/public/components/event_analytics/explorer/events_views/docViewRow.tsx @@ -24,7 +24,7 @@ export interface IDocType { [key: string]: string; } -interface IDocViewRowProps { +interface FlyoutButtonProps { http: HttpStart; doc: IDocType; docId: string; @@ -36,7 +36,7 @@ interface IDocViewRowProps { onFlyoutOpen: (docId: string) => void; } -export const DocViewRow = forwardRef((props: IDocViewRowProps, ref) => { +export const FlyoutButton = forwardRef((props: FlyoutButtonProps, ref) => { const { http, doc, @@ -270,15 +270,11 @@ export const DocViewRow = forwardRef((props: IDocViewRowProps, ref) => { return ( <> - - {memorizedTds} - + toggleDetailOpen()} + iconType={'inspect'} + aria-label="inspect document details" + /> {flyout} ); diff --git a/public/components/event_analytics/explorer/events_views/doc_flyout.tsx b/public/components/event_analytics/explorer/events_views/doc_flyout.tsx index e7183cc4a0..dc1629b713 100644 --- a/public/components/event_analytics/explorer/events_views/doc_flyout.tsx +++ b/public/components/event_analytics/explorer/events_views/doc_flyout.tsx @@ -124,13 +124,6 @@ export const DocFlyout = ({ const flyoutBody = (
- {populateDataGrid( - explorerFields, - getHeaders(explorerFields.queriedFields, DEFAULT_COLUMNS.slice(1), true), - {memorizedTds}, - getHeaders(explorerFields.selectedFields, DEFAULT_COLUMNS.slice(1), true), - {memorizedTds} - )}
diff --git a/public/components/event_analytics/explorer/explorer.tsx b/public/components/event_analytics/explorer/explorer.tsx index 35f7091dbd..ca923f0c0e 100644 --- a/public/components/event_analytics/explorer/explorer.tsx +++ b/public/components/event_analytics/explorer/explorer.tsx @@ -56,6 +56,7 @@ import { TAB_EVENT_ID, TAB_EVENT_TITLE, TIME_INTERVAL_OPTIONS, + DEFAULT_EMPTY_EXPLORER_FIELDS, } from '../../../../common/constants/explorer'; import { LIVE_END_TIME, @@ -462,6 +463,8 @@ export const Explorer = ({ const dateRange = getDateRange(startTime, endTime, query); + const [storedExplorerFields, setStoredExplorerFields] = useState(explorerFields); + const mainContent = useMemo(() => { return ( <> @@ -486,6 +489,12 @@ export const Explorer = ({ isEmpty(explorerData.jsonData) || !isEmpty(queryRef.current![RAW_QUERY].match(PPL_STATS_REGEX)) } + storedExplorerFields={ + storedExplorerFields.availableFields.length > 0 + ? storedExplorerFields + : explorerFields + } + setStoredExplorerFields={setStoredExplorerFields} />
)} @@ -602,6 +611,15 @@ export const Explorer = ({ explorerFields={explorerFields} timeStampField={queryRef.current![SELECTED_TIMESTAMP]} rawQuery={appBasedRef.current || queryRef.current![RAW_QUERY]} + totalHits={_.sum(countDistribution.data['count()'])} + requestParams={requestParams} + startTime={appLogEvents ? startTime : dateRange[0]} + endTime={appLogEvents ? endTime : dateRange[1]} + storedSelectedColumns={ + storedExplorerFields.selectedFields.length > 0 + ? storedExplorerFields.selectedFields + : DEFAULT_EMPTY_EXPLORER_FIELDS + } /> ​ diff --git a/public/components/event_analytics/explorer/sidebar/__tests__/__snapshots__/sidebar.test.tsx.snap b/public/components/event_analytics/explorer/sidebar/__tests__/__snapshots__/sidebar.test.tsx.snap index 595327e584..7ffd701e0c 100644 --- a/public/components/event_analytics/explorer/sidebar/__tests__/__snapshots__/sidebar.test.tsx.snap +++ b/public/components/event_analytics/explorer/sidebar/__tests__/__snapshots__/sidebar.test.tsx.snap @@ -490,6 +490,120 @@ exports[`Siderbar component Renders sidebar component 1`] = ` isFieldToggleButtonDisabled={false} isOverridingTimestamp={false} selectedTimestamp="timestamp" + storedExplorerFields={ + Object { + "availableFields": Array [ + Object { + "name": "agent", + "type": "string", + }, + Object { + "name": "bytes", + "type": "long", + }, + Object { + "name": "clientip", + "type": "ip", + }, + Object { + "name": "event", + "type": "struct", + }, + Object { + "name": "extension", + "type": "string", + }, + Object { + "name": "geo", + "type": "struct", + }, + Object { + "name": "host", + "type": "string", + }, + Object { + "name": "index", + "type": "string", + }, + Object { + "name": "ip", + "type": "ip", + }, + Object { + "name": "machine", + "type": "struct", + }, + Object { + "name": "memory", + "type": "double", + }, + Object { + "name": "message", + "type": "string", + }, + Object { + "name": "phpmemory", + "type": "long", + }, + Object { + "name": "referer", + "type": "string", + }, + Object { + "name": "request", + "type": "string", + }, + Object { + "name": "response", + "type": "string", + }, + Object { + "name": "tags", + "type": "string", + }, + Object { + "name": "timestamp", + "type": "timestamp", + }, + Object { + "name": "url", + "type": "string", + }, + Object { + "name": "utc_time", + "type": "timestamp", + }, + ], + "queriedFields": Array [ + Object { + "name": "double_per_ip_bytes", + "type": "long", + }, + Object { + "name": "host", + "type": "text", + }, + Object { + "name": "ip_count", + "type": "integer", + }, + Object { + "name": "per_ip_bytes", + "type": "long", + }, + Object { + "name": "resp_code", + "type": "text", + }, + Object { + "name": "sum_bytes", + "type": "long", + }, + ], + "selectedFields": Array [], + "unselectedFields": Array [], + } + } > { handleOverrideTimestamp={handleOverrideTimestamp} isFieldToggleButtonDisabled={false} isOverridingTimestamp={false} + storedExplorerFields={explorerFields} /> ); diff --git a/public/components/event_analytics/explorer/sidebar/sidebar.tsx b/public/components/event_analytics/explorer/sidebar/sidebar.tsx index e136cc19c2..89c8c7c52a 100644 --- a/public/components/event_analytics/explorer/sidebar/sidebar.tsx +++ b/public/components/event_analytics/explorer/sidebar/sidebar.tsx @@ -25,6 +25,8 @@ interface ISidebarProps { isFieldToggleButtonDisabled: boolean; handleOverridePattern: (pattern: IField) => void; handleOverrideTimestamp: (timestamp: IField) => void; + storedExplorerFields: IExplorerFields; + setStoredExplorerFields: (explorer: IExplorerFields) => void; } export const Sidebar = (props: ISidebarProps) => { @@ -39,6 +41,8 @@ export const Sidebar = (props: ISidebarProps) => { isFieldToggleButtonDisabled, handleOverridePattern, handleOverrideTimestamp, + storedExplorerFields, + setStoredExplorerFields, } = props; const dispatch = useDispatch(); @@ -87,24 +91,40 @@ export const Sidebar = (props: ISidebarProps) => { }); }; + const checkWithStoredFields = () => { + if ( + explorerFields.selectedFields.length === 0 && + storedExplorerFields.selectedFields.length !== 0 + ) { + return storedExplorerFields; + } + return explorerFields; + }; + const handleAddField = useCallback( (field: IField) => { - updateStoreFields( - toggleFields(explorerFields, field, AVAILABLE_FIELDS, SELECTED_FIELDS), - tabId, + const nextFields = toggleFields( + checkWithStoredFields(), + field, + AVAILABLE_FIELDS, SELECTED_FIELDS ); + updateStoreFields(nextFields, tabId, SELECTED_FIELDS); + setStoredExplorerFields(nextFields); }, [explorerFields, tabId] ); const handleRemoveField = useCallback( (field: IField) => { - updateStoreFields( - toggleFields(explorerFields, field, SELECTED_FIELDS, AVAILABLE_FIELDS), - tabId, + const nextFields = toggleFields( + checkWithStoredFields(), + field, + SELECTED_FIELDS, AVAILABLE_FIELDS ); + updateStoreFields(nextFields, tabId, AVAILABLE_FIELDS); + setStoredExplorerFields(nextFields); }, [explorerFields, tabId] ); @@ -140,7 +160,7 @@ export const Sidebar = (props: ISidebarProps) => { } paddingSize="xs" > - +
    { } paddingSize="xs" > - +
      { > {explorerData && !isEmpty(explorerData.jsonData) && - explorerFields.selectedFields && - explorerFields.selectedFields.map((field) => { + storedExplorerFields.selectedFields && + storedExplorerFields.selectedFields.map((field) => { return (
    • { } paddingSize="xs" > - +
        { aria-labelledby="available_fields" data-test-subj={`fieldList-unpopular`} > - {explorerFields.availableFields && - explorerFields.availableFields + {storedExplorerFields.availableFields && + storedExplorerFields.availableFields .filter((field) => searchTerm === '' || field.name.indexOf(searchTerm) !== -1) .map((field) => { return ( diff --git a/public/components/event_analytics/utils/__tests__/utils.test.tsx b/public/components/event_analytics/utils/__tests__/utils.test.tsx index 3a595f8523..f6b5138d2f 100644 --- a/public/components/event_analytics/utils/__tests__/utils.test.tsx +++ b/public/components/event_analytics/utils/__tests__/utils.test.tsx @@ -14,7 +14,9 @@ import { rangeNumDocs, getHeaders, fillTimeDataWithEmpty, + redoQuery, } from '../utils'; +import { EXPLORER_DATA_GRID_QUERY } from '../../../../../test/event_analytics_constants'; describe('Utils event analytics helper functions', () => { configure({ adapter: new Adapter() }); @@ -50,38 +52,6 @@ describe('Utils event analytics helper functions', () => { expect(rangeNumDocs(2000)).toBe(2000); }); - it('validates getHeaders function', () => { - expect( - getHeaders( - [ - { - name: 'host', - type: 'text', - }, - { - name: 'ip_count', - type: 'integer', - }, - { - name: 'per_ip_bytes', - type: 'long', - }, - { - name: 'resp_code', - type: 'text', - }, - { - name: 'sum_bytes', - type: 'long', - }, - ], - ['', 'Time', '_source'], - undefined - ) - ).toBeTruthy(); - expect(getHeaders([], ['', 'Time', '_source'], undefined)).toBeTruthy(); - }); - it('validates fillTimeDataWithEmpty function', () => { expect( fillTimeDataWithEmpty( @@ -146,4 +116,29 @@ describe('Utils event analytics helper functions', () => { values: [0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 5, 4, 2, 3, 3, 1, 0, 0], }); }); + + it('validates redoQuery function', () => { + const getEvents = jest.fn(); + redoQuery( + '2023-01-01 00:00:00', + '2023-09-28 23:19:10', + "source = opensearch_dashboards_sample_data_logs | where match(request,'filebeat')", + 'timestamp', + { + current: [ + { + id: 'timestamp', + direction: 'asc', + }, + ], + }, + { + current: [0, 100], + }, + getEvents + ); + const expectedFinalQuery = + "source=opensearch_dashboards_sample_data_logs | where timestamp >= '2023-01-01 00:00:00' and timestamp <= '2023-09-28 23:19:10' | where match(request,'filebeat') | sort + timestamp | head 100 from 0"; + expect(getEvents).toBeCalledWith(expectedFinalQuery); + }); }); diff --git a/public/components/event_analytics/utils/utils.tsx b/public/components/event_analytics/utils/utils.tsx index fe7d47aa43..3266b51f26 100644 --- a/public/components/event_analytics/utils/utils.tsx +++ b/public/components/event_analytics/utils/utils.tsx @@ -7,8 +7,8 @@ import { uniqueId, isEmpty } from 'lodash'; import moment from 'moment'; -import React from 'react'; -import { EuiText } from '@elastic/eui'; +import React, { MutableRefObject } from 'react'; +import { EuiDataGridSorting, EuiText } from '@elastic/eui'; import datemath from '@elastic/datemath'; import { HttpStart } from '../../../../../../src/core/public'; import { @@ -19,7 +19,12 @@ import { BREAKDOWNS, DATE_PICKER_FORMAT, } from '../../../../common/constants/explorer'; -import { PPL_DATE_FORMAT, PPL_INDEX_REGEX } from '../../../../common/constants/shared'; +import { + PPL_DATE_FORMAT, + PPL_INDEX_INSERT_POINT_REGEX, + PPL_INDEX_REGEX, + PPL_NEWLINE_REGEX, +} from '../../../../common/constants/shared'; import { ConfigListEntry, GetTooltipHoverInfoType, @@ -38,123 +43,6 @@ import { statsChunk, } from '../../../../common/query_manager/ast/types'; -// Create Individual table rows for events datagrid and flyouts -export const getTrs = ( - http: HttpStart, - explorerFields: IField[], - limit: number, - setLimit: React.Dispatch>, - pageSize: number, - timeStampField: any, - explorerFieldsFull: IExplorerFields, - pplService: PPLService, - rawQuery: string, - rowRefs: Array< - React.RefObject<{ - closeAllFlyouts(openDocId: string): void; - }> - >, - setRowRefs: React.Dispatch< - React.SetStateAction< - Array< - React.RefObject<{ - closeAllFlyouts(openDocId: string): void; - }> - > - > - >, - onFlyoutOpen: (docId: string) => void, - docs: any[] = [], - prevTrs: any[] = [] -) => { - if (prevTrs.length >= docs.length) return prevTrs; - - // reset limit if no previous table rows - if (prevTrs.length === 0 && limit !== pageSize) setLimit(pageSize); - const trs = prevTrs.slice(); - - const upperLimit = Math.min(trs.length === 0 ? pageSize : limit, docs.length); - const tempRefs = rowRefs; - for (let i = trs.length; i < upperLimit; i++) { - const docId = uniqueId('doc_view'); - const tempRowRef = React.createRef<{ - closeAllFlyouts(openDocId: string): void; - }>(); - tempRefs.push(tempRowRef); - trs.push( - - ); - } - setRowRefs(tempRefs); - return trs; -}; - -// Create table headers for events datagrid and flyouts -export const getHeaders = (fields: any, defaultCols: string[], isFlyout?: boolean) => { - let tableHeadContent = null; - if (!fields || fields.length === 0) { - tableHeadContent = ( - <> - {defaultCols.map((colName: string) => { - return {colName}; - })} - - ); - } else { - tableHeadContent = fields.map((selField: any) => { - return {selField.name}; - }); - - if (!isFlyout) { - tableHeadContent.unshift(); - } - } - - return {tableHeadContent}; -}; - -// Populate Events datagrid and flyouts -export const populateDataGrid = ( - explorerFields: IExplorerFields, - header1: JSX.Element, - body1: JSX.Element, - header2: JSX.Element, - body2: JSX.Element -) => { - return ( - <> -
        - {explorerFields?.queriedFields && explorerFields.queriedFields.length > 0 && ( - - {header1} - {body1} -
        - )} - {explorerFields?.queriedFields && - explorerFields?.queriedFields?.length > 0 && - explorerFields.selectedFields?.length === 0 ? null : ( - - {header2} - {body2} -
        - )} -
        - - ); -}; - /* Builds Final Query for the surrounding events * -> Final Query is as follows: * -> finalQuery = indexPartOfQuery + timeQueryFilter + filterPartOfQuery + sortFilter @@ -395,13 +283,15 @@ export const getDefaultVisConfig = (statsToken: statsChunk) => { // const seriesToken = statsToken.aggregations && statsToken.aggregations[0]; const span = getSpanValue(groupByToken); return { - [AGGREGATIONS]: statsToken.aggregations.map((agg) => ({ - label: agg.function?.value_expression, - name: agg.function?.value_expression, - aggregation: agg.function?.name, - [CUSTOM_LABEL]: agg[CUSTOM_LABEL as keyof StatsAggregationChunk], - })), - [GROUPBY]: groupByToken?.group_fields?.map((agg) => ({ + [AGGREGATIONS]: statsToken.aggregations.map( + (agg: { [x: string]: any; function: { value_expression: any; name: any } }) => ({ + label: agg.function?.value_expression, + name: agg.function?.value_expression, + aggregation: agg.function?.name, + [CUSTOM_LABEL]: agg[CUSTOM_LABEL as keyof StatsAggregationChunk], + }) + ), + [GROUPBY]: groupByToken?.group_fields?.map((agg: { [x: string]: any; name: any }) => ({ label: agg.name ?? '', name: agg.name ?? '', [CUSTOM_LABEL]: agg[CUSTOM_LABEL as keyof GroupField] ?? '', @@ -515,3 +405,36 @@ export const fillTimeDataWithEmpty = ( return { buckets, values }; }; + +export const redoQuery = ( + startTime: string, + endTime: string, + rawQuery: string, + timeStampField: string, + sortingFields: MutableRefObject, + pageFields: MutableRefObject, + getEvents: any +) => { + let finalQuery = ''; + + const start = datemath.parse(startTime)?.utc().format(DATE_PICKER_FORMAT); + const end = datemath.parse(endTime, { roundUp: true })?.utc().format(DATE_PICKER_FORMAT); + const tokens = rawQuery.replaceAll(PPL_NEWLINE_REGEX, '').match(PPL_INDEX_INSERT_POINT_REGEX); + + finalQuery = `${tokens![1]}=${ + tokens![2] + } | where ${timeStampField} >= '${start}' and ${timeStampField} <= '${end}'`; + + finalQuery += tokens![3]; + + for (let i = 0; i < sortingFields.current.length; i++) { + const field = sortingFields.current[i]; + const dir = field.direction === 'asc' ? '+' : '-'; + finalQuery = finalQuery + ` | sort ${dir} ${field.id}`; + } + + finalQuery = + finalQuery + + ` | head ${pageFields.current[1]} from ${pageFields.current[0] * pageFields.current[1]}`; + getEvents(finalQuery); +}; diff --git a/test/event_analytics_constants.ts b/test/event_analytics_constants.ts index 8dafbf7f66..0adbd5b65c 100644 --- a/test/event_analytics_constants.ts +++ b/test/event_analytics_constants.ts @@ -571,3 +571,6 @@ export const HORIZONTAL_BAR_TEST_VISUALIZATIONS_DATA = { export const EXPLORER_START_TIME = 'Aug 28, 2023 @ 20:00:00.406'; export const EXPLORER_END_TIME = 'Aug 28, 2023 @ 20:00:00.408'; + +export const EXPLORER_DATA_GRID_QUERY = + "source = opensearch_dashboards_sample_data_logs | where match(request,'filebeat')";