From b77cec28e1fc7a63429dd9726a1adfcf00642d56 Mon Sep 17 00:00:00 2001 From: "opensearch-trigger-bot[bot]" <98922864+opensearch-trigger-bot[bot]@users.noreply.github.com> Date: Tue, 12 Mar 2024 18:48:49 -0700 Subject: [PATCH] Changed Explorer Data Grid useage of timestamp (#1479) (#1520) * add support to use timestamp from set default * update data grid test and snap * direct query now sets default timestamp using same logic * small linting complaints * appeasing lint pt2 * implement i18n for column names * using timestamp prop * add test with different user timestamp * remove console log * direct search to use i18n translate --------- (cherry picked from commit d17cad3fbf1b6102b6354ddcfc9a17bbba4e282d) Signed-off-by: Paul Sebastian Signed-off-by: github-actions[bot] Co-authored-by: github-actions[bot] (cherry picked from commit 3fc3b2537803f5833eaabc7fe876f7cc18967674) --- auto_sync_commit_metadata.json | 4 +- common/constants/explorer.ts | 2 +- common/constants/shared.ts | 2 + .../common/search/direct_search.tsx | 58 +- .../__snapshots__/data_grid.test.tsx.snap | 545 +++++++++++++++++- .../explorer/__tests__/data_grid.test.tsx | 86 ++- .../explorer/events_views/data_grid.tsx | 31 +- 7 files changed, 690 insertions(+), 38 deletions(-) diff --git a/auto_sync_commit_metadata.json b/auto_sync_commit_metadata.json index 06749cddf1..461e26cf15 100644 --- a/auto_sync_commit_metadata.json +++ b/auto_sync_commit_metadata.json @@ -1,4 +1,4 @@ { - "last_github_commit": "04e824dbe1775f94e51ed9424cfe6da4fd564a99", - "last_gitfarm_commit": "fdabb9a2046a73d918f8acfc003779d0cf809f0f" + "last_github_commit": "3fc3b2537803f5833eaabc7fe876f7cc18967674", + "last_gitfarm_commit": "6faa35fb7832356d64e46d5fcb89c47d306e7981" } \ No newline at end of file diff --git a/common/constants/explorer.ts b/common/constants/explorer.ts index d94957c3e8..fc0102d119 100644 --- a/common/constants/explorer.ts +++ b/common/constants/explorer.ts @@ -328,7 +328,7 @@ export const TYPE_TAB_MAPPING = { }; export const DEFAULT_EMPTY_EXPLORER_FIELDS = [ - { name: 'timestamp', type: 'timestamp' }, + // timestamp field will be a default but is added after finding what it is { name: '_source', type: 'string' }, ]; diff --git a/common/constants/shared.ts b/common/constants/shared.ts index b1fa34aa41..1d9bc9b62e 100644 --- a/common/constants/shared.ts +++ b/common/constants/shared.ts @@ -263,3 +263,5 @@ export const DIRECT_DUMMY_QUERY = 'select 1'; export const DEFAULT_START_TIME = 'now-15m'; export const QUERY_ASSIST_START_TIME = 'now-40y'; export const QUERY_ASSIST_END_TIME = 'now'; + +export const TIMESTAMP_DATETIME_TYPES = ['date', 'date_nanos']; diff --git a/public/components/common/search/direct_search.tsx b/public/components/common/search/direct_search.tsx index f1ab57e890..1aac36bb6e 100644 --- a/public/components/common/search/direct_search.tsx +++ b/public/components/common/search/direct_search.tsx @@ -18,13 +18,25 @@ import { EuiPopoverFooter, EuiToolTip, } from '@elastic/eui'; -import { isEqual } from 'lodash'; +import { isEmpty, isEqual } from 'lodash'; import React, { useEffect, useState } from 'react'; import { batch, useDispatch, useSelector } from 'react-redux'; import { ASYNC_POLLING_INTERVAL, QUERY_LANGUAGE } from '../../../../common/constants/data_sources'; -import { APP_ANALYTICS_TAB_ID_REGEX, RAW_QUERY } from '../../../../common/constants/explorer'; -import { PPL_NEWLINE_REGEX, PPL_SPAN_REGEX } from '../../../../common/constants/shared'; -import { DirectQueryLoadingStatus, DirectQueryRequest } from '../../../../common/types/explorer'; +import { + APP_ANALYTICS_TAB_ID_REGEX, + RAW_QUERY, + SELECTED_TIMESTAMP, +} from '../../../../common/constants/explorer'; +import { + PPL_NEWLINE_REGEX, + PPL_SPAN_REGEX, + TIMESTAMP_DATETIME_TYPES, +} from '../../../../common/constants/shared'; +import { + DirectQueryLoadingStatus, + DirectQueryRequest, + IDefaultTimestampState, +} from '../../../../common/types/explorer'; import { uiSettingsService } from '../../../../common/utils'; import { getAsyncSessionId, setAsyncSessionId } from '../../../../common/utils/query_session_utils'; import { get as getObjValue } from '../../../../common/utils/shared'; @@ -42,6 +54,7 @@ import { formatError } from '../../event_analytics/utils'; import { usePolling } from '../../hooks/use_polling'; import { PPLReferenceFlyout } from '../helpers'; import { Autocomplete } from './autocomplete'; +import { i18n } from '@osd/i18n'; export interface IQueryBarProps { query: string; tempQuery: string; @@ -250,6 +263,33 @@ export const DirectSearch = (props: any) => { }); }; + const getDirectQueryTimestamp = (schema: Array<{ name: string; type: string }>) => { + const timestamp: IDefaultTimestampState = { + hasSchemaConflict: false, // schema conflict bool used for OS index w/ different mappings, not needed here + default_timestamp: '', + message: i18n.translate(`discover.events.directQuery.noTimeStampFoundMessage`, { + defaultMessage: 'Index does not contain a valid time field.', + }), + }; + + for (let i = 0; i < schema.length; i++) { + const fieldMapping = schema[i]; + if (!isEmpty(fieldMapping)) { + const fieldName = fieldMapping.name; + const fieldType = fieldMapping.type; + const isValidTimeType = TIMESTAMP_DATETIME_TYPES.some((dateTimeType) => + isEqual(fieldType, dateTimeType) + ); + if (isValidTimeType && isEmpty(timestamp.default_timestamp)) { + timestamp.default_timestamp = fieldName; + timestamp.message = ''; + break; + } + } + } + return timestamp; + }; + useEffect(() => { // cancel direct query if (!pollingResult) return; @@ -258,6 +298,16 @@ export const DirectSearch = (props: any) => { if (status === DirectQueryLoadingStatus.SUCCESS || datarows) { stopPollingWithStatus(status); + // find the timestamp from results + const derivedTimestamp = getDirectQueryTimestamp(pollingResult.schema); + dispatch( + changeQuery({ + tabId, + query: { + [SELECTED_TIMESTAMP]: derivedTimestamp.default_timestamp, + }, + }) + ); // update page with data dispatchOnGettingHis(pollingResult, ''); } else if (status === DirectQueryLoadingStatus.FAILED) { 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 e21e9494f0..3818564438 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 @@ -128,11 +128,59 @@ exports[`Datagrid component Renders data grid component 1`] = ` "unselectedFields": Array [], } } - http={[MockFunction]} + http={ + Object { + "addLoadingCountSource": [MockFunction], + "anonymousPaths": Object { + "isAnonymous": [MockFunction], + "register": [MockFunction], + }, + "basePath": BasePath { + "basePath": "", + "get": [Function], + "prepend": [Function], + "remove": [Function], + "serverBasePath": "", + }, + "delete": [MockFunction], + "fetch": [MockFunction], + "get": [MockFunction], + "getLoadingCount$": [MockFunction], + "head": [MockFunction], + "intercept": [MockFunction], + "options": [MockFunction], + "patch": [MockFunction], + "post": [MockFunction], + "put": [MockFunction], + } + } pplService={ PPLService { "fetch": [Function], - "http": [MockFunction], + "http": Object { + "addLoadingCountSource": [MockFunction], + "anonymousPaths": Object { + "isAnonymous": [MockFunction], + "register": [MockFunction], + }, + "basePath": BasePath { + "basePath": "", + "get": [Function], + "prepend": [Function], + "remove": [Function], + "serverBasePath": "", + }, + "delete": [MockFunction], + "fetch": [MockFunction], + "get": [MockFunction], + "getLoadingCount$": [MockFunction], + "head": [MockFunction], + "intercept": [MockFunction], + "options": [MockFunction], + "patch": [MockFunction], + "post": [MockFunction], + "put": [MockFunction], + }, } } rawQuery="source = opensearch_dashboards_sample_data_logs | where match(request,'filebeat')" @@ -203,14 +251,9 @@ exports[`Datagrid component Renders data grid component 1`] = ` }, ] } - rowsAll={Array []} startTime="now/y" storedSelectedColumns={ Array [ - Object { - "name": "timestamp", - "type": "timestamp", - }, Object { "name": "_source", "type": "string", @@ -243,7 +286,7 @@ exports[`Datagrid component Renders data grid component 1`] = ` columns={ Array [ Object { - "display": "Time", + "display": "Time (timestamp)", "id": "timestamp", "initialWidth": 200, "isSortable": true, @@ -442,3 +485,489 @@ exports[`Datagrid component Renders data grid component 1`] = ` `; + +exports[`Datagrid component renders data grid with different timestamp 1`] = ` + + + +
+
+ + + + + +
+ +
+
+
+ +
+ + + + + +
+
+ + + +`; 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 5a1729adf7..d9af5b86b5 100644 --- a/public/components/event_analytics/explorer/__tests__/data_grid.test.tsx +++ b/public/components/event_analytics/explorer/__tests__/data_grid.test.tsx @@ -14,6 +14,7 @@ import { UNSELECTED_FIELDS, QUERIED_FIELDS, DEFAULT_EMPTY_EXPLORER_FIELDS, + SELECTED_TIMESTAMP, } from '../../../../../common/constants/explorer'; import { AVAILABLE_FIELDS as SIDEBAR_AVAILABLE_FIELDS, @@ -21,14 +22,15 @@ import { 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'; +import { configureStore } from '@reduxjs/toolkit'; +import { queriesReducer } from '../../redux/slices/query_slice'; +import { coreMock } from '../../../../../../../src/core/public/mocks'; + +const coreStartMock = coreMock.createStart(); describe('Datagrid component', () => { configure({ adapter: new Adapter() }); @@ -41,28 +43,82 @@ describe('Datagrid component', () => { [QUERIED_FIELDS]: QUERY_FIELDS, }; - httpClientMock.get = jest.fn(() => - Promise.resolve((sampleEmptyPanel as unknown) as HttpResponse) - ); + coreStartMock.http.get = jest + .fn() + .mockResolvedValue((sampleEmptyPanel as unknown) as HttpResponse); - const http = httpClientMock; - const pplService = new PPLService(httpClientMock); - const store = createStore(rootReducer, applyMiddleware(thunk)); + const tabId = 'explorer-tab-_fbef9141-48eb-11ee-a60a-af33302cfb3c'; + + const pplService = new PPLService(coreStartMock.http); + const preloadedState = { + queries: { + [tabId]: { + [SELECTED_TIMESTAMP]: 'timestamp', + }, + }, + }; + const store = configureStore({ reducer: queriesReducer, preloadedState }); const wrapper = mount( + + ); + + wrapper.update(); + + await waitFor(() => { + expect(wrapper).toMatchSnapshot(); + }); + }); + + it('renders data grid with different timestamp', async () => { + const explorerFields = { + [SELECTED_FIELDS]: [], + [UNSELECTED_FIELDS]: [], + [AVAILABLE_FIELDS]: SIDEBAR_AVAILABLE_FIELDS, + [QUERIED_FIELDS]: QUERY_FIELDS, + }; + + coreStartMock.http.get = jest + .fn() + .mockResolvedValue((sampleEmptyPanel as unknown) as HttpResponse); + + const tabId = 'explorer-tab-_fbef9141-48eb-11ee-a60a-af33302cfb3c'; + + const pplService = new PPLService(coreStartMock.http); + const preloadedState = { + queries: { + [tabId]: { + [SELECTED_TIMESTAMP]: 'utc_time', + }, + }, + }; + const store = configureStore({ reducer: queriesReducer, preloadedState }); + + const wrapper = mount( + + 0 ? explorerFields.selectedFields - : DEFAULT_EMPTY_EXPLORER_FIELDS; + : [{ name: timeStampField, type: 'timestamp' }, ...DEFAULT_EMPTY_EXPLORER_FIELDS]; // 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]); + const pageFields = useRef([0, 100]); // page num, row length const [data, setData] = useState(rows); @@ -110,14 +112,27 @@ export function DataGrid(props: DataGridProps) { ); }; + const columnNameTranslate = (name: string) => { + return i18n.translate(`discover.events.dataGrid.${name.toLowerCase()}Column`, { + defaultMessage: name, + }); + }; + // creates the header for each column listing what that column is const dataGridColumns = () => { const columns: EuiDataGridColumn[] = []; selectedColumns.map(({ name }) => { - if (name === 'timestamp') { - columns.push(DEFAULT_TIMESTAMP_COLUMN); + if (name === timeStampField) { + columns.push({ + ...DEFAULT_TIMESTAMP_COLUMN, + display: `${columnNameTranslate('Time')} (${timeStampField})`, + id: timeStampField, + }); } else if (name === '_source') { - columns.push(DEFAULT_SOURCE_COLUMN); + columns.push({ + ...DEFAULT_SOURCE_COLUMN, + display: columnNameTranslate('Source'), + }); } else { columns.push({ id: name, @@ -183,7 +198,7 @@ export function DataGrid(props: DataGridProps) { // renders what is shown in each cell, i.e. the content of each row const dataGridCellRender = ({ rowIndex, columnId }: { rowIndex: number; columnId: string }) => { - const trueIndex = rowIndex % pageFields.current[1]; + const trueIndex = rowIndex % pageFields.current[1]; // modulo of row length, i.e. pos on current page if (trueIndex < data.length) { if (columnId === '_source') { return ( @@ -201,8 +216,8 @@ export function DataGrid(props: DataGridProps) { ); } - if (columnId === 'timestamp') { - return `${moment(data[trueIndex][columnId]).format(DATE_DISPLAY_FORMAT)}`; + if (columnId === timeStampField) { + return `${moment(data[trueIndex][timeStampField]).format(DATE_DISPLAY_FORMAT)}`; } return `${data[trueIndex][columnId]}`; }