[Discover] Allow for custom number of rows in the results and save the specified number with a Saved Search (#135726)

* [Discover] Persist rowsPerPage in app state and URL

* [Discover] Persist rowsPerPage in saved search objects

* [Discover] Make sure that rowsPerPage is persisted in saved search objects

* [Discover] Support rowsPerPage in embeddables

* [Discover] Allow to save a custom rowsPerPage option

* [Discover] Reflect custom size in the grid dropdown

* [Discover] Fix changing rowsPerPage on Dashboard page

* [Discover] Skip saving rowsPerPage for legacy view

* [Discover] Fix sample size for rendering an embeddable

* [Discover] Update tests

* [Discover] Update tests

* [Discover] Update mapping

* [Discover] Revert setting a default state

* [Discover] Remove rowsPerPage input from SaveSearch modal

* [Discover] Update tests

* [Discover] Ignore the setting for legacy view

* [Discover] Add `discover:sampleRowsPerPage` setting to Advaced Settings

* [Discover] Allow to save rowsPerPage on Dashboard for legacy view too

* [Discover] Add tests

* [Discover] Add tests

* [Discover] Extend "select" type to return values as numbers too

* [Discover] Fix values changes

* [Discover] Update types to support also lists with numbers

* [Discover] Fix disclaimer updates

* [Discover] Update setting copy

* [Discover] Simplify saving of rowsPerPage

* [Discover] Extend number of rowsPerPage options for the legacy view too

* [Discover] Move to utils

* [Discover] Fix deps

* [Discover] Add tests

* [Discover] Update settings copy

* [Discover] Limit max number of rows per page for an embedded legacy table

* [Discover] Prevent invalid values for a custom rows per page

* [Discover] Add tests

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Julia Rechkunova 2022-07-19 15:54:17 +02:00 committed by GitHub
parent 1904b61b87
commit 92a46f5344
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
42 changed files with 485 additions and 57 deletions

View file

@ -298,6 +298,9 @@ When enabled, removes the columns that are not in the new data view.
[[discover-sample-size]]`discover:sampleSize`::
Specifies the number of rows to display in the *Discover* table.
[[discover-sampleRowsPerPage]]`discover:sampleRowsPerPage`::
Specifies the number of rows to display per page in the *Discover* table.
[[discover-searchFieldsFromSource]]`discover:searchFieldsFromSource`::
Load fields from the original JSON {ref}/mapping-source-field.html[`_source`].
When disabled, *Discover* loads fields using the {es} search API's

View file

@ -50,7 +50,7 @@ export interface UiSettingsParams<T = unknown> {
/** used to group the configured setting in the UI */
category?: string[];
/** array of permitted values for this setting */
options?: string[];
options?: string[] | number[];
/** text labels for 'select' type UI element */
optionLabels?: Record<string, string>;
/** a flag indicating whether new value applying requires page reloading */

View file

@ -156,7 +156,7 @@ export class Field extends PureComponent<FieldProps> {
this.onFieldChange(e.target.value);
onFieldChange = (targetValue: any) => {
const { type, value, defVal } = this.props.setting;
const { type, value, defVal, options } = this.props.setting;
let newUnsavedValue;
switch (type) {
@ -170,6 +170,13 @@ export class Field extends PureComponent<FieldProps> {
case 'number':
newUnsavedValue = Number(targetValue);
break;
case 'select':
if (typeof options?.[0] === 'number') {
newUnsavedValue = Number(targetValue);
} else {
newUnsavedValue = targetValue;
}
break;
default:
newUnsavedValue = targetValue;
}

View file

@ -15,7 +15,7 @@ export interface FieldSetting {
name: string;
value: unknown;
description?: string | ReactElement;
options?: string[];
options?: string[] | number[];
optionLabels?: Record<string, string>;
requiresPageReload: boolean;
type: UiSettingsType;

View file

@ -0,0 +1,10 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export const DEFAULT_ROWS_PER_PAGE = 100;
export const ROWS_PER_PAGE_OPTIONS = [10, 25, 50, DEFAULT_ROWS_PER_PAGE, 250, 500];

View file

@ -10,6 +10,7 @@ export const PLUGIN_ID = 'discover';
export const APP_ICON = 'discoverApp';
export const DEFAULT_COLUMNS_SETTING = 'defaultColumns';
export const SAMPLE_SIZE_SETTING = 'discover:sampleSize';
export const SAMPLE_ROWS_PER_PAGE_SETTING = 'discover:sampleRowsPerPage';
export const SORT_DEFAULT_ORDER_SETTING = 'discover:sort:defaultOrder';
export const SEARCH_ON_PAGE_LOAD_SETTING = 'discover:searchOnPageLoad';
export const DOC_HIDE_TIME_COLUMN_SETTING = 'doc_table:hideTimeColumn';

View file

@ -15,6 +15,7 @@ import {
DOC_HIDE_TIME_COLUMN_SETTING,
MAX_DOC_FIELDS_DISPLAYED,
SAMPLE_SIZE_SETTING,
SAMPLE_ROWS_PER_PAGE_SETTING,
SORT_DEFAULT_ORDER_SETTING,
HIDE_ANNOUNCEMENTS,
} from '../../common';
@ -67,6 +68,8 @@ export const discoverServiceMock = {
return false;
} else if (key === SAMPLE_SIZE_SETTING) {
return 250;
} else if (key === SAMPLE_ROWS_PER_PAGE_SETTING) {
return 150;
} else if (key === MAX_DOC_FIELDS_DISPLAYED) {
return 50;
} else if (key === HIDE_ANNOUNCEMENTS) {

View file

@ -12,6 +12,7 @@ import {
DEFAULT_COLUMNS_SETTING,
DOC_TABLE_LEGACY,
SAMPLE_SIZE_SETTING,
SAMPLE_ROWS_PER_PAGE_SETTING,
SHOW_MULTIFIELDS,
SEARCH_FIELDS_FROM_SOURCE,
ROW_HEIGHT_OPTION,
@ -21,6 +22,8 @@ export const uiSettingsMock = {
get: (key: string) => {
if (key === SAMPLE_SIZE_SETTING) {
return 10;
} else if (key === SAMPLE_ROWS_PER_PAGE_SETTING) {
return 100;
} else if (key === DEFAULT_COLUMNS_SETTING) {
return ['default_column'];
} else if (key === DOC_TABLE_LEGACY) {

View file

@ -19,6 +19,7 @@ import {
MAX_DOC_FIELDS_DISPLAYED,
ROW_HEIGHT_OPTION,
SAMPLE_SIZE_SETTING,
SAMPLE_ROWS_PER_PAGE_SETTING,
SEARCH_FIELDS_FROM_SOURCE,
SHOW_MULTIFIELDS,
} from '../../../../../../common';
@ -32,6 +33,8 @@ export const uiSettingsMock = {
return 3;
} else if (key === SAMPLE_SIZE_SETTING) {
return 10;
} else if (key === SAMPLE_ROWS_PER_PAGE_SETTING) {
return 100;
} else if (key === DEFAULT_COLUMNS_SETTING) {
return ['default_column'];
} else if (key === DOC_TABLE_LEGACY) {

View file

@ -86,8 +86,8 @@ function DiscoverDocumentsComponent({
const onResize = useCallback(
(colSettings: { columnId: string; width: number }) => {
const grid = { ...state.grid } || {};
const newColumns = { ...grid.columns } || {};
const grid = { ...(state.grid || {}) };
const newColumns = { ...(grid.columns || {}) };
newColumns[colSettings.columnId] = {
width: colSettings.width,
};
@ -97,6 +97,13 @@ function DiscoverDocumentsComponent({
[stateContainer, state]
);
const onUpdateRowsPerPage = useCallback(
(rowsPerPage: number) => {
stateContainer.setAppState({ rowsPerPage });
},
[stateContainer]
);
const onSort = useCallback(
(sort: string[][]) => {
stateContainer.setAppState({ sort });
@ -190,6 +197,8 @@ function DiscoverDocumentsComponent({
useNewFieldsApi={useNewFieldsApi}
rowHeightState={state.rowHeight}
onUpdateRowHeight={onUpdateRowHeight}
rowsPerPageState={state.rowsPerPage}
onUpdateRowsPerPage={onUpdateRowsPerPage}
onFieldEdited={onFieldEdited}
/>
</div>

View file

@ -22,7 +22,13 @@ test('onSaveSearch', async () => {
i18n: i18nServiceMock.create(),
},
} as unknown as DiscoverServices;
const stateMock = {} as unknown as GetStateReturn;
const stateMock = {
appStateContainer: {
getState: () => ({
rowsPerPage: 250,
}),
},
} as unknown as GetStateReturn;
await onSaveSearch({
indexPattern: indexPatternMock,

View file

@ -8,13 +8,14 @@
import React from 'react';
import { i18n } from '@kbn/i18n';
import { SavedObjectSaveModal, showSaveModal } from '@kbn/saved-objects-plugin/public';
import { SavedObjectSaveModal, showSaveModal, OnSaveProps } from '@kbn/saved-objects-plugin/public';
import { DataView } from '@kbn/data-views-plugin/public';
import { SavedSearch, SaveSavedSearchOptions } from '../../../../services/saved_searches';
import { DiscoverServices } from '../../../../build_services';
import { GetStateReturn } from '../../services/discover_state';
import { setBreadcrumbsTitle } from '../../../../utils/breadcrumbs';
import { persistSavedSearch } from '../../utils/persist_saved_search';
import { DOC_TABLE_LEGACY } from '../../../../../common';
async function saveDataSource({
indexPattern,
@ -97,6 +98,7 @@ export async function onSaveSearch({
state: GetStateReturn;
onClose?: () => void;
}) {
const { uiSettings } = services;
const onSave = async ({
newTitle,
newCopyOnSave,
@ -111,8 +113,12 @@ export async function onSaveSearch({
onTitleDuplicate: () => void;
}) => {
const currentTitle = savedSearch.title;
const currentRowsPerPage = savedSearch.rowsPerPage;
savedSearch.title = newTitle;
savedSearch.description = newDescription;
savedSearch.rowsPerPage = uiSettings.get(DOC_TABLE_LEGACY)
? currentRowsPerPage
: state.appStateContainer.getState().rowsPerPage;
const saveOptions: SaveSavedSearchOptions = {
onTitleDuplicate,
copyOnSave: newCopyOnSave,
@ -129,6 +135,7 @@ export async function onSaveSearch({
// If the save wasn't successful, put the original values back.
if (!response.id || response.error) {
savedSearch.title = currentTitle;
savedSearch.rowsPerPage = currentRowsPerPage;
} else {
state.resetInitialAppState();
}
@ -136,17 +143,39 @@ export async function onSaveSearch({
};
const saveModal = (
<SavedObjectSaveModal
onSave={onSave}
onClose={onClose ?? (() => {})}
<SaveSearchObjectModal
title={savedSearch.title ?? ''}
showCopyOnSave={!!savedSearch.id}
description={savedSearch.description}
objectType={i18n.translate('discover.localMenu.saveSaveSearchObjectType', {
defaultMessage: 'search',
})}
showDescription={true}
onSave={onSave}
onClose={onClose ?? (() => {})}
/>
);
showSaveModal(saveModal, services.core.i18n.Context);
}
const SaveSearchObjectModal: React.FC<{
title: string;
showCopyOnSave: boolean;
description?: string;
onSave: (props: OnSaveProps & { newRowsPerPage?: number }) => void;
onClose: () => void;
}> = ({ title, description, showCopyOnSave, onSave, onClose }) => {
const onModalSave = (params: OnSaveProps) => {
onSave(params);
};
return (
<SavedObjectSaveModal
title={title}
showCopyOnSave={showCopyOnSave}
description={description}
objectType={i18n.translate('discover.localMenu.saveSaveSearchObjectType', {
defaultMessage: 'search',
})}
showDescription={true}
onSave={onModalSave}
onClose={onClose}
/>
);
};

View file

@ -90,6 +90,10 @@ export interface AppState {
* Document explorer row height option
*/
rowHeight?: number;
/**
* Number of rows in the grid per page
*/
rowsPerPage?: number;
}
export interface AppStateUrl extends Omit<AppState, 'sort'> {

View file

@ -55,4 +55,29 @@ describe('cleanupUrlState', () => {
} as AppStateUrl;
expect(cleanupUrlState(state)).toMatchInlineSnapshot(`Object {}`);
});
test('should keep a valid rowsPerPage', async () => {
const state = {
rowsPerPage: 50,
} as AppStateUrl;
expect(cleanupUrlState(state)).toMatchInlineSnapshot(`
Object {
"rowsPerPage": 50,
}
`);
});
test('should remove a negative rowsPerPage', async () => {
const state = {
rowsPerPage: -50,
} as AppStateUrl;
expect(cleanupUrlState(state)).toMatchInlineSnapshot(`Object {}`);
});
test('should remove an invalid rowsPerPage', async () => {
const state = {
rowsPerPage: 'test',
} as unknown as AppStateUrl;
expect(cleanupUrlState(state)).toMatchInlineSnapshot(`Object {}`);
});
});

View file

@ -31,5 +31,14 @@ export function cleanupUrlState(appStateFromUrl: AppStateUrl): AppState {
// This allows the sort prop to be overwritten with the default sorting
delete appStateFromUrl.sort;
}
if (
appStateFromUrl?.rowsPerPage &&
!(typeof appStateFromUrl.rowsPerPage === 'number' && appStateFromUrl.rowsPerPage > 0)
) {
// remove the param if it's invalid
delete appStateFromUrl.rowsPerPage;
}
return appStateFromUrl as AppState;
}

View file

@ -37,6 +37,7 @@ describe('getStateDefaults', () => {
"interval": "auto",
"query": undefined,
"rowHeight": undefined,
"rowsPerPage": undefined,
"savedQuery": undefined,
"sort": Array [
Array [
@ -70,6 +71,7 @@ describe('getStateDefaults', () => {
"interval": "auto",
"query": undefined,
"rowHeight": undefined,
"rowsPerPage": undefined,
"savedQuery": undefined,
"sort": Array [],
"viewMode": undefined,

View file

@ -51,7 +51,7 @@ export function getStateDefaults({
const columns = getDefaultColumns(savedSearch, config);
const chartHidden = storage.get(CHART_HIDDEN_KEY);
const defaultState = {
const defaultState: AppState = {
query,
sort: !sort.length
? getDefaultSort(
@ -63,13 +63,14 @@ export function getStateDefaults({
columns,
index: indexPattern?.id,
interval: 'auto',
filters: cloneDeep(searchSource.getOwnField('filter')),
filters: cloneDeep(searchSource.getOwnField('filter')) as AppState['filters'],
hideChart: typeof chartHidden === 'boolean' ? chartHidden : undefined,
viewMode: undefined,
hideAggregatedPreview: undefined,
savedQuery: undefined,
rowHeight: undefined,
} as AppState;
rowsPerPage: undefined,
};
if (savedSearch.grid) {
defaultState.grid = savedSearch.grid;
}
@ -82,10 +83,12 @@ export function getStateDefaults({
if (savedSearch.viewMode) {
defaultState.viewMode = savedSearch.viewMode;
}
if (savedSearch.hideAggregatedPreview) {
defaultState.hideAggregatedPreview = savedSearch.hideAggregatedPreview;
}
if (savedSearch.rowsPerPage) {
defaultState.rowsPerPage = savedSearch.rowsPerPage;
}
return defaultState;
}

View file

@ -17,8 +17,6 @@ export const GRID_STYLE = {
rowHover: 'none',
} as EuiDataGridStyle;
export const pageSizeArr = [25, 50, 100, 250];
export const defaultPageSize = 100;
export const defaultTimeColumnWidth = 210;
export const toolbarVisibility = {
showColumnSelector: {

View file

@ -34,12 +34,7 @@ import {
getLeadControlColumns,
getVisibleColumns,
} from './discover_grid_columns';
import {
defaultPageSize,
GRID_STYLE,
pageSizeArr,
toolbarVisibility as toolbarVisibilityDefaults,
} from './constants';
import { GRID_STYLE, toolbarVisibility as toolbarVisibilityDefaults } from './constants';
import { getDisplayedColumns } from '../../utils/columns';
import {
DOC_HIDE_TIME_COLUMN_SETTING,
@ -54,6 +49,7 @@ import type { DataTableRecord, ValueToStringConverter } from '../../types';
import { useRowHeightsOptions } from '../../hooks/use_row_heights_options';
import { useDiscoverServices } from '../../hooks/use_discover_services';
import { convertValueToString } from '../../utils/convert_value_to_string';
import { getRowsPerPageOptions, getDefaultRowsPerPage } from '../../utils/rows_per_page';
interface SortObj {
id: string;
@ -166,6 +162,14 @@ export interface DiscoverGridProps {
* Update row height state
*/
onUpdateRowHeight?: (rowHeight: number) => void;
/**
* Current state value for rowsPerPage
*/
rowsPerPageState?: number;
/**
* Update rows per page state
*/
onUpdateRowsPerPage?: (rowsPerPage: number) => void;
/**
* Callback to execute on edit runtime field
*/
@ -203,6 +207,8 @@ export const DiscoverGrid = ({
className,
rowHeightState,
onUpdateRowHeight,
rowsPerPageState,
onUpdateRowsPerPage,
onFieldEdited,
}: DiscoverGridProps) => {
const dataGridRef = useRef<EuiDataGridRefProps>(null);
@ -256,17 +262,28 @@ export const DiscoverGrid = ({
/**
* Pagination
*/
const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: defaultPageSize });
const defaultRowsPerPage = useMemo(
() => getDefaultRowsPerPage(services.uiSettings),
[services.uiSettings]
);
const currentPageSize =
typeof rowsPerPageState === 'number' && rowsPerPageState > 0
? rowsPerPageState
: defaultRowsPerPage;
const [pagination, setPagination] = useState({
pageIndex: 0,
pageSize: currentPageSize,
});
const rowCount = useMemo(() => (displayedRows ? displayedRows.length : 0), [displayedRows]);
const pageCount = useMemo(
() => Math.ceil(rowCount / pagination.pageSize),
[rowCount, pagination]
);
const isOnLastPage = pagination.pageIndex === pageCount - 1;
const paginationObj = useMemo(() => {
const onChangeItemsPerPage = (pageSize: number) =>
setPagination((paginationData) => ({ ...paginationData, pageSize }));
const onChangeItemsPerPage = (pageSize: number) => {
onUpdateRowsPerPage?.(pageSize);
};
const onChangePage = (pageIndex: number) =>
setPagination((paginationData) => ({ ...paginationData, pageIndex }));
@ -277,10 +294,20 @@ export const DiscoverGrid = ({
onChangePage,
pageIndex: pagination.pageIndex > pageCount - 1 ? 0 : pagination.pageIndex,
pageSize: pagination.pageSize,
pageSizeOptions: pageSizeArr,
pageSizeOptions: getRowsPerPageOptions(pagination.pageSize),
}
: undefined;
}, [pagination, pageCount, isPaginationEnabled]);
}, [pagination, pageCount, isPaginationEnabled, onUpdateRowsPerPage]);
const isOnLastPage = paginationObj ? paginationObj.pageIndex === pageCount - 1 : false;
useEffect(() => {
setPagination((paginationData) =>
paginationData.pageSize === currentPageSize
? paginationData
: { ...paginationData, pageSize: currentPageSize }
);
}, [currentPageSize, setPagination]);
/**
* Sorting

View file

@ -19,6 +19,9 @@ import {
import { FormattedMessage } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
import { euiLightVars } from '@kbn/ui-theme';
import { getRowsPerPageOptions } from '../../../../utils/rows_per_page';
export const MAX_ROWS_PER_PAGE_OPTION = 100;
interface ToolBarPaginationProps {
pageSize: number;
@ -53,18 +56,20 @@ export const ToolBarPagination = ({
return size === pageSize ? 'check' : 'empty';
};
const rowsPerPageOptions = [25, 50, 100].map((cur) => (
<EuiContextMenuItem
key={`${cur} rows`}
icon={getIconType(cur)}
onClick={() => {
closePopover();
onPageSizeChange(cur);
}}
>
{cur} {rowsWord}
</EuiContextMenuItem>
));
const rowsPerPageOptions = getRowsPerPageOptions(pageSize)
.filter((option) => option <= MAX_ROWS_PER_PAGE_OPTION) // legacy table is not optimized well for rendering more rows at once
.map((cur) => (
<EuiContextMenuItem
key={`${cur} rows`}
icon={getIconType(cur)}
onClick={() => {
closePopover();
onPageSizeChange(cur);
}}
>
{cur} {rowsWord}
</EuiContextMenuItem>
));
return (
<EuiFlexGroup alignItems="center" gutterSize="xs" responsive={false}>

View file

@ -16,6 +16,8 @@ export function DiscoverDocTableEmbeddable(renderProps: DocTableEmbeddableProps)
<DocTableEmbeddable
columns={renderProps.columns}
rows={renderProps.rows}
rowsPerPageState={renderProps.rowsPerPageState}
onUpdateRowsPerPage={renderProps.onUpdateRowsPerPage}
totalHitCount={renderProps.totalHitCount}
indexPattern={renderProps.indexPattern}
onSort={renderProps.onSort}

View file

@ -12,19 +12,25 @@ import { FormattedMessage } from '@kbn/i18n-react';
import { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui';
import { SAMPLE_SIZE_SETTING } from '../../../common';
import { usePager } from '../../hooks/use_pager';
import { ToolBarPagination } from './components/pager/tool_bar_pagination';
import {
ToolBarPagination,
MAX_ROWS_PER_PAGE_OPTION,
} from './components/pager/tool_bar_pagination';
import { DocTableProps, DocTableRenderProps, DocTableWrapper } from './doc_table_wrapper';
import { TotalDocuments } from '../../application/main/components/total_documents/total_documents';
import { useDiscoverServices } from '../../hooks/use_discover_services';
export interface DocTableEmbeddableProps extends DocTableProps {
totalHitCount: number;
rowsPerPageState?: number;
onUpdateRowsPerPage?: (rowsPerPage?: number) => void;
}
const DocTableWrapperMemoized = memo(DocTableWrapper);
export const DocTableEmbeddable = (props: DocTableEmbeddableProps) => {
const services = useDiscoverServices();
const onUpdateRowsPerPage = props.onUpdateRowsPerPage;
const tableWrapperRef = useRef<HTMLDivElement>(null);
const {
curPageIndex,
@ -35,7 +41,10 @@ export const DocTableEmbeddable = (props: DocTableEmbeddableProps) => {
changePageIndex,
changePageSize,
} = usePager({
initialPageSize: 50,
initialPageSize:
typeof props.rowsPerPageState === 'number' && props.rowsPerPageState > 0
? Math.min(props.rowsPerPageState, MAX_ROWS_PER_PAGE_OPTION)
: 50,
totalItems: props.rows.length,
});
const showPagination = totalPages !== 0;
@ -63,8 +72,9 @@ export const DocTableEmbeddable = (props: DocTableEmbeddableProps) => {
(size: number) => {
scrollTop();
changePageSize(size);
onUpdateRowsPerPage?.(size); // to update `rowsPerPage` input param for the embeddable
},
[changePageSize, scrollTop]
[changePageSize, scrollTop, onUpdateRowsPerPage]
);
const shouldShowLimitedResultsWarning = useMemo(

View file

@ -65,6 +65,7 @@ export type SearchProps = Partial<DiscoverGridProps> &
totalHitCount?: number;
onMoveColumn?: (column: string, index: number) => void;
onUpdateRowHeight?: (rowHeight?: number) => void;
onUpdateRowsPerPage?: (rowsPerPage?: number) => void;
};
interface SearchEmbeddableConfig {
@ -139,7 +140,12 @@ export class SavedSearchEmbeddable
if (titleChanged) {
this.panelTitle = this.output.title || '';
}
if (this.searchProps && (titleChanged || this.isFetchRequired(this.searchProps))) {
if (
this.searchProps &&
(titleChanged ||
this.isFetchRequired(this.searchProps) ||
this.isInputChangedAndRerenderRequired(this.searchProps))
) {
this.pushContainerStateParamsToProps(this.searchProps);
}
});
@ -302,7 +308,7 @@ export class SavedSearchEmbeddable
});
this.updateInput({ sort: sortOrderArr });
},
sampleSize: 500,
sampleSize: this.services.uiSettings.get(SAMPLE_SIZE_SETTING),
onFilter: async (field, value, operator) => {
let filters = generateFilters(
this.filterManager,
@ -329,6 +335,10 @@ export class SavedSearchEmbeddable
onUpdateRowHeight: (rowHeight) => {
this.updateInput({ rowHeight });
},
rowsPerPageState: this.input.rowsPerPage || this.savedSearch.rowsPerPage,
onUpdateRowsPerPage: (rowsPerPage) => {
this.updateInput({ rowsPerPage });
},
};
const timeRangeSearchSource = searchSource.create();
@ -364,6 +374,13 @@ export class SavedSearchEmbeddable
);
}
private isInputChangedAndRerenderRequired(searchProps?: SearchProps) {
if (!searchProps) {
return false;
}
return this.input.rowsPerPage !== searchProps.rowsPerPageState;
}
private async pushContainerStateParamsToProps(
searchProps: SearchProps,
{ forceFetch = false }: { forceFetch: boolean } = { forceFetch: false }
@ -384,6 +401,7 @@ export class SavedSearchEmbeddable
searchProps.sort = this.input.sort || savedSearchSort;
searchProps.sharedItemTitle = this.panelTitle;
searchProps.rowHeightState = this.input.rowHeight || this.savedSearch.rowHeight;
searchProps.rowsPerPageState = this.input.rowsPerPage || this.savedSearch.rowsPerPage;
if (forceFetch || isFetchRequired) {
this.filtersSearchSource.setField('filter', this.input.filters);
this.filtersSearchSource.setField('query', this.input.query);

View file

@ -25,6 +25,7 @@ export interface SearchInput extends EmbeddableInput {
columns?: string[];
sort?: SortOrder[];
rowHeight?: number;
rowsPerPage?: number;
}
export interface SearchOutput extends EmbeddableOutput {

View file

@ -105,6 +105,7 @@ describe('getSavedSearch', () => {
"hideChart": false,
"id": "ccf1af80-2297-11ec-86e0-1155ffb9c7a7",
"rowHeight": undefined,
"rowsPerPage": undefined,
"searchSource": Object {
"create": [MockFunction],
"createChild": [MockFunction],

View file

@ -42,6 +42,7 @@ describe('saved_searches_utils', () => {
"hideChart": true,
"id": "id",
"rowHeight": undefined,
"rowsPerPage": undefined,
"searchSource": SearchSource {
"dependencies": Object {
"aggs": Object {
@ -119,6 +120,7 @@ describe('saved_searches_utils', () => {
"searchSourceJSON": "{}",
},
"rowHeight": undefined,
"rowsPerPage": undefined,
"sort": Array [
Array [
"a",

View file

@ -45,6 +45,7 @@ export const fromSavedSearchAttributes = (
viewMode: attributes.viewMode,
hideAggregatedPreview: attributes.hideAggregatedPreview,
rowHeight: attributes.rowHeight,
rowsPerPage: attributes.rowsPerPage,
});
export const toSavedSearchAttributes = (
@ -61,4 +62,5 @@ export const toSavedSearchAttributes = (
viewMode: savedSearch.viewMode,
hideAggregatedPreview: savedSearch.hideAggregatedPreview,
rowHeight: savedSearch.rowHeight,
rowsPerPage: savedSearch.rowsPerPage,
});

View file

@ -27,6 +27,7 @@ export interface SavedSearchAttributes {
viewMode?: VIEW_MODE;
hideAggregatedPreview?: boolean;
rowHeight?: number;
rowsPerPage?: number;
}
/** @internal **/
@ -53,4 +54,5 @@ export interface SavedSearch {
viewMode?: VIEW_MODE;
hideAggregatedPreview?: boolean;
rowHeight?: number;
rowsPerPage?: number;
}

View file

@ -0,0 +1,42 @@
/*
* 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 { discoverServiceMock } from '../__mocks__/services';
import { SAMPLE_ROWS_PER_PAGE_SETTING } from '../../common';
import { getRowsPerPageOptions, getDefaultRowsPerPage } from './rows_per_page';
const SORTED_OPTIONS = [10, 25, 50, 100, 250, 500];
describe('rows per page', () => {
describe('getRowsPerPageOptions', () => {
it('should return default options if not provided', () => {
expect(getRowsPerPageOptions()).toEqual(SORTED_OPTIONS);
});
it('should return default options if current value is one of them', () => {
expect(getRowsPerPageOptions(250)).toEqual(SORTED_OPTIONS);
});
it('should return extended options if current value is not one of them', () => {
expect(getRowsPerPageOptions(350)).toEqual([10, 25, 50, 100, 250, 350, 500]);
});
});
describe('getDefaultRowsPerPage', () => {
it('should return a value from settings', () => {
expect(getDefaultRowsPerPage(discoverServiceMock.uiSettings)).toEqual(150);
expect(discoverServiceMock.uiSettings.get).toHaveBeenCalledWith(SAMPLE_ROWS_PER_PAGE_SETTING);
});
it('should return a default value', () => {
expect(getDefaultRowsPerPage({ ...discoverServiceMock.uiSettings, get: jest.fn() })).toEqual(
100
);
});
});
});

View file

@ -0,0 +1,26 @@
/*
* 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 { sortBy, uniq } from 'lodash';
import { DEFAULT_ROWS_PER_PAGE, ROWS_PER_PAGE_OPTIONS } from '../../common/constants';
import { SAMPLE_ROWS_PER_PAGE_SETTING } from '../../common';
import { DiscoverServices } from '../build_services';
export const getRowsPerPageOptions = (currentRowsPerPage?: number): number[] => {
return sortBy(
uniq(
typeof currentRowsPerPage === 'number' && currentRowsPerPage > 0
? [...ROWS_PER_PAGE_OPTIONS, currentRowsPerPage]
: ROWS_PER_PAGE_OPTIONS
)
);
};
export const getDefaultRowsPerPage = (uiSettings: DiscoverServices['uiSettings']): number => {
return parseInt(uiSettings.get(SAMPLE_ROWS_PER_PAGE_SETTING), 10) || DEFAULT_ROWS_PER_PAGE;
};

View file

@ -50,6 +50,7 @@ export function getSavedSearchObjectType(
grid: { type: 'object', enabled: false },
version: { type: 'integer' },
rowHeight: { type: 'text' },
rowsPerPage: { type: 'integer', index: false, doc_values: false },
},
},
migrations: () => getAllMigrations(getSearchSourceMigrations()),

View file

@ -14,6 +14,7 @@ import { METRIC_TYPE } from '@kbn/analytics';
import {
DEFAULT_COLUMNS_SETTING,
SAMPLE_SIZE_SETTING,
SAMPLE_ROWS_PER_PAGE_SETTING,
SORT_DEFAULT_ORDER_SETTING,
SEARCH_ON_PAGE_LOAD_SETTING,
DOC_HIDE_TIME_COLUMN_SETTING,
@ -30,6 +31,7 @@ import {
SHOW_FIELD_STATISTICS,
ROW_HEIGHT_OPTION,
} from '../common';
import { DEFAULT_ROWS_PER_PAGE, ROWS_PER_PAGE_OPTIONS } from '../common/constants';
export const getUiSettings: (docLinks: DocLinksServiceSetup) => Record<string, UiSettingsParams> = (
docLinks: DocLinksServiceSetup
@ -59,11 +61,24 @@ export const getUiSettings: (docLinks: DocLinksServiceSetup) => Record<string, U
},
[SAMPLE_SIZE_SETTING]: {
name: i18n.translate('discover.advancedSettings.sampleSizeTitle', {
defaultMessage: 'Number of rows',
defaultMessage: 'Maximum rows per table',
}),
value: 500,
description: i18n.translate('discover.advancedSettings.sampleSizeText', {
defaultMessage: 'The number of rows to show in the table',
defaultMessage: 'Sets the maximum number of rows for the entire document table.',
}),
category: ['discover'],
schema: schema.number(),
},
[SAMPLE_ROWS_PER_PAGE_SETTING]: {
name: i18n.translate('discover.advancedSettings.sampleRowsPerPageTitle', {
defaultMessage: 'Rows per page',
}),
value: DEFAULT_ROWS_PER_PAGE,
options: ROWS_PER_PAGE_OPTIONS,
type: 'select',
description: i18n.translate('discover.advancedSettings.sampleRowsPerPageText', {
defaultMessage: 'Limits the number of rows per page in the document table.',
}),
category: ['discover'],
schema: schema.number(),

View file

@ -171,6 +171,10 @@ export const stackManagementSchema: MakeSchemaFrom<UsageStats> = {
type: 'long',
_meta: { description: 'Non-default value of setting.' },
},
'discover:sampleRowsPerPage': {
type: 'long',
_meta: { description: 'Non-default value of setting.' },
},
'discover:maxDocFieldsDisplayed': {
type: 'long',
_meta: { description: 'Non-default value of setting.' },

View file

@ -73,6 +73,7 @@ export interface UsageStats {
'discover:searchOnPageLoad': boolean;
'doc_table:hideTimeColumn': boolean;
'discover:sampleSize': number;
'discover:sampleRowsPerPage': number;
defaultColumns: string[];
'context:defaultSize': number;
'context:tieBreakerFields': string[];

View file

@ -8020,6 +8020,12 @@
"description": "Non-default value of setting."
}
},
"discover:sampleRowsPerPage": {
"type": "long",
"_meta": {
"description": "Non-default value of setting."
}
},
"discover:maxDocFieldsDisplayed": {
"type": "long",
"_meta": {

View file

@ -10,6 +10,7 @@ import expect from '@kbn/expect';
import { FtrProviderContext } from '../../ftr_provider_context';
export default function ({ getService, getPageObjects }: FtrProviderContext) {
const browser = getService('browser');
const esArchiver = getService('esArchiver');
const kibanaServer = getService('kibanaServer');
const dataGrid = getService('dataGrid');
@ -20,6 +21,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
describe('discover data grid pagination', function describeIndexTests() {
before(async () => {
await browser.setWindowSize(1200, 2000);
await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional');
await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/discover');
});
@ -27,6 +29,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
after(async () => {
await kibanaServer.importExport.unload('test/functional/fixtures/kbn_archiver/discover');
await kibanaServer.uiSettings.replace({});
await kibanaServer.savedObjects.clean({ types: ['search'] });
});
beforeEach(async function () {
@ -62,5 +65,55 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await testSubjects.existOrFail('discoverTableSampleSizeSettingsLink');
});
});
it('should update pagination when rows per page is changed', async () => {
const rows = await dataGrid.getDocTableRows();
expect(rows.length).to.be.above(0);
await testSubjects.existOrFail('pagination-button-0'); // first page
await testSubjects.existOrFail('pagination-button-4'); // last page
await testSubjects.click('tablePaginationPopoverButton');
await retry.try(async function () {
return testSubjects.exists('tablePagination-500-rows');
});
await testSubjects.click('tablePagination-500-rows');
await retry.try(async function () {
return !testSubjects.exists('pagination-button-1'); // only page 0 is left
});
await testSubjects.existOrFail('discoverTableFooter');
});
it('should render exact number of rows which where configured in the saved search or in settings', async () => {
await kibanaServer.uiSettings.update({
...defaultSettings,
'discover:sampleSize': 12,
'discover:sampleRowsPerPage': 6,
hideAnnouncements: true,
});
// first render is based on settings value
await PageObjects.common.navigateToApp('discover');
await PageObjects.discover.waitUntilSearchingHasFinished();
expect((await dataGrid.getDocTableRows()).length).to.be(6);
await dataGrid.checkCurrentRowsPerPageToBe(6);
// now we change it via popover
await dataGrid.changeRowsPerPageTo(10);
// save as a new search
const savedSearchTitle = 'search with saved rowsPerPage';
await PageObjects.discover.saveSearch(savedSearchTitle);
// start a new search session
await testSubjects.click('discoverNewButton');
await PageObjects.header.waitUntilLoadingHasFinished();
expect((await dataGrid.getDocTableRows()).length).to.be(6); // as in settings
await dataGrid.checkCurrentRowsPerPageToBe(6);
// open the saved search
await PageObjects.discover.loadSavedSearch(savedSearchTitle);
await PageObjects.discover.waitUntilSearchingHasFinished();
expect((await dataGrid.getDocTableRows()).length).to.be(10); // as in the saved search
await dataGrid.checkCurrentRowsPerPageToBe(10);
});
});
}

View file

@ -0,0 +1,81 @@
/*
* 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';
export default function ({ getService, getPageObjects }: FtrProviderContext) {
const browser = getService('browser');
const dataGrid = getService('dataGrid');
const dashboardAddPanel = getService('dashboardAddPanel');
const filterBar = getService('filterBar');
const esArchiver = getService('esArchiver');
const kibanaServer = getService('kibanaServer');
const PageObjects = getPageObjects(['common', 'dashboard', 'header', 'timePicker', 'discover']);
describe('discover saved search embeddable', () => {
before(async () => {
await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional');
await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/dashboard/current/data');
await kibanaServer.savedObjects.cleanStandardList();
await kibanaServer.importExport.load(
'test/functional/fixtures/kbn_archiver/dashboard/current/kibana'
);
await kibanaServer.uiSettings.replace({
defaultIndex: '0bf35f60-3dc9-11e8-8660-4d65aa086b3c',
});
await PageObjects.common.navigateToApp('dashboard');
await filterBar.ensureFieldEditorModalIsClosed();
await PageObjects.dashboard.gotoDashboardLandingPage();
await PageObjects.dashboard.clickNewDashboard();
await PageObjects.timePicker.setAbsoluteRange(
'Sep 22, 2015 @ 00:00:00.000',
'Sep 23, 2015 @ 00:00:00.000'
);
});
after(async () => {
await kibanaServer.savedObjects.cleanStandardList();
});
const addSearchEmbeddableToDashboard = async () => {
await dashboardAddPanel.addSavedSearch('Rendering-Test:-saved-search');
await PageObjects.header.waitUntilLoadingHasFinished();
await PageObjects.dashboard.waitForRenderComplete();
const rows = await dataGrid.getDocTableRows();
expect(rows.length).to.be.above(0);
};
const refreshDashboardPage = async () => {
await browser.refresh();
await PageObjects.header.waitUntilLoadingHasFinished();
await PageObjects.dashboard.waitForRenderComplete();
};
it('can save a search embeddable with a defined rows per page number', async function () {
const dashboardName = 'Dashboard with a Paginated Saved Search';
await addSearchEmbeddableToDashboard();
await dataGrid.checkCurrentRowsPerPageToBe(100);
await PageObjects.dashboard.saveDashboard(dashboardName, {
waitDialogIsClosed: true,
exitFromEditMode: false,
});
await refreshDashboardPage();
await dataGrid.checkCurrentRowsPerPageToBe(100);
await dataGrid.changeRowsPerPageTo(10);
await PageObjects.dashboard.saveDashboard(dashboardName);
await refreshDashboardPage();
await dataGrid.checkCurrentRowsPerPageToBe(10);
});
});
}

View file

@ -68,6 +68,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) {
loadTestFile(require.resolve('./_data_view_editor'));
loadTestFile(require.resolve('./_hide_announcements'));
loadTestFile(require.resolve('./classic/_hide_announcements'));
loadTestFile(require.resolve('./embeddable/saved_search_embeddable'));
}
});
}

View file

@ -325,4 +325,23 @@ export class DataGridService extends FtrService {
public async hasNoResults() {
return await this.find.existsByCssSelector('.euiDataGrid__noResults');
}
public async checkCurrentRowsPerPageToBe(value: number) {
await this.retry.try(async () => {
return (
(await this.testSubjects.getVisibleText('tablePaginationPopoverButton')) ===
`Rows per page: ${value}`
);
});
}
public async changeRowsPerPageTo(newValue: number) {
await this.testSubjects.click('tablePaginationPopoverButton');
const option = `tablePagination-${newValue}-rows`;
await this.retry.try(async () => {
return this.testSubjects.exists(option);
});
await this.testSubjects.click(option);
await this.checkCurrentRowsPerPageToBe(newValue);
}
}

View file

@ -2896,8 +2896,6 @@
"discover.advancedSettings.params.maxCellHeightTitle": "Hauteur de cellule maximale dans le tableau classique",
"discover.advancedSettings.params.rowHeightText": "Nombre de sous-lignes à autoriser dans une ligne. La valeur -1 ajuste automatiquement la hauteur de ligne selon le contenu. La valeur 0 affiche le contenu en une seule ligne.",
"discover.advancedSettings.params.rowHeightTitle": "Hauteur de ligne dans l'explorateur de documents",
"discover.advancedSettings.sampleSizeText": "Le nombre de lignes à afficher dans le tableau",
"discover.advancedSettings.sampleSizeTitle": "Nombre de lignes",
"discover.advancedSettings.searchOnPageLoadText": "Détermine si une recherche est exécutée lors du premier chargement de Discover. Ce paramètre n'a pas d'effet lors du chargement dune recherche enregistrée.",
"discover.advancedSettings.searchOnPageLoadTitle": "Recherche au chargement de la page",
"discover.advancedSettings.sortDefaultOrderText": "Détermine le sens de tri par défaut pour les vues de données temporelles dans l'application Discover.",

View file

@ -2896,8 +2896,6 @@
"discover.advancedSettings.params.maxCellHeightTitle": "クラシック表の最大セル高さ",
"discover.advancedSettings.params.rowHeightText": "行に追加できる線数。値-1は、コンテンツに合わせて、行の高さを自動的に調整します。値0はコンテンツが1行に表示されます。",
"discover.advancedSettings.params.rowHeightTitle": "ドキュメントエクスプローラーの行高さ",
"discover.advancedSettings.sampleSizeText": "表に表示する行数です",
"discover.advancedSettings.sampleSizeTitle": "行数",
"discover.advancedSettings.searchOnPageLoadText": "Discover の最初の読み込み時に検索を実行するかを制御します。この設定は、保存された検索の読み込み時には影響しません。",
"discover.advancedSettings.searchOnPageLoadTitle": "ページの読み込み時の検索",
"discover.advancedSettings.sortDefaultOrderText": "Discover アプリのデータビューに基づく時刻のデフォルトの並べ替え方向をコントロールします。",

View file

@ -2898,8 +2898,6 @@
"discover.advancedSettings.params.maxCellHeightTitle": "经典表中的最大单元格高度",
"discover.advancedSettings.params.rowHeightText": "一行中允许的文本行数。值为 -1 时,会自动调整行高以适应内容。值为 0 时,会在单文本行中显示内容。",
"discover.advancedSettings.params.rowHeightTitle": "Document Explorer 中的行高",
"discover.advancedSettings.sampleSizeText": "要在表中显示的行数目",
"discover.advancedSettings.sampleSizeTitle": "行数目",
"discover.advancedSettings.searchOnPageLoadText": "控制在 Discover 首次加载时是否执行搜索。加载已保存搜索时,此设置无效。",
"discover.advancedSettings.searchOnPageLoadTitle": "在页面加载时搜索",
"discover.advancedSettings.sortDefaultOrderText": "在 Discover 应用中控制基于时间的数据视图的默认排序方向。",