[Discover][Main] Split single query into 2 queries for faster results (#104818)
Co-authored-by: Tim Roes <tim.roes@elastic.co>
This commit is contained in:
parent
7e12ea84d5
commit
47f5f81765
|
@ -6,9 +6,11 @@
|
||||||
* Side Public License, v 1.
|
* Side Public License, v 1.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
import { BehaviorSubject } from 'rxjs';
|
import { BehaviorSubject } from 'rxjs';
|
||||||
import { take } from 'rxjs/operators';
|
import { take } from 'rxjs/operators';
|
||||||
import { renderHook, act } from '@testing-library/react-hooks';
|
import { renderHook, act } from '@testing-library/react-hooks';
|
||||||
|
import { render, act as renderAct } from '@testing-library/react';
|
||||||
|
|
||||||
import { EUI_CHARTS_THEME_DARK, EUI_CHARTS_THEME_LIGHT } from '@elastic/eui/dist/eui_charts_theme';
|
import { EUI_CHARTS_THEME_DARK, EUI_CHARTS_THEME_LIGHT } from '@elastic/eui/dist/eui_charts_theme';
|
||||||
|
|
||||||
|
@ -105,6 +107,30 @@ describe('ThemeService', () => {
|
||||||
act(() => darkMode$.next(false));
|
act(() => darkMode$.next(false));
|
||||||
expect(result.current).toBe(EUI_CHARTS_THEME_LIGHT.theme);
|
expect(result.current).toBe(EUI_CHARTS_THEME_LIGHT.theme);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should not rerender when emitting the same value', () => {
|
||||||
|
const darkMode$ = new BehaviorSubject(false);
|
||||||
|
setupMockUiSettings.get$.mockReturnValue(darkMode$);
|
||||||
|
const themeService = new ThemeService();
|
||||||
|
themeService.init(setupMockUiSettings);
|
||||||
|
const { useChartsTheme } = themeService;
|
||||||
|
|
||||||
|
const renderCounter = jest.fn();
|
||||||
|
const Wrapper = () => {
|
||||||
|
useChartsTheme();
|
||||||
|
renderCounter();
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
render(<Wrapper />);
|
||||||
|
expect(renderCounter).toHaveBeenCalledTimes(1);
|
||||||
|
renderAct(() => darkMode$.next(true));
|
||||||
|
expect(renderCounter).toHaveBeenCalledTimes(2);
|
||||||
|
renderAct(() => darkMode$.next(true));
|
||||||
|
renderAct(() => darkMode$.next(true));
|
||||||
|
renderAct(() => darkMode$.next(true));
|
||||||
|
expect(renderCounter).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('useBaseChartTheme', () => {
|
describe('useBaseChartTheme', () => {
|
||||||
|
@ -123,5 +149,29 @@ describe('ThemeService', () => {
|
||||||
act(() => darkMode$.next(false));
|
act(() => darkMode$.next(false));
|
||||||
expect(result.current).toBe(LIGHT_THEME);
|
expect(result.current).toBe(LIGHT_THEME);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should not rerender when emitting the same value', () => {
|
||||||
|
const darkMode$ = new BehaviorSubject(false);
|
||||||
|
setupMockUiSettings.get$.mockReturnValue(darkMode$);
|
||||||
|
const themeService = new ThemeService();
|
||||||
|
themeService.init(setupMockUiSettings);
|
||||||
|
const { useChartsBaseTheme } = themeService;
|
||||||
|
|
||||||
|
const renderCounter = jest.fn();
|
||||||
|
const Wrapper = () => {
|
||||||
|
useChartsBaseTheme();
|
||||||
|
renderCounter();
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
render(<Wrapper />);
|
||||||
|
expect(renderCounter).toHaveBeenCalledTimes(1);
|
||||||
|
renderAct(() => darkMode$.next(true));
|
||||||
|
expect(renderCounter).toHaveBeenCalledTimes(2);
|
||||||
|
renderAct(() => darkMode$.next(true));
|
||||||
|
renderAct(() => darkMode$.next(true));
|
||||||
|
renderAct(() => darkMode$.next(true));
|
||||||
|
expect(renderCounter).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
* Side Public License, v 1.
|
* Side Public License, v 1.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import { Observable, BehaviorSubject } from 'rxjs';
|
import { Observable, BehaviorSubject } from 'rxjs';
|
||||||
|
|
||||||
import { CoreSetup } from 'kibana/public';
|
import { CoreSetup } from 'kibana/public';
|
||||||
|
@ -54,11 +54,18 @@ export class ThemeService {
|
||||||
/** A React hook for consuming the charts theme */
|
/** A React hook for consuming the charts theme */
|
||||||
public useChartsTheme = (): PartialTheme => {
|
public useChartsTheme = (): PartialTheme => {
|
||||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||||
const [value, update] = useState(this.chartsDefaultTheme);
|
const [value, update] = useState(this._chartsTheme$.getValue());
|
||||||
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||||
|
const ref = useRef(value);
|
||||||
|
|
||||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const s = this.chartsTheme$.subscribe(update);
|
const s = this.chartsTheme$.subscribe((val) => {
|
||||||
|
if (val !== ref.current) {
|
||||||
|
ref.current = val;
|
||||||
|
update(val);
|
||||||
|
}
|
||||||
|
});
|
||||||
return () => s.unsubscribe();
|
return () => s.unsubscribe();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
@ -68,11 +75,18 @@ export class ThemeService {
|
||||||
/** A React hook for consuming the charts theme */
|
/** A React hook for consuming the charts theme */
|
||||||
public useChartsBaseTheme = (): Theme => {
|
public useChartsBaseTheme = (): Theme => {
|
||||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||||
const [value, update] = useState(this.chartsDefaultBaseTheme);
|
const [value, update] = useState(this._chartsBaseTheme$.getValue());
|
||||||
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||||
|
const ref = useRef(value);
|
||||||
|
|
||||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const s = this.chartsBaseTheme$.subscribe(update);
|
const s = this.chartsBaseTheme$.subscribe((val) => {
|
||||||
|
if (val !== ref.current) {
|
||||||
|
ref.current = val;
|
||||||
|
update(val);
|
||||||
|
}
|
||||||
|
});
|
||||||
return () => s.unsubscribe();
|
return () => s.unsubscribe();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
|
@ -42,13 +42,17 @@ export const searchSourceCommonMock: jest.Mocked<ISearchStartSearchSource> = {
|
||||||
createEmpty: jest.fn().mockReturnValue(searchSourceInstanceMock),
|
createEmpty: jest.fn().mockReturnValue(searchSourceInstanceMock),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createSearchSourceMock = (fields?: SearchSourceFields) =>
|
export const createSearchSourceMock = (fields?: SearchSourceFields, response?: any) =>
|
||||||
new SearchSource(fields, {
|
new SearchSource(fields, {
|
||||||
getConfig: uiSettingsServiceMock.createStartContract().get,
|
getConfig: uiSettingsServiceMock.createStartContract().get,
|
||||||
search: jest
|
search: jest.fn().mockReturnValue(
|
||||||
.fn()
|
of(
|
||||||
.mockReturnValue(
|
response ?? {
|
||||||
of({ rawResponse: { hits: { hits: [], total: 0 } }, isPartial: false, isRunning: false })
|
rawResponse: { hits: { hits: [], total: 0 } },
|
||||||
),
|
isPartial: false,
|
||||||
|
isRunning: false,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
),
|
||||||
onResponse: jest.fn().mockImplementation((req, res) => res),
|
onResponse: jest.fn().mockImplementation((req, res) => res),
|
||||||
});
|
});
|
||||||
|
|
|
@ -71,6 +71,7 @@ const indexPattern = ({
|
||||||
getSourceFiltering: () => ({}),
|
getSourceFiltering: () => ({}),
|
||||||
getFieldByName: (name: string) => fields.getByName(name),
|
getFieldByName: (name: string) => fields.getByName(name),
|
||||||
timeFieldName: 'timestamp',
|
timeFieldName: 'timestamp',
|
||||||
|
getFormatterForField: () => ({ convert: () => 'formatted' }),
|
||||||
} as unknown) as IndexPattern;
|
} as unknown) as IndexPattern;
|
||||||
|
|
||||||
indexPattern.flattenHit = indexPatterns.flattenHitWrapper(indexPattern, indexPattern.metaFields);
|
indexPattern.flattenHit = indexPatterns.flattenHitWrapper(indexPattern, indexPattern.metaFields);
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
import { SavedSearch } from '../saved_searches';
|
import { SavedSearch } from '../saved_searches';
|
||||||
import { createSearchSourceMock } from '../../../data/public/mocks';
|
import { createSearchSourceMock } from '../../../data/public/mocks';
|
||||||
import { indexPatternMock } from './index_pattern';
|
import { indexPatternMock } from './index_pattern';
|
||||||
|
import { indexPatternWithTimefieldMock } from './index_pattern_with_timefield';
|
||||||
|
|
||||||
export const savedSearchMock = ({
|
export const savedSearchMock = ({
|
||||||
id: 'the-saved-search-id',
|
id: 'the-saved-search-id',
|
||||||
|
@ -31,3 +32,25 @@ export const savedSearchMock = ({
|
||||||
error: undefined,
|
error: undefined,
|
||||||
searchSource: createSearchSourceMock({ index: indexPatternMock }),
|
searchSource: createSearchSourceMock({ index: indexPatternMock }),
|
||||||
} as unknown) as SavedSearch;
|
} as unknown) as SavedSearch;
|
||||||
|
|
||||||
|
export const savedSearchMockWithTimeField = ({
|
||||||
|
id: 'the-saved-search-id-with-timefield',
|
||||||
|
type: 'search',
|
||||||
|
attributes: {
|
||||||
|
title: 'the-saved-search-title',
|
||||||
|
kibanaSavedObjectMeta: {
|
||||||
|
searchSourceJSON:
|
||||||
|
'{"highlightAll":true,"version":true,"query":{"query":"foo : \\"bar\\" ","language":"kuery"},"filter":[],"indexRefName":"kibanaSavedObjectMeta.searchSourceJSON.index"}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
references: [
|
||||||
|
{
|
||||||
|
name: 'kibanaSavedObjectMeta.searchSourceJSON.index',
|
||||||
|
type: 'index-pattern',
|
||||||
|
id: 'the-index-pattern-id',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
migrationVersion: { search: '7.5.0' },
|
||||||
|
error: undefined,
|
||||||
|
searchSource: createSearchSourceMock({ index: indexPatternWithTimefieldMock }),
|
||||||
|
} as unknown) as SavedSearch;
|
||||||
|
|
|
@ -5,10 +5,15 @@
|
||||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||||
* Side Public License, v 1.
|
* Side Public License, v 1.
|
||||||
*/
|
*/
|
||||||
|
import { EUI_CHARTS_THEME_LIGHT } from '@elastic/eui/dist/eui_charts_theme';
|
||||||
import { DiscoverServices } from '../build_services';
|
import { DiscoverServices } from '../build_services';
|
||||||
import { dataPluginMock } from '../../../data/public/mocks';
|
import { dataPluginMock } from '../../../data/public/mocks';
|
||||||
import { chromeServiceMock, coreMock, docLinksServiceMock } from '../../../../core/public/mocks';
|
import { chromeServiceMock, coreMock, docLinksServiceMock } from '../../../../core/public/mocks';
|
||||||
import { DEFAULT_COLUMNS_SETTING } from '../../common';
|
import {
|
||||||
|
DEFAULT_COLUMNS_SETTING,
|
||||||
|
SAMPLE_SIZE_SETTING,
|
||||||
|
SORT_DEFAULT_ORDER_SETTING,
|
||||||
|
} from '../../common';
|
||||||
import { savedSearchMock } from './saved_search';
|
import { savedSearchMock } from './saved_search';
|
||||||
import { UI_SETTINGS } from '../../../data/common';
|
import { UI_SETTINGS } from '../../../data/common';
|
||||||
import { TopNavMenu } from '../../../navigation/public';
|
import { TopNavMenu } from '../../../navigation/public';
|
||||||
|
@ -44,8 +49,15 @@ export const discoverServiceMock = ({
|
||||||
return [];
|
return [];
|
||||||
} else if (key === UI_SETTINGS.META_FIELDS) {
|
} else if (key === UI_SETTINGS.META_FIELDS) {
|
||||||
return [];
|
return [];
|
||||||
|
} else if (key === SAMPLE_SIZE_SETTING) {
|
||||||
|
return 250;
|
||||||
|
} else if (key === SORT_DEFAULT_ORDER_SETTING) {
|
||||||
|
return 'desc';
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
isDefault: (key: string) => {
|
||||||
|
return true;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
indexPatternFieldEditor: {
|
indexPatternFieldEditor: {
|
||||||
openEditor: jest.fn(),
|
openEditor: jest.fn(),
|
||||||
|
@ -60,4 +72,8 @@ export const discoverServiceMock = ({
|
||||||
metadata: {
|
metadata: {
|
||||||
branch: 'test',
|
branch: 'test',
|
||||||
},
|
},
|
||||||
|
theme: {
|
||||||
|
useChartsTheme: jest.fn(() => EUI_CHARTS_THEME_LIGHT.theme),
|
||||||
|
useChartsBaseTheme: jest.fn(() => EUI_CHARTS_THEME_LIGHT.theme),
|
||||||
|
},
|
||||||
} as unknown) as DiscoverServices;
|
} as unknown) as DiscoverServices;
|
||||||
|
|
|
@ -0,0 +1,122 @@
|
||||||
|
/*
|
||||||
|
* 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 React from 'react';
|
||||||
|
import { Subject, BehaviorSubject } from 'rxjs';
|
||||||
|
import { mountWithIntl } from '@kbn/test/jest';
|
||||||
|
import { setHeaderActionMenuMounter } from '../../../../../kibana_services';
|
||||||
|
import { esHits } from '../../../../../__mocks__/es_hits';
|
||||||
|
import { savedSearchMock } from '../../../../../__mocks__/saved_search';
|
||||||
|
import { createSearchSourceMock } from '../../../../../../../data/common/search/search_source/mocks';
|
||||||
|
import { GetStateReturn } from '../../services/discover_state';
|
||||||
|
import { DataCharts$, DataTotalHits$ } from '../../services/use_saved_search';
|
||||||
|
import { discoverServiceMock } from '../../../../../__mocks__/services';
|
||||||
|
import { FetchStatus } from '../../../../types';
|
||||||
|
import { Chart } from './point_series';
|
||||||
|
import { DiscoverChart } from './discover_chart';
|
||||||
|
|
||||||
|
setHeaderActionMenuMounter(jest.fn());
|
||||||
|
|
||||||
|
function getProps(timefield?: string) {
|
||||||
|
const searchSourceMock = createSearchSourceMock({});
|
||||||
|
const services = discoverServiceMock;
|
||||||
|
services.data.query.timefilter.timefilter.getTime = () => {
|
||||||
|
return { from: '2020-05-14T11:05:13.590', to: '2020-05-14T11:20:13.590' };
|
||||||
|
};
|
||||||
|
|
||||||
|
const totalHits$ = new BehaviorSubject({
|
||||||
|
fetchStatus: FetchStatus.COMPLETE,
|
||||||
|
result: Number(esHits.length),
|
||||||
|
}) as DataTotalHits$;
|
||||||
|
|
||||||
|
const chartData = ({
|
||||||
|
xAxisOrderedValues: [
|
||||||
|
1623880800000,
|
||||||
|
1623967200000,
|
||||||
|
1624053600000,
|
||||||
|
1624140000000,
|
||||||
|
1624226400000,
|
||||||
|
1624312800000,
|
||||||
|
1624399200000,
|
||||||
|
1624485600000,
|
||||||
|
1624572000000,
|
||||||
|
1624658400000,
|
||||||
|
1624744800000,
|
||||||
|
1624831200000,
|
||||||
|
1624917600000,
|
||||||
|
1625004000000,
|
||||||
|
1625090400000,
|
||||||
|
],
|
||||||
|
xAxisFormat: { id: 'date', params: { pattern: 'YYYY-MM-DD' } },
|
||||||
|
xAxisLabel: 'order_date per day',
|
||||||
|
yAxisFormat: { id: 'number' },
|
||||||
|
ordered: {
|
||||||
|
date: true,
|
||||||
|
interval: {
|
||||||
|
asMilliseconds: jest.fn(),
|
||||||
|
},
|
||||||
|
intervalESUnit: 'd',
|
||||||
|
intervalESValue: 1,
|
||||||
|
min: '2021-03-18T08:28:56.411Z',
|
||||||
|
max: '2021-07-01T07:28:56.411Z',
|
||||||
|
},
|
||||||
|
yAxisLabel: 'Count',
|
||||||
|
values: [
|
||||||
|
{ x: 1623880800000, y: 134 },
|
||||||
|
{ x: 1623967200000, y: 152 },
|
||||||
|
{ x: 1624053600000, y: 141 },
|
||||||
|
{ x: 1624140000000, y: 138 },
|
||||||
|
{ x: 1624226400000, y: 142 },
|
||||||
|
{ x: 1624312800000, y: 157 },
|
||||||
|
{ x: 1624399200000, y: 149 },
|
||||||
|
{ x: 1624485600000, y: 146 },
|
||||||
|
{ x: 1624572000000, y: 170 },
|
||||||
|
{ x: 1624658400000, y: 137 },
|
||||||
|
{ x: 1624744800000, y: 150 },
|
||||||
|
{ x: 1624831200000, y: 144 },
|
||||||
|
{ x: 1624917600000, y: 147 },
|
||||||
|
{ x: 1625004000000, y: 137 },
|
||||||
|
{ x: 1625090400000, y: 66 },
|
||||||
|
],
|
||||||
|
} as unknown) as Chart;
|
||||||
|
|
||||||
|
const charts$ = new BehaviorSubject({
|
||||||
|
fetchStatus: FetchStatus.COMPLETE,
|
||||||
|
chartData,
|
||||||
|
bucketInterval: {
|
||||||
|
scaled: true,
|
||||||
|
description: 'test',
|
||||||
|
scale: 2,
|
||||||
|
},
|
||||||
|
}) as DataCharts$;
|
||||||
|
|
||||||
|
return {
|
||||||
|
isLegacy: false,
|
||||||
|
resetQuery: jest.fn(),
|
||||||
|
savedSearch: savedSearchMock,
|
||||||
|
savedSearchDataChart$: charts$,
|
||||||
|
savedSearchDataTotalHits$: totalHits$,
|
||||||
|
savedSearchRefetch$: new Subject(),
|
||||||
|
searchSource: searchSourceMock,
|
||||||
|
services,
|
||||||
|
state: { columns: [] },
|
||||||
|
stateContainer: {} as GetStateReturn,
|
||||||
|
timefield,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Discover chart', () => {
|
||||||
|
test('render without timefield', () => {
|
||||||
|
const component = mountWithIntl(<DiscoverChart {...getProps()} />);
|
||||||
|
expect(component.find('[data-test-subj="discoverChartToggle"]').exists()).toBeFalsy();
|
||||||
|
});
|
||||||
|
test('render with filefield', () => {
|
||||||
|
const component = mountWithIntl(<DiscoverChart {...getProps('timefield')} />);
|
||||||
|
expect(component.find('[data-test-subj="discoverChartToggle"]').exists()).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
|
@ -5,48 +5,43 @@
|
||||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||||
* Side Public License, v 1.
|
* Side Public License, v 1.
|
||||||
*/
|
*/
|
||||||
import React, { useCallback, useEffect, useRef } from 'react';
|
import React, { useCallback, useEffect, useRef, memo } from 'react';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import { EuiFlexGroup, EuiFlexItem, EuiButtonEmpty, EuiSpacer } from '@elastic/eui';
|
import { EuiFlexGroup, EuiFlexItem, EuiButtonEmpty, EuiSpacer } from '@elastic/eui';
|
||||||
import { IUiSettingsClient } from 'kibana/public';
|
|
||||||
import { i18n } from '@kbn/i18n';
|
import { i18n } from '@kbn/i18n';
|
||||||
import { HitsCounter } from '../hits_counter';
|
import { HitsCounter } from '../hits_counter';
|
||||||
import { DataPublicPluginStart, IndexPattern, search } from '../../../../../../../data/public';
|
import { search } from '../../../../../../../data/public';
|
||||||
import { TimechartHeader } from '../timechart_header';
|
import { TimechartHeader } from '../timechart_header';
|
||||||
import { SavedSearch } from '../../../../../saved_searches';
|
import { SavedSearch } from '../../../../../saved_searches';
|
||||||
import { AppState, GetStateReturn } from '../../services/discover_state';
|
import { AppState, GetStateReturn } from '../../services/discover_state';
|
||||||
import { TimechartBucketInterval } from '../timechart_header/timechart_header';
|
|
||||||
import { Chart as IChart } from './point_series';
|
|
||||||
import { DiscoverHistogram } from './histogram';
|
import { DiscoverHistogram } from './histogram';
|
||||||
|
import { DataCharts$, DataTotalHits$ } from '../../services/use_saved_search';
|
||||||
|
import { DiscoverServices } from '../../../../../build_services';
|
||||||
|
|
||||||
const TimechartHeaderMemoized = React.memo(TimechartHeader);
|
const TimechartHeaderMemoized = memo(TimechartHeader);
|
||||||
const DiscoverHistogramMemoized = React.memo(DiscoverHistogram);
|
const DiscoverHistogramMemoized = memo(DiscoverHistogram);
|
||||||
export function DiscoverChart({
|
export function DiscoverChart({
|
||||||
config,
|
|
||||||
data,
|
|
||||||
bucketInterval,
|
|
||||||
chartData,
|
|
||||||
hits,
|
|
||||||
isLegacy,
|
isLegacy,
|
||||||
resetQuery,
|
resetQuery,
|
||||||
savedSearch,
|
savedSearch,
|
||||||
|
savedSearchDataChart$,
|
||||||
|
savedSearchDataTotalHits$,
|
||||||
|
services,
|
||||||
state,
|
state,
|
||||||
stateContainer,
|
stateContainer,
|
||||||
timefield,
|
timefield,
|
||||||
}: {
|
}: {
|
||||||
config: IUiSettingsClient;
|
|
||||||
data: DataPublicPluginStart;
|
|
||||||
bucketInterval?: TimechartBucketInterval;
|
|
||||||
chartData?: IChart;
|
|
||||||
hits?: number;
|
|
||||||
indexPattern: IndexPattern;
|
|
||||||
isLegacy: boolean;
|
isLegacy: boolean;
|
||||||
resetQuery: () => void;
|
resetQuery: () => void;
|
||||||
savedSearch: SavedSearch;
|
savedSearch: SavedSearch;
|
||||||
|
savedSearchDataChart$: DataCharts$;
|
||||||
|
savedSearchDataTotalHits$: DataTotalHits$;
|
||||||
|
services: DiscoverServices;
|
||||||
state: AppState;
|
state: AppState;
|
||||||
stateContainer: GetStateReturn;
|
stateContainer: GetStateReturn;
|
||||||
timefield?: string;
|
timefield?: string;
|
||||||
}) {
|
}) {
|
||||||
|
const { data, uiSettings: config } = services;
|
||||||
const chartRef = useRef<{ element: HTMLElement | null; moveFocus: boolean }>({
|
const chartRef = useRef<{ element: HTMLElement | null; moveFocus: boolean }>({
|
||||||
element: null,
|
element: null,
|
||||||
moveFocus: false,
|
moveFocus: false,
|
||||||
|
@ -93,7 +88,7 @@ export function DiscoverChart({
|
||||||
className="dscResuntCount__title eui-textTruncate eui-textNoWrap"
|
className="dscResuntCount__title eui-textTruncate eui-textNoWrap"
|
||||||
>
|
>
|
||||||
<HitsCounter
|
<HitsCounter
|
||||||
hits={hits}
|
savedSearchData$={savedSearchDataTotalHits$}
|
||||||
showResetButton={!!(savedSearch && savedSearch.id)}
|
showResetButton={!!(savedSearch && savedSearch.id)}
|
||||||
onResetQuery={resetQuery}
|
onResetQuery={resetQuery}
|
||||||
/>
|
/>
|
||||||
|
@ -106,7 +101,7 @@ export function DiscoverChart({
|
||||||
options={search.aggs.intervalOptions}
|
options={search.aggs.intervalOptions}
|
||||||
onChangeInterval={onChangeInterval}
|
onChangeInterval={onChangeInterval}
|
||||||
stateInterval={state.interval || ''}
|
stateInterval={state.interval || ''}
|
||||||
bucketInterval={bucketInterval}
|
savedSearchData$={savedSearchDataChart$}
|
||||||
/>
|
/>
|
||||||
</EuiFlexItem>
|
</EuiFlexItem>
|
||||||
)}
|
)}
|
||||||
|
@ -130,7 +125,7 @@ export function DiscoverChart({
|
||||||
)}
|
)}
|
||||||
</EuiFlexGroup>
|
</EuiFlexGroup>
|
||||||
</EuiFlexItem>
|
</EuiFlexItem>
|
||||||
{!state.hideChart && chartData && (
|
{timefield && !state.hideChart && (
|
||||||
<EuiFlexItem grow={false}>
|
<EuiFlexItem grow={false}>
|
||||||
<section
|
<section
|
||||||
ref={(element) => (chartRef.current.element = element)}
|
ref={(element) => (chartRef.current.element = element)}
|
||||||
|
@ -145,8 +140,9 @@ export function DiscoverChart({
|
||||||
data-test-subj="discoverChart"
|
data-test-subj="discoverChart"
|
||||||
>
|
>
|
||||||
<DiscoverHistogramMemoized
|
<DiscoverHistogramMemoized
|
||||||
chartData={chartData}
|
savedSearchData$={savedSearchDataChart$}
|
||||||
timefilterUpdateHandler={timefilterUpdateHandler}
|
timefilterUpdateHandler={timefilterUpdateHandler}
|
||||||
|
services={services}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
.dscChart__loading {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
flex: 1 0 100%;
|
||||||
|
text-align: center;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
|
@ -5,45 +5,41 @@
|
||||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||||
* Side Public License, v 1.
|
* Side Public License, v 1.
|
||||||
*/
|
*/
|
||||||
|
import './histogram.scss';
|
||||||
import moment, { unitOfTime } from 'moment-timezone';
|
import moment, { unitOfTime } from 'moment-timezone';
|
||||||
import React, { Component } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import { EuiLoadingChart, EuiSpacer, EuiText } from '@elastic/eui';
|
||||||
|
import { FormattedMessage } from '@kbn/i18n/react';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Axis,
|
Axis,
|
||||||
|
BrushEndListener,
|
||||||
Chart,
|
Chart,
|
||||||
|
ElementClickListener,
|
||||||
HistogramBarSeries,
|
HistogramBarSeries,
|
||||||
Position,
|
Position,
|
||||||
ScaleType,
|
ScaleType,
|
||||||
Settings,
|
Settings,
|
||||||
TooltipType,
|
TooltipType,
|
||||||
ElementClickListener,
|
|
||||||
XYChartElementEvent,
|
XYChartElementEvent,
|
||||||
BrushEndListener,
|
|
||||||
Theme,
|
|
||||||
} from '@elastic/charts';
|
} from '@elastic/charts';
|
||||||
|
|
||||||
import { IUiSettingsClient } from 'kibana/public';
|
import { IUiSettingsClient } from 'kibana/public';
|
||||||
import { EuiChartThemeType } from '@elastic/eui/dist/eui_charts_theme';
|
|
||||||
import { Subscription, combineLatest } from 'rxjs';
|
|
||||||
import { getServices } from '../../../../../kibana_services';
|
|
||||||
import { Chart as IChart } from './point_series';
|
|
||||||
import {
|
import {
|
||||||
CurrentTime,
|
CurrentTime,
|
||||||
Endzones,
|
Endzones,
|
||||||
getAdjustedInterval,
|
getAdjustedInterval,
|
||||||
renderEndzoneTooltip,
|
renderEndzoneTooltip,
|
||||||
} from '../../../../../../../charts/public';
|
} from '../../../../../../../charts/public';
|
||||||
|
import { DataCharts$, DataChartsMessage } from '../../services/use_saved_search';
|
||||||
|
import { FetchStatus } from '../../../../types';
|
||||||
|
import { DiscoverServices } from '../../../../../build_services';
|
||||||
|
import { useDataState } from '../../utils/use_data_state';
|
||||||
|
|
||||||
export interface DiscoverHistogramProps {
|
export interface DiscoverHistogramProps {
|
||||||
chartData: IChart;
|
savedSearchData$: DataCharts$;
|
||||||
timefilterUpdateHandler: (ranges: { from: number; to: number }) => void;
|
timefilterUpdateHandler: (ranges: { from: number; to: number }) => void;
|
||||||
}
|
services: DiscoverServices;
|
||||||
|
|
||||||
interface DiscoverHistogramState {
|
|
||||||
chartsTheme: EuiChartThemeType['theme'];
|
|
||||||
chartsBaseTheme: Theme;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTimezone(uiSettings: IUiSettingsClient) {
|
function getTimezone(uiSettings: IUiSettingsClient) {
|
||||||
|
@ -56,154 +52,149 @@ function getTimezone(uiSettings: IUiSettingsClient) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class DiscoverHistogram extends Component<DiscoverHistogramProps, DiscoverHistogramState> {
|
export function DiscoverHistogram({
|
||||||
public static propTypes = {
|
savedSearchData$,
|
||||||
chartData: PropTypes.object,
|
timefilterUpdateHandler,
|
||||||
timefilterUpdateHandler: PropTypes.func,
|
services,
|
||||||
};
|
}: DiscoverHistogramProps) {
|
||||||
|
const chartTheme = services.theme.useChartsTheme();
|
||||||
|
const chartBaseTheme = services.theme.useChartsBaseTheme();
|
||||||
|
|
||||||
private subscription?: Subscription;
|
const dataState: DataChartsMessage = useDataState(savedSearchData$);
|
||||||
public state = {
|
|
||||||
chartsTheme: getServices().theme.chartsDefaultTheme,
|
|
||||||
chartsBaseTheme: getServices().theme.chartsDefaultBaseTheme,
|
|
||||||
};
|
|
||||||
|
|
||||||
componentDidMount() {
|
const uiSettings = services.uiSettings;
|
||||||
this.subscription = combineLatest([
|
const timeZone = getTimezone(uiSettings);
|
||||||
getServices().theme.chartsTheme$,
|
const { chartData, fetchStatus } = dataState;
|
||||||
getServices().theme.chartsBaseTheme$,
|
|
||||||
]).subscribe(([chartsTheme, chartsBaseTheme]) =>
|
const onBrushEnd: BrushEndListener = useCallback(
|
||||||
this.setState({ chartsTheme, chartsBaseTheme })
|
({ x }) => {
|
||||||
|
if (!x) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const [from, to] = x;
|
||||||
|
timefilterUpdateHandler({ from, to });
|
||||||
|
},
|
||||||
|
[timefilterUpdateHandler]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onElementClick = useCallback(
|
||||||
|
(xInterval: number): ElementClickListener => ([elementData]) => {
|
||||||
|
const startRange = (elementData as XYChartElementEvent)[0].x;
|
||||||
|
|
||||||
|
const range = {
|
||||||
|
from: startRange,
|
||||||
|
to: startRange + xInterval,
|
||||||
|
};
|
||||||
|
|
||||||
|
timefilterUpdateHandler(range);
|
||||||
|
},
|
||||||
|
[timefilterUpdateHandler]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!chartData && fetchStatus === FetchStatus.LOADING) {
|
||||||
|
return (
|
||||||
|
<div className="dscChart__loading">
|
||||||
|
<EuiText size="xs" color="subdued">
|
||||||
|
<EuiLoadingChart mono size="l" />
|
||||||
|
<EuiSpacer size="s" />
|
||||||
|
<FormattedMessage id="discover.loadingChartResults" defaultMessage="Loading chart" />
|
||||||
|
</EuiText>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
if (!chartData) {
|
||||||
if (this.subscription) {
|
return null;
|
||||||
this.subscription.unsubscribe();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public onBrushEnd: BrushEndListener = ({ x }) => {
|
const formatXValue = (val: string) => {
|
||||||
if (!x) {
|
const xAxisFormat = chartData.xAxisFormat.params!.pattern;
|
||||||
return;
|
|
||||||
}
|
|
||||||
const [from, to] = x;
|
|
||||||
this.props.timefilterUpdateHandler({ from, to });
|
|
||||||
};
|
|
||||||
|
|
||||||
public onElementClick = (xInterval: number): ElementClickListener => ([elementData]) => {
|
|
||||||
const startRange = (elementData as XYChartElementEvent)[0].x;
|
|
||||||
|
|
||||||
const range = {
|
|
||||||
from: startRange,
|
|
||||||
to: startRange + xInterval,
|
|
||||||
};
|
|
||||||
|
|
||||||
this.props.timefilterUpdateHandler(range);
|
|
||||||
};
|
|
||||||
|
|
||||||
public formatXValue = (val: string) => {
|
|
||||||
const xAxisFormat = this.props.chartData.xAxisFormat.params!.pattern;
|
|
||||||
|
|
||||||
return moment(val).format(xAxisFormat);
|
return moment(val).format(xAxisFormat);
|
||||||
};
|
};
|
||||||
|
|
||||||
public render() {
|
const data = chartData.values;
|
||||||
const uiSettings = getServices().uiSettings;
|
const isDarkMode = uiSettings.get('theme:darkMode');
|
||||||
const timeZone = getTimezone(uiSettings);
|
|
||||||
const { chartData } = this.props;
|
|
||||||
const { chartsTheme, chartsBaseTheme } = this.state;
|
|
||||||
|
|
||||||
if (!chartData) {
|
/*
|
||||||
return null;
|
* Deprecation: [interval] on [date_histogram] is deprecated, use [fixed_interval] or [calendar_interval].
|
||||||
}
|
* see https://github.com/elastic/kibana/issues/27410
|
||||||
|
* TODO: Once the Discover query has been update, we should change the below to use the new field
|
||||||
|
*/
|
||||||
|
const { intervalESValue, intervalESUnit, interval } = chartData.ordered;
|
||||||
|
const xInterval = interval.asMilliseconds();
|
||||||
|
|
||||||
const data = chartData.values;
|
const xValues = chartData.xAxisOrderedValues;
|
||||||
const isDarkMode = uiSettings.get('theme:darkMode');
|
const lastXValue = xValues[xValues.length - 1];
|
||||||
|
|
||||||
/*
|
const domain = chartData.ordered;
|
||||||
* Deprecation: [interval] on [date_histogram] is deprecated, use [fixed_interval] or [calendar_interval].
|
const domainStart = domain.min.valueOf();
|
||||||
* see https://github.com/elastic/kibana/issues/27410
|
const domainEnd = domain.max.valueOf();
|
||||||
* TODO: Once the Discover query has been update, we should change the below to use the new field
|
|
||||||
*/
|
|
||||||
const { intervalESValue, intervalESUnit, interval } = chartData.ordered;
|
|
||||||
const xInterval = interval.asMilliseconds();
|
|
||||||
|
|
||||||
const xValues = chartData.xAxisOrderedValues;
|
const domainMin = Math.min(data[0]?.x, domainStart);
|
||||||
const lastXValue = xValues[xValues.length - 1];
|
const domainMax = Math.max(domainEnd - xInterval, lastXValue);
|
||||||
|
|
||||||
const domain = chartData.ordered;
|
const xDomain = {
|
||||||
const domainStart = domain.min.valueOf();
|
min: domainMin,
|
||||||
const domainEnd = domain.max.valueOf();
|
max: domainMax,
|
||||||
|
minInterval: getAdjustedInterval(
|
||||||
|
xValues,
|
||||||
|
intervalESValue,
|
||||||
|
intervalESUnit as unitOfTime.Base,
|
||||||
|
timeZone
|
||||||
|
),
|
||||||
|
};
|
||||||
|
const tooltipProps = {
|
||||||
|
headerFormatter: renderEndzoneTooltip(xInterval, domainStart, domainEnd, formatXValue),
|
||||||
|
type: TooltipType.VerticalCursor,
|
||||||
|
};
|
||||||
|
|
||||||
const domainMin = Math.min(data[0]?.x, domainStart);
|
const xAxisFormatter = services.data.fieldFormats.deserialize(chartData.yAxisFormat);
|
||||||
const domainMax = Math.max(domainEnd - xInterval, lastXValue);
|
|
||||||
|
|
||||||
const xDomain = {
|
return (
|
||||||
min: domainMin,
|
<Chart size="100%">
|
||||||
max: domainMax,
|
<Settings
|
||||||
minInterval: getAdjustedInterval(
|
xDomain={xDomain}
|
||||||
xValues,
|
onBrushEnd={onBrushEnd}
|
||||||
intervalESValue,
|
onElementClick={onElementClick(xInterval)}
|
||||||
intervalESUnit as unitOfTime.Base,
|
tooltip={tooltipProps}
|
||||||
timeZone
|
theme={chartTheme}
|
||||||
),
|
baseTheme={chartBaseTheme}
|
||||||
};
|
/>
|
||||||
const tooltipProps = {
|
<Axis
|
||||||
headerFormatter: renderEndzoneTooltip(xInterval, domainStart, domainEnd, this.formatXValue),
|
id="discover-histogram-left-axis"
|
||||||
type: TooltipType.VerticalCursor,
|
position={Position.Left}
|
||||||
};
|
ticks={5}
|
||||||
|
title={chartData.yAxisLabel}
|
||||||
const xAxisFormatter = getServices().data.fieldFormats.deserialize(
|
integersOnly
|
||||||
this.props.chartData.yAxisFormat
|
tickFormat={(value) => xAxisFormatter.convert(value)}
|
||||||
);
|
/>
|
||||||
|
<Axis
|
||||||
return (
|
id="discover-histogram-bottom-axis"
|
||||||
<Chart size="100%">
|
position={Position.Bottom}
|
||||||
<Settings
|
title={chartData.xAxisLabel}
|
||||||
xDomain={xDomain}
|
tickFormat={formatXValue}
|
||||||
onBrushEnd={this.onBrushEnd}
|
ticks={10}
|
||||||
onElementClick={this.onElementClick(xInterval)}
|
/>
|
||||||
tooltip={tooltipProps}
|
<CurrentTime isDarkMode={isDarkMode} domainEnd={domainEnd} />
|
||||||
theme={chartsTheme}
|
<Endzones
|
||||||
baseTheme={chartsBaseTheme}
|
isDarkMode={isDarkMode}
|
||||||
/>
|
domainStart={domainStart}
|
||||||
<Axis
|
domainEnd={domainEnd}
|
||||||
id="discover-histogram-left-axis"
|
interval={xDomain.minInterval}
|
||||||
position={Position.Left}
|
domainMin={xDomain.min}
|
||||||
ticks={5}
|
domainMax={xDomain.max}
|
||||||
title={chartData.yAxisLabel}
|
/>
|
||||||
integersOnly
|
<HistogramBarSeries
|
||||||
tickFormat={(value) => xAxisFormatter.convert(value)}
|
id="discover-histogram"
|
||||||
/>
|
minBarHeight={2}
|
||||||
<Axis
|
xScaleType={ScaleType.Time}
|
||||||
id="discover-histogram-bottom-axis"
|
yScaleType={ScaleType.Linear}
|
||||||
position={Position.Bottom}
|
xAccessor="x"
|
||||||
title={chartData.xAxisLabel}
|
yAccessors={['y']}
|
||||||
tickFormat={this.formatXValue}
|
data={data}
|
||||||
ticks={10}
|
timeZone={timeZone}
|
||||||
/>
|
name={chartData.yAxisLabel}
|
||||||
<CurrentTime isDarkMode={isDarkMode} domainEnd={domainEnd} />
|
/>
|
||||||
<Endzones
|
</Chart>
|
||||||
isDarkMode={isDarkMode}
|
);
|
||||||
domainStart={domainStart}
|
|
||||||
domainEnd={domainEnd}
|
|
||||||
interval={xDomain.minInterval}
|
|
||||||
domainMin={xDomain.min}
|
|
||||||
domainMax={xDomain.max}
|
|
||||||
/>
|
|
||||||
<HistogramBarSeries
|
|
||||||
id="discover-histogram"
|
|
||||||
minBarHeight={2}
|
|
||||||
xScaleType={ScaleType.Time}
|
|
||||||
yScaleType={ScaleType.Linear}
|
|
||||||
xAccessor="x"
|
|
||||||
yAccessors={['y']}
|
|
||||||
data={data}
|
|
||||||
timeZone={timeZone}
|
|
||||||
name={chartData.yAxisLabel}
|
|
||||||
/>
|
|
||||||
</Chart>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,120 @@
|
||||||
|
/*
|
||||||
|
* 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 { buildPointSeriesData } from './point_series';
|
||||||
|
import moment from 'moment';
|
||||||
|
import { Unit } from '@elastic/datemath';
|
||||||
|
|
||||||
|
describe('buildPointSeriesData', () => {
|
||||||
|
test('with valid data', () => {
|
||||||
|
const table = {
|
||||||
|
type: 'datatable',
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
id: 'col-0-2',
|
||||||
|
name: 'order_date per 30 days',
|
||||||
|
meta: {
|
||||||
|
type: 'date',
|
||||||
|
field: 'order_date',
|
||||||
|
index: 'kibana_sample_data_ecommerce',
|
||||||
|
params: { id: 'date', params: { pattern: 'YYYY-MM-DD' } },
|
||||||
|
source: 'esaggs',
|
||||||
|
sourceParams: {
|
||||||
|
indexPatternId: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f',
|
||||||
|
id: '2',
|
||||||
|
enabled: true,
|
||||||
|
type: 'date_histogram',
|
||||||
|
params: {
|
||||||
|
field: 'order_date',
|
||||||
|
timeRange: { from: 'now-15y', to: 'now' },
|
||||||
|
useNormalizedEsInterval: true,
|
||||||
|
scaleMetricValues: false,
|
||||||
|
interval: 'auto',
|
||||||
|
used_interval: '30d',
|
||||||
|
drop_partials: false,
|
||||||
|
min_doc_count: 1,
|
||||||
|
extended_bounds: {},
|
||||||
|
},
|
||||||
|
schema: 'segment',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'col-1-1',
|
||||||
|
name: 'Count',
|
||||||
|
meta: {
|
||||||
|
type: 'number',
|
||||||
|
index: 'kibana_sample_data_ecommerce',
|
||||||
|
params: { id: 'number' },
|
||||||
|
source: 'esaggs',
|
||||||
|
sourceParams: {
|
||||||
|
indexPatternId: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f',
|
||||||
|
id: '1',
|
||||||
|
enabled: true,
|
||||||
|
type: 'count',
|
||||||
|
params: {},
|
||||||
|
schema: 'metric',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
rows: [{ 'col-0-2': 1625176800000, 'col-1-1': 2139 }],
|
||||||
|
};
|
||||||
|
const dimensions = {
|
||||||
|
x: {
|
||||||
|
accessor: 0,
|
||||||
|
label: 'order_date per 30 days',
|
||||||
|
format: { id: 'date', params: { pattern: 'YYYY-MM-DD' } },
|
||||||
|
params: {
|
||||||
|
date: true,
|
||||||
|
interval: moment.duration(30, 'd'),
|
||||||
|
intervalESValue: 30,
|
||||||
|
intervalESUnit: 'd' as Unit,
|
||||||
|
format: 'YYYY-MM-DD',
|
||||||
|
bounds: {
|
||||||
|
min: moment('2006-07-29T11:08:13.078Z'),
|
||||||
|
max: moment('2021-07-29T11:08:13.078Z'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
y: { accessor: 1, format: { id: 'number' }, label: 'Count' },
|
||||||
|
} as const;
|
||||||
|
expect(buildPointSeriesData(table, dimensions)).toMatchInlineSnapshot(`
|
||||||
|
Object {
|
||||||
|
"ordered": Object {
|
||||||
|
"date": true,
|
||||||
|
"interval": "P30D",
|
||||||
|
"intervalESUnit": "d",
|
||||||
|
"intervalESValue": 30,
|
||||||
|
"max": "2021-07-29T11:08:13.078Z",
|
||||||
|
"min": "2006-07-29T11:08:13.078Z",
|
||||||
|
},
|
||||||
|
"values": Array [
|
||||||
|
Object {
|
||||||
|
"x": 1625176800000,
|
||||||
|
"y": 2139,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"xAxisFormat": Object {
|
||||||
|
"id": "date",
|
||||||
|
"params": Object {
|
||||||
|
"pattern": "YYYY-MM-DD",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"xAxisLabel": "order_date per 30 days",
|
||||||
|
"xAxisOrderedValues": Array [
|
||||||
|
1625176800000,
|
||||||
|
],
|
||||||
|
"yAxisFormat": Object {
|
||||||
|
"id": "number",
|
||||||
|
},
|
||||||
|
"yAxisLabel": "Count",
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
});
|
|
@ -11,6 +11,9 @@ import { mountWithIntl } from '@kbn/test/jest';
|
||||||
import { ReactWrapper } from 'enzyme';
|
import { ReactWrapper } from 'enzyme';
|
||||||
import { HitsCounter, HitsCounterProps } from './hits_counter';
|
import { HitsCounter, HitsCounterProps } from './hits_counter';
|
||||||
import { findTestSubject } from '@elastic/eui/lib/test';
|
import { findTestSubject } from '@elastic/eui/lib/test';
|
||||||
|
import { BehaviorSubject } from 'rxjs';
|
||||||
|
import { FetchStatus } from '../../../../types';
|
||||||
|
import { DataTotalHits$ } from '../../services/use_saved_search';
|
||||||
|
|
||||||
describe('hits counter', function () {
|
describe('hits counter', function () {
|
||||||
let props: HitsCounterProps;
|
let props: HitsCounterProps;
|
||||||
|
@ -20,7 +23,10 @@ describe('hits counter', function () {
|
||||||
props = {
|
props = {
|
||||||
onResetQuery: jest.fn(),
|
onResetQuery: jest.fn(),
|
||||||
showResetButton: true,
|
showResetButton: true,
|
||||||
hits: 2,
|
savedSearchData$: new BehaviorSubject({
|
||||||
|
fetchStatus: FetchStatus.COMPLETE,
|
||||||
|
result: 2,
|
||||||
|
}) as DataTotalHits$,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -30,9 +36,7 @@ describe('hits counter', function () {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('HitsCounter not renders a button when the showResetButton property is false', () => {
|
it('HitsCounter not renders a button when the showResetButton property is false', () => {
|
||||||
component = mountWithIntl(
|
component = mountWithIntl(<HitsCounter {...props} showResetButton={false} />);
|
||||||
<HitsCounter hits={2} showResetButton={false} onResetQuery={jest.fn()} />
|
|
||||||
);
|
|
||||||
expect(findTestSubject(component, 'resetSavedSearch').length).toBe(0);
|
expect(findTestSubject(component, 'resetSavedSearch').length).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -43,8 +47,17 @@ describe('hits counter', function () {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('expect to render 1,899 hits if 1899 hits given', function () {
|
it('expect to render 1,899 hits if 1899 hits given', function () {
|
||||||
|
const data$ = new BehaviorSubject({
|
||||||
|
fetchStatus: FetchStatus.COMPLETE,
|
||||||
|
result: 1899,
|
||||||
|
}) as DataTotalHits$;
|
||||||
component = mountWithIntl(
|
component = mountWithIntl(
|
||||||
<HitsCounter hits={1899} showResetButton={false} onResetQuery={jest.fn()} />
|
<HitsCounter
|
||||||
|
{...props}
|
||||||
|
savedSearchData$={data$}
|
||||||
|
showResetButton={false}
|
||||||
|
onResetQuery={jest.fn()}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
const hits = findTestSubject(component, 'discoverQueryHits');
|
const hits = findTestSubject(component, 'discoverQueryHits');
|
||||||
expect(hits.text()).toBe('1,899');
|
expect(hits.text()).toBe('1,899');
|
||||||
|
|
|
@ -7,18 +7,21 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import './hits_counter.scss';
|
import './hits_counter.scss';
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui';
|
import {
|
||||||
import { FormattedMessage, I18nProvider } from '@kbn/i18n/react';
|
EuiButtonEmpty,
|
||||||
|
EuiFlexGroup,
|
||||||
|
EuiFlexItem,
|
||||||
|
EuiText,
|
||||||
|
EuiLoadingSpinner,
|
||||||
|
} from '@elastic/eui';
|
||||||
|
import { FormattedMessage, FormattedNumber } from '@kbn/i18n/react';
|
||||||
import { i18n } from '@kbn/i18n';
|
import { i18n } from '@kbn/i18n';
|
||||||
import { formatNumWithCommas } from '../../../../helpers';
|
import { DataTotalHits$, DataTotalHitsMsg } from '../../services/use_saved_search';
|
||||||
|
import { FetchStatus } from '../../../../types';
|
||||||
|
import { useDataState } from '../../utils/use_data_state';
|
||||||
|
|
||||||
export interface HitsCounterProps {
|
export interface HitsCounterProps {
|
||||||
/**
|
|
||||||
* the number of query hits
|
|
||||||
*/
|
|
||||||
hits?: number;
|
|
||||||
/**
|
/**
|
||||||
* displays the reset button
|
* displays the reset button
|
||||||
*/
|
*/
|
||||||
|
@ -27,52 +30,81 @@ export interface HitsCounterProps {
|
||||||
* resets the query
|
* resets the query
|
||||||
*/
|
*/
|
||||||
onResetQuery: () => void;
|
onResetQuery: () => void;
|
||||||
|
/**
|
||||||
|
* saved search data observable
|
||||||
|
*/
|
||||||
|
savedSearchData$: DataTotalHits$;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function HitsCounter({ hits, showResetButton, onResetQuery }: HitsCounterProps) {
|
export function HitsCounter({ showResetButton, onResetQuery, savedSearchData$ }: HitsCounterProps) {
|
||||||
if (typeof hits === 'undefined') {
|
const data: DataTotalHitsMsg = useDataState(savedSearchData$);
|
||||||
|
|
||||||
|
const hits = data.result || 0;
|
||||||
|
if (!hits && data.fetchStatus === FetchStatus.LOADING) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const formattedHits = (
|
||||||
|
<strong
|
||||||
|
data-test-subj={
|
||||||
|
data.fetchStatus === FetchStatus.PARTIAL ? 'discoverQueryHitsPartial' : 'discoverQueryHits'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<FormattedNumber value={hits} />
|
||||||
|
</strong>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<I18nProvider>
|
<EuiFlexGroup
|
||||||
<EuiFlexGroup
|
className="dscHitsCounter"
|
||||||
className="dscHitsCounter"
|
gutterSize="s"
|
||||||
gutterSize="s"
|
responsive={false}
|
||||||
responsive={false}
|
justifyContent="center"
|
||||||
justifyContent="center"
|
alignItems="center"
|
||||||
alignItems="center"
|
>
|
||||||
>
|
<EuiFlexItem grow={false} aria-live="polite">
|
||||||
<EuiFlexItem grow={false}>
|
<EuiText>
|
||||||
<EuiText>
|
{data.fetchStatus === FetchStatus.PARTIAL && (
|
||||||
<strong data-test-subj="discoverQueryHits">{formatNumWithCommas(hits)}</strong>{' '}
|
<FormattedMessage
|
||||||
|
id="discover.partialHits"
|
||||||
|
defaultMessage="≥{formattedHits} {hits, plural, one {hit} other {hits}}"
|
||||||
|
values={{ hits, formattedHits }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{data.fetchStatus !== FetchStatus.PARTIAL && (
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id="discover.hitsPluralTitle"
|
id="discover.hitsPluralTitle"
|
||||||
defaultMessage="{hits, plural, one {hit} other {hits}}"
|
defaultMessage="{formattedHits} {hits, plural, one {hit} other {hits}}"
|
||||||
values={{
|
values={{ hits, formattedHits }}
|
||||||
hits,
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</EuiText>
|
)}
|
||||||
|
</EuiText>
|
||||||
|
</EuiFlexItem>
|
||||||
|
{data.fetchStatus === FetchStatus.PARTIAL && (
|
||||||
|
<EuiFlexItem grow={false}>
|
||||||
|
<EuiLoadingSpinner
|
||||||
|
size="m"
|
||||||
|
aria-label={i18n.translate('discover.hitCountSpinnerAriaLabel', {
|
||||||
|
defaultMessage: 'Final hit count still loading',
|
||||||
|
})}
|
||||||
|
/>
|
||||||
</EuiFlexItem>
|
</EuiFlexItem>
|
||||||
{showResetButton && (
|
)}
|
||||||
<EuiFlexItem grow={false}>
|
{showResetButton && (
|
||||||
<EuiButtonEmpty
|
<EuiFlexItem grow={false}>
|
||||||
iconType="refresh"
|
<EuiButtonEmpty
|
||||||
data-test-subj="resetSavedSearch"
|
iconType="refresh"
|
||||||
onClick={onResetQuery}
|
data-test-subj="resetSavedSearch"
|
||||||
size="s"
|
onClick={onResetQuery}
|
||||||
aria-label={i18n.translate('discover.reloadSavedSearchButton', {
|
size="s"
|
||||||
defaultMessage: 'Reset search',
|
aria-label={i18n.translate('discover.reloadSavedSearchButton', {
|
||||||
})}
|
defaultMessage: 'Reset search',
|
||||||
>
|
})}
|
||||||
<FormattedMessage
|
>
|
||||||
id="discover.reloadSavedSearchButton"
|
<FormattedMessage id="discover.reloadSavedSearchButton" defaultMessage="Reset search" />
|
||||||
defaultMessage="Reset search"
|
</EuiButtonEmpty>
|
||||||
/>
|
</EuiFlexItem>
|
||||||
</EuiButtonEmpty>
|
)}
|
||||||
</EuiFlexItem>
|
</EuiFlexGroup>
|
||||||
)}
|
|
||||||
</EuiFlexGroup>
|
|
||||||
</I18nProvider>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,74 @@
|
||||||
|
/*
|
||||||
|
* 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 React from 'react';
|
||||||
|
import { BehaviorSubject } from 'rxjs';
|
||||||
|
import { mountWithIntl } from '@kbn/test/jest';
|
||||||
|
import { setHeaderActionMenuMounter } from '../../../../../kibana_services';
|
||||||
|
import { esHits } from '../../../../../__mocks__/es_hits';
|
||||||
|
import { savedSearchMock } from '../../../../../__mocks__/saved_search';
|
||||||
|
import { GetStateReturn } from '../../services/discover_state';
|
||||||
|
import { DataDocuments$ } from '../../services/use_saved_search';
|
||||||
|
import { discoverServiceMock } from '../../../../../__mocks__/services';
|
||||||
|
import { FetchStatus } from '../../../../types';
|
||||||
|
import { DiscoverDocuments } from './discover_documents';
|
||||||
|
import { ElasticSearchHit } from '../../../../doc_views/doc_views_types';
|
||||||
|
import { indexPatternMock } from '../../../../../__mocks__/index_pattern';
|
||||||
|
|
||||||
|
setHeaderActionMenuMounter(jest.fn());
|
||||||
|
|
||||||
|
function getProps(fetchStatus: FetchStatus, hits: ElasticSearchHit[]) {
|
||||||
|
const services = discoverServiceMock;
|
||||||
|
services.data.query.timefilter.timefilter.getTime = () => {
|
||||||
|
return { from: '2020-05-14T11:05:13.590', to: '2020-05-14T11:20:13.590' };
|
||||||
|
};
|
||||||
|
|
||||||
|
const documents$ = new BehaviorSubject({
|
||||||
|
fetchStatus,
|
||||||
|
result: hits,
|
||||||
|
}) as DataDocuments$;
|
||||||
|
|
||||||
|
return {
|
||||||
|
expandedDoc: undefined,
|
||||||
|
indexPattern: indexPatternMock,
|
||||||
|
isMobile: jest.fn(() => false),
|
||||||
|
onAddFilter: jest.fn(),
|
||||||
|
savedSearch: savedSearchMock,
|
||||||
|
documents$,
|
||||||
|
searchSource: documents$,
|
||||||
|
services,
|
||||||
|
setExpandedDoc: jest.fn(),
|
||||||
|
state: { columns: [] },
|
||||||
|
stateContainer: {} as GetStateReturn,
|
||||||
|
navigateTo: jest.fn(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Discover documents layout', () => {
|
||||||
|
test('render loading when loading and no documents', () => {
|
||||||
|
const component = mountWithIntl(<DiscoverDocuments {...getProps(FetchStatus.LOADING, [])} />);
|
||||||
|
expect(component.find('.dscDocuments__loading').exists()).toBeTruthy();
|
||||||
|
expect(component.find('.dscTable').exists()).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('render complete when loading but documents were already fetched', () => {
|
||||||
|
const component = mountWithIntl(
|
||||||
|
<DiscoverDocuments {...getProps(FetchStatus.LOADING, esHits as ElasticSearchHit[])} />
|
||||||
|
);
|
||||||
|
expect(component.find('.dscDocuments__loading').exists()).toBeFalsy();
|
||||||
|
expect(component.find('.dscTable').exists()).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('render complete', () => {
|
||||||
|
const component = mountWithIntl(
|
||||||
|
<DiscoverDocuments {...getProps(FetchStatus.COMPLETE, esHits as ElasticSearchHit[])} />
|
||||||
|
);
|
||||||
|
expect(component.find('.dscDocuments__loading').exists()).toBeFalsy();
|
||||||
|
expect(component.find('.dscTable').exists()).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,194 @@
|
||||||
|
/*
|
||||||
|
* 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 React, { useRef, useMemo, useCallback, memo } from 'react';
|
||||||
|
import { EuiFlexItem, EuiSpacer, EuiText, EuiLoadingSpinner } from '@elastic/eui';
|
||||||
|
import { FormattedMessage } from '@kbn/i18n/react';
|
||||||
|
import { DocTableLegacy } from '../../../../angular/doc_table/create_doc_table_react';
|
||||||
|
import { SortPairArr } from '../../../../angular/doc_table/lib/get_sort';
|
||||||
|
import { DocViewFilterFn, ElasticSearchHit } from '../../../../doc_views/doc_views_types';
|
||||||
|
import { DiscoverGrid } from '../../../../components/discover_grid/discover_grid';
|
||||||
|
import { FetchStatus } from '../../../../types';
|
||||||
|
import {
|
||||||
|
DOC_HIDE_TIME_COLUMN_SETTING,
|
||||||
|
DOC_TABLE_LEGACY,
|
||||||
|
SAMPLE_SIZE_SETTING,
|
||||||
|
SEARCH_FIELDS_FROM_SOURCE,
|
||||||
|
} from '../../../../../../common';
|
||||||
|
import { useDataGridColumns } from '../../../../helpers/use_data_grid_columns';
|
||||||
|
import { IndexPattern } from '../../../../../../../data/common';
|
||||||
|
import { SavedSearch } from '../../../../../saved_searches';
|
||||||
|
import { DataDocumentsMsg, DataDocuments$ } from '../../services/use_saved_search';
|
||||||
|
import { DiscoverServices } from '../../../../../build_services';
|
||||||
|
import { AppState, GetStateReturn } from '../../services/discover_state';
|
||||||
|
import { useDataState } from '../../utils/use_data_state';
|
||||||
|
|
||||||
|
const DocTableLegacyMemoized = React.memo(DocTableLegacy);
|
||||||
|
const DataGridMemoized = React.memo(DiscoverGrid);
|
||||||
|
|
||||||
|
function DiscoverDocumentsComponent({
|
||||||
|
documents$,
|
||||||
|
expandedDoc,
|
||||||
|
indexPattern,
|
||||||
|
isMobile,
|
||||||
|
onAddFilter,
|
||||||
|
savedSearch,
|
||||||
|
services,
|
||||||
|
setExpandedDoc,
|
||||||
|
state,
|
||||||
|
stateContainer,
|
||||||
|
}: {
|
||||||
|
documents$: DataDocuments$;
|
||||||
|
expandedDoc?: ElasticSearchHit;
|
||||||
|
indexPattern: IndexPattern;
|
||||||
|
isMobile: () => boolean;
|
||||||
|
navigateTo: (url: string) => void;
|
||||||
|
onAddFilter: DocViewFilterFn;
|
||||||
|
savedSearch: SavedSearch;
|
||||||
|
services: DiscoverServices;
|
||||||
|
setExpandedDoc: (doc: ElasticSearchHit | undefined) => void;
|
||||||
|
state: AppState;
|
||||||
|
stateContainer: GetStateReturn;
|
||||||
|
}) {
|
||||||
|
const { capabilities, indexPatterns, uiSettings } = services;
|
||||||
|
const useNewFieldsApi = useMemo(() => !uiSettings.get(SEARCH_FIELDS_FROM_SOURCE), [uiSettings]);
|
||||||
|
|
||||||
|
const scrollableDesktop = useRef<HTMLDivElement>(null);
|
||||||
|
const isLegacy = useMemo(() => uiSettings.get(DOC_TABLE_LEGACY), [uiSettings]);
|
||||||
|
const sampleSize = useMemo(() => uiSettings.get(SAMPLE_SIZE_SETTING), [uiSettings]);
|
||||||
|
|
||||||
|
const documentState: DataDocumentsMsg = useDataState(documents$);
|
||||||
|
|
||||||
|
const rows = useMemo(() => documentState.result || [], [documentState.result]);
|
||||||
|
|
||||||
|
const { columns, onAddColumn, onRemoveColumn, onMoveColumn, onSetColumns } = useDataGridColumns({
|
||||||
|
capabilities,
|
||||||
|
config: uiSettings,
|
||||||
|
indexPattern,
|
||||||
|
indexPatterns,
|
||||||
|
setAppState: stateContainer.setAppState,
|
||||||
|
state,
|
||||||
|
useNewFieldsApi,
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Legacy function, remove once legacy grid is removed
|
||||||
|
*/
|
||||||
|
const onBackToTop = useCallback(() => {
|
||||||
|
if (scrollableDesktop && scrollableDesktop.current) {
|
||||||
|
scrollableDesktop.current.focus();
|
||||||
|
}
|
||||||
|
// Only the desktop one needs to target a specific container
|
||||||
|
if (!isMobile() && scrollableDesktop.current) {
|
||||||
|
scrollableDesktop.current.scrollTo(0, 0);
|
||||||
|
} else if (window) {
|
||||||
|
window.scrollTo(0, 0);
|
||||||
|
}
|
||||||
|
}, [scrollableDesktop, isMobile]);
|
||||||
|
|
||||||
|
const onResize = useCallback(
|
||||||
|
(colSettings: { columnId: string; width: number }) => {
|
||||||
|
const grid = { ...state.grid } || {};
|
||||||
|
const newColumns = { ...grid.columns } || {};
|
||||||
|
newColumns[colSettings.columnId] = {
|
||||||
|
width: colSettings.width,
|
||||||
|
};
|
||||||
|
const newGrid = { ...grid, columns: newColumns };
|
||||||
|
stateContainer.setAppState({ grid: newGrid });
|
||||||
|
},
|
||||||
|
[stateContainer, state]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onSort = useCallback(
|
||||||
|
(sort: string[][]) => {
|
||||||
|
stateContainer.setAppState({ sort });
|
||||||
|
},
|
||||||
|
[stateContainer]
|
||||||
|
);
|
||||||
|
|
||||||
|
const showTimeCol = useMemo(
|
||||||
|
() => !uiSettings.get(DOC_HIDE_TIME_COLUMN_SETTING, false) && !!indexPattern.timeFieldName,
|
||||||
|
[uiSettings, indexPattern.timeFieldName]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
(!documentState.result || documentState.result.length === 0) &&
|
||||||
|
documentState.fetchStatus === FetchStatus.LOADING
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<div className="dscDocuments__loading">
|
||||||
|
<EuiText size="xs" color="subdued">
|
||||||
|
<EuiLoadingSpinner />
|
||||||
|
<EuiSpacer size="s" />
|
||||||
|
<FormattedMessage id="discover.loadingDocuments" defaultMessage="Loading documents" />
|
||||||
|
</EuiText>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EuiFlexItem className="eui-yScroll">
|
||||||
|
<section
|
||||||
|
className="dscTable eui-yScroll eui-xScroll"
|
||||||
|
aria-labelledby="documentsAriaLabel"
|
||||||
|
ref={scrollableDesktop}
|
||||||
|
tabIndex={-1}
|
||||||
|
>
|
||||||
|
<h2 className="euiScreenReaderOnly" id="documentsAriaLabel">
|
||||||
|
<FormattedMessage id="discover.documentsAriaLabel" defaultMessage="Documents" />
|
||||||
|
</h2>
|
||||||
|
{isLegacy && rows && rows.length && (
|
||||||
|
<DocTableLegacyMemoized
|
||||||
|
columns={columns}
|
||||||
|
indexPattern={indexPattern}
|
||||||
|
rows={rows}
|
||||||
|
sort={state.sort || []}
|
||||||
|
searchDescription={savedSearch.description}
|
||||||
|
searchTitle={savedSearch.lastSavedTitle}
|
||||||
|
onAddColumn={onAddColumn}
|
||||||
|
onBackToTop={onBackToTop}
|
||||||
|
onFilter={onAddFilter}
|
||||||
|
onMoveColumn={onMoveColumn}
|
||||||
|
onRemoveColumn={onRemoveColumn}
|
||||||
|
onSort={onSort}
|
||||||
|
sampleSize={sampleSize}
|
||||||
|
useNewFieldsApi={useNewFieldsApi}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{!isLegacy && (
|
||||||
|
<div className="dscDiscoverGrid">
|
||||||
|
<DataGridMemoized
|
||||||
|
ariaLabelledBy="documentsAriaLabel"
|
||||||
|
columns={columns}
|
||||||
|
expandedDoc={expandedDoc}
|
||||||
|
indexPattern={indexPattern}
|
||||||
|
isLoading={documentState.fetchStatus === FetchStatus.LOADING}
|
||||||
|
rows={rows}
|
||||||
|
sort={(state.sort as SortPairArr[]) || []}
|
||||||
|
sampleSize={sampleSize}
|
||||||
|
searchDescription={savedSearch.description}
|
||||||
|
searchTitle={savedSearch.lastSavedTitle}
|
||||||
|
setExpandedDoc={setExpandedDoc}
|
||||||
|
showTimeCol={showTimeCol}
|
||||||
|
services={services}
|
||||||
|
settings={state.grid}
|
||||||
|
onAddColumn={onAddColumn}
|
||||||
|
onFilter={onAddFilter as DocViewFilterFn}
|
||||||
|
onRemoveColumn={onRemoveColumn}
|
||||||
|
onSetColumns={onSetColumns}
|
||||||
|
onSort={onSort}
|
||||||
|
onResize={onResize}
|
||||||
|
useNewFieldsApi={useNewFieldsApi}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
</EuiFlexItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DiscoverDocuments = memo(DiscoverDocumentsComponent);
|
|
@ -103,3 +103,12 @@ discover-app {
|
||||||
padding: $euiSizeXS $euiSizeS;
|
padding: $euiSizeXS $euiSizeS;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dscDocuments__loading {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
|
@ -20,9 +20,17 @@ import { SavedObject } from '../../../../../../../../core/types';
|
||||||
import { indexPatternWithTimefieldMock } from '../../../../../__mocks__/index_pattern_with_timefield';
|
import { indexPatternWithTimefieldMock } from '../../../../../__mocks__/index_pattern_with_timefield';
|
||||||
import { GetStateReturn } from '../../services/discover_state';
|
import { GetStateReturn } from '../../services/discover_state';
|
||||||
import { DiscoverLayoutProps } from './types';
|
import { DiscoverLayoutProps } from './types';
|
||||||
import { SavedSearchDataSubject } from '../../services/use_saved_search';
|
import {
|
||||||
|
DataCharts$,
|
||||||
|
DataDocuments$,
|
||||||
|
DataMain$,
|
||||||
|
DataTotalHits$,
|
||||||
|
} from '../../services/use_saved_search';
|
||||||
import { discoverServiceMock } from '../../../../../__mocks__/services';
|
import { discoverServiceMock } from '../../../../../__mocks__/services';
|
||||||
import { FetchStatus } from '../../../../types';
|
import { FetchStatus } from '../../../../types';
|
||||||
|
import { ElasticSearchHit } from '../../../../doc_views/doc_views_types';
|
||||||
|
import { RequestAdapter } from '../../../../../../../inspector';
|
||||||
|
import { Chart } from '../chart/point_series';
|
||||||
|
|
||||||
setHeaderActionMenuMounter(jest.fn());
|
setHeaderActionMenuMounter(jest.fn());
|
||||||
|
|
||||||
|
@ -37,23 +45,99 @@ function getProps(indexPattern: IndexPattern): DiscoverLayoutProps {
|
||||||
return { ...ip, ...{ attributes: { title: ip.title } } };
|
return { ...ip, ...{ attributes: { title: ip.title } } };
|
||||||
}) as unknown) as Array<SavedObject<IndexPatternAttributes>>;
|
}) as unknown) as Array<SavedObject<IndexPatternAttributes>>;
|
||||||
|
|
||||||
const savedSearch$ = new BehaviorSubject({
|
const main$ = new BehaviorSubject({
|
||||||
state: FetchStatus.COMPLETE,
|
fetchStatus: FetchStatus.COMPLETE,
|
||||||
rows: esHits,
|
foundDocuments: true,
|
||||||
fetchCounter: 1,
|
}) as DataMain$;
|
||||||
fieldCounts: {},
|
|
||||||
hits: Number(esHits.length),
|
const documents$ = new BehaviorSubject({
|
||||||
}) as SavedSearchDataSubject;
|
fetchStatus: FetchStatus.COMPLETE,
|
||||||
|
result: esHits as ElasticSearchHit[],
|
||||||
|
}) as DataDocuments$;
|
||||||
|
|
||||||
|
const totalHits$ = new BehaviorSubject({
|
||||||
|
fetchStatus: FetchStatus.COMPLETE,
|
||||||
|
result: Number(esHits.length),
|
||||||
|
}) as DataTotalHits$;
|
||||||
|
|
||||||
|
const chartData = ({
|
||||||
|
xAxisOrderedValues: [
|
||||||
|
1623880800000,
|
||||||
|
1623967200000,
|
||||||
|
1624053600000,
|
||||||
|
1624140000000,
|
||||||
|
1624226400000,
|
||||||
|
1624312800000,
|
||||||
|
1624399200000,
|
||||||
|
1624485600000,
|
||||||
|
1624572000000,
|
||||||
|
1624658400000,
|
||||||
|
1624744800000,
|
||||||
|
1624831200000,
|
||||||
|
1624917600000,
|
||||||
|
1625004000000,
|
||||||
|
1625090400000,
|
||||||
|
],
|
||||||
|
xAxisFormat: { id: 'date', params: { pattern: 'YYYY-MM-DD' } },
|
||||||
|
xAxisLabel: 'order_date per day',
|
||||||
|
yAxisFormat: { id: 'number' },
|
||||||
|
ordered: {
|
||||||
|
date: true,
|
||||||
|
interval: {
|
||||||
|
asMilliseconds: jest.fn(),
|
||||||
|
},
|
||||||
|
intervalESUnit: 'd',
|
||||||
|
intervalESValue: 1,
|
||||||
|
min: '2021-03-18T08:28:56.411Z',
|
||||||
|
max: '2021-07-01T07:28:56.411Z',
|
||||||
|
},
|
||||||
|
yAxisLabel: 'Count',
|
||||||
|
values: [
|
||||||
|
{ x: 1623880800000, y: 134 },
|
||||||
|
{ x: 1623967200000, y: 152 },
|
||||||
|
{ x: 1624053600000, y: 141 },
|
||||||
|
{ x: 1624140000000, y: 138 },
|
||||||
|
{ x: 1624226400000, y: 142 },
|
||||||
|
{ x: 1624312800000, y: 157 },
|
||||||
|
{ x: 1624399200000, y: 149 },
|
||||||
|
{ x: 1624485600000, y: 146 },
|
||||||
|
{ x: 1624572000000, y: 170 },
|
||||||
|
{ x: 1624658400000, y: 137 },
|
||||||
|
{ x: 1624744800000, y: 150 },
|
||||||
|
{ x: 1624831200000, y: 144 },
|
||||||
|
{ x: 1624917600000, y: 147 },
|
||||||
|
{ x: 1625004000000, y: 137 },
|
||||||
|
{ x: 1625090400000, y: 66 },
|
||||||
|
],
|
||||||
|
} as unknown) as Chart;
|
||||||
|
|
||||||
|
const charts$ = new BehaviorSubject({
|
||||||
|
fetchStatus: FetchStatus.COMPLETE,
|
||||||
|
chartData,
|
||||||
|
bucketInterval: {
|
||||||
|
scaled: true,
|
||||||
|
description: 'test',
|
||||||
|
scale: 2,
|
||||||
|
},
|
||||||
|
}) as DataCharts$;
|
||||||
|
|
||||||
|
const savedSearchData$ = {
|
||||||
|
main$,
|
||||||
|
documents$,
|
||||||
|
totalHits$,
|
||||||
|
charts$,
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
indexPattern,
|
indexPattern,
|
||||||
indexPatternList,
|
indexPatternList,
|
||||||
|
inspectorAdapters: { requests: new RequestAdapter() },
|
||||||
navigateTo: jest.fn(),
|
navigateTo: jest.fn(),
|
||||||
onChangeIndexPattern: jest.fn(),
|
onChangeIndexPattern: jest.fn(),
|
||||||
onUpdateQuery: jest.fn(),
|
onUpdateQuery: jest.fn(),
|
||||||
resetQuery: jest.fn(),
|
resetQuery: jest.fn(),
|
||||||
savedSearch: savedSearchMock,
|
savedSearch: savedSearchMock,
|
||||||
savedSearchData$: savedSearch$,
|
savedSearchData$,
|
||||||
savedSearchRefetch$: new Subject(),
|
savedSearchRefetch$: new Subject(),
|
||||||
searchSource: searchSourceMock,
|
searchSource: searchSourceMock,
|
||||||
services,
|
services,
|
||||||
|
|
|
@ -20,11 +20,10 @@ import {
|
||||||
} from '@elastic/eui';
|
} from '@elastic/eui';
|
||||||
import { i18n } from '@kbn/i18n';
|
import { i18n } from '@kbn/i18n';
|
||||||
import { METRIC_TYPE } from '@kbn/analytics';
|
import { METRIC_TYPE } from '@kbn/analytics';
|
||||||
import { FormattedMessage, I18nProvider } from '@kbn/i18n/react';
|
import { I18nProvider } from '@kbn/i18n/react';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { DiscoverNoResults } from '../no_results';
|
import { DiscoverNoResults } from '../no_results';
|
||||||
import { LoadingSpinner } from '../loading_spinner/loading_spinner';
|
import { LoadingSpinner } from '../loading_spinner/loading_spinner';
|
||||||
import { DocTableLegacy } from '../../../../angular/doc_table/create_doc_table_react';
|
|
||||||
import {
|
import {
|
||||||
esFilters,
|
esFilters,
|
||||||
IndexPatternField,
|
IndexPatternField,
|
||||||
|
@ -32,42 +31,28 @@ import {
|
||||||
} from '../../../../../../../data/public';
|
} from '../../../../../../../data/public';
|
||||||
import { DiscoverSidebarResponsive } from '../sidebar';
|
import { DiscoverSidebarResponsive } from '../sidebar';
|
||||||
import { DiscoverLayoutProps } from './types';
|
import { DiscoverLayoutProps } from './types';
|
||||||
import { SortPairArr } from '../../../../angular/doc_table/lib/get_sort';
|
import { DOC_TABLE_LEGACY, SEARCH_FIELDS_FROM_SOURCE } from '../../../../../../common';
|
||||||
import {
|
|
||||||
DOC_HIDE_TIME_COLUMN_SETTING,
|
|
||||||
DOC_TABLE_LEGACY,
|
|
||||||
SAMPLE_SIZE_SETTING,
|
|
||||||
SEARCH_FIELDS_FROM_SOURCE,
|
|
||||||
} from '../../../../../../common';
|
|
||||||
import { popularizeField } from '../../../../helpers/popularize_field';
|
import { popularizeField } from '../../../../helpers/popularize_field';
|
||||||
import { DocViewFilterFn } from '../../../../doc_views/doc_views_types';
|
|
||||||
import { DiscoverGrid } from '../../../../components/discover_grid/discover_grid';
|
|
||||||
import { DiscoverTopNav } from '../top_nav/discover_topnav';
|
import { DiscoverTopNav } from '../top_nav/discover_topnav';
|
||||||
import { ElasticSearchHit } from '../../../../doc_views/doc_views_types';
|
import { DocViewFilterFn, ElasticSearchHit } from '../../../../doc_views/doc_views_types';
|
||||||
import { DiscoverChart } from '../chart';
|
import { DiscoverChart } from '../chart';
|
||||||
import { getResultState } from '../../utils/get_result_state';
|
import { getResultState } from '../../utils/get_result_state';
|
||||||
import { InspectorSession } from '../../../../../../../inspector/public';
|
import { InspectorSession } from '../../../../../../../inspector/public';
|
||||||
import { DiscoverUninitialized } from '../uninitialized/uninitialized';
|
import { DiscoverUninitialized } from '../uninitialized/uninitialized';
|
||||||
import { SavedSearchDataMessage } from '../../services/use_saved_search';
|
import { DataMainMsg } from '../../services/use_saved_search';
|
||||||
import { useDataGridColumns } from '../../../../helpers/use_data_grid_columns';
|
import { useDataGridColumns } from '../../../../helpers/use_data_grid_columns';
|
||||||
|
import { DiscoverDocuments } from './discover_documents';
|
||||||
import { FetchStatus } from '../../../../types';
|
import { FetchStatus } from '../../../../types';
|
||||||
|
import { useDataState } from '../../utils/use_data_state';
|
||||||
|
|
||||||
const DocTableLegacyMemoized = React.memo(DocTableLegacy);
|
|
||||||
const SidebarMemoized = React.memo(DiscoverSidebarResponsive);
|
const SidebarMemoized = React.memo(DiscoverSidebarResponsive);
|
||||||
const DataGridMemoized = React.memo(DiscoverGrid);
|
|
||||||
const TopNavMemoized = React.memo(DiscoverTopNav);
|
const TopNavMemoized = React.memo(DiscoverTopNav);
|
||||||
const DiscoverChartMemoized = React.memo(DiscoverChart);
|
const DiscoverChartMemoized = React.memo(DiscoverChart);
|
||||||
|
|
||||||
interface DiscoverLayoutFetchState extends SavedSearchDataMessage {
|
|
||||||
state: FetchStatus;
|
|
||||||
fetchCounter: number;
|
|
||||||
fieldCounts: Record<string, number>;
|
|
||||||
rows: ElasticSearchHit[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function DiscoverLayout({
|
export function DiscoverLayout({
|
||||||
indexPattern,
|
indexPattern,
|
||||||
indexPatternList,
|
indexPatternList,
|
||||||
|
inspectorAdapters,
|
||||||
navigateTo,
|
navigateTo,
|
||||||
onChangeIndexPattern,
|
onChangeIndexPattern,
|
||||||
onUpdateQuery,
|
onUpdateQuery,
|
||||||
|
@ -82,38 +67,22 @@ export function DiscoverLayout({
|
||||||
}: DiscoverLayoutProps) {
|
}: DiscoverLayoutProps) {
|
||||||
const { trackUiMetric, capabilities, indexPatterns, data, uiSettings, filterManager } = services;
|
const { trackUiMetric, capabilities, indexPatterns, data, uiSettings, filterManager } = services;
|
||||||
|
|
||||||
const sampleSize = useMemo(() => uiSettings.get(SAMPLE_SIZE_SETTING), [uiSettings]);
|
|
||||||
const [expandedDoc, setExpandedDoc] = useState<ElasticSearchHit | undefined>(undefined);
|
const [expandedDoc, setExpandedDoc] = useState<ElasticSearchHit | undefined>(undefined);
|
||||||
const [inspectorSession, setInspectorSession] = useState<InspectorSession | undefined>(undefined);
|
const [inspectorSession, setInspectorSession] = useState<InspectorSession | undefined>(undefined);
|
||||||
const scrollableDesktop = useRef<HTMLDivElement>(null);
|
|
||||||
const collapseIcon = useRef<HTMLButtonElement>(null);
|
const collapseIcon = useRef<HTMLButtonElement>(null);
|
||||||
|
const fetchCounter = useRef<number>(0);
|
||||||
|
const { main$, charts$, totalHits$ } = savedSearchData$;
|
||||||
|
|
||||||
const [fetchState, setFetchState] = useState<DiscoverLayoutFetchState>({
|
const dataState: DataMainMsg = useDataState(main$);
|
||||||
state: savedSearchData$.getValue().state,
|
|
||||||
fetchCounter: 0,
|
|
||||||
fieldCounts: {},
|
|
||||||
rows: [],
|
|
||||||
});
|
|
||||||
const { state: fetchStatus, fetchCounter, inspectorAdapters, rows } = fetchState;
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const subscription = savedSearchData$.subscribe((next) => {
|
if (dataState.fetchStatus === FetchStatus.LOADING) {
|
||||||
if (
|
fetchCounter.current++;
|
||||||
(next.state && next.state !== fetchState.state) ||
|
}
|
||||||
(next.fetchCounter && next.fetchCounter !== fetchState.fetchCounter) ||
|
}, [dataState.fetchStatus]);
|
||||||
(next.rows && next.rows !== fetchState.rows) ||
|
|
||||||
(next.chartData && next.chartData !== fetchState.chartData)
|
|
||||||
) {
|
|
||||||
setFetchState({ ...fetchState, ...next });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return () => {
|
|
||||||
subscription.unsubscribe();
|
|
||||||
};
|
|
||||||
}, [savedSearchData$, fetchState]);
|
|
||||||
|
|
||||||
// collapse icon isn't displayed in mobile view, use it to detect which view is displayed
|
// collapse icon isn't displayed in mobile view, use it to detect which view is displayed
|
||||||
const isMobile = () => collapseIcon && !collapseIcon.current;
|
const isMobile = useCallback(() => collapseIcon && !collapseIcon.current, []);
|
||||||
const timeField = useMemo(() => {
|
const timeField = useMemo(() => {
|
||||||
return indexPatternsUtils.isDefault(indexPattern) ? indexPattern.timeFieldName : undefined;
|
return indexPatternsUtils.isDefault(indexPattern) ? indexPattern.timeFieldName : undefined;
|
||||||
}, [indexPattern]);
|
}, [indexPattern]);
|
||||||
|
@ -122,27 +91,18 @@ export function DiscoverLayout({
|
||||||
const isLegacy = useMemo(() => uiSettings.get(DOC_TABLE_LEGACY), [uiSettings]);
|
const isLegacy = useMemo(() => uiSettings.get(DOC_TABLE_LEGACY), [uiSettings]);
|
||||||
const useNewFieldsApi = useMemo(() => !uiSettings.get(SEARCH_FIELDS_FROM_SOURCE), [uiSettings]);
|
const useNewFieldsApi = useMemo(() => !uiSettings.get(SEARCH_FIELDS_FROM_SOURCE), [uiSettings]);
|
||||||
|
|
||||||
const resultState = useMemo(() => getResultState(fetchStatus, rows!), [fetchStatus, rows]);
|
const resultState = useMemo(
|
||||||
|
() => getResultState(dataState.fetchStatus, dataState.foundDocuments!),
|
||||||
const { columns, onAddColumn, onRemoveColumn, onMoveColumn, onSetColumns } = useDataGridColumns({
|
[dataState.fetchStatus, dataState.foundDocuments]
|
||||||
capabilities,
|
);
|
||||||
config: uiSettings,
|
|
||||||
indexPattern,
|
|
||||||
indexPatterns,
|
|
||||||
setAppState: stateContainer.setAppState,
|
|
||||||
state,
|
|
||||||
useNewFieldsApi,
|
|
||||||
});
|
|
||||||
|
|
||||||
const onOpenInspector = useCallback(() => {
|
const onOpenInspector = useCallback(() => {
|
||||||
// prevent overlapping
|
// prevent overlapping
|
||||||
if (inspectorAdapters) {
|
setExpandedDoc(undefined);
|
||||||
setExpandedDoc(undefined);
|
const session = services.inspector.open(inspectorAdapters, {
|
||||||
const session = services.inspector.open(inspectorAdapters, {
|
title: savedSearch.title,
|
||||||
title: savedSearch.title,
|
});
|
||||||
});
|
setInspectorSession(session);
|
||||||
setInspectorSession(session);
|
|
||||||
}
|
|
||||||
}, [setExpandedDoc, inspectorAdapters, savedSearch, services.inspector]);
|
}, [setExpandedDoc, inspectorAdapters, savedSearch, services.inspector]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -154,12 +114,15 @@ export function DiscoverLayout({
|
||||||
};
|
};
|
||||||
}, [inspectorSession]);
|
}, [inspectorSession]);
|
||||||
|
|
||||||
const onSort = useCallback(
|
const { columns, onAddColumn, onRemoveColumn } = useDataGridColumns({
|
||||||
(sort: string[][]) => {
|
capabilities,
|
||||||
stateContainer.setAppState({ sort });
|
config: uiSettings,
|
||||||
},
|
indexPattern,
|
||||||
[stateContainer]
|
indexPatterns,
|
||||||
);
|
setAppState: stateContainer.setAppState,
|
||||||
|
state,
|
||||||
|
useNewFieldsApi,
|
||||||
|
});
|
||||||
|
|
||||||
const onAddFilter = useCallback(
|
const onAddFilter = useCallback(
|
||||||
(field: IndexPatternField | string, values: string, operation: '+' | '-') => {
|
(field: IndexPatternField | string, values: string, operation: '+' | '-') => {
|
||||||
|
@ -179,33 +142,6 @@ export function DiscoverLayout({
|
||||||
},
|
},
|
||||||
[filterManager, indexPattern, indexPatterns, trackUiMetric]
|
[filterManager, indexPattern, indexPatterns, trackUiMetric]
|
||||||
);
|
);
|
||||||
/**
|
|
||||||
* Legacy function, remove once legacy grid is removed
|
|
||||||
*/
|
|
||||||
const onBackToTop = useCallback(() => {
|
|
||||||
if (scrollableDesktop && scrollableDesktop.current) {
|
|
||||||
scrollableDesktop.current.focus();
|
|
||||||
}
|
|
||||||
// Only the desktop one needs to target a specific container
|
|
||||||
if (!isMobile() && scrollableDesktop.current) {
|
|
||||||
scrollableDesktop.current.scrollTo(0, 0);
|
|
||||||
} else if (window) {
|
|
||||||
window.scrollTo(0, 0);
|
|
||||||
}
|
|
||||||
}, [scrollableDesktop]);
|
|
||||||
|
|
||||||
const onResize = useCallback(
|
|
||||||
(colSettings: { columnId: string; width: number }) => {
|
|
||||||
const grid = { ...state.grid } || {};
|
|
||||||
const newColumns = { ...grid.columns } || {};
|
|
||||||
newColumns[colSettings.columnId] = {
|
|
||||||
width: colSettings.width,
|
|
||||||
};
|
|
||||||
const newGrid = { ...grid, columns: newColumns };
|
|
||||||
stateContainer.setAppState({ grid: newGrid });
|
|
||||||
},
|
|
||||||
[stateContainer, state]
|
|
||||||
);
|
|
||||||
|
|
||||||
const onEditRuntimeField = useCallback(() => {
|
const onEditRuntimeField = useCallback(() => {
|
||||||
savedSearchRefetch$.next('reset');
|
savedSearchRefetch$.next('reset');
|
||||||
|
@ -219,14 +155,10 @@ export function DiscoverLayout({
|
||||||
}, [filterManager]);
|
}, [filterManager]);
|
||||||
|
|
||||||
const contentCentered = resultState === 'uninitialized' || resultState === 'none';
|
const contentCentered = resultState === 'uninitialized' || resultState === 'none';
|
||||||
const showTimeCol = useMemo(
|
|
||||||
() => !uiSettings.get(DOC_HIDE_TIME_COLUMN_SETTING, false) && !!indexPattern.timeFieldName,
|
|
||||||
[uiSettings, indexPattern.timeFieldName]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<I18nProvider>
|
<I18nProvider>
|
||||||
<EuiPage className="dscPage" data-fetch-counter={fetchCounter}>
|
<EuiPage className="dscPage" data-fetch-counter={fetchCounter.current}>
|
||||||
<TopNavMemoized
|
<TopNavMemoized
|
||||||
indexPattern={indexPattern}
|
indexPattern={indexPattern}
|
||||||
onOpenInspector={onOpenInspector}
|
onOpenInspector={onOpenInspector}
|
||||||
|
@ -247,8 +179,7 @@ export function DiscoverLayout({
|
||||||
<EuiFlexItem grow={false}>
|
<EuiFlexItem grow={false}>
|
||||||
<SidebarMemoized
|
<SidebarMemoized
|
||||||
columns={columns}
|
columns={columns}
|
||||||
fieldCounts={fetchState.fieldCounts}
|
documents$={savedSearchData$.documents$}
|
||||||
hits={rows}
|
|
||||||
indexPatternList={indexPatternList}
|
indexPatternList={indexPatternList}
|
||||||
onAddField={onAddColumn}
|
onAddField={onAddColumn}
|
||||||
onAddFilter={onAddFilter}
|
onAddFilter={onAddFilter}
|
||||||
|
@ -298,7 +229,7 @@ export function DiscoverLayout({
|
||||||
<DiscoverNoResults
|
<DiscoverNoResults
|
||||||
timeFieldName={timeField}
|
timeFieldName={timeField}
|
||||||
data={data}
|
data={data}
|
||||||
error={fetchState.fetchError}
|
error={dataState.error}
|
||||||
hasQuery={!!state.query?.query}
|
hasQuery={!!state.query?.query}
|
||||||
hasFilters={
|
hasFilters={
|
||||||
state.filters && state.filters.filter((f) => !f.meta.disabled).length > 0
|
state.filters && state.filters.filter((f) => !f.meta.disabled).length > 0
|
||||||
|
@ -320,82 +251,31 @@ export function DiscoverLayout({
|
||||||
>
|
>
|
||||||
<EuiFlexItem grow={false}>
|
<EuiFlexItem grow={false}>
|
||||||
<DiscoverChartMemoized
|
<DiscoverChartMemoized
|
||||||
config={uiSettings}
|
|
||||||
chartData={fetchState.chartData}
|
|
||||||
bucketInterval={fetchState.bucketInterval}
|
|
||||||
data={data}
|
|
||||||
hits={fetchState.hits}
|
|
||||||
indexPattern={indexPattern}
|
|
||||||
isLegacy={isLegacy}
|
isLegacy={isLegacy}
|
||||||
state={state}
|
state={state}
|
||||||
resetQuery={resetQuery}
|
resetQuery={resetQuery}
|
||||||
savedSearch={savedSearch}
|
savedSearch={savedSearch}
|
||||||
|
savedSearchDataChart$={charts$}
|
||||||
|
savedSearchDataTotalHits$={totalHits$}
|
||||||
|
services={services}
|
||||||
stateContainer={stateContainer}
|
stateContainer={stateContainer}
|
||||||
timefield={timeField}
|
timefield={timeField}
|
||||||
/>
|
/>
|
||||||
</EuiFlexItem>
|
</EuiFlexItem>
|
||||||
<EuiHorizontalRule margin="none" />
|
<EuiHorizontalRule margin="none" />
|
||||||
|
<DiscoverDocuments
|
||||||
<EuiFlexItem className="eui-yScroll">
|
documents$={savedSearchData$.documents$}
|
||||||
<section
|
expandedDoc={expandedDoc}
|
||||||
className="dscTable eui-yScroll eui-xScroll"
|
indexPattern={indexPattern}
|
||||||
aria-labelledby="documentsAriaLabel"
|
isMobile={isMobile}
|
||||||
ref={scrollableDesktop}
|
navigateTo={navigateTo}
|
||||||
tabIndex={-1}
|
onAddFilter={onAddFilter as DocViewFilterFn}
|
||||||
>
|
savedSearch={savedSearch}
|
||||||
<h2 className="euiScreenReaderOnly" id="documentsAriaLabel">
|
services={services}
|
||||||
<FormattedMessage
|
setExpandedDoc={setExpandedDoc}
|
||||||
id="discover.documentsAriaLabel"
|
state={state}
|
||||||
defaultMessage="Documents"
|
stateContainer={stateContainer}
|
||||||
/>
|
/>
|
||||||
</h2>
|
|
||||||
{isLegacy && rows && rows.length && (
|
|
||||||
<DocTableLegacyMemoized
|
|
||||||
columns={columns}
|
|
||||||
indexPattern={indexPattern}
|
|
||||||
rows={rows}
|
|
||||||
sort={state.sort || []}
|
|
||||||
searchDescription={savedSearch.description}
|
|
||||||
searchTitle={savedSearch.lastSavedTitle}
|
|
||||||
onAddColumn={onAddColumn}
|
|
||||||
onBackToTop={onBackToTop}
|
|
||||||
onFilter={onAddFilter}
|
|
||||||
onMoveColumn={onMoveColumn}
|
|
||||||
onRemoveColumn={onRemoveColumn}
|
|
||||||
onSort={onSort}
|
|
||||||
sampleSize={sampleSize}
|
|
||||||
useNewFieldsApi={useNewFieldsApi}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{!isLegacy && rows && rows.length && (
|
|
||||||
<div className="dscDiscoverGrid">
|
|
||||||
<DataGridMemoized
|
|
||||||
ariaLabelledBy="documentsAriaLabel"
|
|
||||||
columns={columns}
|
|
||||||
expandedDoc={expandedDoc}
|
|
||||||
indexPattern={indexPattern}
|
|
||||||
isLoading={fetchStatus === 'loading'}
|
|
||||||
rows={rows}
|
|
||||||
sort={(state.sort as SortPairArr[]) || []}
|
|
||||||
sampleSize={sampleSize}
|
|
||||||
searchDescription={savedSearch.description}
|
|
||||||
searchTitle={savedSearch.lastSavedTitle}
|
|
||||||
setExpandedDoc={setExpandedDoc}
|
|
||||||
showTimeCol={showTimeCol}
|
|
||||||
services={services}
|
|
||||||
settings={state.grid}
|
|
||||||
onAddColumn={onAddColumn}
|
|
||||||
onFilter={onAddFilter as DocViewFilterFn}
|
|
||||||
onRemoveColumn={onRemoveColumn}
|
|
||||||
onSetColumns={onSetColumns}
|
|
||||||
onSort={onSort}
|
|
||||||
onResize={onResize}
|
|
||||||
useNewFieldsApi={useNewFieldsApi}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</section>
|
|
||||||
</EuiFlexItem>
|
|
||||||
</EuiFlexGroup>
|
</EuiFlexGroup>
|
||||||
)}
|
)}
|
||||||
</EuiPageContent>
|
</EuiPageContent>
|
||||||
|
|
|
@ -15,20 +15,22 @@ import {
|
||||||
} from '../../../../../../../data/common';
|
} from '../../../../../../../data/common';
|
||||||
import { ISearchSource } from '../../../../../../../data/public';
|
import { ISearchSource } from '../../../../../../../data/public';
|
||||||
import { AppState, GetStateReturn } from '../../services/discover_state';
|
import { AppState, GetStateReturn } from '../../services/discover_state';
|
||||||
import { SavedSearchRefetchSubject, SavedSearchDataSubject } from '../../services/use_saved_search';
|
import { DataRefetch$, SavedSearchData } from '../../services/use_saved_search';
|
||||||
import { DiscoverServices } from '../../../../../build_services';
|
import { DiscoverServices } from '../../../../../build_services';
|
||||||
import { SavedSearch } from '../../../../../saved_searches';
|
import { SavedSearch } from '../../../../../saved_searches';
|
||||||
|
import { RequestAdapter } from '../../../../../../../inspector';
|
||||||
|
|
||||||
export interface DiscoverLayoutProps {
|
export interface DiscoverLayoutProps {
|
||||||
indexPattern: IndexPattern;
|
indexPattern: IndexPattern;
|
||||||
indexPatternList: Array<SavedObject<IndexPatternAttributes>>;
|
indexPatternList: Array<SavedObject<IndexPatternAttributes>>;
|
||||||
|
inspectorAdapters: { requests: RequestAdapter };
|
||||||
navigateTo: (url: string) => void;
|
navigateTo: (url: string) => void;
|
||||||
onChangeIndexPattern: (id: string) => void;
|
onChangeIndexPattern: (id: string) => void;
|
||||||
onUpdateQuery: (payload: { dateRange: TimeRange; query?: Query }, isUpdate?: boolean) => void;
|
onUpdateQuery: (payload: { dateRange: TimeRange; query?: Query }, isUpdate?: boolean) => void;
|
||||||
resetQuery: () => void;
|
resetQuery: () => void;
|
||||||
savedSearch: SavedSearch;
|
savedSearch: SavedSearch;
|
||||||
savedSearchData$: SavedSearchDataSubject;
|
savedSearchData$: SavedSearchData;
|
||||||
savedSearchRefetch$: SavedSearchRefetchSubject;
|
savedSearchRefetch$: DataRefetch$;
|
||||||
searchSource: ISearchSource;
|
searchSource: ISearchSource;
|
||||||
services: DiscoverServices;
|
services: DiscoverServices;
|
||||||
state: AppState;
|
state: AppState;
|
||||||
|
|
|
@ -63,7 +63,7 @@ function getCompProps(): DiscoverSidebarProps {
|
||||||
return {
|
return {
|
||||||
columns: ['extension'],
|
columns: ['extension'],
|
||||||
fieldCounts,
|
fieldCounts,
|
||||||
hits,
|
documents: hits,
|
||||||
indexPatternList,
|
indexPatternList,
|
||||||
onChangeIndexPattern: jest.fn(),
|
onChangeIndexPattern: jest.fn(),
|
||||||
onAddFilter: jest.fn(),
|
onAddFilter: jest.fn(),
|
||||||
|
|
|
@ -36,13 +36,14 @@ import { FieldFilterState, getDefaultFieldFilter, setFieldFilterProp } from './l
|
||||||
import { getIndexPatternFieldList } from './lib/get_index_pattern_field_list';
|
import { getIndexPatternFieldList } from './lib/get_index_pattern_field_list';
|
||||||
import { DiscoverSidebarResponsiveProps } from './discover_sidebar_responsive';
|
import { DiscoverSidebarResponsiveProps } from './discover_sidebar_responsive';
|
||||||
import { DiscoverIndexPatternManagement } from './discover_index_pattern_management';
|
import { DiscoverIndexPatternManagement } from './discover_index_pattern_management';
|
||||||
|
import { ElasticSearchHit } from '../../../../doc_views/doc_views_types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Default number of available fields displayed and added on scroll
|
* Default number of available fields displayed and added on scroll
|
||||||
*/
|
*/
|
||||||
const FIELDS_PER_PAGE = 50;
|
const FIELDS_PER_PAGE = 50;
|
||||||
|
|
||||||
export interface DiscoverSidebarProps extends DiscoverSidebarResponsiveProps {
|
export interface DiscoverSidebarProps extends Omit<DiscoverSidebarResponsiveProps, 'documents$'> {
|
||||||
/**
|
/**
|
||||||
* Current state of the field filter, filtering fields by name, type, ...
|
* Current state of the field filter, filtering fields by name, type, ...
|
||||||
*/
|
*/
|
||||||
|
@ -64,6 +65,15 @@ export interface DiscoverSidebarProps extends DiscoverSidebarResponsiveProps {
|
||||||
setFieldEditorRef?: (ref: () => void | undefined) => void;
|
setFieldEditorRef?: (ref: () => void | undefined) => void;
|
||||||
|
|
||||||
editField: (fieldName?: string) => void;
|
editField: (fieldName?: string) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* a statistics of the distribution of fields in the given hits
|
||||||
|
*/
|
||||||
|
fieldCounts: Record<string, number>;
|
||||||
|
/**
|
||||||
|
* hits fetched from ES, displayed in the doc table
|
||||||
|
*/
|
||||||
|
documents: ElasticSearchHit[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DiscoverSidebar({
|
export function DiscoverSidebar({
|
||||||
|
@ -71,7 +81,7 @@ export function DiscoverSidebar({
|
||||||
columns,
|
columns,
|
||||||
fieldCounts,
|
fieldCounts,
|
||||||
fieldFilter,
|
fieldFilter,
|
||||||
hits,
|
documents,
|
||||||
indexPatternList,
|
indexPatternList,
|
||||||
onAddField,
|
onAddField,
|
||||||
onAddFilter,
|
onAddFilter,
|
||||||
|
@ -101,7 +111,7 @@ export function DiscoverSidebar({
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const newFields = getIndexPatternFieldList(selectedIndexPattern, fieldCounts);
|
const newFields = getIndexPatternFieldList(selectedIndexPattern, fieldCounts);
|
||||||
setFields(newFields);
|
setFields(newFields);
|
||||||
}, [selectedIndexPattern, fieldCounts, hits]);
|
}, [selectedIndexPattern, fieldCounts, documents]);
|
||||||
|
|
||||||
const scrollDimensions = useResizeObserver(scrollContainer);
|
const scrollDimensions = useResizeObserver(scrollContainer);
|
||||||
|
|
||||||
|
@ -115,8 +125,8 @@ export function DiscoverSidebar({
|
||||||
);
|
);
|
||||||
|
|
||||||
const getDetailsByField = useCallback(
|
const getDetailsByField = useCallback(
|
||||||
(ipField: IndexPatternField) => getDetails(ipField, hits, columns, selectedIndexPattern),
|
(ipField: IndexPatternField) => getDetails(ipField, documents, columns, selectedIndexPattern),
|
||||||
[hits, columns, selectedIndexPattern]
|
[documents, columns, selectedIndexPattern]
|
||||||
);
|
);
|
||||||
|
|
||||||
const popularLimit = useMemo(() => services.uiSettings.get(FIELDS_LIMIT_SETTING), [
|
const popularLimit = useMemo(() => services.uiSettings.get(FIELDS_LIMIT_SETTING), [
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { each, cloneDeep } from 'lodash';
|
import { each, cloneDeep } from 'lodash';
|
||||||
|
import { BehaviorSubject } from 'rxjs';
|
||||||
import { ReactWrapper } from 'enzyme';
|
import { ReactWrapper } from 'enzyme';
|
||||||
import { findTestSubject } from '@elastic/eui/lib/test';
|
import { findTestSubject } from '@elastic/eui/lib/test';
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
|
@ -25,6 +26,8 @@ import {
|
||||||
} from './discover_sidebar_responsive';
|
} from './discover_sidebar_responsive';
|
||||||
import { DiscoverServices } from '../../../../../build_services';
|
import { DiscoverServices } from '../../../../../build_services';
|
||||||
import { ElasticSearchHit } from '../../../../doc_views/doc_views_types';
|
import { ElasticSearchHit } from '../../../../doc_views/doc_views_types';
|
||||||
|
import { FetchStatus } from '../../../../types';
|
||||||
|
import { DataDocuments$ } from '../../services/use_saved_search';
|
||||||
|
|
||||||
const mockServices = ({
|
const mockServices = ({
|
||||||
history: () => ({
|
history: () => ({
|
||||||
|
@ -86,8 +89,10 @@ function getCompProps(): DiscoverSidebarResponsiveProps {
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
columns: ['extension'],
|
columns: ['extension'],
|
||||||
fieldCounts,
|
documents$: new BehaviorSubject({
|
||||||
hits,
|
fetchStatus: FetchStatus.COMPLETE,
|
||||||
|
result: hits as ElasticSearchHit[],
|
||||||
|
}) as DataDocuments$,
|
||||||
indexPatternList,
|
indexPatternList,
|
||||||
onChangeIndexPattern: jest.fn(),
|
onChangeIndexPattern: jest.fn(),
|
||||||
onAddFilter: jest.fn(),
|
onAddFilter: jest.fn(),
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
* Side Public License, v 1.
|
* Side Public License, v 1.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useEffect, useRef, useState } from 'react';
|
import React, { useEffect, useRef, useState, useCallback } from 'react';
|
||||||
import { sortBy } from 'lodash';
|
import { sortBy } from 'lodash';
|
||||||
import { i18n } from '@kbn/i18n';
|
import { i18n } from '@kbn/i18n';
|
||||||
import { FormattedMessage } from '@kbn/i18n/react';
|
import { FormattedMessage } from '@kbn/i18n/react';
|
||||||
|
@ -33,9 +33,10 @@ import { IndexPatternField, IndexPattern } from '../../../../../../../data/publi
|
||||||
import { getDefaultFieldFilter } from './lib/field_filter';
|
import { getDefaultFieldFilter } from './lib/field_filter';
|
||||||
import { DiscoverSidebar } from './discover_sidebar';
|
import { DiscoverSidebar } from './discover_sidebar';
|
||||||
import { DiscoverServices } from '../../../../../build_services';
|
import { DiscoverServices } from '../../../../../build_services';
|
||||||
import { ElasticSearchHit } from '../../../../doc_views/doc_views_types';
|
|
||||||
import { AppState } from '../../services/discover_state';
|
import { AppState } from '../../services/discover_state';
|
||||||
import { DiscoverIndexPatternManagement } from './discover_index_pattern_management';
|
import { DiscoverIndexPatternManagement } from './discover_index_pattern_management';
|
||||||
|
import { DataDocuments$ } from '../../services/use_saved_search';
|
||||||
|
import { calcFieldCounts } from '../../utils/calc_field_counts';
|
||||||
|
|
||||||
export interface DiscoverSidebarResponsiveProps {
|
export interface DiscoverSidebarResponsiveProps {
|
||||||
/**
|
/**
|
||||||
|
@ -46,14 +47,10 @@ export interface DiscoverSidebarResponsiveProps {
|
||||||
* the selected columns displayed in the doc table in discover
|
* the selected columns displayed in the doc table in discover
|
||||||
*/
|
*/
|
||||||
columns: string[];
|
columns: string[];
|
||||||
/**
|
|
||||||
* a statistics of the distribution of fields in the given hits
|
|
||||||
*/
|
|
||||||
fieldCounts: Record<string, number>;
|
|
||||||
/**
|
/**
|
||||||
* hits fetched from ES, displayed in the doc table
|
* hits fetched from ES, displayed in the doc table
|
||||||
*/
|
*/
|
||||||
hits: ElasticSearchHit[];
|
documents$: DataDocuments$;
|
||||||
/**
|
/**
|
||||||
* List of available index patterns
|
* List of available index patterns
|
||||||
*/
|
*/
|
||||||
|
@ -119,6 +116,36 @@ export interface DiscoverSidebarResponsiveProps {
|
||||||
export function DiscoverSidebarResponsive(props: DiscoverSidebarResponsiveProps) {
|
export function DiscoverSidebarResponsive(props: DiscoverSidebarResponsiveProps) {
|
||||||
const [fieldFilter, setFieldFilter] = useState(getDefaultFieldFilter());
|
const [fieldFilter, setFieldFilter] = useState(getDefaultFieldFilter());
|
||||||
const [isFlyoutVisible, setIsFlyoutVisible] = useState(false);
|
const [isFlyoutVisible, setIsFlyoutVisible] = useState(false);
|
||||||
|
/**
|
||||||
|
* needed for merging new with old field counts, high likely legacy, but kept this behavior
|
||||||
|
* because not 100% sure in this case
|
||||||
|
*/
|
||||||
|
const fieldCounts = useRef<Record<string, number>>(
|
||||||
|
calcFieldCounts({}, props.documents$.getValue().result, props.selectedIndexPattern)
|
||||||
|
);
|
||||||
|
|
||||||
|
const [documentState, setDocumentState] = useState(props.documents$.getValue());
|
||||||
|
useEffect(() => {
|
||||||
|
const subscription = props.documents$.subscribe((next) => {
|
||||||
|
if (next.fetchStatus !== documentState.fetchStatus) {
|
||||||
|
if (next.result) {
|
||||||
|
fieldCounts.current = calcFieldCounts(
|
||||||
|
next.result.length ? fieldCounts.current : {},
|
||||||
|
next.result,
|
||||||
|
props.selectedIndexPattern!
|
||||||
|
);
|
||||||
|
}
|
||||||
|
setDocumentState({ ...documentState, ...next });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return () => subscription.unsubscribe();
|
||||||
|
}, [props.documents$, props.selectedIndexPattern, documentState, setDocumentState]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// when index pattern changes fieldCounts needs to be cleaned up to prevent displaying
|
||||||
|
// fields of the previous index pattern
|
||||||
|
fieldCounts.current = {};
|
||||||
|
}, [props.selectedIndexPattern]);
|
||||||
|
|
||||||
const closeFieldEditor = useRef<() => void | undefined>();
|
const closeFieldEditor = useRef<() => void | undefined>();
|
||||||
|
|
||||||
|
@ -134,18 +161,18 @@ export function DiscoverSidebarResponsive(props: DiscoverSidebarResponsiveProps)
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const setFieldEditorRef = useCallback((ref: () => void | undefined) => {
|
||||||
|
closeFieldEditor.current = ref;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const closeFlyout = useCallback(() => {
|
||||||
|
setIsFlyoutVisible(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
if (!props.selectedIndexPattern) {
|
if (!props.selectedIndexPattern) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const setFieldEditorRef = (ref: () => void | undefined) => {
|
|
||||||
closeFieldEditor.current = ref;
|
|
||||||
};
|
|
||||||
|
|
||||||
const closeFlyout = () => {
|
|
||||||
setIsFlyoutVisible(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const { indexPatternFieldEditor } = props.services;
|
const { indexPatternFieldEditor } = props.services;
|
||||||
const indexPatternFieldEditPermission = indexPatternFieldEditor?.userPermissions.editIndexPattern();
|
const indexPatternFieldEditPermission = indexPatternFieldEditor?.userPermissions.editIndexPattern();
|
||||||
const canEditIndexPatternField = !!indexPatternFieldEditPermission && props.useNewFieldsApi;
|
const canEditIndexPatternField = !!indexPatternFieldEditPermission && props.useNewFieldsApi;
|
||||||
|
@ -177,7 +204,9 @@ export function DiscoverSidebarResponsive(props: DiscoverSidebarResponsiveProps)
|
||||||
<EuiHideFor sizes={['xs', 's']}>
|
<EuiHideFor sizes={['xs', 's']}>
|
||||||
<DiscoverSidebar
|
<DiscoverSidebar
|
||||||
{...props}
|
{...props}
|
||||||
|
documents={documentState.result ?? []}
|
||||||
fieldFilter={fieldFilter}
|
fieldFilter={fieldFilter}
|
||||||
|
fieldCounts={fieldCounts.current}
|
||||||
setFieldFilter={setFieldFilter}
|
setFieldFilter={setFieldFilter}
|
||||||
editField={editField}
|
editField={editField}
|
||||||
/>
|
/>
|
||||||
|
@ -262,6 +291,8 @@ export function DiscoverSidebarResponsive(props: DiscoverSidebarResponsiveProps)
|
||||||
<div className="euiFlyoutBody">
|
<div className="euiFlyoutBody">
|
||||||
<DiscoverSidebar
|
<DiscoverSidebar
|
||||||
{...props}
|
{...props}
|
||||||
|
documents={documentState.result ?? []}
|
||||||
|
fieldCounts={fieldCounts.current}
|
||||||
fieldFilter={fieldFilter}
|
fieldFilter={fieldFilter}
|
||||||
setFieldFilter={setFieldFilter}
|
setFieldFilter={setFieldFilter}
|
||||||
alwaysShowActionButtons={true}
|
alwaysShowActionButtons={true}
|
||||||
|
|
|
@ -13,7 +13,61 @@ import { TimechartHeader, TimechartHeaderProps } from './timechart_header';
|
||||||
import { EuiIconTip } from '@elastic/eui';
|
import { EuiIconTip } from '@elastic/eui';
|
||||||
import { findTestSubject } from '@elastic/eui/lib/test';
|
import { findTestSubject } from '@elastic/eui/lib/test';
|
||||||
import { DataPublicPluginStart } from '../../../../../../../data/public';
|
import { DataPublicPluginStart } from '../../../../../../../data/public';
|
||||||
|
import { FetchStatus } from '../../../../types';
|
||||||
|
import { BehaviorSubject } from 'rxjs';
|
||||||
|
import { Chart } from '../chart/point_series';
|
||||||
|
import { DataCharts$ } from '../../services/use_saved_search';
|
||||||
|
|
||||||
|
const chartData = ({
|
||||||
|
xAxisOrderedValues: [
|
||||||
|
1623880800000,
|
||||||
|
1623967200000,
|
||||||
|
1624053600000,
|
||||||
|
1624140000000,
|
||||||
|
1624226400000,
|
||||||
|
1624312800000,
|
||||||
|
1624399200000,
|
||||||
|
1624485600000,
|
||||||
|
1624572000000,
|
||||||
|
1624658400000,
|
||||||
|
1624744800000,
|
||||||
|
1624831200000,
|
||||||
|
1624917600000,
|
||||||
|
1625004000000,
|
||||||
|
1625090400000,
|
||||||
|
],
|
||||||
|
xAxisFormat: { id: 'date', params: { pattern: 'YYYY-MM-DD' } },
|
||||||
|
xAxisLabel: 'order_date per day',
|
||||||
|
yAxisFormat: { id: 'number' },
|
||||||
|
ordered: {
|
||||||
|
date: true,
|
||||||
|
interval: {
|
||||||
|
asMilliseconds: jest.fn(),
|
||||||
|
},
|
||||||
|
intervalESUnit: 'd',
|
||||||
|
intervalESValue: 1,
|
||||||
|
min: '2021-03-18T08:28:56.411Z',
|
||||||
|
max: '2021-07-01T07:28:56.411Z',
|
||||||
|
},
|
||||||
|
yAxisLabel: 'Count',
|
||||||
|
values: [
|
||||||
|
{ x: 1623880800000, y: 134 },
|
||||||
|
{ x: 1623967200000, y: 152 },
|
||||||
|
{ x: 1624053600000, y: 141 },
|
||||||
|
{ x: 1624140000000, y: 138 },
|
||||||
|
{ x: 1624226400000, y: 142 },
|
||||||
|
{ x: 1624312800000, y: 157 },
|
||||||
|
{ x: 1624399200000, y: 149 },
|
||||||
|
{ x: 1624485600000, y: 146 },
|
||||||
|
{ x: 1624572000000, y: 170 },
|
||||||
|
{ x: 1624658400000, y: 137 },
|
||||||
|
{ x: 1624744800000, y: 150 },
|
||||||
|
{ x: 1624831200000, y: 144 },
|
||||||
|
{ x: 1624917600000, y: 147 },
|
||||||
|
{ x: 1625004000000, y: 137 },
|
||||||
|
{ x: 1625090400000, y: 66 },
|
||||||
|
],
|
||||||
|
} as unknown) as Chart;
|
||||||
describe('timechart header', function () {
|
describe('timechart header', function () {
|
||||||
let props: TimechartHeaderProps;
|
let props: TimechartHeaderProps;
|
||||||
let component: ReactWrapper<TimechartHeaderProps>;
|
let component: ReactWrapper<TimechartHeaderProps>;
|
||||||
|
@ -48,11 +102,16 @@ describe('timechart header', function () {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
onChangeInterval: jest.fn(),
|
onChangeInterval: jest.fn(),
|
||||||
bucketInterval: {
|
|
||||||
scaled: undefined,
|
savedSearchData$: new BehaviorSubject({
|
||||||
description: 'second',
|
fetchStatus: FetchStatus.COMPLETE,
|
||||||
scale: undefined,
|
chartData,
|
||||||
},
|
bucketInterval: {
|
||||||
|
scaled: false,
|
||||||
|
description: 'second',
|
||||||
|
scale: undefined,
|
||||||
|
},
|
||||||
|
}) as DataCharts$,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -62,7 +121,15 @@ describe('timechart header', function () {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('TimechartHeader renders an info when bucketInterval.scale is set to true', () => {
|
it('TimechartHeader renders an info when bucketInterval.scale is set to true', () => {
|
||||||
props.bucketInterval!.scaled = true;
|
props.savedSearchData$ = new BehaviorSubject({
|
||||||
|
fetchStatus: FetchStatus.COMPLETE,
|
||||||
|
chartData,
|
||||||
|
bucketInterval: {
|
||||||
|
scaled: true,
|
||||||
|
description: 'second',
|
||||||
|
scale: undefined,
|
||||||
|
},
|
||||||
|
}) as DataCharts$;
|
||||||
component = mountWithIntl(<TimechartHeader {...props} />);
|
component = mountWithIntl(<TimechartHeader {...props} />);
|
||||||
expect(component.find(EuiIconTip).length).toBe(1);
|
expect(component.find(EuiIconTip).length).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
|
@ -20,6 +20,8 @@ import { i18n } from '@kbn/i18n';
|
||||||
import dateMath from '@elastic/datemath';
|
import dateMath from '@elastic/datemath';
|
||||||
import './timechart_header.scss';
|
import './timechart_header.scss';
|
||||||
import { DataPublicPluginStart } from '../../../../../../../data/public';
|
import { DataPublicPluginStart } from '../../../../../../../data/public';
|
||||||
|
import { DataCharts$, DataChartsMessage } from '../../services/use_saved_search';
|
||||||
|
import { useDataState } from '../../utils/use_data_state';
|
||||||
|
|
||||||
export interface TimechartBucketInterval {
|
export interface TimechartBucketInterval {
|
||||||
scaled?: boolean;
|
scaled?: boolean;
|
||||||
|
@ -32,10 +34,7 @@ export interface TimechartHeaderProps {
|
||||||
* Format of date to be displayed
|
* Format of date to be displayed
|
||||||
*/
|
*/
|
||||||
dateFormat?: string;
|
dateFormat?: string;
|
||||||
/**
|
|
||||||
* Interval for the buckets of the recent request
|
|
||||||
*/
|
|
||||||
bucketInterval?: TimechartBucketInterval;
|
|
||||||
data: DataPublicPluginStart;
|
data: DataPublicPluginStart;
|
||||||
/**
|
/**
|
||||||
* Interval Options
|
* Interval Options
|
||||||
|
@ -49,17 +48,23 @@ export interface TimechartHeaderProps {
|
||||||
* selected interval
|
* selected interval
|
||||||
*/
|
*/
|
||||||
stateInterval: string;
|
stateInterval: string;
|
||||||
|
|
||||||
|
savedSearchData$: DataCharts$;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TimechartHeader({
|
export function TimechartHeader({
|
||||||
bucketInterval,
|
|
||||||
dateFormat,
|
dateFormat,
|
||||||
data,
|
data: dataPluginStart,
|
||||||
options,
|
options,
|
||||||
onChangeInterval,
|
onChangeInterval,
|
||||||
stateInterval,
|
stateInterval,
|
||||||
|
savedSearchData$,
|
||||||
}: TimechartHeaderProps) {
|
}: TimechartHeaderProps) {
|
||||||
const { timefilter } = data.query.timefilter;
|
const { timefilter } = dataPluginStart.query.timefilter;
|
||||||
|
|
||||||
|
const data: DataChartsMessage = useDataState(savedSearchData$);
|
||||||
|
|
||||||
|
const { bucketInterval } = data;
|
||||||
const { from, to } = timefilter.getTime();
|
const { from, to } = timefilter.getTime();
|
||||||
const timeRange = {
|
const timeRange = {
|
||||||
from: dateMath.parse(from),
|
from: dateMath.parse(from),
|
||||||
|
|
|
@ -58,6 +58,7 @@ export function DiscoverMainApp(props: DiscoverMainProps) {
|
||||||
const {
|
const {
|
||||||
data$,
|
data$,
|
||||||
indexPattern,
|
indexPattern,
|
||||||
|
inspectorAdapters,
|
||||||
onChangeIndexPattern,
|
onChangeIndexPattern,
|
||||||
onUpdateQuery,
|
onUpdateQuery,
|
||||||
refetch$,
|
refetch$,
|
||||||
|
@ -105,6 +106,7 @@ export function DiscoverMainApp(props: DiscoverMainProps) {
|
||||||
<DiscoverLayoutMemoized
|
<DiscoverLayoutMemoized
|
||||||
indexPattern={indexPattern}
|
indexPattern={indexPattern}
|
||||||
indexPatternList={indexPatternList}
|
indexPatternList={indexPatternList}
|
||||||
|
inspectorAdapters={inspectorAdapters}
|
||||||
onChangeIndexPattern={onChangeIndexPattern}
|
onChangeIndexPattern={onChangeIndexPattern}
|
||||||
onUpdateQuery={onUpdateQuery}
|
onUpdateQuery={onUpdateQuery}
|
||||||
resetQuery={resetQuery}
|
resetQuery={resetQuery}
|
||||||
|
|
|
@ -112,7 +112,7 @@ export interface GetStateReturn {
|
||||||
*/
|
*/
|
||||||
appStateContainer: ReduxLikeStateContainer<AppState>;
|
appStateContainer: ReduxLikeStateContainer<AppState>;
|
||||||
/**
|
/**
|
||||||
* Function starting state sync when Discover main is loaded
|
* Initialize state with filters and query, start state syncing
|
||||||
*/
|
*/
|
||||||
initializeAndSync: (
|
initializeAndSync: (
|
||||||
indexPattern: IndexPattern,
|
indexPattern: IndexPattern,
|
||||||
|
|
|
@ -41,7 +41,7 @@ export function useDiscoverState({
|
||||||
const [indexPattern, setIndexPattern] = useState(initialIndexPattern);
|
const [indexPattern, setIndexPattern] = useState(initialIndexPattern);
|
||||||
const [savedSearch, setSavedSearch] = useState(initialSavedSearch);
|
const [savedSearch, setSavedSearch] = useState(initialSavedSearch);
|
||||||
const useNewFieldsApi = useMemo(() => !config.get(SEARCH_FIELDS_FROM_SOURCE), [config]);
|
const useNewFieldsApi = useMemo(() => !config.get(SEARCH_FIELDS_FROM_SOURCE), [config]);
|
||||||
const timefilter = data.query.timefilter.timefilter;
|
const { timefilter } = data.query.timefilter;
|
||||||
|
|
||||||
const searchSource = useMemo(() => {
|
const searchSource = useMemo(() => {
|
||||||
savedSearch.searchSource.setField('index', indexPattern);
|
savedSearch.searchSource.setField('index', indexPattern);
|
||||||
|
@ -88,8 +88,7 @@ export function useDiscoverState({
|
||||||
/**
|
/**
|
||||||
* Data fetching logic
|
* Data fetching logic
|
||||||
*/
|
*/
|
||||||
const { data$, refetch$, reset } = useSavedSearchData({
|
const { data$, refetch$, reset, inspectorAdapters } = useSavedSearchData({
|
||||||
indexPattern,
|
|
||||||
initialFetchStatus,
|
initialFetchStatus,
|
||||||
searchSessionManager,
|
searchSessionManager,
|
||||||
searchSource,
|
searchSource,
|
||||||
|
@ -100,9 +99,7 @@ export function useDiscoverState({
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const stopSync = stateContainer.initializeAndSync(indexPattern, filterManager, data);
|
const stopSync = stateContainer.initializeAndSync(indexPattern, filterManager, data);
|
||||||
return () => {
|
return () => stopSync();
|
||||||
stopSync();
|
|
||||||
};
|
|
||||||
}, [stateContainer, filterManager, data, indexPattern]);
|
}, [stateContainer, filterManager, data, indexPattern]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -138,8 +135,11 @@ export function useDiscoverState({
|
||||||
setState(nextState);
|
setState(nextState);
|
||||||
});
|
});
|
||||||
return () => unsubscribe();
|
return () => unsubscribe();
|
||||||
}, [config, indexPatterns, appStateContainer, setState, state, refetch$, data$, reset]);
|
}, [config, indexPatterns, appStateContainer, setState, state, refetch$, reset]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* function to revert any changes to a given saved search
|
||||||
|
*/
|
||||||
const resetSavedSearch = useCallback(
|
const resetSavedSearch = useCallback(
|
||||||
async (id?: string) => {
|
async (id?: string) => {
|
||||||
const newSavedSearch = await services.getSavedSearchById(id);
|
const newSavedSearch = await services.getSavedSearchById(id);
|
||||||
|
@ -201,11 +201,12 @@ export function useDiscoverState({
|
||||||
if (initialFetchStatus === FetchStatus.LOADING) {
|
if (initialFetchStatus === FetchStatus.LOADING) {
|
||||||
refetch$.next();
|
refetch$.next();
|
||||||
}
|
}
|
||||||
}, [initialFetchStatus, refetch$, indexPattern, data$]);
|
}, [initialFetchStatus, refetch$, indexPattern]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data$,
|
data$,
|
||||||
indexPattern,
|
indexPattern,
|
||||||
|
inspectorAdapters,
|
||||||
refetch$,
|
refetch$,
|
||||||
resetSavedSearch,
|
resetSavedSearch,
|
||||||
onChangeIndexPattern,
|
onChangeIndexPattern,
|
||||||
|
|
|
@ -28,7 +28,6 @@ describe('test useSavedSearch', () => {
|
||||||
|
|
||||||
const { result } = renderHook(() => {
|
const { result } = renderHook(() => {
|
||||||
return useSavedSearch({
|
return useSavedSearch({
|
||||||
indexPattern: indexPatternMock,
|
|
||||||
initialFetchStatus: FetchStatus.LOADING,
|
initialFetchStatus: FetchStatus.LOADING,
|
||||||
searchSessionManager,
|
searchSessionManager,
|
||||||
searchSource: savedSearchMock.searchSource.createCopy(),
|
searchSource: savedSearchMock.searchSource.createCopy(),
|
||||||
|
@ -39,11 +38,10 @@ describe('test useSavedSearch', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.current.refetch$).toBeInstanceOf(Subject);
|
expect(result.current.refetch$).toBeInstanceOf(Subject);
|
||||||
expect(result.current.data$.value).toMatchInlineSnapshot(`
|
expect(result.current.data$.main$.getValue().fetchStatus).toBe(FetchStatus.LOADING);
|
||||||
Object {
|
expect(result.current.data$.documents$.getValue().fetchStatus).toBe(FetchStatus.LOADING);
|
||||||
"state": "loading",
|
expect(result.current.data$.totalHits$.getValue().fetchStatus).toBe(FetchStatus.LOADING);
|
||||||
}
|
expect(result.current.data$.charts$.getValue().fetchStatus).toBe(FetchStatus.LOADING);
|
||||||
`);
|
|
||||||
});
|
});
|
||||||
test('refetch$ triggers a search', async () => {
|
test('refetch$ triggers a search', async () => {
|
||||||
const { history, searchSessionManager } = createSearchSessionMock();
|
const { history, searchSessionManager } = createSearchSessionMock();
|
||||||
|
@ -68,7 +66,6 @@ describe('test useSavedSearch', () => {
|
||||||
|
|
||||||
const { result, waitForValueToChange } = renderHook(() => {
|
const { result, waitForValueToChange } = renderHook(() => {
|
||||||
return useSavedSearch({
|
return useSavedSearch({
|
||||||
indexPattern: indexPatternMock,
|
|
||||||
initialFetchStatus: FetchStatus.LOADING,
|
initialFetchStatus: FetchStatus.LOADING,
|
||||||
searchSessionManager,
|
searchSessionManager,
|
||||||
searchSource: resultState.current.searchSource,
|
searchSource: resultState.current.searchSource,
|
||||||
|
@ -81,11 +78,11 @@ describe('test useSavedSearch', () => {
|
||||||
result.current.refetch$.next();
|
result.current.refetch$.next();
|
||||||
|
|
||||||
await waitForValueToChange(() => {
|
await waitForValueToChange(() => {
|
||||||
return result.current.data$.value.state === 'complete';
|
return result.current.data$.main$.value.fetchStatus === 'complete';
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.current.data$.value.hits).toBe(0);
|
expect(result.current.data$.totalHits$.value.result).toBe(0);
|
||||||
expect(result.current.data$.value.rows).toEqual([]);
|
expect(result.current.data$.documents$.value.result).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('reset sets back to initial state', async () => {
|
test('reset sets back to initial state', async () => {
|
||||||
|
@ -111,7 +108,6 @@ describe('test useSavedSearch', () => {
|
||||||
|
|
||||||
const { result, waitForValueToChange } = renderHook(() => {
|
const { result, waitForValueToChange } = renderHook(() => {
|
||||||
return useSavedSearch({
|
return useSavedSearch({
|
||||||
indexPattern: indexPatternMock,
|
|
||||||
initialFetchStatus: FetchStatus.LOADING,
|
initialFetchStatus: FetchStatus.LOADING,
|
||||||
searchSessionManager,
|
searchSessionManager,
|
||||||
searchSource: resultState.current.searchSource,
|
searchSource: resultState.current.searchSource,
|
||||||
|
@ -124,10 +120,10 @@ describe('test useSavedSearch', () => {
|
||||||
result.current.refetch$.next();
|
result.current.refetch$.next();
|
||||||
|
|
||||||
await waitForValueToChange(() => {
|
await waitForValueToChange(() => {
|
||||||
return result.current.data$.value.state === FetchStatus.COMPLETE;
|
return result.current.data$.main$.value.fetchStatus === FetchStatus.COMPLETE;
|
||||||
});
|
});
|
||||||
|
|
||||||
result.current.reset();
|
result.current.reset();
|
||||||
expect(result.current.data$.value.state).toBe(FetchStatus.LOADING);
|
expect(result.current.data$.main$.value.fetchStatus).toBe(FetchStatus.LOADING);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -5,53 +5,71 @@
|
||||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||||
* Side Public License, v 1.
|
* Side Public License, v 1.
|
||||||
*/
|
*/
|
||||||
import { useEffect, useRef, useCallback } from 'react';
|
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||||
import { i18n } from '@kbn/i18n';
|
import { BehaviorSubject, merge, Subject } from 'rxjs';
|
||||||
import { merge, Subject, BehaviorSubject } from 'rxjs';
|
import { debounceTime, filter, tap } from 'rxjs/operators';
|
||||||
import { debounceTime, tap, filter } from 'rxjs/operators';
|
|
||||||
import { DiscoverServices } from '../../../../build_services';
|
import { DiscoverServices } from '../../../../build_services';
|
||||||
import { DiscoverSearchSessionManager } from './discover_search_session';
|
import { DiscoverSearchSessionManager } from './discover_search_session';
|
||||||
import {
|
import { SearchSource } from '../../../../../../data/common';
|
||||||
IndexPattern,
|
|
||||||
isCompleteResponse,
|
|
||||||
SearchSource,
|
|
||||||
tabifyAggResponse,
|
|
||||||
} from '../../../../../../data/common';
|
|
||||||
import { GetStateReturn } from './discover_state';
|
import { GetStateReturn } from './discover_state';
|
||||||
import { ElasticSearchHit } from '../../../doc_views/doc_views_types';
|
import { ElasticSearchHit } from '../../../doc_views/doc_views_types';
|
||||||
import { RequestAdapter } from '../../../../../../inspector/public';
|
import { RequestAdapter } from '../../../../../../inspector/public';
|
||||||
import { AutoRefreshDoneFn, search } from '../../../../../../data/public';
|
import { AutoRefreshDoneFn } from '../../../../../../data/public';
|
||||||
import { calcFieldCounts } from '../utils/calc_field_counts';
|
|
||||||
import { validateTimeRange } from '../utils/validate_time_range';
|
import { validateTimeRange } from '../utils/validate_time_range';
|
||||||
import { updateSearchSource } from '../utils/update_search_source';
|
import { Chart } from '../components/chart/point_series';
|
||||||
import { SortOrder } from '../../../../saved_searches/types';
|
|
||||||
import { getDimensions, getChartAggConfigs } from '../utils';
|
|
||||||
import { buildPointSeriesData, Chart } from '../components/chart/point_series';
|
|
||||||
import { TimechartBucketInterval } from '../components/timechart_header/timechart_header';
|
import { TimechartBucketInterval } from '../components/timechart_header/timechart_header';
|
||||||
import { useSingleton } from '../utils/use_singleton';
|
import { useSingleton } from '../utils/use_singleton';
|
||||||
import { FetchStatus } from '../../../types';
|
import { FetchStatus } from '../../../types';
|
||||||
|
|
||||||
export type SavedSearchDataSubject = BehaviorSubject<SavedSearchDataMessage>;
|
import { fetchAll } from '../utils/fetch_all';
|
||||||
export type SavedSearchRefetchSubject = Subject<SavedSearchRefetchMsg>;
|
import { useBehaviorSubject } from '../utils/use_behavior_subject';
|
||||||
|
import { sendResetMsg } from './use_saved_search_messages';
|
||||||
|
|
||||||
export interface UseSavedSearch {
|
export interface SavedSearchData {
|
||||||
refetch$: SavedSearchRefetchSubject;
|
main$: DataMain$;
|
||||||
data$: SavedSearchDataSubject;
|
documents$: DataDocuments$;
|
||||||
reset: () => void;
|
totalHits$: DataTotalHits$;
|
||||||
|
charts$: DataCharts$;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SavedSearchRefetchMsg = 'reset' | undefined;
|
export type DataMain$ = BehaviorSubject<DataMainMsg>;
|
||||||
|
export type DataDocuments$ = BehaviorSubject<DataDocumentsMsg>;
|
||||||
|
export type DataTotalHits$ = BehaviorSubject<DataTotalHitsMsg>;
|
||||||
|
export type DataCharts$ = BehaviorSubject<DataChartsMessage>;
|
||||||
|
|
||||||
export interface SavedSearchDataMessage {
|
export type DataRefetch$ = Subject<DataRefetchMsg>;
|
||||||
|
|
||||||
|
export interface UseSavedSearch {
|
||||||
|
refetch$: DataRefetch$;
|
||||||
|
data$: SavedSearchData;
|
||||||
|
reset: () => void;
|
||||||
|
inspectorAdapters: { requests: RequestAdapter };
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DataRefetchMsg = 'reset' | undefined;
|
||||||
|
|
||||||
|
export interface DataMsg {
|
||||||
|
fetchStatus: FetchStatus;
|
||||||
|
error?: Error;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DataMainMsg extends DataMsg {
|
||||||
|
foundDocuments?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DataDocumentsMsg extends DataMsg {
|
||||||
|
result?: ElasticSearchHit[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DataTotalHitsMsg extends DataMsg {
|
||||||
|
fetchStatus: FetchStatus;
|
||||||
|
error?: Error;
|
||||||
|
result?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DataChartsMessage extends DataMsg {
|
||||||
bucketInterval?: TimechartBucketInterval;
|
bucketInterval?: TimechartBucketInterval;
|
||||||
chartData?: Chart;
|
chartData?: Chart;
|
||||||
fetchCounter?: number;
|
|
||||||
fetchError?: Error;
|
|
||||||
fieldCounts?: Record<string, number>;
|
|
||||||
hits?: number;
|
|
||||||
inspectorAdapters?: { requests: RequestAdapter };
|
|
||||||
rows?: ElasticSearchHit[];
|
|
||||||
state: FetchStatus;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -59,7 +77,6 @@ export interface SavedSearchDataMessage {
|
||||||
* to the data fetching
|
* to the data fetching
|
||||||
*/
|
*/
|
||||||
export const useSavedSearch = ({
|
export const useSavedSearch = ({
|
||||||
indexPattern,
|
|
||||||
initialFetchStatus,
|
initialFetchStatus,
|
||||||
searchSessionManager,
|
searchSessionManager,
|
||||||
searchSource,
|
searchSource,
|
||||||
|
@ -67,244 +84,148 @@ export const useSavedSearch = ({
|
||||||
stateContainer,
|
stateContainer,
|
||||||
useNewFieldsApi,
|
useNewFieldsApi,
|
||||||
}: {
|
}: {
|
||||||
indexPattern: IndexPattern;
|
|
||||||
initialFetchStatus: FetchStatus;
|
initialFetchStatus: FetchStatus;
|
||||||
searchSessionManager: DiscoverSearchSessionManager;
|
searchSessionManager: DiscoverSearchSessionManager;
|
||||||
searchSource: SearchSource;
|
searchSource: SearchSource;
|
||||||
services: DiscoverServices;
|
services: DiscoverServices;
|
||||||
stateContainer: GetStateReturn;
|
stateContainer: GetStateReturn;
|
||||||
useNewFieldsApi: boolean;
|
useNewFieldsApi: boolean;
|
||||||
}): UseSavedSearch => {
|
}) => {
|
||||||
const { data, filterManager } = services;
|
const { data, filterManager } = services;
|
||||||
const timefilter = data.query.timefilter.timefilter;
|
const timefilter = data.query.timefilter.timefilter;
|
||||||
|
|
||||||
|
const inspectorAdapters = useMemo(() => ({ requests: new RequestAdapter() }), []);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The observable the UI (aka React component) subscribes to get notified about
|
* The observables the UI (aka React component) subscribes to get notified about
|
||||||
* the changes in the data fetching process (high level: fetching started, data was received)
|
* the changes in the data fetching process (high level: fetching started, data was received)
|
||||||
*/
|
*/
|
||||||
const data$: SavedSearchDataSubject = useSingleton(
|
const main$: DataMain$ = useBehaviorSubject({ fetchStatus: initialFetchStatus });
|
||||||
() =>
|
|
||||||
new BehaviorSubject<SavedSearchDataMessage>({
|
const documents$: DataDocuments$ = useBehaviorSubject({ fetchStatus: initialFetchStatus });
|
||||||
state: initialFetchStatus,
|
|
||||||
})
|
const totalHits$: DataTotalHits$ = useBehaviorSubject({ fetchStatus: initialFetchStatus });
|
||||||
);
|
|
||||||
|
const charts$: DataCharts$ = useBehaviorSubject({ fetchStatus: initialFetchStatus });
|
||||||
|
|
||||||
|
const dataSubjects = useMemo(() => {
|
||||||
|
return {
|
||||||
|
main$,
|
||||||
|
documents$,
|
||||||
|
totalHits$,
|
||||||
|
charts$,
|
||||||
|
};
|
||||||
|
}, [main$, charts$, documents$, totalHits$]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The observable to trigger data fetching in UI
|
* The observable to trigger data fetching in UI
|
||||||
* By refetch$.next('reset') rows and fieldcounts are reset to allow e.g. editing of runtime fields
|
* By refetch$.next('reset') rows and fieldcounts are reset to allow e.g. editing of runtime fields
|
||||||
* to be processed correctly
|
* to be processed correctly
|
||||||
*/
|
*/
|
||||||
const refetch$ = useSingleton(() => new Subject<SavedSearchRefetchMsg>());
|
const refetch$ = useSingleton(() => new Subject<DataRefetchMsg>());
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Values that shouldn't trigger re-rendering when changed
|
* Values that shouldn't trigger re-rendering when changed
|
||||||
*/
|
*/
|
||||||
const refs = useRef<{
|
const refs = useRef<{
|
||||||
abortController?: AbortController;
|
abortController?: AbortController;
|
||||||
/**
|
}>({});
|
||||||
* handler emitted by `timefilter.getAutoRefreshFetch$()`
|
|
||||||
* to notify when data completed loading and to start a new autorefresh loop
|
|
||||||
*/
|
|
||||||
autoRefreshDoneCb?: AutoRefreshDoneFn;
|
|
||||||
/**
|
|
||||||
* Number of fetches used for functional testing
|
|
||||||
*/
|
|
||||||
fetchCounter: number;
|
|
||||||
/**
|
|
||||||
* needed to right auto refresh behavior, a new auto refresh shouldnt be triggered when
|
|
||||||
* loading is still ongoing
|
|
||||||
*/
|
|
||||||
fetchStatus: FetchStatus;
|
|
||||||
/**
|
|
||||||
* needed for merging new with old field counts, high likely legacy, but kept this behavior
|
|
||||||
* because not 100% sure in this case
|
|
||||||
*/
|
|
||||||
fieldCounts: Record<string, number>;
|
|
||||||
}>({
|
|
||||||
fetchCounter: 0,
|
|
||||||
fieldCounts: {},
|
|
||||||
fetchStatus: initialFetchStatus,
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resets the fieldCounts cache and sends a reset message
|
|
||||||
* It is set to initial state (no documents, fetchCounter to 0)
|
|
||||||
* Needed when index pattern is switched or a new runtime field is added
|
|
||||||
*/
|
|
||||||
const sendResetMsg = useCallback(
|
|
||||||
(fetchStatus?: FetchStatus) => {
|
|
||||||
refs.current.fieldCounts = {};
|
|
||||||
refs.current.fetchStatus = fetchStatus ?? initialFetchStatus;
|
|
||||||
data$.next({
|
|
||||||
state: initialFetchStatus,
|
|
||||||
fetchCounter: 0,
|
|
||||||
rows: [],
|
|
||||||
fieldCounts: {},
|
|
||||||
chartData: undefined,
|
|
||||||
bucketInterval: undefined,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[data$, initialFetchStatus]
|
|
||||||
);
|
|
||||||
/**
|
|
||||||
* Function to fetch data from ElasticSearch
|
|
||||||
*/
|
|
||||||
const fetchAll = useCallback(
|
|
||||||
(reset = false) => {
|
|
||||||
if (!validateTimeRange(timefilter.getTime(), services.toastNotifications)) {
|
|
||||||
return Promise.reject();
|
|
||||||
}
|
|
||||||
const inspectorAdapters = { requests: new RequestAdapter() };
|
|
||||||
|
|
||||||
if (refs.current.abortController) refs.current.abortController.abort();
|
|
||||||
refs.current.abortController = new AbortController();
|
|
||||||
const sessionId = searchSessionManager.getNextSearchSessionId();
|
|
||||||
|
|
||||||
if (reset) {
|
|
||||||
sendResetMsg(FetchStatus.LOADING);
|
|
||||||
} else {
|
|
||||||
// Let the UI know, data fetching started
|
|
||||||
data$.next({
|
|
||||||
state: FetchStatus.LOADING,
|
|
||||||
fetchCounter: ++refs.current.fetchCounter,
|
|
||||||
});
|
|
||||||
refs.current.fetchStatus = FetchStatus.LOADING;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { sort, hideChart, interval } = stateContainer.appStateContainer.getState();
|
|
||||||
updateSearchSource(searchSource, false, {
|
|
||||||
indexPattern,
|
|
||||||
services,
|
|
||||||
sort: sort as SortOrder[],
|
|
||||||
useNewFieldsApi,
|
|
||||||
});
|
|
||||||
const chartAggConfigs =
|
|
||||||
indexPattern.timeFieldName && !hideChart && interval
|
|
||||||
? getChartAggConfigs(searchSource, interval, data)
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
if (!chartAggConfigs) {
|
|
||||||
searchSource.removeField('aggs');
|
|
||||||
} else {
|
|
||||||
searchSource.setField('aggs', chartAggConfigs.toDsl());
|
|
||||||
}
|
|
||||||
|
|
||||||
const searchSourceFetch$ = searchSource.fetch$({
|
|
||||||
abortSignal: refs.current.abortController.signal,
|
|
||||||
sessionId,
|
|
||||||
inspector: {
|
|
||||||
adapter: inspectorAdapters.requests,
|
|
||||||
title: i18n.translate('discover.inspectorRequestDataTitle', {
|
|
||||||
defaultMessage: 'data',
|
|
||||||
}),
|
|
||||||
description: i18n.translate('discover.inspectorRequestDescriptionDocument', {
|
|
||||||
defaultMessage: 'This request queries Elasticsearch to fetch the data for the search.',
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
searchSourceFetch$.pipe(filter((res) => isCompleteResponse(res))).subscribe(
|
|
||||||
(res) => {
|
|
||||||
const documents = res.rawResponse.hits.hits;
|
|
||||||
|
|
||||||
const message: SavedSearchDataMessage = {
|
|
||||||
state: FetchStatus.COMPLETE,
|
|
||||||
rows: documents,
|
|
||||||
inspectorAdapters,
|
|
||||||
fieldCounts: calcFieldCounts(refs.current.fieldCounts, documents, indexPattern),
|
|
||||||
hits: res.rawResponse.hits.total as number,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (chartAggConfigs) {
|
|
||||||
const bucketAggConfig = chartAggConfigs.aggs[1];
|
|
||||||
const tabifiedData = tabifyAggResponse(chartAggConfigs, res.rawResponse);
|
|
||||||
const dimensions = getDimensions(chartAggConfigs, data);
|
|
||||||
if (dimensions) {
|
|
||||||
if (bucketAggConfig && search.aggs.isDateHistogramBucketAggConfig(bucketAggConfig)) {
|
|
||||||
message.bucketInterval = bucketAggConfig.buckets?.getInterval();
|
|
||||||
}
|
|
||||||
message.chartData = buildPointSeriesData(tabifiedData, dimensions);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
refs.current.fieldCounts = message.fieldCounts!;
|
|
||||||
refs.current.fetchStatus = message.state;
|
|
||||||
data$.next(message);
|
|
||||||
},
|
|
||||||
(error) => {
|
|
||||||
if (error instanceof Error && error.name === 'AbortError') return;
|
|
||||||
data.search.showError(error);
|
|
||||||
refs.current.fetchStatus = FetchStatus.ERROR;
|
|
||||||
data$.next({
|
|
||||||
state: FetchStatus.ERROR,
|
|
||||||
inspectorAdapters,
|
|
||||||
fetchError: error,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
() => {
|
|
||||||
refs.current.autoRefreshDoneCb?.();
|
|
||||||
refs.current.autoRefreshDoneCb = undefined;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
},
|
|
||||||
[
|
|
||||||
timefilter,
|
|
||||||
services,
|
|
||||||
searchSessionManager,
|
|
||||||
stateContainer.appStateContainer,
|
|
||||||
searchSource,
|
|
||||||
indexPattern,
|
|
||||||
useNewFieldsApi,
|
|
||||||
data,
|
|
||||||
sendResetMsg,
|
|
||||||
data$,
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This part takes care of triggering the data fetching by creating and subscribing
|
* This part takes care of triggering the data fetching by creating and subscribing
|
||||||
* to an observable of various possible changes in state
|
* to an observable of various possible changes in state
|
||||||
*/
|
*/
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
/**
|
||||||
|
* handler emitted by `timefilter.getAutoRefreshFetch$()`
|
||||||
|
* to notify when data completed loading and to start a new autorefresh loop
|
||||||
|
*/
|
||||||
|
let autoRefreshDoneCb: AutoRefreshDoneFn | undefined;
|
||||||
const fetch$ = merge(
|
const fetch$ = merge(
|
||||||
refetch$,
|
refetch$,
|
||||||
filterManager.getFetches$(),
|
filterManager.getFetches$(),
|
||||||
timefilter.getFetch$(),
|
timefilter.getFetch$(),
|
||||||
timefilter.getAutoRefreshFetch$().pipe(
|
timefilter.getAutoRefreshFetch$().pipe(
|
||||||
tap((done) => {
|
tap((done) => {
|
||||||
refs.current.autoRefreshDoneCb = done;
|
autoRefreshDoneCb = done;
|
||||||
}),
|
}),
|
||||||
filter(() => refs.current.fetchStatus !== FetchStatus.LOADING)
|
filter(() => {
|
||||||
|
/**
|
||||||
|
* filter to prevent auto-refresh triggered fetch when
|
||||||
|
* loading is still ongoing
|
||||||
|
*/
|
||||||
|
const currentFetchStatus = main$.getValue().fetchStatus;
|
||||||
|
return (
|
||||||
|
currentFetchStatus !== FetchStatus.LOADING && currentFetchStatus !== FetchStatus.PARTIAL
|
||||||
|
);
|
||||||
|
})
|
||||||
),
|
),
|
||||||
data.query.queryString.getUpdates$(),
|
data.query.queryString.getUpdates$(),
|
||||||
searchSessionManager.newSearchSessionIdFromURL$.pipe(filter((sessionId) => !!sessionId))
|
searchSessionManager.newSearchSessionIdFromURL$.pipe(filter((sessionId) => !!sessionId))
|
||||||
).pipe(debounceTime(100));
|
).pipe(debounceTime(100));
|
||||||
|
|
||||||
const subscription = fetch$.subscribe((val) => {
|
const subscription = fetch$.subscribe((val) => {
|
||||||
|
if (!validateTimeRange(timefilter.getTime(), services.toastNotifications)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
inspectorAdapters.requests.reset();
|
||||||
|
|
||||||
|
refs.current.abortController?.abort();
|
||||||
|
refs.current.abortController = new AbortController();
|
||||||
try {
|
try {
|
||||||
fetchAll(val === 'reset');
|
fetchAll(dataSubjects, searchSource, val === 'reset', {
|
||||||
|
abortController: refs.current.abortController,
|
||||||
|
appStateContainer: stateContainer.appStateContainer,
|
||||||
|
inspectorAdapters,
|
||||||
|
data,
|
||||||
|
initialFetchStatus,
|
||||||
|
searchSessionId: searchSessionManager.getNextSearchSessionId(),
|
||||||
|
services,
|
||||||
|
useNewFieldsApi,
|
||||||
|
}).subscribe({
|
||||||
|
complete: () => {
|
||||||
|
// if this function was set and is executed, another refresh fetch can be triggered
|
||||||
|
autoRefreshDoneCb?.();
|
||||||
|
autoRefreshDoneCb = undefined;
|
||||||
|
},
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
data$.next({
|
main$.next({
|
||||||
state: FetchStatus.ERROR,
|
fetchStatus: FetchStatus.ERROR,
|
||||||
fetchError: error,
|
error,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => {
|
return () => subscription.unsubscribe();
|
||||||
subscription.unsubscribe();
|
|
||||||
};
|
|
||||||
}, [
|
}, [
|
||||||
data$,
|
data,
|
||||||
data.query.queryString,
|
data.query.queryString,
|
||||||
|
dataSubjects,
|
||||||
filterManager,
|
filterManager,
|
||||||
|
initialFetchStatus,
|
||||||
|
inspectorAdapters,
|
||||||
|
main$,
|
||||||
refetch$,
|
refetch$,
|
||||||
|
searchSessionManager,
|
||||||
searchSessionManager.newSearchSessionIdFromURL$,
|
searchSessionManager.newSearchSessionIdFromURL$,
|
||||||
|
searchSource,
|
||||||
|
services,
|
||||||
|
services.toastNotifications,
|
||||||
|
stateContainer.appStateContainer,
|
||||||
timefilter,
|
timefilter,
|
||||||
fetchAll,
|
useNewFieldsApi,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const reset = useCallback(() => sendResetMsg(dataSubjects, initialFetchStatus), [
|
||||||
|
dataSubjects,
|
||||||
|
initialFetchStatus,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
refetch$,
|
refetch$,
|
||||||
data$,
|
data$: dataSubjects,
|
||||||
reset: sendResetMsg,
|
reset,
|
||||||
|
inspectorAdapters,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,90 @@
|
||||||
|
/*
|
||||||
|
* 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 { FetchStatus } from '../../../types';
|
||||||
|
import {
|
||||||
|
DataCharts$,
|
||||||
|
DataDocuments$,
|
||||||
|
DataMain$,
|
||||||
|
DataTotalHits$,
|
||||||
|
SavedSearchData,
|
||||||
|
} from './use_saved_search';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send COMPLETE message via main observable used when
|
||||||
|
* 1.) first fetch resolved, and there are no documents
|
||||||
|
* 2.) all fetches resolved, and there are documents
|
||||||
|
*/
|
||||||
|
export function sendCompleteMsg(main$: DataMain$, foundDocuments = true) {
|
||||||
|
if (main$.getValue().fetchStatus === FetchStatus.COMPLETE) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
main$.next({
|
||||||
|
fetchStatus: FetchStatus.COMPLETE,
|
||||||
|
foundDocuments,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send PARTIAL message via main observable when first result is returned
|
||||||
|
*/
|
||||||
|
export function sendPartialMsg(main$: DataMain$) {
|
||||||
|
if (main$.getValue().fetchStatus === FetchStatus.LOADING) {
|
||||||
|
main$.next({
|
||||||
|
fetchStatus: FetchStatus.PARTIAL,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send LOADING message via main observable
|
||||||
|
*/
|
||||||
|
export function sendLoadingMsg(data$: DataMain$ | DataDocuments$ | DataTotalHits$ | DataCharts$) {
|
||||||
|
if (data$.getValue().fetchStatus !== FetchStatus.LOADING) {
|
||||||
|
data$.next({
|
||||||
|
fetchStatus: FetchStatus.LOADING,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send ERROR message
|
||||||
|
*/
|
||||||
|
export function sendErrorMsg(
|
||||||
|
data$: DataMain$ | DataDocuments$ | DataTotalHits$ | DataCharts$,
|
||||||
|
error: Error
|
||||||
|
) {
|
||||||
|
data$.next({
|
||||||
|
fetchStatus: FetchStatus.ERROR,
|
||||||
|
error,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends a RESET message to all data subjects
|
||||||
|
* Needed when index pattern is switched or a new runtime field is added
|
||||||
|
*/
|
||||||
|
export function sendResetMsg(data: SavedSearchData, initialFetchStatus: FetchStatus) {
|
||||||
|
data.main$.next({
|
||||||
|
fetchStatus: initialFetchStatus,
|
||||||
|
foundDocuments: undefined,
|
||||||
|
});
|
||||||
|
data.documents$.next({
|
||||||
|
fetchStatus: initialFetchStatus,
|
||||||
|
result: [],
|
||||||
|
});
|
||||||
|
data.charts$.next({
|
||||||
|
fetchStatus: initialFetchStatus,
|
||||||
|
chartData: undefined,
|
||||||
|
bucketInterval: undefined,
|
||||||
|
});
|
||||||
|
data.totalHits$.next({
|
||||||
|
fetchStatus: initialFetchStatus,
|
||||||
|
result: undefined,
|
||||||
|
});
|
||||||
|
}
|
|
@ -15,9 +15,12 @@ import { ElasticSearchHit } from '../../../doc_views/doc_views_types';
|
||||||
*/
|
*/
|
||||||
export function calcFieldCounts(
|
export function calcFieldCounts(
|
||||||
counts = {} as Record<string, number>,
|
counts = {} as Record<string, number>,
|
||||||
rows: ElasticSearchHit[],
|
rows?: ElasticSearchHit[],
|
||||||
indexPattern: IndexPattern
|
indexPattern?: IndexPattern
|
||||||
) {
|
) {
|
||||||
|
if (!rows || !indexPattern) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
for (const hit of rows) {
|
for (const hit of rows) {
|
||||||
const fields = Object.keys(indexPattern.flattenHit(hit));
|
const fields = Object.keys(indexPattern.flattenHit(hit));
|
||||||
for (const fieldName of fields) {
|
for (const fieldName of fields) {
|
||||||
|
|
|
@ -0,0 +1,59 @@
|
||||||
|
/*
|
||||||
|
* 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 { FetchStatus } from '../../../types';
|
||||||
|
import { BehaviorSubject } from 'rxjs';
|
||||||
|
import { RequestAdapter } from '../../../../../../inspector';
|
||||||
|
import { savedSearchMock } from '../../../../__mocks__/saved_search';
|
||||||
|
import { ReduxLikeStateContainer } from '../../../../../../kibana_utils/common';
|
||||||
|
import { AppState } from '../services/discover_state';
|
||||||
|
import { discoverServiceMock } from '../../../../__mocks__/services';
|
||||||
|
import { fetchAll } from './fetch_all';
|
||||||
|
|
||||||
|
describe('test fetchAll', () => {
|
||||||
|
test('changes of fetchStatus when starting with FetchStatus.UNINITIALIZED', async (done) => {
|
||||||
|
const subjects = {
|
||||||
|
main$: new BehaviorSubject({ fetchStatus: FetchStatus.UNINITIALIZED }),
|
||||||
|
documents$: new BehaviorSubject({ fetchStatus: FetchStatus.UNINITIALIZED }),
|
||||||
|
totalHits$: new BehaviorSubject({ fetchStatus: FetchStatus.UNINITIALIZED }),
|
||||||
|
charts$: new BehaviorSubject({ fetchStatus: FetchStatus.UNINITIALIZED }),
|
||||||
|
};
|
||||||
|
const deps = {
|
||||||
|
appStateContainer: {
|
||||||
|
getState: () => {
|
||||||
|
return { interval: 'auto' };
|
||||||
|
},
|
||||||
|
} as ReduxLikeStateContainer<AppState>,
|
||||||
|
abortController: new AbortController(),
|
||||||
|
data: discoverServiceMock.data,
|
||||||
|
inspectorAdapters: { requests: new RequestAdapter() },
|
||||||
|
onResults: jest.fn(),
|
||||||
|
searchSessionId: '123',
|
||||||
|
initialFetchStatus: FetchStatus.UNINITIALIZED,
|
||||||
|
useNewFieldsApi: true,
|
||||||
|
services: discoverServiceMock,
|
||||||
|
};
|
||||||
|
|
||||||
|
const stateArr: FetchStatus[] = [];
|
||||||
|
|
||||||
|
subjects.main$.subscribe((value) => stateArr.push(value.fetchStatus));
|
||||||
|
|
||||||
|
const parentSearchSource = savedSearchMock.searchSource;
|
||||||
|
const childSearchSource = parentSearchSource.createChild();
|
||||||
|
|
||||||
|
fetchAll(subjects, childSearchSource, false, deps).subscribe({
|
||||||
|
complete: () => {
|
||||||
|
expect(stateArr).toEqual([
|
||||||
|
FetchStatus.UNINITIALIZED,
|
||||||
|
FetchStatus.LOADING,
|
||||||
|
FetchStatus.COMPLETE,
|
||||||
|
]);
|
||||||
|
done();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,96 @@
|
||||||
|
/*
|
||||||
|
* 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 { forkJoin, of } from 'rxjs';
|
||||||
|
import {
|
||||||
|
sendCompleteMsg,
|
||||||
|
sendErrorMsg,
|
||||||
|
sendLoadingMsg,
|
||||||
|
sendPartialMsg,
|
||||||
|
sendResetMsg,
|
||||||
|
} from '../services/use_saved_search_messages';
|
||||||
|
import { updateSearchSource } from './update_search_source';
|
||||||
|
import { SortOrder } from '../../../../saved_searches/types';
|
||||||
|
import { fetchDocuments } from './fetch_documents';
|
||||||
|
import { fetchTotalHits } from './fetch_total_hits';
|
||||||
|
import { fetchChart } from './fetch_chart';
|
||||||
|
import { SearchSource } from '../../../../../../data/common';
|
||||||
|
import { Adapters } from '../../../../../../inspector';
|
||||||
|
import { AppState } from '../services/discover_state';
|
||||||
|
import { FetchStatus } from '../../../types';
|
||||||
|
import { DataPublicPluginStart } from '../../../../../../data/public';
|
||||||
|
import { SavedSearchData } from '../services/use_saved_search';
|
||||||
|
import { DiscoverServices } from '../../../../build_services';
|
||||||
|
import { ReduxLikeStateContainer } from '../../../../../../kibana_utils/common';
|
||||||
|
|
||||||
|
export function fetchAll(
|
||||||
|
dataSubjects: SavedSearchData,
|
||||||
|
searchSource: SearchSource,
|
||||||
|
reset = false,
|
||||||
|
fetchDeps: {
|
||||||
|
abortController: AbortController;
|
||||||
|
appStateContainer: ReduxLikeStateContainer<AppState>;
|
||||||
|
inspectorAdapters: Adapters;
|
||||||
|
data: DataPublicPluginStart;
|
||||||
|
initialFetchStatus: FetchStatus;
|
||||||
|
searchSessionId: string;
|
||||||
|
services: DiscoverServices;
|
||||||
|
useNewFieldsApi: boolean;
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
const { initialFetchStatus, appStateContainer, services, useNewFieldsApi, data } = fetchDeps;
|
||||||
|
|
||||||
|
const indexPattern = searchSource.getField('index')!;
|
||||||
|
|
||||||
|
if (reset) {
|
||||||
|
sendResetMsg(dataSubjects, initialFetchStatus);
|
||||||
|
}
|
||||||
|
|
||||||
|
sendLoadingMsg(dataSubjects.main$);
|
||||||
|
|
||||||
|
const { hideChart, sort } = appStateContainer.getState();
|
||||||
|
// Update the base searchSource, base for all child fetches
|
||||||
|
updateSearchSource(searchSource, false, {
|
||||||
|
indexPattern,
|
||||||
|
services,
|
||||||
|
sort: sort as SortOrder[],
|
||||||
|
useNewFieldsApi,
|
||||||
|
});
|
||||||
|
|
||||||
|
const subFetchDeps = {
|
||||||
|
...fetchDeps,
|
||||||
|
onResults: (foundDocuments: boolean) => {
|
||||||
|
if (!foundDocuments) {
|
||||||
|
sendCompleteMsg(dataSubjects.main$, foundDocuments);
|
||||||
|
} else {
|
||||||
|
sendPartialMsg(dataSubjects.main$);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const all = forkJoin({
|
||||||
|
documents: fetchDocuments(dataSubjects, searchSource.createCopy(), subFetchDeps),
|
||||||
|
totalHits:
|
||||||
|
hideChart || !indexPattern.timeFieldName
|
||||||
|
? fetchTotalHits(dataSubjects, searchSource.createCopy(), subFetchDeps)
|
||||||
|
: of(null),
|
||||||
|
chart:
|
||||||
|
!hideChart && indexPattern.timeFieldName
|
||||||
|
? fetchChart(dataSubjects, searchSource.createCopy(), subFetchDeps)
|
||||||
|
: of(null),
|
||||||
|
});
|
||||||
|
|
||||||
|
all.subscribe(
|
||||||
|
() => sendCompleteMsg(dataSubjects.main$, true),
|
||||||
|
(error) => {
|
||||||
|
if (error instanceof Error && error.name === 'AbortError') return;
|
||||||
|
data.search.showError(error);
|
||||||
|
sendErrorMsg(dataSubjects.main$, error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return all;
|
||||||
|
}
|
|
@ -0,0 +1,174 @@
|
||||||
|
/*
|
||||||
|
* 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 { FetchStatus } from '../../../types';
|
||||||
|
import { BehaviorSubject, of, throwError as throwErrorRx } from 'rxjs';
|
||||||
|
import { RequestAdapter } from '../../../../../../inspector';
|
||||||
|
import { savedSearchMockWithTimeField } from '../../../../__mocks__/saved_search';
|
||||||
|
import { fetchChart, updateSearchSource } from './fetch_chart';
|
||||||
|
import { ReduxLikeStateContainer } from '../../../../../../kibana_utils/common';
|
||||||
|
import { AppState } from '../services/discover_state';
|
||||||
|
import { discoverServiceMock } from '../../../../__mocks__/services';
|
||||||
|
import { calculateBounds, IKibanaSearchResponse } from '../../../../../../data/common';
|
||||||
|
import { estypes } from '@elastic/elasticsearch';
|
||||||
|
|
||||||
|
function getDataSubjects() {
|
||||||
|
return {
|
||||||
|
main$: new BehaviorSubject({ fetchStatus: FetchStatus.UNINITIALIZED }),
|
||||||
|
documents$: new BehaviorSubject({ fetchStatus: FetchStatus.UNINITIALIZED }),
|
||||||
|
totalHits$: new BehaviorSubject({ fetchStatus: FetchStatus.UNINITIALIZED }),
|
||||||
|
charts$: new BehaviorSubject({ fetchStatus: FetchStatus.UNINITIALIZED }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('test fetchCharts', () => {
|
||||||
|
test('updateSearchSource helper function', () => {
|
||||||
|
const chartAggConfigs = updateSearchSource(
|
||||||
|
savedSearchMockWithTimeField.searchSource,
|
||||||
|
'auto',
|
||||||
|
discoverServiceMock.data
|
||||||
|
);
|
||||||
|
expect(chartAggConfigs.aggs).toMatchInlineSnapshot(`
|
||||||
|
Array [
|
||||||
|
Object {
|
||||||
|
"enabled": true,
|
||||||
|
"id": "1",
|
||||||
|
"params": Object {},
|
||||||
|
"schema": "metric",
|
||||||
|
"type": "count",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"enabled": true,
|
||||||
|
"id": "2",
|
||||||
|
"params": Object {
|
||||||
|
"drop_partials": false,
|
||||||
|
"extended_bounds": Object {},
|
||||||
|
"field": "timestamp",
|
||||||
|
"interval": "auto",
|
||||||
|
"min_doc_count": 1,
|
||||||
|
"scaleMetricValues": false,
|
||||||
|
"useNormalizedEsInterval": true,
|
||||||
|
"used_interval": "0ms",
|
||||||
|
},
|
||||||
|
"schema": "segment",
|
||||||
|
"type": "date_histogram",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('changes of fetchStatus when starting with FetchStatus.UNINITIALIZED', async (done) => {
|
||||||
|
const subjects = getDataSubjects();
|
||||||
|
const deps = {
|
||||||
|
appStateContainer: {
|
||||||
|
getState: () => {
|
||||||
|
return { interval: 'auto' };
|
||||||
|
},
|
||||||
|
} as ReduxLikeStateContainer<AppState>,
|
||||||
|
abortController: new AbortController(),
|
||||||
|
data: discoverServiceMock.data,
|
||||||
|
inspectorAdapters: { requests: new RequestAdapter() },
|
||||||
|
onResults: jest.fn(),
|
||||||
|
searchSessionId: '123',
|
||||||
|
};
|
||||||
|
deps.data.query.timefilter.timefilter.getTime = () => {
|
||||||
|
return { from: '2021-07-07T00:05:13.590', to: '2021-07-07T11:20:13.590' };
|
||||||
|
};
|
||||||
|
|
||||||
|
deps.data.query.timefilter.timefilter.calculateBounds = (timeRange) =>
|
||||||
|
calculateBounds(timeRange);
|
||||||
|
|
||||||
|
const stateArrChart: FetchStatus[] = [];
|
||||||
|
const stateArrHits: FetchStatus[] = [];
|
||||||
|
|
||||||
|
subjects.charts$.subscribe((value) => stateArrChart.push(value.fetchStatus));
|
||||||
|
subjects.totalHits$.subscribe((value) => stateArrHits.push(value.fetchStatus));
|
||||||
|
|
||||||
|
savedSearchMockWithTimeField.searchSource.fetch$ = () =>
|
||||||
|
of(({
|
||||||
|
id: 'Fjk5bndxTHJWU2FldVRVQ0tYR0VqOFEcRWtWNDhOdG5SUzJYcFhONVVZVTBJQToxMDMwOQ==',
|
||||||
|
rawResponse: {
|
||||||
|
took: 2,
|
||||||
|
timed_out: false,
|
||||||
|
_shards: { total: 1, successful: 1, skipped: 0, failed: 0 },
|
||||||
|
hits: { max_score: null, hits: [] },
|
||||||
|
aggregations: {
|
||||||
|
'2': {
|
||||||
|
buckets: [
|
||||||
|
{
|
||||||
|
key_as_string: '2021-07-07T06:36:00.000+02:00',
|
||||||
|
key: 1625632560000,
|
||||||
|
doc_count: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
isPartial: false,
|
||||||
|
isRunning: false,
|
||||||
|
total: 1,
|
||||||
|
loaded: 1,
|
||||||
|
isRestored: false,
|
||||||
|
} as unknown) as IKibanaSearchResponse<estypes.SearchResponse<unknown>>);
|
||||||
|
|
||||||
|
fetchChart(subjects, savedSearchMockWithTimeField.searchSource, deps).subscribe({
|
||||||
|
complete: () => {
|
||||||
|
expect(stateArrChart).toEqual([
|
||||||
|
FetchStatus.UNINITIALIZED,
|
||||||
|
FetchStatus.LOADING,
|
||||||
|
FetchStatus.COMPLETE,
|
||||||
|
]);
|
||||||
|
expect(stateArrHits).toEqual([
|
||||||
|
FetchStatus.UNINITIALIZED,
|
||||||
|
FetchStatus.LOADING,
|
||||||
|
FetchStatus.COMPLETE,
|
||||||
|
]);
|
||||||
|
done();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
test('change of fetchStatus on fetch error', async (done) => {
|
||||||
|
const subjects = getDataSubjects();
|
||||||
|
|
||||||
|
const deps = {
|
||||||
|
appStateContainer: {
|
||||||
|
getState: () => {
|
||||||
|
return { interval: 'auto' };
|
||||||
|
},
|
||||||
|
} as ReduxLikeStateContainer<AppState>,
|
||||||
|
abortController: new AbortController(),
|
||||||
|
data: discoverServiceMock.data,
|
||||||
|
inspectorAdapters: { requests: new RequestAdapter() },
|
||||||
|
onResults: jest.fn(),
|
||||||
|
searchSessionId: '123',
|
||||||
|
};
|
||||||
|
|
||||||
|
savedSearchMockWithTimeField.searchSource.fetch$ = () => throwErrorRx({ msg: 'Oh noes!' });
|
||||||
|
|
||||||
|
const stateArrChart: FetchStatus[] = [];
|
||||||
|
const stateArrHits: FetchStatus[] = [];
|
||||||
|
|
||||||
|
subjects.charts$.subscribe((value) => stateArrChart.push(value.fetchStatus));
|
||||||
|
subjects.totalHits$.subscribe((value) => stateArrHits.push(value.fetchStatus));
|
||||||
|
|
||||||
|
fetchChart(subjects, savedSearchMockWithTimeField.searchSource, deps).subscribe({
|
||||||
|
error: () => {
|
||||||
|
expect(stateArrChart).toEqual([
|
||||||
|
FetchStatus.UNINITIALIZED,
|
||||||
|
FetchStatus.LOADING,
|
||||||
|
FetchStatus.ERROR,
|
||||||
|
]);
|
||||||
|
expect(stateArrHits).toEqual([
|
||||||
|
FetchStatus.UNINITIALIZED,
|
||||||
|
FetchStatus.LOADING,
|
||||||
|
FetchStatus.ERROR,
|
||||||
|
]);
|
||||||
|
done();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,121 @@
|
||||||
|
/*
|
||||||
|
* 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 } from 'rxjs/operators';
|
||||||
|
import {
|
||||||
|
DataPublicPluginStart,
|
||||||
|
isCompleteResponse,
|
||||||
|
search,
|
||||||
|
SearchSource,
|
||||||
|
} from '../../../../../../data/public';
|
||||||
|
import { Adapters } from '../../../../../../inspector';
|
||||||
|
import { getChartAggConfigs, getDimensions } from './index';
|
||||||
|
import { tabifyAggResponse } from '../../../../../../data/common';
|
||||||
|
import { buildPointSeriesData } from '../components/chart/point_series';
|
||||||
|
import { FetchStatus } from '../../../types';
|
||||||
|
import { SavedSearchData } from '../services/use_saved_search';
|
||||||
|
import { AppState } from '../services/discover_state';
|
||||||
|
import { ReduxLikeStateContainer } from '../../../../../../kibana_utils/common';
|
||||||
|
import { sendErrorMsg, sendLoadingMsg } from '../services/use_saved_search_messages';
|
||||||
|
|
||||||
|
export function fetchChart(
|
||||||
|
data$: SavedSearchData,
|
||||||
|
searchSource: SearchSource,
|
||||||
|
{
|
||||||
|
abortController,
|
||||||
|
appStateContainer,
|
||||||
|
data,
|
||||||
|
inspectorAdapters,
|
||||||
|
onResults,
|
||||||
|
searchSessionId,
|
||||||
|
}: {
|
||||||
|
abortController: AbortController;
|
||||||
|
appStateContainer: ReduxLikeStateContainer<AppState>;
|
||||||
|
data: DataPublicPluginStart;
|
||||||
|
inspectorAdapters: Adapters;
|
||||||
|
onResults: (foundDocuments: boolean) => void;
|
||||||
|
searchSessionId: string;
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
const { charts$, totalHits$ } = data$;
|
||||||
|
|
||||||
|
const interval = appStateContainer.getState().interval ?? 'auto';
|
||||||
|
const chartAggConfigs = updateSearchSource(searchSource, interval, data);
|
||||||
|
|
||||||
|
sendLoadingMsg(charts$);
|
||||||
|
sendLoadingMsg(totalHits$);
|
||||||
|
|
||||||
|
const fetch$ = searchSource
|
||||||
|
.fetch$({
|
||||||
|
abortSignal: abortController.signal,
|
||||||
|
sessionId: searchSessionId,
|
||||||
|
inspector: {
|
||||||
|
adapter: inspectorAdapters.requests,
|
||||||
|
title: i18n.translate('discover.inspectorRequestDataTitleChart', {
|
||||||
|
defaultMessage: 'Chart data',
|
||||||
|
}),
|
||||||
|
description: i18n.translate('discover.inspectorRequestDescriptionChart', {
|
||||||
|
defaultMessage:
|
||||||
|
'This request queries Elasticsearch to fetch the aggregation data for the chart.',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.pipe(filter((res) => isCompleteResponse(res)));
|
||||||
|
|
||||||
|
fetch$.subscribe(
|
||||||
|
(res) => {
|
||||||
|
try {
|
||||||
|
const totalHitsNr = res.rawResponse.hits.total as number;
|
||||||
|
totalHits$.next({ fetchStatus: FetchStatus.COMPLETE, result: totalHitsNr });
|
||||||
|
onResults(totalHitsNr > 0);
|
||||||
|
|
||||||
|
const bucketAggConfig = chartAggConfigs.aggs[1];
|
||||||
|
const tabifiedData = tabifyAggResponse(chartAggConfigs, res.rawResponse);
|
||||||
|
const dimensions = getDimensions(chartAggConfigs, data);
|
||||||
|
const bucketInterval = search.aggs.isDateHistogramBucketAggConfig(bucketAggConfig)
|
||||||
|
? bucketAggConfig?.buckets?.getInterval()
|
||||||
|
: undefined;
|
||||||
|
const chartData = buildPointSeriesData(tabifiedData, dimensions!);
|
||||||
|
charts$.next({
|
||||||
|
fetchStatus: FetchStatus.COMPLETE,
|
||||||
|
chartData,
|
||||||
|
bucketInterval,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
charts$.next({
|
||||||
|
fetchStatus: FetchStatus.ERROR,
|
||||||
|
error: e,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
if (error instanceof Error && error.name === 'AbortError') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
sendErrorMsg(charts$, error);
|
||||||
|
sendErrorMsg(totalHits$, error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return fetch$;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateSearchSource(
|
||||||
|
searchSource: SearchSource,
|
||||||
|
interval: string,
|
||||||
|
data: DataPublicPluginStart
|
||||||
|
) {
|
||||||
|
const indexPattern = searchSource.getField('index')!;
|
||||||
|
searchSource.setField('filter', data.query.timefilter.timefilter.createFilter(indexPattern));
|
||||||
|
searchSource.setField('size', 0);
|
||||||
|
searchSource.setField('trackTotalHits', true);
|
||||||
|
const chartAggConfigs = getChartAggConfigs(searchSource, interval, data);
|
||||||
|
searchSource.setField('aggs', chartAggConfigs.toDsl());
|
||||||
|
searchSource.removeField('sort');
|
||||||
|
searchSource.removeField('fields');
|
||||||
|
return chartAggConfigs;
|
||||||
|
}
|
|
@ -0,0 +1,79 @@
|
||||||
|
/*
|
||||||
|
* 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 { fetchDocuments } from './fetch_documents';
|
||||||
|
import { FetchStatus } from '../../../types';
|
||||||
|
import { BehaviorSubject, throwError as throwErrorRx } from 'rxjs';
|
||||||
|
import { RequestAdapter } from '../../../../../../inspector';
|
||||||
|
import { savedSearchMock } from '../../../../__mocks__/saved_search';
|
||||||
|
import { discoverServiceMock } from '../../../../__mocks__/services';
|
||||||
|
|
||||||
|
function getDataSubjects() {
|
||||||
|
return {
|
||||||
|
main$: new BehaviorSubject({ fetchStatus: FetchStatus.UNINITIALIZED }),
|
||||||
|
documents$: new BehaviorSubject({ fetchStatus: FetchStatus.UNINITIALIZED }),
|
||||||
|
totalHits$: new BehaviorSubject({ fetchStatus: FetchStatus.UNINITIALIZED }),
|
||||||
|
charts$: new BehaviorSubject({ fetchStatus: FetchStatus.UNINITIALIZED }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('test fetchDocuments', () => {
|
||||||
|
test('changes of fetchStatus are correct when starting with FetchStatus.UNINITIALIZED', async (done) => {
|
||||||
|
const subjects = getDataSubjects();
|
||||||
|
const { documents$ } = subjects;
|
||||||
|
const deps = {
|
||||||
|
abortController: new AbortController(),
|
||||||
|
inspectorAdapters: { requests: new RequestAdapter() },
|
||||||
|
onResults: jest.fn(),
|
||||||
|
searchSessionId: '123',
|
||||||
|
services: discoverServiceMock,
|
||||||
|
};
|
||||||
|
|
||||||
|
const stateArr: FetchStatus[] = [];
|
||||||
|
|
||||||
|
documents$.subscribe((value) => stateArr.push(value.fetchStatus));
|
||||||
|
|
||||||
|
fetchDocuments(subjects, savedSearchMock.searchSource, deps).subscribe({
|
||||||
|
complete: () => {
|
||||||
|
expect(stateArr).toEqual([
|
||||||
|
FetchStatus.UNINITIALIZED,
|
||||||
|
FetchStatus.LOADING,
|
||||||
|
FetchStatus.COMPLETE,
|
||||||
|
]);
|
||||||
|
done();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
test('change of fetchStatus on fetch error', async (done) => {
|
||||||
|
const subjects = getDataSubjects();
|
||||||
|
const { documents$ } = subjects;
|
||||||
|
const deps = {
|
||||||
|
abortController: new AbortController(),
|
||||||
|
inspectorAdapters: { requests: new RequestAdapter() },
|
||||||
|
onResults: jest.fn(),
|
||||||
|
searchSessionId: '123',
|
||||||
|
services: discoverServiceMock,
|
||||||
|
};
|
||||||
|
|
||||||
|
savedSearchMock.searchSource.fetch$ = () => throwErrorRx({ msg: 'Oh noes!' });
|
||||||
|
|
||||||
|
const stateArr: FetchStatus[] = [];
|
||||||
|
|
||||||
|
documents$.subscribe((value) => stateArr.push(value.fetchStatus));
|
||||||
|
|
||||||
|
fetchDocuments(subjects, savedSearchMock.searchSource, deps).subscribe({
|
||||||
|
error: () => {
|
||||||
|
expect(stateArr).toEqual([
|
||||||
|
FetchStatus.UNINITIALIZED,
|
||||||
|
FetchStatus.LOADING,
|
||||||
|
FetchStatus.ERROR,
|
||||||
|
]);
|
||||||
|
done();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,88 @@
|
||||||
|
/*
|
||||||
|
* 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 } from 'rxjs/operators';
|
||||||
|
import { Adapters } from '../../../../../../inspector/common';
|
||||||
|
import { isCompleteResponse, SearchSource } from '../../../../../../data/common';
|
||||||
|
import { FetchStatus } from '../../../types';
|
||||||
|
import { SavedSearchData } from '../services/use_saved_search';
|
||||||
|
import { sendErrorMsg, sendLoadingMsg } from '../services/use_saved_search_messages';
|
||||||
|
import { SAMPLE_SIZE_SETTING } from '../../../../../common';
|
||||||
|
import { DiscoverServices } from '../../../../build_services';
|
||||||
|
|
||||||
|
export const fetchDocuments = (
|
||||||
|
data$: SavedSearchData,
|
||||||
|
searchSource: SearchSource,
|
||||||
|
{
|
||||||
|
abortController,
|
||||||
|
inspectorAdapters,
|
||||||
|
onResults,
|
||||||
|
searchSessionId,
|
||||||
|
services,
|
||||||
|
}: {
|
||||||
|
abortController: AbortController;
|
||||||
|
inspectorAdapters: Adapters;
|
||||||
|
onResults: (foundDocuments: boolean) => void;
|
||||||
|
searchSessionId: string;
|
||||||
|
services: DiscoverServices;
|
||||||
|
}
|
||||||
|
) => {
|
||||||
|
const { documents$, totalHits$ } = data$;
|
||||||
|
|
||||||
|
searchSource.setField('size', services.uiSettings.get(SAMPLE_SIZE_SETTING));
|
||||||
|
searchSource.setField('trackTotalHits', false);
|
||||||
|
searchSource.setField('highlightAll', true);
|
||||||
|
searchSource.setField('version', true);
|
||||||
|
|
||||||
|
sendLoadingMsg(documents$);
|
||||||
|
|
||||||
|
const fetch$ = searchSource
|
||||||
|
.fetch$({
|
||||||
|
abortSignal: abortController.signal,
|
||||||
|
sessionId: searchSessionId,
|
||||||
|
inspector: {
|
||||||
|
adapter: inspectorAdapters.requests,
|
||||||
|
title: i18n.translate('discover.inspectorRequestDataTitleDocuments', {
|
||||||
|
defaultMessage: 'Documents',
|
||||||
|
}),
|
||||||
|
description: i18n.translate('discover.inspectorRequestDescriptionDocument', {
|
||||||
|
defaultMessage: 'This request queries Elasticsearch to fetch the documents.',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.pipe(filter((res) => isCompleteResponse(res)));
|
||||||
|
|
||||||
|
fetch$.subscribe(
|
||||||
|
(res) => {
|
||||||
|
const documents = res.rawResponse.hits.hits;
|
||||||
|
|
||||||
|
// If the total hits query is still loading for hits, emit a partial
|
||||||
|
// hit count that's at least our document count
|
||||||
|
if (totalHits$.getValue().fetchStatus === FetchStatus.LOADING) {
|
||||||
|
totalHits$.next({
|
||||||
|
fetchStatus: FetchStatus.PARTIAL,
|
||||||
|
result: documents.length,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
documents$.next({
|
||||||
|
fetchStatus: FetchStatus.COMPLETE,
|
||||||
|
result: documents,
|
||||||
|
});
|
||||||
|
onResults(documents.length > 0);
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
if (error instanceof Error && error.name === 'AbortError') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
sendErrorMsg(documents$, error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return fetch$;
|
||||||
|
};
|
|
@ -0,0 +1,80 @@
|
||||||
|
/*
|
||||||
|
* 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 { FetchStatus } from '../../../types';
|
||||||
|
import { BehaviorSubject, throwError as throwErrorRx } from 'rxjs';
|
||||||
|
import { RequestAdapter } from '../../../../../../inspector';
|
||||||
|
import { savedSearchMock } from '../../../../__mocks__/saved_search';
|
||||||
|
import { fetchTotalHits } from './fetch_total_hits';
|
||||||
|
import { discoverServiceMock } from '../../../../__mocks__/services';
|
||||||
|
|
||||||
|
function getDataSubjects() {
|
||||||
|
return {
|
||||||
|
main$: new BehaviorSubject({ fetchStatus: FetchStatus.UNINITIALIZED }),
|
||||||
|
documents$: new BehaviorSubject({ fetchStatus: FetchStatus.UNINITIALIZED }),
|
||||||
|
totalHits$: new BehaviorSubject({ fetchStatus: FetchStatus.UNINITIALIZED }),
|
||||||
|
charts$: new BehaviorSubject({ fetchStatus: FetchStatus.UNINITIALIZED }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('test fetchTotalHits', () => {
|
||||||
|
test('changes of fetchStatus are correct when starting with FetchStatus.UNINITIALIZED', async (done) => {
|
||||||
|
const subjects = getDataSubjects();
|
||||||
|
const { totalHits$ } = subjects;
|
||||||
|
|
||||||
|
const deps = {
|
||||||
|
abortController: new AbortController(),
|
||||||
|
inspectorAdapters: { requests: new RequestAdapter() },
|
||||||
|
onResults: jest.fn(),
|
||||||
|
searchSessionId: '123',
|
||||||
|
data: discoverServiceMock.data,
|
||||||
|
};
|
||||||
|
|
||||||
|
const stateArr: FetchStatus[] = [];
|
||||||
|
|
||||||
|
totalHits$.subscribe((value) => stateArr.push(value.fetchStatus));
|
||||||
|
|
||||||
|
fetchTotalHits(subjects, savedSearchMock.searchSource, deps).subscribe({
|
||||||
|
complete: () => {
|
||||||
|
expect(stateArr).toEqual([
|
||||||
|
FetchStatus.UNINITIALIZED,
|
||||||
|
FetchStatus.LOADING,
|
||||||
|
FetchStatus.COMPLETE,
|
||||||
|
]);
|
||||||
|
done();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
test('change of fetchStatus on fetch error', async (done) => {
|
||||||
|
const subjects = getDataSubjects();
|
||||||
|
const { totalHits$ } = subjects;
|
||||||
|
const deps = {
|
||||||
|
abortController: new AbortController(),
|
||||||
|
inspectorAdapters: { requests: new RequestAdapter() },
|
||||||
|
onResults: jest.fn(),
|
||||||
|
searchSessionId: '123',
|
||||||
|
data: discoverServiceMock.data,
|
||||||
|
};
|
||||||
|
|
||||||
|
savedSearchMock.searchSource.fetch$ = () => throwErrorRx({ msg: 'Oh noes!' });
|
||||||
|
|
||||||
|
const stateArr: FetchStatus[] = [];
|
||||||
|
|
||||||
|
totalHits$.subscribe((value) => stateArr.push(value.fetchStatus));
|
||||||
|
|
||||||
|
fetchTotalHits(subjects, savedSearchMock.searchSource, deps).subscribe({
|
||||||
|
error: () => {
|
||||||
|
expect(stateArr).toEqual([
|
||||||
|
FetchStatus.UNINITIALIZED,
|
||||||
|
FetchStatus.LOADING,
|
||||||
|
FetchStatus.ERROR,
|
||||||
|
]);
|
||||||
|
done();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,80 @@
|
||||||
|
/*
|
||||||
|
* 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 } from 'rxjs/operators';
|
||||||
|
import {
|
||||||
|
DataPublicPluginStart,
|
||||||
|
isCompleteResponse,
|
||||||
|
SearchSource,
|
||||||
|
} from '../../../../../../data/public';
|
||||||
|
import { Adapters } from '../../../../../../inspector/common';
|
||||||
|
import { FetchStatus } from '../../../types';
|
||||||
|
import { SavedSearchData } from '../services/use_saved_search';
|
||||||
|
import { sendErrorMsg, sendLoadingMsg } from '../services/use_saved_search_messages';
|
||||||
|
|
||||||
|
export function fetchTotalHits(
|
||||||
|
data$: SavedSearchData,
|
||||||
|
searchSource: SearchSource,
|
||||||
|
{
|
||||||
|
abortController,
|
||||||
|
data,
|
||||||
|
inspectorAdapters,
|
||||||
|
onResults,
|
||||||
|
searchSessionId,
|
||||||
|
}: {
|
||||||
|
abortController: AbortController;
|
||||||
|
data: DataPublicPluginStart;
|
||||||
|
onResults: (foundDocuments: boolean) => void;
|
||||||
|
inspectorAdapters: Adapters;
|
||||||
|
searchSessionId: string;
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
const { totalHits$ } = data$;
|
||||||
|
const indexPattern = searchSource.getField('index');
|
||||||
|
searchSource.setField('trackTotalHits', true);
|
||||||
|
searchSource.setField('filter', data.query.timefilter.timefilter.createFilter(indexPattern!));
|
||||||
|
searchSource.setField('size', 0);
|
||||||
|
searchSource.removeField('sort');
|
||||||
|
searchSource.removeField('fields');
|
||||||
|
searchSource.removeField('aggs');
|
||||||
|
|
||||||
|
sendLoadingMsg(totalHits$);
|
||||||
|
|
||||||
|
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,
|
||||||
|
})
|
||||||
|
.pipe(filter((res) => isCompleteResponse(res)));
|
||||||
|
|
||||||
|
fetch$.subscribe(
|
||||||
|
(res) => {
|
||||||
|
const totalHitsNr = res.rawResponse.hits.total as number;
|
||||||
|
totalHits$.next({ fetchStatus: FetchStatus.COMPLETE, result: totalHitsNr });
|
||||||
|
onResults(totalHitsNr > 0);
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
if (error instanceof Error && error.name === 'AbortError') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
sendErrorMsg(totalHits$, error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return fetch$;
|
||||||
|
}
|
|
@ -6,40 +6,36 @@
|
||||||
* Side Public License, v 1.
|
* Side Public License, v 1.
|
||||||
*/
|
*/
|
||||||
import { getResultState, resultStatuses } from './get_result_state';
|
import { getResultState, resultStatuses } from './get_result_state';
|
||||||
import { ElasticSearchHit } from '../../../doc_views/doc_views_types';
|
|
||||||
import { FetchStatus } from '../../../types';
|
import { FetchStatus } from '../../../types';
|
||||||
|
|
||||||
describe('getResultState', () => {
|
describe('getResultState', () => {
|
||||||
test('fetching uninitialized', () => {
|
test('fetching uninitialized', () => {
|
||||||
const actual = getResultState(FetchStatus.UNINITIALIZED, []);
|
const actual = getResultState(FetchStatus.UNINITIALIZED, false);
|
||||||
expect(actual).toBe(resultStatuses.UNINITIALIZED);
|
expect(actual).toBe(resultStatuses.UNINITIALIZED);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('fetching complete with no records', () => {
|
test('fetching complete with no records', () => {
|
||||||
const actual = getResultState(FetchStatus.COMPLETE, []);
|
const actual = getResultState(FetchStatus.COMPLETE, false);
|
||||||
expect(actual).toBe(resultStatuses.NO_RESULTS);
|
expect(actual).toBe(resultStatuses.NO_RESULTS);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('fetching ongoing aka loading', () => {
|
test('fetching ongoing aka loading', () => {
|
||||||
const actual = getResultState(FetchStatus.LOADING, []);
|
const actual = getResultState(FetchStatus.LOADING, false);
|
||||||
expect(actual).toBe(resultStatuses.LOADING);
|
expect(actual).toBe(resultStatuses.LOADING);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('fetching ready', () => {
|
test('fetching ready', () => {
|
||||||
const record = ({ _id: 123 } as unknown) as ElasticSearchHit;
|
const actual = getResultState(FetchStatus.COMPLETE, true);
|
||||||
const actual = getResultState(FetchStatus.COMPLETE, [record]);
|
|
||||||
expect(actual).toBe(resultStatuses.READY);
|
expect(actual).toBe(resultStatuses.READY);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('re-fetching after already data is available', () => {
|
test('re-fetching after already data is available', () => {
|
||||||
const record = ({ _id: 123 } as unknown) as ElasticSearchHit;
|
const actual = getResultState(FetchStatus.LOADING, true);
|
||||||
const actual = getResultState(FetchStatus.LOADING, [record]);
|
|
||||||
expect(actual).toBe(resultStatuses.READY);
|
expect(actual).toBe(resultStatuses.READY);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('after a fetch error when data was successfully fetched before ', () => {
|
test('after a fetch error when data was successfully fetched before ', () => {
|
||||||
const record = ({ _id: 123 } as unknown) as ElasticSearchHit;
|
const actual = getResultState(FetchStatus.ERROR, true);
|
||||||
const actual = getResultState(FetchStatus.ERROR, [record]);
|
|
||||||
expect(actual).toBe(resultStatuses.READY);
|
expect(actual).toBe(resultStatuses.READY);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -5,7 +5,6 @@
|
||||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||||
* Side Public License, v 1.
|
* Side Public License, v 1.
|
||||||
*/
|
*/
|
||||||
import { ElasticSearchHit } from '../../../doc_views/doc_views_types';
|
|
||||||
import { FetchStatus } from '../../../types';
|
import { FetchStatus } from '../../../types';
|
||||||
|
|
||||||
export const resultStatuses = {
|
export const resultStatuses = {
|
||||||
|
@ -19,13 +18,13 @@ export const resultStatuses = {
|
||||||
* Returns the current state of the result, depends on fetchStatus and the given fetched rows
|
* Returns the current state of the result, depends on fetchStatus and the given fetched rows
|
||||||
* Determines what is displayed in Discover main view (loading view, data view, empty data view, ...)
|
* Determines what is displayed in Discover main view (loading view, data view, empty data view, ...)
|
||||||
*/
|
*/
|
||||||
export function getResultState(fetchStatus: FetchStatus, rows: ElasticSearchHit[]) {
|
export function getResultState(fetchStatus: FetchStatus, foundDocuments: boolean = false) {
|
||||||
if (fetchStatus === FetchStatus.UNINITIALIZED) {
|
if (fetchStatus === FetchStatus.UNINITIALIZED) {
|
||||||
return resultStatuses.UNINITIALIZED;
|
return resultStatuses.UNINITIALIZED;
|
||||||
}
|
}
|
||||||
|
|
||||||
const rowsEmpty = !Array.isArray(rows) || rows.length === 0;
|
if (!foundDocuments && fetchStatus === FetchStatus.LOADING) return resultStatuses.LOADING;
|
||||||
if (rowsEmpty && fetchStatus === FetchStatus.LOADING) return resultStatuses.LOADING;
|
else if (foundDocuments) return resultStatuses.READY;
|
||||||
else if (!rowsEmpty) return resultStatuses.READY;
|
else if (fetchStatus === FetchStatus.PARTIAL) return resultStatuses.READY;
|
||||||
else return resultStatuses.NO_RESULTS;
|
else return resultStatuses.NO_RESULTS;
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,39 +9,21 @@
|
||||||
import { updateSearchSource } from './update_search_source';
|
import { updateSearchSource } from './update_search_source';
|
||||||
import { createSearchSourceMock } from '../../../../../../data/common/search/search_source/mocks';
|
import { createSearchSourceMock } from '../../../../../../data/common/search/search_source/mocks';
|
||||||
import { indexPatternMock } from '../../../../__mocks__/index_pattern';
|
import { indexPatternMock } from '../../../../__mocks__/index_pattern';
|
||||||
import { IUiSettingsClient } from 'kibana/public';
|
|
||||||
import { DiscoverServices } from '../../../../build_services';
|
|
||||||
import { dataPluginMock } from '../../../../../../data/public/mocks';
|
|
||||||
import { SAMPLE_SIZE_SETTING } from '../../../../../common';
|
|
||||||
import { SortOrder } from '../../../../saved_searches/types';
|
import { SortOrder } from '../../../../saved_searches/types';
|
||||||
|
import { discoverServiceMock } from '../../../../__mocks__/services';
|
||||||
|
|
||||||
describe('updateSearchSource', () => {
|
describe('updateSearchSource', () => {
|
||||||
test('updates a given search source', async () => {
|
test('updates a given search source', async () => {
|
||||||
const persistentSearchSourceMock = createSearchSourceMock({});
|
const persistentSearchSourceMock = createSearchSourceMock({});
|
||||||
const volatileSearchSourceMock = createSearchSourceMock({});
|
const volatileSearchSourceMock = createSearchSourceMock({});
|
||||||
volatileSearchSourceMock.setParent(persistentSearchSourceMock);
|
volatileSearchSourceMock.setParent(persistentSearchSourceMock);
|
||||||
const sampleSize = 250;
|
|
||||||
updateSearchSource(volatileSearchSourceMock, false, {
|
updateSearchSource(volatileSearchSourceMock, false, {
|
||||||
indexPattern: indexPatternMock,
|
indexPattern: indexPatternMock,
|
||||||
services: ({
|
services: discoverServiceMock,
|
||||||
data: dataPluginMock.createStartContract(),
|
|
||||||
timefilter: {
|
|
||||||
createFilter: jest.fn(),
|
|
||||||
},
|
|
||||||
uiSettings: ({
|
|
||||||
get: (key: string) => {
|
|
||||||
if (key === SAMPLE_SIZE_SETTING) {
|
|
||||||
return sampleSize;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
} as unknown) as IUiSettingsClient,
|
|
||||||
} as unknown) as DiscoverServices,
|
|
||||||
sort: [] as SortOrder[],
|
sort: [] as SortOrder[],
|
||||||
useNewFieldsApi: false,
|
useNewFieldsApi: false,
|
||||||
});
|
});
|
||||||
expect(persistentSearchSourceMock.getField('index')).toEqual(indexPatternMock);
|
expect(persistentSearchSourceMock.getField('index')).toEqual(indexPatternMock);
|
||||||
expect(volatileSearchSourceMock.getField('size')).toEqual(sampleSize);
|
|
||||||
expect(volatileSearchSourceMock.getField('fields')).toBe(undefined);
|
expect(volatileSearchSourceMock.getField('fields')).toBe(undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -49,28 +31,13 @@ describe('updateSearchSource', () => {
|
||||||
const persistentSearchSourceMock = createSearchSourceMock({});
|
const persistentSearchSourceMock = createSearchSourceMock({});
|
||||||
const volatileSearchSourceMock = createSearchSourceMock({});
|
const volatileSearchSourceMock = createSearchSourceMock({});
|
||||||
volatileSearchSourceMock.setParent(persistentSearchSourceMock);
|
volatileSearchSourceMock.setParent(persistentSearchSourceMock);
|
||||||
const sampleSize = 250;
|
|
||||||
updateSearchSource(volatileSearchSourceMock, false, {
|
updateSearchSource(volatileSearchSourceMock, false, {
|
||||||
indexPattern: indexPatternMock,
|
indexPattern: indexPatternMock,
|
||||||
services: ({
|
services: discoverServiceMock,
|
||||||
data: dataPluginMock.createStartContract(),
|
|
||||||
timefilter: {
|
|
||||||
createFilter: jest.fn(),
|
|
||||||
},
|
|
||||||
uiSettings: ({
|
|
||||||
get: (key: string) => {
|
|
||||||
if (key === SAMPLE_SIZE_SETTING) {
|
|
||||||
return sampleSize;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
} as unknown) as IUiSettingsClient,
|
|
||||||
} as unknown) as DiscoverServices,
|
|
||||||
sort: [] as SortOrder[],
|
sort: [] as SortOrder[],
|
||||||
useNewFieldsApi: true,
|
useNewFieldsApi: true,
|
||||||
});
|
});
|
||||||
expect(persistentSearchSourceMock.getField('index')).toEqual(indexPatternMock);
|
expect(persistentSearchSourceMock.getField('index')).toEqual(indexPatternMock);
|
||||||
expect(volatileSearchSourceMock.getField('size')).toEqual(sampleSize);
|
|
||||||
expect(volatileSearchSourceMock.getField('fields')).toEqual([
|
expect(volatileSearchSourceMock.getField('fields')).toEqual([
|
||||||
{ field: '*', include_unmapped: 'true' },
|
{ field: '*', include_unmapped: 'true' },
|
||||||
]);
|
]);
|
||||||
|
@ -81,28 +48,13 @@ describe('updateSearchSource', () => {
|
||||||
const persistentSearchSourceMock = createSearchSourceMock({});
|
const persistentSearchSourceMock = createSearchSourceMock({});
|
||||||
const volatileSearchSourceMock = createSearchSourceMock({});
|
const volatileSearchSourceMock = createSearchSourceMock({});
|
||||||
volatileSearchSourceMock.setParent(persistentSearchSourceMock);
|
volatileSearchSourceMock.setParent(persistentSearchSourceMock);
|
||||||
const sampleSize = 250;
|
|
||||||
updateSearchSource(volatileSearchSourceMock, false, {
|
updateSearchSource(volatileSearchSourceMock, false, {
|
||||||
indexPattern: indexPatternMock,
|
indexPattern: indexPatternMock,
|
||||||
services: ({
|
services: discoverServiceMock,
|
||||||
data: dataPluginMock.createStartContract(),
|
|
||||||
timefilter: {
|
|
||||||
createFilter: jest.fn(),
|
|
||||||
},
|
|
||||||
uiSettings: ({
|
|
||||||
get: (key: string) => {
|
|
||||||
if (key === SAMPLE_SIZE_SETTING) {
|
|
||||||
return sampleSize;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
} as unknown) as IUiSettingsClient,
|
|
||||||
} as unknown) as DiscoverServices,
|
|
||||||
sort: [] as SortOrder[],
|
sort: [] as SortOrder[],
|
||||||
useNewFieldsApi: true,
|
useNewFieldsApi: true,
|
||||||
});
|
});
|
||||||
expect(persistentSearchSourceMock.getField('index')).toEqual(indexPatternMock);
|
expect(persistentSearchSourceMock.getField('index')).toEqual(indexPatternMock);
|
||||||
expect(volatileSearchSourceMock.getField('size')).toEqual(sampleSize);
|
|
||||||
expect(volatileSearchSourceMock.getField('fields')).toEqual([
|
expect(volatileSearchSourceMock.getField('fields')).toEqual([
|
||||||
{ field: '*', include_unmapped: 'true' },
|
{ field: '*', include_unmapped: 'true' },
|
||||||
]);
|
]);
|
||||||
|
@ -113,28 +65,13 @@ describe('updateSearchSource', () => {
|
||||||
const persistentSearchSourceMock = createSearchSourceMock({});
|
const persistentSearchSourceMock = createSearchSourceMock({});
|
||||||
const volatileSearchSourceMock = createSearchSourceMock({});
|
const volatileSearchSourceMock = createSearchSourceMock({});
|
||||||
volatileSearchSourceMock.setParent(persistentSearchSourceMock);
|
volatileSearchSourceMock.setParent(persistentSearchSourceMock);
|
||||||
const sampleSize = 250;
|
|
||||||
updateSearchSource(volatileSearchSourceMock, false, {
|
updateSearchSource(volatileSearchSourceMock, false, {
|
||||||
indexPattern: indexPatternMock,
|
indexPattern: indexPatternMock,
|
||||||
services: ({
|
services: discoverServiceMock,
|
||||||
data: dataPluginMock.createStartContract(),
|
|
||||||
timefilter: {
|
|
||||||
createFilter: jest.fn(),
|
|
||||||
},
|
|
||||||
uiSettings: ({
|
|
||||||
get: (key: string) => {
|
|
||||||
if (key === SAMPLE_SIZE_SETTING) {
|
|
||||||
return sampleSize;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
} as unknown) as IUiSettingsClient,
|
|
||||||
} as unknown) as DiscoverServices,
|
|
||||||
sort: [] as SortOrder[],
|
sort: [] as SortOrder[],
|
||||||
useNewFieldsApi: false,
|
useNewFieldsApi: false,
|
||||||
});
|
});
|
||||||
expect(persistentSearchSourceMock.getField('index')).toEqual(indexPatternMock);
|
expect(persistentSearchSourceMock.getField('index')).toEqual(indexPatternMock);
|
||||||
expect(volatileSearchSourceMock.getField('size')).toEqual(sampleSize);
|
|
||||||
expect(volatileSearchSourceMock.getField('fields')).toEqual(undefined);
|
expect(volatileSearchSourceMock.getField('fields')).toEqual(undefined);
|
||||||
expect(volatileSearchSourceMock.getField('fieldsFromSource')).toBe(undefined);
|
expect(volatileSearchSourceMock.getField('fieldsFromSource')).toBe(undefined);
|
||||||
});
|
});
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { getSortForSearchSource } from '../../../angular/doc_table';
|
import { getSortForSearchSource } from '../../../angular/doc_table';
|
||||||
import { SAMPLE_SIZE_SETTING, SORT_DEFAULT_ORDER_SETTING } from '../../../../../common';
|
import { SORT_DEFAULT_ORDER_SETTING } from '../../../../../common';
|
||||||
import { IndexPattern, ISearchSource } from '../../../../../../data/common';
|
import { IndexPattern, ISearchSource } from '../../../../../../data/common';
|
||||||
import { SortOrder } from '../../../../saved_searches/types';
|
import { SortOrder } from '../../../../saved_searches/types';
|
||||||
import { DiscoverServices } from '../../../../build_services';
|
import { DiscoverServices } from '../../../../build_services';
|
||||||
|
@ -32,25 +32,22 @@ export function updateSearchSource(
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
const { uiSettings, data } = services;
|
const { uiSettings, data } = services;
|
||||||
const usedSort = getSortForSearchSource(
|
const parentSearchSource = persist ? searchSource : searchSource.getParent()!;
|
||||||
sort,
|
|
||||||
indexPattern,
|
|
||||||
uiSettings.get(SORT_DEFAULT_ORDER_SETTING)
|
|
||||||
);
|
|
||||||
const usedSearchSource = persist ? searchSource : searchSource.getParent()!;
|
|
||||||
|
|
||||||
usedSearchSource
|
parentSearchSource
|
||||||
.setField('index', indexPattern)
|
.setField('index', indexPattern)
|
||||||
.setField('query', data.query.queryString.getQuery() || null)
|
.setField('query', data.query.queryString.getQuery() || null)
|
||||||
.setField('filter', data.query.filterManager.getFilters());
|
.setField('filter', data.query.filterManager.getFilters());
|
||||||
|
|
||||||
if (!persist) {
|
if (!persist) {
|
||||||
|
const usedSort = getSortForSearchSource(
|
||||||
|
sort,
|
||||||
|
indexPattern,
|
||||||
|
uiSettings.get(SORT_DEFAULT_ORDER_SETTING)
|
||||||
|
);
|
||||||
searchSource
|
searchSource
|
||||||
.setField('trackTotalHits', true)
|
.setField('trackTotalHits', true)
|
||||||
.setField('size', uiSettings.get(SAMPLE_SIZE_SETTING))
|
|
||||||
.setField('sort', usedSort)
|
.setField('sort', usedSort)
|
||||||
.setField('highlightAll', true)
|
|
||||||
.setField('version', true)
|
|
||||||
// Even when searching rollups, we want to use the default strategy so that we get back a
|
// Even when searching rollups, we want to use the default strategy so that we get back a
|
||||||
// document-like response.
|
// document-like response.
|
||||||
.setPreferredSearchStrategyId('default');
|
.setPreferredSearchStrategyId('default');
|
||||||
|
|
|
@ -5,12 +5,15 @@
|
||||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||||
* Side Public License, v 1.
|
* Side Public License, v 1.
|
||||||
*/
|
*/
|
||||||
|
import { useRef } from 'react';
|
||||||
|
import { BehaviorSubject } from 'rxjs';
|
||||||
|
|
||||||
const COMMA_SEPARATOR_RE = /(\d)(?=(\d{3})+(?!\d))/g;
|
export function useBehaviorSubject<T>(props: T): BehaviorSubject<T> {
|
||||||
|
const ref = useRef<BehaviorSubject<T> | null>(null);
|
||||||
|
|
||||||
/**
|
if (ref.current === null) {
|
||||||
* Converts a number to a string and adds commas
|
ref.current = new BehaviorSubject(props);
|
||||||
* as thousands separators
|
}
|
||||||
*/
|
|
||||||
export const formatNumWithCommas = (input: number) =>
|
return ref.current;
|
||||||
String(input).replace(COMMA_SEPARATOR_RE, '$1,');
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
/*
|
||||||
|
* 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 { useState, useEffect } from 'react';
|
||||||
|
import { BehaviorSubject } from 'rxjs';
|
||||||
|
import { DataMsg } from '../services/use_saved_search';
|
||||||
|
|
||||||
|
export function useDataState(data$: BehaviorSubject<DataMsg>) {
|
||||||
|
const [fetchState, setFetchState] = useState<DataMsg>(data$.getValue());
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const subscription = data$.subscribe((next) => {
|
||||||
|
if (next.fetchStatus !== fetchState.fetchStatus) {
|
||||||
|
setFetchState({ ...fetchState, ...next });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return () => subscription.unsubscribe();
|
||||||
|
}, [data$, fetchState, setFetchState]);
|
||||||
|
return fetchState;
|
||||||
|
}
|
|
@ -55,6 +55,7 @@ describe('Test of <Doc /> helper / hook', () => {
|
||||||
},
|
},
|
||||||
"script_fields": Array [],
|
"script_fields": Array [],
|
||||||
"stored_fields": Array [],
|
"stored_fields": Array [],
|
||||||
|
"version": true,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
`);
|
`);
|
||||||
|
@ -84,6 +85,7 @@ describe('Test of <Doc /> helper / hook', () => {
|
||||||
"runtime_mappings": Object {},
|
"runtime_mappings": Object {},
|
||||||
"script_fields": Array [],
|
"script_fields": Array [],
|
||||||
"stored_fields": Array [],
|
"stored_fields": Array [],
|
||||||
|
"version": true,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
`);
|
`);
|
||||||
|
@ -114,6 +116,7 @@ describe('Test of <Doc /> helper / hook', () => {
|
||||||
"runtime_mappings": Object {},
|
"runtime_mappings": Object {},
|
||||||
"script_fields": Array [],
|
"script_fields": Array [],
|
||||||
"stored_fields": Array [],
|
"stored_fields": Array [],
|
||||||
|
"version": true,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
`);
|
`);
|
||||||
|
@ -162,6 +165,7 @@ describe('Test of <Doc /> helper / hook', () => {
|
||||||
},
|
},
|
||||||
"script_fields": Array [],
|
"script_fields": Array [],
|
||||||
"stored_fields": Array [],
|
"stored_fields": Array [],
|
||||||
|
"version": true,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
`);
|
`);
|
||||||
|
|
|
@ -37,6 +37,7 @@ export function buildSearchBody(
|
||||||
},
|
},
|
||||||
stored_fields: computedFields.storedFields,
|
stored_fields: computedFields.storedFields,
|
||||||
script_fields: computedFields.scriptFields,
|
script_fields: computedFields.scriptFields,
|
||||||
|
version: true,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
if (!request.body) {
|
if (!request.body) {
|
||||||
|
|
|
@ -1,9 +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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export { formatNumWithCommas } from './format_number_with_commas';
|
|
|
@ -9,6 +9,7 @@
|
||||||
export enum FetchStatus {
|
export enum FetchStatus {
|
||||||
UNINITIALIZED = 'uninitialized',
|
UNINITIALIZED = 'uninitialized',
|
||||||
LOADING = 'loading',
|
LOADING = 'loading',
|
||||||
|
PARTIAL = 'partial',
|
||||||
COMPLETE = 'complete',
|
COMPLETE = 'complete',
|
||||||
ERROR = 'error',
|
ERROR = 'error',
|
||||||
}
|
}
|
||||||
|
|
|
@ -108,7 +108,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
||||||
it('should modify the time range when the histogram is brushed', async function () {
|
it('should modify the time range when the histogram is brushed', async function () {
|
||||||
// this is the number of renderings of the histogram needed when new data is fetched
|
// this is the number of renderings of the histogram needed when new data is fetched
|
||||||
// this needs to be improved
|
// this needs to be improved
|
||||||
const renderingCountInc = 1;
|
const renderingCountInc = 2;
|
||||||
const prevRenderingCount = await elasticChart.getVisualizationRenderingCount();
|
const prevRenderingCount = await elasticChart.getVisualizationRenderingCount();
|
||||||
await PageObjects.timePicker.setDefaultAbsoluteRange();
|
await PageObjects.timePicker.setDefaultAbsoluteRange();
|
||||||
await PageObjects.discover.waitUntilSearchingHasFinished();
|
await PageObjects.discover.waitUntilSearchingHasFinished();
|
||||||
|
|
|
@ -15,6 +15,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
||||||
const esArchiver = getService('esArchiver');
|
const esArchiver = getService('esArchiver');
|
||||||
const kibanaServer = getService('kibanaServer');
|
const kibanaServer = getService('kibanaServer');
|
||||||
const inspector = getService('inspector');
|
const inspector = getService('inspector');
|
||||||
|
const testSubjects = getService('testSubjects');
|
||||||
|
|
||||||
const STATS_ROW_NAME_INDEX = 0;
|
const STATS_ROW_NAME_INDEX = 0;
|
||||||
const STATS_ROW_VALUE_INDEX = 1;
|
const STATS_ROW_VALUE_INDEX = 1;
|
||||||
|
@ -50,15 +51,28 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
||||||
|
|
||||||
it('should display request stats with no results', async () => {
|
it('should display request stats with no results', async () => {
|
||||||
await inspector.open();
|
await inspector.open();
|
||||||
const requestStats = await inspector.getTableData();
|
await testSubjects.click('inspectorRequestChooser');
|
||||||
|
let foundZero = false;
|
||||||
expect(getHitCount(requestStats)).to.be('0');
|
for (const subj of ['Documents', 'Total hits', 'Charts']) {
|
||||||
|
await testSubjects.click(`inspectorRequestChooser${subj}`);
|
||||||
|
if (testSubjects.exists('inspectorRequestDetailStatistics', { timeout: 500 })) {
|
||||||
|
await testSubjects.click(`inspectorRequestDetailStatistics`);
|
||||||
|
const requestStatsTotalHits = getHitCount(await inspector.getTableData());
|
||||||
|
if (requestStatsTotalHits === '0') {
|
||||||
|
foundZero = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
expect(foundZero).to.be(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should display request stats with results', async () => {
|
it('should display request stats with results', async () => {
|
||||||
await PageObjects.timePicker.setDefaultAbsoluteRange();
|
await PageObjects.timePicker.setDefaultAbsoluteRange();
|
||||||
|
|
||||||
await inspector.open();
|
await inspector.open();
|
||||||
|
await testSubjects.click('inspectorRequestChooser');
|
||||||
|
await testSubjects.click(`inspectorRequestChooserDocuments`);
|
||||||
|
await testSubjects.click(`inspectorRequestDetailStatistics`);
|
||||||
const requestStats = await inspector.getTableData();
|
const requestStats = await inspector.getTableData();
|
||||||
|
|
||||||
expect(getHitCount(requestStats)).to.be('500');
|
expect(getHitCount(requestStats)).to.be('500');
|
||||||
|
|
|
@ -1715,7 +1715,6 @@
|
||||||
"discover.howToChangeTheTimeTooltip": "時刻を変更するには、グローバル時刻フィルターを使用します。",
|
"discover.howToChangeTheTimeTooltip": "時刻を変更するには、グローバル時刻フィルターを使用します。",
|
||||||
"discover.howToSeeOtherMatchingDocumentsDescription": "これらは検索条件に一致した初めの {sampleSize} 件のドキュメントです。他の結果を表示するには検索条件を絞ってください。",
|
"discover.howToSeeOtherMatchingDocumentsDescription": "これらは検索条件に一致した初めの {sampleSize} 件のドキュメントです。他の結果を表示するには検索条件を絞ってください。",
|
||||||
"discover.howToSeeOtherMatchingDocumentsDescriptionGrid": "これらは検索条件に一致した初めの {sampleSize} 件のドキュメントです。他の結果を表示するには検索条件を絞ってください。",
|
"discover.howToSeeOtherMatchingDocumentsDescriptionGrid": "これらは検索条件に一致した初めの {sampleSize} 件のドキュメントです。他の結果を表示するには検索条件を絞ってください。",
|
||||||
"discover.inspectorRequestDataTitle": "データ",
|
|
||||||
"discover.inspectorRequestDescriptionDocument": "このリクエストはElasticsearchにクエリをかけ、検索データを取得します。",
|
"discover.inspectorRequestDescriptionDocument": "このリクエストはElasticsearchにクエリをかけ、検索データを取得します。",
|
||||||
"discover.json.codeEditorAriaLabel": "Elasticsearch ドキュメントの JSON ビューのみを読み込む",
|
"discover.json.codeEditorAriaLabel": "Elasticsearch ドキュメントの JSON ビューのみを読み込む",
|
||||||
"discover.json.copyToClipboardLabel": "クリップボードにコピー",
|
"discover.json.copyToClipboardLabel": "クリップボードにコピー",
|
||||||
|
|
|
@ -1721,11 +1721,9 @@
|
||||||
"discover.helpMenu.appName": "Discover",
|
"discover.helpMenu.appName": "Discover",
|
||||||
"discover.hideChart": "隐藏图表",
|
"discover.hideChart": "隐藏图表",
|
||||||
"discover.histogramOfFoundDocumentsAriaLabel": "已找到文档的直方图",
|
"discover.histogramOfFoundDocumentsAriaLabel": "已找到文档的直方图",
|
||||||
"discover.hitsPluralTitle": "{hits, plural, other {命中}}",
|
|
||||||
"discover.howToChangeTheTimeTooltip": "要更改时间,请使用全局时间筛选。",
|
"discover.howToChangeTheTimeTooltip": "要更改时间,请使用全局时间筛选。",
|
||||||
"discover.howToSeeOtherMatchingDocumentsDescription": "下面是与您的搜索匹配的前 {sampleSize} 个文档,请优化您的搜索以查看其他文档。",
|
"discover.howToSeeOtherMatchingDocumentsDescription": "下面是与您的搜索匹配的前 {sampleSize} 个文档,请优化您的搜索以查看其他文档。",
|
||||||
"discover.howToSeeOtherMatchingDocumentsDescriptionGrid": "下面是与您的搜索匹配的前 {sampleSize} 个文档,请优化您的搜索以查看其他文档。",
|
"discover.howToSeeOtherMatchingDocumentsDescriptionGrid": "下面是与您的搜索匹配的前 {sampleSize} 个文档,请优化您的搜索以查看其他文档。",
|
||||||
"discover.inspectorRequestDataTitle": "数据",
|
|
||||||
"discover.inspectorRequestDescriptionDocument": "此请求将查询 Elasticsearch 以获取搜索的数据。",
|
"discover.inspectorRequestDescriptionDocument": "此请求将查询 Elasticsearch 以获取搜索的数据。",
|
||||||
"discover.json.codeEditorAriaLabel": "Elasticsearch 文档的只读 JSON 视图",
|
"discover.json.codeEditorAriaLabel": "Elasticsearch 文档的只读 JSON 视图",
|
||||||
"discover.json.copyToClipboardLabel": "复制到剪贴板",
|
"discover.json.copyToClipboardLabel": "复制到剪贴板",
|
||||||
|
|
Loading…
Reference in a new issue