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 2aede681f..2e5543713 100644 --- a/public/components/event_analytics/explorer/events_views/data_grid.tsx +++ b/public/components/event_analytics/explorer/events_views/data_grid.tsx @@ -29,7 +29,7 @@ import PPLService from '../../../../services/requests/ppl'; import { useFetchEvents } from '../../hooks'; import { redoQuery } from '../../utils/utils'; import { FlyoutButton } from './docViewRow'; -import { HitsCounter } from '../hits_counter/hits_counter'; +import { HitsCounter } from '../timechart/hits_counter'; export interface DataGridProps { http: HttpSetup; diff --git a/public/components/event_analytics/explorer/explorer.tsx b/public/components/event_analytics/explorer/explorer.tsx index 0dd15575a..2e6cd674d 100644 --- a/public/components/event_analytics/explorer/explorer.tsx +++ b/public/components/event_analytics/explorer/explorer.tsx @@ -21,6 +21,7 @@ import { EuiText, } from '@elastic/eui'; import { FormattedMessage } from '@osd/i18n/react'; +import { createBrowserHistory } from 'history'; import _, { isEmpty, isEqual, reduce } from 'lodash'; import React, { ReactElement, @@ -32,7 +33,6 @@ import React, { useState, } from 'react'; import { batch, useDispatch, useSelector } from 'react-redux'; -import { createBrowserHistory } from 'history'; import { LogExplorerRouterContext } from '..'; import { DEFAULT_DATA_SOURCE_TYPE, @@ -127,13 +127,11 @@ import { formatError, getContentTabTitle } from '../utils/utils'; import { DataSourceSelection } from './datasources/datasources_selection'; import { DirectQueryRunning } from './direct_query_running'; import { DataGrid } from './events_views/data_grid'; -import { HitsCounter } from './hits_counter/hits_counter'; import { LogPatterns } from './log_patterns/log_patterns'; import { NoResults } from './no_results'; import { ObservabilitySideBar } from './sidebar/observability_sidebar'; -import { TimechartHeader } from './timechart_header'; +import { getTimeRangeFromCountDistribution, HitsCounter, Timechart } from './timechart'; import { ExplorerVisualizations } from './visualizations'; -import { CountDistribution } from './visualizations/count_distribution'; import { DirectQueryVisualization } from './visualizations/direct_query_vis'; export const Explorer = ({ @@ -527,6 +525,19 @@ export const Explorer = ({ handleQuerySearch(); }; + /** + * If query assist is enabled, the time range is fixed to + * QUERY_ASSIST_START_TIME and QUERY_ASSIST_END_TIME and not useful. Return + * the time range based on aggregation buckets instead. + * + * @returns startTime and endTime + */ + const getTimeChartRange = (): { startTime?: string; endTime?: string } => { + if (!coreRefs.queryAssistEnabled) return { startTime, endTime }; + const { startTime: start, endTime: end } = getTimeRangeFromCountDistribution(countDistribution); + return { startTime: start ?? startTime, endTime: end ?? endTime }; + }; + const totalHits: number = useMemo(() => { if (isLiveTailOn && countDistribution?.data) { const hits = reduce( @@ -558,13 +569,9 @@ export const Explorer = ({ {countDistribution?.data && !isLiveTailOnRef.current && ( - {}} - /> - { const intervalOptionsIndex = timeIntervalOptions.findIndex( (item) => item.value === selectedIntrv @@ -580,20 +587,10 @@ export const Explorer = ({ selectedIntervalRef.current = timeIntervalOptions[intervalOptionsIndex]; getPatterns(intrv, getErrorHandler('Error fetching patterns')); }} - stateInterval={ - countDistribution.selectedInterval || selectedIntervalRef.current?.value - } - startTime={startTime} - endTime={endTime} - /> - - )} diff --git a/public/components/event_analytics/explorer/timechart/__tests__/__snapshots__/timechart.test.tsx.snap b/public/components/event_analytics/explorer/timechart/__tests__/__snapshots__/timechart.test.tsx.snap new file mode 100644 index 000000000..1cee2d034 --- /dev/null +++ b/public/components/event_analytics/explorer/timechart/__tests__/__snapshots__/timechart.test.tsx.snap @@ -0,0 +1,122 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` spec should match snapshot 1`] = ` +
+
+
+
+ + 194 + + + hits +
+
+
+
+
+ +
+ Jan 1, 2024 @ 00:00:00.000 - Jan 1, 2024 @ 00:00:00.000 +
+
+
+
+
+
+ +
+ + + +
+
+
+
+
+
+
+
+`; diff --git a/public/components/event_analytics/explorer/timechart/__tests__/timechart.test.tsx b/public/components/event_analytics/explorer/timechart/__tests__/timechart.test.tsx new file mode 100644 index 000000000..b87a34685 --- /dev/null +++ b/public/components/event_analytics/explorer/timechart/__tests__/timechart.test.tsx @@ -0,0 +1,61 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { fireEvent, render } from '@testing-library/react'; +import React, { ComponentProps } from 'react'; +import { Timechart } from '../timechart'; + +const renderTimechart = (overrideProps: Partial> = {}) => { + const props: jest.Mocked> = Object.assign< + ComponentProps, + Partial> + >( + { + countDistribution: { + selectedInterval: 'y', + data: { 'count()': [194], 'span(timestamp,1y)': ['2024-01-01 00:00:00'] }, + metadata: { + fields: [ + { name: 'count()', type: 'integer' }, + { name: 'span(timestamp,1y)', type: 'timestamp' }, + ], + }, + size: 1, + status: 200, + jsonData: [{ 'count()': 194, 'span(timestamp,1y)': '2024-01-01 00:00:00' }], + }, + timeIntervalOptions: [ + { text: 'Minute', value: 'm' }, + { text: 'Hour', value: 'h' }, + { text: 'Day', value: 'd' }, + { text: 'Week', value: 'w' }, + { text: 'Month', value: 'M' }, + { text: 'Year', value: 'y' }, + ], + onChangeInterval: jest.fn(), + selectedInterval: 'y', + startTime: '2024-01-01 00:00:00', + endTime: '2024-01-01 00:00:00', + }, + overrideProps + ); + const component = render(); + return { component, props }; +}; + +describe(' spec', () => { + it('should change to week', () => { + const { component, props } = renderTimechart(); + fireEvent.change(component.getByTestId('eventAnalytics__EventIntervalSelect'), { + target: { value: 'w' }, + }); + expect(props.onChangeInterval).toBeCalledWith('w'); + }); + + it('should match snapshot', () => { + const { component } = renderTimechart(); + expect(component.container).toMatchSnapshot(); + }); +}); diff --git a/public/components/event_analytics/explorer/timechart/__tests__/utils.test.ts b/public/components/event_analytics/explorer/timechart/__tests__/utils.test.ts new file mode 100644 index 000000000..16e0cd52d --- /dev/null +++ b/public/components/event_analytics/explorer/timechart/__tests__/utils.test.ts @@ -0,0 +1,52 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { getTimeRangeFromCountDistribution } from '../utils'; + +describe('getTimeRangeFromCountDistribution', () => { + it('gets from first and last element of span', () => { + const results = getTimeRangeFromCountDistribution({ + data: { + 'count()': [194], + 'span(timestamp,1d)': ['2024-01-01 00:00:00', '2024-01-02 00:00:00', '2024-01-03 00:00:00'], + }, + metadata: { + fields: [ + { name: 'count()', type: 'integer' }, + { name: 'span(timestamp,1d)', type: 'timestamp' }, + ], + }, + }); + + expect(results).toMatchInlineSnapshot(` + Object { + "endTime": "2024-01-03 00:00:00", + "startTime": "2024-01-01 00:00:00", + } + `); + }); + + it('handles empty inputs and returns undefined', () => { + const results = getTimeRangeFromCountDistribution({ + data: { + 'count()': [194], + 'span(timestamp,1d)': [], + }, + metadata: { + fields: [ + { name: 'count()', type: 'integer' }, + { name: 'span(timestamp,1d)', type: 'timestamp' }, + ], + }, + }); + + expect(results).toMatchInlineSnapshot(` + Object { + "endTime": undefined, + "startTime": undefined, + } + `); + }); +}); diff --git a/public/components/event_analytics/explorer/hits_counter/__tests__/__snapshots__/hits_counter.test.tsx.snap b/public/components/event_analytics/explorer/timechart/hits_counter/__tests__/__snapshots__/hits_counter.test.tsx.snap similarity index 100% rename from public/components/event_analytics/explorer/hits_counter/__tests__/__snapshots__/hits_counter.test.tsx.snap rename to public/components/event_analytics/explorer/timechart/hits_counter/__tests__/__snapshots__/hits_counter.test.tsx.snap diff --git a/public/components/event_analytics/explorer/hits_counter/__tests__/hits_counter.test.tsx b/public/components/event_analytics/explorer/timechart/hits_counter/__tests__/hits_counter.test.tsx similarity index 100% rename from public/components/event_analytics/explorer/hits_counter/__tests__/hits_counter.test.tsx rename to public/components/event_analytics/explorer/timechart/hits_counter/__tests__/hits_counter.test.tsx diff --git a/public/components/event_analytics/explorer/hits_counter/hits_counter.tsx b/public/components/event_analytics/explorer/timechart/hits_counter/hits_counter.tsx similarity index 96% rename from public/components/event_analytics/explorer/hits_counter/hits_counter.tsx rename to public/components/event_analytics/explorer/timechart/hits_counter/hits_counter.tsx index 0941658eb..a8a8eacb5 100644 --- a/public/components/event_analytics/explorer/hits_counter/hits_counter.tsx +++ b/public/components/event_analytics/explorer/timechart/hits_counter/hits_counter.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; import { FormattedMessage, I18nProvider } from '@osd/i18n/react'; import { i18n } from '@osd/i18n'; -import { formatNumWithCommas } from '../../../common/helpers'; +import { formatNumWithCommas } from '../../../../common/helpers'; export interface HitsCounterProps { /** diff --git a/public/components/event_analytics/explorer/hits_counter/index.ts b/public/components/event_analytics/explorer/timechart/hits_counter/index.ts similarity index 100% rename from public/components/event_analytics/explorer/hits_counter/index.ts rename to public/components/event_analytics/explorer/timechart/hits_counter/index.ts diff --git a/public/components/event_analytics/explorer/timechart/index.ts b/public/components/event_analytics/explorer/timechart/index.ts new file mode 100644 index 000000000..3122b0c23 --- /dev/null +++ b/public/components/event_analytics/explorer/timechart/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export { HitsCounter } from './hits_counter/hits_counter'; +export { Timechart } from './timechart'; +export { getTimeRangeFromCountDistribution } from './utils'; diff --git a/public/components/event_analytics/explorer/timechart/timechart.tsx b/public/components/event_analytics/explorer/timechart/timechart.tsx new file mode 100644 index 000000000..f488f5c7f --- /dev/null +++ b/public/components/event_analytics/explorer/timechart/timechart.tsx @@ -0,0 +1,47 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { EuiSpacer } from '@elastic/eui'; +import _ from 'lodash'; +import React from 'react'; +import { selectCountDistribution } from '../../redux/slices/count_distribution_slice'; +import { CountDistribution } from '../visualizations/count_distribution'; +import { HitsCounter } from './hits_counter'; +import { TimechartHeader } from './timechart_header'; + +interface TimechartProps { + countDistribution: ReturnType[string]; + timeIntervalOptions: Array<{ text: string; value: string }>; + onChangeInterval: (interval: string) => void; + selectedInterval: string; + startTime?: string; + endTime?: string; +} + +export const Timechart: React.FC = (props) => { + return ( + <> + {}} + /> + + + + + ); +}; diff --git a/public/components/event_analytics/explorer/timechart_header/__tests__/__snapshots__/timechart_header.test.tsx.snap b/public/components/event_analytics/explorer/timechart/timechart_header/__tests__/__snapshots__/timechart_header.test.tsx.snap similarity index 100% rename from public/components/event_analytics/explorer/timechart_header/__tests__/__snapshots__/timechart_header.test.tsx.snap rename to public/components/event_analytics/explorer/timechart/timechart_header/__tests__/__snapshots__/timechart_header.test.tsx.snap diff --git a/public/components/event_analytics/explorer/timechart_header/__tests__/timechart_header.test.tsx b/public/components/event_analytics/explorer/timechart/timechart_header/__tests__/timechart_header.test.tsx similarity index 85% rename from public/components/event_analytics/explorer/timechart_header/__tests__/timechart_header.test.tsx rename to public/components/event_analytics/explorer/timechart/timechart_header/__tests__/timechart_header.test.tsx index 084142907..5107fb33f 100644 --- a/public/components/event_analytics/explorer/timechart_header/__tests__/timechart_header.test.tsx +++ b/public/components/event_analytics/explorer/timechart/timechart_header/__tests__/timechart_header.test.tsx @@ -8,11 +8,11 @@ import Adapter from 'enzyme-adapter-react-16'; import React from 'react'; import { waitFor } from '@testing-library/react'; import { TimechartHeader } from '../timechart_header'; -import { TIME_INTERVAL_OPTIONS } from '../../../../../../common/constants/explorer'; +import { TIME_INTERVAL_OPTIONS } from '../../../../../../../common/constants/explorer'; import { EXPLORER_START_TIME, EXPLORER_END_TIME, -} from '../../../../../../test/event_analytics_constants'; +} from '../../../../../../../test/event_analytics_constants'; describe('Time chart header component', () => { configure({ adapter: new Adapter() }); diff --git a/public/components/event_analytics/explorer/timechart_header/index.ts b/public/components/event_analytics/explorer/timechart/timechart_header/index.ts similarity index 100% rename from public/components/event_analytics/explorer/timechart_header/index.ts rename to public/components/event_analytics/explorer/timechart/timechart_header/index.ts diff --git a/public/components/event_analytics/explorer/timechart_header/timechart_header.tsx b/public/components/event_analytics/explorer/timechart/timechart_header/timechart_header.tsx similarity index 97% rename from public/components/event_analytics/explorer/timechart_header/timechart_header.tsx rename to public/components/event_analytics/explorer/timechart/timechart_header/timechart_header.tsx index f294adc3f..1c6f1f85d 100644 --- a/public/components/event_analytics/explorer/timechart_header/timechart_header.tsx +++ b/public/components/event_analytics/explorer/timechart/timechart_header/timechart_header.tsx @@ -12,7 +12,7 @@ import datemath from '@elastic/datemath'; import { DATE_DISPLAY_FORMAT, DEFAULT_DATETIME_STRING, -} from '../../../../../common/constants/explorer'; +} from '../../../../../../common/constants/explorer'; function reformatDate(inputDate: string | undefined) { return moment(datemath.parse(inputDate ?? DEFAULT_DATETIME_STRING)).format(DATE_DISPLAY_FORMAT); diff --git a/public/components/event_analytics/explorer/timechart/utils.ts b/public/components/event_analytics/explorer/timechart/utils.ts new file mode 100644 index 000000000..adaaf77c8 --- /dev/null +++ b/public/components/event_analytics/explorer/timechart/utils.ts @@ -0,0 +1,17 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { selectCountDistribution } from '../../redux/slices/count_distribution_slice'; + +export const getTimeRangeFromCountDistribution = ( + countDistribution: ReturnType[string] +): { startTime?: string; endTime?: string } => { + const { + data, + metadata: { fields }, + } = countDistribution; + // fields[1] is the x-axis (time buckets) in count distribution + return { startTime: data[fields[1].name].at(0), endTime: data[fields[1].name].at(-1) }; +}; diff --git a/public/components/event_analytics/home/home.tsx b/public/components/event_analytics/home/home.tsx index 5c7ed46e5..0eec324ff 100644 --- a/public/components/event_analytics/home/home.tsx +++ b/public/components/event_analytics/home/home.tsx @@ -70,15 +70,12 @@ interface IHomeProps { const EventAnalyticsHome = (props: IHomeProps) => { const { setToast, http } = props; const history = useHistory(); - const [selectedDateRange, _setSelectedDateRange] = useState(['now-40y', 'now']); const [savedHistories, setSavedHistories] = useState([]); const [selectedHistories, setSelectedHistories] = useState([]); const [isActionsPopoverOpen, setIsActionsPopoverOpen] = useState(false); const [isTableLoading, setIsTableLoading] = useState(false); const [modalLayout, setModalLayout] = useState(); const [isModalVisible, setIsModalVisible] = useState(false); - const selectedDateRangeRef = useRef(); - selectedDateRangeRef.current = selectedDateRange; const closeModal = () => { setIsModalVisible(false);