mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[OnWeek][Discover] Allow to fetch more documents on Discover page (#163784)
> [!WARNING] > Sorry, I had to recreate the PR https://github.com/elastic/kibana/pull/157241 > Please submit your review again. - Closes https://github.com/elastic/kibana/issues/155019 Per docs https://www.elastic.co/guide/en/elasticsearch/reference/current/paginate-search-results.html <img width="851" alt="Screenshot 2023-05-10 at 10 25 20" src="b4b9fef4
-7dd8-40ed-8244-343889fc4367"> ## Summary 1. This PR improves `search_after` pagination for `date_nanos` time fields. `sort` value will be returned from ES as a string instead of a rounded and incorrect timestamp. This change allows to also simplify logic on Surrounding document page. Before: <img width="400" alt="Screenshot 2023-05-08 at 17 36 19" src="fd9f45c4
-5dc2-4103-83b9-8810e3a6e0df"> After: <img width="400" alt="Screenshot 2023-05-08 at 17 37 13" src="fe9090c0
-2116-4f77-9a57-a96ae6b00365"> 2. Also in this PR we now allow users to load more documents within the same time range. Once the button is pressed, it will load next portion of documents (same "sampleSize" value will be used). Currently, we limit max total loaded documents to 10000. "Load more" demo:  If refresh interval is on, the button becomes disabled:  Date nanos demo:  100x Flaky test runner https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/2801 --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
8ffbc7164d
commit
110449df5c
45 changed files with 1806 additions and 207 deletions
|
@ -49,3 +49,8 @@ export const esHitsMock = [
|
|||
},
|
||||
},
|
||||
];
|
||||
|
||||
export const esHitsMockWithSort = esHitsMock.map((hit) => ({
|
||||
...hit,
|
||||
sort: [hit._source.date], // some `sort` param should be specified for "fetch more" to work
|
||||
}));
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
export const MAX_LOADED_GRID_ROWS = 10000;
|
||||
export const DEFAULT_ROWS_PER_PAGE = 100;
|
||||
export const ROWS_PER_PAGE_OPTIONS = [10, 25, 50, DEFAULT_ROWS_PER_PAGE, 250, 500];
|
||||
export enum VIEW_MODE {
|
||||
|
|
|
@ -0,0 +1,144 @@
|
|||
/*
|
||||
* 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 { SortDirection } from '@kbn/data-plugin/common';
|
||||
import { createStubDataView } from '@kbn/data-views-plugin/common/data_view.stub';
|
||||
import {
|
||||
getEsQuerySort,
|
||||
getESQuerySortForTieBreaker,
|
||||
getESQuerySortForTimeField,
|
||||
getTieBreakerFieldName,
|
||||
} from './get_es_query_sort';
|
||||
import { CONTEXT_TIE_BREAKER_FIELDS_SETTING } from '@kbn/discover-utils';
|
||||
import { IUiSettingsClient } from '@kbn/core-ui-settings-browser';
|
||||
|
||||
const dataView = createStubDataView({
|
||||
spec: {
|
||||
id: 'logstash-*',
|
||||
fields: {
|
||||
test_field: {
|
||||
name: 'test_field',
|
||||
type: 'string',
|
||||
esTypes: ['keyword'],
|
||||
aggregatable: true,
|
||||
searchable: true,
|
||||
},
|
||||
test_field_not_sortable: {
|
||||
name: 'test_field_not_sortable',
|
||||
type: 'string',
|
||||
esTypes: ['keyword'],
|
||||
aggregatable: false,
|
||||
searchable: false,
|
||||
},
|
||||
},
|
||||
title: 'logstash-*',
|
||||
timeFieldName: '@timestamp',
|
||||
},
|
||||
});
|
||||
|
||||
describe('get_es_query_sort', function () {
|
||||
test('getEsQuerySort should return sort params', function () {
|
||||
expect(
|
||||
getEsQuerySort({
|
||||
sortDir: SortDirection.desc,
|
||||
timeFieldName: 'testTimeField',
|
||||
isTimeNanosBased: false,
|
||||
tieBreakerFieldName: 'testTieBreakerField',
|
||||
})
|
||||
).toStrictEqual([
|
||||
{ testTimeField: { format: 'strict_date_optional_time', order: 'desc' } },
|
||||
{ testTieBreakerField: 'desc' },
|
||||
]);
|
||||
|
||||
expect(
|
||||
getEsQuerySort({
|
||||
sortDir: SortDirection.asc,
|
||||
timeFieldName: 'testTimeField',
|
||||
isTimeNanosBased: true,
|
||||
tieBreakerFieldName: 'testTieBreakerField',
|
||||
})
|
||||
).toStrictEqual([
|
||||
{
|
||||
testTimeField: {
|
||||
format: 'strict_date_optional_time_nanos',
|
||||
numeric_type: 'date_nanos',
|
||||
order: 'asc',
|
||||
},
|
||||
},
|
||||
{ testTieBreakerField: 'asc' },
|
||||
]);
|
||||
});
|
||||
|
||||
test('getESQuerySortForTimeField should return time field as sort param', function () {
|
||||
expect(
|
||||
getESQuerySortForTimeField({
|
||||
sortDir: SortDirection.desc,
|
||||
timeFieldName: 'testTimeField',
|
||||
isTimeNanosBased: false,
|
||||
})
|
||||
).toStrictEqual({
|
||||
testTimeField: {
|
||||
format: 'strict_date_optional_time',
|
||||
order: 'desc',
|
||||
},
|
||||
});
|
||||
|
||||
expect(
|
||||
getESQuerySortForTimeField({
|
||||
sortDir: SortDirection.asc,
|
||||
timeFieldName: 'testTimeField',
|
||||
isTimeNanosBased: true,
|
||||
})
|
||||
).toStrictEqual({
|
||||
testTimeField: {
|
||||
format: 'strict_date_optional_time_nanos',
|
||||
numeric_type: 'date_nanos',
|
||||
order: 'asc',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('getESQuerySortForTieBreaker should return tie breaker as sort param', function () {
|
||||
expect(
|
||||
getESQuerySortForTieBreaker({
|
||||
sortDir: SortDirection.desc,
|
||||
tieBreakerFieldName: 'testTieBreaker',
|
||||
})
|
||||
).toStrictEqual({ testTieBreaker: 'desc' });
|
||||
});
|
||||
|
||||
test('getTieBreakerFieldName should return a correct tie breaker', function () {
|
||||
expect(
|
||||
getTieBreakerFieldName(dataView, {
|
||||
get: (key) => (key === CONTEXT_TIE_BREAKER_FIELDS_SETTING ? ['_doc'] : undefined),
|
||||
} as IUiSettingsClient)
|
||||
).toBe('_doc');
|
||||
|
||||
expect(
|
||||
getTieBreakerFieldName(dataView, {
|
||||
get: (key) =>
|
||||
key === CONTEXT_TIE_BREAKER_FIELDS_SETTING
|
||||
? ['test_field_not_sortable', '_doc']
|
||||
: undefined,
|
||||
} as IUiSettingsClient)
|
||||
).toBe('_doc');
|
||||
|
||||
expect(
|
||||
getTieBreakerFieldName(dataView, {
|
||||
get: (key) =>
|
||||
key === CONTEXT_TIE_BREAKER_FIELDS_SETTING ? ['test_field', '_doc'] : undefined,
|
||||
} as IUiSettingsClient)
|
||||
).toBe('test_field');
|
||||
|
||||
expect(
|
||||
getTieBreakerFieldName(dataView, {
|
||||
get: (key) => undefined,
|
||||
} as IUiSettingsClient)
|
||||
).toBeUndefined();
|
||||
});
|
||||
});
|
107
src/plugins/discover/common/utils/sorting/get_es_query_sort.ts
Normal file
107
src/plugins/discover/common/utils/sorting/get_es_query_sort.ts
Normal file
|
@ -0,0 +1,107 @@
|
|||
/*
|
||||
* 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 type { EsQuerySortValue, SortDirection } from '@kbn/data-plugin/public';
|
||||
import type { DataView } from '@kbn/data-views-plugin/common';
|
||||
import type { IUiSettingsClient } from '@kbn/core-ui-settings-browser';
|
||||
import { CONTEXT_TIE_BREAKER_FIELDS_SETTING } from '@kbn/discover-utils';
|
||||
|
||||
/**
|
||||
* Returns `EsQuerySort` which is used to sort records in the ES query
|
||||
* https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-sort.html
|
||||
* @param sortDir
|
||||
* @param timeFieldName
|
||||
* @param tieBreakerFieldName
|
||||
* @param isTimeNanosBased
|
||||
*/
|
||||
export function getEsQuerySort({
|
||||
sortDir,
|
||||
timeFieldName,
|
||||
tieBreakerFieldName,
|
||||
isTimeNanosBased,
|
||||
}: {
|
||||
sortDir: SortDirection;
|
||||
timeFieldName: string;
|
||||
tieBreakerFieldName: string;
|
||||
isTimeNanosBased: boolean;
|
||||
}): [EsQuerySortValue, EsQuerySortValue] {
|
||||
return [
|
||||
getESQuerySortForTimeField({ sortDir, timeFieldName, isTimeNanosBased }),
|
||||
getESQuerySortForTieBreaker({ sortDir, tieBreakerFieldName }),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares "sort" structure for a time field for next ES request
|
||||
* @param sortDir
|
||||
* @param timeFieldName
|
||||
* @param isTimeNanosBased
|
||||
*/
|
||||
export function getESQuerySortForTimeField({
|
||||
sortDir,
|
||||
timeFieldName,
|
||||
isTimeNanosBased,
|
||||
}: {
|
||||
sortDir: SortDirection;
|
||||
timeFieldName: string;
|
||||
isTimeNanosBased: boolean;
|
||||
}): EsQuerySortValue {
|
||||
return {
|
||||
[timeFieldName]: {
|
||||
order: sortDir,
|
||||
...(isTimeNanosBased
|
||||
? {
|
||||
format: 'strict_date_optional_time_nanos',
|
||||
numeric_type: 'date_nanos',
|
||||
}
|
||||
: { format: 'strict_date_optional_time' }),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares "sort" structure for a tie breaker for next ES request
|
||||
* @param sortDir
|
||||
* @param tieBreakerFieldName
|
||||
*/
|
||||
export function getESQuerySortForTieBreaker({
|
||||
sortDir,
|
||||
tieBreakerFieldName,
|
||||
}: {
|
||||
sortDir: SortDirection;
|
||||
tieBreakerFieldName: string;
|
||||
}): EsQuerySortValue {
|
||||
return { [tieBreakerFieldName]: sortDir };
|
||||
}
|
||||
|
||||
/**
|
||||
* The default tie breaker for Discover
|
||||
*/
|
||||
export const DEFAULT_TIE_BREAKER_NAME = '_doc';
|
||||
|
||||
/**
|
||||
* The list of field names that are allowed for sorting, but not included in
|
||||
* data view fields.
|
||||
*/
|
||||
const META_FIELD_NAMES: string[] = ['_seq_no', '_doc', '_uid'];
|
||||
|
||||
/**
|
||||
* Returns a field from the intersection of the set of sortable fields in the
|
||||
* given data view and a given set of candidate field names.
|
||||
*/
|
||||
export function getTieBreakerFieldName(
|
||||
dataView: DataView,
|
||||
uiSettings: Pick<IUiSettingsClient, 'get'>
|
||||
) {
|
||||
const sortableFields = (uiSettings.get(CONTEXT_TIE_BREAKER_FIELDS_SETTING) || []).filter(
|
||||
(fieldName: string) =>
|
||||
META_FIELD_NAMES.includes(fieldName) ||
|
||||
(dataView.fields.getByName(fieldName) || { sortable: false }).sortable
|
||||
);
|
||||
return sortableFields[0];
|
||||
}
|
|
@ -16,25 +16,80 @@ describe('getSortForSearchSource function', function () {
|
|||
|
||||
test('should return an object to use for searchSource when columns are given', function () {
|
||||
const cols = [['bytes', 'desc']] as SortOrder[];
|
||||
expect(getSortForSearchSource(cols, stubDataView)).toEqual([{ bytes: 'desc' }]);
|
||||
expect(getSortForSearchSource(cols, stubDataView, 'asc')).toEqual([{ bytes: 'desc' }]);
|
||||
expect(
|
||||
getSortForSearchSource({
|
||||
sort: cols,
|
||||
dataView: stubDataView,
|
||||
defaultSortDir: 'desc',
|
||||
includeTieBreaker: true,
|
||||
})
|
||||
).toEqual([{ bytes: 'desc' }, { _doc: 'desc' }]);
|
||||
|
||||
expect(getSortForSearchSource(cols, stubDataViewWithoutTimeField)).toEqual([{ bytes: 'desc' }]);
|
||||
expect(getSortForSearchSource(cols, stubDataViewWithoutTimeField, 'asc')).toEqual([
|
||||
{ bytes: 'desc' },
|
||||
]);
|
||||
expect(
|
||||
getSortForSearchSource({
|
||||
sort: cols,
|
||||
dataView: stubDataView,
|
||||
defaultSortDir: 'asc',
|
||||
includeTieBreaker: true,
|
||||
})
|
||||
).toEqual([{ bytes: 'desc' }, { _doc: 'desc' }]);
|
||||
|
||||
expect(
|
||||
getSortForSearchSource({
|
||||
sort: cols,
|
||||
dataView: stubDataView,
|
||||
defaultSortDir: 'asc',
|
||||
})
|
||||
).toEqual([{ bytes: 'desc' }]);
|
||||
|
||||
expect(
|
||||
getSortForSearchSource({
|
||||
sort: cols,
|
||||
dataView: stubDataViewWithoutTimeField,
|
||||
defaultSortDir: 'desc',
|
||||
includeTieBreaker: true,
|
||||
})
|
||||
).toEqual([{ bytes: 'desc' }]);
|
||||
|
||||
expect(
|
||||
getSortForSearchSource({
|
||||
sort: cols,
|
||||
dataView: stubDataViewWithoutTimeField,
|
||||
defaultSortDir: 'asc',
|
||||
})
|
||||
).toEqual([{ bytes: 'desc' }]);
|
||||
});
|
||||
|
||||
test('should return an object to use for searchSource when no columns are given', function () {
|
||||
const cols = [] as SortOrder[];
|
||||
expect(getSortForSearchSource(cols, stubDataView)).toEqual([{ _doc: 'desc' }]);
|
||||
expect(getSortForSearchSource(cols, stubDataView, 'asc')).toEqual([{ _doc: 'asc' }]);
|
||||
expect(
|
||||
getSortForSearchSource({
|
||||
sort: cols,
|
||||
dataView: stubDataView,
|
||||
defaultSortDir: 'desc',
|
||||
})
|
||||
).toEqual([{ _doc: 'desc' }]);
|
||||
expect(
|
||||
getSortForSearchSource({
|
||||
sort: cols,
|
||||
dataView: stubDataView,
|
||||
defaultSortDir: 'asc',
|
||||
})
|
||||
).toEqual([{ _doc: 'asc' }]);
|
||||
|
||||
expect(getSortForSearchSource(cols, stubDataViewWithoutTimeField)).toEqual([
|
||||
{ _score: 'desc' },
|
||||
]);
|
||||
expect(getSortForSearchSource(cols, stubDataViewWithoutTimeField, 'asc')).toEqual([
|
||||
{ _score: 'asc' },
|
||||
]);
|
||||
expect(
|
||||
getSortForSearchSource({
|
||||
sort: cols,
|
||||
dataView: stubDataViewWithoutTimeField,
|
||||
defaultSortDir: 'desc',
|
||||
})
|
||||
).toEqual([{ _score: 'desc' }]);
|
||||
expect(
|
||||
getSortForSearchSource({
|
||||
sort: cols,
|
||||
dataView: stubDataViewWithoutTimeField,
|
||||
defaultSortDir: 'asc',
|
||||
})
|
||||
).toEqual([{ _score: 'asc' }]);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,10 +6,15 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import type { DataView } from '@kbn/data-views-plugin/public';
|
||||
import type { EsQuerySortValue } from '@kbn/data-plugin/public';
|
||||
import type { DataView } from '@kbn/data-views-plugin/common';
|
||||
import type { EsQuerySortValue, SortDirection } from '@kbn/data-plugin/common';
|
||||
import type { SortOrder } from '@kbn/saved-search-plugin/public';
|
||||
import { getSort } from './get_sort';
|
||||
import {
|
||||
getESQuerySortForTimeField,
|
||||
getESQuerySortForTieBreaker,
|
||||
DEFAULT_TIE_BREAKER_NAME,
|
||||
} from './get_es_query_sort';
|
||||
|
||||
/**
|
||||
* Prepares sort for search source, that's sending the request to ES
|
||||
|
@ -18,11 +23,19 @@ import { getSort } from './get_sort';
|
|||
* the addon of the numeric_type guarantees the right sort order
|
||||
* when there are indices with date and indices with date_nanos field
|
||||
*/
|
||||
export function getSortForSearchSource(
|
||||
sort?: SortOrder[],
|
||||
dataView?: DataView,
|
||||
defaultDirection: string = 'desc'
|
||||
): EsQuerySortValue[] {
|
||||
export function getSortForSearchSource({
|
||||
sort,
|
||||
dataView,
|
||||
defaultSortDir,
|
||||
includeTieBreaker = false,
|
||||
}: {
|
||||
sort: SortOrder[] | undefined;
|
||||
dataView: DataView | undefined;
|
||||
defaultSortDir: string;
|
||||
includeTieBreaker?: boolean;
|
||||
}): EsQuerySortValue[] {
|
||||
const defaultDirection = defaultSortDir || 'desc';
|
||||
|
||||
if (!sort || !dataView || (Array.isArray(sort) && sort.length === 0)) {
|
||||
if (dataView?.timeFieldName) {
|
||||
// sorting by index order
|
||||
|
@ -31,16 +44,35 @@ export function getSortForSearchSource(
|
|||
return [{ _score: defaultDirection } as EsQuerySortValue];
|
||||
}
|
||||
}
|
||||
|
||||
const { timeFieldName } = dataView;
|
||||
return getSort(sort, dataView).map((sortPair: Record<string, string>) => {
|
||||
if (dataView.isTimeNanosBased() && timeFieldName && sortPair[timeFieldName]) {
|
||||
return {
|
||||
[timeFieldName]: {
|
||||
order: sortPair[timeFieldName],
|
||||
numeric_type: 'date_nanos',
|
||||
},
|
||||
} as EsQuerySortValue;
|
||||
const sortPairs = getSort(sort, dataView);
|
||||
|
||||
const sortForSearchSource = sortPairs.map((sortPair: Record<string, string>) => {
|
||||
if (timeFieldName && sortPair[timeFieldName]) {
|
||||
return getESQuerySortForTimeField({
|
||||
sortDir: sortPair[timeFieldName] as SortDirection,
|
||||
timeFieldName,
|
||||
isTimeNanosBased: dataView.isTimeNanosBased(),
|
||||
});
|
||||
}
|
||||
return sortPair as EsQuerySortValue;
|
||||
});
|
||||
|
||||
// This tie breaker is skipped for CSV reports as it uses PIT
|
||||
if (includeTieBreaker && timeFieldName && sortPairs.length) {
|
||||
const firstSortPair = sortPairs[0];
|
||||
const firstPairSortDir = Array.isArray(firstSortPair)
|
||||
? firstSortPair[1]
|
||||
: Object.values(firstSortPair)[0];
|
||||
|
||||
sortForSearchSource.push(
|
||||
getESQuerySortForTieBreaker({
|
||||
sortDir: firstPairSortDir,
|
||||
tieBreakerFieldName: DEFAULT_TIE_BREAKER_NAME,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return sortForSearchSource;
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React, { useState, Fragment, useMemo, useCallback } from 'react';
|
||||
import React, { Fragment, useCallback, useMemo, useState } from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { EuiHorizontalRule, EuiSpacer, EuiText } from '@elastic/eui';
|
||||
import type { DataView } from '@kbn/data-views-plugin/public';
|
||||
|
@ -15,13 +15,13 @@ import type { SortOrder } from '@kbn/saved-search-plugin/public';
|
|||
import { CellActionsProvider } from '@kbn/cell-actions';
|
||||
import type { DataTableRecord } from '@kbn/discover-utils/types';
|
||||
import {
|
||||
SearchResponseWarnings,
|
||||
type SearchResponseInterceptedWarning,
|
||||
SearchResponseWarnings,
|
||||
} from '@kbn/search-response-warnings';
|
||||
import { CONTEXT_STEP_SETTING, DOC_HIDE_TIME_COLUMN_SETTING } from '@kbn/discover-utils';
|
||||
import { LoadingStatus } from './services/context_query_state';
|
||||
import { ActionBar } from './components/action_bar/action_bar';
|
||||
import { DiscoverGrid } from '../../components/discover_grid/discover_grid';
|
||||
import { DataLoadingState, DiscoverGrid } from '../../components/discover_grid/discover_grid';
|
||||
import { DocViewFilterFn } from '../../services/doc_views/doc_views_types';
|
||||
import { AppState } from './services/context_state';
|
||||
import { SurrDocType } from './services/context';
|
||||
|
@ -169,7 +169,7 @@ export function ContextAppContent({
|
|||
rows={rows}
|
||||
dataView={dataView}
|
||||
expandedDoc={expandedDoc}
|
||||
isLoading={isAnchorLoading}
|
||||
loadingState={isAnchorLoading ? DataLoadingState.loading : DataLoadingState.loaded}
|
||||
sampleSize={0}
|
||||
sort={sort as SortOrder[]}
|
||||
isSortEnabled={false}
|
||||
|
|
|
@ -12,7 +12,6 @@ import { MarkdownSimple } from '@kbn/kibana-react-plugin/public';
|
|||
import type { DataView } from '@kbn/data-views-plugin/public';
|
||||
import { SortDirection } from '@kbn/data-plugin/public';
|
||||
import type { DataTableRecord } from '@kbn/discover-utils/types';
|
||||
import { CONTEXT_TIE_BREAKER_FIELDS_SETTING } from '@kbn/discover-utils';
|
||||
import { fetchAnchor } from '../services/anchor';
|
||||
import { fetchSurroundingDocs, SurrDocType } from '../services/context';
|
||||
import {
|
||||
|
@ -22,8 +21,11 @@ import {
|
|||
LoadingStatus,
|
||||
} from '../services/context_query_state';
|
||||
import { AppState } from '../services/context_state';
|
||||
import { getFirstSortableField } from '../utils/sorting';
|
||||
import { useDiscoverServices } from '../../../hooks/use_discover_services';
|
||||
import {
|
||||
getTieBreakerFieldName,
|
||||
getEsQuerySort,
|
||||
} from '../../../../common/utils/sorting/get_es_query_sort';
|
||||
|
||||
const createError = (statusKey: string, reason: FailureReason, error?: Error) => ({
|
||||
[statusKey]: { value: LoadingStatus.FAILED, error, reason },
|
||||
|
@ -48,8 +50,8 @@ export function useContextAppFetch({
|
|||
const searchSource = useMemo(() => {
|
||||
return data.search.searchSource.createEmpty();
|
||||
}, [data.search.searchSource]);
|
||||
const tieBreakerField = useMemo(
|
||||
() => getFirstSortableField(dataView, config.get(CONTEXT_TIE_BREAKER_FIELDS_SETTING)),
|
||||
const tieBreakerFieldName = useMemo(
|
||||
() => getTieBreakerFieldName(dataView, config),
|
||||
[config, dataView]
|
||||
);
|
||||
|
||||
|
@ -66,7 +68,7 @@ export function useContextAppFetch({
|
|||
defaultMessage: 'Unable to load the anchor document',
|
||||
});
|
||||
|
||||
if (!tieBreakerField) {
|
||||
if (!tieBreakerFieldName) {
|
||||
setState(createError('anchorStatus', FailureReason.INVALID_TIEBREAKER));
|
||||
toastNotifications.addDanger({
|
||||
title: errorTitle,
|
||||
|
@ -79,10 +81,12 @@ export function useContextAppFetch({
|
|||
|
||||
try {
|
||||
setState({ anchorStatus: { value: LoadingStatus.LOADING } });
|
||||
const sort = [
|
||||
{ [dataView.timeFieldName!]: SortDirection.desc },
|
||||
{ [tieBreakerField]: SortDirection.desc },
|
||||
];
|
||||
const sort = getEsQuerySort({
|
||||
sortDir: SortDirection.desc,
|
||||
timeFieldName: dataView.timeFieldName!,
|
||||
tieBreakerFieldName,
|
||||
isTimeNanosBased: dataView.isTimeNanosBased(),
|
||||
});
|
||||
const result = await fetchAnchor(
|
||||
anchorId,
|
||||
dataView,
|
||||
|
@ -109,7 +113,7 @@ export function useContextAppFetch({
|
|||
}
|
||||
}, [
|
||||
services,
|
||||
tieBreakerField,
|
||||
tieBreakerFieldName,
|
||||
setState,
|
||||
toastNotifications,
|
||||
dataView,
|
||||
|
@ -138,7 +142,7 @@ export function useContextAppFetch({
|
|||
type,
|
||||
dataView,
|
||||
anchor,
|
||||
tieBreakerField,
|
||||
tieBreakerFieldName,
|
||||
SortDirection.desc,
|
||||
count,
|
||||
filters,
|
||||
|
@ -168,7 +172,7 @@ export function useContextAppFetch({
|
|||
filterManager,
|
||||
appState,
|
||||
fetchedState.anchor,
|
||||
tieBreakerField,
|
||||
tieBreakerFieldName,
|
||||
setState,
|
||||
dataView,
|
||||
toastNotifications,
|
||||
|
|
|
@ -18,7 +18,7 @@ import { convertIsoToMillis, extractNanos } from '../utils/date_conversion';
|
|||
import { fetchHitsInInterval } from '../utils/fetch_hits_in_interval';
|
||||
import { generateIntervals } from '../utils/generate_intervals';
|
||||
import { getEsQuerySearchAfter } from '../utils/get_es_query_search_after';
|
||||
import { getEsQuerySort } from '../utils/get_es_query_sort';
|
||||
import { getEsQuerySort } from '../../../../common/utils/sorting/get_es_query_sort';
|
||||
import type { DiscoverServices } from '../../../build_services';
|
||||
|
||||
export enum SurrDocType {
|
||||
|
@ -88,16 +88,14 @@ export async function fetchSurroundingDocs(
|
|||
break;
|
||||
}
|
||||
|
||||
const searchAfter = getEsQuerySearchAfter(
|
||||
type,
|
||||
rows,
|
||||
timeField,
|
||||
anchor,
|
||||
nanos,
|
||||
useNewFieldsApi
|
||||
);
|
||||
const searchAfter = getEsQuerySearchAfter(type, rows, anchor);
|
||||
|
||||
const sort = getEsQuerySort(timeField, tieBreakerField, sortDirToApply, nanos);
|
||||
const sort = getEsQuerySort({
|
||||
timeFieldName: timeField,
|
||||
tieBreakerFieldName: tieBreakerField,
|
||||
sortDir: sortDirToApply,
|
||||
isTimeNanosBased: dataView.isTimeNanosBased(),
|
||||
});
|
||||
|
||||
const result = await fetchHitsInInterval(
|
||||
searchSource,
|
||||
|
|
|
@ -17,35 +17,18 @@ import { SurrDocType } from '../services/context';
|
|||
*/
|
||||
export function getEsQuerySearchAfter(
|
||||
type: SurrDocType,
|
||||
documents: DataTableRecord[],
|
||||
timeFieldName: string,
|
||||
anchor: DataTableRecord,
|
||||
nanoSeconds: string,
|
||||
useNewFieldsApi?: boolean
|
||||
rows: DataTableRecord[],
|
||||
anchor: DataTableRecord
|
||||
): EsQuerySearchAfter {
|
||||
if (documents.length) {
|
||||
if (rows.length) {
|
||||
// already surrounding docs -> first or last record is used
|
||||
const afterTimeRecIdx =
|
||||
type === SurrDocType.SUCCESSORS && documents.length ? documents.length - 1 : 0;
|
||||
const afterTimeDoc = documents[afterTimeRecIdx];
|
||||
const afterTimeDocRaw = afterTimeDoc.raw;
|
||||
let afterTimeValue = afterTimeDocRaw.sort?.[0] as string | number;
|
||||
if (nanoSeconds) {
|
||||
afterTimeValue = useNewFieldsApi
|
||||
? afterTimeDocRaw.fields?.[timeFieldName][0]
|
||||
: afterTimeDocRaw._source?.[timeFieldName];
|
||||
}
|
||||
return [afterTimeValue, afterTimeDoc.raw.sort?.[1] as string | number];
|
||||
const afterTimeRecIdx = type === SurrDocType.SUCCESSORS && rows.length ? rows.length - 1 : 0;
|
||||
const afterTimeDocRaw = rows[afterTimeRecIdx].raw;
|
||||
return [
|
||||
afterTimeDocRaw.sort?.[0] as string | number,
|
||||
afterTimeDocRaw.sort?.[1] as string | number,
|
||||
];
|
||||
}
|
||||
// if data_nanos adapt timestamp value for sorting, since numeric value was rounded by browser
|
||||
// ES search_after also works when number is provided as string
|
||||
const searchAfter = new Array(2) as EsQuerySearchAfter;
|
||||
searchAfter[0] = anchor.raw.sort?.[0] as string | number;
|
||||
if (nanoSeconds) {
|
||||
searchAfter[0] = useNewFieldsApi
|
||||
? anchor.raw.fields?.[timeFieldName][0]
|
||||
: anchor.raw._source?.[timeFieldName];
|
||||
}
|
||||
searchAfter[1] = anchor.raw.sort?.[1] as string | number;
|
||||
return searchAfter;
|
||||
return [anchor.raw.sort?.[0] as string | number, anchor.raw.sort?.[1] as string | number];
|
||||
}
|
||||
|
|
|
@ -1,34 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import type { EsQuerySortValue, SortDirection } from '@kbn/data-plugin/public';
|
||||
|
||||
/**
|
||||
* Returns `EsQuerySort` which is used to sort records in the ES query
|
||||
* https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-sort.html
|
||||
* @param timeField
|
||||
* @param tieBreakerField
|
||||
* @param sortDir
|
||||
* @param nanos
|
||||
*/
|
||||
export function getEsQuerySort(
|
||||
timeField: string,
|
||||
tieBreakerField: string,
|
||||
sortDir: SortDirection,
|
||||
nanos?: string
|
||||
): [EsQuerySortValue, EsQuerySortValue] {
|
||||
return [
|
||||
{
|
||||
[timeField]: {
|
||||
order: sortDir,
|
||||
format: nanos ? 'strict_date_optional_time_nanos' : 'strict_date_optional_time',
|
||||
},
|
||||
},
|
||||
{ [tieBreakerField]: sortDir },
|
||||
];
|
||||
}
|
|
@ -24,15 +24,15 @@ import { SearchResponseWarnings } from '@kbn/search-response-warnings';
|
|||
import {
|
||||
DOC_HIDE_TIME_COLUMN_SETTING,
|
||||
DOC_TABLE_LEGACY,
|
||||
HIDE_ANNOUNCEMENTS,
|
||||
SAMPLE_SIZE_SETTING,
|
||||
SEARCH_FIELDS_FROM_SOURCE,
|
||||
HIDE_ANNOUNCEMENTS,
|
||||
} from '@kbn/discover-utils';
|
||||
import { useInternalStateSelector } from '../../services/discover_internal_state_container';
|
||||
import { useAppStateSelector } from '../../services/discover_app_state_container';
|
||||
import { useDiscoverServices } from '../../../../hooks/use_discover_services';
|
||||
import { DocViewFilterFn } from '../../../../services/doc_views/doc_views_types';
|
||||
import { DiscoverGrid } from '../../../../components/discover_grid/discover_grid';
|
||||
import { DataLoadingState, DiscoverGrid } from '../../../../components/discover_grid/discover_grid';
|
||||
import { FetchStatus } from '../../../types';
|
||||
import { useColumns } from '../../../../hooks/use_data_grid_columns';
|
||||
import { RecordRawType } from '../../services/discover_data_state_container';
|
||||
|
@ -46,6 +46,7 @@ import { getRawRecordType } from '../../utils/get_raw_record_type';
|
|||
import { DiscoverGridFlyout } from '../../../../components/discover_grid/discover_grid_flyout';
|
||||
import { DocViewer } from '../../../../services/doc_views/components/doc_viewer';
|
||||
import { useSavedSearchInitial } from '../../services/discover_state_provider';
|
||||
import { useFetchMoreRecords } from './use_fetch_more_records';
|
||||
|
||||
const containerStyles = css`
|
||||
position: relative;
|
||||
|
@ -56,7 +57,7 @@ const progressStyle = css`
|
|||
`;
|
||||
|
||||
const DocTableInfiniteMemoized = React.memo(DocTableInfinite);
|
||||
const DataGridMemoized = React.memo(DiscoverGrid);
|
||||
const DiscoverGridMemoized = React.memo(DiscoverGrid);
|
||||
|
||||
// export needs for testing
|
||||
export const onResize = (
|
||||
|
@ -134,6 +135,11 @@ function DiscoverDocumentsComponent({
|
|||
isTextBasedQuery || !documentState.result || documentState.result.length === 0;
|
||||
const rows = useMemo(() => documentState.result || [], [documentState.result]);
|
||||
|
||||
const { isMoreDataLoading, totalHits, onFetchMoreRecords } = useFetchMoreRecords({
|
||||
isTextBasedQuery,
|
||||
stateContainer,
|
||||
});
|
||||
|
||||
const {
|
||||
columns: currentColumns,
|
||||
onAddColumn,
|
||||
|
@ -245,12 +251,18 @@ function DiscoverDocumentsComponent({
|
|||
<CellActionsProvider
|
||||
getTriggerCompatibleActions={uiActions.getTriggerCompatibleActions}
|
||||
>
|
||||
<DataGridMemoized
|
||||
<DiscoverGridMemoized
|
||||
ariaLabelledBy="documentsAriaLabel"
|
||||
columns={currentColumns}
|
||||
expandedDoc={expandedDoc}
|
||||
dataView={dataView}
|
||||
isLoading={isDataLoading}
|
||||
loadingState={
|
||||
isDataLoading
|
||||
? DataLoadingState.loading
|
||||
: isMoreDataLoading
|
||||
? DataLoadingState.loadingMore
|
||||
: DataLoadingState.loaded
|
||||
}
|
||||
rows={rows}
|
||||
sort={(sort as SortOrder[]) || []}
|
||||
sampleSize={sampleSize}
|
||||
|
@ -277,6 +289,8 @@ function DiscoverDocumentsComponent({
|
|||
savedSearchId={savedSearch.id}
|
||||
DocumentView={DiscoverGridFlyout}
|
||||
services={services}
|
||||
totalHits={totalHits}
|
||||
onFetchMoreRecords={onFetchMoreRecords}
|
||||
/>
|
||||
</CellActionsProvider>
|
||||
</div>
|
||||
|
|
|
@ -362,7 +362,10 @@ describe('useDiscoverHistogram', () => {
|
|||
describe('refetching', () => {
|
||||
it('should call refetch when savedSearchFetch$ is triggered', async () => {
|
||||
const savedSearchFetch$ = new Subject<{
|
||||
reset: boolean;
|
||||
options: {
|
||||
reset: boolean;
|
||||
fetchMore: boolean;
|
||||
};
|
||||
searchSessionId: string;
|
||||
}>();
|
||||
const stateContainer = getStateContainer();
|
||||
|
@ -374,14 +377,20 @@ describe('useDiscoverHistogram', () => {
|
|||
});
|
||||
expect(api.refetch).not.toHaveBeenCalled();
|
||||
act(() => {
|
||||
savedSearchFetch$.next({ reset: false, searchSessionId: '1234' });
|
||||
savedSearchFetch$.next({
|
||||
options: { reset: false, fetchMore: false },
|
||||
searchSessionId: '1234',
|
||||
});
|
||||
});
|
||||
expect(api.refetch).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should skip the next refetch when hideChart changes from true to false', async () => {
|
||||
const savedSearchFetch$ = new Subject<{
|
||||
reset: boolean;
|
||||
options: {
|
||||
reset: boolean;
|
||||
fetchMore: boolean;
|
||||
};
|
||||
searchSessionId: string;
|
||||
}>();
|
||||
const stateContainer = getStateContainer();
|
||||
|
@ -398,9 +407,44 @@ describe('useDiscoverHistogram', () => {
|
|||
hook.rerender({ ...initialProps, hideChart: false });
|
||||
});
|
||||
act(() => {
|
||||
savedSearchFetch$.next({ reset: false, searchSessionId: '1234' });
|
||||
savedSearchFetch$.next({
|
||||
options: { reset: false, fetchMore: false },
|
||||
searchSessionId: '1234',
|
||||
});
|
||||
});
|
||||
expect(api.refetch).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should skip the next refetch when fetching more', async () => {
|
||||
const savedSearchFetch$ = new Subject<{
|
||||
options: {
|
||||
reset: boolean;
|
||||
fetchMore: boolean;
|
||||
};
|
||||
searchSessionId: string;
|
||||
}>();
|
||||
const stateContainer = getStateContainer();
|
||||
stateContainer.dataState.fetch$ = savedSearchFetch$;
|
||||
const { hook } = await renderUseDiscoverHistogram({ stateContainer });
|
||||
const api = createMockUnifiedHistogramApi();
|
||||
act(() => {
|
||||
hook.result.current.ref(api);
|
||||
});
|
||||
act(() => {
|
||||
savedSearchFetch$.next({
|
||||
options: { reset: false, fetchMore: true },
|
||||
searchSessionId: '1234',
|
||||
});
|
||||
});
|
||||
expect(api.refetch).not.toHaveBeenCalled();
|
||||
|
||||
act(() => {
|
||||
savedSearchFetch$.next({
|
||||
options: { reset: false, fetchMore: false },
|
||||
searchSessionId: '1234',
|
||||
});
|
||||
});
|
||||
expect(api.refetch).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -279,7 +279,10 @@ export const useDiscoverHistogram = ({
|
|||
textBasedFetchComplete$.pipe(map(() => 'discover'))
|
||||
).pipe(debounceTime(50));
|
||||
} else {
|
||||
fetch$ = stateContainer.dataState.fetch$.pipe(map(() => 'discover'));
|
||||
fetch$ = stateContainer.dataState.fetch$.pipe(
|
||||
filter(({ options }) => !options.fetchMore), // don't update histogram for "Load more" in the grid
|
||||
map(() => 'discover')
|
||||
);
|
||||
}
|
||||
|
||||
const subscription = fetch$.subscribe((source) => {
|
||||
|
|
|
@ -0,0 +1,130 @@
|
|||
/*
|
||||
* 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 { BehaviorSubject } from 'rxjs';
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
import { buildDataTableRecord } from '@kbn/discover-utils';
|
||||
import { dataViewMock, esHitsMockWithSort } from '@kbn/discover-utils/src/__mocks__';
|
||||
import { useFetchMoreRecords } from './use_fetch_more_records';
|
||||
import { getDiscoverStateMock } from '../../../../__mocks__/discover_state.mock';
|
||||
import { DataDocuments$, DataTotalHits$ } from '../../services/discover_data_state_container';
|
||||
import { FetchStatus } from '../../../types';
|
||||
|
||||
describe('useFetchMoreRecords', () => {
|
||||
const records = esHitsMockWithSort.map((hit) => buildDataTableRecord(hit, dataViewMock));
|
||||
|
||||
const getStateContainer = ({
|
||||
fetchStatus,
|
||||
loadedRecordsCount,
|
||||
totalRecordsCount,
|
||||
}: {
|
||||
fetchStatus: FetchStatus;
|
||||
loadedRecordsCount: number;
|
||||
totalRecordsCount: number;
|
||||
}) => {
|
||||
const stateContainer = getDiscoverStateMock({ isTimeBased: true });
|
||||
stateContainer.dataState.data$.documents$ = new BehaviorSubject({
|
||||
fetchStatus,
|
||||
result: records.slice(0, loadedRecordsCount),
|
||||
}) as DataDocuments$;
|
||||
stateContainer.dataState.data$.totalHits$ = new BehaviorSubject({
|
||||
fetchStatus,
|
||||
result: totalRecordsCount,
|
||||
}) as DataTotalHits$;
|
||||
|
||||
return stateContainer;
|
||||
};
|
||||
|
||||
it('should not be allowed if all records are already loaded', async () => {
|
||||
const {
|
||||
result: { current },
|
||||
} = renderHook(() =>
|
||||
useFetchMoreRecords({
|
||||
isTextBasedQuery: false,
|
||||
stateContainer: getStateContainer({
|
||||
fetchStatus: FetchStatus.COMPLETE,
|
||||
loadedRecordsCount: 3,
|
||||
totalRecordsCount: 3,
|
||||
}),
|
||||
})
|
||||
);
|
||||
expect(current.onFetchMoreRecords).toBeUndefined();
|
||||
expect(current.isMoreDataLoading).toBe(false);
|
||||
expect(current.totalHits).toBe(3);
|
||||
});
|
||||
|
||||
it('should be allowed when there are more records to load', async () => {
|
||||
const {
|
||||
result: { current },
|
||||
} = renderHook(() =>
|
||||
useFetchMoreRecords({
|
||||
isTextBasedQuery: false,
|
||||
stateContainer: getStateContainer({
|
||||
fetchStatus: FetchStatus.COMPLETE,
|
||||
loadedRecordsCount: 3,
|
||||
totalRecordsCount: 5,
|
||||
}),
|
||||
})
|
||||
);
|
||||
expect(current.onFetchMoreRecords).toBeDefined();
|
||||
expect(current.isMoreDataLoading).toBe(false);
|
||||
expect(current.totalHits).toBe(5);
|
||||
});
|
||||
|
||||
it('should not be allowed when there is no initial documents', async () => {
|
||||
const {
|
||||
result: { current },
|
||||
} = renderHook(() =>
|
||||
useFetchMoreRecords({
|
||||
isTextBasedQuery: false,
|
||||
stateContainer: getStateContainer({
|
||||
fetchStatus: FetchStatus.COMPLETE,
|
||||
loadedRecordsCount: 0,
|
||||
totalRecordsCount: 5,
|
||||
}),
|
||||
})
|
||||
);
|
||||
expect(current.onFetchMoreRecords).toBeUndefined();
|
||||
expect(current.isMoreDataLoading).toBe(false);
|
||||
expect(current.totalHits).toBe(5);
|
||||
});
|
||||
|
||||
it('should return loading status correctly', async () => {
|
||||
const {
|
||||
result: { current },
|
||||
} = renderHook(() =>
|
||||
useFetchMoreRecords({
|
||||
isTextBasedQuery: false,
|
||||
stateContainer: getStateContainer({
|
||||
fetchStatus: FetchStatus.LOADING_MORE,
|
||||
loadedRecordsCount: 3,
|
||||
totalRecordsCount: 5,
|
||||
}),
|
||||
})
|
||||
);
|
||||
expect(current.onFetchMoreRecords).toBeDefined();
|
||||
expect(current.isMoreDataLoading).toBe(true);
|
||||
expect(current.totalHits).toBe(5);
|
||||
});
|
||||
|
||||
it('should not be allowed for text-based queries', async () => {
|
||||
const {
|
||||
result: { current },
|
||||
} = renderHook(() =>
|
||||
useFetchMoreRecords({
|
||||
isTextBasedQuery: true,
|
||||
stateContainer: getStateContainer({
|
||||
fetchStatus: FetchStatus.COMPLETE,
|
||||
loadedRecordsCount: 3,
|
||||
totalRecordsCount: 5,
|
||||
}),
|
||||
})
|
||||
);
|
||||
expect(current.onFetchMoreRecords).toBeUndefined();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* 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 { useMemo } from 'react';
|
||||
import { FetchStatus } from '../../../types';
|
||||
import { useDataState } from '../../hooks/use_data_state';
|
||||
import type { DiscoverStateContainer } from '../../services/discover_state';
|
||||
|
||||
/**
|
||||
* Params for the hook
|
||||
*/
|
||||
export interface UseFetchMoreRecordsParams {
|
||||
isTextBasedQuery: boolean;
|
||||
stateContainer: DiscoverStateContainer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return type for the hook
|
||||
*/
|
||||
export interface UseFetchMoreRecordsResult {
|
||||
isMoreDataLoading: boolean;
|
||||
totalHits: number;
|
||||
onFetchMoreRecords: (() => void) | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if more records can be loaded and returns a handler for it
|
||||
* @param isTextBasedQuery
|
||||
* @param stateContainer
|
||||
*/
|
||||
export const useFetchMoreRecords = ({
|
||||
isTextBasedQuery,
|
||||
stateContainer,
|
||||
}: UseFetchMoreRecordsParams): UseFetchMoreRecordsResult => {
|
||||
const documents$ = stateContainer.dataState.data$.documents$;
|
||||
const totalHits$ = stateContainer.dataState.data$.totalHits$;
|
||||
const documentState = useDataState(documents$);
|
||||
const totalHitsState = useDataState(totalHits$);
|
||||
|
||||
const rows = documentState.result || [];
|
||||
const isMoreDataLoading = documentState.fetchStatus === FetchStatus.LOADING_MORE;
|
||||
|
||||
const totalHits = totalHitsState.result || 0;
|
||||
const canFetchMoreRecords =
|
||||
!isTextBasedQuery &&
|
||||
rows.length > 0 &&
|
||||
totalHits > rows.length &&
|
||||
Boolean(rows[rows.length - 1].raw.sort?.length);
|
||||
|
||||
const onFetchMoreRecords = useMemo(
|
||||
() =>
|
||||
canFetchMoreRecords
|
||||
? () => {
|
||||
stateContainer.dataState.fetchMore();
|
||||
}
|
||||
: undefined,
|
||||
[canFetchMoreRecords, stateContainer.dataState]
|
||||
);
|
||||
|
||||
return {
|
||||
isMoreDataLoading,
|
||||
totalHits,
|
||||
onFetchMoreRecords,
|
||||
};
|
||||
};
|
|
@ -11,13 +11,22 @@ import {
|
|||
sendErrorMsg,
|
||||
sendErrorTo,
|
||||
sendLoadingMsg,
|
||||
sendLoadingMoreMsg,
|
||||
sendLoadingMoreFinishedMsg,
|
||||
sendNoResultsFoundMsg,
|
||||
sendPartialMsg,
|
||||
} from './use_saved_search_messages';
|
||||
import { FetchStatus } from '../../types';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { DataMainMsg, RecordRawType } from '../services/discover_data_state_container';
|
||||
import {
|
||||
DataDocumentsMsg,
|
||||
DataMainMsg,
|
||||
RecordRawType,
|
||||
} from '../services/discover_data_state_container';
|
||||
import { filter } from 'rxjs/operators';
|
||||
import { dataViewMock, esHitsMockWithSort } from '@kbn/discover-utils/src/__mocks__';
|
||||
import { buildDataTableRecord } from '@kbn/discover-utils';
|
||||
import { searchResponseWarningsMock } from '@kbn/search-response-warnings/src/__mocks__/search_response_warnings';
|
||||
|
||||
describe('test useSavedSearch message generators', () => {
|
||||
test('sendCompleteMsg', (done) => {
|
||||
|
@ -69,6 +78,66 @@ describe('test useSavedSearch message generators', () => {
|
|||
recordRawType: RecordRawType.DOCUMENT,
|
||||
});
|
||||
});
|
||||
test('sendLoadingMoreMsg', (done) => {
|
||||
const documents$ = new BehaviorSubject<DataDocumentsMsg>({
|
||||
fetchStatus: FetchStatus.COMPLETE,
|
||||
});
|
||||
documents$.subscribe((value) => {
|
||||
if (value.fetchStatus !== FetchStatus.COMPLETE) {
|
||||
expect(value.fetchStatus).toBe(FetchStatus.LOADING_MORE);
|
||||
done();
|
||||
}
|
||||
});
|
||||
sendLoadingMoreMsg(documents$);
|
||||
});
|
||||
test('sendLoadingMoreFinishedMsg', (done) => {
|
||||
const records = esHitsMockWithSort.map((hit) => buildDataTableRecord(hit, dataViewMock));
|
||||
const initialRecords = [records[0], records[1]];
|
||||
const moreRecords = [records[2], records[3]];
|
||||
|
||||
const documents$ = new BehaviorSubject<DataDocumentsMsg>({
|
||||
fetchStatus: FetchStatus.LOADING_MORE,
|
||||
result: initialRecords,
|
||||
});
|
||||
documents$.subscribe((value) => {
|
||||
if (value.fetchStatus !== FetchStatus.LOADING_MORE) {
|
||||
expect(value.fetchStatus).toBe(FetchStatus.COMPLETE);
|
||||
expect(value.result).toStrictEqual([...initialRecords, ...moreRecords]);
|
||||
expect(value.interceptedWarnings).toHaveLength(searchResponseWarningsMock.length);
|
||||
done();
|
||||
}
|
||||
});
|
||||
sendLoadingMoreFinishedMsg(documents$, {
|
||||
moreRecords,
|
||||
interceptedWarnings: searchResponseWarningsMock.map((warning) => ({
|
||||
originalWarning: warning,
|
||||
})),
|
||||
});
|
||||
});
|
||||
test('sendLoadingMoreFinishedMsg after an exception', (done) => {
|
||||
const records = esHitsMockWithSort.map((hit) => buildDataTableRecord(hit, dataViewMock));
|
||||
const initialRecords = [records[0], records[1]];
|
||||
|
||||
const documents$ = new BehaviorSubject<DataDocumentsMsg>({
|
||||
fetchStatus: FetchStatus.LOADING_MORE,
|
||||
result: initialRecords,
|
||||
interceptedWarnings: searchResponseWarningsMock.map((warning) => ({
|
||||
originalWarning: warning,
|
||||
})),
|
||||
});
|
||||
documents$.subscribe((value) => {
|
||||
if (value.fetchStatus !== FetchStatus.LOADING_MORE) {
|
||||
expect(value.fetchStatus).toBe(FetchStatus.COMPLETE);
|
||||
expect(value.result).toBe(initialRecords);
|
||||
expect(value.interceptedWarnings).toBeUndefined();
|
||||
done();
|
||||
}
|
||||
});
|
||||
sendLoadingMoreFinishedMsg(documents$, {
|
||||
moreRecords: [],
|
||||
interceptedWarnings: undefined,
|
||||
});
|
||||
});
|
||||
test('sendErrorMsg', (done) => {
|
||||
const main$ = new BehaviorSubject<DataMainMsg>({ fetchStatus: FetchStatus.PARTIAL });
|
||||
main$.subscribe((value) => {
|
||||
|
|
|
@ -7,6 +7,8 @@
|
|||
*/
|
||||
|
||||
import type { BehaviorSubject } from 'rxjs';
|
||||
import type { DataTableRecord } from '@kbn/discover-utils/src/types';
|
||||
import type { SearchResponseInterceptedWarning } from '@kbn/search-response-warnings';
|
||||
import { FetchStatus } from '../../types';
|
||||
import type {
|
||||
DataDocuments$,
|
||||
|
@ -16,6 +18,7 @@ import type {
|
|||
SavedSearchData,
|
||||
} from '../services/discover_data_state_container';
|
||||
import { RecordRawType } from '../services/discover_data_state_container';
|
||||
|
||||
/**
|
||||
* Sends COMPLETE message to the main$ observable with the information
|
||||
* that no documents have been found, allowing Discover to show a no
|
||||
|
@ -71,6 +74,44 @@ export function sendLoadingMsg<T extends DataMsg>(
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send LOADING_MORE message via main observable
|
||||
*/
|
||||
export function sendLoadingMoreMsg(documents$: DataDocuments$) {
|
||||
if (documents$.getValue().fetchStatus !== FetchStatus.LOADING_MORE) {
|
||||
documents$.next({
|
||||
...documents$.getValue(),
|
||||
fetchStatus: FetchStatus.LOADING_MORE,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finishing LOADING_MORE message
|
||||
*/
|
||||
export function sendLoadingMoreFinishedMsg(
|
||||
documents$: DataDocuments$,
|
||||
{
|
||||
moreRecords,
|
||||
interceptedWarnings,
|
||||
}: {
|
||||
moreRecords: DataTableRecord[];
|
||||
interceptedWarnings: SearchResponseInterceptedWarning[] | undefined;
|
||||
}
|
||||
) {
|
||||
const currentValue = documents$.getValue();
|
||||
if (currentValue.fetchStatus === FetchStatus.LOADING_MORE) {
|
||||
documents$.next({
|
||||
...currentValue,
|
||||
fetchStatus: FetchStatus.COMPLETE,
|
||||
result: moreRecords?.length
|
||||
? [...(currentValue.result || []), ...moreRecords]
|
||||
: currentValue.result,
|
||||
interceptedWarnings,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send ERROR message
|
||||
*/
|
||||
|
|
|
@ -5,15 +5,28 @@
|
|||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
import { Subject } from 'rxjs';
|
||||
import { BehaviorSubject, Subject } from 'rxjs';
|
||||
import { waitFor } from '@testing-library/react';
|
||||
import { buildDataTableRecord } from '@kbn/discover-utils';
|
||||
import { dataViewMock, esHitsMockWithSort } from '@kbn/discover-utils/src/__mocks__';
|
||||
import { discoverServiceMock } from '../../../__mocks__/services';
|
||||
import { savedSearchMockWithSQL } from '../../../__mocks__/saved_search';
|
||||
import { FetchStatus } from '../../types';
|
||||
import { setUrlTracker } from '../../../kibana_services';
|
||||
import { urlTrackerMock } from '../../../__mocks__/url_tracker.mock';
|
||||
import { RecordRawType } from './discover_data_state_container';
|
||||
import { DataDocuments$, RecordRawType } from './discover_data_state_container';
|
||||
import { getDiscoverStateMock } from '../../../__mocks__/discover_state.mock';
|
||||
import { fetchDocuments } from '../utils/fetch_documents';
|
||||
|
||||
jest.mock('../utils/fetch_documents', () => ({
|
||||
fetchDocuments: jest.fn().mockResolvedValue({ records: [] }),
|
||||
}));
|
||||
|
||||
jest.mock('@kbn/ebt-tools', () => ({
|
||||
reportPerformanceMetricEvent: jest.fn(),
|
||||
}));
|
||||
|
||||
const mockFetchDocuments = fetchDocuments as unknown as jest.MockedFunction<typeof fetchDocuments>;
|
||||
|
||||
setUrlTracker(urlTrackerMock);
|
||||
describe('test getDataStateContainer', () => {
|
||||
|
@ -28,6 +41,11 @@ describe('test getDataStateContainer', () => {
|
|||
});
|
||||
test('refetch$ triggers a search', async () => {
|
||||
const stateContainer = getDiscoverStateMock({ isTimeBased: true });
|
||||
jest.spyOn(stateContainer.searchSessionManager, 'getNextSearchSessionId');
|
||||
jest.spyOn(stateContainer.searchSessionManager, 'getCurrentSearchSessionId');
|
||||
expect(
|
||||
stateContainer.searchSessionManager.getNextSearchSessionId as jest.Mock
|
||||
).not.toHaveBeenCalled();
|
||||
|
||||
discoverServiceMock.data.query.timefilter.timefilter.getTime = jest.fn(() => {
|
||||
return { from: '2021-05-01T20:00:00Z', to: '2021-05-02T20:00:00Z' };
|
||||
|
@ -46,6 +64,15 @@ describe('test getDataStateContainer', () => {
|
|||
|
||||
expect(dataState.data$.totalHits$.value.result).toBe(0);
|
||||
expect(dataState.data$.documents$.value.result).toEqual([]);
|
||||
|
||||
// gets a new search session id
|
||||
expect(
|
||||
stateContainer.searchSessionManager.getNextSearchSessionId as jest.Mock
|
||||
).toHaveBeenCalled();
|
||||
expect(
|
||||
stateContainer.searchSessionManager.getCurrentSearchSessionId as jest.Mock
|
||||
).not.toHaveBeenCalled();
|
||||
|
||||
unsubscribe();
|
||||
});
|
||||
|
||||
|
@ -84,4 +111,51 @@ describe('test getDataStateContainer', () => {
|
|||
|
||||
expect(stateContainer.dataState.data$.main$.getValue().recordRawType).toBe(RecordRawType.PLAIN);
|
||||
});
|
||||
|
||||
test('refetch$ accepts "fetch_more" signal', (done) => {
|
||||
const records = esHitsMockWithSort.map((hit) => buildDataTableRecord(hit, dataViewMock));
|
||||
const initialRecords = [records[0], records[1]];
|
||||
const moreRecords = [records[2], records[3]];
|
||||
|
||||
mockFetchDocuments.mockResolvedValue({ records: moreRecords });
|
||||
|
||||
const stateContainer = getDiscoverStateMock({ isTimeBased: true });
|
||||
stateContainer.dataState.data$.documents$ = new BehaviorSubject({
|
||||
fetchStatus: FetchStatus.COMPLETE,
|
||||
result: initialRecords,
|
||||
}) as DataDocuments$;
|
||||
|
||||
jest.spyOn(stateContainer.searchSessionManager, 'getCurrentSearchSessionId');
|
||||
expect(
|
||||
stateContainer.searchSessionManager.getCurrentSearchSessionId as jest.Mock
|
||||
).not.toHaveBeenCalled();
|
||||
|
||||
const dataState = stateContainer.dataState;
|
||||
|
||||
const unsubscribe = dataState.subscribe();
|
||||
|
||||
expect(dataState.data$.documents$.value.result).toEqual(initialRecords);
|
||||
|
||||
let hasLoadingMoreStarted = false;
|
||||
|
||||
stateContainer.dataState.data$.documents$.subscribe((value) => {
|
||||
if (value.fetchStatus === FetchStatus.LOADING_MORE) {
|
||||
hasLoadingMoreStarted = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (hasLoadingMoreStarted && value.fetchStatus === FetchStatus.COMPLETE) {
|
||||
expect(value.result).toEqual([...initialRecords, ...moreRecords]);
|
||||
// it uses the same current search session id
|
||||
expect(
|
||||
stateContainer.searchSessionManager.getCurrentSearchSessionId as jest.Mock
|
||||
).toHaveBeenCalled();
|
||||
|
||||
unsubscribe();
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
dataState.refetch$.next('fetch_more');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -25,7 +25,7 @@ import { DiscoverServices } from '../../../build_services';
|
|||
import { DiscoverSearchSessionManager } from './discover_search_session';
|
||||
import { FetchStatus } from '../../types';
|
||||
import { validateTimeRange } from '../utils/validate_time_range';
|
||||
import { fetchAll } from '../utils/fetch_all';
|
||||
import { fetchAll, fetchMoreDocuments } from '../utils/fetch_all';
|
||||
import { sendResetMsg } from '../hooks/use_saved_search_messages';
|
||||
import { getFetch$ } from '../utils/get_fetch_observable';
|
||||
import { InternalState } from './discover_internal_state_container';
|
||||
|
@ -42,7 +42,10 @@ export type DataDocuments$ = BehaviorSubject<DataDocumentsMsg>;
|
|||
export type DataTotalHits$ = BehaviorSubject<DataTotalHitsMsg>;
|
||||
export type AvailableFields$ = BehaviorSubject<DataAvailableFieldsMsg>;
|
||||
export type DataFetch$ = Observable<{
|
||||
reset: boolean;
|
||||
options: {
|
||||
reset: boolean;
|
||||
fetchMore: boolean;
|
||||
};
|
||||
searchSessionId: string;
|
||||
}>;
|
||||
|
||||
|
@ -59,7 +62,7 @@ export enum RecordRawType {
|
|||
PLAIN = 'plain',
|
||||
}
|
||||
|
||||
export type DataRefetchMsg = 'reset' | undefined;
|
||||
export type DataRefetchMsg = 'reset' | 'fetch_more' | undefined;
|
||||
|
||||
export interface DataMsg {
|
||||
fetchStatus: FetchStatus;
|
||||
|
@ -95,6 +98,10 @@ export interface DiscoverDataStateContainer {
|
|||
* Implicitly starting fetching data from ES
|
||||
*/
|
||||
fetch: () => void;
|
||||
/**
|
||||
* Fetch more data from ES
|
||||
*/
|
||||
fetchMore: () => void;
|
||||
/**
|
||||
* Observable emitting when a next fetch is triggered
|
||||
*/
|
||||
|
@ -197,22 +204,22 @@ export function getDataStateContainer({
|
|||
filter(() => validateTimeRange(timefilter.getTime(), toastNotifications)),
|
||||
tap(() => inspectorAdapters.requests.reset()),
|
||||
map((val) => ({
|
||||
reset: val === 'reset',
|
||||
searchSessionId: searchSessionManager.getNextSearchSessionId(),
|
||||
options: {
|
||||
reset: val === 'reset',
|
||||
fetchMore: val === 'fetch_more',
|
||||
},
|
||||
searchSessionId:
|
||||
(val === 'fetch_more' && searchSessionManager.getCurrentSearchSessionId()) ||
|
||||
searchSessionManager.getNextSearchSessionId(),
|
||||
})),
|
||||
share()
|
||||
);
|
||||
let abortController: AbortController;
|
||||
let abortControllerFetchMore: AbortController;
|
||||
|
||||
function subscribe() {
|
||||
const subscription = fetch$.subscribe(async ({ reset, searchSessionId }) => {
|
||||
abortController?.abort();
|
||||
abortController = new AbortController();
|
||||
const prevAutoRefreshDone = autoRefreshDone;
|
||||
|
||||
const fetchAllStartTime = window.performance.now();
|
||||
await fetchAll(dataSubjects, reset, {
|
||||
abortController,
|
||||
const subscription = fetch$.subscribe(async ({ options, searchSessionId }) => {
|
||||
const commonFetchDeps = {
|
||||
initialFetchStatus: getInitialFetchStatus(),
|
||||
inspectorAdapters,
|
||||
searchSessionId,
|
||||
|
@ -221,6 +228,34 @@ export function getDataStateContainer({
|
|||
getInternalState,
|
||||
savedSearch: getSavedSearch(),
|
||||
useNewFieldsApi: !uiSettings.get(SEARCH_FIELDS_FROM_SOURCE),
|
||||
};
|
||||
|
||||
abortController?.abort();
|
||||
abortControllerFetchMore?.abort();
|
||||
|
||||
if (options.fetchMore) {
|
||||
abortControllerFetchMore = new AbortController();
|
||||
|
||||
const fetchMoreStartTime = window.performance.now();
|
||||
await fetchMoreDocuments(dataSubjects, {
|
||||
abortController: abortControllerFetchMore,
|
||||
...commonFetchDeps,
|
||||
});
|
||||
const fetchMoreDuration = window.performance.now() - fetchMoreStartTime;
|
||||
reportPerformanceMetricEvent(services.analytics, {
|
||||
eventName: 'discoverFetchMore',
|
||||
duration: fetchMoreDuration,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
abortController = new AbortController();
|
||||
const prevAutoRefreshDone = autoRefreshDone;
|
||||
|
||||
const fetchAllStartTime = window.performance.now();
|
||||
await fetchAll(dataSubjects, options.reset, {
|
||||
abortController,
|
||||
...commonFetchDeps,
|
||||
});
|
||||
const fetchAllDuration = window.performance.now() - fetchAllStartTime;
|
||||
reportPerformanceMetricEvent(services.analytics, {
|
||||
|
@ -240,6 +275,7 @@ export function getDataStateContainer({
|
|||
|
||||
return () => {
|
||||
abortController?.abort();
|
||||
abortControllerFetchMore?.abort();
|
||||
subscription.unsubscribe();
|
||||
};
|
||||
}
|
||||
|
@ -263,6 +299,11 @@ export function getDataStateContainer({
|
|||
return refetch$;
|
||||
};
|
||||
|
||||
const fetchMore = () => {
|
||||
refetch$.next('fetch_more');
|
||||
return refetch$;
|
||||
};
|
||||
|
||||
const reset = (savedSearch: SavedSearch) => {
|
||||
const recordType = getRawRecordType(savedSearch.searchSource.getField('query'));
|
||||
sendResetMsg(dataSubjects, getInitialFetchStatus(), recordType);
|
||||
|
@ -270,6 +311,7 @@ export function getDataStateContainer({
|
|||
|
||||
return {
|
||||
fetch: fetchQuery,
|
||||
fetchMore,
|
||||
fetch$,
|
||||
data$: dataSubjects,
|
||||
refetch$,
|
||||
|
|
|
@ -70,6 +70,13 @@ export class DiscoverSearchSessionManager {
|
|||
return searchSessionIdFromURL ?? this.deps.session.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current search session id
|
||||
*/
|
||||
getCurrentSearchSessionId() {
|
||||
return this.deps.session.getSessionId();
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes Discovers {@link SEARCH_SESSION_ID_QUERY_PARAM} from the URL
|
||||
* @param replace - methods to change the URL
|
||||
|
|
|
@ -12,7 +12,7 @@ import { SearchSource } from '@kbn/data-plugin/public';
|
|||
import { RequestAdapter } from '@kbn/inspector-plugin/common';
|
||||
import { savedSearchMock } from '../../../__mocks__/saved_search';
|
||||
import { discoverServiceMock } from '../../../__mocks__/services';
|
||||
import { fetchAll } from './fetch_all';
|
||||
import { fetchAll, fetchMoreDocuments } from './fetch_all';
|
||||
import {
|
||||
DataAvailableFieldsMsg,
|
||||
DataDocumentsMsg,
|
||||
|
@ -24,7 +24,9 @@ import {
|
|||
import { fetchDocuments } from './fetch_documents';
|
||||
import { fetchSql } from './fetch_sql';
|
||||
import { buildDataTableRecord } from '@kbn/discover-utils';
|
||||
import { dataViewMock } from '@kbn/discover-utils/src/__mocks__';
|
||||
import { dataViewMock, esHitsMockWithSort } from '@kbn/discover-utils/src/__mocks__';
|
||||
import { searchResponseWarningsMock } from '@kbn/search-response-warnings/src/__mocks__/search_response_warnings';
|
||||
|
||||
jest.mock('./fetch_documents', () => ({
|
||||
fetchDocuments: jest.fn().mockResolvedValue([]),
|
||||
}));
|
||||
|
@ -288,4 +290,95 @@ describe('test fetchAll', () => {
|
|||
},
|
||||
]);
|
||||
});
|
||||
|
||||
describe('fetchMoreDocuments', () => {
|
||||
const records = esHitsMockWithSort.map((hit) => buildDataTableRecord(hit, dataViewMock));
|
||||
const initialRecords = [records[0], records[1]];
|
||||
const moreRecords = [records[2], records[3]];
|
||||
|
||||
const interceptedWarnings = searchResponseWarningsMock.map((warning) => ({
|
||||
originalWarning: warning,
|
||||
}));
|
||||
|
||||
test('should add more records', async () => {
|
||||
const collectDocuments = subjectCollector(subjects.documents$);
|
||||
const collectMain = subjectCollector(subjects.main$);
|
||||
mockFetchDocuments.mockResolvedValue({ records: moreRecords, interceptedWarnings });
|
||||
subjects.documents$.next({
|
||||
fetchStatus: FetchStatus.COMPLETE,
|
||||
recordRawType: RecordRawType.DOCUMENT,
|
||||
result: initialRecords,
|
||||
});
|
||||
fetchMoreDocuments(subjects, deps);
|
||||
await waitForNextTick();
|
||||
|
||||
expect(await collectDocuments()).toEqual([
|
||||
{ fetchStatus: FetchStatus.UNINITIALIZED },
|
||||
{
|
||||
fetchStatus: FetchStatus.COMPLETE,
|
||||
recordRawType: RecordRawType.DOCUMENT,
|
||||
result: initialRecords,
|
||||
},
|
||||
{
|
||||
fetchStatus: FetchStatus.LOADING_MORE,
|
||||
recordRawType: RecordRawType.DOCUMENT,
|
||||
result: initialRecords,
|
||||
},
|
||||
{
|
||||
fetchStatus: FetchStatus.COMPLETE,
|
||||
recordRawType: RecordRawType.DOCUMENT,
|
||||
result: [...initialRecords, ...moreRecords],
|
||||
interceptedWarnings,
|
||||
},
|
||||
]);
|
||||
expect(await collectMain()).toEqual([
|
||||
{
|
||||
fetchStatus: FetchStatus.UNINITIALIZED,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('should handle exceptions', async () => {
|
||||
const collectDocuments = subjectCollector(subjects.documents$);
|
||||
const collectMain = subjectCollector(subjects.main$);
|
||||
mockFetchDocuments.mockRejectedValue({ msg: 'This query failed' });
|
||||
subjects.documents$.next({
|
||||
fetchStatus: FetchStatus.COMPLETE,
|
||||
recordRawType: RecordRawType.DOCUMENT,
|
||||
result: initialRecords,
|
||||
});
|
||||
fetchMoreDocuments(subjects, deps);
|
||||
await waitForNextTick();
|
||||
|
||||
expect(await collectDocuments()).toEqual([
|
||||
{ fetchStatus: FetchStatus.UNINITIALIZED },
|
||||
{
|
||||
fetchStatus: FetchStatus.COMPLETE,
|
||||
recordRawType: RecordRawType.DOCUMENT,
|
||||
result: initialRecords,
|
||||
},
|
||||
{
|
||||
fetchStatus: FetchStatus.LOADING_MORE,
|
||||
recordRawType: RecordRawType.DOCUMENT,
|
||||
result: initialRecords,
|
||||
},
|
||||
{
|
||||
fetchStatus: FetchStatus.COMPLETE,
|
||||
recordRawType: RecordRawType.DOCUMENT,
|
||||
result: initialRecords,
|
||||
},
|
||||
]);
|
||||
expect(await collectMain()).toEqual([
|
||||
{
|
||||
fetchStatus: FetchStatus.UNINITIALIZED,
|
||||
},
|
||||
{
|
||||
error: {
|
||||
msg: 'This query failed',
|
||||
},
|
||||
fetchStatus: 'error',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -19,6 +19,8 @@ import {
|
|||
sendErrorMsg,
|
||||
sendErrorTo,
|
||||
sendLoadingMsg,
|
||||
sendLoadingMoreMsg,
|
||||
sendLoadingMoreFinishedMsg,
|
||||
sendResetMsg,
|
||||
} from '../hooks/use_saved_search_messages';
|
||||
import { fetchDocuments } from './fetch_documents';
|
||||
|
@ -165,6 +167,60 @@ export function fetchAll(
|
|||
}
|
||||
}
|
||||
|
||||
export async function fetchMoreDocuments(
|
||||
dataSubjects: SavedSearchData,
|
||||
fetchDeps: FetchDeps
|
||||
): Promise<void> {
|
||||
try {
|
||||
const { getAppState, getInternalState, services, savedSearch } = fetchDeps;
|
||||
const searchSource = savedSearch.searchSource.createChild();
|
||||
|
||||
const dataView = searchSource.getField('index')!;
|
||||
const query = getAppState().query;
|
||||
const recordRawType = getRawRecordType(query);
|
||||
|
||||
if (recordRawType === RecordRawType.PLAIN) {
|
||||
// not supported yet
|
||||
return;
|
||||
}
|
||||
|
||||
const lastDocuments = dataSubjects.documents$.getValue().result || [];
|
||||
const lastDocumentSort = lastDocuments[lastDocuments.length - 1]?.raw?.sort;
|
||||
|
||||
if (!lastDocumentSort) {
|
||||
return;
|
||||
}
|
||||
|
||||
searchSource.setField('searchAfter', lastDocumentSort);
|
||||
|
||||
// Mark as loading
|
||||
sendLoadingMoreMsg(dataSubjects.documents$);
|
||||
|
||||
// Update the searchSource
|
||||
updateVolatileSearchSource(searchSource, {
|
||||
dataView,
|
||||
services,
|
||||
sort: getAppState().sort as SortOrder[],
|
||||
customFilters: getInternalState().customFilters,
|
||||
});
|
||||
|
||||
// Fetch more documents
|
||||
const { records, interceptedWarnings } = await fetchDocuments(searchSource, fetchDeps);
|
||||
|
||||
// Update the state and finish the loading state
|
||||
sendLoadingMoreFinishedMsg(dataSubjects.documents$, {
|
||||
moreRecords: records,
|
||||
interceptedWarnings,
|
||||
});
|
||||
} catch (error) {
|
||||
sendLoadingMoreFinishedMsg(dataSubjects.documents$, {
|
||||
moreRecords: [],
|
||||
interceptedWarnings: undefined,
|
||||
});
|
||||
sendErrorTo(dataSubjects.main$)(error);
|
||||
}
|
||||
}
|
||||
|
||||
const fetchStatusByType = <T extends DataMsg>(subject: BehaviorSubject<T>, type: string) =>
|
||||
subject.pipe(map(({ fetchStatus }) => ({ type, fetchStatus })));
|
||||
|
||||
|
|
|
@ -16,6 +16,7 @@ import { FetchDeps } from './fetch_all';
|
|||
import type { EsHitRecord } from '@kbn/discover-utils/types';
|
||||
import { buildDataTableRecord } from '@kbn/discover-utils';
|
||||
import { dataViewMock } from '@kbn/discover-utils/src/__mocks__';
|
||||
import { createSearchSourceMock } from '@kbn/data-plugin/common/search/search_source/mocks';
|
||||
|
||||
const getDeps = () =>
|
||||
({
|
||||
|
@ -48,4 +49,47 @@ describe('test fetchDocuments', () => {
|
|||
new Error('Oh noes!')
|
||||
);
|
||||
});
|
||||
|
||||
test('passes a correct session id', async () => {
|
||||
const deps = getDeps();
|
||||
const hits = [
|
||||
{ _id: '1', foo: 'bar' },
|
||||
{ _id: '2', foo: 'baz' },
|
||||
] as unknown as EsHitRecord[];
|
||||
const documents = hits.map((hit) => buildDataTableRecord(hit, dataViewMock));
|
||||
|
||||
// regular search source
|
||||
|
||||
const searchSourceRegular = createSearchSourceMock({ index: dataViewMock });
|
||||
searchSourceRegular.fetch$ = <T>() =>
|
||||
of({ rawResponse: { hits: { hits } } } as IKibanaSearchResponse<SearchResponse<T>>);
|
||||
|
||||
jest.spyOn(searchSourceRegular, 'fetch$');
|
||||
|
||||
expect(fetchDocuments(searchSourceRegular, deps)).resolves.toEqual({
|
||||
records: documents,
|
||||
});
|
||||
|
||||
expect(searchSourceRegular.fetch$ as jest.Mock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ sessionId: deps.searchSessionId })
|
||||
);
|
||||
|
||||
// search source with `search_after` for "Load more" requests
|
||||
|
||||
const searchSourceForLoadMore = createSearchSourceMock({ index: dataViewMock });
|
||||
searchSourceForLoadMore.setField('searchAfter', ['100']);
|
||||
|
||||
searchSourceForLoadMore.fetch$ = <T>() =>
|
||||
of({ rawResponse: { hits: { hits } } } as IKibanaSearchResponse<SearchResponse<T>>);
|
||||
|
||||
jest.spyOn(searchSourceForLoadMore, 'fetch$');
|
||||
|
||||
expect(fetchDocuments(searchSourceForLoadMore, deps)).resolves.toEqual({
|
||||
records: documents,
|
||||
});
|
||||
|
||||
expect(searchSourceForLoadMore.fetch$ as jest.Mock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ sessionId: undefined })
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -36,20 +36,25 @@ export const fetchDocuments = (
|
|||
searchSource.setOverwriteDataViewType(undefined);
|
||||
}
|
||||
const dataView = searchSource.getField('index')!;
|
||||
const isFetchingMore = Boolean(searchSource.getField('searchAfter'));
|
||||
|
||||
const executionContext = {
|
||||
description: 'fetch documents',
|
||||
description: isFetchingMore ? 'fetch more documents' : 'fetch documents',
|
||||
};
|
||||
|
||||
const fetch$ = searchSource
|
||||
.fetch$({
|
||||
abortSignal: abortController.signal,
|
||||
sessionId: searchSessionId,
|
||||
sessionId: isFetchingMore ? undefined : searchSessionId,
|
||||
inspector: {
|
||||
adapter: inspectorAdapters.requests,
|
||||
title: i18n.translate('discover.inspectorRequestDataTitleDocuments', {
|
||||
defaultMessage: 'Documents',
|
||||
}),
|
||||
title: isFetchingMore // TODO: show it as a separate request in Inspect flyout
|
||||
? i18n.translate('discover.inspectorRequestDataTitleMoreDocuments', {
|
||||
defaultMessage: 'More documents',
|
||||
})
|
||||
: i18n.translate('discover.inspectorRequestDataTitleDocuments', {
|
||||
defaultMessage: 'Documents',
|
||||
}),
|
||||
description: i18n.translate('discover.inspectorRequestDescriptionDocument', {
|
||||
defaultMessage: 'This request queries Elasticsearch to fetch the documents.',
|
||||
}),
|
||||
|
|
|
@ -32,13 +32,17 @@ export function updateVolatileSearchSource(
|
|||
}
|
||||
) {
|
||||
const { uiSettings, data } = services;
|
||||
const usedSort = getSortForSearchSource(
|
||||
const useNewFieldsApi = !uiSettings.get(SEARCH_FIELDS_FROM_SOURCE);
|
||||
|
||||
const usedSort = getSortForSearchSource({
|
||||
sort,
|
||||
dataView,
|
||||
uiSettings.get(SORT_DEFAULT_ORDER_SETTING)
|
||||
);
|
||||
const useNewFieldsApi = !uiSettings.get(SEARCH_FIELDS_FROM_SOURCE);
|
||||
searchSource.setField('trackTotalHits', true).setField('sort', usedSort);
|
||||
defaultSortDir: uiSettings.get(SORT_DEFAULT_ORDER_SETTING),
|
||||
includeTieBreaker: true,
|
||||
});
|
||||
searchSource.setField('sort', usedSort);
|
||||
|
||||
searchSource.setField('trackTotalHits', true);
|
||||
|
||||
let filters = [...customFilters];
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
export enum FetchStatus {
|
||||
UNINITIALIZED = 'uninitialized',
|
||||
LOADING = 'loading',
|
||||
LOADING_MORE = 'loading_more',
|
||||
PARTIAL = 'partial',
|
||||
COMPLETE = 'complete',
|
||||
ERROR = 'error',
|
||||
|
|
|
@ -65,14 +65,6 @@
|
|||
min-height: 0;
|
||||
}
|
||||
|
||||
.dscDiscoverGrid__footer {
|
||||
flex-shrink: 0;
|
||||
background-color: $euiColorLightShade;
|
||||
padding: $euiSize / 2 $euiSize;
|
||||
margin-top: $euiSize / 4;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.dscTable__flyoutHeader {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@ import { act } from 'react-dom/test-utils';
|
|||
import { findTestSubject } from '@elastic/eui/lib/test';
|
||||
import { buildDataViewMock, deepMockedFields, esHitsMock } from '@kbn/discover-utils/src/__mocks__';
|
||||
import { mountWithIntl } from '@kbn/test-jest-helpers';
|
||||
import { DiscoverGrid, DiscoverGridProps } from './discover_grid';
|
||||
import { DiscoverGrid, DiscoverGridProps, DataLoadingState } from './discover_grid';
|
||||
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
|
||||
import { discoverServiceMock } from '../../__mocks__/services';
|
||||
import { buildDataTableRecord, getDocId } from '@kbn/discover-utils';
|
||||
|
@ -38,7 +38,7 @@ function getProps() {
|
|||
ariaLabelledBy: '',
|
||||
columns: [],
|
||||
dataView: dataViewMock,
|
||||
isLoading: false,
|
||||
loadingState: DataLoadingState.loaded,
|
||||
expandedDoc: undefined,
|
||||
onAddColumn: jest.fn(),
|
||||
onFilter: jest.fn(),
|
||||
|
|
|
@ -56,11 +56,18 @@ import {
|
|||
import { GRID_STYLE, toolbarVisibility as toolbarVisibilityDefaults } from './constants';
|
||||
import { getDisplayedColumns } from '../../utils/columns';
|
||||
import { DiscoverGridDocumentToolbarBtn } from './discover_grid_document_selection';
|
||||
import { DiscoverGridFooter } from './discover_grid_footer';
|
||||
import type { ValueToStringConverter } from '../../types';
|
||||
import { useRowHeightsOptions } from '../../hooks/use_row_heights_options';
|
||||
import { convertValueToString } from '../../utils/convert_value_to_string';
|
||||
import { getRowsPerPageOptions, getDefaultRowsPerPage } from '../../utils/rows_per_page';
|
||||
|
||||
export enum DataLoadingState {
|
||||
loading = 'loading',
|
||||
loadingMore = 'loadingMore',
|
||||
loaded = 'loaded',
|
||||
}
|
||||
|
||||
const themeDefault = { darkMode: false };
|
||||
|
||||
interface SortObj {
|
||||
|
@ -92,7 +99,7 @@ export interface DiscoverGridProps {
|
|||
/**
|
||||
* Determines if data is currently loaded
|
||||
*/
|
||||
isLoading: boolean;
|
||||
loadingState: DataLoadingState;
|
||||
/**
|
||||
* Function used to add a column in the document flyout
|
||||
*/
|
||||
|
@ -225,7 +232,16 @@ export interface DiscoverGridProps {
|
|||
dataViewFieldEditor: DataViewFieldEditorStart;
|
||||
toastNotifications: ToastsStart;
|
||||
};
|
||||
/**
|
||||
* Number total hits from ES
|
||||
*/
|
||||
totalHits?: number;
|
||||
/**
|
||||
* To fetch more
|
||||
*/
|
||||
onFetchMoreRecords?: () => void;
|
||||
}
|
||||
|
||||
export const EuiDataGridMemoized = React.memo(EuiDataGrid);
|
||||
|
||||
const CONTROL_COLUMN_IDS_DEFAULT = ['openDetails', 'select'];
|
||||
|
@ -234,7 +250,7 @@ export const DiscoverGrid = ({
|
|||
ariaLabelledBy,
|
||||
columns,
|
||||
dataView,
|
||||
isLoading,
|
||||
loadingState,
|
||||
expandedDoc,
|
||||
onAddColumn,
|
||||
filters,
|
||||
|
@ -268,6 +284,8 @@ export const DiscoverGrid = ({
|
|||
onFieldEdited,
|
||||
DocumentView,
|
||||
services,
|
||||
totalHits,
|
||||
onFetchMoreRecords,
|
||||
}: DiscoverGridProps) => {
|
||||
const { fieldFormats, toastNotifications, dataViewFieldEditor, uiSettings } = services;
|
||||
const { darkMode } = useObservable(services.core.theme?.theme$ ?? of(themeDefault), themeDefault);
|
||||
|
@ -358,8 +376,6 @@ export const DiscoverGrid = ({
|
|||
: undefined;
|
||||
}, [pagination, pageCount, isPaginationEnabled, onUpdateRowsPerPage]);
|
||||
|
||||
const isOnLastPage = paginationObj ? paginationObj.pageIndex === pageCount - 1 : false;
|
||||
|
||||
useEffect(() => {
|
||||
setPagination((paginationData) =>
|
||||
paginationData.pageSize === currentPageSize
|
||||
|
@ -413,7 +429,6 @@ export const DiscoverGrid = ({
|
|||
/**
|
||||
* Render variables
|
||||
*/
|
||||
const showDisclaimer = rowCount === sampleSize && isOnLastPage;
|
||||
const randomId = useMemo(() => htmlIdGenerator()(), []);
|
||||
const closeFieldEditor = useRef<() => void | undefined>();
|
||||
|
||||
|
@ -602,7 +617,9 @@ export const DiscoverGrid = ({
|
|||
onUpdateRowHeight,
|
||||
});
|
||||
|
||||
if (!rowCount && isLoading) {
|
||||
const isRenderComplete = loadingState !== DataLoadingState.loading;
|
||||
|
||||
if (!rowCount && loadingState === DataLoadingState.loading) {
|
||||
return (
|
||||
<div className="euiDataGrid__loading">
|
||||
<EuiText size="xs" color="subdued">
|
||||
|
@ -618,7 +635,7 @@ export const DiscoverGrid = ({
|
|||
return (
|
||||
<div
|
||||
className="euiDataGrid__noResults"
|
||||
data-render-complete={!isLoading}
|
||||
data-render-complete={isRenderComplete}
|
||||
data-shared-item=""
|
||||
data-title={searchTitle}
|
||||
data-description={searchDescription}
|
||||
|
@ -655,7 +672,7 @@ export const DiscoverGrid = ({
|
|||
<span className="dscDiscoverGrid__inner">
|
||||
<div
|
||||
data-test-subj="discoverDocTable"
|
||||
data-render-complete={!isLoading}
|
||||
data-render-complete={isRenderComplete}
|
||||
data-shared-item=""
|
||||
data-title={searchTitle}
|
||||
data-description={searchDescription}
|
||||
|
@ -682,17 +699,18 @@ export const DiscoverGrid = ({
|
|||
gridStyle={GRID_STYLE}
|
||||
/>
|
||||
</div>
|
||||
{showDisclaimer && (
|
||||
<p className="dscDiscoverGrid__footer" data-test-subj="discoverTableFooter">
|
||||
<FormattedMessage
|
||||
id="discover.gridSampleSize.limitDescription"
|
||||
defaultMessage="Search results are limited to {sampleSize} documents. Add more search terms to narrow your search."
|
||||
values={{
|
||||
sampleSize,
|
||||
}}
|
||||
{loadingState !== DataLoadingState.loading &&
|
||||
isPaginationEnabled && ( // we hide the footer for Surrounding Documents page
|
||||
<DiscoverGridFooter
|
||||
isLoadingMore={loadingState === DataLoadingState.loadingMore}
|
||||
rowCount={rowCount}
|
||||
sampleSize={sampleSize}
|
||||
pageCount={pageCount}
|
||||
pageIndex={paginationObj?.pageIndex}
|
||||
totalHits={totalHits}
|
||||
onFetchMoreRecords={onFetchMoreRecords}
|
||||
/>
|
||||
</p>
|
||||
)}
|
||||
)}
|
||||
{searchTitle && (
|
||||
<EuiScreenReaderOnly>
|
||||
<p id={String(randomId)}>
|
||||
|
|
|
@ -0,0 +1,138 @@
|
|||
/*
|
||||
* 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 { mountWithIntl } from '@kbn/test-jest-helpers';
|
||||
import { findTestSubject } from '@elastic/eui/lib/test';
|
||||
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
|
||||
import { DiscoverGridFooter } from './discover_grid_footer';
|
||||
import { discoverServiceMock } from '../../__mocks__/services';
|
||||
|
||||
describe('DiscoverGridFooter', function () {
|
||||
it('should not render anything when not on the last page', async () => {
|
||||
const component = mountWithIntl(
|
||||
<KibanaContextProvider services={discoverServiceMock}>
|
||||
<DiscoverGridFooter
|
||||
pageCount={5}
|
||||
pageIndex={0}
|
||||
sampleSize={500}
|
||||
totalHits={1000}
|
||||
rowCount={500}
|
||||
/>
|
||||
</KibanaContextProvider>
|
||||
);
|
||||
expect(component.isEmptyRender()).toBe(true);
|
||||
});
|
||||
|
||||
it('should not render anything yet when all rows shown', async () => {
|
||||
const component = mountWithIntl(
|
||||
<KibanaContextProvider services={discoverServiceMock}>
|
||||
<DiscoverGridFooter
|
||||
pageCount={5}
|
||||
pageIndex={4}
|
||||
sampleSize={500}
|
||||
totalHits={500}
|
||||
rowCount={500}
|
||||
/>
|
||||
</KibanaContextProvider>
|
||||
);
|
||||
expect(component.isEmptyRender()).toBe(true);
|
||||
});
|
||||
|
||||
it('should render a message for the last page', async () => {
|
||||
const component = mountWithIntl(
|
||||
<KibanaContextProvider services={discoverServiceMock}>
|
||||
<DiscoverGridFooter
|
||||
pageCount={5}
|
||||
pageIndex={4}
|
||||
sampleSize={500}
|
||||
totalHits={1000}
|
||||
rowCount={500}
|
||||
/>
|
||||
</KibanaContextProvider>
|
||||
);
|
||||
expect(findTestSubject(component, 'discoverTableFooter').text()).toBe(
|
||||
'Search results are limited to 500 documents. Add more search terms to narrow your search.'
|
||||
);
|
||||
expect(findTestSubject(component, 'dscGridSampleSizeFetchMoreLink').exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('should render a message and the button for the last page', async () => {
|
||||
const mockLoadMore = jest.fn();
|
||||
|
||||
const component = mountWithIntl(
|
||||
<KibanaContextProvider services={discoverServiceMock}>
|
||||
<DiscoverGridFooter
|
||||
pageCount={5}
|
||||
pageIndex={4}
|
||||
sampleSize={500}
|
||||
totalHits={1000}
|
||||
rowCount={500}
|
||||
isLoadingMore={false}
|
||||
onFetchMoreRecords={mockLoadMore}
|
||||
/>
|
||||
</KibanaContextProvider>
|
||||
);
|
||||
expect(findTestSubject(component, 'discoverTableFooter').text()).toBe(
|
||||
'Search results are limited to 500 documents.Load more'
|
||||
);
|
||||
|
||||
const button = findTestSubject(component, 'dscGridSampleSizeFetchMoreLink');
|
||||
expect(button.exists()).toBe(true);
|
||||
|
||||
button.simulate('click');
|
||||
|
||||
expect(mockLoadMore).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should render a disabled button when loading more', async () => {
|
||||
const mockLoadMore = jest.fn();
|
||||
|
||||
const component = mountWithIntl(
|
||||
<KibanaContextProvider services={discoverServiceMock}>
|
||||
<DiscoverGridFooter
|
||||
pageCount={5}
|
||||
pageIndex={4}
|
||||
sampleSize={500}
|
||||
totalHits={1000}
|
||||
rowCount={500}
|
||||
isLoadingMore={true}
|
||||
onFetchMoreRecords={mockLoadMore}
|
||||
/>
|
||||
</KibanaContextProvider>
|
||||
);
|
||||
expect(findTestSubject(component, 'discoverTableFooter').text()).toBe(
|
||||
'Search results are limited to 500 documents.Load more'
|
||||
);
|
||||
|
||||
const button = findTestSubject(component, 'dscGridSampleSizeFetchMoreLink');
|
||||
expect(button.exists()).toBe(true);
|
||||
expect(button.prop('disabled')).toBe(true);
|
||||
|
||||
button.simulate('click');
|
||||
|
||||
expect(mockLoadMore).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should render a message when max total limit is reached', async () => {
|
||||
const component = mountWithIntl(
|
||||
<KibanaContextProvider services={discoverServiceMock}>
|
||||
<DiscoverGridFooter
|
||||
pageCount={100}
|
||||
pageIndex={99}
|
||||
sampleSize={500}
|
||||
totalHits={11000}
|
||||
rowCount={10000}
|
||||
/>
|
||||
</KibanaContextProvider>
|
||||
);
|
||||
expect(findTestSubject(component, 'discoverTableFooter').text()).toBe(
|
||||
'Search results are limited to 10000 documents. Add more search terms to narrow your search.'
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,161 @@
|
|||
/*
|
||||
* 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, { useEffect, useState } from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { EuiButtonEmpty, EuiToolTip, useEuiTheme } from '@elastic/eui';
|
||||
import { css } from '@emotion/react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { ES_FIELD_TYPES, KBN_FIELD_TYPES } from '@kbn/field-types';
|
||||
import { MAX_LOADED_GRID_ROWS } from '../../../common/constants';
|
||||
import { useDiscoverServices } from '../../hooks/use_discover_services';
|
||||
|
||||
export interface DiscoverGridFooterProps {
|
||||
isLoadingMore?: boolean;
|
||||
rowCount: number;
|
||||
sampleSize: number;
|
||||
pageIndex?: number; // starts from 0
|
||||
pageCount: number;
|
||||
totalHits?: number;
|
||||
onFetchMoreRecords?: () => void;
|
||||
}
|
||||
|
||||
export const DiscoverGridFooter: React.FC<DiscoverGridFooterProps> = (props) => {
|
||||
const {
|
||||
isLoadingMore,
|
||||
rowCount,
|
||||
sampleSize,
|
||||
pageIndex,
|
||||
pageCount,
|
||||
totalHits = 0,
|
||||
onFetchMoreRecords,
|
||||
} = props;
|
||||
const { data } = useDiscoverServices();
|
||||
const timefilter = data.query.timefilter.timefilter;
|
||||
const [refreshInterval, setRefreshInterval] = useState(timefilter.getRefreshInterval());
|
||||
|
||||
useEffect(() => {
|
||||
const subscriber = timefilter.getRefreshIntervalUpdate$().subscribe(() => {
|
||||
setRefreshInterval(timefilter.getRefreshInterval());
|
||||
});
|
||||
|
||||
return () => subscriber.unsubscribe();
|
||||
}, [timefilter, setRefreshInterval]);
|
||||
|
||||
const isRefreshIntervalOn = Boolean(
|
||||
refreshInterval && refreshInterval.pause === false && refreshInterval.value > 0
|
||||
);
|
||||
|
||||
const { euiTheme } = useEuiTheme();
|
||||
const isOnLastPage = pageIndex === pageCount - 1 && rowCount < totalHits;
|
||||
|
||||
if (!isOnLastPage) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// allow to fetch more records on Discover page
|
||||
if (onFetchMoreRecords && typeof isLoadingMore === 'boolean') {
|
||||
if (rowCount <= MAX_LOADED_GRID_ROWS - sampleSize) {
|
||||
return (
|
||||
<DiscoverGridFooterContainer hasButton={true} {...props}>
|
||||
<EuiToolTip
|
||||
content={
|
||||
isRefreshIntervalOn
|
||||
? i18n.translate('discover.gridSampleSize.fetchMoreLinkDisabledTooltip', {
|
||||
defaultMessage: 'To load more the refresh interval needs to be disabled first',
|
||||
})
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<EuiButtonEmpty
|
||||
disabled={isRefreshIntervalOn}
|
||||
isLoading={isLoadingMore}
|
||||
flush="both"
|
||||
data-test-subj="dscGridSampleSizeFetchMoreLink"
|
||||
onClick={onFetchMoreRecords}
|
||||
css={css`
|
||||
margin-left: ${euiTheme.size.xs};
|
||||
`}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="discover.gridSampleSize.fetchMoreLinkLabel"
|
||||
defaultMessage="Load more"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</EuiToolTip>
|
||||
</DiscoverGridFooterContainer>
|
||||
);
|
||||
}
|
||||
|
||||
return <DiscoverGridFooterContainer hasButton={false} {...props} />;
|
||||
}
|
||||
|
||||
if (rowCount < totalHits) {
|
||||
// show only a message for embeddable
|
||||
return <DiscoverGridFooterContainer hasButton={false} {...props} />;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
interface DiscoverGridFooterContainerProps extends DiscoverGridFooterProps {
|
||||
hasButton: boolean;
|
||||
}
|
||||
|
||||
const DiscoverGridFooterContainer: React.FC<DiscoverGridFooterContainerProps> = ({
|
||||
hasButton,
|
||||
rowCount,
|
||||
children,
|
||||
}) => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
const { fieldFormats } = useDiscoverServices();
|
||||
|
||||
const formattedRowCount = fieldFormats
|
||||
.getDefaultInstance(KBN_FIELD_TYPES.NUMBER, [ES_FIELD_TYPES.INTEGER])
|
||||
.convert(rowCount);
|
||||
|
||||
return (
|
||||
<p
|
||||
data-test-subj="discoverTableFooter"
|
||||
css={css`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
background-color: ${euiTheme.colors.lightestShade};
|
||||
padding: ${hasButton
|
||||
? `0 ${euiTheme.size.base}`
|
||||
: `${euiTheme.size.s} ${euiTheme.size.base}`};
|
||||
margin-top: ${euiTheme.size.xs};
|
||||
text-align: center;
|
||||
`}
|
||||
>
|
||||
<span>
|
||||
{hasButton ? (
|
||||
<FormattedMessage
|
||||
id="discover.gridSampleSize.lastPageDescription"
|
||||
defaultMessage="Search results are limited to {rowCount} documents."
|
||||
values={{
|
||||
rowCount: formattedRowCount,
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id="discover.gridSampleSize.limitDescription"
|
||||
defaultMessage="Search results are limited to {sampleSize} documents. Add more search terms to narrow your search."
|
||||
values={{
|
||||
sampleSize: formattedRowCount,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
{children}
|
||||
</p>
|
||||
);
|
||||
};
|
|
@ -21,7 +21,7 @@ import { useDiscoverServices } from '../../hooks/use_discover_services';
|
|||
import { SavedSearchEmbeddableBase } from '../../embeddable/saved_search_embeddable_base';
|
||||
|
||||
export interface DocTableEmbeddableProps extends DocTableProps {
|
||||
totalHitCount: number;
|
||||
totalHitCount?: number;
|
||||
rowsPerPageState?: number;
|
||||
interceptedWarnings?: SearchResponseInterceptedWarning[];
|
||||
onUpdateRowsPerPage?: (rowsPerPage?: number) => void;
|
||||
|
@ -79,7 +79,7 @@ export const DocTableEmbeddable = (props: DocTableEmbeddableProps) => {
|
|||
);
|
||||
|
||||
const shouldShowLimitedResultsWarning = useMemo(
|
||||
() => !hasNextPage && props.rows.length < props.totalHitCount,
|
||||
() => !hasNextPage && props.totalHitCount && props.rows.length < props.totalHitCount,
|
||||
[hasNextPage, props.rows.length, props.totalHitCount]
|
||||
);
|
||||
|
||||
|
|
|
@ -280,7 +280,7 @@ export class SavedSearchEmbeddable
|
|||
useNewFieldsApi,
|
||||
{
|
||||
sampleSize: this.services.uiSettings.get(SAMPLE_SIZE_SETTING),
|
||||
defaultSort: this.services.uiSettings.get(SORT_DEFAULT_ORDER_SETTING),
|
||||
sortDir: this.services.uiSettings.get(SORT_DEFAULT_ORDER_SETTING),
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
@ -20,7 +20,7 @@ const containerStyles = css`
|
|||
|
||||
export interface SavedSearchEmbeddableBaseProps {
|
||||
isLoading: boolean;
|
||||
totalHitCount: number;
|
||||
totalHitCount?: number;
|
||||
prepend?: React.ReactElement;
|
||||
append?: React.ReactElement;
|
||||
dataTestSubj?: string;
|
||||
|
@ -55,7 +55,7 @@ export const SavedSearchEmbeddableBase: React.FC<SavedSearchEmbeddableBaseProps>
|
|||
>
|
||||
{Boolean(prepend) && <EuiFlexItem grow={false}>{prepend}</EuiFlexItem>}
|
||||
|
||||
{Boolean(totalHitCount) && (
|
||||
{!!totalHitCount && (
|
||||
<EuiFlexItem grow={false} data-test-subj="toolBarTotalDocsText">
|
||||
<TotalDocuments totalHitCount={totalHitCount} />
|
||||
</EuiFlexItem>
|
||||
|
|
|
@ -8,7 +8,11 @@
|
|||
|
||||
import React from 'react';
|
||||
import { AggregateQuery, Query } from '@kbn/es-query';
|
||||
import { DiscoverGridEmbeddable, DiscoverGridEmbeddableProps } from './saved_search_grid';
|
||||
import {
|
||||
DiscoverGridEmbeddable,
|
||||
DiscoverGridEmbeddableProps,
|
||||
DataLoadingState,
|
||||
} from './saved_search_grid';
|
||||
import { DiscoverDocTableEmbeddable } from '../components/doc_table/create_doc_table_embeddable';
|
||||
import { DocTableEmbeddableProps } from '../components/doc_table/doc_table_embeddable';
|
||||
import { isTextBasedQuery } from '../application/main/utils/is_text_based_query';
|
||||
|
@ -32,14 +36,15 @@ export function SavedSearchEmbeddableComponent({
|
|||
const isPlainRecord = isTextBasedQuery(query);
|
||||
return (
|
||||
<DiscoverDocTableEmbeddableMemoized
|
||||
{...(searchProps as DocTableEmbeddableProps)}
|
||||
{...(searchProps as DocTableEmbeddableProps)} // TODO later: remove the type casting to prevent unexpected errors due to missing props!
|
||||
isPlainRecord={isPlainRecord}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<DiscoverGridEmbeddableMemoized
|
||||
{...(searchProps as DiscoverGridEmbeddableProps)}
|
||||
{...(searchProps as DiscoverGridEmbeddableProps)} // TODO later: remove the type casting to prevent unexpected errors due to missing props!
|
||||
loadingState={searchProps.isLoading ? DataLoadingState.loading : DataLoadingState.loaded}
|
||||
showFullScreenButton={false}
|
||||
query={query}
|
||||
className="dscDiscoverGrid"
|
||||
|
|
|
@ -5,20 +5,25 @@
|
|||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
import React, { useState, memo } from 'react';
|
||||
import React, { memo, useState } from 'react';
|
||||
import type { DataTableRecord } from '@kbn/discover-utils/types';
|
||||
import type { SearchResponseInterceptedWarning } from '@kbn/search-response-warnings';
|
||||
import { DiscoverGrid, DiscoverGridProps } from '../components/discover_grid/discover_grid';
|
||||
import {
|
||||
DiscoverGrid,
|
||||
DiscoverGridProps,
|
||||
DataLoadingState as DiscoverDataLoadingState,
|
||||
} from '../components/discover_grid/discover_grid';
|
||||
import './saved_search_grid.scss';
|
||||
import { DiscoverGridFlyout } from '../components/discover_grid/discover_grid_flyout';
|
||||
import { SavedSearchEmbeddableBase } from './saved_search_embeddable_base';
|
||||
export { DataLoadingState } from '../components/discover_grid/discover_grid';
|
||||
|
||||
export interface DiscoverGridEmbeddableProps extends DiscoverGridProps {
|
||||
totalHitCount: number;
|
||||
totalHitCount?: number;
|
||||
interceptedWarnings?: SearchResponseInterceptedWarning[];
|
||||
}
|
||||
|
||||
export const DataGridMemoized = memo(DiscoverGrid);
|
||||
export const DiscoverGridMemoized = memo(DiscoverGrid);
|
||||
|
||||
export function DiscoverGridEmbeddable(props: DiscoverGridEmbeddableProps) {
|
||||
const { interceptedWarnings, ...gridProps } = props;
|
||||
|
@ -27,12 +32,13 @@ export function DiscoverGridEmbeddable(props: DiscoverGridEmbeddableProps) {
|
|||
return (
|
||||
<SavedSearchEmbeddableBase
|
||||
totalHitCount={props.totalHitCount}
|
||||
isLoading={props.isLoading}
|
||||
isLoading={props.loadingState === DiscoverDataLoadingState.loading}
|
||||
dataTestSubj="embeddedSavedSearchDocTable"
|
||||
interceptedWarnings={props.interceptedWarnings}
|
||||
>
|
||||
<DataGridMemoized
|
||||
<DiscoverGridMemoized
|
||||
{...gridProps}
|
||||
totalHits={props.totalHitCount}
|
||||
setExpandedDoc={setExpandedDoc}
|
||||
expandedDoc={expandedDoc}
|
||||
DocumentView={DiscoverGridFlyout}
|
||||
|
|
|
@ -7,13 +7,23 @@
|
|||
*/
|
||||
import { createSearchSourceMock } from '@kbn/data-plugin/common/search/search_source/mocks';
|
||||
import { updateSearchSource } from './update_search_source';
|
||||
import { dataViewMock } from '@kbn/discover-utils/src/__mocks__';
|
||||
import {
|
||||
buildDataViewMock,
|
||||
dataViewMock,
|
||||
shallowMockedFields,
|
||||
} from '@kbn/discover-utils/src/__mocks__';
|
||||
import type { SortOrder } from '@kbn/saved-search-plugin/public';
|
||||
|
||||
const dataViewMockWithTimeField = buildDataViewMock({
|
||||
name: 'the-data-view',
|
||||
fields: shallowMockedFields,
|
||||
timeFieldName: '@timestamp',
|
||||
});
|
||||
|
||||
describe('updateSearchSource', () => {
|
||||
const defaults = {
|
||||
sampleSize: 50,
|
||||
defaultSort: 'asc',
|
||||
sortDir: 'asc',
|
||||
};
|
||||
|
||||
it('updates a given search source', async () => {
|
||||
|
@ -30,4 +40,34 @@ describe('updateSearchSource', () => {
|
|||
expect(searchSource.getField('fields')).toEqual([{ field: '*', include_unmapped: 'true' }]);
|
||||
expect(searchSource.getField('fieldsFromSource')).toBe(undefined);
|
||||
});
|
||||
|
||||
it('updates a given search source with sort field', async () => {
|
||||
const searchSource1 = createSearchSourceMock({});
|
||||
updateSearchSource(searchSource1, dataViewMock, [] as SortOrder[], true, defaults);
|
||||
expect(searchSource1.getField('sort')).toEqual([{ _score: 'asc' }]);
|
||||
|
||||
const searchSource2 = createSearchSourceMock({});
|
||||
updateSearchSource(searchSource2, dataViewMockWithTimeField, [] as SortOrder[], true, {
|
||||
sampleSize: 50,
|
||||
sortDir: 'desc',
|
||||
});
|
||||
expect(searchSource2.getField('sort')).toEqual([{ _doc: 'desc' }]);
|
||||
|
||||
const searchSource3 = createSearchSourceMock({});
|
||||
updateSearchSource(
|
||||
searchSource3,
|
||||
dataViewMockWithTimeField,
|
||||
[['bytes', 'desc']] as SortOrder[],
|
||||
true,
|
||||
defaults
|
||||
);
|
||||
expect(searchSource3.getField('sort')).toEqual([
|
||||
{
|
||||
bytes: 'desc',
|
||||
},
|
||||
{
|
||||
_doc: 'desc',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -17,12 +17,20 @@ export const updateSearchSource = (
|
|||
useNewFieldsApi: boolean,
|
||||
defaults: {
|
||||
sampleSize: number;
|
||||
defaultSort: string;
|
||||
sortDir: string;
|
||||
}
|
||||
) => {
|
||||
const { sampleSize, defaultSort } = defaults;
|
||||
const { sampleSize, sortDir } = defaults;
|
||||
searchSource.setField('size', sampleSize);
|
||||
searchSource.setField('sort', getSortForSearchSource(sort, dataView, defaultSort));
|
||||
searchSource.setField(
|
||||
'sort',
|
||||
getSortForSearchSource({
|
||||
sort,
|
||||
dataView,
|
||||
defaultSortDir: sortDir,
|
||||
includeTieBreaker: true,
|
||||
})
|
||||
);
|
||||
if (useNewFieldsApi) {
|
||||
searchSource.removeField('fieldsFromSource');
|
||||
const fields: Record<string, string> = { field: '*', include_unmapped: 'true' };
|
||||
|
|
|
@ -35,14 +35,18 @@ export async function getSharingData(
|
|||
services: { uiSettings: IUiSettingsClient; data: DataPublicPluginStart },
|
||||
isPlainRecord?: boolean
|
||||
) {
|
||||
const { uiSettings: config, data } = services;
|
||||
const { uiSettings, data } = services;
|
||||
const searchSource = currentSearchSource.createCopy();
|
||||
const index = searchSource.getField('index')!;
|
||||
let existingFilter = searchSource.getField('filter') as Filter[] | Filter | undefined;
|
||||
|
||||
searchSource.setField(
|
||||
'sort',
|
||||
getSortForSearchSource(state.sort as SortOrder[], index, config.get(SORT_DEFAULT_ORDER_SETTING))
|
||||
getSortForSearchSource({
|
||||
sort: state.sort as SortOrder[],
|
||||
dataView: index,
|
||||
defaultSortDir: uiSettings.get(SORT_DEFAULT_ORDER_SETTING),
|
||||
})
|
||||
);
|
||||
|
||||
searchSource.removeField('filter');
|
||||
|
@ -57,7 +61,7 @@ export async function getSharingData(
|
|||
if (columns && columns.length > 0) {
|
||||
// conditionally add the time field column:
|
||||
let timeFieldName: string | undefined;
|
||||
const hideTimeColumn = config.get(DOC_HIDE_TIME_COLUMN_SETTING);
|
||||
const hideTimeColumn = uiSettings.get(DOC_HIDE_TIME_COLUMN_SETTING);
|
||||
if (!hideTimeColumn && index && index.timeFieldName && !isPlainRecord) {
|
||||
timeFieldName = index.timeFieldName;
|
||||
}
|
||||
|
@ -98,7 +102,7 @@ export async function getSharingData(
|
|||
* Otherwise, the requests will ask for all fields, even if only a few are really needed.
|
||||
* Discover does not set fields, since having all fields is needed for the UI.
|
||||
*/
|
||||
const useFieldsApi = !config.get(SEARCH_FIELDS_FROM_SOURCE);
|
||||
const useFieldsApi = !uiSettings.get(SEARCH_FIELDS_FROM_SOURCE);
|
||||
if (useFieldsApi) {
|
||||
searchSource.removeField('fieldsFromSource');
|
||||
const fields = columns.length
|
||||
|
|
|
@ -11,6 +11,7 @@ import { DataView } from '@kbn/data-views-plugin/common';
|
|||
import { AggregateQuery, Filter, Query } from '@kbn/es-query';
|
||||
import { SavedSearch } from '@kbn/saved-search-plugin/common';
|
||||
import { getSavedSearch } from '@kbn/saved-search-plugin/server';
|
||||
import { SORT_DEFAULT_ORDER_SETTING } from '@kbn/discover-utils';
|
||||
import { LocatorServicesDeps } from '.';
|
||||
import { DiscoverAppLocatorParams } from '../../common';
|
||||
import { getSortForSearchSource } from '../../common/utils/sorting';
|
||||
|
@ -147,7 +148,13 @@ export function searchSourceFromLocatorFactory(services: LocatorServicesDeps) {
|
|||
|
||||
// Inject sort
|
||||
if (savedSearch.sort) {
|
||||
const sort = getSortForSearchSource(savedSearch.sort as Array<[string, string]>, index);
|
||||
const defaultSortDir = await services.uiSettings.get(SORT_DEFAULT_ORDER_SETTING);
|
||||
|
||||
const sort = getSortForSearchSource({
|
||||
sort: savedSearch.sort as Array<[string, string]>,
|
||||
dataView: index,
|
||||
defaultSortDir,
|
||||
});
|
||||
searchSource.setField('sort', sort);
|
||||
}
|
||||
|
||||
|
|
|
@ -199,6 +199,7 @@ export const getUiSettings: (docLinks: DocLinksServiceSetup) => Record<string, U
|
|||
'</a>',
|
||||
},
|
||||
}),
|
||||
requiresPageReload: true,
|
||||
category: ['discover'],
|
||||
schema: schema.boolean(),
|
||||
metric: {
|
||||
|
@ -206,7 +207,6 @@ export const getUiSettings: (docLinks: DocLinksServiceSetup) => Record<string, U
|
|||
name: 'discover:useLegacyDataGrid',
|
||||
},
|
||||
},
|
||||
|
||||
[MODIFY_COLUMNS_ON_SWITCH]: {
|
||||
name: i18n.translate('discover.advancedSettings.discover.modifyColumnsOnSwitchTitle', {
|
||||
defaultMessage: 'Modify columns when changing data views',
|
||||
|
|
|
@ -65,6 +65,7 @@
|
|||
"@kbn/core-application-browser",
|
||||
"@kbn/core-saved-objects-server",
|
||||
"@kbn/discover-utils",
|
||||
"@kbn/field-types",
|
||||
"@kbn/search-response-warnings",
|
||||
"@kbn/content-management-plugin",
|
||||
"@kbn/serverless",
|
||||
|
|
226
test/functional/apps/discover/group2/_data_grid_footer.ts
Normal file
226
test/functional/apps/discover/group2/_data_grid_footer.ts
Normal file
|
@ -0,0 +1,226 @@
|
|||
/*
|
||||
* 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 expect from '@kbn/expect';
|
||||
import { FtrProviderContext } from '../ftr_provider_context';
|
||||
|
||||
const FOOTER_SELECTOR = 'discoverTableFooter';
|
||||
const LOAD_MORE_SELECTOR = 'dscGridSampleSizeFetchMoreLink';
|
||||
|
||||
export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
||||
const esArchiver = getService('esArchiver');
|
||||
const kibanaServer = getService('kibanaServer');
|
||||
const dataGrid = getService('dataGrid');
|
||||
const PageObjects = getPageObjects(['common', 'discover', 'timePicker', 'unifiedFieldList']);
|
||||
const defaultSettings = { defaultIndex: 'logstash-*' };
|
||||
const testSubjects = getService('testSubjects');
|
||||
const retry = getService('retry');
|
||||
const security = getService('security');
|
||||
|
||||
describe('discover data grid footer', function () {
|
||||
describe('time field with date type', function () {
|
||||
before(async () => {
|
||||
await security.testUser.setRoles(['kibana_admin', 'test_logstash_reader']);
|
||||
await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional');
|
||||
await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/discover');
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await kibanaServer.importExport.unload('test/functional/fixtures/kbn_archiver/discover');
|
||||
await esArchiver.unload('test/functional/fixtures/es_archiver/logstash_functional');
|
||||
await kibanaServer.savedObjects.cleanStandardList();
|
||||
await kibanaServer.uiSettings.replace({});
|
||||
});
|
||||
|
||||
beforeEach(async function () {
|
||||
await PageObjects.timePicker.setDefaultAbsoluteRangeViaUiSettings();
|
||||
await kibanaServer.uiSettings.update(defaultSettings);
|
||||
await PageObjects.common.navigateToApp('discover');
|
||||
await PageObjects.discover.waitUntilSearchingHasFinished();
|
||||
});
|
||||
|
||||
it('should show footer only for the last page and allow to load more', async () => {
|
||||
// footer is not shown
|
||||
await testSubjects.missingOrFail(FOOTER_SELECTOR);
|
||||
|
||||
// go to next page
|
||||
await testSubjects.click('pagination-button-next');
|
||||
// footer is not shown yet
|
||||
await retry.try(async function () {
|
||||
await testSubjects.missingOrFail(FOOTER_SELECTOR);
|
||||
});
|
||||
|
||||
// go to the last page
|
||||
await testSubjects.click('pagination-button-4');
|
||||
// footer is shown now
|
||||
await retry.try(async function () {
|
||||
await testSubjects.existOrFail(FOOTER_SELECTOR);
|
||||
});
|
||||
expect(await testSubjects.getVisibleText(FOOTER_SELECTOR)).to.be(
|
||||
'Search results are limited to 500 documents.\nLoad more'
|
||||
);
|
||||
|
||||
// there is no other pages to see
|
||||
await testSubjects.missingOrFail('pagination-button-5');
|
||||
|
||||
// press "Load more"
|
||||
await testSubjects.click(LOAD_MORE_SELECTOR);
|
||||
await PageObjects.discover.waitUntilSearchingHasFinished();
|
||||
|
||||
// more pages appeared and the footer is gone
|
||||
await retry.try(async function () {
|
||||
await testSubjects.missingOrFail(FOOTER_SELECTOR);
|
||||
});
|
||||
|
||||
// go to the last page
|
||||
await testSubjects.click('pagination-button-9');
|
||||
expect(await testSubjects.getVisibleText(FOOTER_SELECTOR)).to.be(
|
||||
'Search results are limited to 1,000 documents.\nLoad more'
|
||||
);
|
||||
|
||||
// press "Load more"
|
||||
await testSubjects.click(LOAD_MORE_SELECTOR);
|
||||
await PageObjects.discover.waitUntilSearchingHasFinished();
|
||||
|
||||
// more pages appeared and the footer is gone
|
||||
await retry.try(async function () {
|
||||
await testSubjects.missingOrFail(FOOTER_SELECTOR);
|
||||
});
|
||||
|
||||
// go to the last page
|
||||
await testSubjects.click('pagination-button-14');
|
||||
expect(await testSubjects.getVisibleText(FOOTER_SELECTOR)).to.be(
|
||||
'Search results are limited to 1,500 documents.\nLoad more'
|
||||
);
|
||||
});
|
||||
|
||||
it('should disable "Load more" button when refresh interval is on', async () => {
|
||||
// go to the last page
|
||||
await testSubjects.click('pagination-button-4');
|
||||
await retry.try(async function () {
|
||||
await testSubjects.existOrFail(FOOTER_SELECTOR);
|
||||
});
|
||||
|
||||
expect(await testSubjects.isEnabled(LOAD_MORE_SELECTOR)).to.be(true);
|
||||
|
||||
// enable the refresh interval
|
||||
await PageObjects.timePicker.startAutoRefresh(10);
|
||||
|
||||
// the button is disabled now
|
||||
await retry.waitFor('disabled state', async function () {
|
||||
return (await testSubjects.isEnabled(LOAD_MORE_SELECTOR)) === false;
|
||||
});
|
||||
|
||||
// disable the refresh interval
|
||||
await PageObjects.timePicker.pauseAutoRefresh();
|
||||
|
||||
// the button is enabled again
|
||||
await retry.waitFor('enabled state', async function () {
|
||||
return (await testSubjects.isEnabled(LOAD_MORE_SELECTOR)) === true;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('time field with date nano type', function () {
|
||||
before(async () => {
|
||||
await security.testUser.setRoles(['kibana_admin', 'kibana_date_nanos']);
|
||||
await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/date_nanos');
|
||||
await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/date_nanos');
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await esArchiver.unload('test/functional/fixtures/es_archiver/date_nanos');
|
||||
await kibanaServer.importExport.unload('test/functional/fixtures/kbn_archiver/date_nanos');
|
||||
await kibanaServer.savedObjects.cleanStandardList();
|
||||
await kibanaServer.uiSettings.replace({});
|
||||
});
|
||||
|
||||
beforeEach(async function () {
|
||||
await PageObjects.common.setTime({
|
||||
from: 'Sep 10, 2015 @ 00:00:00.000',
|
||||
to: 'Sep 30, 2019 @ 00:00:00.000',
|
||||
});
|
||||
await kibanaServer.uiSettings.update({
|
||||
defaultIndex: 'date-nanos',
|
||||
'discover:sampleSize': 4,
|
||||
'discover:sampleRowsPerPage': 2,
|
||||
});
|
||||
await PageObjects.common.navigateToApp('discover');
|
||||
await PageObjects.discover.waitUntilSearchingHasFinished();
|
||||
});
|
||||
|
||||
it('should work for date nanos too', async () => {
|
||||
await PageObjects.unifiedFieldList.clickFieldListItemAdd('_id');
|
||||
await PageObjects.discover.waitUntilSearchingHasFinished();
|
||||
|
||||
expect(await dataGrid.getRowsText()).to.eql([
|
||||
'Sep 22, 2019 @ 23:50:13.253123345AU_x3-TaGFA8no6QjiSJ',
|
||||
'Sep 18, 2019 @ 06:50:13.000000104AU_x3-TaGFA8no6Qjis104Z',
|
||||
]);
|
||||
|
||||
// footer is not shown
|
||||
await testSubjects.missingOrFail(FOOTER_SELECTOR);
|
||||
|
||||
// go to the last page
|
||||
await testSubjects.click('pagination-button-1');
|
||||
await retry.try(async function () {
|
||||
await testSubjects.existOrFail(FOOTER_SELECTOR);
|
||||
});
|
||||
expect(await testSubjects.getVisibleText(FOOTER_SELECTOR)).to.be(
|
||||
'Search results are limited to 4 documents.\nLoad more'
|
||||
);
|
||||
expect(await dataGrid.getRowsText()).to.eql([
|
||||
'Sep 18, 2019 @ 06:50:13.000000103BU_x3-TaGFA8no6Qjis103Z',
|
||||
'Sep 18, 2019 @ 06:50:13.000000102AU_x3-TaGFA8no6Qji102Z',
|
||||
]);
|
||||
|
||||
// there is no other pages to see yet
|
||||
await testSubjects.missingOrFail('pagination-button-2');
|
||||
|
||||
// press "Load more"
|
||||
await testSubjects.click(LOAD_MORE_SELECTOR);
|
||||
await PageObjects.discover.waitUntilSearchingHasFinished();
|
||||
|
||||
// more pages appeared and the footer is gone
|
||||
await retry.try(async function () {
|
||||
await testSubjects.missingOrFail(FOOTER_SELECTOR);
|
||||
});
|
||||
|
||||
// go to the last page
|
||||
await testSubjects.click('pagination-button-3');
|
||||
expect(await testSubjects.getVisibleText(FOOTER_SELECTOR)).to.be(
|
||||
'Search results are limited to 8 documents.\nLoad more'
|
||||
);
|
||||
|
||||
expect(await dataGrid.getRowsText()).to.eql([
|
||||
'Sep 18, 2019 @ 06:50:13.000000000CU_x3-TaGFA8no6QjiSX000Z',
|
||||
'Sep 18, 2019 @ 06:50:12.999999999AU_x3-TaGFA8no6Qj999Z',
|
||||
]);
|
||||
|
||||
// press "Load more"
|
||||
await testSubjects.click(LOAD_MORE_SELECTOR);
|
||||
await PageObjects.discover.waitUntilSearchingHasFinished();
|
||||
|
||||
// more pages appeared and the footer is gone
|
||||
await retry.try(async function () {
|
||||
await testSubjects.missingOrFail(FOOTER_SELECTOR);
|
||||
});
|
||||
|
||||
// go to the last page
|
||||
await testSubjects.click('pagination-button-4');
|
||||
await retry.try(async function () {
|
||||
await testSubjects.missingOrFail(FOOTER_SELECTOR);
|
||||
});
|
||||
|
||||
expect(await dataGrid.getRowsText()).to.eql([
|
||||
'Sep 19, 2015 @ 06:50:13.000100001AU_x3-TaGFA8no000100001Z',
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -30,6 +30,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) {
|
|||
loadTestFile(require.resolve('./_data_grid_doc_table'));
|
||||
loadTestFile(require.resolve('./_data_grid_copy_to_clipboard'));
|
||||
loadTestFile(require.resolve('./_data_grid_pagination'));
|
||||
loadTestFile(require.resolve('./_data_grid_footer'));
|
||||
loadTestFile(require.resolve('./_adhoc_data_views'));
|
||||
loadTestFile(require.resolve('./_sql_view'));
|
||||
loadTestFile(require.resolve('./_indexpattern_with_unmapped_fields'));
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue