diff --git a/packages/kbn-unified-data-table/src/components/data_table.test.tsx b/packages/kbn-unified-data-table/src/components/data_table.test.tsx
index c59149132cdf4..97bcc0e2c6654 100644
--- a/packages/kbn-unified-data-table/src/components/data_table.test.tsx
+++ b/packages/kbn-unified-data-table/src/components/data_table.test.tsx
@@ -10,6 +10,7 @@ import { ReactWrapper } from 'enzyme';
import {
EuiButton,
EuiCopy,
+ EuiDataGrid,
EuiDataGridCellValueElementProps,
EuiDataGridCustomBodyProps,
} from '@elastic/eui';
@@ -52,7 +53,7 @@ function getProps(): UnifiedDataTableProps {
onSetColumns: jest.fn(),
onSort: jest.fn(),
rows: esHitsMock.map((hit) => buildDataTableRecord(hit, dataViewMock)),
- sampleSize: 30,
+ sampleSizeState: 30,
searchDescription: '',
searchTitle: '',
setExpandedDoc: jest.fn(),
@@ -301,6 +302,74 @@ describe('UnifiedDataTable', () => {
});
});
+ describe('display settings', () => {
+ it('should include additional display settings if onUpdateSampleSize is provided', async () => {
+ const component = await getComponent({
+ ...getProps(),
+ sampleSizeState: 150,
+ onUpdateSampleSize: jest.fn(),
+ onUpdateRowHeight: jest.fn(),
+ });
+
+ expect(component.find(EuiDataGrid).prop('toolbarVisibility')).toMatchInlineSnapshot(`
+ Object {
+ "additionalControls": ,
+ "showColumnSelector": false,
+ "showDisplaySelector": Object {
+ "additionalDisplaySettings": ,
+ "allowDensity": false,
+ "allowResetButton": false,
+ "allowRowHeight": true,
+ },
+ "showFullScreenSelector": true,
+ "showSortSelector": true,
+ }
+ `);
+ });
+
+ it('should not include additional display settings if onUpdateSampleSize is not provided', async () => {
+ const component = await getComponent({
+ ...getProps(),
+ sampleSizeState: 200,
+ onUpdateRowHeight: jest.fn(),
+ });
+
+ expect(component.find(EuiDataGrid).prop('toolbarVisibility')).toMatchInlineSnapshot(`
+ Object {
+ "additionalControls": ,
+ "showColumnSelector": false,
+ "showDisplaySelector": Object {
+ "allowDensity": false,
+ "allowRowHeight": true,
+ },
+ "showFullScreenSelector": true,
+ "showSortSelector": true,
+ }
+ `);
+ });
+
+ it('should hide display settings if no handlers provided', async () => {
+ const component = await getComponent({
+ ...getProps(),
+ onUpdateRowHeight: undefined,
+ onUpdateSampleSize: undefined,
+ });
+
+ expect(component.find(EuiDataGrid).prop('toolbarVisibility')).toMatchInlineSnapshot(`
+ Object {
+ "additionalControls": ,
+ "showColumnSelector": false,
+ "showDisplaySelector": undefined,
+ "showFullScreenSelector": true,
+ "showSortSelector": true,
+ }
+ `);
+ });
+ });
+
describe('externalControlColumns', () => {
it('should render external leading control columns', async () => {
const component = await getComponent({
diff --git a/packages/kbn-unified-data-table/src/components/data_table.tsx b/packages/kbn-unified-data-table/src/components/data_table.tsx
index a1540e88a5cd6..22a625f479e3b 100644
--- a/packages/kbn-unified-data-table/src/components/data_table.tsx
+++ b/packages/kbn-unified-data-table/src/components/data_table.tsx
@@ -27,6 +27,7 @@ import {
EuiDataGridControlColumn,
EuiDataGridCustomBodyProps,
EuiDataGridCellValueElementProps,
+ EuiDataGridToolBarVisibilityDisplaySelectorOptions,
EuiDataGridStyle,
} from '@elastic/eui';
import type { DataView } from '@kbn/data-views-plugin/public';
@@ -63,6 +64,7 @@ import {
toolbarVisibility as toolbarVisibilityDefaults,
} from '../constants';
import { UnifiedDataTableFooter } from './data_table_footer';
+import { UnifiedDataTableAdditionalDisplaySettings } from './data_table_additional_display_settings';
export type SortOrder = [string, string];
@@ -137,10 +139,6 @@ export interface UnifiedDataTableProps {
* Array of documents provided by Elasticsearch
*/
rows?: DataTableRecord[];
- /**
- * The max size of the documents returned by Elasticsearch
- */
- sampleSize: number;
/**
* Function to set the expanded document, which is displayed in a flyout
*/
@@ -205,6 +203,18 @@ export interface UnifiedDataTableProps {
* Update rows per page state
*/
onUpdateRowsPerPage?: (rowsPerPage: number) => void;
+ /**
+ * Configuration option to limit sample size slider
+ */
+ maxAllowedSampleSize?: number;
+ /**
+ * The max size of the documents returned by Elasticsearch
+ */
+ sampleSizeState: number;
+ /**
+ * Update rows per page state
+ */
+ onUpdateSampleSize?: (sampleSize: number) => void;
/**
* Callback to execute on edit runtime field
*/
@@ -328,7 +338,6 @@ export const UnifiedDataTable = ({
onSetColumns,
onSort,
rows,
- sampleSize,
searchDescription,
searchTitle,
settings,
@@ -342,6 +351,9 @@ export const UnifiedDataTable = ({
className,
rowHeightState,
onUpdateRowHeight,
+ maxAllowedSampleSize,
+ sampleSizeState,
+ onUpdateSampleSize,
isPlainRecord = false,
rowsPerPageState,
onUpdateRowsPerPage,
@@ -715,16 +727,27 @@ export const UnifiedDataTable = ({
[usedSelectedDocs, isFilterActive, rows, externalAdditionalControls]
);
- const showDisplaySelector = useMemo(
- () =>
- !!onUpdateRowHeight
- ? {
- allowDensity: false,
- allowRowHeight: true,
- }
- : undefined,
- [onUpdateRowHeight]
- );
+ const showDisplaySelector = useMemo(() => {
+ const options: EuiDataGridToolBarVisibilityDisplaySelectorOptions = {};
+
+ if (onUpdateRowHeight) {
+ options.allowDensity = false;
+ options.allowRowHeight = true;
+ }
+
+ if (onUpdateSampleSize) {
+ options.allowResetButton = false;
+ options.additionalDisplaySettings = (
+
+ );
+ }
+
+ return Object.keys(options).length ? options : undefined;
+ }, [maxAllowedSampleSize, sampleSizeState, onUpdateRowHeight, onUpdateSampleSize]);
const inMemory = useMemo(() => {
return isPlainRecord && columns.length
@@ -837,7 +860,7 @@ export const UnifiedDataTable = ({
fn);
+
+describe('UnifiedDataTableAdditionalDisplaySettings', function () {
+ describe('sampleSize', function () {
+ it('should work correctly', async () => {
+ const onChangeSampleSizeMock = jest.fn();
+
+ const component = mountWithIntl(
+
+ );
+ const input = findTestSubject(component, 'unifiedDataTableSampleSizeInput').last();
+ expect(input.prop('value')).toBe(10);
+
+ await act(async () => {
+ input.simulate('change', {
+ target: {
+ value: 100,
+ },
+ });
+ });
+
+ expect(onChangeSampleSizeMock).toHaveBeenCalledWith(100);
+
+ await new Promise((resolve) => setTimeout(resolve, 0));
+ component.update();
+
+ expect(
+ findTestSubject(component, 'unifiedDataTableSampleSizeInput').last().prop('value')
+ ).toBe(100);
+ });
+
+ it('should not execute the callback for an invalid input', async () => {
+ const invalidValue = 600;
+ const onChangeSampleSizeMock = jest.fn();
+
+ const component = mountWithIntl(
+
+ );
+ const input = findTestSubject(component, 'unifiedDataTableSampleSizeInput').last();
+ expect(input.prop('value')).toBe(50);
+
+ await act(async () => {
+ input.simulate('change', {
+ target: {
+ value: invalidValue,
+ },
+ });
+ });
+
+ await new Promise((resolve) => setTimeout(resolve, 0));
+ component.update();
+
+ expect(
+ findTestSubject(component, 'unifiedDataTableSampleSizeInput').last().prop('value')
+ ).toBe(invalidValue);
+
+ expect(onChangeSampleSizeMock).not.toHaveBeenCalled();
+ });
+
+ it('should render value changes correctly', async () => {
+ const onChangeSampleSizeMock = jest.fn();
+
+ const component = mountWithIntl(
+
+ );
+
+ expect(
+ findTestSubject(component, 'unifiedDataTableSampleSizeInput').last().prop('value')
+ ).toBe(200);
+
+ component.setProps({
+ sampleSize: 500,
+ onChangeSampleSize: onChangeSampleSizeMock,
+ });
+
+ await new Promise((resolve) => setTimeout(resolve, 0));
+ component.update();
+
+ expect(
+ findTestSubject(component, 'unifiedDataTableSampleSizeInput').last().prop('value')
+ ).toBe(500);
+
+ expect(onChangeSampleSizeMock).not.toHaveBeenCalled();
+ });
+ });
+});
diff --git a/packages/kbn-unified-data-table/src/components/data_table_additional_display_settings.tsx b/packages/kbn-unified-data-table/src/components/data_table_additional_display_settings.tsx
new file mode 100644
index 0000000000000..2555c5f253929
--- /dev/null
+++ b/packages/kbn-unified-data-table/src/components/data_table_additional_display_settings.tsx
@@ -0,0 +1,85 @@
+/*
+ * 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, { useCallback, useEffect, useMemo, useState } from 'react';
+import { EuiFormRow, EuiRange } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import { debounce } from 'lodash';
+
+export const DEFAULT_MAX_ALLOWED_SAMPLE_SIZE = 1000;
+export const MIN_ALLOWED_SAMPLE_SIZE = 1;
+export const RANGE_MIN_SAMPLE_SIZE = 10; // it's necessary to be able to use `step={10}` configuration for EuiRange
+export const RANGE_STEP_SAMPLE_SIZE = 10;
+
+export interface UnifiedDataTableAdditionalDisplaySettingsProps {
+ maxAllowedSampleSize?: number;
+ sampleSize: number;
+ onChangeSampleSize: (sampleSize: number) => void;
+}
+
+export const UnifiedDataTableAdditionalDisplaySettings: React.FC<
+ UnifiedDataTableAdditionalDisplaySettingsProps
+> = ({
+ maxAllowedSampleSize = DEFAULT_MAX_ALLOWED_SAMPLE_SIZE,
+ sampleSize,
+ onChangeSampleSize,
+}) => {
+ const [activeSampleSize, setActiveSampleSize] = useState(sampleSize);
+ const minRangeSampleSize = Math.max(
+ Math.min(RANGE_MIN_SAMPLE_SIZE, sampleSize),
+ MIN_ALLOWED_SAMPLE_SIZE
+ ); // flexible: allows to go lower than RANGE_MIN_SAMPLE_SIZE but greater than MIN_ALLOWED_SAMPLE_SIZE
+
+ const debouncedOnChangeSampleSize = useMemo(
+ () => debounce(onChangeSampleSize, 300, { leading: false, trailing: true }),
+ [onChangeSampleSize]
+ );
+
+ const onChangeActiveSampleSize = useCallback(
+ (event) => {
+ if (!event.target.value) {
+ setActiveSampleSize('');
+ return;
+ }
+
+ const newSampleSize = Number(event.target.value);
+
+ if (newSampleSize >= MIN_ALLOWED_SAMPLE_SIZE) {
+ setActiveSampleSize(newSampleSize);
+ if (newSampleSize <= maxAllowedSampleSize) {
+ debouncedOnChangeSampleSize(newSampleSize);
+ }
+ }
+ },
+ [maxAllowedSampleSize, setActiveSampleSize, debouncedOnChangeSampleSize]
+ );
+
+ const sampleSizeLabel = i18n.translate('unifiedDataTable.sampleSizeSettings.sampleSizeLabel', {
+ defaultMessage: 'Sample size',
+ });
+
+ useEffect(() => {
+ setActiveSampleSize(sampleSize); // reset local state
+ }, [sampleSize, setActiveSampleSize]);
+
+ return (
+
+
+
+ );
+};
diff --git a/src/core/server/integration_tests/saved_objects/migrations/group2/check_registered_types.test.ts b/src/core/server/integration_tests/saved_objects/migrations/group2/check_registered_types.test.ts
index 62780b66727ef..73fef09887c69 100644
--- a/src/core/server/integration_tests/saved_objects/migrations/group2/check_registered_types.test.ts
+++ b/src/core/server/integration_tests/saved_objects/migrations/group2/check_registered_types.test.ts
@@ -133,7 +133,7 @@ describe('checking migration metadata changes on all registered SO types', () =>
"risk-engine-configuration": "b105d4a3c6adce40708d729d12e5ef3c8fbd9508",
"rules-settings": "892a2918ebaeba809a612b8d97cec0b07c800b5f",
"sample-data-telemetry": "37441b12f5b0159c2d6d5138a494c9f440e950b5",
- "search": "8d5184dd5b986d57250b6ffd9ae48a1925e4c7a3",
+ "search": "2c1ab8a17e6972be2fa8d3880ba2305dfd9a5a6e",
"search-session": "b2fcd840e12a45039ada50b1355faeafa39876d1",
"search-telemetry": "b568601618744720b5662946d3103e3fb75fe8ee",
"security-rule": "07abb4d7e707d91675ec0495c73816394c7b521f",
diff --git a/src/plugins/discover/public/application/context/context_app_content.tsx b/src/plugins/discover/public/application/context/context_app_content.tsx
index ff99c46816f25..81ca3e6f81b66 100644
--- a/src/plugins/discover/public/application/context/context_app_content.tsx
+++ b/src/plugins/discover/public/application/context/context_app_content.tsx
@@ -197,7 +197,7 @@ export function ContextAppContent({
dataView={dataView}
expandedDoc={expandedDoc}
loadingState={isAnchorLoading ? DataLoadingState.loading : DataLoadingState.loaded}
- sampleSize={0}
+ sampleSizeState={0}
sort={sort as SortOrder[]}
isSortEnabled={false}
showTimeCol={showTimeCol}
diff --git a/src/plugins/discover/public/application/main/components/layout/discover_documents.tsx b/src/plugins/discover/public/application/main/components/layout/discover_documents.tsx
index d1c772e7ec1bf..60367b83d02ed 100644
--- a/src/plugins/discover/public/application/main/components/layout/discover_documents.tsx
+++ b/src/plugins/discover/public/application/main/components/layout/discover_documents.tsx
@@ -34,7 +34,6 @@ import {
HIDE_ANNOUNCEMENTS,
MAX_DOC_FIELDS_DISPLAYED,
ROW_HEIGHT_OPTION,
- SAMPLE_SIZE_SETTING,
SEARCH_FIELDS_FROM_SOURCE,
SHOW_MULTIFIELDS,
SORT_DEFAULT_ORDER_SETTING,
@@ -56,6 +55,10 @@ import {
DiscoverTourProvider,
} from '../../../../components/discover_tour';
import { getRawRecordType } from '../../utils/get_raw_record_type';
+import {
+ getMaxAllowedSampleSize,
+ getAllowedSampleSize,
+} from '../../../../utils/get_allowed_sample_size';
import { DiscoverGridFlyout } from '../../../../components/discover_grid_flyout';
import { useSavedSearchInitial } from '../../services/discover_state_provider';
import { useFetchMoreRecords } from './use_fetch_more_records';
@@ -103,8 +106,8 @@ function DiscoverDocumentsComponent({
const documents$ = stateContainer.dataState.data$.documents$;
const savedSearch = useSavedSearchInitial();
const { dataViews, capabilities, uiSettings, uiActions } = services;
- const [query, sort, rowHeight, rowsPerPage, grid, columns, index] = useAppStateSelector(
- (state) => {
+ const [query, sort, rowHeight, rowsPerPage, grid, columns, index, sampleSizeState] =
+ useAppStateSelector((state) => {
return [
state.query,
state.sort,
@@ -113,9 +116,9 @@ function DiscoverDocumentsComponent({
state.grid,
state.columns,
state.index,
+ state.sampleSize,
];
- }
- );
+ });
const setExpandedDoc = useCallback(
(doc: DataTableRecord | undefined) => {
stateContainer.internalState.transitions.setExpandedDoc(doc);
@@ -128,7 +131,6 @@ function DiscoverDocumentsComponent({
const useNewFieldsApi = useMemo(() => !uiSettings.get(SEARCH_FIELDS_FROM_SOURCE), [uiSettings]);
const hideAnnouncements = useMemo(() => uiSettings.get(HIDE_ANNOUNCEMENTS), [uiSettings]);
const isLegacy = useMemo(() => uiSettings.get(DOC_TABLE_LEGACY), [uiSettings]);
- const sampleSize = useMemo(() => uiSettings.get(SAMPLE_SIZE_SETTING), [uiSettings]);
const documentState = useDataState(documents$);
const isDataLoading =
@@ -183,6 +185,13 @@ function DiscoverDocumentsComponent({
[stateContainer]
);
+ const onUpdateSampleSize = useCallback(
+ (newSampleSize: number) => {
+ stateContainer.appState.update({ sampleSize: newSampleSize });
+ },
+ [stateContainer]
+ );
+
const onSort = useCallback(
(nextSort: string[][]) => {
stateContainer.appState.update({ sort: nextSort });
@@ -315,7 +324,6 @@ function DiscoverDocumentsComponent({
}
rows={rows}
sort={(sort as SortOrder[]) || []}
- sampleSize={sampleSize}
searchDescription={savedSearch.description}
searchTitle={savedSearch.title}
setExpandedDoc={setExpandedDoc}
@@ -332,6 +340,9 @@ function DiscoverDocumentsComponent({
isPlainRecord={isTextBasedQuery}
rowsPerPageState={rowsPerPage ?? getDefaultRowsPerPage(services.uiSettings)}
onUpdateRowsPerPage={onUpdateRowsPerPage}
+ maxAllowedSampleSize={getMaxAllowedSampleSize(services.uiSettings)}
+ sampleSizeState={getAllowedSampleSize(sampleSizeState, services.uiSettings)}
+ onUpdateSampleSize={!isTextBasedQuery ? onUpdateSampleSize : undefined}
onFieldEdited={onFieldEdited}
configRowHeight={uiSettings.get(ROW_HEIGHT_OPTION)}
showMultiFields={uiSettings.get(SHOW_MULTIFIELDS)}
diff --git a/src/plugins/discover/public/application/main/components/top_nav/on_save_search.tsx b/src/plugins/discover/public/application/main/components/top_nav/on_save_search.tsx
index 4d1b9ccbdc22d..abae8e83b41a1 100644
--- a/src/plugins/discover/public/application/main/components/top_nav/on_save_search.tsx
+++ b/src/plugins/discover/public/application/main/components/top_nav/on_save_search.tsx
@@ -15,6 +15,7 @@ import { SavedSearch, SaveSavedSearchOptions } from '@kbn/saved-search-plugin/pu
import { DOC_TABLE_LEGACY } from '@kbn/discover-utils';
import { DiscoverServices } from '../../../../build_services';
import { DiscoverStateContainer } from '../../services/discover_state';
+import { getAllowedSampleSize } from '../../../../utils/get_allowed_sample_size';
async function saveDataSource({
savedSearch,
@@ -110,6 +111,7 @@ export async function onSaveSearch({
const currentTitle = savedSearch.title;
const currentTimeRestore = savedSearch.timeRestore;
const currentRowsPerPage = savedSearch.rowsPerPage;
+ const currentSampleSize = savedSearch.sampleSize;
const currentDescription = savedSearch.description;
const currentTags = savedSearch.tags;
savedSearch.title = newTitle;
@@ -118,6 +120,15 @@ export async function onSaveSearch({
savedSearch.rowsPerPage = uiSettings.get(DOC_TABLE_LEGACY)
? currentRowsPerPage
: state.appState.getState().rowsPerPage;
+
+ // save the custom value or reset it if it's invalid
+ const appStateSampleSize = state.appState.getState().sampleSize;
+ const allowedSampleSize = getAllowedSampleSize(appStateSampleSize, uiSettings);
+ savedSearch.sampleSize =
+ appStateSampleSize && allowedSampleSize === appStateSampleSize
+ ? appStateSampleSize
+ : undefined;
+
if (savedObjectsTagging) {
savedSearch.tags = newTags;
}
@@ -144,6 +155,7 @@ export async function onSaveSearch({
savedSearch.title = currentTitle;
savedSearch.timeRestore = currentTimeRestore;
savedSearch.rowsPerPage = currentRowsPerPage;
+ savedSearch.sampleSize = currentSampleSize;
savedSearch.description = currentDescription;
if (savedObjectsTagging) {
savedSearch.tags = currentTags;
diff --git a/src/plugins/discover/public/application/main/hooks/utils/build_state_subscribe.ts b/src/plugins/discover/public/application/main/hooks/utils/build_state_subscribe.ts
index 40838edd35c35..27407822553bb 100644
--- a/src/plugins/discover/public/application/main/hooks/utils/build_state_subscribe.ts
+++ b/src/plugins/discover/public/application/main/hooks/utils/build_state_subscribe.ts
@@ -53,7 +53,7 @@ export const buildStateSubscribe =
return;
}
addLog('[appstate] subscribe triggered', nextState);
- const { hideChart, interval, breakdownField, sort, index } = prevState;
+ const { hideChart, interval, breakdownField, sampleSize, sort, index } = prevState;
const isTextBasedQueryLang = isTextBasedQuery(nextQuery);
if (isTextBasedQueryLang) {
@@ -68,6 +68,7 @@ export const buildStateSubscribe =
const chartDisplayChanged = Boolean(nextState.hideChart) !== Boolean(hideChart);
const chartIntervalChanged = nextState.interval !== interval && !isTextBasedQueryLang;
const breakdownFieldChanged = nextState.breakdownField !== breakdownField;
+ const sampleSizeChanged = nextState.sampleSize !== sampleSize;
const docTableSortChanged = !isEqual(nextState.sort, sort) && !isTextBasedQueryLang;
const dataViewChanged = !isEqual(nextState.index, index) && !isTextBasedQueryLang;
let savedSearchDataView;
@@ -101,6 +102,7 @@ export const buildStateSubscribe =
chartDisplayChanged ||
chartIntervalChanged ||
breakdownFieldChanged ||
+ sampleSizeChanged ||
docTableSortChanged ||
dataViewChanged ||
queryChanged
diff --git a/src/plugins/discover/public/application/main/services/discover_app_state_container.ts b/src/plugins/discover/public/application/main/services/discover_app_state_container.ts
index 046e8fd6393f1..124c83beda236 100644
--- a/src/plugins/discover/public/application/main/services/discover_app_state_container.ts
+++ b/src/plugins/discover/public/application/main/services/discover_app_state_container.ts
@@ -134,6 +134,10 @@ export interface DiscoverAppState {
* Number of rows in the grid per page
*/
rowsPerPage?: number;
+ /**
+ * Custom sample size
+ */
+ sampleSize?: number;
/**
* Breakdown field of chart
*/
@@ -299,7 +303,7 @@ export function getInitialState(
? defaultAppState
: {
...defaultAppState,
- ...cleanupUrlState(stateStorageURL),
+ ...cleanupUrlState(stateStorageURL, services.uiSettings),
},
services.uiSettings
);
diff --git a/src/plugins/discover/public/application/main/services/load_saved_search.ts b/src/plugins/discover/public/application/main/services/load_saved_search.ts
index 3631ca876cce6..8b8dcc2beb2f4 100644
--- a/src/plugins/discover/public/application/main/services/load_saved_search.ts
+++ b/src/plugins/discover/public/application/main/services/load_saved_search.ts
@@ -91,7 +91,7 @@ export const loadSavedSearch = async (
// Update app state container with the next state derived from the next saved search
const nextAppState = getInitialState(undefined, nextSavedSearch, services);
const mergedAppState = appState
- ? { ...nextAppState, ...cleanupUrlState({ ...appState }) }
+ ? { ...nextAppState, ...cleanupUrlState({ ...appState }, services.uiSettings) }
: nextAppState;
appStateContainer.resetToState(mergedAppState);
diff --git a/src/plugins/discover/public/application/main/utils/cleanup_url_state.test.ts b/src/plugins/discover/public/application/main/utils/cleanup_url_state.test.ts
index ea1af49f48e89..2d49639e02884 100644
--- a/src/plugins/discover/public/application/main/utils/cleanup_url_state.test.ts
+++ b/src/plugins/discover/public/application/main/utils/cleanup_url_state.test.ts
@@ -8,11 +8,14 @@
import { AppStateUrl } from '../services/discover_app_state_container';
import { cleanupUrlState } from './cleanup_url_state';
+import { createDiscoverServicesMock } from '../../../__mocks__/services';
+
+const services = createDiscoverServicesMock();
describe('cleanupUrlState', () => {
test('cleaning up legacy sort', async () => {
const state = { sort: ['batman', 'desc'] } as AppStateUrl;
- expect(cleanupUrlState(state)).toMatchInlineSnapshot(`
+ expect(cleanupUrlState(state, services.uiSettings)).toMatchInlineSnapshot(`
Object {
"sort": Array [
Array [
@@ -25,7 +28,7 @@ describe('cleanupUrlState', () => {
});
test('not cleaning up broken legacy sort', async () => {
const state = { sort: ['batman'] } as unknown as AppStateUrl;
- expect(cleanupUrlState(state)).toMatchInlineSnapshot(`Object {}`);
+ expect(cleanupUrlState(state, services.uiSettings)).toMatchInlineSnapshot(`Object {}`);
});
test('not cleaning up regular sort', async () => {
const state = {
@@ -34,7 +37,7 @@ describe('cleanupUrlState', () => {
['robin', 'asc'],
],
} as AppStateUrl;
- expect(cleanupUrlState(state)).toMatchInlineSnapshot(`
+ expect(cleanupUrlState(state, services.uiSettings)).toMatchInlineSnapshot(`
Object {
"sort": Array [
Array [
@@ -53,14 +56,14 @@ describe('cleanupUrlState', () => {
const state = {
sort: [],
} as AppStateUrl;
- expect(cleanupUrlState(state)).toMatchInlineSnapshot(`Object {}`);
+ expect(cleanupUrlState(state, services.uiSettings)).toMatchInlineSnapshot(`Object {}`);
});
test('should keep a valid rowsPerPage', async () => {
const state = {
rowsPerPage: 50,
} as AppStateUrl;
- expect(cleanupUrlState(state)).toMatchInlineSnapshot(`
+ expect(cleanupUrlState(state, services.uiSettings)).toMatchInlineSnapshot(`
Object {
"rowsPerPage": 50,
}
@@ -71,13 +74,63 @@ describe('cleanupUrlState', () => {
const state = {
rowsPerPage: -50,
} as AppStateUrl;
- expect(cleanupUrlState(state)).toMatchInlineSnapshot(`Object {}`);
+ expect(cleanupUrlState(state, services.uiSettings)).toMatchInlineSnapshot(`Object {}`);
});
test('should remove an invalid rowsPerPage', async () => {
const state = {
rowsPerPage: 'test',
} as unknown as AppStateUrl;
- expect(cleanupUrlState(state)).toMatchInlineSnapshot(`Object {}`);
+ expect(cleanupUrlState(state, services.uiSettings)).toMatchInlineSnapshot(`Object {}`);
+ });
+
+ describe('sampleSize', function () {
+ test('should keep a valid sampleSize', async () => {
+ const state = {
+ sampleSize: 50,
+ } as AppStateUrl;
+ expect(cleanupUrlState(state, services.uiSettings)).toMatchInlineSnapshot(`
+ Object {
+ "sampleSize": 50,
+ }
+ `);
+ });
+
+ test('should remove for ES|QL', async () => {
+ const state = {
+ sampleSize: 50,
+ query: {
+ esql: 'from test',
+ },
+ } as AppStateUrl;
+ expect(cleanupUrlState(state, services.uiSettings)).toMatchInlineSnapshot(`
+ Object {
+ "query": Object {
+ "esql": "from test",
+ },
+ }
+ `);
+ });
+
+ test('should remove a negative sampleSize', async () => {
+ const state = {
+ sampleSize: -50,
+ } as AppStateUrl;
+ expect(cleanupUrlState(state, services.uiSettings)).toMatchInlineSnapshot(`Object {}`);
+ });
+
+ test('should remove an invalid sampleSize', async () => {
+ const state = {
+ sampleSize: 'test',
+ } as unknown as AppStateUrl;
+ expect(cleanupUrlState(state, services.uiSettings)).toMatchInlineSnapshot(`Object {}`);
+ });
+
+ test('should remove a too large sampleSize', async () => {
+ const state = {
+ sampleSize: 500000,
+ } as AppStateUrl;
+ expect(cleanupUrlState(state, services.uiSettings)).toMatchInlineSnapshot(`Object {}`);
+ });
});
});
diff --git a/src/plugins/discover/public/application/main/utils/cleanup_url_state.ts b/src/plugins/discover/public/application/main/utils/cleanup_url_state.ts
index 3abeed97d4cdc..cdfb95d87f134 100644
--- a/src/plugins/discover/public/application/main/utils/cleanup_url_state.ts
+++ b/src/plugins/discover/public/application/main/utils/cleanup_url_state.ts
@@ -6,14 +6,19 @@
* Side Public License, v 1.
*/
import { isOfAggregateQueryType } from '@kbn/es-query';
+import type { IUiSettingsClient } from '@kbn/core-ui-settings-browser';
import { DiscoverAppState, AppStateUrl } from '../services/discover_app_state_container';
import { migrateLegacyQuery } from '../../../utils/migrate_legacy_query';
+import { getMaxAllowedSampleSize } from '../../../utils/get_allowed_sample_size';
/**
* Takes care of the given url state, migrates legacy props and cleans up empty props
* @param appStateFromUrl
*/
-export function cleanupUrlState(appStateFromUrl: AppStateUrl): DiscoverAppState {
+export function cleanupUrlState(
+ appStateFromUrl: AppStateUrl,
+ uiSettings: IUiSettingsClient
+): DiscoverAppState {
if (
appStateFromUrl &&
appStateFromUrl.query &&
@@ -46,5 +51,18 @@ export function cleanupUrlState(appStateFromUrl: AppStateUrl): DiscoverAppState
delete appStateFromUrl.rowsPerPage;
}
+ if (
+ appStateFromUrl?.sampleSize &&
+ (isOfAggregateQueryType(appStateFromUrl.query) || // not supported yet for ES|QL
+ !(
+ typeof appStateFromUrl.sampleSize === 'number' &&
+ appStateFromUrl.sampleSize > 0 &&
+ appStateFromUrl.sampleSize <= getMaxAllowedSampleSize(uiSettings)
+ ))
+ ) {
+ // remove the param if it's invalid
+ delete appStateFromUrl.sampleSize;
+ }
+
return appStateFromUrl as DiscoverAppState;
}
diff --git a/src/plugins/discover/public/application/main/utils/fetch_documents.test.ts b/src/plugins/discover/public/application/main/utils/fetch_documents.test.ts
index cbc62a3cd6068..36847a0a08929 100644
--- a/src/plugins/discover/public/application/main/utils/fetch_documents.test.ts
+++ b/src/plugins/discover/public/application/main/utils/fetch_documents.test.ts
@@ -26,6 +26,7 @@ const getDeps = () =>
searchSessionId: '123',
services: discoverServiceMock,
savedSearch: savedSearchMock,
+ getAppState: () => ({ sampleSize: 100 }),
} as unknown as FetchDeps);
describe('test fetchDocuments', () => {
diff --git a/src/plugins/discover/public/application/main/utils/fetch_documents.ts b/src/plugins/discover/public/application/main/utils/fetch_documents.ts
index 99e87f13558a8..b1e18273479bf 100644
--- a/src/plugins/discover/public/application/main/utils/fetch_documents.ts
+++ b/src/plugins/discover/public/application/main/utils/fetch_documents.ts
@@ -9,10 +9,11 @@ import { i18n } from '@kbn/i18n';
import { filter, map } from 'rxjs/operators';
import { lastValueFrom } from 'rxjs';
import { isRunningResponse, ISearchSource } from '@kbn/data-plugin/public';
-import { SAMPLE_SIZE_SETTING, buildDataTableRecordList } from '@kbn/discover-utils';
+import { buildDataTableRecordList } from '@kbn/discover-utils';
import type { EsHitRecord } from '@kbn/discover-utils/types';
import { getSearchResponseInterceptedWarnings } from '@kbn/search-response-warnings';
import type { RecordsFetchResponse } from '../../types';
+import { getAllowedSampleSize } from '../../../utils/get_allowed_sample_size';
import { FetchDeps } from './fetch_all';
/**
@@ -21,9 +22,10 @@ import { FetchDeps } from './fetch_all';
*/
export const fetchDocuments = (
searchSource: ISearchSource,
- { abortController, inspectorAdapters, searchSessionId, services }: FetchDeps
+ { abortController, inspectorAdapters, searchSessionId, services, getAppState }: FetchDeps
): Promise => {
- searchSource.setField('size', services.uiSettings.get(SAMPLE_SIZE_SETTING));
+ const sampleSize = getAppState().sampleSize;
+ searchSource.setField('size', getAllowedSampleSize(sampleSize, services.uiSettings));
searchSource.setField('trackTotalHits', false);
searchSource.setField('highlightAll', true);
searchSource.setField('version', true);
diff --git a/src/plugins/discover/public/application/main/utils/get_state_defaults.test.ts b/src/plugins/discover/public/application/main/utils/get_state_defaults.test.ts
index 19e9f6a64c88b..a659f543f9993 100644
--- a/src/plugins/discover/public/application/main/utils/get_state_defaults.test.ts
+++ b/src/plugins/discover/public/application/main/utils/get_state_defaults.test.ts
@@ -36,6 +36,7 @@ describe('getStateDefaults', () => {
"query": undefined,
"rowHeight": undefined,
"rowsPerPage": undefined,
+ "sampleSize": undefined,
"savedQuery": undefined,
"sort": Array [
Array [
@@ -70,6 +71,7 @@ describe('getStateDefaults', () => {
"query": undefined,
"rowHeight": undefined,
"rowsPerPage": undefined,
+ "sampleSize": undefined,
"savedQuery": undefined,
"sort": Array [],
"viewMode": undefined,
diff --git a/src/plugins/discover/public/application/main/utils/get_state_defaults.ts b/src/plugins/discover/public/application/main/utils/get_state_defaults.ts
index 78c8946825374..943d9b4c98cf0 100644
--- a/src/plugins/discover/public/application/main/utils/get_state_defaults.ts
+++ b/src/plugins/discover/public/application/main/utils/get_state_defaults.ts
@@ -70,6 +70,7 @@ export function getStateDefaults({
savedQuery: undefined,
rowHeight: undefined,
rowsPerPage: undefined,
+ sampleSize: undefined,
grid: undefined,
breakdownField: undefined,
};
@@ -94,7 +95,9 @@ export function getStateDefaults({
if (savedSearch.rowsPerPage) {
defaultState.rowsPerPage = savedSearch.rowsPerPage;
}
-
+ if (savedSearch.sampleSize) {
+ defaultState.sampleSize = savedSearch.sampleSize;
+ }
if (savedSearch.breakdownField) {
defaultState.breakdownField = savedSearch.breakdownField;
}
diff --git a/src/plugins/discover/public/components/doc_table/create_doc_table_embeddable.tsx b/src/plugins/discover/public/components/doc_table/create_doc_table_embeddable.tsx
index e0f24c2839113..a0a55a17a9cba 100644
--- a/src/plugins/discover/public/components/doc_table/create_doc_table_embeddable.tsx
+++ b/src/plugins/discover/public/components/doc_table/create_doc_table_embeddable.tsx
@@ -17,6 +17,7 @@ export function DiscoverDocTableEmbeddable(renderProps: DocTableEmbeddableProps)
columns={renderProps.columns}
rows={renderProps.rows}
rowsPerPageState={renderProps.rowsPerPageState}
+ sampleSizeState={renderProps.sampleSizeState}
onUpdateRowsPerPage={renderProps.onUpdateRowsPerPage}
totalHitCount={renderProps.totalHitCount}
dataView={renderProps.dataView}
diff --git a/src/plugins/discover/public/components/doc_table/doc_table_embeddable.tsx b/src/plugins/discover/public/components/doc_table/doc_table_embeddable.tsx
index fbe8ed083ebc3..36e3629f089aa 100644
--- a/src/plugins/discover/public/components/doc_table/doc_table_embeddable.tsx
+++ b/src/plugins/discover/public/components/doc_table/doc_table_embeddable.tsx
@@ -10,19 +10,19 @@ import React, { memo, useCallback, useMemo, useRef } from 'react';
import './index.scss';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiText } from '@elastic/eui';
-import { SAMPLE_SIZE_SETTING, usePager } from '@kbn/discover-utils';
+import { usePager } from '@kbn/discover-utils';
import type { SearchResponseInterceptedWarning } from '@kbn/search-response-warnings';
import {
ToolBarPagination,
MAX_ROWS_PER_PAGE_OPTION,
} from './components/pager/tool_bar_pagination';
import { DocTableProps, DocTableRenderProps, DocTableWrapper } from './doc_table_wrapper';
-import { useDiscoverServices } from '../../hooks/use_discover_services';
import { SavedSearchEmbeddableBase } from '../../embeddable/saved_search_embeddable_base';
export interface DocTableEmbeddableProps extends DocTableProps {
totalHitCount?: number;
rowsPerPageState?: number;
+ sampleSizeState: number;
interceptedWarnings?: SearchResponseInterceptedWarning[];
onUpdateRowsPerPage?: (rowsPerPage?: number) => void;
}
@@ -30,7 +30,6 @@ export interface DocTableEmbeddableProps extends DocTableProps {
const DocTableWrapperMemoized = memo(DocTableWrapper);
export const DocTableEmbeddable = (props: DocTableEmbeddableProps) => {
- const services = useDiscoverServices();
const onUpdateRowsPerPage = props.onUpdateRowsPerPage;
const tableWrapperRef = useRef(null);
const {
@@ -83,10 +82,6 @@ export const DocTableEmbeddable = (props: DocTableEmbeddableProps) => {
[hasNextPage, props.rows.length, props.totalHitCount]
);
- const sampleSize = useMemo(() => {
- return services.uiSettings.get(SAMPLE_SIZE_SETTING, 500);
- }, [services]);
-
const renderDocTable = useCallback(
(renderProps: DocTableRenderProps) => {
return (
@@ -112,7 +107,7 @@ export const DocTableEmbeddable = (props: DocTableEmbeddableProps) => {
) : undefined
diff --git a/src/plugins/discover/public/components/doc_table/doc_table_infinite.tsx b/src/plugins/discover/public/components/doc_table/doc_table_infinite.tsx
index cb285746963ac..92265f731bf13 100644
--- a/src/plugins/discover/public/components/doc_table/doc_table_infinite.tsx
+++ b/src/plugins/discover/public/components/doc_table/doc_table_infinite.tsx
@@ -6,16 +6,17 @@
* Side Public License, v 1.
*/
-import React, { Fragment, memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
+import React, { Fragment, memo, useCallback, useEffect, useRef, useState } from 'react';
import './index.scss';
import { FormattedMessage } from '@kbn/i18n-react';
import { debounce } from 'lodash';
import { EuiButtonEmpty } from '@elastic/eui';
-import { SAMPLE_SIZE_SETTING } from '@kbn/discover-utils';
import { DocTableProps, DocTableRenderProps, DocTableWrapper } from './doc_table_wrapper';
import { SkipBottomButton } from '../../application/main/components/skip_bottom_button';
import { shouldLoadNextDocPatch } from './utils/should_load_next_doc_patch';
import { useDiscoverServices } from '../../hooks/use_discover_services';
+import { getAllowedSampleSize } from '../../utils/get_allowed_sample_size';
+import { useAppStateSelector } from '../../application/main/services/discover_app_state_container';
const FOOTER_PADDING = { padding: 0 };
@@ -38,8 +39,9 @@ const DocTableInfiniteContent = ({
onBackToTop,
}: DocTableInfiniteContentProps) => {
const { uiSettings } = useDiscoverServices();
-
- const sampleSize = useMemo(() => uiSettings.get(SAMPLE_SIZE_SETTING, 500), [uiSettings]);
+ const sampleSize = useAppStateSelector((state) =>
+ getAllowedSampleSize(state.sampleSize, uiSettings)
+ );
const onSkipBottomButton = useCallback(() => {
onSetMaxLimit();
diff --git a/src/plugins/discover/public/embeddable/saved_search_embeddable.test.ts b/src/plugins/discover/public/embeddable/saved_search_embeddable.test.ts
index 1473d07ba72b6..eaa7680137fe3 100644
--- a/src/plugins/discover/public/embeddable/saved_search_embeddable.test.ts
+++ b/src/plugins/discover/public/embeddable/saved_search_embeddable.test.ts
@@ -118,6 +118,7 @@ describe('saved search embeddable', () => {
columns: ['message', 'extension'],
rowHeight: 30,
rowsPerPage: 50,
+ sampleSize: 250,
};
const searchInput: SearchInput = byValue
? { ...baseInput, attributes: {} as SavedSearchByValueAttributes }
@@ -194,6 +195,11 @@ describe('saved search embeddable', () => {
await waitOneTick();
expect(searchProps.rowsPerPageState).toEqual(100);
+ expect(searchProps.sampleSizeState).toEqual(250);
+ searchProps.onUpdateSampleSize!(300);
+ await waitOneTick();
+ expect(searchProps.sampleSizeState).toEqual(300);
+
searchProps.onFilter!({ name: 'customer_id', type: 'string', scripted: false }, [17], '+');
await waitOneTick();
expect(executeTriggerActions).toHaveBeenCalled();
diff --git a/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx b/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx
index 34f6043936d92..e5896215e56de 100644
--- a/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx
+++ b/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx
@@ -53,7 +53,6 @@ import type { DataTableRecord, EsHitRecord } from '@kbn/discover-utils/types';
import {
DOC_HIDE_TIME_COLUMN_SETTING,
DOC_TABLE_LEGACY,
- SAMPLE_SIZE_SETTING,
SEARCH_FIELDS_FROM_SOURCE,
SHOW_FIELD_STATISTICS,
SORT_DEFAULT_ORDER_SETTING,
@@ -65,6 +64,7 @@ import { VIEW_MODE, getDefaultRowsPerPage } from '../../common/constants';
import type { ISearchEmbeddable, SearchInput, SearchOutput } from './types';
import type { DiscoverServices } from '../build_services';
import { getSortForEmbeddable, SortPair } from '../utils/sorting';
+import { getMaxAllowedSampleSize, getAllowedSampleSize } from '../utils/get_allowed_sample_size';
import { SEARCH_EMBEDDABLE_TYPE, SEARCH_EMBEDDABLE_CELL_ACTIONS_TRIGGER_ID } from './constants';
import { SavedSearchEmbeddableComponent } from './saved_search_embeddable_component';
import { handleSourceColumnState } from '../utils/state_helpers';
@@ -93,6 +93,7 @@ export type SearchProps = Partial &
onMoveColumn?: (column: string, index: number) => void;
onUpdateRowHeight?: (rowHeight?: number) => void;
onUpdateRowsPerPage?: (rowsPerPage?: number) => void;
+ onUpdateSampleSize?: (sampleSize?: number) => void;
};
export interface SearchEmbeddableConfig {
@@ -126,6 +127,7 @@ export class SavedSearchEmbeddable
private prevQuery?: Query;
private prevSort?: SortOrder[];
private prevSearchSessionId?: string;
+ private prevSampleSizeInput?: number;
private searchProps?: SearchProps;
private initialized?: boolean;
private node?: HTMLElement;
@@ -256,6 +258,10 @@ export class SavedSearchEmbeddable
return isTextBasedQuery(query);
};
+ private getFetchedSampleSize = (searchProps: SearchProps): number => {
+ return getAllowedSampleSize(searchProps.sampleSizeState, this.services.uiSettings);
+ };
+
private fetch = async () => {
const savedSearch = this.savedSearch;
const searchProps = this.searchProps;
@@ -276,9 +282,9 @@ export class SavedSearchEmbeddable
savedSearch.searchSource,
searchProps.dataView,
searchProps.sort,
+ this.getFetchedSampleSize(searchProps),
useNewFieldsApi,
{
- sampleSize: this.services.uiSettings.get(SAMPLE_SIZE_SETTING),
sortDir: this.services.uiSettings.get(SORT_DEFAULT_ORDER_SETTING),
}
);
@@ -472,7 +478,6 @@ export class SavedSearchEmbeddable
});
this.updateInput({ sort: sortOrderArr });
},
- sampleSize: this.services.uiSettings.get(SAMPLE_SIZE_SETTING),
onFilter: async (field, value, operator) => {
let filters = generateFilters(
this.services.filterManager,
@@ -503,6 +508,10 @@ export class SavedSearchEmbeddable
onUpdateRowsPerPage: (rowsPerPage) => {
this.updateInput({ rowsPerPage });
},
+ sampleSizeState: this.input.sampleSize || savedSearch.sampleSize,
+ onUpdateSampleSize: (sampleSize) => {
+ this.updateInput({ sampleSize });
+ },
cellActionsTriggerId: SEARCH_EMBEDDABLE_CELL_ACTIONS_TRIGGER_ID,
};
@@ -547,6 +556,7 @@ export class SavedSearchEmbeddable
!isEqual(this.prevQuery, this.input.query) ||
!isEqual(this.prevTimeRange, this.getTimeRange()) ||
!isEqual(this.prevSort, this.input.sort) ||
+ this.prevSampleSizeInput !== this.input.sampleSize ||
this.prevSearchSessionId !== this.input.searchSessionId
);
}
@@ -557,6 +567,7 @@ export class SavedSearchEmbeddable
}
return (
this.input.rowsPerPage !== searchProps.rowsPerPageState ||
+ this.input.sampleSize !== searchProps.sampleSizeState ||
(this.input.columns && !isEqual(this.input.columns, searchProps.columns))
);
}
@@ -589,6 +600,8 @@ export class SavedSearchEmbeddable
this.input.rowsPerPage ||
savedSearch.rowsPerPage ||
getDefaultRowsPerPage(this.services.uiSettings);
+ searchProps.maxAllowedSampleSize = getMaxAllowedSampleSize(this.services.uiSettings);
+ searchProps.sampleSizeState = this.input.sampleSize || savedSearch.sampleSize;
searchProps.filters = savedSearch.searchSource.getField('filter') as Filter[];
searchProps.savedSearchId = savedSearch.id;
@@ -607,6 +620,7 @@ export class SavedSearchEmbeddable
this.prevTimeRange = this.getTimeRange();
this.prevSearchSessionId = this.input.searchSessionId;
this.prevSort = this.input.sort;
+ this.prevSampleSizeInput = this.input.sampleSize;
this.searchProps = searchProps;
await this.fetch();
@@ -692,7 +706,10 @@ export class SavedSearchEmbeddable
>
-
+
,
diff --git a/src/plugins/discover/public/embeddable/saved_search_embeddable_component.tsx b/src/plugins/discover/public/embeddable/saved_search_embeddable_component.tsx
index 6c499a09d4152..43085e3c0902e 100644
--- a/src/plugins/discover/public/embeddable/saved_search_embeddable_component.tsx
+++ b/src/plugins/discover/public/embeddable/saved_search_embeddable_component.tsx
@@ -16,6 +16,7 @@ import { isTextBasedQuery } from '../application/main/utils/is_text_based_query'
import { SearchProps } from './saved_search_embeddable';
interface SavedSearchEmbeddableComponentProps {
+ fetchedSampleSize: number;
searchProps: SearchProps;
useLegacyTable: boolean;
query?: AggregateQuery | Query;
@@ -25,6 +26,7 @@ const DiscoverDocTableEmbeddableMemoized = React.memo(DiscoverDocTableEmbeddable
const DiscoverGridEmbeddableMemoized = React.memo(DiscoverGridEmbeddable);
export function SavedSearchEmbeddableComponent({
+ fetchedSampleSize,
searchProps,
useLegacyTable,
query,
@@ -34,6 +36,7 @@ export function SavedSearchEmbeddableComponent({
return (
);
@@ -41,6 +44,7 @@ export function SavedSearchEmbeddableComponent({
return (
{
+ sampleSizeState: number; // a required prop
totalHitCount?: number;
query?: AggregateQuery | Query;
interceptedWarnings?: SearchResponseInterceptedWarning[];
diff --git a/src/plugins/discover/public/embeddable/utils/update_search_source.test.ts b/src/plugins/discover/public/embeddable/utils/update_search_source.test.ts
index 6d440d89cf413..0b56ea8397728 100644
--- a/src/plugins/discover/public/embeddable/utils/update_search_source.test.ts
+++ b/src/plugins/discover/public/embeddable/utils/update_search_source.test.ts
@@ -22,35 +22,65 @@ const dataViewMockWithTimeField = buildDataViewMock({
describe('updateSearchSource', () => {
const defaults = {
- sampleSize: 50,
sortDir: 'asc',
};
+ const customSampleSize = 70;
+
it('updates a given search source', async () => {
const searchSource = createSearchSourceMock({});
- updateSearchSource(searchSource, dataViewMock, [] as SortOrder[], false, defaults);
+ updateSearchSource(
+ searchSource,
+ dataViewMock,
+ [] as SortOrder[],
+ customSampleSize,
+ false,
+ defaults
+ );
expect(searchSource.getField('fields')).toBe(undefined);
// does not explicitly request fieldsFromSource when not using fields API
expect(searchSource.getField('fieldsFromSource')).toBe(undefined);
+ expect(searchSource.getField('size')).toEqual(customSampleSize);
});
it('updates a given search source with the usage of the new fields api', async () => {
const searchSource = createSearchSourceMock({});
- updateSearchSource(searchSource, dataViewMock, [] as SortOrder[], true, defaults);
+ updateSearchSource(
+ searchSource,
+ dataViewMock,
+ [] as SortOrder[],
+ customSampleSize,
+ true,
+ defaults
+ );
expect(searchSource.getField('fields')).toEqual([{ field: '*', include_unmapped: 'true' }]);
expect(searchSource.getField('fieldsFromSource')).toBe(undefined);
+ expect(searchSource.getField('size')).toEqual(customSampleSize);
});
it('updates a given search source with sort field', async () => {
const searchSource1 = createSearchSourceMock({});
- updateSearchSource(searchSource1, dataViewMock, [] as SortOrder[], true, defaults);
+ updateSearchSource(
+ searchSource1,
+ dataViewMock,
+ [] as SortOrder[],
+ customSampleSize,
+ true,
+ defaults
+ );
expect(searchSource1.getField('sort')).toEqual([{ _score: 'asc' }]);
const searchSource2 = createSearchSourceMock({});
- updateSearchSource(searchSource2, dataViewMockWithTimeField, [] as SortOrder[], true, {
- sampleSize: 50,
- sortDir: 'desc',
- });
+ updateSearchSource(
+ searchSource2,
+ dataViewMockWithTimeField,
+ [] as SortOrder[],
+ customSampleSize,
+ true,
+ {
+ sortDir: 'desc',
+ }
+ );
expect(searchSource2.getField('sort')).toEqual([{ _doc: 'desc' }]);
const searchSource3 = createSearchSourceMock({});
@@ -58,6 +88,7 @@ describe('updateSearchSource', () => {
searchSource3,
dataViewMockWithTimeField,
[['bytes', 'desc']] as SortOrder[],
+ customSampleSize,
true,
defaults
);
diff --git a/src/plugins/discover/public/embeddable/utils/update_search_source.ts b/src/plugins/discover/public/embeddable/utils/update_search_source.ts
index 0215a26e649b0..ce2e72664e7d5 100644
--- a/src/plugins/discover/public/embeddable/utils/update_search_source.ts
+++ b/src/plugins/discover/public/embeddable/utils/update_search_source.ts
@@ -14,13 +14,13 @@ export const updateSearchSource = (
searchSource: ISearchSource,
dataView: DataView | undefined,
sort: (SortOrder[] & string[][]) | undefined,
+ sampleSize: number,
useNewFieldsApi: boolean,
defaults: {
- sampleSize: number;
sortDir: string;
}
) => {
- const { sampleSize, sortDir } = defaults;
+ const { sortDir } = defaults;
searchSource.setField('size', sampleSize);
searchSource.setField(
'sort',
diff --git a/src/plugins/discover/public/utils/get_allowed_sample_size.test.ts b/src/plugins/discover/public/utils/get_allowed_sample_size.test.ts
new file mode 100644
index 0000000000000..e7431dab6d478
--- /dev/null
+++ b/src/plugins/discover/public/utils/get_allowed_sample_size.test.ts
@@ -0,0 +1,49 @@
+/*
+ * 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 { SAMPLE_SIZE_SETTING } from '@kbn/discover-utils';
+import { getAllowedSampleSize, getMaxAllowedSampleSize } from './get_allowed_sample_size';
+import { IUiSettingsClient } from '@kbn/core-ui-settings-browser';
+
+describe('allowed sample size', () => {
+ function getUiSettingsMock(sampleSize?: number): IUiSettingsClient {
+ return {
+ get: (key: string) => {
+ if (key === SAMPLE_SIZE_SETTING) {
+ return sampleSize;
+ }
+ },
+ } as IUiSettingsClient;
+ }
+
+ const uiSettings = getUiSettingsMock(500);
+
+ describe('getAllowedSampleSize', function () {
+ test('should work correctly for a valid input', function () {
+ expect(getAllowedSampleSize(1, uiSettings)).toBe(1);
+ expect(getAllowedSampleSize(100, uiSettings)).toBe(100);
+ expect(getAllowedSampleSize(500, uiSettings)).toBe(500);
+ });
+
+ test('should work correctly for an invalid input', function () {
+ expect(getAllowedSampleSize(-10, uiSettings)).toBe(500);
+ expect(getAllowedSampleSize(undefined, uiSettings)).toBe(500);
+ expect(getAllowedSampleSize(50_000, uiSettings)).toBe(500);
+ });
+ });
+
+ describe('getMaxAllowedSampleSize', function () {
+ test('should work correctly', function () {
+ expect(getMaxAllowedSampleSize(uiSettings)).toBe(500);
+ expect(getMaxAllowedSampleSize(getUiSettingsMock(1000))).toBe(1000);
+ expect(getMaxAllowedSampleSize(getUiSettingsMock(100))).toBe(100);
+ expect(getMaxAllowedSampleSize(getUiSettingsMock(20_000))).toBe(10_000);
+ expect(getMaxAllowedSampleSize(getUiSettingsMock(undefined))).toBe(500);
+ });
+ });
+});
diff --git a/src/plugins/discover/public/utils/get_allowed_sample_size.ts b/src/plugins/discover/public/utils/get_allowed_sample_size.ts
new file mode 100644
index 0000000000000..588a33545e2a7
--- /dev/null
+++ b/src/plugins/discover/public/utils/get_allowed_sample_size.ts
@@ -0,0 +1,30 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 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 { IUiSettingsClient } from '@kbn/core-ui-settings-browser';
+import { SAMPLE_SIZE_SETTING } from '@kbn/discover-utils';
+import {
+ MIN_SAVED_SEARCH_SAMPLE_SIZE,
+ MAX_SAVED_SEARCH_SAMPLE_SIZE,
+} from '@kbn/saved-search-plugin/common';
+
+export const getMaxAllowedSampleSize = (uiSettings: IUiSettingsClient): number => {
+ return Math.min(uiSettings.get(SAMPLE_SIZE_SETTING) || 500, MAX_SAVED_SEARCH_SAMPLE_SIZE);
+};
+
+export const getAllowedSampleSize = (
+ customSampleSize: number | undefined,
+ uiSettings: IUiSettingsClient
+): number => {
+ if (!customSampleSize || customSampleSize < 0) {
+ return uiSettings.get(SAMPLE_SIZE_SETTING);
+ }
+ return Math.max(
+ Math.min(customSampleSize, getMaxAllowedSampleSize(uiSettings)),
+ MIN_SAVED_SEARCH_SAMPLE_SIZE
+ );
+};
diff --git a/src/plugins/saved_search/common/constants.ts b/src/plugins/saved_search/common/constants.ts
index 57e3cfff51ebb..a980bd40e3e26 100644
--- a/src/plugins/saved_search/common/constants.ts
+++ b/src/plugins/saved_search/common/constants.ts
@@ -10,4 +10,7 @@ export const SavedSearchType = 'search';
export const LATEST_VERSION = 1;
+export const MIN_SAVED_SEARCH_SAMPLE_SIZE = 1;
+export const MAX_SAVED_SEARCH_SAMPLE_SIZE = 10000;
+
export type SavedSearchContentType = typeof SavedSearchType;
diff --git a/src/plugins/saved_search/common/content_management/v1/cm_services.ts b/src/plugins/saved_search/common/content_management/v1/cm_services.ts
index 781f111b18bfb..0cbbe69c4bfeb 100644
--- a/src/plugins/saved_search/common/content_management/v1/cm_services.ts
+++ b/src/plugins/saved_search/common/content_management/v1/cm_services.ts
@@ -15,6 +15,7 @@ import {
updateOptionsSchema,
createResultSchema,
} from '@kbn/content-management-utils';
+import { MIN_SAVED_SEARCH_SAMPLE_SIZE, MAX_SAVED_SEARCH_SAMPLE_SIZE } from '../../constants';
const sortSchema = schema.arrayOf(schema.string(), { maxSize: 2 });
@@ -60,6 +61,12 @@ const savedSearchAttributesSchema = schema.object(
})
),
rowsPerPage: schema.maybe(schema.number()),
+ sampleSize: schema.maybe(
+ schema.number({
+ min: MIN_SAVED_SEARCH_SAMPLE_SIZE,
+ max: MAX_SAVED_SEARCH_SAMPLE_SIZE,
+ })
+ ),
breakdownField: schema.maybe(schema.string()),
version: schema.maybe(schema.number()),
},
diff --git a/src/plugins/saved_search/common/index.ts b/src/plugins/saved_search/common/index.ts
index 4669ecd3bd4b9..0ac92232fb3b8 100644
--- a/src/plugins/saved_search/common/index.ts
+++ b/src/plugins/saved_search/common/index.ts
@@ -21,5 +21,10 @@ export enum VIEW_MODE {
AGGREGATED_LEVEL = 'aggregated',
}
-export { SavedSearchType, LATEST_VERSION } from './constants';
+export {
+ SavedSearchType,
+ LATEST_VERSION,
+ MIN_SAVED_SEARCH_SAMPLE_SIZE,
+ MAX_SAVED_SEARCH_SAMPLE_SIZE,
+} from './constants';
export { getKibanaContextFn } from './expressions/kibana_context';
diff --git a/src/plugins/saved_search/common/saved_searches_utils.ts b/src/plugins/saved_search/common/saved_searches_utils.ts
index 324baca435232..d2a179e36817b 100644
--- a/src/plugins/saved_search/common/saved_searches_utils.ts
+++ b/src/plugins/saved_search/common/saved_searches_utils.ts
@@ -32,5 +32,6 @@ export const fromSavedSearchAttributes = (
timeRange: attributes.timeRange,
refreshInterval: attributes.refreshInterval,
rowsPerPage: attributes.rowsPerPage,
+ sampleSize: attributes.sampleSize,
breakdownField: attributes.breakdownField,
});
diff --git a/src/plugins/saved_search/common/service/get_saved_searches.test.ts b/src/plugins/saved_search/common/service/get_saved_searches.test.ts
index 05893f5c36e64..2b26b82eafece 100644
--- a/src/plugins/saved_search/common/service/get_saved_searches.test.ts
+++ b/src/plugins/saved_search/common/service/get_saved_searches.test.ts
@@ -58,6 +58,7 @@ describe('getSavedSearch', () => {
description: 'description',
grid: {},
hideChart: false,
+ sampleSize: 100,
},
id: 'ccf1af80-2297-11ec-86e0-1155ffb9c7a7',
type: 'search',
@@ -103,6 +104,7 @@ describe('getSavedSearch', () => {
"refreshInterval": undefined,
"rowHeight": undefined,
"rowsPerPage": undefined,
+ "sampleSize": 100,
"searchSource": Object {
"create": [MockFunction],
"createChild": [MockFunction],
@@ -208,6 +210,7 @@ describe('getSavedSearch', () => {
"refreshInterval": undefined,
"rowHeight": undefined,
"rowsPerPage": undefined,
+ "sampleSize": undefined,
"searchSource": Object {
"create": [MockFunction],
"createChild": [MockFunction],
diff --git a/src/plugins/saved_search/common/service/saved_searches_utils.test.ts b/src/plugins/saved_search/common/service/saved_searches_utils.test.ts
index 67f368637d3f5..b118799858348 100644
--- a/src/plugins/saved_search/common/service/saved_searches_utils.test.ts
+++ b/src/plugins/saved_search/common/service/saved_searches_utils.test.ts
@@ -25,6 +25,9 @@ describe('saved_searches_utils', () => {
hideChart: true,
isTextBasedQuery: false,
usesAdHocDataView: false,
+ rowsPerPage: 250,
+ sampleSize: 1000,
+ breakdownField: 'extension.keyword',
};
expect(
@@ -38,7 +41,7 @@ describe('saved_searches_utils', () => {
)
).toMatchInlineSnapshot(`
Object {
- "breakdownField": undefined,
+ "breakdownField": "extension.keyword",
"columns": Array [
"a",
"b",
@@ -52,7 +55,8 @@ describe('saved_searches_utils', () => {
"references": Array [],
"refreshInterval": undefined,
"rowHeight": undefined,
- "rowsPerPage": undefined,
+ "rowsPerPage": 250,
+ "sampleSize": 1000,
"searchSource": SearchSource {
"dependencies": Object {
"aggs": Object {
@@ -122,6 +126,7 @@ describe('saved_searches_utils', () => {
"refreshInterval": undefined,
"rowHeight": undefined,
"rowsPerPage": undefined,
+ "sampleSize": undefined,
"sort": Array [
Array [
"a",
diff --git a/src/plugins/saved_search/common/service/saved_searches_utils.ts b/src/plugins/saved_search/common/service/saved_searches_utils.ts
index ef99a0b87ad5c..ab4720b7802f8 100644
--- a/src/plugins/saved_search/common/service/saved_searches_utils.ts
+++ b/src/plugins/saved_search/common/service/saved_searches_utils.ts
@@ -46,5 +46,6 @@ export const toSavedSearchAttributes = (
timeRange: savedSearch.timeRange ? pick(savedSearch.timeRange, ['from', 'to']) : undefined,
refreshInterval: savedSearch.refreshInterval,
rowsPerPage: savedSearch.rowsPerPage,
+ sampleSize: savedSearch.sampleSize,
breakdownField: savedSearch.breakdownField,
});
diff --git a/src/plugins/saved_search/common/types.ts b/src/plugins/saved_search/common/types.ts
index 3da4276aeb1dd..c47548aebd8d4 100644
--- a/src/plugins/saved_search/common/types.ts
+++ b/src/plugins/saved_search/common/types.ts
@@ -43,6 +43,7 @@ export interface SavedSearchAttributes {
refreshInterval?: RefreshInterval;
rowsPerPage?: number;
+ sampleSize?: number;
breakdownField?: string;
}
@@ -74,6 +75,7 @@ export interface SavedSearch {
refreshInterval?: RefreshInterval;
rowsPerPage?: number;
+ sampleSize?: number;
breakdownField?: string;
references?: SavedObjectReference[];
sharingSavedObjectProps?: {
diff --git a/src/plugins/saved_search/public/services/saved_searches/save_saved_searches.test.ts b/src/plugins/saved_search/public/services/saved_searches/save_saved_searches.test.ts
index 9c7eb23c98e0a..a04f0af45eb29 100644
--- a/src/plugins/saved_search/public/services/saved_searches/save_saved_searches.test.ts
+++ b/src/plugins/saved_search/public/services/saved_searches/save_saved_searches.test.ts
@@ -128,6 +128,7 @@ describe('saveSavedSearch', () => {
refreshInterval: undefined,
rowHeight: undefined,
rowsPerPage: undefined,
+ sampleSize: undefined,
sort: [],
timeRange: undefined,
timeRestore: false,
@@ -162,6 +163,7 @@ describe('saveSavedSearch', () => {
refreshInterval: undefined,
rowHeight: undefined,
rowsPerPage: undefined,
+ sampleSize: undefined,
timeRange: undefined,
sort: [],
title: 'title',
@@ -211,6 +213,7 @@ describe('saveSavedSearch', () => {
refreshInterval: undefined,
rowHeight: undefined,
rowsPerPage: undefined,
+ sampleSize: undefined,
sort: [],
timeRange: undefined,
timeRestore: false,
diff --git a/src/plugins/saved_search/public/services/saved_searches/saved_search_attribute_service.test.ts b/src/plugins/saved_search/public/services/saved_searches/saved_search_attribute_service.test.ts
index cc6a6ec79ffea..35c35e669bff8 100644
--- a/src/plugins/saved_search/public/services/saved_searches/saved_search_attribute_service.test.ts
+++ b/src/plugins/saved_search/public/services/saved_searches/saved_search_attribute_service.test.ts
@@ -200,6 +200,7 @@ describe('getSavedSearchAttributeService', () => {
"refreshInterval": undefined,
"rowHeight": undefined,
"rowsPerPage": undefined,
+ "sampleSize": undefined,
"searchSource": Object {
"create": [MockFunction],
"createChild": [MockFunction],
diff --git a/src/plugins/saved_search/public/services/saved_searches/types.ts b/src/plugins/saved_search/public/services/saved_searches/types.ts
index 5e0f2637ae2aa..086d71848b6c6 100644
--- a/src/plugins/saved_search/public/services/saved_searches/types.ts
+++ b/src/plugins/saved_search/public/services/saved_searches/types.ts
@@ -34,6 +34,7 @@ interface SearchBaseInput extends EmbeddableInput {
sort?: SortOrder[];
rowHeight?: number;
rowsPerPage?: number;
+ sampleSize?: number;
}
export type SavedSearchByValueAttributes = Omit & {
diff --git a/src/plugins/saved_search/server/content_management/saved_search_storage.ts b/src/plugins/saved_search/server/content_management/saved_search_storage.ts
index 797430a159159..0615dbdc3049e 100644
--- a/src/plugins/saved_search/server/content_management/saved_search_storage.ts
+++ b/src/plugins/saved_search/server/content_management/saved_search_storage.ts
@@ -43,6 +43,7 @@ export class SavedSearchStorage extends SOContentStorage {
'refreshInterval',
'rowsPerPage',
'breakdownField',
+ 'sampleSize',
],
logger,
throwOnResultValidationError,
diff --git a/src/plugins/saved_search/server/saved_objects/schema.ts b/src/plugins/saved_search/server/saved_objects/schema.ts
new file mode 100644
index 0000000000000..19dfdf5e7a11c
--- /dev/null
+++ b/src/plugins/saved_search/server/saved_objects/schema.ts
@@ -0,0 +1,95 @@
+/*
+ * 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 { schema } from '@kbn/config-schema';
+import {
+ MIN_SAVED_SEARCH_SAMPLE_SIZE,
+ MAX_SAVED_SEARCH_SAMPLE_SIZE,
+ VIEW_MODE,
+} from '../../common';
+
+const SCHEMA_SEARCH_BASE = {
+ // General
+ title: schema.string(),
+ description: schema.string({ defaultValue: '' }),
+
+ // Data grid
+ columns: schema.arrayOf(schema.string(), { defaultValue: [] }),
+ sort: schema.oneOf(
+ [
+ schema.arrayOf(schema.arrayOf(schema.string(), { maxSize: 2 })),
+ schema.arrayOf(schema.string(), { maxSize: 2 }),
+ ],
+ { defaultValue: [] }
+ ),
+ grid: schema.object(
+ {
+ columns: schema.maybe(
+ schema.recordOf(
+ schema.string(),
+ schema.object({
+ width: schema.maybe(schema.number()),
+ })
+ )
+ ),
+ },
+ { defaultValue: {} }
+ ),
+ rowHeight: schema.maybe(schema.number()),
+ rowsPerPage: schema.maybe(schema.number()),
+
+ // Chart
+ hideChart: schema.boolean({ defaultValue: false }),
+ breakdownField: schema.maybe(schema.string()),
+
+ // Search
+ kibanaSavedObjectMeta: schema.object({
+ searchSourceJSON: schema.string(),
+ }),
+ isTextBasedQuery: schema.boolean({ defaultValue: false }),
+ usesAdHocDataView: schema.maybe(schema.boolean()),
+
+ // Time
+ timeRestore: schema.maybe(schema.boolean()),
+ timeRange: schema.maybe(
+ schema.object({
+ from: schema.string(),
+ to: schema.string(),
+ })
+ ),
+ refreshInterval: schema.maybe(
+ schema.object({
+ pause: schema.boolean(),
+ value: schema.number(),
+ })
+ ),
+
+ // Display
+ viewMode: schema.maybe(
+ schema.oneOf([
+ schema.literal(VIEW_MODE.DOCUMENT_LEVEL),
+ schema.literal(VIEW_MODE.AGGREGATED_LEVEL),
+ ])
+ ),
+ hideAggregatedPreview: schema.maybe(schema.boolean()),
+
+ // Legacy
+ hits: schema.maybe(schema.number()),
+ version: schema.maybe(schema.number()),
+};
+
+export const SCHEMA_SEARCH_V8_8_0 = schema.object(SCHEMA_SEARCH_BASE);
+export const SCHEMA_SEARCH_V8_12_0 = schema.object({
+ ...SCHEMA_SEARCH_BASE,
+ sampleSize: schema.maybe(
+ schema.number({
+ min: MIN_SAVED_SEARCH_SAMPLE_SIZE,
+ max: MAX_SAVED_SEARCH_SAMPLE_SIZE,
+ })
+ ),
+});
diff --git a/src/plugins/saved_search/server/saved_objects/search.ts b/src/plugins/saved_search/server/saved_objects/search.ts
index 9b78f5ea4aecb..2d3844f098c6a 100644
--- a/src/plugins/saved_search/server/saved_objects/search.ts
+++ b/src/plugins/saved_search/server/saved_objects/search.ts
@@ -6,12 +6,11 @@
* Side Public License, v 1.
*/
-import { schema } from '@kbn/config-schema';
import { ANALYTICS_SAVED_OBJECT_INDEX } from '@kbn/core-saved-objects-server';
import { SavedObjectsType } from '@kbn/core/server';
import { MigrateFunctionsObject } from '@kbn/kibana-utils-plugin/common';
-import { VIEW_MODE } from '../../common';
import { getAllMigrations } from './search_migrations';
+import { SCHEMA_SEARCH_V8_8_0, SCHEMA_SEARCH_V8_12_0 } from './schema';
export function getSavedSearchObjectType(
getSearchSourceMigrations: () => MigrateFunctionsObject
@@ -44,75 +43,8 @@ export function getSavedSearchObjectType(
},
},
schemas: {
- '8.8.0': schema.object({
- // General
- title: schema.string(),
- description: schema.string({ defaultValue: '' }),
-
- // Data grid
- columns: schema.arrayOf(schema.string(), { defaultValue: [] }),
- sort: schema.oneOf(
- [
- schema.arrayOf(schema.arrayOf(schema.string(), { maxSize: 2 })),
- schema.arrayOf(schema.string(), { maxSize: 2 }),
- ],
- { defaultValue: [] }
- ),
- grid: schema.object(
- {
- columns: schema.maybe(
- schema.recordOf(
- schema.string(),
- schema.object({
- width: schema.maybe(schema.number()),
- })
- )
- ),
- },
- { defaultValue: {} }
- ),
- rowHeight: schema.maybe(schema.number()),
- rowsPerPage: schema.maybe(schema.number()),
-
- // Chart
- hideChart: schema.boolean({ defaultValue: false }),
- breakdownField: schema.maybe(schema.string()),
-
- // Search
- kibanaSavedObjectMeta: schema.object({
- searchSourceJSON: schema.string(),
- }),
- isTextBasedQuery: schema.boolean({ defaultValue: false }),
- usesAdHocDataView: schema.maybe(schema.boolean()),
-
- // Time
- timeRestore: schema.maybe(schema.boolean()),
- timeRange: schema.maybe(
- schema.object({
- from: schema.string(),
- to: schema.string(),
- })
- ),
- refreshInterval: schema.maybe(
- schema.object({
- pause: schema.boolean(),
- value: schema.number(),
- })
- ),
-
- // Display
- viewMode: schema.maybe(
- schema.oneOf([
- schema.literal(VIEW_MODE.DOCUMENT_LEVEL),
- schema.literal(VIEW_MODE.AGGREGATED_LEVEL),
- ])
- ),
- hideAggregatedPreview: schema.maybe(schema.boolean()),
-
- // Legacy
- hits: schema.maybe(schema.number()),
- version: schema.maybe(schema.number()),
- }),
+ '8.8.0': SCHEMA_SEARCH_V8_8_0,
+ '8.12.0': SCHEMA_SEARCH_V8_12_0,
},
migrations: () => getAllMigrations(getSearchSourceMigrations()),
};
diff --git a/test/functional/apps/discover/group2/_data_grid_row_height.ts b/test/functional/apps/discover/group2/_data_grid_row_height.ts
index 2c385b67aaa02..84574655cb406 100644
--- a/test/functional/apps/discover/group2/_data_grid_row_height.ts
+++ b/test/functional/apps/discover/group2/_data_grid_row_height.ts
@@ -14,6 +14,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const esArchiver = getService('esArchiver');
const kibanaServer = getService('kibanaServer');
const dataGrid = getService('dataGrid');
+ const testSubjects = getService('testSubjects');
const PageObjects = getPageObjects(['settings', 'common', 'discover', 'header', 'timePicker']);
const defaultSettings = { defaultIndex: 'logstash-*' };
const security = getService('security');
@@ -47,7 +48,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
expect(await dataGrid.getCurrentRowHeightValue()).to.be('Auto fit');
});
- it('should allow to change row height and reset it', async () => {
+ it('should allow to change row height', async () => {
await dataGrid.clickGridSettings();
expect(await dataGrid.getCurrentRowHeightValue()).to.be('Auto fit');
@@ -59,13 +60,16 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
expect(await dataGrid.getCurrentRowHeightValue()).to.be('Single');
- await dataGrid.resetRowHeightValue();
-
- expect(await dataGrid.getCurrentRowHeightValue()).to.be('Auto fit');
+ // we hide "Reset to default" action in Discover
+ await testSubjects.missingOrFail('resetDisplaySelector');
await dataGrid.changeRowHeightValue('Custom');
- await dataGrid.resetRowHeightValue();
+ expect(await dataGrid.getCurrentRowHeightValue()).to.be('Custom');
+
+ await testSubjects.missingOrFail('resetDisplaySelector');
+
+ await dataGrid.changeRowHeightValue('Auto fit');
expect(await dataGrid.getCurrentRowHeightValue()).to.be('Auto fit');
});
diff --git a/test/functional/apps/discover/group2/_data_grid_sample_size.ts b/test/functional/apps/discover/group2/_data_grid_sample_size.ts
new file mode 100644
index 0000000000000..891363f0868db
--- /dev/null
+++ b/test/functional/apps/discover/group2/_data_grid_sample_size.ts
@@ -0,0 +1,195 @@
+/*
+ * 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 DEFAULT_ROWS_PER_PAGE = 100;
+const DEFAULT_SAMPLE_SIZE = 500;
+const CUSTOM_SAMPLE_SIZE = 250;
+const CUSTOM_SAMPLE_SIZE_FOR_SAVED_SEARCH = 150;
+const CUSTOM_SAMPLE_SIZE_FOR_DASHBOARD_PANEL = 10;
+const FOOTER_SELECTOR = 'unifiedDataTableFooter';
+const SAVED_SEARCH_NAME = 'With sample size';
+
+export default function ({ getService, getPageObjects }: FtrProviderContext) {
+ const browser = getService('browser');
+ const esArchiver = getService('esArchiver');
+ const kibanaServer = getService('kibanaServer');
+ const dataGrid = getService('dataGrid');
+ const testSubjects = getService('testSubjects');
+ const retry = getService('retry');
+ const dashboardAddPanel = getService('dashboardAddPanel');
+ const PageObjects = getPageObjects([
+ 'settings',
+ 'common',
+ 'discover',
+ 'header',
+ 'timePicker',
+ 'dashboard',
+ ]);
+ const security = getService('security');
+ const defaultSettings = {
+ defaultIndex: 'logstash-*',
+ 'discover:sampleSize': DEFAULT_SAMPLE_SIZE,
+ 'discover:rowHeightOption': 0, // single line
+ 'discover:sampleRowsPerPage': DEFAULT_ROWS_PER_PAGE,
+ hideAnnouncements: true,
+ };
+
+ describe('discover data grid sample size', function describeIndexTests() {
+ 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 kibanaServer.uiSettings.replace({});
+ await kibanaServer.savedObjects.cleanStandardList();
+ });
+
+ beforeEach(async function () {
+ await PageObjects.timePicker.setDefaultAbsoluteRangeViaUiSettings();
+ await kibanaServer.uiSettings.update(defaultSettings);
+ await PageObjects.common.navigateToApp('discover');
+ await PageObjects.header.waitUntilLoadingHasFinished();
+ await PageObjects.discover.waitUntilSearchingHasFinished();
+ });
+
+ async function goToLastPageAndCheckFooterMessage(sampleSize: number) {
+ const lastPageNumber = Math.ceil(sampleSize / DEFAULT_ROWS_PER_PAGE) - 1;
+
+ // go to the last page
+ await testSubjects.click(`pagination-button-${lastPageNumber}`);
+ // footer is shown now
+ await retry.try(async function () {
+ await testSubjects.existOrFail(FOOTER_SELECTOR);
+ });
+ expect(
+ (await testSubjects.getVisibleText(FOOTER_SELECTOR)).includes(String(sampleSize))
+ ).to.be(true);
+ }
+
+ it('should use the default sample size', async () => {
+ await dataGrid.clickGridSettings();
+ expect(await dataGrid.getCurrentSampleSizeValue()).to.be(DEFAULT_SAMPLE_SIZE);
+ await goToLastPageAndCheckFooterMessage(DEFAULT_SAMPLE_SIZE);
+ });
+
+ it('should allow to change sample size', async () => {
+ await dataGrid.clickGridSettings();
+ expect(await dataGrid.getCurrentSampleSizeValue()).to.be(DEFAULT_SAMPLE_SIZE);
+
+ await dataGrid.changeSampleSizeValue(CUSTOM_SAMPLE_SIZE);
+
+ await PageObjects.header.waitUntilLoadingHasFinished();
+ await PageObjects.discover.waitUntilSearchingHasFinished();
+
+ expect(await dataGrid.getCurrentSampleSizeValue()).to.be(CUSTOM_SAMPLE_SIZE);
+ await goToLastPageAndCheckFooterMessage(CUSTOM_SAMPLE_SIZE);
+ });
+
+ it('should persist the selection after reloading the page', async () => {
+ await dataGrid.clickGridSettings();
+ expect(await dataGrid.getCurrentSampleSizeValue()).to.be(DEFAULT_SAMPLE_SIZE);
+
+ await dataGrid.changeSampleSizeValue(CUSTOM_SAMPLE_SIZE);
+
+ await PageObjects.header.waitUntilLoadingHasFinished();
+ await PageObjects.discover.waitUntilSearchingHasFinished();
+
+ await browser.refresh();
+
+ await PageObjects.discover.waitUntilSearchingHasFinished();
+ await dataGrid.clickGridSettings();
+
+ expect(await dataGrid.getCurrentSampleSizeValue()).to.be(CUSTOM_SAMPLE_SIZE);
+ await goToLastPageAndCheckFooterMessage(CUSTOM_SAMPLE_SIZE);
+ });
+
+ it('should save a custom sample size with a search', async () => {
+ await dataGrid.clickGridSettings();
+ expect(await dataGrid.getCurrentSampleSizeValue()).to.be(DEFAULT_SAMPLE_SIZE);
+
+ await dataGrid.changeSampleSizeValue(CUSTOM_SAMPLE_SIZE_FOR_SAVED_SEARCH);
+
+ await PageObjects.header.waitUntilLoadingHasFinished();
+ await PageObjects.discover.waitUntilSearchingHasFinished();
+
+ await PageObjects.discover.saveSearch(SAVED_SEARCH_NAME);
+
+ await PageObjects.discover.waitUntilSearchingHasFinished();
+ await dataGrid.clickGridSettings();
+
+ expect(await dataGrid.getCurrentSampleSizeValue()).to.be(CUSTOM_SAMPLE_SIZE_FOR_SAVED_SEARCH);
+ await goToLastPageAndCheckFooterMessage(CUSTOM_SAMPLE_SIZE_FOR_SAVED_SEARCH);
+
+ // reset to the default value
+ await PageObjects.discover.clickNewSearchButton();
+ await dataGrid.clickGridSettings();
+ expect(await dataGrid.getCurrentSampleSizeValue()).to.be(DEFAULT_SAMPLE_SIZE);
+ await goToLastPageAndCheckFooterMessage(DEFAULT_SAMPLE_SIZE);
+
+ // load the saved search again
+ await PageObjects.discover.loadSavedSearch(SAVED_SEARCH_NAME);
+ await dataGrid.clickGridSettings();
+ expect(await dataGrid.getCurrentSampleSizeValue()).to.be(CUSTOM_SAMPLE_SIZE_FOR_SAVED_SEARCH);
+ await goToLastPageAndCheckFooterMessage(CUSTOM_SAMPLE_SIZE_FOR_SAVED_SEARCH);
+
+ // load another saved search without a custom sample size
+ await PageObjects.discover.loadSavedSearch('A Saved Search');
+ await dataGrid.clickGridSettings();
+ expect(await dataGrid.getCurrentSampleSizeValue()).to.be(DEFAULT_SAMPLE_SIZE);
+ await goToLastPageAndCheckFooterMessage(DEFAULT_SAMPLE_SIZE);
+ });
+
+ it('should use the default sample size on Dashboard', async () => {
+ await PageObjects.common.navigateToApp('dashboard');
+ await PageObjects.dashboard.clickNewDashboard();
+ await dashboardAddPanel.clickOpenAddPanel();
+ await dashboardAddPanel.addSavedSearch('A Saved Search');
+
+ await dataGrid.clickGridSettings();
+ expect(await dataGrid.getCurrentSampleSizeValue()).to.be(DEFAULT_SAMPLE_SIZE);
+ await goToLastPageAndCheckFooterMessage(DEFAULT_SAMPLE_SIZE);
+ });
+
+ it('should use custom sample size on Dashboard when specified', async () => {
+ await PageObjects.common.navigateToApp('dashboard');
+ await PageObjects.dashboard.clickNewDashboard();
+ await dashboardAddPanel.clickOpenAddPanel();
+ await dashboardAddPanel.addSavedSearch(SAVED_SEARCH_NAME);
+
+ await dataGrid.clickGridSettings();
+ expect(await dataGrid.getCurrentSampleSizeValue()).to.be(CUSTOM_SAMPLE_SIZE_FOR_SAVED_SEARCH);
+
+ await dataGrid.changeSampleSizeValue(CUSTOM_SAMPLE_SIZE_FOR_DASHBOARD_PANEL);
+
+ await PageObjects.header.waitUntilLoadingHasFinished();
+
+ await dataGrid.clickGridSettings();
+ expect(await dataGrid.getCurrentSampleSizeValue()).to.be(
+ CUSTOM_SAMPLE_SIZE_FOR_DASHBOARD_PANEL
+ );
+ await goToLastPageAndCheckFooterMessage(CUSTOM_SAMPLE_SIZE_FOR_DASHBOARD_PANEL);
+
+ await PageObjects.dashboard.saveDashboard('test');
+
+ await browser.refresh();
+ await PageObjects.header.waitUntilLoadingHasFinished();
+
+ await dataGrid.clickGridSettings();
+ expect(await dataGrid.getCurrentSampleSizeValue()).to.be(
+ CUSTOM_SAMPLE_SIZE_FOR_DASHBOARD_PANEL
+ );
+ await goToLastPageAndCheckFooterMessage(CUSTOM_SAMPLE_SIZE_FOR_DASHBOARD_PANEL);
+ });
+ });
+}
diff --git a/test/functional/apps/discover/group2/index.ts b/test/functional/apps/discover/group2/index.ts
index 8174e3ef93aba..6b35f6707bb78 100644
--- a/test/functional/apps/discover/group2/index.ts
+++ b/test/functional/apps/discover/group2/index.ts
@@ -28,6 +28,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_row_height'));
+ loadTestFile(require.resolve('./_data_grid_sample_size'));
loadTestFile(require.resolve('./_data_grid_pagination'));
loadTestFile(require.resolve('./_data_grid_footer'));
loadTestFile(require.resolve('./_data_grid_field_tokens'));
diff --git a/test/functional/services/data_grid.ts b/test/functional/services/data_grid.ts
index 337fea7c3ff45..df5ba570cfc51 100644
--- a/test/functional/services/data_grid.ts
+++ b/test/functional/services/data_grid.ts
@@ -7,6 +7,7 @@
*/
import { chunk } from 'lodash';
+import { Key } from 'selenium-webdriver';
import { FtrService } from '../ftr_provider_context';
import { WebElementWrapper } from './lib/web_element_wrapper';
@@ -366,6 +367,28 @@ export class DataGridService extends FtrService {
await this.testSubjects.click('resetDisplaySelector');
}
+ private async findSampleSizeInput() {
+ return await this.find.byCssSelector(
+ 'input[type="number"][data-test-subj="unifiedDataTableSampleSizeInput"]'
+ );
+ }
+
+ public async getCurrentSampleSizeValue() {
+ const sampleSizeInput = await this.findSampleSizeInput();
+ return Number(await sampleSizeInput.getAttribute('value'));
+ }
+
+ public async changeSampleSizeValue(newValue: number) {
+ const sampleSizeInput = await this.findSampleSizeInput();
+ await sampleSizeInput.focus();
+ // replacing the input values with a new one
+ await sampleSizeInput.pressKeys([
+ Key[process.platform === 'darwin' ? 'COMMAND' : 'CONTROL'],
+ 'a',
+ ]);
+ await sampleSizeInput.type(String(newValue));
+ }
+
public async getDetailsRow(): Promise {
const detailRows = await this.getDetailsRows();
return detailRows[0];
diff --git a/x-pack/plugins/cloud_security_posture/public/components/cloud_security_data_table/cloud_security_data_table.tsx b/x-pack/plugins/cloud_security_posture/public/components/cloud_security_data_table/cloud_security_data_table.tsx
index 12afa013aed18..e988a169219ea 100644
--- a/x-pack/plugins/cloud_security_posture/public/components/cloud_security_data_table/cloud_security_data_table.tsx
+++ b/x-pack/plugins/cloud_security_posture/public/components/cloud_security_data_table/cloud_security_data_table.tsx
@@ -250,7 +250,7 @@ export const CloudSecurityDataTable = ({
onSetColumns={onSetColumns}
onSort={onSort}
rows={rows}
- sampleSize={MAX_FINDINGS_TO_LOAD}
+ sampleSizeState={MAX_FINDINGS_TO_LOAD}
setExpandedDoc={setExpandedDoc}
renderDocumentView={renderDocumentView}
sort={sort}