mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[Discover] [ES|QL] Prevent redundant requests when loading Discover sessions and toggling chart visibility (#206699)
## Summary This PR prevents redundant Discover requests in ES|QL mode for the following scenarios: - Creating a new Discover session. - Saving the current Discover session. - Loading a saved Discover session. - Toggling the Unified Histogram chart visibility. It does so by addressing several underlying state related issues that were triggering the redundant requests: - Skipping the initial emission of `currentSuggestionContext` on Unified Histogram mount, which immediately triggered a second fetch. - Treating the Unified Histogram `table` prop the same as other props which affect Lens suggestions (data view, query, columns), and deferring updates to it until result fetching completes to avoid unnecessary suggestion updates. - Removing all auto-fetching behaviour from Unified Histogram and instead relying solely on the consumer to control when fetching should occur (including the initial fetch). Resolves #165192. ### Checklist - [ ] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/src/platform/packages/shared/kbn-i18n/README.md) - [ ] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [ ] If a plugin configuration key changed, check if it needs to be allowlisted in the cloud and added to the [docker list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker) - [ ] This was checked for breaking HTTP API changes, and any breaking changes have been approved by the breaking-change committee. The `release_note:breaking` label should be applied in these situations. - [x] [Flaky Test Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was used on any tests changed - [x] The PR description includes the appropriate Release Notes section, and the correct `release_note:*` label is applied per the [guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) --------- Co-authored-by: Matthias Wilhelm <matthias.wilhelm@elastic.co>
This commit is contained in:
parent
075806bffa
commit
1c7a823920
22 changed files with 387 additions and 676 deletions
|
@ -7,16 +7,13 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import React, { useCallback } from 'react';
|
||||
import { UnifiedHistogramContainer } from '@kbn/unified-histogram-plugin/public';
|
||||
import { css } from '@emotion/react';
|
||||
import useObservable from 'react-use/lib/useObservable';
|
||||
import { ESQL_TABLE_TYPE } from '@kbn/data-plugin/common';
|
||||
import type { Datatable } from '@kbn/expressions-plugin/common';
|
||||
import { useDiscoverHistogram } from './use_discover_histogram';
|
||||
import { type DiscoverMainContentProps, DiscoverMainContent } from './discover_main_content';
|
||||
import { useAppStateSelector } from '../../state_management/discover_app_state_container';
|
||||
import { FetchStatus } from '../../../types';
|
||||
import { useIsEsqlMode } from '../../hooks/use_is_esql_mode';
|
||||
|
||||
export interface DiscoverHistogramLayoutProps extends DiscoverMainContentProps {
|
||||
|
@ -44,7 +41,6 @@ export const DiscoverHistogramLayout = ({
|
|||
hideChart,
|
||||
});
|
||||
|
||||
const datatable = useObservable(dataState.data$.documents$);
|
||||
const renderCustomChartToggleActions = useCallback(
|
||||
() =>
|
||||
React.isValidElement(panelsToggle)
|
||||
|
@ -53,23 +49,6 @@ export const DiscoverHistogramLayout = ({
|
|||
[panelsToggle]
|
||||
);
|
||||
|
||||
const table: Datatable | undefined = useMemo(() => {
|
||||
if (
|
||||
isEsqlMode &&
|
||||
datatable &&
|
||||
[FetchStatus.PARTIAL, FetchStatus.COMPLETE].includes(datatable.fetchStatus)
|
||||
) {
|
||||
return {
|
||||
type: 'datatable' as 'datatable',
|
||||
rows: datatable.result!.map((r) => r.raw),
|
||||
columns: datatable.esqlQueryColumns || [],
|
||||
meta: {
|
||||
type: ESQL_TABLE_TYPE,
|
||||
},
|
||||
};
|
||||
}
|
||||
}, [datatable, isEsqlMode]);
|
||||
|
||||
// Initialized when the first search has been requested or
|
||||
// when in ES|QL mode since search sessions are not supported
|
||||
if (!searchSessionId && !isEsqlMode) {
|
||||
|
@ -81,7 +60,6 @@ export const DiscoverHistogramLayout = ({
|
|||
{...unifiedHistogramProps}
|
||||
searchSessionId={searchSessionId}
|
||||
requestAdapter={dataState.inspectorAdapters.requests}
|
||||
table={table}
|
||||
container={container}
|
||||
css={histogramLayoutCss}
|
||||
renderCustomChartToggleActions={renderCustomChartToggleActions}
|
||||
|
|
|
@ -161,7 +161,6 @@ describe('useDiscoverHistogram', () => {
|
|||
const { hook } = await renderUseDiscoverHistogram();
|
||||
const params = hook.result.current.getCreationOptions();
|
||||
expect(params?.localStorageKeyPrefix).toBe('discover');
|
||||
expect(params?.disableAutoFetching).toBe(true);
|
||||
expect(Object.keys(params?.initialState ?? {})).toEqual([
|
||||
'chartHidden',
|
||||
'timeInterval',
|
||||
|
@ -398,8 +397,8 @@ describe('useDiscoverHistogram', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('refetching', () => {
|
||||
it('should call refetch when savedSearchFetch$ is triggered', async () => {
|
||||
describe('fetching', () => {
|
||||
it('should call fetch when savedSearchFetch$ is triggered', async () => {
|
||||
const savedSearchFetch$ = new Subject<void>();
|
||||
const stateContainer = getStateContainer();
|
||||
stateContainer.dataState.fetchChart$ = savedSearchFetch$;
|
||||
|
@ -408,33 +407,11 @@ describe('useDiscoverHistogram', () => {
|
|||
act(() => {
|
||||
hook.result.current.ref(api);
|
||||
});
|
||||
expect(api.refetch).toHaveBeenCalled();
|
||||
expect(api.fetch).toHaveBeenCalled();
|
||||
act(() => {
|
||||
savedSearchFetch$.next();
|
||||
});
|
||||
expect(api.refetch).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('should skip the next refetch when hideChart changes from true to false', async () => {
|
||||
const savedSearchFetch$ = new Subject<void>();
|
||||
const stateContainer = getStateContainer();
|
||||
stateContainer.dataState.fetchChart$ = savedSearchFetch$;
|
||||
const { hook, initialProps } = await renderUseDiscoverHistogram({ stateContainer });
|
||||
const api = createMockUnifiedHistogramApi();
|
||||
act(() => {
|
||||
hook.result.current.ref(api);
|
||||
});
|
||||
expect(api.refetch).toHaveBeenCalled();
|
||||
act(() => {
|
||||
hook.rerender({ ...initialProps, hideChart: true });
|
||||
});
|
||||
act(() => {
|
||||
hook.rerender({ ...initialProps, hideChart: false });
|
||||
});
|
||||
act(() => {
|
||||
savedSearchFetch$.next();
|
||||
});
|
||||
expect(api.refetch).toHaveBeenCalledTimes(1);
|
||||
expect(api.fetch).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@ import {
|
|||
UnifiedHistogramVisContext,
|
||||
} from '@kbn/unified-histogram-plugin/public';
|
||||
import { isEqual } from 'lodash';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import {
|
||||
debounceTime,
|
||||
distinctUntilChanged,
|
||||
|
@ -28,13 +28,15 @@ import {
|
|||
merge,
|
||||
Observable,
|
||||
pairwise,
|
||||
skip,
|
||||
startWith,
|
||||
} from 'rxjs';
|
||||
import useObservable from 'react-use/lib/useObservable';
|
||||
import type { RequestAdapter } from '@kbn/inspector-plugin/common';
|
||||
import type { DatatableColumn } from '@kbn/expressions-plugin/common';
|
||||
import type { Datatable, DatatableColumn } from '@kbn/expressions-plugin/common';
|
||||
import type { SavedSearch } from '@kbn/saved-search-plugin/common';
|
||||
import { Filter } from '@kbn/es-query';
|
||||
import { Filter, isOfAggregateQueryType } from '@kbn/es-query';
|
||||
import { ESQL_TABLE_TYPE } from '@kbn/data-plugin/common';
|
||||
import { useDiscoverCustomization } from '../../../../customizations';
|
||||
import { useDiscoverServices } from '../../../../hooks/use_discover_services';
|
||||
import { FetchStatus } from '../../../types';
|
||||
|
@ -240,6 +242,7 @@ export const useDiscoverHistogram = ({
|
|||
dataView: esqlDataView,
|
||||
query: esqlQuery,
|
||||
columns: esqlColumns,
|
||||
table,
|
||||
} = useObservable(esqlFetchComplete$, initialEsqlProps);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -249,9 +252,7 @@ export const useDiscoverHistogram = ({
|
|||
}
|
||||
|
||||
const fetchStart = stateContainer.dataState.fetchChart$.subscribe(() => {
|
||||
if (!skipRefetch.current) {
|
||||
setIsSuggestionLoading(true);
|
||||
}
|
||||
setIsSuggestionLoading(true);
|
||||
});
|
||||
const fetchComplete = esqlFetchComplete$.subscribe(() => {
|
||||
setIsSuggestionLoading(false);
|
||||
|
@ -267,18 +268,6 @@ export const useDiscoverHistogram = ({
|
|||
* Data fetching
|
||||
*/
|
||||
|
||||
const skipRefetch = useRef<boolean>();
|
||||
|
||||
// Skip refetching when showing the chart since Lens will
|
||||
// automatically fetch when the chart is shown
|
||||
useEffect(() => {
|
||||
if (skipRefetch.current === undefined) {
|
||||
skipRefetch.current = false;
|
||||
} else {
|
||||
skipRefetch.current = !hideChart;
|
||||
}
|
||||
}, [hideChart]);
|
||||
|
||||
// Handle unified histogram refetching
|
||||
useEffect(() => {
|
||||
if (!unifiedHistogram) {
|
||||
|
@ -304,18 +293,14 @@ export const useDiscoverHistogram = ({
|
|||
}
|
||||
|
||||
const subscription = fetchChart$.subscribe((source) => {
|
||||
if (!skipRefetch.current) {
|
||||
if (source === 'discover') addLog('Unified Histogram - Discover refetch');
|
||||
if (source === 'lens') addLog('Unified Histogram - Lens suggestion refetch');
|
||||
unifiedHistogram.refetch();
|
||||
}
|
||||
|
||||
skipRefetch.current = false;
|
||||
if (source === 'discover') addLog('Unified Histogram - Discover refetch');
|
||||
if (source === 'lens') addLog('Unified Histogram - Lens suggestion refetch');
|
||||
unifiedHistogram.fetch();
|
||||
});
|
||||
|
||||
// triggering the initial request for total hits hook
|
||||
if (!isEsqlMode && !skipRefetch.current) {
|
||||
unifiedHistogram.refetch();
|
||||
// triggering the initial chart request
|
||||
if (!isEsqlMode) {
|
||||
unifiedHistogram.fetch();
|
||||
}
|
||||
|
||||
return () => {
|
||||
|
@ -397,6 +382,7 @@ export const useDiscoverHistogram = ({
|
|||
timeRange: timeRangeMemoized,
|
||||
relativeTimeRange,
|
||||
columns: isEsqlMode ? esqlColumns : undefined,
|
||||
table: isEsqlMode ? table : undefined,
|
||||
onFilter: histogramCustomization?.onFilter,
|
||||
onBrushEnd: histogramCustomization?.onBrushEnd,
|
||||
withDefaultActions: histogramCustomization?.withDefaultActions,
|
||||
|
@ -495,7 +481,10 @@ const createTotalHitsObservable = (state$?: Observable<UnifiedHistogramState>) =
|
|||
const createCurrentSuggestionObservable = (state$: Observable<UnifiedHistogramState>) => {
|
||||
return state$.pipe(
|
||||
map((state) => state.currentSuggestionContext),
|
||||
distinctUntilChanged(isEqual)
|
||||
distinctUntilChanged(isEqual),
|
||||
// Skip the first emission since it's the
|
||||
// initial state and doesn't need a refetch
|
||||
skip(1)
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -507,11 +496,23 @@ function getUnifiedHistogramPropsForEsql({
|
|||
savedSearch: SavedSearch;
|
||||
}) {
|
||||
const columns = documentsValue?.esqlQueryColumns || EMPTY_ESQL_COLUMNS;
|
||||
const query = savedSearch.searchSource.getField('query');
|
||||
const isEsqlMode = isOfAggregateQueryType(query);
|
||||
const table: Datatable | undefined =
|
||||
isEsqlMode && documentsValue?.result
|
||||
? {
|
||||
type: 'datatable',
|
||||
rows: documentsValue.result.map((r) => r.raw),
|
||||
columns,
|
||||
meta: { type: ESQL_TABLE_TYPE },
|
||||
}
|
||||
: undefined;
|
||||
|
||||
const nextProps = {
|
||||
dataView: savedSearch.searchSource.getField('index')!,
|
||||
query: savedSearch.searchSource.getField('query'),
|
||||
columns,
|
||||
table,
|
||||
};
|
||||
|
||||
addLog('[UnifiedHistogram] delayed next props for ES|QL', nextProps);
|
||||
|
|
|
@ -7,30 +7,15 @@ It manages its own state and data fetching, and can easily be dropped into pages
|
|||
|
||||
```tsx
|
||||
// Import the container component
|
||||
import {
|
||||
UnifiedHistogramContainer,
|
||||
} from '@kbn/unified-histogram-plugin/public';
|
||||
import { UnifiedHistogramContainer } from '@kbn/unified-histogram-plugin/public';
|
||||
|
||||
// Import modules required for your application
|
||||
import {
|
||||
useServices,
|
||||
useResizeRef,
|
||||
useRequestParams,
|
||||
MyLayout,
|
||||
MyButton,
|
||||
} from './my-modules';
|
||||
import { useServices, useResizeRef, useRequestParams, MyLayout, MyButton } from './my-modules';
|
||||
|
||||
const services = useServices();
|
||||
const resizeRef = useResizeRef();
|
||||
const {
|
||||
dataView,
|
||||
query,
|
||||
filters,
|
||||
timeRange,
|
||||
relativeTimeRange,
|
||||
searchSessionId,
|
||||
requestAdapter,
|
||||
} = useRequestParams();
|
||||
const { dataView, query, filters, timeRange, relativeTimeRange, searchSessionId, requestAdapter } =
|
||||
useRequestParams();
|
||||
|
||||
return (
|
||||
<UnifiedHistogramContainer
|
||||
|
@ -71,7 +56,7 @@ import {
|
|||
useCallbacks,
|
||||
useRequestParams,
|
||||
useStateParams,
|
||||
useManualRefetch,
|
||||
useFetch,
|
||||
MyLayout,
|
||||
MyButton,
|
||||
} from './my-modules';
|
||||
|
@ -97,20 +82,13 @@ const getCreationOptions = useCallback(() => ({
|
|||
// Optionally provide a local storage key prefix to save parts of the state,
|
||||
// such as the chart hidden state and top panel height, to local storage
|
||||
localStorageKeyPrefix: 'myApp',
|
||||
// By default Unified Histogram will automatically refetch based on certain
|
||||
// state changes, such as chart hidden and request params, but this can be
|
||||
// disabled in favour of manual fetching if preferred. Note that an initial
|
||||
// request is always triggered when first initialized, and when the chart
|
||||
// changes from hidden to visible, Lens will automatically trigger a refetch
|
||||
// regardless of what this property is set to
|
||||
disableAutoFetching: true,
|
||||
// Customize the initial state in order to override the defaults
|
||||
initialState: { chartHidden, breakdownField },
|
||||
}), [...]);
|
||||
|
||||
// Manually refetch if disableAutoFetching is true
|
||||
useManualRefetch(() => {
|
||||
unifiedHistogram?.refetch();
|
||||
// Trigger a fetch, must be called on init to render the chart
|
||||
useFetch(() => {
|
||||
unifiedHistogram?.fetch();
|
||||
});
|
||||
|
||||
// Update the Unified Histogram state when our state params change
|
||||
|
|
|
@ -19,7 +19,7 @@ import type { ReactWrapper } from 'enzyme';
|
|||
import { unifiedHistogramServicesMock } from '../__mocks__/services';
|
||||
import { getLensVisMock } from '../__mocks__/lens_vis';
|
||||
import { searchSourceInstanceMock } from '@kbn/data-plugin/common/search/search_source/mocks';
|
||||
import { of } from 'rxjs';
|
||||
import { Subject, of } from 'rxjs';
|
||||
import { dataViewWithTimefieldMock } from '../__mocks__/data_view_with_timefield';
|
||||
import { dataViewMock } from '../__mocks__/data_view';
|
||||
import { BreakdownFieldSelector } from './breakdown_field_selector';
|
||||
|
@ -135,6 +135,7 @@ async function mountComponent({
|
|||
withDefaultActions: undefined,
|
||||
isChartAvailable: checkChartAvailability({ chart, dataView, isPlainRecord }),
|
||||
renderCustomChartToggleActions: customToggle ? () => customToggle : undefined,
|
||||
input$: new Subject(),
|
||||
};
|
||||
|
||||
let instance: ReactWrapper = {} as ReactWrapper;
|
||||
|
@ -142,6 +143,7 @@ async function mountComponent({
|
|||
instance = mountWithIntl(<Chart {...props} />);
|
||||
// wait for initial async loading to complete
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
props.input$?.next({ type: 'fetch' });
|
||||
instance.update();
|
||||
});
|
||||
return instance;
|
||||
|
|
|
@ -18,11 +18,19 @@ import type {
|
|||
LensEmbeddableInput,
|
||||
LensEmbeddableOutput,
|
||||
} from '@kbn/lens-plugin/public';
|
||||
import type { DatatableColumn } from '@kbn/expressions-plugin/common';
|
||||
import type {
|
||||
Datatable,
|
||||
DatatableColumn,
|
||||
DefaultInspectorAdapters,
|
||||
} from '@kbn/expressions-plugin/common';
|
||||
import type { DataView, DataViewField } from '@kbn/data-views-plugin/public';
|
||||
import type { TimeRange } from '@kbn/es-query';
|
||||
import { PublishingSubject } from '@kbn/presentation-publishing';
|
||||
import { RequestStatus } from '@kbn/inspector-plugin/public';
|
||||
import { IKibanaSearchResponse } from '@kbn/search-types';
|
||||
import { estypes } from '@elastic/elasticsearch';
|
||||
import { Histogram } from './histogram';
|
||||
import type {
|
||||
import {
|
||||
UnifiedHistogramSuggestionContext,
|
||||
UnifiedHistogramBreakdownContext,
|
||||
UnifiedHistogramChartContext,
|
||||
|
@ -33,6 +41,7 @@ import type {
|
|||
UnifiedHistogramInputMessage,
|
||||
UnifiedHistogramRequestContext,
|
||||
UnifiedHistogramServices,
|
||||
UnifiedHistogramBucketInterval,
|
||||
} from '../types';
|
||||
import { UnifiedHistogramSuggestionType } from '../types';
|
||||
import { BreakdownFieldSelector } from './breakdown_field_selector';
|
||||
|
@ -41,11 +50,14 @@ import { useTotalHits } from './hooks/use_total_hits';
|
|||
import { useChartStyles } from './hooks/use_chart_styles';
|
||||
import { useChartActions } from './hooks/use_chart_actions';
|
||||
import { ChartConfigPanel } from './chart_config_panel';
|
||||
import { useRefetch } from './hooks/use_refetch';
|
||||
import { useFetch } from './hooks/use_fetch';
|
||||
import { useEditVisualization } from './hooks/use_edit_visualization';
|
||||
import { LensVisService } from '../services/lens_vis_service';
|
||||
import type { UseRequestParamsResult } from '../hooks/use_request_params';
|
||||
import { removeTablesFromLensAttributes } from '../utils/lens_vis_from_table';
|
||||
import { useLensProps } from './hooks/use_lens_props';
|
||||
import { useStableCallback } from '../hooks/use_stable_callback';
|
||||
import { buildBucketInterval } from './utils/build_bucket_interval';
|
||||
|
||||
export interface ChartProps {
|
||||
abortController?: AbortController;
|
||||
|
@ -64,7 +76,6 @@ export interface ChartProps {
|
|||
breakdown?: UnifiedHistogramBreakdownContext;
|
||||
renderCustomChartToggleActions?: () => ReactElement | undefined;
|
||||
appendHistogram?: ReactElement;
|
||||
disableAutoFetching?: boolean;
|
||||
disableTriggers?: LensEmbeddableInput['disableTriggers'];
|
||||
disabledActions?: LensEmbeddableInput['disabledActions'];
|
||||
input$?: UnifiedHistogramInput$;
|
||||
|
@ -99,7 +110,6 @@ export function Chart({
|
|||
isPlainRecord,
|
||||
renderCustomChartToggleActions,
|
||||
appendHistogram,
|
||||
disableAutoFetching,
|
||||
disableTriggers,
|
||||
disabledActions,
|
||||
input$: originalInput$,
|
||||
|
@ -140,20 +150,9 @@ export function Chart({
|
|||
|
||||
const { filters, query, getTimeRange, updateTimeRange, relativeTimeRange } = requestParams;
|
||||
|
||||
const refetch$ = useRefetch({
|
||||
dataView,
|
||||
request,
|
||||
hits,
|
||||
chart,
|
||||
chartVisible,
|
||||
breakdown,
|
||||
filters,
|
||||
query,
|
||||
relativeTimeRange,
|
||||
currentSuggestion,
|
||||
disableAutoFetching,
|
||||
const fetch$ = useFetch({
|
||||
input$,
|
||||
beforeRefetch: updateTimeRange,
|
||||
beforeFetch: updateTimeRange,
|
||||
});
|
||||
|
||||
useTotalHits({
|
||||
|
@ -165,11 +164,67 @@ export function Chart({
|
|||
filters,
|
||||
query,
|
||||
getTimeRange,
|
||||
refetch$,
|
||||
fetch$,
|
||||
onTotalHitsChange,
|
||||
isPlainRecord,
|
||||
});
|
||||
|
||||
const [bucketInterval, setBucketInterval] = useState<UnifiedHistogramBucketInterval>();
|
||||
const onLoad = useStableCallback(
|
||||
(
|
||||
isLoading: boolean,
|
||||
adapters: Partial<DefaultInspectorAdapters> | undefined,
|
||||
dataLoadingSubject$?: PublishingSubject<boolean | undefined>
|
||||
) => {
|
||||
const lensRequest = adapters?.requests?.getRequests()[0];
|
||||
const requestFailed = lensRequest?.status === RequestStatus.ERROR;
|
||||
const json = lensRequest?.response?.json as
|
||||
| IKibanaSearchResponse<estypes.SearchResponse>
|
||||
| undefined;
|
||||
const response = json?.rawResponse;
|
||||
|
||||
if (requestFailed) {
|
||||
onTotalHitsChange?.(UnifiedHistogramFetchStatus.error, undefined);
|
||||
onChartLoad?.({ adapters: adapters ?? {} });
|
||||
return;
|
||||
}
|
||||
|
||||
const adapterTables = adapters?.tables?.tables;
|
||||
const totalHits = computeTotalHits(hasLensSuggestions, adapterTables, isPlainRecord);
|
||||
|
||||
if (response?._shards?.failed || response?.timed_out) {
|
||||
onTotalHitsChange?.(UnifiedHistogramFetchStatus.error, totalHits);
|
||||
} else {
|
||||
onTotalHitsChange?.(
|
||||
isLoading ? UnifiedHistogramFetchStatus.loading : UnifiedHistogramFetchStatus.complete,
|
||||
totalHits ?? hits?.total
|
||||
);
|
||||
}
|
||||
|
||||
if (response) {
|
||||
const newBucketInterval = buildBucketInterval({
|
||||
data: services.data,
|
||||
dataView,
|
||||
timeInterval: chart?.timeInterval,
|
||||
timeRange: getTimeRange(),
|
||||
response,
|
||||
});
|
||||
|
||||
setBucketInterval(newBucketInterval);
|
||||
}
|
||||
|
||||
onChartLoad?.({ adapters: adapters ?? {}, dataLoading$: dataLoadingSubject$ });
|
||||
}
|
||||
);
|
||||
|
||||
const lensPropsContext = useLensProps({
|
||||
request,
|
||||
getTimeRange,
|
||||
fetch$,
|
||||
visContext,
|
||||
onLoad,
|
||||
});
|
||||
|
||||
const { chartToolbarCss, histogramCss } = useChartStyles(chartVisible);
|
||||
|
||||
const onSuggestionContextEdit = useCallback(
|
||||
|
@ -356,26 +411,24 @@ export function Chart({
|
|||
<EuiProgress size="xs" color="accent" position="absolute" />
|
||||
</EuiDelayRender>
|
||||
)}
|
||||
<HistogramMemoized
|
||||
abortController={abortController}
|
||||
services={services}
|
||||
dataView={dataView}
|
||||
request={request}
|
||||
hits={hits}
|
||||
chart={chart}
|
||||
getTimeRange={getTimeRange}
|
||||
refetch$={refetch$}
|
||||
visContext={visContext}
|
||||
isPlainRecord={isPlainRecord}
|
||||
disableTriggers={disableTriggers}
|
||||
disabledActions={disabledActions}
|
||||
onTotalHitsChange={onTotalHitsChange}
|
||||
hasLensSuggestions={hasLensSuggestions}
|
||||
onChartLoad={onChartLoad}
|
||||
onFilter={onFilter}
|
||||
onBrushEnd={onBrushEnd}
|
||||
withDefaultActions={withDefaultActions}
|
||||
/>
|
||||
{lensPropsContext && (
|
||||
<HistogramMemoized
|
||||
abortController={abortController}
|
||||
services={services}
|
||||
dataView={dataView}
|
||||
chart={chart}
|
||||
bucketInterval={bucketInterval}
|
||||
getTimeRange={getTimeRange}
|
||||
visContext={visContext}
|
||||
isPlainRecord={isPlainRecord}
|
||||
disableTriggers={disableTriggers}
|
||||
disabledActions={disabledActions}
|
||||
onFilter={onFilter}
|
||||
onBrushEnd={onBrushEnd}
|
||||
withDefaultActions={withDefaultActions}
|
||||
{...lensPropsContext}
|
||||
/>
|
||||
)}
|
||||
</section>
|
||||
{appendHistogram}
|
||||
</EuiFlexItem>
|
||||
|
@ -405,3 +458,30 @@ export function Chart({
|
|||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
|
||||
const computeTotalHits = (
|
||||
hasLensSuggestions: boolean,
|
||||
adapterTables:
|
||||
| {
|
||||
[key: string]: Datatable;
|
||||
}
|
||||
| undefined,
|
||||
isPlainRecord?: boolean
|
||||
) => {
|
||||
if (isPlainRecord && hasLensSuggestions) {
|
||||
return Object.values(adapterTables ?? {})?.[0]?.rows?.length;
|
||||
} else if (isPlainRecord && !hasLensSuggestions) {
|
||||
// ES|QL histogram case
|
||||
const rows = Object.values(adapterTables ?? {})?.[0]?.rows;
|
||||
if (!rows) {
|
||||
return undefined;
|
||||
}
|
||||
let rowsCount = 0;
|
||||
rows.forEach((r) => {
|
||||
rowsCount += r.results;
|
||||
});
|
||||
return rowsCount;
|
||||
} else {
|
||||
return adapterTables?.unifiedHistogram?.meta?.statistics?.totalCount;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -8,23 +8,17 @@
|
|||
*/
|
||||
|
||||
import { mountWithIntl } from '@kbn/test-jest-helpers';
|
||||
import { Histogram } from './histogram';
|
||||
import { Histogram, HistogramProps } from './histogram';
|
||||
import React from 'react';
|
||||
import { BehaviorSubject, Subject } from 'rxjs';
|
||||
import { unifiedHistogramServicesMock } from '../__mocks__/services';
|
||||
import { getLensVisMock } from '../__mocks__/lens_vis';
|
||||
import { dataViewWithTimefieldMock } from '../__mocks__/data_view_with_timefield';
|
||||
import { createDefaultInspectorAdapters } from '@kbn/expressions-plugin/common';
|
||||
import { UnifiedHistogramFetchStatus, UnifiedHistogramInput$ } from '../types';
|
||||
import { UnifiedHistogramInput$ } from '../types';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import * as buildBucketInterval from './utils/build_bucket_interval';
|
||||
import * as useTimeRange from './hooks/use_time_range';
|
||||
import { RequestStatus } from '@kbn/inspector-plugin/public';
|
||||
import { getLensProps } from './hooks/use_lens_props';
|
||||
|
||||
const mockBucketInterval = { description: '1 minute', scale: undefined, scaled: false };
|
||||
jest.spyOn(buildBucketInterval, 'buildBucketInterval').mockReturnValue(mockBucketInterval);
|
||||
jest.spyOn(useTimeRange, 'useTimeRange');
|
||||
import { getLensProps, useLensProps } from './hooks/use_lens_props';
|
||||
|
||||
const getMockLensAttributes = async () => {
|
||||
const query = {
|
||||
|
@ -44,46 +38,48 @@ const getMockLensAttributes = async () => {
|
|||
).visContext;
|
||||
};
|
||||
|
||||
type CombinedProps = Omit<HistogramProps, 'requestData' | 'lensProps'> &
|
||||
Parameters<typeof useLensProps>[0];
|
||||
|
||||
async function mountComponent(isPlainRecord = false, hasLensSuggestions = false) {
|
||||
const services = unifiedHistogramServicesMock;
|
||||
services.data.query.timefilter.timefilter.getAbsoluteTime = () => {
|
||||
return { from: '2020-05-14T11:05:13.590', to: '2020-05-14T11:20:13.590' };
|
||||
};
|
||||
|
||||
const timefilterUpdateHandler = jest.fn();
|
||||
const refetch$: UnifiedHistogramInput$ = new Subject();
|
||||
const props = {
|
||||
const fetch$: UnifiedHistogramInput$ = new Subject();
|
||||
const props: CombinedProps = {
|
||||
services: unifiedHistogramServicesMock,
|
||||
request: {
|
||||
searchSessionId: '123',
|
||||
},
|
||||
hasLensSuggestions,
|
||||
isPlainRecord,
|
||||
hits: {
|
||||
status: UnifiedHistogramFetchStatus.loading,
|
||||
total: undefined,
|
||||
},
|
||||
chart: {
|
||||
hidden: false,
|
||||
timeInterval: 'auto',
|
||||
},
|
||||
timefilterUpdateHandler,
|
||||
dataView: dataViewWithTimefieldMock,
|
||||
getTimeRange: () => ({
|
||||
from: '2020-05-14T11:05:13.590',
|
||||
to: '2020-05-14T11:20:13.590',
|
||||
}),
|
||||
refetch$,
|
||||
fetch$,
|
||||
visContext: (await getMockLensAttributes())!,
|
||||
onTotalHitsChange: jest.fn(),
|
||||
onChartLoad: jest.fn(),
|
||||
onLoad: jest.fn(),
|
||||
withDefaultActions: undefined,
|
||||
};
|
||||
|
||||
return {
|
||||
props,
|
||||
component: mountWithIntl(<Histogram {...props} />),
|
||||
const Wrapper = (wrapperProps: CombinedProps) => {
|
||||
const lensPropsContext = useLensProps(wrapperProps);
|
||||
return lensPropsContext ? <Histogram {...wrapperProps} {...lensPropsContext} /> : null;
|
||||
};
|
||||
|
||||
const component = mountWithIntl(<Wrapper {...props} />);
|
||||
|
||||
act(() => {
|
||||
fetch$?.next({ type: 'fetch' });
|
||||
});
|
||||
|
||||
return { props, fetch$, component: component.update() };
|
||||
}
|
||||
|
||||
describe('Histogram', () => {
|
||||
|
@ -92,13 +88,13 @@ describe('Histogram', () => {
|
|||
expect(component.find('[data-test-subj="unifiedHistogramChart"]').exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('should only update lens.EmbeddableComponent props when refetch$ is triggered', async () => {
|
||||
const { component, props } = await mountComponent();
|
||||
it('should only update lens.EmbeddableComponent props when fetch$ is triggered', async () => {
|
||||
const { component, props, fetch$ } = await mountComponent();
|
||||
const embeddable = unifiedHistogramServicesMock.lens.EmbeddableComponent;
|
||||
expect(component.find(embeddable).exists()).toBe(true);
|
||||
let lensProps = component.find(embeddable).props();
|
||||
const originalProps = getLensProps({
|
||||
searchSessionId: props.request.searchSessionId,
|
||||
searchSessionId: props.request?.searchSessionId,
|
||||
getTimeRange: props.getTimeRange,
|
||||
attributes: (await getMockLensAttributes())!.attributes,
|
||||
onLoad: lensProps.onLoad!,
|
||||
|
@ -108,7 +104,7 @@ describe('Histogram', () => {
|
|||
lensProps = component.find(embeddable).props();
|
||||
expect(lensProps).toMatchObject(expect.objectContaining(originalProps));
|
||||
await act(async () => {
|
||||
props.refetch$.next({ type: 'refetch' });
|
||||
fetch$.next({ type: 'fetch' });
|
||||
});
|
||||
component.update();
|
||||
lensProps = component.find(embeddable).props();
|
||||
|
@ -174,27 +170,11 @@ describe('Histogram', () => {
|
|||
.mockReturnValue([{ response: { json: { rawResponse } } } as any]);
|
||||
const dataLoading$ = new BehaviorSubject<boolean | undefined>(false);
|
||||
onLoad(true, undefined, dataLoading$);
|
||||
expect(props.onTotalHitsChange).toHaveBeenLastCalledWith(
|
||||
UnifiedHistogramFetchStatus.loading,
|
||||
undefined
|
||||
);
|
||||
expect(props.onChartLoad).toHaveBeenLastCalledWith({ adapters: {}, dataLoading$ });
|
||||
expect(buildBucketInterval.buildBucketInterval).not.toHaveBeenCalled();
|
||||
expect(useTimeRange.useTimeRange).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({ bucketInterval: undefined })
|
||||
);
|
||||
expect(props.onLoad).toHaveBeenLastCalledWith(true, undefined, dataLoading$);
|
||||
act(() => {
|
||||
onLoad?.(false, adapters, dataLoading$);
|
||||
});
|
||||
expect(props.onTotalHitsChange).toHaveBeenLastCalledWith(
|
||||
UnifiedHistogramFetchStatus.complete,
|
||||
100
|
||||
);
|
||||
expect(props.onChartLoad).toHaveBeenLastCalledWith({ adapters, dataLoading$ });
|
||||
expect(buildBucketInterval.buildBucketInterval).toHaveBeenCalled();
|
||||
expect(useTimeRange.useTimeRange).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({ bucketInterval: mockBucketInterval })
|
||||
);
|
||||
expect(props.onLoad).toHaveBeenLastCalledWith(false, adapters, dataLoading$);
|
||||
});
|
||||
|
||||
it('should execute onLoad correctly when the request has a failure status', async () => {
|
||||
|
@ -206,11 +186,7 @@ describe('Histogram', () => {
|
|||
.spyOn(adapters.requests, 'getRequests')
|
||||
.mockReturnValue([{ status: RequestStatus.ERROR } as any]);
|
||||
onLoad?.(false, adapters);
|
||||
expect(props.onTotalHitsChange).toHaveBeenLastCalledWith(
|
||||
UnifiedHistogramFetchStatus.error,
|
||||
undefined
|
||||
);
|
||||
expect(props.onChartLoad).toHaveBeenLastCalledWith({ adapters });
|
||||
expect(props.onLoad).toHaveBeenLastCalledWith(false, adapters);
|
||||
});
|
||||
|
||||
it('should execute onLoad correctly when the response has shard failures', async () => {
|
||||
|
@ -239,11 +215,7 @@ describe('Histogram', () => {
|
|||
act(() => {
|
||||
onLoad?.(false, adapters);
|
||||
});
|
||||
expect(props.onTotalHitsChange).toHaveBeenLastCalledWith(
|
||||
UnifiedHistogramFetchStatus.error,
|
||||
100
|
||||
);
|
||||
expect(props.onChartLoad).toHaveBeenLastCalledWith({ adapters });
|
||||
expect(props.onLoad).toHaveBeenLastCalledWith(false, adapters);
|
||||
});
|
||||
|
||||
it('should execute onLoad correctly for textbased language and no Lens suggestions', async () => {
|
||||
|
@ -275,11 +247,7 @@ describe('Histogram', () => {
|
|||
act(() => {
|
||||
onLoad?.(false, adapters);
|
||||
});
|
||||
expect(props.onTotalHitsChange).toHaveBeenLastCalledWith(
|
||||
UnifiedHistogramFetchStatus.complete,
|
||||
20
|
||||
);
|
||||
expect(props.onChartLoad).toHaveBeenLastCalledWith({ adapters });
|
||||
expect(props.onLoad).toHaveBeenLastCalledWith(false, adapters);
|
||||
});
|
||||
|
||||
it('should execute onLoad correctly for textbased language and Lens suggestions', async () => {
|
||||
|
@ -311,10 +279,6 @@ describe('Histogram', () => {
|
|||
act(() => {
|
||||
onLoad?.(false, adapters);
|
||||
});
|
||||
expect(props.onTotalHitsChange).toHaveBeenLastCalledWith(
|
||||
UnifiedHistogramFetchStatus.complete,
|
||||
2
|
||||
);
|
||||
expect(props.onChartLoad).toHaveBeenLastCalledWith({ adapters });
|
||||
expect(props.onLoad).toHaveBeenLastCalledWith(false, adapters);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -9,101 +9,54 @@
|
|||
|
||||
import { useEuiTheme } from '@elastic/eui';
|
||||
import { css } from '@emotion/react';
|
||||
import React, { useState } from 'react';
|
||||
import React from 'react';
|
||||
import type { DataView } from '@kbn/data-views-plugin/public';
|
||||
import type { DefaultInspectorAdapters, Datatable } from '@kbn/expressions-plugin/common';
|
||||
import type { IKibanaSearchResponse } from '@kbn/search-types';
|
||||
import type { estypes } from '@elastic/elasticsearch';
|
||||
import type { TimeRange } from '@kbn/es-query';
|
||||
import type { EmbeddableComponentProps, LensEmbeddableInput } from '@kbn/lens-plugin/public';
|
||||
import { RequestStatus } from '@kbn/inspector-plugin/public';
|
||||
import type { Observable } from 'rxjs';
|
||||
import { PublishingSubject } from '@kbn/presentation-publishing';
|
||||
import {
|
||||
import type {
|
||||
UnifiedHistogramBucketInterval,
|
||||
UnifiedHistogramChartContext,
|
||||
UnifiedHistogramFetchStatus,
|
||||
UnifiedHistogramHitsContext,
|
||||
UnifiedHistogramChartLoadEvent,
|
||||
UnifiedHistogramRequestContext,
|
||||
UnifiedHistogramServices,
|
||||
UnifiedHistogramInputMessage,
|
||||
UnifiedHistogramVisContext,
|
||||
} from '../types';
|
||||
import { buildBucketInterval } from './utils/build_bucket_interval';
|
||||
import { useTimeRange } from './hooks/use_time_range';
|
||||
import { useStableCallback } from '../hooks/use_stable_callback';
|
||||
import { useLensProps } from './hooks/use_lens_props';
|
||||
import type { LensProps } from './hooks/use_lens_props';
|
||||
|
||||
export interface HistogramProps {
|
||||
abortController?: AbortController;
|
||||
services: UnifiedHistogramServices;
|
||||
dataView: DataView;
|
||||
request?: UnifiedHistogramRequestContext;
|
||||
hits?: UnifiedHistogramHitsContext;
|
||||
chart: UnifiedHistogramChartContext;
|
||||
bucketInterval?: UnifiedHistogramBucketInterval;
|
||||
isPlainRecord?: boolean;
|
||||
hasLensSuggestions: boolean;
|
||||
getTimeRange: () => TimeRange;
|
||||
refetch$: Observable<UnifiedHistogramInputMessage>;
|
||||
requestData: string;
|
||||
lensProps: LensProps;
|
||||
visContext: UnifiedHistogramVisContext;
|
||||
disableTriggers?: LensEmbeddableInput['disableTriggers'];
|
||||
disabledActions?: LensEmbeddableInput['disabledActions'];
|
||||
onTotalHitsChange?: (status: UnifiedHistogramFetchStatus, result?: number | Error) => void;
|
||||
onChartLoad?: (event: UnifiedHistogramChartLoadEvent) => void;
|
||||
onFilter?: LensEmbeddableInput['onFilter'];
|
||||
onBrushEnd?: LensEmbeddableInput['onBrushEnd'];
|
||||
withDefaultActions: EmbeddableComponentProps['withDefaultActions'];
|
||||
}
|
||||
|
||||
const computeTotalHits = (
|
||||
hasLensSuggestions: boolean,
|
||||
adapterTables:
|
||||
| {
|
||||
[key: string]: Datatable;
|
||||
}
|
||||
| undefined,
|
||||
isPlainRecord?: boolean
|
||||
) => {
|
||||
if (isPlainRecord && hasLensSuggestions) {
|
||||
return Object.values(adapterTables ?? {})?.[0]?.rows?.length;
|
||||
} else if (isPlainRecord && !hasLensSuggestions) {
|
||||
// ES|QL histogram case
|
||||
const rows = Object.values(adapterTables ?? {})?.[0]?.rows;
|
||||
if (!rows) {
|
||||
return undefined;
|
||||
}
|
||||
let rowsCount = 0;
|
||||
rows.forEach((r) => {
|
||||
rowsCount += r.results;
|
||||
});
|
||||
return rowsCount;
|
||||
} else {
|
||||
return adapterTables?.unifiedHistogram?.meta?.statistics?.totalCount;
|
||||
}
|
||||
};
|
||||
|
||||
export function Histogram({
|
||||
services: { data, lens, uiSettings },
|
||||
services: { lens, uiSettings },
|
||||
dataView,
|
||||
request,
|
||||
hits,
|
||||
chart: { timeInterval },
|
||||
bucketInterval,
|
||||
isPlainRecord,
|
||||
hasLensSuggestions,
|
||||
getTimeRange,
|
||||
refetch$,
|
||||
requestData,
|
||||
lensProps,
|
||||
visContext,
|
||||
disableTriggers,
|
||||
disabledActions,
|
||||
onTotalHitsChange,
|
||||
onChartLoad,
|
||||
onFilter,
|
||||
onBrushEnd,
|
||||
withDefaultActions,
|
||||
abortController,
|
||||
}: HistogramProps) {
|
||||
const [bucketInterval, setBucketInterval] = useState<UnifiedHistogramBucketInterval>();
|
||||
const { timeRangeText, timeRangeDisplay } = useTimeRange({
|
||||
uiSettings,
|
||||
bucketInterval,
|
||||
|
@ -113,63 +66,8 @@ export function Histogram({
|
|||
timeField: dataView.timeFieldName,
|
||||
});
|
||||
const { attributes } = visContext;
|
||||
|
||||
const onLoad = useStableCallback(
|
||||
(
|
||||
isLoading: boolean,
|
||||
adapters: Partial<DefaultInspectorAdapters> | undefined,
|
||||
dataLoading$?: PublishingSubject<boolean | undefined>
|
||||
) => {
|
||||
const lensRequest = adapters?.requests?.getRequests()[0];
|
||||
const requestFailed = lensRequest?.status === RequestStatus.ERROR;
|
||||
const json = lensRequest?.response?.json as
|
||||
| IKibanaSearchResponse<estypes.SearchResponse>
|
||||
| undefined;
|
||||
const response = json?.rawResponse;
|
||||
|
||||
if (requestFailed) {
|
||||
onTotalHitsChange?.(UnifiedHistogramFetchStatus.error, undefined);
|
||||
onChartLoad?.({ adapters: adapters ?? {} });
|
||||
return;
|
||||
}
|
||||
|
||||
const adapterTables = adapters?.tables?.tables;
|
||||
const totalHits = computeTotalHits(hasLensSuggestions, adapterTables, isPlainRecord);
|
||||
|
||||
if (response?._shards?.failed || response?.timed_out) {
|
||||
onTotalHitsChange?.(UnifiedHistogramFetchStatus.error, totalHits);
|
||||
} else {
|
||||
onTotalHitsChange?.(
|
||||
isLoading ? UnifiedHistogramFetchStatus.loading : UnifiedHistogramFetchStatus.complete,
|
||||
totalHits ?? hits?.total
|
||||
);
|
||||
}
|
||||
|
||||
if (response) {
|
||||
const newBucketInterval = buildBucketInterval({
|
||||
data,
|
||||
dataView,
|
||||
timeInterval,
|
||||
timeRange: getTimeRange(),
|
||||
response,
|
||||
});
|
||||
|
||||
setBucketInterval(newBucketInterval);
|
||||
}
|
||||
|
||||
onChartLoad?.({ adapters: adapters ?? {}, dataLoading$ });
|
||||
}
|
||||
);
|
||||
|
||||
const { lensProps, requestData } = useLensProps({
|
||||
request,
|
||||
getTimeRange,
|
||||
refetch$,
|
||||
visContext,
|
||||
onLoad,
|
||||
});
|
||||
|
||||
const { euiTheme } = useEuiTheme();
|
||||
|
||||
const boxShadow = `0 2px 2px -1px ${euiTheme.colors.mediumShade},
|
||||
0 1px 5px -2px ${euiTheme.colors.mediumShade}`;
|
||||
const chartCss = css`
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { useFetch } from './use_fetch';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { UnifiedHistogramInput$ } from '../../types';
|
||||
import { Subject } from 'rxjs';
|
||||
|
||||
describe('useFetch', () => {
|
||||
const getDeps: () => {
|
||||
input$: UnifiedHistogramInput$;
|
||||
beforeFetch: () => void;
|
||||
} = () => ({
|
||||
input$: new Subject(),
|
||||
beforeFetch: () => {},
|
||||
});
|
||||
|
||||
it('should trigger the fetch observable when the input$ observable is triggered', () => {
|
||||
const originalDeps = getDeps();
|
||||
const hook = renderHook((deps) => useFetch(deps), {
|
||||
initialProps: originalDeps,
|
||||
});
|
||||
const fetch = jest.fn();
|
||||
hook.result.current.subscribe(fetch);
|
||||
expect(fetch).not.toHaveBeenCalled();
|
||||
originalDeps.input$.next({ type: 'fetch' });
|
||||
expect(fetch).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { useMemo } from 'react';
|
||||
import { filter, share, tap } from 'rxjs';
|
||||
import { UnifiedHistogramInput$ } from '../../types';
|
||||
|
||||
export const useFetch = ({
|
||||
input$,
|
||||
beforeFetch,
|
||||
}: {
|
||||
input$: UnifiedHistogramInput$;
|
||||
beforeFetch: () => void;
|
||||
}) => {
|
||||
return useMemo(
|
||||
() =>
|
||||
input$.pipe(
|
||||
filter((message) => message.type === 'fetch'),
|
||||
tap(beforeFetch),
|
||||
share()
|
||||
),
|
||||
[beforeFetch, input$]
|
||||
);
|
||||
};
|
|
@ -17,7 +17,7 @@ import { getLensProps, useLensProps } from './use_lens_props';
|
|||
describe('useLensProps', () => {
|
||||
it('should return lens props', async () => {
|
||||
const getTimeRange = jest.fn();
|
||||
const refetch$ = new Subject<UnifiedHistogramInputMessage>();
|
||||
const fetch$ = new Subject<UnifiedHistogramInputMessage>();
|
||||
const onLoad = jest.fn();
|
||||
const query = {
|
||||
language: 'kuery',
|
||||
|
@ -41,12 +41,15 @@ describe('useLensProps', () => {
|
|||
adapter: undefined,
|
||||
},
|
||||
getTimeRange,
|
||||
refetch$,
|
||||
fetch$,
|
||||
visContext: attributesContext!,
|
||||
onLoad,
|
||||
});
|
||||
});
|
||||
expect(lensProps.result.current.lensProps).toEqual(
|
||||
act(() => {
|
||||
fetch$.next({ type: 'fetch' });
|
||||
});
|
||||
expect(lensProps.result.current?.lensProps).toEqual(
|
||||
getLensProps({
|
||||
searchSessionId: 'id',
|
||||
getTimeRange,
|
||||
|
@ -58,7 +61,7 @@ describe('useLensProps', () => {
|
|||
|
||||
it('should return lens props for text based languages', async () => {
|
||||
const getTimeRange = jest.fn();
|
||||
const refetch$ = new Subject<UnifiedHistogramInputMessage>();
|
||||
const fetch$ = new Subject<UnifiedHistogramInputMessage>();
|
||||
const onLoad = jest.fn();
|
||||
const query = {
|
||||
language: 'kuery',
|
||||
|
@ -82,12 +85,15 @@ describe('useLensProps', () => {
|
|||
adapter: undefined,
|
||||
},
|
||||
getTimeRange,
|
||||
refetch$,
|
||||
fetch$,
|
||||
visContext: attributesContext!,
|
||||
onLoad,
|
||||
});
|
||||
});
|
||||
expect(lensProps.result.current.lensProps).toEqual(
|
||||
act(() => {
|
||||
fetch$.next({ type: 'fetch' });
|
||||
});
|
||||
expect(lensProps.result.current?.lensProps).toEqual(
|
||||
getLensProps({
|
||||
searchSessionId: 'id',
|
||||
getTimeRange,
|
||||
|
@ -97,9 +103,9 @@ describe('useLensProps', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('should only update lens props when refetch$ is triggered', async () => {
|
||||
it('should only return lens props after fetch$ is triggered', async () => {
|
||||
const getTimeRange = jest.fn();
|
||||
const refetch$ = new Subject<UnifiedHistogramInputMessage>();
|
||||
const fetch$ = new Subject<UnifiedHistogramInputMessage>();
|
||||
const onLoad = jest.fn();
|
||||
const query = {
|
||||
language: 'kuery',
|
||||
|
@ -122,7 +128,7 @@ describe('useLensProps', () => {
|
|||
adapter: undefined,
|
||||
},
|
||||
getTimeRange,
|
||||
refetch$,
|
||||
fetch$,
|
||||
visContext: attributesContext!,
|
||||
onLoad,
|
||||
};
|
||||
|
@ -132,12 +138,12 @@ describe('useLensProps', () => {
|
|||
},
|
||||
{ initialProps: lensProps }
|
||||
);
|
||||
const originalProps = hook.result.current;
|
||||
expect(hook.result.current).toEqual(undefined);
|
||||
hook.rerender({ ...lensProps, request: { searchSessionId: '456', adapter: undefined } });
|
||||
expect(hook.result.current).toEqual(originalProps);
|
||||
expect(hook.result.current).toEqual(undefined);
|
||||
act(() => {
|
||||
refetch$.next({ type: 'refetch' });
|
||||
fetch$.next({ type: 'fetch' });
|
||||
});
|
||||
expect(hook.result.current).not.toEqual(originalProps);
|
||||
expect(hook.result.current).not.toEqual(undefined);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
import type { TimeRange } from '@kbn/data-plugin/common';
|
||||
import { ViewMode } from '@kbn/embeddable-plugin/public';
|
||||
import type { DefaultInspectorAdapters } from '@kbn/expressions-plugin/common';
|
||||
import type { TypedLensByValueInput } from '@kbn/lens-plugin/public';
|
||||
import type { EmbeddableComponentProps, TypedLensByValueInput } from '@kbn/lens-plugin/public';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import type { Observable } from 'rxjs';
|
||||
import type {
|
||||
|
@ -20,21 +20,38 @@ import type {
|
|||
} from '../../types';
|
||||
import { useStableCallback } from '../../hooks/use_stable_callback';
|
||||
|
||||
export type LensProps = Pick<
|
||||
EmbeddableComponentProps,
|
||||
| 'id'
|
||||
| 'viewMode'
|
||||
| 'timeRange'
|
||||
| 'attributes'
|
||||
| 'noPadding'
|
||||
| 'searchSessionId'
|
||||
| 'executionContext'
|
||||
| 'onLoad'
|
||||
>;
|
||||
|
||||
export const useLensProps = ({
|
||||
request,
|
||||
getTimeRange,
|
||||
refetch$,
|
||||
fetch$,
|
||||
visContext,
|
||||
onLoad,
|
||||
}: {
|
||||
request?: UnifiedHistogramRequestContext;
|
||||
getTimeRange: () => TimeRange;
|
||||
refetch$: Observable<UnifiedHistogramInputMessage>;
|
||||
visContext: UnifiedHistogramVisContext;
|
||||
fetch$: Observable<UnifiedHistogramInputMessage>;
|
||||
visContext?: UnifiedHistogramVisContext;
|
||||
onLoad: (isLoading: boolean, adapters: Partial<DefaultInspectorAdapters> | undefined) => void;
|
||||
}) => {
|
||||
const buildLensProps = useCallback(() => {
|
||||
if (!visContext) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { attributes, requestData } = visContext;
|
||||
|
||||
return {
|
||||
requestData: JSON.stringify(requestData),
|
||||
lensProps: getLensProps({
|
||||
|
@ -46,13 +63,14 @@ export const useLensProps = ({
|
|||
};
|
||||
}, [visContext, getTimeRange, onLoad, request?.searchSessionId]);
|
||||
|
||||
const [lensPropsContext, setLensPropsContext] = useState(buildLensProps());
|
||||
// Initialize with undefined to avoid rendering Lens until a fetch has been triggered
|
||||
const [lensPropsContext, setLensPropsContext] = useState<ReturnType<typeof buildLensProps>>();
|
||||
const updateLensPropsContext = useStableCallback(() => setLensPropsContext(buildLensProps()));
|
||||
|
||||
useEffect(() => {
|
||||
const subscription = refetch$.subscribe(updateLensPropsContext);
|
||||
const subscription = fetch$.subscribe(updateLensPropsContext);
|
||||
return () => subscription.unsubscribe();
|
||||
}, [refetch$, updateLensPropsContext]);
|
||||
}, [fetch$, updateLensPropsContext]);
|
||||
|
||||
return lensPropsContext;
|
||||
};
|
||||
|
@ -67,7 +85,7 @@ export const getLensProps = ({
|
|||
getTimeRange: () => TimeRange;
|
||||
attributes: TypedLensByValueInput['attributes'];
|
||||
onLoad: (isLoading: boolean, adapters: Partial<DefaultInspectorAdapters> | undefined) => void;
|
||||
}) => ({
|
||||
}): LensProps => ({
|
||||
id: 'unifiedHistogramLensComponent',
|
||||
viewMode: ViewMode.VIEW,
|
||||
timeRange: getTimeRange(),
|
||||
|
|
|
@ -1,86 +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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { useRefetch } from './use_refetch';
|
||||
import { DataView } from '@kbn/data-views-plugin/common';
|
||||
import { AggregateQuery, Filter, Query, TimeRange } from '@kbn/es-query';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import {
|
||||
UnifiedHistogramBreakdownContext,
|
||||
UnifiedHistogramChartContext,
|
||||
UnifiedHistogramHitsContext,
|
||||
UnifiedHistogramInput$,
|
||||
UnifiedHistogramRequestContext,
|
||||
} from '../../types';
|
||||
import { dataViewWithTimefieldMock } from '../../__mocks__/data_view_with_timefield';
|
||||
import { Subject } from 'rxjs';
|
||||
|
||||
describe('useRefetch', () => {
|
||||
const getDeps: () => {
|
||||
dataView: DataView;
|
||||
request: UnifiedHistogramRequestContext | undefined;
|
||||
hits: UnifiedHistogramHitsContext | undefined;
|
||||
chart: UnifiedHistogramChartContext | undefined;
|
||||
chartVisible: boolean;
|
||||
breakdown: UnifiedHistogramBreakdownContext | undefined;
|
||||
filters: Filter[];
|
||||
query: Query | AggregateQuery;
|
||||
relativeTimeRange: TimeRange;
|
||||
input$: UnifiedHistogramInput$;
|
||||
beforeRefetch: () => void;
|
||||
} = () => ({
|
||||
dataView: dataViewWithTimefieldMock,
|
||||
request: undefined,
|
||||
hits: undefined,
|
||||
chart: undefined,
|
||||
chartVisible: true,
|
||||
breakdown: undefined,
|
||||
filters: [],
|
||||
query: { language: 'kuery', query: '' },
|
||||
relativeTimeRange: { from: 'now-15m', to: 'now' },
|
||||
input$: new Subject(),
|
||||
beforeRefetch: () => {},
|
||||
});
|
||||
|
||||
it('should trigger the refetch observable when any of the arguments change', () => {
|
||||
const originalDeps = getDeps();
|
||||
const hook = renderHook((deps) => useRefetch(deps), {
|
||||
initialProps: originalDeps,
|
||||
});
|
||||
const refetch = jest.fn();
|
||||
hook.result.current.subscribe(refetch);
|
||||
hook.rerender({ ...originalDeps });
|
||||
expect(refetch).not.toHaveBeenCalled();
|
||||
hook.rerender({ ...originalDeps, chartVisible: false });
|
||||
expect(refetch).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should not trigger the refetch observable when disableAutoFetching is true', () => {
|
||||
const originalDeps = { ...getDeps(), disableAutoFetching: true };
|
||||
const hook = renderHook((deps) => useRefetch(deps), {
|
||||
initialProps: originalDeps,
|
||||
});
|
||||
const refetch = jest.fn();
|
||||
hook.result.current.subscribe(refetch);
|
||||
hook.rerender({ ...originalDeps, chartVisible: false });
|
||||
expect(refetch).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should trigger the refetch observable when the input$ observable is triggered', () => {
|
||||
const originalDeps = { ...getDeps(), disableAutoFetching: true };
|
||||
const hook = renderHook((deps) => useRefetch(deps), {
|
||||
initialProps: originalDeps,
|
||||
});
|
||||
const refetch = jest.fn();
|
||||
hook.result.current.subscribe(refetch);
|
||||
expect(refetch).not.toHaveBeenCalled();
|
||||
originalDeps.input$.next({ type: 'refetch' });
|
||||
expect(refetch).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
|
@ -1,145 +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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import type { DataView } from '@kbn/data-views-plugin/common';
|
||||
import type { AggregateQuery, Filter, Query, TimeRange } from '@kbn/es-query';
|
||||
import type { Suggestion } from '@kbn/lens-plugin/public';
|
||||
import { cloneDeep, isEqual } from 'lodash';
|
||||
import { useEffect, useMemo, useRef } from 'react';
|
||||
import { filter, share, tap } from 'rxjs';
|
||||
import {
|
||||
UnifiedHistogramBreakdownContext,
|
||||
UnifiedHistogramChartContext,
|
||||
UnifiedHistogramHitsContext,
|
||||
UnifiedHistogramInput$,
|
||||
UnifiedHistogramRequestContext,
|
||||
} from '../../types';
|
||||
|
||||
export const useRefetch = ({
|
||||
dataView,
|
||||
request,
|
||||
hits,
|
||||
chart,
|
||||
chartVisible,
|
||||
breakdown,
|
||||
filters,
|
||||
query,
|
||||
relativeTimeRange,
|
||||
currentSuggestion,
|
||||
disableAutoFetching,
|
||||
input$,
|
||||
beforeRefetch,
|
||||
}: {
|
||||
dataView: DataView;
|
||||
request: UnifiedHistogramRequestContext | undefined;
|
||||
hits: UnifiedHistogramHitsContext | undefined;
|
||||
chart: UnifiedHistogramChartContext | undefined;
|
||||
chartVisible: boolean;
|
||||
breakdown: UnifiedHistogramBreakdownContext | undefined;
|
||||
filters: Filter[];
|
||||
query: Query | AggregateQuery;
|
||||
relativeTimeRange: TimeRange;
|
||||
currentSuggestion?: Suggestion;
|
||||
disableAutoFetching?: boolean;
|
||||
input$: UnifiedHistogramInput$;
|
||||
beforeRefetch: () => void;
|
||||
}) => {
|
||||
const refetchDeps = useRef<ReturnType<typeof getRefetchDeps>>();
|
||||
|
||||
// When the Unified Histogram props change, we must compare the current subset
|
||||
// that should trigger a histogram refetch against the previous subset. If they
|
||||
// are different, we must refetch the histogram to ensure it's up to date.
|
||||
useEffect(() => {
|
||||
// Skip if auto fetching if disabled
|
||||
if (disableAutoFetching) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newRefetchDeps = getRefetchDeps({
|
||||
dataView,
|
||||
request,
|
||||
hits,
|
||||
chart,
|
||||
chartVisible,
|
||||
breakdown,
|
||||
filters,
|
||||
query,
|
||||
relativeTimeRange,
|
||||
currentSuggestion,
|
||||
});
|
||||
|
||||
if (!isEqual(refetchDeps.current, newRefetchDeps)) {
|
||||
if (refetchDeps.current) {
|
||||
input$.next({ type: 'refetch' });
|
||||
}
|
||||
|
||||
refetchDeps.current = newRefetchDeps;
|
||||
}
|
||||
}, [
|
||||
breakdown,
|
||||
chart,
|
||||
chartVisible,
|
||||
currentSuggestion,
|
||||
dataView,
|
||||
disableAutoFetching,
|
||||
filters,
|
||||
hits,
|
||||
input$,
|
||||
query,
|
||||
relativeTimeRange,
|
||||
request,
|
||||
]);
|
||||
|
||||
return useMemo(
|
||||
() =>
|
||||
input$.pipe(
|
||||
filter((message) => message.type === 'refetch'),
|
||||
tap(beforeRefetch),
|
||||
share()
|
||||
),
|
||||
[beforeRefetch, input$]
|
||||
);
|
||||
};
|
||||
|
||||
const getRefetchDeps = ({
|
||||
dataView,
|
||||
request,
|
||||
hits,
|
||||
chart,
|
||||
chartVisible,
|
||||
breakdown,
|
||||
filters,
|
||||
query,
|
||||
relativeTimeRange,
|
||||
currentSuggestion,
|
||||
}: {
|
||||
dataView: DataView;
|
||||
request: UnifiedHistogramRequestContext | undefined;
|
||||
hits: UnifiedHistogramHitsContext | undefined;
|
||||
chart: UnifiedHistogramChartContext | undefined;
|
||||
chartVisible: boolean;
|
||||
breakdown: UnifiedHistogramBreakdownContext | undefined;
|
||||
filters: Filter[];
|
||||
query: Query | AggregateQuery;
|
||||
relativeTimeRange: TimeRange;
|
||||
currentSuggestion?: Suggestion;
|
||||
}) =>
|
||||
cloneDeep([
|
||||
dataView.id,
|
||||
request?.searchSessionId,
|
||||
Boolean(hits),
|
||||
chartVisible,
|
||||
chart?.timeInterval,
|
||||
Boolean(breakdown),
|
||||
breakdown?.field,
|
||||
filters,
|
||||
query,
|
||||
relativeTimeRange,
|
||||
currentSuggestion?.visualizationId,
|
||||
]);
|
|
@ -28,7 +28,7 @@ jest.mock('react-use/lib/useDebounce', () => {
|
|||
|
||||
describe('useTotalHits', () => {
|
||||
const timeRange = { from: 'now-15m', to: 'now' };
|
||||
const refetch$: UnifiedHistogramInput$ = new Subject();
|
||||
const fetch$: UnifiedHistogramInput$ = new Subject();
|
||||
const getDeps = () => ({
|
||||
services: {
|
||||
data: dataPluginMock.createStartContract(),
|
||||
|
@ -54,7 +54,7 @@ describe('useTotalHits', () => {
|
|||
filters: [],
|
||||
query: { query: '', language: 'kuery' },
|
||||
getTimeRange: () => timeRange,
|
||||
refetch$,
|
||||
fetch$,
|
||||
onTotalHitsChange: jest.fn(),
|
||||
});
|
||||
|
||||
|
@ -95,11 +95,11 @@ describe('useTotalHits', () => {
|
|||
},
|
||||
query,
|
||||
filters,
|
||||
refetch$,
|
||||
fetch$,
|
||||
onTotalHitsChange,
|
||||
})
|
||||
);
|
||||
refetch$.next({ type: 'refetch' });
|
||||
fetch$.next({ type: 'fetch' });
|
||||
rerender();
|
||||
expect(onTotalHitsChange).toBeCalledTimes(1);
|
||||
expect(onTotalHitsChange).toBeCalledWith(UnifiedHistogramFetchStatus.loading, undefined);
|
||||
|
@ -128,7 +128,7 @@ describe('useTotalHits', () => {
|
|||
query: { esql: 'from test' },
|
||||
};
|
||||
const { rerender } = renderHook(() => useTotalHits(deps));
|
||||
refetch$.next({ type: 'refetch' });
|
||||
fetch$.next({ type: 'fetch' });
|
||||
rerender();
|
||||
expect(onTotalHitsChange).not.toHaveBeenCalled();
|
||||
});
|
||||
|
@ -153,7 +153,7 @@ describe('useTotalHits', () => {
|
|||
expect(fetchSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not fetch if refetch$ is not triggered', async () => {
|
||||
it('should not fetch if fetch$ is not triggered', async () => {
|
||||
const onTotalHitsChange = jest.fn();
|
||||
const fetchSpy = jest.spyOn(searchSourceInstanceMock, 'fetch$').mockClear();
|
||||
const setFieldSpy = jest.spyOn(searchSourceInstanceMock, 'setField').mockClear();
|
||||
|
@ -165,14 +165,14 @@ describe('useTotalHits', () => {
|
|||
expect(fetchSpy).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('should fetch a second time if refetch$ is triggered', async () => {
|
||||
it('should fetch a second time if fetch$ is triggered', async () => {
|
||||
const abortSpy = jest.spyOn(AbortController.prototype, 'abort').mockClear();
|
||||
const onTotalHitsChange = jest.fn();
|
||||
const fetchSpy = jest.spyOn(searchSourceInstanceMock, 'fetch$').mockClear();
|
||||
const setFieldSpy = jest.spyOn(searchSourceInstanceMock, 'setField').mockClear();
|
||||
const options = { ...getDeps(), onTotalHitsChange };
|
||||
const { rerender } = renderHook(() => useTotalHits(options));
|
||||
refetch$.next({ type: 'refetch' });
|
||||
fetch$.next({ type: 'fetch' });
|
||||
rerender();
|
||||
expect(onTotalHitsChange).toBeCalledTimes(1);
|
||||
expect(setFieldSpy).toHaveBeenCalled();
|
||||
|
@ -180,7 +180,7 @@ describe('useTotalHits', () => {
|
|||
await waitFor(() => {
|
||||
expect(onTotalHitsChange).toBeCalledTimes(2);
|
||||
});
|
||||
refetch$.next({ type: 'refetch' });
|
||||
fetch$.next({ type: 'fetch' });
|
||||
rerender();
|
||||
expect(abortSpy).toHaveBeenCalled();
|
||||
expect(onTotalHitsChange).toBeCalledTimes(3);
|
||||
|
@ -199,7 +199,7 @@ describe('useTotalHits', () => {
|
|||
.mockClear()
|
||||
.mockReturnValue(throwError(() => error));
|
||||
const { rerender } = renderHook(() => useTotalHits({ ...getDeps(), onTotalHitsChange }));
|
||||
refetch$.next({ type: 'refetch' });
|
||||
fetch$.next({ type: 'fetch' });
|
||||
rerender();
|
||||
await waitFor(() => {
|
||||
expect(onTotalHitsChange).toBeCalledTimes(2);
|
||||
|
@ -228,7 +228,7 @@ describe('useTotalHits', () => {
|
|||
filters,
|
||||
})
|
||||
);
|
||||
refetch$.next({ type: 'refetch' });
|
||||
fetch$.next({ type: 'fetch' });
|
||||
rerender();
|
||||
expect(setOverwriteDataViewTypeSpy).toHaveBeenCalledWith(undefined);
|
||||
expect(setFieldSpy).toHaveBeenCalledWith('filter', filters);
|
||||
|
|
|
@ -31,7 +31,7 @@ export const useTotalHits = ({
|
|||
filters,
|
||||
query,
|
||||
getTimeRange,
|
||||
refetch$,
|
||||
fetch$,
|
||||
onTotalHitsChange,
|
||||
isPlainRecord,
|
||||
}: {
|
||||
|
@ -43,7 +43,7 @@ export const useTotalHits = ({
|
|||
filters: Filter[];
|
||||
query: Query | AggregateQuery;
|
||||
getTimeRange: () => TimeRange;
|
||||
refetch$: Observable<UnifiedHistogramInputMessage>;
|
||||
fetch$: Observable<UnifiedHistogramInputMessage>;
|
||||
onTotalHitsChange?: (status: UnifiedHistogramFetchStatus, result?: number | Error) => void;
|
||||
isPlainRecord?: boolean;
|
||||
}) => {
|
||||
|
@ -65,9 +65,9 @@ export const useTotalHits = ({
|
|||
});
|
||||
|
||||
useEffect(() => {
|
||||
const subscription = refetch$.subscribe(fetch);
|
||||
const subscription = fetch$.subscribe(fetch);
|
||||
return () => subscription.unsubscribe();
|
||||
}, [fetch, refetch$]);
|
||||
}, [fetch, fetch$]);
|
||||
};
|
||||
|
||||
const fetchTotalHits = async ({
|
||||
|
|
|
@ -46,17 +46,15 @@ describe('UnifiedHistogramContainer', () => {
|
|||
expect(api).toBeDefined();
|
||||
});
|
||||
|
||||
it('should trigger input$ when refetch is called', async () => {
|
||||
it('should trigger input$ when fetch is called', async () => {
|
||||
let api: UnifiedHistogramApi | undefined;
|
||||
const setApi = (ref: UnifiedHistogramApi) => {
|
||||
api = ref;
|
||||
};
|
||||
const getCreationOptions = jest.fn(() => ({ disableAutoFetching: true }));
|
||||
const component = mountWithIntl(
|
||||
<UnifiedHistogramContainer
|
||||
services={unifiedHistogramServicesMock}
|
||||
ref={setApi}
|
||||
getCreationOptions={getCreationOptions}
|
||||
dataView={dataViewWithTimefieldMock}
|
||||
filters={[]}
|
||||
query={{ language: 'kuery', query: '' }}
|
||||
|
@ -72,9 +70,9 @@ describe('UnifiedHistogramContainer', () => {
|
|||
const inputSpy = jest.fn();
|
||||
input$?.subscribe(inputSpy);
|
||||
act(() => {
|
||||
api?.refetch();
|
||||
api?.fetch();
|
||||
});
|
||||
expect(inputSpy).toHaveBeenCalledTimes(1);
|
||||
expect(inputSpy).toHaveBeenCalledWith({ type: 'refetch' });
|
||||
expect(inputSpy).toHaveBeenCalledWith({ type: 'fetch' });
|
||||
});
|
||||
});
|
||||
|
|
|
@ -30,10 +30,7 @@ import { topPanelHeightSelector } from './utils/state_selectors';
|
|||
import { exportVisContext } from '../utils/external_vis_context';
|
||||
import { getBreakdownField } from './utils/local_storage_utils';
|
||||
|
||||
type LayoutProps = Pick<
|
||||
UnifiedHistogramLayoutProps,
|
||||
'disableAutoFetching' | 'disableTriggers' | 'disabledActions'
|
||||
>;
|
||||
type LayoutProps = Pick<UnifiedHistogramLayoutProps, 'disableTriggers' | 'disabledActions'>;
|
||||
|
||||
/**
|
||||
* The options used to initialize the container
|
||||
|
@ -84,9 +81,9 @@ export type UnifiedHistogramContainerProps = {
|
|||
*/
|
||||
export type UnifiedHistogramApi = {
|
||||
/**
|
||||
* Manually trigger a refetch of the data
|
||||
* Trigger a fetch of the data
|
||||
*/
|
||||
refetch: () => void;
|
||||
fetch: () => void;
|
||||
} & Pick<
|
||||
UnifiedHistogramStateService,
|
||||
'state$' | 'setChartHidden' | 'setTopPanelHeight' | 'setTimeInterval' | 'setTotalHits'
|
||||
|
@ -112,7 +109,7 @@ export const UnifiedHistogramContainer = forwardRef<
|
|||
const options = await getCreationOptions?.();
|
||||
const apiHelper = await services.lens.stateHelperApi();
|
||||
|
||||
setLayoutProps(pick(options, 'disableAutoFetching', 'disableTriggers', 'disabledActions'));
|
||||
setLayoutProps(pick(options, 'disableTriggers', 'disabledActions'));
|
||||
setLocalStorageKeyPrefix(options?.localStorageKeyPrefix);
|
||||
setStateService(createStateService({ services, ...options }));
|
||||
setLensSuggestionsApi(() => apiHelper.suggestions);
|
||||
|
@ -125,8 +122,8 @@ export const UnifiedHistogramContainer = forwardRef<
|
|||
}
|
||||
|
||||
setApi({
|
||||
refetch: () => {
|
||||
input$.next({ type: 'refetch' });
|
||||
fetch: () => {
|
||||
input$.next({ type: 'fetch' });
|
||||
},
|
||||
...pick(
|
||||
stateService,
|
||||
|
|
|
@ -119,10 +119,6 @@ export interface UnifiedHistogramLayoutProps extends PropsWithChildren<unknown>
|
|||
* This element would replace the default chart toggle buttons
|
||||
*/
|
||||
renderCustomChartToggleActions?: () => ReactElement | undefined;
|
||||
/**
|
||||
* Disable automatic refetching based on props changes, and instead wait for a `refetch` message
|
||||
*/
|
||||
disableAutoFetching?: boolean;
|
||||
/**
|
||||
* Disable triggers for the Lens embeddable
|
||||
*/
|
||||
|
@ -219,7 +215,6 @@ export const UnifiedHistogramLayout = ({
|
|||
container,
|
||||
topPanelHeight,
|
||||
renderCustomChartToggleActions,
|
||||
disableAutoFetching,
|
||||
disableTriggers,
|
||||
disabledActions,
|
||||
lensSuggestionsApi,
|
||||
|
@ -359,7 +354,6 @@ export const UnifiedHistogramLayout = ({
|
|||
breakdown={breakdown}
|
||||
renderCustomChartToggleActions={renderCustomChartToggleActions}
|
||||
appendHistogram={chartSpacer}
|
||||
disableAutoFetching={disableAutoFetching}
|
||||
disableTriggers={disableTriggers}
|
||||
disabledActions={disabledActions}
|
||||
input$={input$}
|
||||
|
|
|
@ -17,7 +17,7 @@ export const createMockUnifiedHistogramApi = () => {
|
|||
setTopPanelHeight: jest.fn(),
|
||||
setTimeInterval: jest.fn(),
|
||||
setTotalHits: jest.fn(),
|
||||
refetch: jest.fn(),
|
||||
fetch: jest.fn(),
|
||||
};
|
||||
return api;
|
||||
};
|
||||
|
|
|
@ -126,16 +126,16 @@ export interface UnifiedHistogramBreakdownContext {
|
|||
}
|
||||
|
||||
/**
|
||||
* Message to refetch the chart and total hits
|
||||
* Message to fetch the chart and total hits
|
||||
*/
|
||||
export interface UnifiedHistogramRefetchMessage {
|
||||
type: 'refetch';
|
||||
export interface UnifiedHistogramFetchMessage {
|
||||
type: 'fetch';
|
||||
}
|
||||
|
||||
/**
|
||||
* Unified histogram input message
|
||||
*/
|
||||
export type UnifiedHistogramInputMessage = UnifiedHistogramRefetchMessage;
|
||||
export type UnifiedHistogramInputMessage = UnifiedHistogramFetchMessage;
|
||||
|
||||
/**
|
||||
* Unified histogram input observable
|
||||
|
|
|
@ -65,11 +65,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
.getEntries()
|
||||
.filter((entry: any) => ['fetch', 'xmlhttprequest'].includes(entry.initiatorType))
|
||||
);
|
||||
|
||||
const result = requests.filter((entry) =>
|
||||
entry.name.endsWith(`/internal/search/${endpoint}`)
|
||||
);
|
||||
|
||||
const count = result.length;
|
||||
if (count !== searchCount) {
|
||||
log.warning('Request count differs:', result);
|
||||
|
@ -80,18 +78,18 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
);
|
||||
};
|
||||
|
||||
const waitForLoadingToFinish = async () => {
|
||||
await header.waitUntilLoadingHasFinished();
|
||||
await discover.waitForDocTableLoadingComplete();
|
||||
await elasticChart.canvasExists();
|
||||
};
|
||||
|
||||
const expectSearches = async (type: 'ese' | 'esql', expected: number, cb: Function) => {
|
||||
await expectSearchCount(type, 0);
|
||||
await cb();
|
||||
await expectSearchCount(type, expected);
|
||||
};
|
||||
|
||||
const waitForLoadingToFinish = async () => {
|
||||
await header.waitUntilLoadingHasFinished();
|
||||
await discover.waitForDocTableLoadingComplete();
|
||||
await elasticChart.canvasExists();
|
||||
};
|
||||
|
||||
const getSharedTests = ({
|
||||
type,
|
||||
savedSearch,
|
||||
|
@ -99,7 +97,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
query2,
|
||||
savedSearchesRequests,
|
||||
setQuery,
|
||||
expectedRequests = 2,
|
||||
}: {
|
||||
type: 'ese' | 'esql';
|
||||
savedSearch: string;
|
||||
|
@ -107,10 +104,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
query2: string;
|
||||
savedSearchesRequests?: number;
|
||||
setQuery: (query: string) => Promise<void>;
|
||||
expectedRequests?: number;
|
||||
expectedRefreshRequest?: number;
|
||||
}) => {
|
||||
it(`should send no more than ${expectedRequests} search requests (documents + chart) on page load`, async () => {
|
||||
it(`should send 2 search requests (documents + chart) on page load`, async () => {
|
||||
if (type === 'ese') {
|
||||
await browser.refresh();
|
||||
}
|
||||
|
@ -118,29 +113,29 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
performance.setResourceTimingBufferSize(Number.MAX_SAFE_INTEGER);
|
||||
});
|
||||
if (type === 'esql') {
|
||||
await expectSearches(type, expectedRequests, async () => {
|
||||
await expectSearches(type, 2, async () => {
|
||||
await queryBar.clickQuerySubmitButton();
|
||||
});
|
||||
} else {
|
||||
await expectSearchCount(type, expectedRequests);
|
||||
await expectSearchCount(type, 2);
|
||||
}
|
||||
});
|
||||
|
||||
it(`should send no more than ${expectedRequests} requests (documents + chart) when refreshing`, async () => {
|
||||
await expectSearches(type, expectedRequests, async () => {
|
||||
it(`should send 2 requests (documents + chart) when refreshing`, async () => {
|
||||
await expectSearches(type, 2, async () => {
|
||||
await queryBar.clickQuerySubmitButton();
|
||||
});
|
||||
});
|
||||
|
||||
it(`should send no more than ${expectedRequests} requests (documents + chart) when changing the query`, async () => {
|
||||
await expectSearches(type, expectedRequests, async () => {
|
||||
it(`should send 2 requests (documents + chart) when changing the query`, async () => {
|
||||
await expectSearches(type, 2, async () => {
|
||||
await setQuery(query1);
|
||||
await queryBar.clickQuerySubmitButton();
|
||||
});
|
||||
});
|
||||
|
||||
it(`should send no more than ${expectedRequests} requests (documents + chart) when changing the time range`, async () => {
|
||||
await expectSearches(type, expectedRequests, async () => {
|
||||
it(`should send 2 requests (documents + chart) when changing the time range`, async () => {
|
||||
await expectSearches(type, 2, async () => {
|
||||
await timePicker.setAbsoluteRange(
|
||||
'Sep 21, 2015 @ 06:31:44.000',
|
||||
'Sep 23, 2015 @ 00:00:00.000'
|
||||
|
@ -156,6 +151,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
await discover.toggleChartVisibility();
|
||||
});
|
||||
});
|
||||
|
||||
it(`should send a request for chart data when toggling the chart visibility after a time range change`, async () => {
|
||||
// hide chart
|
||||
await discover.toggleChartVisibility();
|
||||
|
@ -170,7 +166,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
});
|
||||
});
|
||||
|
||||
it(`should send ${savedSearchesRequests} requests for saved search changes`, async () => {
|
||||
const actualSavedSearchRequests = savedSearchesRequests ?? 2;
|
||||
|
||||
it(`should send no more than ${actualSavedSearchRequests} requests for saved search changes`, async () => {
|
||||
await setQuery(query1);
|
||||
await queryBar.clickQuerySubmitButton();
|
||||
await timePicker.setAbsoluteRange(
|
||||
|
@ -178,42 +176,29 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
'Sep 23, 2015 @ 00:00:00.000'
|
||||
);
|
||||
await waitForLoadingToFinish();
|
||||
const actualExpectedRequests = savedSearchesRequests ?? expectedRequests;
|
||||
log.debug('Creating saved search');
|
||||
await expectSearches(
|
||||
type,
|
||||
type === 'esql' ? actualExpectedRequests + 2 : actualExpectedRequests,
|
||||
async () => {
|
||||
await discover.saveSearch(savedSearch);
|
||||
}
|
||||
);
|
||||
await expectSearches(type, actualSavedSearchRequests, async () => {
|
||||
await discover.saveSearch(savedSearch);
|
||||
});
|
||||
log.debug('Resetting saved search');
|
||||
await setQuery(query2);
|
||||
await queryBar.clickQuerySubmitButton();
|
||||
await waitForLoadingToFinish();
|
||||
await expectSearches(type, actualExpectedRequests, async () => {
|
||||
await expectSearches(type, 2, async () => {
|
||||
await discover.revertUnsavedChanges();
|
||||
});
|
||||
log.debug('Clearing saved search');
|
||||
await expectSearches(
|
||||
type,
|
||||
type === 'esql' ? actualExpectedRequests + 1 : actualExpectedRequests,
|
||||
async () => {
|
||||
await testSubjects.click('discoverNewButton');
|
||||
if (type === 'esql') {
|
||||
await queryBar.clickQuerySubmitButton();
|
||||
}
|
||||
await waitForLoadingToFinish();
|
||||
await expectSearches(type, actualSavedSearchRequests, async () => {
|
||||
await testSubjects.click('discoverNewButton');
|
||||
if (type === 'esql') {
|
||||
await queryBar.clickQuerySubmitButton();
|
||||
}
|
||||
);
|
||||
await waitForLoadingToFinish();
|
||||
});
|
||||
log.debug('Loading saved search');
|
||||
await expectSearches(
|
||||
type,
|
||||
type === 'esql' ? actualExpectedRequests + 2 : actualExpectedRequests,
|
||||
async () => {
|
||||
await discover.loadSavedSearch(savedSearch);
|
||||
}
|
||||
);
|
||||
await expectSearches(type, actualSavedSearchRequests, async () => {
|
||||
await discover.loadSavedSearch(savedSearch);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -233,7 +218,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
setQuery: (query) => queryBar.setQuery(query),
|
||||
});
|
||||
|
||||
it('should send no more than 2 requests (documents + chart) when adding a filter', async () => {
|
||||
it('should send 2 requests (documents + chart) when adding a filter', async () => {
|
||||
await expectSearches(type, 2, async () => {
|
||||
await filterBar.addFilter({
|
||||
field: 'extension',
|
||||
|
@ -243,39 +228,41 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
});
|
||||
});
|
||||
|
||||
it('should send no more than 2 requests (documents + chart) when sorting', async () => {
|
||||
it('should send 2 requests (documents + chart) when sorting', async () => {
|
||||
await expectSearches(type, 2, async () => {
|
||||
await discover.clickFieldSort('@timestamp', 'Sort Old-New');
|
||||
});
|
||||
});
|
||||
|
||||
it('should send no more than 2 requests (documents + chart) when changing to a breakdown field without an other bucket', async () => {
|
||||
it('should send 2 requests (documents + chart) when changing to a breakdown field without an other bucket', async () => {
|
||||
await expectSearches(type, 2, async () => {
|
||||
await discover.chooseBreakdownField('type');
|
||||
});
|
||||
});
|
||||
|
||||
it('should send no more than 3 requests (documents + chart + other bucket) when changing to a breakdown field with an other bucket', async () => {
|
||||
it('should send 3 requests (documents + chart + other bucket) when changing to a breakdown field with an other bucket', async () => {
|
||||
await testSubjects.click('discoverNewButton');
|
||||
await expectSearches(type, 3, async () => {
|
||||
await discover.chooseBreakdownField('extension.raw');
|
||||
});
|
||||
});
|
||||
|
||||
it('should send no more than 2 requests (documents + chart) when changing the chart interval', async () => {
|
||||
it('should send 2 requests (documents + chart) when changing the chart interval', async () => {
|
||||
await expectSearches(type, 2, async () => {
|
||||
await discover.setChartInterval('Day');
|
||||
});
|
||||
});
|
||||
|
||||
it('should send no more than 2 requests (documents + chart) when changing the data view', async () => {
|
||||
it('should send 2 requests (documents + chart) when changing the data view', async () => {
|
||||
await expectSearches(type, 2, async () => {
|
||||
await discover.selectIndexPattern('long-window-logstash-*');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('ES|QL mode', () => {
|
||||
const type = 'esql';
|
||||
|
||||
before(async () => {
|
||||
await kibanaServer.uiSettings.update({
|
||||
'discover:searchOnPageLoad': false,
|
||||
|
@ -293,9 +280,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
savedSearch: 'esql test',
|
||||
query1: 'from logstash-* | where bytes > 1000 ',
|
||||
query2: 'from logstash-* | where bytes < 2000 ',
|
||||
savedSearchesRequests: 2,
|
||||
savedSearchesRequests: 3,
|
||||
setQuery: (query) => monacoEditor.setCodeEditorValue(query),
|
||||
expectedRequests: 2,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue