From d1ed5b55cf9a7b859475ef4e76409e1d9f3f97c6 Mon Sep 17 00:00:00 2001 From: Joshua Li Date: Mon, 20 May 2024 13:23:51 -0700 Subject: [PATCH] Improve query assist user experiences (#1817) Signed-off-by: Joshua Li --- .../common/search/__tests__/search.test.tsx | 69 +++++++++- .../components/common/search/query_area.tsx | 2 +- public/components/common/search/search.tsx | 36 +++++- .../explorer/events_views/data_grid.tsx | 2 +- .../event_analytics/explorer/explorer.tsx | 50 +++---- .../event_analytics/explorer/log_explorer.tsx | 7 +- .../__snapshots__/callouts.test.tsx.snap | 2 +- .../explorer/query_assist/callouts.tsx | 14 +- .../explorer/query_assist/input.tsx | 45 +++++-- .../__snapshots__/timechart.test.tsx.snap | 122 ++++++++++++++++++ .../timechart/__tests__/timechart.test.tsx | 61 +++++++++ .../timechart/__tests__/utils.test.ts | 52 ++++++++ .../__snapshots__/hits_counter.test.tsx.snap | 0 .../__tests__/hits_counter.test.tsx | 0 .../hits_counter/hits_counter.tsx | 2 +- .../{ => timechart}/hits_counter/index.ts | 0 .../explorer/timechart/index.ts | 8 ++ .../explorer/timechart/timechart.tsx | 47 +++++++ .../timechart_header.test.tsx.snap | 0 .../__tests__/timechart_header.test.tsx | 4 +- .../{ => timechart}/timechart_header/index.ts | 0 .../timechart_header/timechart_header.tsx | 2 +- .../explorer/timechart/utils.ts | 17 +++ .../components/event_analytics/home/home.tsx | 5 +- 24 files changed, 481 insertions(+), 66 deletions(-) create mode 100644 public/components/event_analytics/explorer/timechart/__tests__/__snapshots__/timechart.test.tsx.snap create mode 100644 public/components/event_analytics/explorer/timechart/__tests__/timechart.test.tsx create mode 100644 public/components/event_analytics/explorer/timechart/__tests__/utils.test.ts rename public/components/event_analytics/explorer/{ => timechart}/hits_counter/__tests__/__snapshots__/hits_counter.test.tsx.snap (100%) rename public/components/event_analytics/explorer/{ => timechart}/hits_counter/__tests__/hits_counter.test.tsx (100%) rename public/components/event_analytics/explorer/{ => timechart}/hits_counter/hits_counter.tsx (96%) rename public/components/event_analytics/explorer/{ => timechart}/hits_counter/index.ts (100%) create mode 100644 public/components/event_analytics/explorer/timechart/index.ts create mode 100644 public/components/event_analytics/explorer/timechart/timechart.tsx rename public/components/event_analytics/explorer/{ => timechart}/timechart_header/__tests__/__snapshots__/timechart_header.test.tsx.snap (100%) rename public/components/event_analytics/explorer/{ => timechart}/timechart_header/__tests__/timechart_header.test.tsx (85%) rename public/components/event_analytics/explorer/{ => timechart}/timechart_header/index.ts (100%) rename public/components/event_analytics/explorer/{ => timechart}/timechart_header/timechart_header.tsx (97%) create mode 100644 public/components/event_analytics/explorer/timechart/utils.ts diff --git a/public/components/common/search/__tests__/search.test.tsx b/public/components/common/search/__tests__/search.test.tsx index dafa7567a9..821e264e95 100644 --- a/public/components/common/search/__tests__/search.test.tsx +++ b/public/components/common/search/__tests__/search.test.tsx @@ -3,15 +3,18 @@ * SPDX-License-Identifier: Apache-2.0 */ +import { applyMiddleware, createStore } from '@reduxjs/toolkit'; +import { render } from '@testing-library/react'; import { configure, mount } from 'enzyme'; import Adapter from 'enzyme-adapter-react-16'; import React from 'react'; -import { applyMiddleware, createStore } from '@reduxjs/toolkit'; -import { rootReducer } from '../../../../framework/redux/reducers'; import { Provider } from 'react-redux'; -import { Search } from '../search'; import thunk from 'redux-thunk'; +import { coreRefs } from '../../../../framework/core_refs'; +import { rootReducer } from '../../../../framework/redux/reducers'; import { initialTabId } from '../../../../framework/redux/store/shared_state'; +import * as hookExports from '../../../event_analytics/explorer/query_assist/hooks'; +import { Search } from '../search'; describe('Explorer Search component', () => { configure({ adapter: new Adapter() }); @@ -27,3 +30,63 @@ describe('Explorer Search component', () => { expect(wrapper).toMatchSnapshot(); }); }); + +describe('Search state', () => { + const catIndicesSpy = jest.spyOn(hookExports, 'useCatIndices'); + const getIndexPatternsSpy = jest.spyOn(hookExports, 'useGetIndexPatterns'); + const store = createStore(rootReducer, applyMiddleware(thunk)); + + beforeEach(() => { + coreRefs.queryAssistEnabled = true; + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('selects logs sample data over others', async () => { + catIndicesSpy.mockReturnValue({ + data: [{ label: 'opensearch_dashboards_sample_data_flights' }], + loading: false, + refresh: jest.fn(), + }); + getIndexPatternsSpy.mockReturnValue({ + data: [{ label: 'test-index' }, { label: 'opensearch_dashboards_sample_data_logs' }], + loading: false, + refresh: jest.fn(), + }); + const component = render( + + Promise.resolve() }} + /> + + ); + expect(component.getByText('opensearch_dashboards_sample_data_logs')).toBeInTheDocument(); + }); + + it('selects other sample data', async () => { + catIndicesSpy.mockReturnValue({ + data: [{ label: 'test-index' }, { label: 'opensearch_dashboards_sample_data_flights' }], + loading: false, + refresh: jest.fn(), + }); + getIndexPatternsSpy.mockReturnValue({ + data: [], + loading: false, + refresh: jest.fn(), + }); + const component = render( + + Promise.resolve() }} + /> + + ); + expect(component.getByText('opensearch_dashboards_sample_data_flights')).toBeInTheDocument(); + }); +}); diff --git a/public/components/common/search/query_area.tsx b/public/components/common/search/query_area.tsx index 2345f0b2cb..b176f45596 100644 --- a/public/components/common/search/query_area.tsx +++ b/public/components/common/search/query_area.tsx @@ -37,7 +37,7 @@ export function QueryArea({ const memoizedGetAvailableFields = useMemo(() => getAvailableFields, []); const memoizedHandleQueryChange = useMemo(() => handleQueryChange, []); useEffect(() => { - const indexQuery = `source = ${selectedIndex[0].label}`; + const indexQuery = `source = ${selectedIndex[0]?.label || ''}`; memoizedHandleQueryChange(indexQuery); memoizedGetAvailableFields(indexQuery); }, [selectedIndex, memoizedGetAvailableFields, memoizedHandleQueryChange]); diff --git a/public/components/common/search/search.tsx b/public/components/common/search/search.tsx index a99e02763e..f101079fbe 100644 --- a/public/components/common/search/search.tsx +++ b/public/components/common/search/search.tsx @@ -30,7 +30,11 @@ import { OLLY_QUERY_ASSISTANT, RAW_QUERY, } from '../../../../common/constants/explorer'; -import { PPL_SPAN_REGEX } from '../../../../common/constants/shared'; +import { + PPL_SPAN_REGEX, + QUERY_ASSIST_END_TIME, + QUERY_ASSIST_START_TIME, +} from '../../../../common/constants/shared'; import { uiSettingsService } from '../../../../common/utils'; import { useFetchEvents } from '../../../components/event_analytics/hooks'; import { usePolling } from '../../../components/hooks/use_polling'; @@ -277,14 +281,16 @@ export const Search = (props: any) => { dispatch(changeQuery({ tabId, query: { [RAW_QUERY]: tempQuery } })); }); onQuerySearch(queryLang); - handleTimePickerChange([startTime, endTime]); + if (coreRefs.queryAssistEnabled) { + handleTimePickerChange([QUERY_ASSIST_START_TIME, QUERY_ASSIST_END_TIME]); + } else { + handleTimePickerChange([startTime, endTime]); + } setNeedsUpdate(false); }; // STATE FOR LANG PICKER AND INDEX PICKER - const [selectedIndex, setSelectedIndex] = useState([ - { label: 'opensearch_dashboards_sample_data_logs' }, - ]); + const [selectedIndex, setSelectedIndex] = useState([]); const { data: indices, loading: indicesLoading } = useCatIndices(); const { data: indexPatterns, loading: indexPatternsLoading } = useGetIndexPatterns(); const indicesAndIndexPatterns = @@ -292,9 +298,25 @@ export const Search = (props: any) => { ? [...indexPatterns, ...indices].filter( (v1, index, array) => array.findIndex((v2) => v1.label === v2.label) === index ) - : undefined; + : []; const loading = indicesLoading || indexPatternsLoading; + useEffect(() => { + if (selectedIndex.length || !indicesAndIndexPatterns.length) return; + // pre-fill selected index with sample logs or other sample data index + const sampleLogOption = indicesAndIndexPatterns.find( + (option) => option.label === 'opensearch_dashboards_sample_data_logs' + ); + if (sampleLogOption) { + setSelectedIndex([sampleLogOption]); + return; + } + const sampleDataOption = indicesAndIndexPatterns.find((option) => + option.label.startsWith('opensearch_dashboards_sample_data_') + ); + if (sampleDataOption) setSelectedIndex([sampleDataOption]); + }, [indicesAndIndexPatterns]); + const onLanguagePopoverClick = () => { setLanguagePopoverOpen(!_isLanguagePopoverOpen); }; @@ -352,7 +374,7 @@ export const Search = (props: any) => { Index} singleSelection={{ asPlainText: true }} isLoading={loading} 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 2aede681f5..2e55437130 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 0dd15575a6..bccd7740bd 100644 --- a/public/components/event_analytics/explorer/explorer.tsx +++ b/public/components/event_analytics/explorer/explorer.tsx @@ -21,7 +21,11 @@ import { EuiText, } from '@elastic/eui'; import { FormattedMessage } from '@osd/i18n/react'; -import _, { isEmpty, isEqual, reduce } from 'lodash'; +import { createBrowserHistory } from 'history'; +import isEmpty from 'lodash/isEmpty'; +import isEqual from 'lodash/isEqual'; +import reduce from 'lodash/reduce'; +import sum from 'lodash/sum'; import React, { ReactElement, useCallback, @@ -32,7 +36,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 +130,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,12 +528,25 @@ 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( countDistribution.data['count()'], - (sum, n) => { - return sum + n; + (total, n) => { + return total + n; }, liveHits ); @@ -558,13 +572,9 @@ export const Explorer = ({ {countDistribution?.data && !isLiveTailOnRef.current && ( - {}} - /> - { const intervalOptionsIndex = timeIntervalOptions.findIndex( (item) => item.value === selectedIntrv @@ -580,20 +590,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/log_explorer.tsx b/public/components/event_analytics/explorer/log_explorer.tsx index 8d7ea7b807..f495651614 100644 --- a/public/components/event_analytics/explorer/log_explorer.tsx +++ b/public/components/event_analytics/explorer/log_explorer.tsx @@ -18,13 +18,8 @@ import { EmptyTabParams, ILogExplorerProps } from '../../../../common/types/expl import { selectQueryResult } from '../redux/slices/query_result_slice'; import { selectQueries } from '../redux/slices/query_slice'; import { selectQueryTabs } from '../redux/slices/query_tab_slice'; -import { Explorer } from './explorer'; import { getDateRange } from '../utils/utils'; -import { - QUERY_ASSIST_END_TIME, - QUERY_ASSIST_START_TIME, -} from '../../../../common/constants/shared'; -import { coreRefs } from '../../../../public/framework/core_refs'; +import { Explorer } from './explorer'; const searchBarConfigs = { [TAB_EVENT_ID]: { diff --git a/public/components/event_analytics/explorer/query_assist/__tests__/__snapshots__/callouts.test.tsx.snap b/public/components/event_analytics/explorer/query_assist/__tests__/__snapshots__/callouts.test.tsx.snap index 4bd92a4253..6989a255d8 100644 --- a/public/components/event_analytics/explorer/query_assist/__tests__/__snapshots__/callouts.test.tsx.snap +++ b/public/components/event_analytics/explorer/query_assist/__tests__/__snapshots__/callouts.test.tsx.snap @@ -4,7 +4,7 @@ exports[`Callouts spec EmptyQueryCallOut should match snapshot 1`] = `
= (props) /> ); +export const EmptyIndexCallOut: React.FC = (props) => ( + +); + export const EmptyQueryCallOut: React.FC = (props) => ( > = (props const explorerData = useSelector(selectQueryResult)[props.tabId]; // @ts-ignore const summaryData = useSelector(selectQueryAssistantSummarization)[props.tabId]; - const loading = summaryData.loading; + const [generatingPPL, setGeneratingPPL] = useState(false); + const loading = summaryData.loading || generatingPPL; const inputRef = useRef(null); + const selectedIndex = props.selectedIndex[0]?.label || ''; useEffect(() => { if ( @@ -162,7 +172,7 @@ export const QueryAssistInput: React.FC> = (props const generatedPPL = await getOSDHttp().post(QUERY_ASSIST_API.GENERATE_PPL, { body: JSON.stringify({ question: props.nlqInput, - index: props.selectedIndex[0].label, + index: selectedIndex, }), }); await props.handleQueryChange(generatedPPL); @@ -182,13 +192,16 @@ export const QueryAssistInput: React.FC> = (props const generatePPL = async () => { dispatch(reset({ tabId: props.tabId })); dispatch(resetSummary({ tabId: props.tabId })); - if (!props.selectedIndex.length) return; + if (!selectedIndex) { + props.setCallOut(); + return; + } if (props.nlqInput.trim().length === 0) { props.setCallOut(); return; } try { - dispatch(setLoading({ tabId: props.tabId, loading: true })); + setGeneratingPPL(true); dismissCallOut(); await request(); } catch (err) { @@ -199,7 +212,7 @@ export const QueryAssistInput: React.FC> = (props } coreRefs.toasts?.addError(error, { title: 'Failed to generate results' }); } finally { - dispatch(setLoading({ tabId: props.tabId, loading: false })); + setGeneratingPPL(false); } }; const generateSummary = async (context?: Partial) => { @@ -208,7 +221,7 @@ export const QueryAssistInput: React.FC> = (props const isError = summaryData.responseForSummaryStatus === 'failure'; const summarizationContext: SummarizationContext = { question: props.nlqInput, - index: props.selectedIndex[0].label, + index: selectedIndex, isError, query: queryRedux.rawQuery, response: isError @@ -273,7 +286,10 @@ export const QueryAssistInput: React.FC> = (props const runAndSummarize = async () => { dispatch(reset({ tabId: props.tabId })); dispatch(resetSummary({ tabId: props.tabId })); - if (!props.selectedIndex.length) return; + if (!selectedIndex) { + props.setCallOut(); + return; + } if (props.nlqInput.trim().length === 0) { props.setCallOut(); return; @@ -281,8 +297,10 @@ export const QueryAssistInput: React.FC> = (props try { dispatch(setLoading({ tabId: props.tabId, loading: true })); dismissCallOut(); + setGeneratingPPL(true); await request(); - await props.handleTimePickerChange([QUERY_ASSIST_START_TIME, 'now']); + setGeneratingPPL(false); + await props.handleTimePickerChange([QUERY_ASSIST_START_TIME, QUERY_ASSIST_END_TIME]); await props.handleTimeRangePickerRefresh(undefined, true); } catch (err) { const error = formatError(err); @@ -296,6 +314,7 @@ export const QueryAssistInput: React.FC> = (props coreRefs.toasts?.addError(error, { title: 'Failed to generate results' }); } } finally { + setGeneratingPPL(false); dispatch(setLoading({ tabId: props.tabId, loading: false })); } }; @@ -309,8 +328,8 @@ export const QueryAssistInput: React.FC> = (props > = (props }} > - {HARDCODED_SUGGESTIONS[props.selectedIndex[0]?.label]?.map((question) => ( + {HARDCODED_SUGGESTIONS[selectedIndex]?.map((question) => ( { props.setNlqInput(question); 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 0000000000..1cee2d0349 --- /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 0000000000..b87a34685a --- /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 0000000000..16e0cd52db --- /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 0941658eb4..a8a8eacb52 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 0000000000..3122b0c234 --- /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 0000000000..f488f5c7f3 --- /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 0841429076..5107fb33f9 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 f294adc3f2..1c6f1f85de 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 0000000000..adaaf77c8d --- /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 5c7ed46e57..87d5df3403 100644 --- a/public/components/event_analytics/home/home.tsx +++ b/public/components/event_analytics/home/home.tsx @@ -24,7 +24,7 @@ import { EuiText, EuiTitle, } from '@elastic/eui'; -import React, { ReactElement, useEffect, useRef, useState } from 'react'; +import React, { ReactElement, useEffect, useState } from 'react'; import { connect } from 'react-redux'; import { useHistory } from 'react-router-dom'; import { HttpStart } from '../../../../../../src/core/public'; @@ -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);