From e98658b5ea34fc74e97fba02a79a4080fc56b7c8 Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Thu, 19 Oct 2023 08:16:25 +0200 Subject: [PATCH] [OnWeek][Discover] Allow to change current sample size and save it with a saved search (#157269) - Closes https://github.com/elastic/kibana/issues/94140 - https://github.com/elastic/kibana/issues/11758 - https://github.com/elastic/kibana/issues/4060 - https://github.com/elastic/kibana/issues/3220 - https://github.com/elastic/kibana/issues/23307 - Closes https://github.com/elastic/kibana/issues/131130 ## Summary This PR allows to change current sample size right from Discover page, no need to modify the global default value. Saved search panels on Dashboard will also use the saved value to fetch only the requested sample size. This customisation was requested by many customers as it will allow to load Dashboards faster. Current range for the slider: from 10 to 1000 (with a step 10). Screenshot 2023-10-09 at 11 10 52 ### Checklist - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [x] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)) - [x] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers) --- .../src/components/data_table.test.tsx | 71 ++++++- .../src/components/data_table.tsx | 55 +++-- ...table_additional_display_settings.test.tsx | 111 ++++++++++ ...data_table_additional_display_settings.tsx | 85 ++++++++ .../group2/check_registered_types.test.ts | 2 +- .../context/context_app_content.tsx | 2 +- .../components/layout/discover_documents.tsx | 25 ++- .../components/top_nav/on_save_search.tsx | 12 ++ .../main/hooks/utils/build_state_subscribe.ts | 4 +- .../services/discover_app_state_container.ts | 6 +- .../main/services/load_saved_search.ts | 2 +- .../main/utils/cleanup_url_state.test.ts | 67 +++++- .../main/utils/cleanup_url_state.ts | 20 +- .../main/utils/fetch_documents.test.ts | 1 + .../application/main/utils/fetch_documents.ts | 8 +- .../main/utils/get_state_defaults.test.ts | 2 + .../main/utils/get_state_defaults.ts | 5 +- .../doc_table/create_doc_table_embeddable.tsx | 1 + .../doc_table/doc_table_embeddable.tsx | 11 +- .../doc_table/doc_table_infinite.tsx | 10 +- .../saved_search_embeddable.test.ts | 6 + .../embeddable/saved_search_embeddable.tsx | 25 ++- .../saved_search_embeddable_component.tsx | 4 + .../public/embeddable/saved_search_grid.tsx | 4 +- .../utils/update_search_source.test.ts | 47 ++++- .../embeddable/utils/update_search_source.ts | 4 +- .../utils/get_allowed_sample_size.test.ts | 49 +++++ .../public/utils/get_allowed_sample_size.ts | 30 +++ src/plugins/saved_search/common/constants.ts | 3 + .../content_management/v1/cm_services.ts | 7 + src/plugins/saved_search/common/index.ts | 7 +- .../common/saved_searches_utils.ts | 1 + .../common/service/get_saved_searches.test.ts | 3 + .../service/saved_searches_utils.test.ts | 9 +- .../common/service/saved_searches_utils.ts | 1 + src/plugins/saved_search/common/types.ts | 2 + .../save_saved_searches.test.ts | 3 + .../saved_search_attribute_service.test.ts | 1 + .../public/services/saved_searches/types.ts | 1 + .../saved_search_storage.ts | 1 + .../server/saved_objects/schema.ts | 95 +++++++++ .../server/saved_objects/search.ts | 74 +------ .../discover/group2/_data_grid_row_height.ts | 14 +- .../discover/group2/_data_grid_sample_size.ts | 195 ++++++++++++++++++ test/functional/apps/discover/group2/index.ts | 1 + test/functional/services/data_grid.ts | 23 +++ .../cloud_security_data_table.tsx | 2 +- 47 files changed, 964 insertions(+), 148 deletions(-) create mode 100644 packages/kbn-unified-data-table/src/components/data_table_additional_display_settings.test.tsx create mode 100644 packages/kbn-unified-data-table/src/components/data_table_additional_display_settings.tsx create mode 100644 src/plugins/discover/public/utils/get_allowed_sample_size.test.ts create mode 100644 src/plugins/discover/public/utils/get_allowed_sample_size.ts create mode 100644 src/plugins/saved_search/server/saved_objects/schema.ts create mode 100644 test/functional/apps/discover/group2/_data_grid_sample_size.ts 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 c59149132cdf461..97bcc0e2c665425 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 a1540e88a5cd6f7..22a625f479e3b43 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 000000000000000..2555c5f25392918 --- /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 62780b66727efce..73fef09887c6916 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 ff99c46816f25e2..81ca3e6f81b6696 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 d1c772e7ec1bf68..60367b83d02eda8 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 4d1b9ccbdc22de8..abae8e83b41a165 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 40838edd35c355a..27407822553bba2 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 046e8fd6393f11b..124c83beda236ce 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 3631ca876cce633..8b8dcc2beb2f484 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 ea1af49f48e8946..2d49639e02884da 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 3abeed97d4cdc1e..cdfb95d87f134da 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 cbc62a3cd6068ab..36847a0a08929db 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 99e87f13558a83c..b1e18273479bf54 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 19e9f6a64c88b53..a659f543f9993e0 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 78c89468253742b..943d9b4c98cf01a 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 e0f24c2839113b5..a0a55a17a9cba27 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 fbe8ed083ebc3d7..36e3629f089aa2f 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 cb285746963acfa..92265f731bf1345 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 1473d07ba72b6f5..eaa7680137fe3e3 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 34f6043936d9218..e5896215e56de9e 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 6c499a09d4152ac..43085e3c0902e2a 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 6d440d89cf41346..0b56ea839772831 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 0215a26e649b0d9..ce2e72664e7d5b0 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 000000000000000..e7431dab6d47817 --- /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 000000000000000..588a33545e2a76a --- /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 57e3cfff51ebb1a..a980bd40e3e26fd 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 781f111b18bfbd8..0cbbe69c4bfeb49 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 4669ecd3bd4b9b6..0ac92232fb3b8fa 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 324baca43523248..d2a179e36817b18 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 05893f5c36e6438..2b26b82eafeced2 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 67f368637d3f5ae..b118799858348cc 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 ef99a0b87ad5c66..ab4720b7802f821 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 3da4276aeb1dd0b..c47548aebd8d4ec 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 9c7eb23c98e0ae5..a04f0af45eb2986 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 cc6a6ec79ffea3d..35c35e669bff87a 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 5e0f2637ae2aa46..086d71848b6c63c 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 797430a159159bd..0615dbdc3049ed9 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 000000000000000..19dfdf5e7a11cc0 --- /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 9b78f5ea4aecb19..2d3844f098c6ab2 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 2c385b67aaa0287..84574655cb4066e 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 000000000000000..891363f0868dbf6 --- /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 8174e3ef93aba85..6b35f6707bb78ba 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 337fea7c3ff4505..df5ba570cfc51e0 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 12afa013aed182c..e988a169219ea44 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}