From 838445cc6ee229ae4c5dff5d3e6bc790b4736cb6 Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Sat, 29 Oct 2022 02:02:13 -0300 Subject: [PATCH] [Discover] Update unfiied histogram to make total hits request --- .../layout/__stories__/get_layout_props.ts | 12 - .../components/layout/discover_layout.tsx | 3 + .../layout/discover_main_content.tsx | 12 + .../main/components/layout/types.ts | 2 + .../layout/use_discover_histogram.ts | 68 +++++- .../application/main/discover_main_app.tsx | 2 + .../main/hooks/use_discover_state.ts | 1 + .../main/hooks/use_saved_search.ts | 5 +- .../main/hooks/use_saved_search_messages.ts | 24 +- .../services/discover_search_session.test.ts | 1 + .../main/services/discover_search_session.ts | 12 +- .../application/main/utils/fetch_all.ts | 38 +--- .../main/utils/fetch_total_hits.test.ts | 57 ----- .../main/utils/fetch_total_hits.ts | 58 ----- .../unified_histogram/public/chart/chart.tsx | 136 +++++------- .../public/chart/histogram.tsx | 64 +++--- .../public/chart/use_chart_actions.ts | 53 +++++ .../public/chart/use_chart_panels.ts | 4 +- .../public/chart/use_chart_styles.tsx | 63 ++++++ .../public/chart/use_request_params.tsx | 51 +++++ .../public/chart/use_total_hits.ts | 207 ++++++++++++++++++ .../public/hits_counter/hits_counter.tsx | 19 +- .../public/layout/layout.tsx | 15 ++ src/plugins/unified_histogram/public/types.ts | 19 ++ .../translations/translations/fr-FR.json | 2 - .../translations/translations/ja-JP.json | 2 - .../translations/translations/zh-CN.json | 2 - 27 files changed, 617 insertions(+), 315 deletions(-) delete mode 100644 src/plugins/discover/public/application/main/utils/fetch_total_hits.test.ts delete mode 100644 src/plugins/discover/public/application/main/utils/fetch_total_hits.ts create mode 100644 src/plugins/unified_histogram/public/chart/use_chart_actions.ts create mode 100644 src/plugins/unified_histogram/public/chart/use_chart_styles.tsx create mode 100644 src/plugins/unified_histogram/public/chart/use_request_params.tsx create mode 100644 src/plugins/unified_histogram/public/chart/use_total_hits.ts diff --git a/src/plugins/discover/public/application/main/components/layout/__stories__/get_layout_props.ts b/src/plugins/discover/public/application/main/components/layout/__stories__/get_layout_props.ts index 56c0f349615e0..334e899b04aee 100644 --- a/src/plugins/discover/public/application/main/components/layout/__stories__/get_layout_props.ts +++ b/src/plugins/discover/public/application/main/components/layout/__stories__/get_layout_props.ts @@ -11,11 +11,9 @@ import { SearchSource } from '@kbn/data-plugin/common'; import { BehaviorSubject, Subject } from 'rxjs'; import { RequestAdapter } from '@kbn/inspector-plugin/common'; import { action } from '@storybook/addon-actions'; -import type { SearchResponse } from '@elastic/elasticsearch/lib/api/types'; import { FetchStatus } from '../../../../types'; import { AvailableFields$, - DataCharts$, DataDocuments$, DataMain$, DataTotalHits$, @@ -47,11 +45,6 @@ const documentObservables = { fetchStatus: FetchStatus.COMPLETE, result: Number(esHits.length), }) as DataTotalHits$, - - charts$: new BehaviorSubject({ - fetchStatus: FetchStatus.COMPLETE, - response: {} as unknown as SearchResponse, - }) as DataCharts$, }; const plainRecordObservables = { @@ -77,11 +70,6 @@ const plainRecordObservables = { fetchStatus: FetchStatus.COMPLETE, recordRawType: RecordRawType.PLAIN, }) as DataTotalHits$, - - charts$: new BehaviorSubject({ - fetchStatus: FetchStatus.COMPLETE, - recordRawType: RecordRawType.PLAIN, - }) as DataCharts$, }; const getCommonProps = (dataView: DataView) => { diff --git a/src/plugins/discover/public/application/main/components/layout/discover_layout.tsx b/src/plugins/discover/public/application/main/components/layout/discover_layout.tsx index a7cde67d4869b..331e4b04c736d 100644 --- a/src/plugins/discover/public/application/main/components/layout/discover_layout.tsx +++ b/src/plugins/discover/public/application/main/components/layout/discover_layout.tsx @@ -72,6 +72,7 @@ export function DiscoverLayout({ persistDataView, updateAdHocDataViewId, adHocDataViewList, + searchSessionManager, }: DiscoverLayoutProps) { const { trackUiMetric, @@ -324,6 +325,8 @@ export function DiscoverLayout({ onFieldEdited={onFieldEdited} columns={columns} resizeRef={resizeRef} + inspectorAdapters={inspectorAdapters} + searchSessionManager={searchSessionManager} /> )} diff --git a/src/plugins/discover/public/application/main/components/layout/discover_main_content.tsx b/src/plugins/discover/public/application/main/components/layout/discover_main_content.tsx index 4551f3d6d903e..70ca210e87c81 100644 --- a/src/plugins/discover/public/application/main/components/layout/discover_main_content.tsx +++ b/src/plugins/discover/public/application/main/components/layout/discover_main_content.tsx @@ -15,6 +15,7 @@ import { UnifiedHistogramLayout } from '@kbn/unified-histogram-plugin/public'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { css } from '@emotion/react'; +import type { RequestAdapter } from '@kbn/inspector-plugin/public'; import { useDiscoverServices } from '../../../../hooks/use_discover_services'; import { DataTableRecord } from '../../../../types'; import { DocumentViewModeToggle, VIEW_MODE } from '../../../../components/view_mode_toggle'; @@ -25,6 +26,7 @@ import { FieldStatisticsTable } from '../field_stats_table'; import { DiscoverDocuments } from './discover_documents'; import { DOCUMENTS_VIEW_CLICK, FIELD_STATISTICS_VIEW_CLICK } from '../field_stats_table/constants'; import { useDiscoverHistogram } from './use_discover_histogram'; +import type { DiscoverSearchSessionManager } from '../../services/discover_search_session'; const FieldStatisticsTableMemoized = React.memo(FieldStatisticsTable); @@ -46,6 +48,8 @@ export interface DiscoverMainContentProps { onFieldEdited: () => Promise; columns: string[]; resizeRef: RefObject; + inspectorAdapters: { requests: RequestAdapter }; + searchSessionManager: DiscoverSearchSessionManager; } export const DiscoverMainContent = ({ @@ -66,6 +70,8 @@ export const DiscoverMainContent = ({ onFieldEdited, columns, resizeRef, + inspectorAdapters, + searchSessionManager, }: DiscoverMainContentProps) => { const services = useDiscoverServices(); const { trackUiMetric } = services; @@ -87,6 +93,7 @@ export const DiscoverMainContent = ({ const { topPanelHeight, + request, hits, chart, breakdown, @@ -95,6 +102,7 @@ export const DiscoverMainContent = ({ onChartHiddenChange, onTimeIntervalChange, onBreakdownFieldChange, + onTotalHitsChange, } = useDiscoverHistogram({ stateContainer, state, @@ -103,6 +111,8 @@ export const DiscoverMainContent = ({ savedSearch, isTimeBased, isPlainRecord, + inspectorAdapters, + searchSessionManager, }); const resetSearchButtonWrapper = css` @@ -114,6 +124,7 @@ export const DiscoverMainContent = ({ className="dscPageContent__inner" services={services} dataView={dataView} + request={request} hits={hits} chart={chart} breakdown={breakdown} @@ -144,6 +155,7 @@ export const DiscoverMainContent = ({ onChartHiddenChange={onChartHiddenChange} onTimeIntervalChange={onTimeIntervalChange} onBreakdownFieldChange={onBreakdownFieldChange} + onTotalHitsChange={onTotalHitsChange} > Promise; updateAdHocDataViewId: (dataView: DataView) => Promise; adHocDataViewList: DataView[]; + searchSessionManager: DiscoverSearchSessionManager; } diff --git a/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.ts b/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.ts index f6d52122202eb..0681251eb01eb 100644 --- a/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.ts +++ b/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.ts @@ -13,12 +13,17 @@ import { triggerVisualizeActions, } from '@kbn/unified-field-list-plugin/public'; import { useCallback, useEffect, useMemo, useState } from 'react'; +import type { UnifiedHistogramFetchStatus } from '@kbn/unified-histogram-plugin/public'; +import type { RequestAdapter } from '@kbn/inspector-plugin/public'; +import useDebounce from 'react-use/lib/useDebounce'; import { getUiActions } from '../../../../kibana_services'; import { PLUGIN_ID } from '../../../../../common'; import { useDiscoverServices } from '../../../../hooks/use_discover_services'; import { useDataState } from '../../hooks/use_data_state'; import type { SavedSearchData } from '../../hooks/use_saved_search'; import type { AppState, GetStateReturn } from '../../services/discover_state'; +import { FetchStatus } from '../../../types'; +import type { DiscoverSearchSessionManager } from '../../services/discover_search_session'; export const CHART_HIDDEN_KEY = 'discover:chartHidden'; export const HISTOGRAM_HEIGHT_KEY = 'discover:histogramHeight'; @@ -32,6 +37,8 @@ export const useDiscoverHistogram = ({ savedSearch, isTimeBased, isPlainRecord, + inspectorAdapters, + searchSessionManager, }: { stateContainer: GetStateReturn; state: AppState; @@ -40,6 +47,8 @@ export const useDiscoverHistogram = ({ savedSearch: SavedSearch; isTimeBased: boolean; isPlainRecord: boolean; + inspectorAdapters: { requests: RequestAdapter }; + searchSessionManager: DiscoverSearchSessionManager; }) => { const { storage } = useDiscoverServices(); @@ -115,9 +124,60 @@ export const useDiscoverHistogram = ({ ); /** - * Data + * Request */ + const [lastReloadRequestTime, setLastReloadRequestTime] = useState(0); + const { fetchStatus: mainFetchStatus } = useDataState(savedSearchData$.main$); + + // Reload unified histogram when a refetch is triggered, + // with a debounce to avoid multiple requests + const [, cancelDebounce] = useDebounce( + () => { + if (mainFetchStatus === FetchStatus.LOADING) { + setLastReloadRequestTime(Date.now()); + } + }, + 100, + [mainFetchStatus] + ); + + // A refetch is triggered when the data view is changed, + // but we don't want to reload unified histogram in this case, + // so cancel the debounced effect on unmount + useEffect(() => cancelDebounce, [cancelDebounce]); + + const searchSessionId = searchSessionManager.getLastSearchSessionId(); + const request = useMemo( + () => ({ + searchSessionId, + adapter: inspectorAdapters.requests, + lastReloadRequestTime, + }), + [inspectorAdapters.requests, lastReloadRequestTime, searchSessionId] + ); + + /** + * Total hits + */ + + const onTotalHitsChange = useCallback( + (status: UnifiedHistogramFetchStatus, totalHits?: number) => { + const { fetchStatus, recordRawType } = savedSearchData$.totalHits$.getValue(); + + if (fetchStatus === 'partial' && status === 'loading') { + return; + } + + savedSearchData$.totalHits$.next({ + fetchStatus: status as FetchStatus, + result: totalHits, + recordRawType, + }); + }, + [savedSearchData$.totalHits$] + ); + const { fetchStatus: hitsFetchStatus, result: hitsTotal } = useDataState( savedSearchData$.totalHits$ ); @@ -133,6 +193,10 @@ export const useDiscoverHistogram = ({ [hitsFetchStatus, hitsTotal, isPlainRecord] ); + /** + * Chart + */ + const chart = useMemo( () => isPlainRecord || !isTimeBased @@ -168,6 +232,7 @@ export const useDiscoverHistogram = ({ return { topPanelHeight, + request, hits, chart, breakdown, @@ -176,5 +241,6 @@ export const useDiscoverHistogram = ({ onChartHiddenChange, onTimeIntervalChange, onBreakdownFieldChange, + onTotalHitsChange, }; }; diff --git a/src/plugins/discover/public/application/main/discover_main_app.tsx b/src/plugins/discover/public/application/main/discover_main_app.tsx index abd714fea8f07..5b407c35a210d 100644 --- a/src/plugins/discover/public/application/main/discover_main_app.tsx +++ b/src/plugins/discover/public/application/main/discover_main_app.tsx @@ -62,6 +62,7 @@ export function DiscoverMainApp(props: DiscoverMainProps) { state, stateContainer, adHocDataViewList, + searchSessionManager, } = useDiscoverState({ services, history: usedHistory, @@ -121,6 +122,7 @@ export function DiscoverMainApp(props: DiscoverMainProps) { persistDataView={persistDataView} updateAdHocDataViewId={updateAdHocDataViewId} adHocDataViewList={adHocDataViewList} + searchSessionManager={searchSessionManager} /> ); diff --git a/src/plugins/discover/public/application/main/hooks/use_discover_state.ts b/src/plugins/discover/public/application/main/hooks/use_discover_state.ts index ff448e59917a8..7d17279af2d4f 100644 --- a/src/plugins/discover/public/application/main/hooks/use_discover_state.ts +++ b/src/plugins/discover/public/application/main/hooks/use_discover_state.ts @@ -308,5 +308,6 @@ export function useDiscoverState({ adHocDataViewList, persistDataView, updateAdHocDataViewId, + searchSessionManager, }; } diff --git a/src/plugins/discover/public/application/main/hooks/use_saved_search.ts b/src/plugins/discover/public/application/main/hooks/use_saved_search.ts index 787a077b0d5e3..8c47ba95ec892 100644 --- a/src/plugins/discover/public/application/main/hooks/use_saved_search.ts +++ b/src/plugins/discover/public/application/main/hooks/use_saved_search.ts @@ -36,7 +36,6 @@ export interface SavedSearchData { export type DataMain$ = BehaviorSubject; export type DataDocuments$ = BehaviorSubject; export type DataTotalHits$ = BehaviorSubject; -export type DataCharts$ = BehaviorSubject; export type AvailableFields$ = BehaviorSubject; export type DataRefetch$ = Subject; @@ -125,7 +124,6 @@ export const useSavedSearch = ({ const main$: DataMain$ = useBehaviorSubject(initialState) as DataMain$; const documents$: DataDocuments$ = useBehaviorSubject(initialState) as DataDocuments$; const totalHits$: DataTotalHits$ = useBehaviorSubject(initialState) as DataTotalHits$; - const charts$: DataCharts$ = useBehaviorSubject(initialState) as DataCharts$; const availableFields$: AvailableFields$ = useBehaviorSubject(initialState) as AvailableFields$; const dataSubjects = useMemo(() => { @@ -133,10 +131,9 @@ export const useSavedSearch = ({ main$, documents$, totalHits$, - charts$, availableFields$, }; - }, [main$, charts$, documents$, totalHits$, availableFields$]); + }, [main$, documents$, totalHits$, availableFields$]); /** * The observable to trigger data fetching in UI diff --git a/src/plugins/discover/public/application/main/hooks/use_saved_search_messages.ts b/src/plugins/discover/public/application/main/hooks/use_saved_search_messages.ts index c0ad37b952207..1657534353a75 100644 --- a/src/plugins/discover/public/application/main/hooks/use_saved_search_messages.ts +++ b/src/plugins/discover/public/application/main/hooks/use_saved_search_messages.ts @@ -6,14 +6,13 @@ * Side Public License, v 1. */ -import { AggregateQuery, Query } from '@kbn/es-query'; +import type { BehaviorSubject } from 'rxjs'; import { FetchStatus } from '../../types'; -import { - DataCharts$, +import type { DataDocuments$, DataMain$, + DataMsg, DataTotalHits$, - RecordRawType, SavedSearchData, } from './use_saved_search'; @@ -60,27 +59,22 @@ export function sendPartialMsg(main$: DataMain$) { /** * Send LOADING message via main observable */ -export function sendLoadingMsg( - data$: DataMain$ | DataDocuments$ | DataTotalHits$ | DataCharts$, - recordRawType: RecordRawType, - query?: AggregateQuery | Query +export function sendLoadingMsg( + data$: BehaviorSubject, + props: Omit ) { if (data$.getValue().fetchStatus !== FetchStatus.LOADING) { data$.next({ + ...props, fetchStatus: FetchStatus.LOADING, - recordRawType, - query, - }); + } as T); } } /** * Send ERROR message */ -export function sendErrorMsg( - data$: DataMain$ | DataDocuments$ | DataTotalHits$ | DataCharts$, - error: Error -) { +export function sendErrorMsg(data$: DataMain$ | DataDocuments$ | DataTotalHits$, error: Error) { const recordRawType = data$.getValue().recordRawType; data$.next({ fetchStatus: FetchStatus.ERROR, diff --git a/src/plugins/discover/public/application/main/services/discover_search_session.test.ts b/src/plugins/discover/public/application/main/services/discover_search_session.test.ts index 0f854438b6749..d1d1fb398727c 100644 --- a/src/plugins/discover/public/application/main/services/discover_search_session.test.ts +++ b/src/plugins/discover/public/application/main/services/discover_search_session.test.ts @@ -28,6 +28,7 @@ describe('DiscoverSearchSessionManager', () => { const id = searchSessionManager.getNextSearchSessionId(); expect(id).toEqual(nextId); expect(session.start).toBeCalled(); + expect(searchSessionManager.getLastSearchSessionId()).toEqual(id); }); test('restores a session using query param from the URL', () => { diff --git a/src/plugins/discover/public/application/main/services/discover_search_session.ts b/src/plugins/discover/public/application/main/services/discover_search_session.ts index 5797b0381b1bf..e91238b8a1c47 100644 --- a/src/plugins/discover/public/application/main/services/discover_search_session.ts +++ b/src/plugins/discover/public/application/main/services/discover_search_session.ts @@ -32,6 +32,7 @@ export class DiscoverSearchSessionManager { */ readonly newSearchSessionIdFromURL$: Rx.Observable; private readonly deps: DiscoverSearchSessionManagerDeps; + private lastSearchSessionId?: string; constructor(deps: DiscoverSearchSessionManagerDeps) { this.deps = deps; @@ -65,7 +66,16 @@ export class DiscoverSearchSessionManager { } } - return searchSessionIdFromURL ?? this.deps.session.start(); + this.lastSearchSessionId = searchSessionIdFromURL ?? this.deps.session.start(); + + return this.lastSearchSessionId; + } + + /** + * Get the last returned session id by {@link getNextSearchSessionId} + */ + getLastSearchSessionId() { + return this.lastSearchSessionId; } /** diff --git a/src/plugins/discover/public/application/main/utils/fetch_all.ts b/src/plugins/discover/public/application/main/utils/fetch_all.ts index 5a09f07dbdbfd..146f41c871276 100644 --- a/src/plugins/discover/public/application/main/utils/fetch_all.ts +++ b/src/plugins/discover/public/application/main/utils/fetch_all.ts @@ -8,7 +8,6 @@ import { DataPublicPluginStart, ISearchSource } from '@kbn/data-plugin/public'; import { Adapters } from '@kbn/inspector-plugin/common'; import { ReduxLikeStateContainer } from '@kbn/kibana-utils-plugin/common'; -import { DataViewType } from '@kbn/data-views-plugin/public'; import type { SavedSearch, SortOrder } from '@kbn/saved-search-plugin/public'; import { getRawRecordType } from './get_raw_record_type'; import { @@ -21,14 +20,11 @@ import { } from '../hooks/use_saved_search_messages'; import { updateSearchSource } from './update_search_source'; import { fetchDocuments } from './fetch_documents'; -import { fetchTotalHits } from './fetch_total_hits'; import { AppState } from '../services/discover_state'; import { FetchStatus } from '../../types'; import { - DataCharts$, DataDocuments$, DataMain$, - DataTotalHits$, RecordRawType, SavedSearchData, } from '../hooks/use_saved_search'; @@ -49,8 +45,7 @@ export interface FetchDeps { /** * This function starts fetching all required queries in Discover. This will be the query to load the individual - * documents, and depending on whether a chart is shown either the aggregation query to load the chart data - * or a query to retrieve just the total hits. + * documents as well as any other requests that might be required to load the main view. * * This method returns a promise, which will resolve (without a value), as soon as all queries that have been started * have been completed (failed or successfully). @@ -68,9 +63,7 @@ export function fetchAll( * to the specified subjects. It will ignore AbortErrors and will use the data * plugin to show a toast for the error (e.g. allowing better insights into shard failures). */ - const sendErrorTo = ( - ...errorSubjects: Array - ) => { + const sendErrorTo = (...errorSubjects: Array) => { return (error: Error) => { if (error instanceof Error && error.name === 'AbortError') { return; @@ -86,7 +79,7 @@ export function fetchAll( if (reset) { sendResetMsg(dataSubjects, initialFetchStatus); } - const { hideChart, sort, query } = appStateContainer.getState(); + const { sort, query } = appStateContainer.getState(); const recordRawType = getRawRecordType(query); const useSql = recordRawType === RecordRawType.PLAIN; @@ -101,20 +94,16 @@ export function fetchAll( } // Mark all subjects as loading - sendLoadingMsg(dataSubjects.main$, recordRawType); - sendLoadingMsg(dataSubjects.documents$, recordRawType, query); - sendLoadingMsg(dataSubjects.totalHits$, recordRawType); - - const isChartVisible = - !hideChart && dataView.isTimeBased() && dataView.type !== DataViewType.ROLLUP; + sendLoadingMsg(dataSubjects.main$, { recordRawType }); + sendLoadingMsg(dataSubjects.documents$, { recordRawType, query }); + sendLoadingMsg(dataSubjects.totalHits$, { recordRawType }); // Start fetching all required requests const documents = useSql && query ? fetchSql(query, services.dataViews, data, services.expressions) : fetchDocuments(searchSource.createCopy(), fetchDeps); - const totalHits = - !isChartVisible && !useSql ? fetchTotalHits(searchSource.createCopy(), fetchDeps) : undefined; + /** * This method checks the passed in hit count and will send a PARTIAL message to main$ * if there are results, indicating that we have finished some of the requests that have been @@ -158,19 +147,8 @@ export function fetchAll( // but their errors will be shown in-place (e.g. of the chart). .catch(sendErrorTo(dataSubjects.documents$, dataSubjects.main$)); - totalHits - ?.then((hitCount) => { - dataSubjects.totalHits$.next({ - fetchStatus: FetchStatus.COMPLETE, - result: hitCount, - recordRawType, - }); - checkHitCount(hitCount); - }) - .catch(sendErrorTo(dataSubjects.totalHits$)); - // Return a promise that will resolve once all the requests have finished or failed - return Promise.allSettled([documents, totalHits]).then(() => { + return Promise.allSettled([documents]).then(() => { // Send a complete message to main$ once all queries are done and if main$ // is not already in an ERROR state, e.g. because the document query has failed. // This will only complete main$, if it hasn't already been completed previously diff --git a/src/plugins/discover/public/application/main/utils/fetch_total_hits.test.ts b/src/plugins/discover/public/application/main/utils/fetch_total_hits.test.ts deleted file mode 100644 index f2851a57e7365..0000000000000 --- a/src/plugins/discover/public/application/main/utils/fetch_total_hits.test.ts +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ -import { throwError as throwErrorRx, of } from 'rxjs'; -import { RequestAdapter } from '@kbn/inspector-plugin/common'; -import { savedSearchMock, savedSearchMockWithTimeField } from '../../../__mocks__/saved_search'; -import { fetchTotalHits } from './fetch_total_hits'; -import { discoverServiceMock } from '../../../__mocks__/services'; -import { SearchResponse } from '@elastic/elasticsearch/lib/api/types'; -import { IKibanaSearchResponse } from '@kbn/data-plugin/public'; -import { FetchDeps } from './fetch_all'; - -const getDeps = () => - ({ - abortController: new AbortController(), - inspectorAdapters: { requests: new RequestAdapter() }, - searchSessionId: '123', - data: discoverServiceMock.data, - savedSearch: savedSearchMock, - } as FetchDeps); - -describe('test fetchTotalHits', () => { - test('resolves returned promise with hit count', async () => { - savedSearchMock.searchSource.fetch$ = () => - of({ rawResponse: { hits: { total: 45 } } } as IKibanaSearchResponse>); - - await expect(fetchTotalHits(savedSearchMock.searchSource, getDeps())).resolves.toBe(45); - }); - - test('rejects in case of an error', async () => { - savedSearchMock.searchSource.fetch$ = () => throwErrorRx(() => new Error('Oh noes!')); - - await expect(fetchTotalHits(savedSearchMock.searchSource, getDeps())).rejects.toEqual( - new Error('Oh noes!') - ); - }); - test('fetch$ is called with execution context containing savedSearch id', async () => { - const fetch$Mock = jest - .fn() - .mockReturnValue( - of({ rawResponse: { hits: { total: 45 } } } as IKibanaSearchResponse) - ); - - savedSearchMockWithTimeField.searchSource.fetch$ = fetch$Mock; - - await fetchTotalHits(savedSearchMockWithTimeField.searchSource, getDeps()); - expect(fetch$Mock.mock.calls[0][0].executionContext).toMatchInlineSnapshot(` - Object { - "description": "fetch total hits", - } - `); - }); -}); diff --git a/src/plugins/discover/public/application/main/utils/fetch_total_hits.ts b/src/plugins/discover/public/application/main/utils/fetch_total_hits.ts deleted file mode 100644 index 16bd138e2caf5..0000000000000 --- a/src/plugins/discover/public/application/main/utils/fetch_total_hits.ts +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { i18n } from '@kbn/i18n'; -import { filter, map } from 'rxjs/operators'; -import { lastValueFrom } from 'rxjs'; -import { isCompleteResponse, ISearchSource } from '@kbn/data-plugin/public'; -import { DataViewType } from '@kbn/data-views-plugin/public'; -import { FetchDeps } from './fetch_all'; - -export function fetchTotalHits( - searchSource: ISearchSource, - { abortController, inspectorAdapters, searchSessionId, savedSearch }: FetchDeps -) { - searchSource.setField('trackTotalHits', true); - searchSource.setField('size', 0); - searchSource.removeField('sort'); - searchSource.removeField('fields'); - searchSource.removeField('aggs'); - if (searchSource.getField('index')?.type === DataViewType.ROLLUP) { - // We treat that data view as "normal" even if it was a rollup data view, - // since the rollup endpoint does not support querying individual documents, but we - // can get them from the regular _search API that will be used if the data view - // not a rollup data view. - searchSource.setOverwriteDataViewType(undefined); - } - - const executionContext = { - description: 'fetch total hits', - }; - - const fetch$ = searchSource - .fetch$({ - inspector: { - adapter: inspectorAdapters.requests, - title: i18n.translate('discover.inspectorRequestDataTitleTotalHits', { - defaultMessage: 'Total hits', - }), - description: i18n.translate('discover.inspectorRequestDescriptionTotalHits', { - defaultMessage: 'This request queries Elasticsearch to fetch the total hits.', - }), - }, - abortSignal: abortController.signal, - sessionId: searchSessionId, - executionContext, - }) - .pipe( - filter((res) => isCompleteResponse(res)), - map((res) => res.rawResponse.hits.total as number) - ); - - return lastValueFrom(fetch$); -} diff --git a/src/plugins/unified_histogram/public/chart/chart.tsx b/src/plugins/unified_histogram/public/chart/chart.tsx index e960a94031cbc..50b856829b182 100644 --- a/src/plugins/unified_histogram/public/chart/chart.tsx +++ b/src/plugins/unified_histogram/public/chart/chart.tsx @@ -7,7 +7,7 @@ */ import type { ReactElement } from 'react'; -import React, { memo, useCallback, useEffect, useRef, useState } from 'react'; +import React, { memo } from 'react'; import { EuiButtonIcon, EuiContextMenu, @@ -15,11 +15,8 @@ import { EuiFlexItem, EuiPopover, EuiToolTip, - useEuiBreakpoint, - useEuiTheme, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { css } from '@emotion/react'; import { DataView, DataViewField, DataViewType } from '@kbn/data-views-plugin/public'; import { HitsCounter } from '../hits_counter'; import { Histogram } from './histogram'; @@ -27,15 +24,22 @@ import { useChartPanels } from './use_chart_panels'; import type { UnifiedHistogramBreakdownContext, UnifiedHistogramChartContext, + UnifiedHistogramFetchStatus, UnifiedHistogramHitsContext, + UnifiedHistogramRequestContext, UnifiedHistogramServices, } from '../types'; import { BreakdownFieldSelector } from './breakdown_field_selector'; +import { useTotalHits } from './use_total_hits'; +import { useRequestParams } from './use_request_params'; +import { useChartStyles } from './use_chart_styles'; +import { useChartActions } from './use_chart_actions'; export interface ChartProps { className?: string; services: UnifiedHistogramServices; dataView: DataView; + request?: UnifiedHistogramRequestContext; hits?: UnifiedHistogramHitsContext; chart?: UnifiedHistogramChartContext; breakdown?: UnifiedHistogramBreakdownContext; @@ -46,6 +50,7 @@ export interface ChartProps { onChartHiddenChange?: (chartHidden: boolean) => void; onTimeIntervalChange?: (timeInterval: string) => void; onBreakdownFieldChange?: (breakdownField: DataViewField | undefined) => void; + onTotalHitsChange?: (status: UnifiedHistogramFetchStatus, totalHits?: number) => void; } const HistogramMemoized = memo(Histogram); @@ -54,6 +59,7 @@ export function Chart({ className, services, dataView, + request, hits, chart, breakdown, @@ -64,95 +70,58 @@ export function Chart({ onChartHiddenChange, onTimeIntervalChange, onBreakdownFieldChange, + onTotalHitsChange, }: ChartProps) { - const [showChartOptionsPopover, setShowChartOptionsPopover] = useState(false); - - const chartRef = useRef<{ element: HTMLElement | null; moveFocus: boolean }>({ - element: null, - moveFocus: false, + const { + showChartOptionsPopover, + chartRef, + toggleChartOptions, + closeChartOptions, + toggleHideChart, + } = useChartActions({ + chart, + onChartHiddenChange, }); - const onShowChartOptions = useCallback(() => { - setShowChartOptionsPopover(!showChartOptionsPopover); - }, [showChartOptionsPopover]); - - const closeChartOptions = useCallback(() => { - setShowChartOptionsPopover(false); - }, [setShowChartOptionsPopover]); - - useEffect(() => { - if (chartRef.current.moveFocus && chartRef.current.element) { - chartRef.current.element.focus(); - } - }, [chart?.hidden]); - - const toggleHideChart = useCallback(() => { - const chartHidden = !chart?.hidden; - chartRef.current.moveFocus = !chartHidden; - onChartHiddenChange?.(chartHidden); - }, [chart?.hidden, onChartHiddenChange]); - const panels = useChartPanels({ chart, toggleHideChart, - onTimeIntervalChange: (timeInterval) => onTimeIntervalChange?.(timeInterval), - closePopover: () => setShowChartOptionsPopover(false), + onTimeIntervalChange, + closePopover: closeChartOptions, onResetChartHeight, }); - const [totalHits, setTotalHits] = useState(); - - const onTotalHitsChange = useCallback((newTotalHits: number) => { - setTotalHits(newTotalHits); - }, []); - - const chartVisible = + const chartVisible = !!( chart && !chart.hidden && dataView.id && dataView.type !== DataViewType.ROLLUP && - dataView.isTimeBased(); + dataView.isTimeBased() + ); - const { euiTheme } = useEuiTheme(); - const resultCountCss = css` - padding: ${euiTheme.size.s} ${euiTheme.size.s} ${chartVisible ? 0 : euiTheme.size.s} - ${euiTheme.size.s}; - min-height: ${euiTheme.base * 2.5}px; - `; - const resultCountTitleCss = css` - ${useEuiBreakpoint(['xs', 's'])} { - margin-bottom: 0 !important; - } - `; - const resultCountToggleCss = css` - ${useEuiBreakpoint(['xs', 's'])} { - align-items: flex-end; - } - `; - const timechartCss = css` - flex-grow: 1; - display: flex; - flex-direction: column; - position: relative; + const { filters, query, timeRange } = useRequestParams(services); + + useTotalHits({ + services, + request, + chartVisible, + hits, + dataView, + filters, + query, + timeRange, + onTotalHitsChange, + }); - // SASSTODO: the visualizing component should have an option or a modifier - .series > rect { - fill-opacity: 0.5; - stroke-width: 1; - } - `; - const breakdownFieldSelectorGroupCss = css` - width: 100%; - `; - const breakdownFieldSelectorItemCss = css` - align-items: flex-end; - padding-left: ${euiTheme.size.s}; - `; - const chartToolButtonCss = css` - display: flex; - justify-content: center; - padding-left: ${euiTheme.size.s}; - `; + const { + resultCountCss, + resultCountTitleCss, + resultCountToggleCss, + histogramCss, + breakdownFieldSelectorGroupCss, + breakdownFieldSelectorItemCss, + chartToolButtonCss, + } = useChartStyles(chartVisible); return ( - {hits && } + {hits && } {chart && ( @@ -220,7 +189,7 @@ export function Chart({ diff --git a/src/plugins/unified_histogram/public/chart/histogram.tsx b/src/plugins/unified_histogram/public/chart/histogram.tsx index 283c1e982083d..e5981574a228b 100644 --- a/src/plugins/unified_histogram/public/chart/histogram.tsx +++ b/src/plugins/unified_histogram/public/chart/histogram.tsx @@ -8,17 +8,20 @@ import { useEuiTheme } from '@elastic/eui'; import { css } from '@emotion/react'; -import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import React, { useCallback, useMemo, useState } from 'react'; import { ViewMode } from '@kbn/embeddable-plugin/public'; import type { DataView } from '@kbn/data-views-plugin/public'; import type { DefaultInspectorAdapters } from '@kbn/expressions-plugin/common'; -import { connectToQueryState, IKibanaSearchResponse, QueryState } from '@kbn/data-plugin/public'; +import type { IKibanaSearchResponse } from '@kbn/data-plugin/public'; import type { estypes } from '@elastic/elasticsearch'; -import { createStateContainer, useContainerState } from '@kbn/kibana-utils-plugin/public'; +import type { AggregateQuery, Query, Filter, TimeRange } from '@kbn/es-query'; import type { UnifiedHistogramBreakdownContext, UnifiedHistogramBucketInterval, UnifiedHistogramChartContext, + UnifiedHistogramFetchStatus, + UnifiedHistogramHitsContext, + UnifiedHistogramRequestContext, UnifiedHistogramServices, } from '../types'; import { getLensAttributes } from './get_lens_attributes'; @@ -28,48 +31,32 @@ import { useTimeRange } from './use_time_range'; export interface HistogramProps { services: UnifiedHistogramServices; dataView: DataView; + request?: UnifiedHistogramRequestContext; + hits?: UnifiedHistogramHitsContext; chart: UnifiedHistogramChartContext; breakdown?: UnifiedHistogramBreakdownContext; - onTotalHitsChange: (totalHits: number) => void; + filters: Filter[]; + query: Query | AggregateQuery; + timeRange: TimeRange; + onTotalHitsChange?: (status: UnifiedHistogramFetchStatus, totalHits?: number) => void; } export function Histogram({ services: { data, lens, uiSettings }, dataView, + request, + hits, chart: { timeInterval }, breakdown: { field: breakdownField } = {}, + filters, + query, + timeRange, onTotalHitsChange, }: HistogramProps) { - const queryStateContainer = useMemo(() => { - return createStateContainer({ - filters: data.query.filterManager.getFilters(), - query: data.query.queryString.getQuery(), - refreshInterval: data.query.timefilter.timefilter.getRefreshInterval(), - time: data.query.timefilter.timefilter.getTime(), - }); - }, [data.query.filterManager, data.query.queryString, data.query.timefilter.timefilter]); - - const queryState = useContainerState(queryStateContainer); - - useEffect(() => { - return connectToQueryState(data.query, queryStateContainer, { - time: true, - query: true, - filters: true, - refreshInterval: true, - }); - }, [data.query, queryStateContainer]); - - const filters = useMemo(() => queryState.filters ?? [], [queryState.filters]); - const query = useMemo( - () => queryState.query ?? data.query.queryString.getDefaultQuery(), - [data.query.queryString, queryState.query] - ); const attributes = useMemo( () => getLensAttributes({ filters, query, dataView, timeInterval, breakdownField }), [breakdownField, dataView, filters, query, timeInterval] ); - const timeRange = data.query.timefilter.timefilter.getAbsoluteTime(); const [bucketInterval, setBucketInterval] = useState(); const { timeRangeText, timeRangeDisplay } = useTimeRange({ uiSettings, @@ -79,15 +66,13 @@ export function Histogram({ }); const onLoad = useCallback( - (_, adapters: Partial | undefined) => { + (isLoading, adapters: Partial | undefined) => { const totalHits = adapters?.tables?.tables?.unifiedHistogram?.meta?.statistics?.totalCount; - if (totalHits) { - onTotalHitsChange(totalHits); - } + onTotalHitsChange?.(isLoading ? 'loading' : 'complete', totalHits ?? hits?.total); - const request = adapters?.requests?.getRequests()[0]; - const json = request?.response?.json as IKibanaSearchResponse; + const lensRequest = adapters?.requests?.getRequests()[0]; + const json = lensRequest?.response?.json as IKibanaSearchResponse; const response = json?.rawResponse; if (response) { @@ -101,7 +86,7 @@ export function Histogram({ setBucketInterval(newBucketInterval); } }, - [data, dataView, onTotalHitsChange, timeInterval] + [data, dataView, hits?.total, onTotalHitsChange, timeInterval] ); const { euiTheme } = useEuiTheme(); @@ -134,6 +119,11 @@ export function Histogram({ timeRange={timeRange} attributes={attributes} noPadding + searchSessionId={request?.searchSessionId} + executionContext={{ + description: 'fetch chart data and total hits', + }} + lastReloadRequestTime={request?.lastReloadRequestTime} onLoad={onLoad} /> diff --git a/src/plugins/unified_histogram/public/chart/use_chart_actions.ts b/src/plugins/unified_histogram/public/chart/use_chart_actions.ts new file mode 100644 index 0000000000000..85b876e0862c1 --- /dev/null +++ b/src/plugins/unified_histogram/public/chart/use_chart_actions.ts @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { useCallback, useEffect, useRef, useState } from 'react'; +import type { UnifiedHistogramChartContext } from '../types'; + +export const useChartActions = ({ + chart, + onChartHiddenChange, +}: { + chart: UnifiedHistogramChartContext | undefined; + onChartHiddenChange?: (chartHidden: boolean) => void; +}) => { + const [showChartOptionsPopover, setShowChartOptionsPopover] = useState(false); + + const toggleChartOptions = useCallback(() => { + setShowChartOptionsPopover(!showChartOptionsPopover); + }, [showChartOptionsPopover]); + + const closeChartOptions = useCallback(() => { + setShowChartOptionsPopover(false); + }, [setShowChartOptionsPopover]); + + const chartRef = useRef<{ element: HTMLElement | null; moveFocus: boolean }>({ + element: null, + moveFocus: false, + }); + + useEffect(() => { + if (chartRef.current.moveFocus && chartRef.current.element) { + chartRef.current.element.focus(); + } + }, [chart?.hidden]); + + const toggleHideChart = useCallback(() => { + const chartHidden = !chart?.hidden; + chartRef.current.moveFocus = !chartHidden; + onChartHiddenChange?.(chartHidden); + }, [chart?.hidden, onChartHiddenChange]); + + return { + showChartOptionsPopover, + chartRef, + toggleChartOptions, + closeChartOptions, + toggleHideChart, + }; +}; diff --git a/src/plugins/unified_histogram/public/chart/use_chart_panels.ts b/src/plugins/unified_histogram/public/chart/use_chart_panels.ts index dd6f162b352f6..8f2874baa624e 100644 --- a/src/plugins/unified_histogram/public/chart/use_chart_panels.ts +++ b/src/plugins/unified_histogram/public/chart/use_chart_panels.ts @@ -23,7 +23,7 @@ export function useChartPanels({ }: { chart?: UnifiedHistogramChartContext; toggleHideChart: () => void; - onTimeIntervalChange: (timeInterval: string) => void; + onTimeIntervalChange?: (timeInterval: string) => void; closePopover: () => void; onResetChartHeight?: () => void; }) { @@ -107,7 +107,7 @@ export function useChartPanels({ label: display, icon: val === chart.timeInterval ? 'check' : 'empty', onClick: () => { - onTimeIntervalChange(val); + onTimeIntervalChange?.(val); closePopover(); }, 'data-test-subj': `unifiedHistogramTimeInterval-${display}`, diff --git a/src/plugins/unified_histogram/public/chart/use_chart_styles.tsx b/src/plugins/unified_histogram/public/chart/use_chart_styles.tsx new file mode 100644 index 0000000000000..dc551626bc791 --- /dev/null +++ b/src/plugins/unified_histogram/public/chart/use_chart_styles.tsx @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { useEuiBreakpoint, useEuiTheme } from '@elastic/eui'; +import { css } from '@emotion/react'; + +export const useChartStyles = (chartVisible: boolean) => { + const { euiTheme } = useEuiTheme(); + const resultCountCss = css` + padding: ${euiTheme.size.s} ${euiTheme.size.s} ${chartVisible ? 0 : euiTheme.size.s} + ${euiTheme.size.s}; + min-height: ${euiTheme.base * 2.5}px; + `; + const resultCountTitleCss = css` + ${useEuiBreakpoint(['xs', 's'])} { + margin-bottom: 0 !important; + } + `; + const resultCountToggleCss = css` + ${useEuiBreakpoint(['xs', 's'])} { + align-items: flex-end; + } + `; + const histogramCss = css` + flex-grow: 1; + display: flex; + flex-direction: column; + position: relative; + + // SASSTODO: the visualizing component should have an option or a modifier + .series > rect { + fill-opacity: 0.5; + stroke-width: 1; + } + `; + const breakdownFieldSelectorGroupCss = css` + width: 100%; + `; + const breakdownFieldSelectorItemCss = css` + align-items: flex-end; + padding-left: ${euiTheme.size.s}; + `; + const chartToolButtonCss = css` + display: flex; + justify-content: center; + padding-left: ${euiTheme.size.s}; + `; + + return { + resultCountCss, + resultCountTitleCss, + resultCountToggleCss, + histogramCss, + breakdownFieldSelectorGroupCss, + breakdownFieldSelectorItemCss, + chartToolButtonCss, + }; +}; diff --git a/src/plugins/unified_histogram/public/chart/use_request_params.tsx b/src/plugins/unified_histogram/public/chart/use_request_params.tsx new file mode 100644 index 0000000000000..e132e0532280f --- /dev/null +++ b/src/plugins/unified_histogram/public/chart/use_request_params.tsx @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { connectToQueryState, QueryState } from '@kbn/data-plugin/public'; +import { createStateContainer, useContainerState } from '@kbn/kibana-utils-plugin/public'; +import { useEffect, useMemo } from 'react'; +import type { UnifiedHistogramServices } from '../types'; + +export const useRequestParams = (services: UnifiedHistogramServices) => { + const { data } = services; + + const queryStateContainer = useMemo(() => { + return createStateContainer({ + filters: data.query.filterManager.getFilters(), + query: data.query.queryString.getQuery(), + refreshInterval: data.query.timefilter.timefilter.getRefreshInterval(), + time: data.query.timefilter.timefilter.getTime(), + }); + }, [data.query.filterManager, data.query.queryString, data.query.timefilter.timefilter]); + + const queryState = useContainerState(queryStateContainer); + + useEffect(() => { + return connectToQueryState(data.query, queryStateContainer, { + time: true, + query: true, + filters: true, + refreshInterval: true, + }); + }, [data.query, queryStateContainer]); + + const filters = useMemo(() => queryState.filters ?? [], [queryState.filters]); + + const query = useMemo( + () => queryState.query ?? data.query.queryString.getDefaultQuery(), + [data.query.queryString, queryState.query] + ); + + const timeRange = useMemo( + () => data.query.timefilter.timefilter.getAbsoluteTime(), + // eslint-disable-next-line react-hooks/exhaustive-deps + [data.query.timefilter.timefilter, queryState.time] + ); + + return { filters, query, timeRange }; +}; diff --git a/src/plugins/unified_histogram/public/chart/use_total_hits.ts b/src/plugins/unified_histogram/public/chart/use_total_hits.ts new file mode 100644 index 0000000000000..5ec6f4768d9a5 --- /dev/null +++ b/src/plugins/unified_histogram/public/chart/use_total_hits.ts @@ -0,0 +1,207 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { isCompleteResponse } from '@kbn/data-plugin/public'; +import { DataView, DataViewType } from '@kbn/data-views-plugin/public'; +import type { AggregateQuery, Filter, Query, TimeRange } from '@kbn/es-query'; +import { i18n } from '@kbn/i18n'; +import { cloneDeep, isEqual } from 'lodash'; +import { MutableRefObject, useEffect, useRef } from 'react'; +import { filter, lastValueFrom, map } from 'rxjs'; +import type { + UnifiedHistogramFetchStatus, + UnifiedHistogramHitsContext, + UnifiedHistogramRequestContext, + UnifiedHistogramServices, +} from '../types'; + +export const useTotalHits = ({ + services, + request, + chartVisible, + hits, + dataView, + filters, + query, + timeRange, + onTotalHitsChange, +}: { + services: UnifiedHistogramServices; + request: UnifiedHistogramRequestContext | undefined; + chartVisible: boolean; + hits: UnifiedHistogramHitsContext | undefined; + dataView: DataView; + filters: Filter[]; + query: Query | AggregateQuery; + timeRange: TimeRange; + onTotalHitsChange?: (status: UnifiedHistogramFetchStatus, totalHits?: number) => void; +}) => { + const abortController = useRef(); + const totalHitsDeps = useRef>(); + + useEffect(() => { + const newTotalHitsDeps = getTotalHitsDeps({ + chartVisible, + request, + hits, + dataView, + filters, + query, + timeRange, + }); + + if (!isEqual(totalHitsDeps.current, newTotalHitsDeps)) { + totalHitsDeps.current = newTotalHitsDeps; + + fetchTotalHits({ + services, + abortController, + request, + chartVisible, + hits, + dataView, + filters, + query, + timeRange, + onTotalHitsChange, + }); + } + }, [ + chartVisible, + dataView, + filters, + hits, + onTotalHitsChange, + query, + request, + services, + timeRange, + ]); +}; + +const getTotalHitsDeps = ({ + chartVisible, + request, + hits, + dataView, + filters, + query, + timeRange, +}: { + chartVisible: boolean; + request: UnifiedHistogramRequestContext | undefined; + hits: UnifiedHistogramHitsContext | undefined; + dataView: DataView; + filters: Filter[]; + query: Query | AggregateQuery; + timeRange: TimeRange; +}) => + cloneDeep([ + chartVisible, + Boolean(hits), + dataView.id, + filters, + query, + timeRange, + request?.lastReloadRequestTime, + ]); + +const fetchTotalHits = async ({ + services: { data }, + abortController, + request, + chartVisible, + hits, + dataView, + filters: originalFilters, + query, + timeRange, + onTotalHitsChange, +}: { + services: UnifiedHistogramServices; + abortController: MutableRefObject; + request: UnifiedHistogramRequestContext | undefined; + chartVisible: boolean; + hits: UnifiedHistogramHitsContext | undefined; + dataView: DataView; + filters: Filter[]; + query: Query | AggregateQuery; + timeRange: TimeRange; + onTotalHitsChange?: (status: UnifiedHistogramFetchStatus, totalHits?: number) => void; +}) => { + abortController.current?.abort(); + abortController.current = undefined; + + // Either the chart is visible, in which case Lens will make the request, + // or there is no hits context, which means the total hits should be hidden + if (chartVisible || !hits) { + return; + } + + onTotalHitsChange?.('loading', hits.total); + + const searchSource = data.search.searchSource.createEmpty(); + + searchSource + .setField('index', dataView) + .setField('query', query) + .setField('size', 0) + .setField('trackTotalHits', true); + + let filters = originalFilters; + + if (dataView.type === DataViewType.ROLLUP) { + // We treat that data view as "normal" even if it was a rollup data view, + // since the rollup endpoint does not support querying individual documents, but we + // can get them from the regular _search API that will be used if the data view + // not a rollup data view. + searchSource.setOverwriteDataViewType(undefined); + } else { + // Set the date range filter fields from timeFilter using the absolute format. + // Search sessions requires that it be converted from a relative range + const timeFilter = data.query.timefilter.timefilter.createFilter(dataView, timeRange); + + if (timeFilter) { + filters = [...filters, timeFilter]; + } + } + + searchSource.setField('filter', filters); + + abortController.current = new AbortController(); + + const inspector = request?.adapter + ? { + adapter: request.adapter, + title: i18n.translate('unifiedHistogram.inspectorRequestDataTitleTotalHits', { + defaultMessage: 'Total hits', + }), + description: i18n.translate('unifiedHistogram.inspectorRequestDescriptionTotalHits', { + defaultMessage: 'This request queries Elasticsearch to fetch the total hits.', + }), + } + : undefined; + + const fetch$ = searchSource + .fetch$({ + inspector, + sessionId: request?.searchSessionId, + abortSignal: abortController.current.signal, + executionContext: { + description: 'fetch total hits', + }, + }) + .pipe( + filter((res) => isCompleteResponse(res)), + map((res) => res.rawResponse.hits.total as number) + ); + + const totalHits = await lastValueFrom(fetch$); + + onTotalHitsChange?.('complete', totalHits); +}; diff --git a/src/plugins/unified_histogram/public/hits_counter/hits_counter.tsx b/src/plugins/unified_histogram/public/hits_counter/hits_counter.tsx index e6296429576a8..39df40650557c 100644 --- a/src/plugins/unified_histogram/public/hits_counter/hits_counter.tsx +++ b/src/plugins/unified_histogram/public/hits_counter/hits_counter.tsx @@ -16,24 +16,21 @@ import type { UnifiedHistogramHitsContext } from '../types'; export interface HitsCounterProps { hits: UnifiedHistogramHitsContext; - totalHits?: number; append?: ReactElement; } -export function HitsCounter({ hits, totalHits, append }: HitsCounterProps) { - if (!hits.total && hits.status === 'loading' && !totalHits) { +export function HitsCounter({ hits, append }: HitsCounterProps) { + if (!hits.total && hits.status === 'loading') { return null; } const formattedHits = ( - + ); @@ -51,23 +48,23 @@ export function HitsCounter({ hits, totalHits, append }: HitsCounterProps) { > - {hits.status === 'partial' && !totalHits && ( + {hits.status === 'partial' && ( )} - {(hits.status !== 'partial' || totalHits) && ( + {hits.status !== 'partial' && ( )} - {hits.status === 'partial' && !totalHits && ( + {hits.status === 'partial' && ( { className?: string; services: UnifiedHistogramServices; dataView: DataView; + /** + * Context object for requests made by unified histogram components -- optional + */ + request?: UnifiedHistogramRequestContext; /** * Context object for the hits count -- leave undefined to hide the hits count */ @@ -69,12 +75,18 @@ export interface UnifiedHistogramLayoutProps extends PropsWithChildren * Callback to update the breakdown field -- should set {@link UnifiedHistogramBreakdownContext.field} to breakdownField */ onBreakdownFieldChange?: (breakdownField: DataViewField | undefined) => void; + /** + * Callback to update the total hits -- should set {@link UnifiedHistogramHitsContext.status} to status + * and {@link UnifiedHistogramHitsContext.total} to totalHits + */ + onTotalHitsChange?: (status: UnifiedHistogramFetchStatus, totalHits?: number) => void; } export const UnifiedHistogramLayout = ({ className, services, dataView, + request, hits, chart, breakdown, @@ -86,6 +98,7 @@ export const UnifiedHistogramLayout = ({ onChartHiddenChange, onTimeIntervalChange, onBreakdownFieldChange, + onTotalHitsChange, children, }: UnifiedHistogramLayoutProps) => { const topPanelNode = useMemo( @@ -134,6 +147,7 @@ export const UnifiedHistogramLayout = ({ className={chartClassName} services={services} dataView={dataView} + request={request} hits={hits} chart={chart} breakdown={breakdown} @@ -144,6 +158,7 @@ export const UnifiedHistogramLayout = ({ onChartHiddenChange={onChartHiddenChange} onTimeIntervalChange={onTimeIntervalChange} onBreakdownFieldChange={onBreakdownFieldChange} + onTotalHitsChange={onTotalHitsChange} /> {children} diff --git a/src/plugins/unified_histogram/public/types.ts b/src/plugins/unified_histogram/public/types.ts index 275ed0de96cf3..1c213f8bd8e49 100644 --- a/src/plugins/unified_histogram/public/types.ts +++ b/src/plugins/unified_histogram/public/types.ts @@ -12,6 +12,7 @@ import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; import type { LensPublicStart } from '@kbn/lens-plugin/public'; import type { DataViewField } from '@kbn/data-views-plugin/public'; +import type { RequestAdapter } from '@kbn/inspector-plugin/public'; /** * The fetch status of a unified histogram request @@ -44,6 +45,24 @@ export interface UnifiedHistogramBucketInterval { scale?: number; } +/** + * Context object for requests made by unified histogram components + */ +export interface UnifiedHistogramRequestContext { + /** + * Current search session ID + */ + searchSessionId?: string; + /** + * The adapter to use for requests + */ + adapter?: RequestAdapter; + /** + * Can be updated to `Date.now()` to force a refresh + */ + lastReloadRequestTime?: number; +} + /** * Context object for the hits count */ diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index faacde782d509..57fc3ad319fe7 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -2221,10 +2221,8 @@ "discover.helpMenu.appName": "Découverte", "discover.inspectorRequestDataTitleChart": "Données du graphique", "discover.inspectorRequestDataTitleDocuments": "Documents", - "discover.inspectorRequestDataTitleTotalHits": "Nombre total de résultats", "discover.inspectorRequestDescriptionChart": "Cette requête interroge Elasticsearch afin de récupérer les données d'agrégation pour le graphique.", "discover.inspectorRequestDescriptionDocument": "Cette requête interroge Elasticsearch afin de récupérer les documents.", - "discover.inspectorRequestDescriptionTotalHits": "Cette requête interroge Elasticsearch afin de récupérer le nombre total de résultats.", "discover.json.codeEditorAriaLabel": "Affichage JSON en lecture seule d’un document Elasticsearch", "discover.json.copyToClipboardLabel": "Copier dans le presse-papiers", "discover.loadingDocuments": "Chargement des documents", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 59e637a55f71c..3e0425fcb222b 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -2217,10 +2217,8 @@ "discover.helpMenu.appName": "Discover", "discover.inspectorRequestDataTitleChart": "グラフデータ", "discover.inspectorRequestDataTitleDocuments": "ドキュメント", - "discover.inspectorRequestDataTitleTotalHits": "総ヒット数", "discover.inspectorRequestDescriptionChart": "このリクエストはElasticsearchにクエリをかけ、グラフの集計データを取得します。", "discover.inspectorRequestDescriptionDocument": "このリクエストはElasticsearchにクエリをかけ、ドキュメントを取得します。", - "discover.inspectorRequestDescriptionTotalHits": "このリクエストはElasticsearchにクエリをかけ、合計一致数を取得します。", "discover.json.codeEditorAriaLabel": "Elasticsearch ドキュメントの JSON ビューのみを読み込む", "discover.json.copyToClipboardLabel": "クリップボードにコピー", "discover.loadingDocuments": "ドキュメントを読み込み中", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 921639d7cdab4..4ca62119353f0 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -2221,10 +2221,8 @@ "discover.helpMenu.appName": "Discover", "discover.inspectorRequestDataTitleChart": "图表数据", "discover.inspectorRequestDataTitleDocuments": "文档", - "discover.inspectorRequestDataTitleTotalHits": "总命中数", "discover.inspectorRequestDescriptionChart": "此请求将查询 Elasticsearch 以获取图表的聚合数据。", "discover.inspectorRequestDescriptionDocument": "此请求将查询 Elasticsearch 以获取文档。", - "discover.inspectorRequestDescriptionTotalHits": "此请求将查询 Elasticsearch 以获取总命中数。", "discover.json.codeEditorAriaLabel": "Elasticsearch 文档的只读 JSON 视图", "discover.json.copyToClipboardLabel": "复制到剪贴板", "discover.loadingDocuments": "正在加载文档",