diff --git a/examples/content_management_examples/public/examples/finder/finder_app.tsx b/examples/content_management_examples/public/examples/finder/finder_app.tsx index 99ec949fac7d1..b8aaa6fe5f34b 100644 --- a/examples/content_management_examples/public/examples/finder/finder_app.tsx +++ b/examples/content_management_examples/public/examples/finder/finder_app.tsx @@ -23,6 +23,7 @@ export const FinderApp = (props: { ({ defaultValue: false, }, }); + +export const localStorageMock = (): IStorage => { + let store: Record = {}; + + return { + getItem: (key: string) => { + return store[key] || null; + }, + setItem: (key: string, value: unknown) => { + store[key] = value; + }, + clear() { + store = {}; + }, + removeItem(key: string) { + delete store[key]; + }, + }; +}; diff --git a/packages/content-management/table_list_view_table/src/table_list_view.test.tsx b/packages/content-management/table_list_view_table/src/table_list_view.test.tsx index 38229399f2ec8..aebaca335db5f 100644 --- a/packages/content-management/table_list_view_table/src/table_list_view.test.tsx +++ b/packages/content-management/table_list_view_table/src/table_list_view.test.tsx @@ -18,7 +18,7 @@ import type { LocationDescriptor, History } from 'history'; import type { UserContentCommonSchema } from '@kbn/content-management-table-list-view-common'; import { WithServices } from './__jest__'; -import { getTagList } from './mocks'; +import { getTagList, localStorageMock } from './mocks'; import { TableListViewTable, type TableListViewTableProps } from './table_list_view_table'; import { getActions } from './table_list_view.test.helpers'; import type { Services } from './services'; @@ -335,6 +335,12 @@ describe('TableListView', () => { const totalItems = 30; const updatedAt = new Date().toISOString(); + beforeEach(() => { + Object.defineProperty(window, 'localStorage', { + value: localStorageMock(), + }); + }); + const hits: UserContentCommonSchema[] = [...Array(totalItems)].map((_, i) => ({ id: `item${i}`, type: 'dashboard', @@ -429,6 +435,54 @@ describe('TableListView', () => { expect(firstRowTitle).toBe('Item 20'); expect(lastRowTitle).toBe('Item 29'); }); + + test('should persist the number of rows in the table', async () => { + let testBed: TestBed; + + const tableId = 'myTable'; + + await act(async () => { + testBed = await setup({ + initialPageSize, + findItems: jest.fn().mockResolvedValue({ total: hits.length, hits: [...hits] }), + id: tableId, + }); + }); + + { + const { component, table, find } = testBed!; + component.update(); + + const { tableCellsValues } = table.getMetaData('itemsInMemTable'); + expect(tableCellsValues.length).toBe(20); // 20 by default + + let storageValue = localStorage.getItem(`tablePersist:${tableId}`); + expect(storageValue).toBe(null); + + find('tablePaginationPopoverButton').simulate('click'); + find('tablePagination-10-rows').simulate('click'); + + storageValue = localStorage.getItem(`tablePersist:${tableId}`); + expect(storageValue).not.toBe(null); + expect(JSON.parse(storageValue!).pageSize).toBe(10); + } + + // Mount a second table and verify that is shows only 10 rows + { + await act(async () => { + testBed = await setup({ + initialPageSize, + findItems: jest.fn().mockResolvedValue({ total: hits.length, hits: [...hits] }), + id: tableId, + }); + }); + + const { component, table } = testBed!; + component.update(); + const { tableCellsValues } = table.getMetaData('itemsInMemTable'); + expect(tableCellsValues.length).toBe(10); // 10 items this time + } + }); }); describe('column sorting', () => { diff --git a/packages/content-management/table_list_view_table/src/table_list_view_table.tsx b/packages/content-management/table_list_view_table/src/table_list_view_table.tsx index 1fe5123d54151..c7653c668f0df 100644 --- a/packages/content-management/table_list_view_table/src/table_list_view_table.tsx +++ b/packages/content-management/table_list_view_table/src/table_list_view_table.tsx @@ -43,6 +43,7 @@ import { ContentInsightsProvider, useContentInsightsServices, } from '@kbn/content-management-content-insights-public'; +import { useEuiTablePersist } from '@kbn/shared-ux-table-persist'; import { Table, @@ -443,7 +444,7 @@ function TableListViewTableComp({ hasUpdatedAtMetadata, hasCreatedByMetadata, hasRecentlyAccessedMetadata, - pagination, + pagination: _pagination, tableSort, tableFilter, } = state; @@ -903,7 +904,7 @@ function TableListViewTableComp({ [updateTableSortFilterAndPagination] ); - const onTableChange = useCallback( + const customOnTableChange = useCallback( (criteria: CriteriaWithPagination) => { const data: { sort?: State['tableSort']; @@ -1038,6 +1039,20 @@ function TableListViewTableComp({ ); }, [entityName, fetchError]); + const { pageSize, onTableChange } = useEuiTablePersist({ + tableId: listingId, + initialPageSize, + customOnTableChange, + pageSizeOptions: uniq([10, 20, 50, initialPageSize]).sort(), + }); + + const pagination = useMemo(() => { + return { + ..._pagination, + pageSize, + }; + }, [_pagination, pageSize]); + // ------------ // Effects // ------------ diff --git a/packages/content-management/table_list_view_table/tsconfig.json b/packages/content-management/table_list_view_table/tsconfig.json index a5530ee717e49..90a96953570fb 100644 --- a/packages/content-management/table_list_view_table/tsconfig.json +++ b/packages/content-management/table_list_view_table/tsconfig.json @@ -37,7 +37,9 @@ "@kbn/content-management-user-profiles", "@kbn/recently-accessed", "@kbn/content-management-content-insights-public", - "@kbn/content-management-favorites-public" + "@kbn/content-management-favorites-public", + "@kbn/kibana-utils-plugin", + "@kbn/shared-ux-table-persist" ], "exclude": [ "target/**/*" diff --git a/packages/kbn-alerts-ui-shared/src/alert_fields_table/index.tsx b/packages/kbn-alerts-ui-shared/src/alert_fields_table/index.tsx index 3f3940e98bf4a..3da86b5f848f7 100644 --- a/packages/kbn-alerts-ui-shared/src/alert_fields_table/index.tsx +++ b/packages/kbn-alerts-ui-shared/src/alert_fields_table/index.tsx @@ -13,12 +13,13 @@ import { EuiTabbedContent, EuiTabbedContentProps, useEuiOverflowScroll, + EuiBasicTableColumn, } from '@elastic/eui'; import { css } from '@emotion/react'; -import React, { memo, useCallback, useMemo, useState } from 'react'; +import React, { memo, useMemo } from 'react'; import { Alert } from '@kbn/alerting-types'; import { euiThemeVars } from '@kbn/ui-theme'; -import { EuiBasicTableColumn } from '@elastic/eui/src/components/basic_table/basic_table'; +import { useEuiTablePersist } from '@kbn/shared-ux-table-persist'; export const search = { box: { @@ -66,28 +67,6 @@ export const ScrollableFlyoutTabbedContent = (props: EuiTabbedContentProps) => ( const COUNT_PER_PAGE_OPTIONS = [25, 50, 100]; -const useFieldBrowserPagination = () => { - const [pagination, setPagination] = useState<{ pageIndex: number }>({ - pageIndex: 0, - }); - - const onTableChange = useCallback(({ page: { index } }: { page: { index: number } }) => { - setPagination({ pageIndex: index }); - }, []); - const paginationTableProp = useMemo( - () => ({ - ...pagination, - pageSizeOptions: COUNT_PER_PAGE_OPTIONS, - }), - [pagination] - ); - - return { - onTableChange, - paginationTableProp, - }; -}; - type AlertField = Exclude< { [K in keyof Alert]: { key: K; value: Alert[K] }; @@ -111,7 +90,11 @@ export interface AlertFieldsTableProps { * A paginated, filterable table to show alert object fields */ export const AlertFieldsTable = memo(({ alert, fields }: AlertFieldsTableProps) => { - const { onTableChange, paginationTableProp } = useFieldBrowserPagination(); + const { pageSize, sorting, onTableChange } = useEuiTablePersist({ + tableId: 'obltAlertFields', + initialPageSize: 25, + }); + const items = useMemo(() => { let _items = Object.entries(alert).map( ([key, value]) => @@ -131,7 +114,11 @@ export const AlertFieldsTable = memo(({ alert, fields }: AlertFieldsTableProps) itemId="key" columns={columns} onTableChange={onTableChange} - pagination={paginationTableProp} + pagination={{ + pageSize, + pageSizeOptions: COUNT_PER_PAGE_OPTIONS, + }} + sorting={sorting} search={search} css={css` & .euiTableRow { diff --git a/packages/kbn-alerts-ui-shared/tsconfig.json b/packages/kbn-alerts-ui-shared/tsconfig.json index 0da17dfe3d1ac..317f80dd209f3 100644 --- a/packages/kbn-alerts-ui-shared/tsconfig.json +++ b/packages/kbn-alerts-ui-shared/tsconfig.json @@ -49,6 +49,7 @@ "@kbn/core-ui-settings-browser", "@kbn/core-http-browser-mocks", "@kbn/core-notifications-browser-mocks", - "@kbn/kibana-react-plugin" + "@kbn/kibana-react-plugin", + "@kbn/shared-ux-table-persist" ] } diff --git a/packages/kbn-esql-editor/src/editor_footer/query_history.tsx b/packages/kbn-esql-editor/src/editor_footer/query_history.tsx index 864306737e9ca..7316a5b49ddea 100644 --- a/packages/kbn-esql-editor/src/editor_footer/query_history.tsx +++ b/packages/kbn-esql-editor/src/editor_footer/query_history.tsx @@ -17,7 +17,6 @@ import { EuiInMemoryTable, EuiBasicTableColumn, EuiButtonEmpty, - Criteria, EuiButtonIcon, CustomItemAction, EuiCopy, @@ -25,6 +24,7 @@ import { euiScrollBarStyles, } from '@elastic/eui'; import { css, Interpolation, Theme } from '@emotion/react'; +import { useEuiTablePersist } from '@kbn/shared-ux-table-persist'; import { type QueryHistoryItem, getHistoryItems } from '../history_local_storage'; import { getReducedSpaceStyling, swapArrayElements } from './query_history_helpers'; @@ -212,8 +212,16 @@ export function QueryHistory({ }) { const theme = useEuiTheme(); const scrollBarStyles = euiScrollBarStyles(theme); - const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('desc'); - const historyItems: QueryHistoryItem[] = getHistoryItems(sortDirection); + + const { sorting, onTableChange } = useEuiTablePersist({ + tableId: 'esqlQueryHistory', + initialSort: { + field: 'timeRan', + direction: 'desc', + }, + }); + + const historyItems: QueryHistoryItem[] = getHistoryItems(sorting.sort.direction); const actions: Array> = useMemo(() => { return [ @@ -276,19 +284,6 @@ export function QueryHistory({ return getTableColumns(containerWidth, isOnReducedSpaceLayout, actions); }, [actions, containerWidth, isOnReducedSpaceLayout]); - const onTableChange = ({ page, sort }: Criteria) => { - if (sort) { - const { direction } = sort; - setSortDirection(direction); - } - }; - - const sorting = { - sort: { - field: 'timeRan', - direction: sortDirection, - }, - }; const { euiTheme } = theme; const extraStyling = isOnReducedSpaceLayout ? getReducedSpaceStyling() : ''; diff --git a/packages/kbn-esql-editor/tsconfig.json b/packages/kbn-esql-editor/tsconfig.json index c26b971e5231c..075c5ff9ab457 100644 --- a/packages/kbn-esql-editor/tsconfig.json +++ b/packages/kbn-esql-editor/tsconfig.json @@ -28,6 +28,7 @@ "@kbn/fields-metadata-plugin", "@kbn/esql-validation-autocomplete", "@kbn/esql-utils", + "@kbn/shared-ux-table-persist", ], "exclude": [ "target/**/*", diff --git a/packages/kbn-event-annotation-components/components/event_annotation_group_saved_object_finder.tsx b/packages/kbn-event-annotation-components/components/event_annotation_group_saved_object_finder.tsx index 2f9c7afcd2190..38a701abdd81c 100644 --- a/packages/kbn-event-annotation-components/components/event_annotation_group_saved_object_finder.tsx +++ b/packages/kbn-event-annotation-components/components/event_annotation_group_saved_object_finder.tsx @@ -100,6 +100,7 @@ export const EventAnnotationGroupSavedObjectFinder = ({ ) : ( { onChoose({ id, type, fullName, savedObject }); diff --git a/packages/shared-ux/table_persist/index.ts b/packages/shared-ux/table_persist/index.ts index da2596dac14ab..c46f6a8e8a00a 100644 --- a/packages/shared-ux/table_persist/index.ts +++ b/packages/shared-ux/table_persist/index.ts @@ -7,4 +7,5 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -export { useEuiTablePersist, DEFAULT_PAGE_SIZE_OPTIONS } from './src'; +export { useEuiTablePersist, DEFAULT_PAGE_SIZE_OPTIONS, withEuiTablePersist } from './src'; +export type { EuiTablePersistInjectedProps, EuiTablePersistPropsGetter, HOCProps } from './src'; diff --git a/packages/shared-ux/table_persist/src/index.ts b/packages/shared-ux/table_persist/src/index.ts index 20416b5cec902..33ac8b18d1d34 100644 --- a/packages/shared-ux/table_persist/src/index.ts +++ b/packages/shared-ux/table_persist/src/index.ts @@ -9,3 +9,9 @@ export { useEuiTablePersist } from './use_table_persist'; export { DEFAULT_PAGE_SIZE_OPTIONS } from './constants'; +export { withEuiTablePersist } from './table_persist_hoc'; +export type { + EuiTablePersistInjectedProps, + EuiTablePersistPropsGetter, + HOCProps, +} from './table_persist_hoc'; diff --git a/packages/shared-ux/table_persist/src/table_persist_hoc.test.tsx b/packages/shared-ux/table_persist/src/table_persist_hoc.test.tsx new file mode 100644 index 0000000000000..bed84111379bb --- /dev/null +++ b/packages/shared-ux/table_persist/src/table_persist_hoc.test.tsx @@ -0,0 +1,113 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import React, { PureComponent } from 'react'; +import { render, screen } from '@testing-library/react'; + +import { withEuiTablePersist, type EuiTablePersistInjectedProps } from './table_persist_hoc'; + +const mockUseEuiTablePersist = jest.fn().mockReturnValue({ + pageSize: 'mockPageSize', + sorting: 'mockSorting', + onTableChange: 'mockOnTableChange', +}); + +jest.mock('./use_table_persist', () => { + const original = jest.requireActual('./use_table_persist'); + + return { + ...original, + useEuiTablePersist: (...args: unknown[]) => mockUseEuiTablePersist(...args), + }; +}); + +class TestComponent extends PureComponent> { + constructor(props: EuiTablePersistInjectedProps) { + super(props); + } + + render() { + return
{JSON.stringify(this.props.euiTablePersist)}
; + } +} + +describe('withEuiTablePersist', () => { + it('should call useEuiTablePersist and return its values', () => { + const customOnTableChange = jest.fn(); + const pageSizeOptions = [5, 10, 25, 50]; + + const WrappedComponent = withEuiTablePersist(TestComponent, { + tableId: 'testTableId', + initialPageSize: 10, + initialSort: { field: 'testField', direction: 'asc' }, + customOnTableChange, + pageSizeOptions, + }); + + render(); + + expect(mockUseEuiTablePersist).toHaveBeenCalledWith({ + tableId: 'testTableId', + customOnTableChange, + initialPageSize: 10, + initialSort: { field: 'testField', direction: 'asc' }, + pageSizeOptions, + }); + + expect(screen.getByTestId('value').textContent).toBe( + JSON.stringify({ + pageSize: 'mockPageSize', + sorting: 'mockSorting', + onTableChange: 'mockOnTableChange', + }) + ); + }); + + it('should allow override through props', () => { + const customOnTableChangeDefault = jest.fn(); + const customOnTableChangeProp = jest.fn(); + const pageSizeOptions = [5, 10, 25, 50]; + + const WrappedComponent = withEuiTablePersist(TestComponent, { + tableId: 'testTableId', + initialPageSize: 10, + initialSort: { field: 'testField', direction: 'asc' }, + customOnTableChange: customOnTableChangeDefault, + pageSizeOptions, + }); + + render( + + ); + + expect(mockUseEuiTablePersist).toHaveBeenCalledWith({ + tableId: 'testTableIdChanged', + customOnTableChange: customOnTableChangeProp, + initialPageSize: 20, + initialSort: { field: 'testFieldChanged', direction: 'desc' }, + pageSizeOptions: [5], + }); + + expect(screen.getByTestId('value').textContent).toBe( + JSON.stringify({ + pageSize: 'mockPageSize', + sorting: 'mockSorting', + onTableChange: 'mockOnTableChange', + }) + ); + }); +}); diff --git a/packages/shared-ux/table_persist/src/table_persist_hoc.tsx b/packages/shared-ux/table_persist/src/table_persist_hoc.tsx new file mode 100644 index 0000000000000..313cf1a1a21a2 --- /dev/null +++ b/packages/shared-ux/table_persist/src/table_persist_hoc.tsx @@ -0,0 +1,74 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ +import React from 'react'; +import { type CriteriaWithPagination } from '@elastic/eui'; +import { EuiTablePersistProps, useEuiTablePersist } from './use_table_persist'; +import { PropertySort } from './types'; + +export interface EuiTablePersistInjectedProps { + euiTablePersist: { + /** The EuiInMemoryTable onTableChange prop */ + onTableChange: (change: CriteriaWithPagination) => void; + /** The EuiInMemoryTable sorting prop */ + sorting: { sort: PropertySort } | true; + /** The EuiInMemoryTable pagination.pageSize value */ + pageSize: number; + }; +} + +export type EuiTablePersistPropsGetter = ( + props: Omit> +) => EuiTablePersistProps; + +export type HOCProps = P & { + /** Custom value for the EuiTablePersist HOC */ + euiTablePersistProps?: Partial>; +}; + +export function withEuiTablePersist( + WrappedComponent: React.ComponentClass>, + euiTablePersistDefault: + | (EuiTablePersistProps & { get?: undefined }) + | { + get: EuiTablePersistPropsGetter; + } +) { + const HOC: React.FC>>> = ( + props + ) => { + const getterOverride = euiTablePersistDefault.get ? euiTablePersistDefault.get(props) : {}; + + const mergedProps = { + ...euiTablePersistDefault, + ...props.euiTablePersistProps, + ...getterOverride, // Getter override other props + }; + + const { tableId, customOnTableChange, initialSort, initialPageSize, pageSizeOptions } = + mergedProps; + + if (!tableId) { + throw new Error('tableId is required'); + } + + const euiTablePersist = useEuiTablePersist({ + tableId, + customOnTableChange, + initialSort, + initialPageSize, + pageSizeOptions, + }); + + const { euiTablePersistProps, ...rest } = props; + + return ; + }; + + return HOC; +} diff --git a/packages/shared-ux/table_persist/src/use_table_persist.test.ts b/packages/shared-ux/table_persist/src/use_table_persist.test.ts index 235777aa5d294..51fbd93f7a214 100644 --- a/packages/shared-ux/table_persist/src/use_table_persist.test.ts +++ b/packages/shared-ux/table_persist/src/use_table_persist.test.ts @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { Criteria } from '@elastic/eui'; +import { CriteriaWithPagination } from '@elastic/eui'; import { renderHook, act } from '@testing-library/react-hooks'; import { useEuiTablePersist } from './use_table_persist'; import { createStorage } from './storage'; // Mock this if it's external @@ -58,7 +58,7 @@ describe('useEuiTablePersist', () => { }; act(() => { - result.current.onTableChange(nextCriteria as Criteria); + result.current.onTableChange(nextCriteria as CriteriaWithPagination); }); expect(result.current.pageSize).toBe(100); @@ -85,7 +85,7 @@ describe('useEuiTablePersist', () => { }; act(() => { - result.current.onTableChange(nextCriteria as Criteria); + result.current.onTableChange(nextCriteria as CriteriaWithPagination); }); expect(customOnTableChange).toHaveBeenCalledWith(nextCriteria); @@ -98,7 +98,7 @@ describe('useEuiTablePersist', () => { const { result } = renderHook(() => useEuiTablePersist({ tableId: 'testTable' })); act(() => { - result.current.onTableChange({}); // Empty change + result.current.onTableChange({} as CriteriaWithPagination); // Empty change }); expect(result.current.pageSize).toBe(25); @@ -118,7 +118,7 @@ describe('useEuiTablePersist', () => { }; act(() => { - result.current.onTableChange(nextCriteria as Criteria); + result.current.onTableChange(nextCriteria as CriteriaWithPagination); }); expect(result.current.pageSize).toBe(100); diff --git a/packages/shared-ux/table_persist/src/use_table_persist.ts b/packages/shared-ux/table_persist/src/use_table_persist.ts index 9c3b7a75788b0..bf91f66beb292 100644 --- a/packages/shared-ux/table_persist/src/use_table_persist.ts +++ b/packages/shared-ux/table_persist/src/use_table_persist.ts @@ -8,7 +8,7 @@ */ import { useState, useCallback } from 'react'; -import { Criteria } from '@elastic/eui'; +import type { CriteriaWithPagination } from '@elastic/eui'; import { DEFAULT_INITIAL_PAGE_SIZE, DEFAULT_PAGE_SIZE_OPTIONS } from './constants'; import { createStorage } from './storage'; import { validatePersistData } from './validate_persist_data'; @@ -18,7 +18,7 @@ export interface EuiTablePersistProps { /** A unique id that will be included in the local storage variable for this table. */ tableId: string; /** (Optional) Specifies a custom onTableChange handler. */ - customOnTableChange?: (change: Criteria) => void; + customOnTableChange?: (change: CriteriaWithPagination) => void; /** (Optional) Specifies a custom initial table sorting. */ initialSort?: PropertySort; /** (Optional) Specifies a custom initial page size for the table. Defaults to 50. */ @@ -33,13 +33,37 @@ export interface EuiTablePersistProps { * Returns the persisting page size and sort and the onTableChange handler that should be passed * as props to an Eui table component. */ -export const useEuiTablePersist = ({ +export function useEuiTablePersist( + props: EuiTablePersistProps & { initialSort: PropertySort } +): { + sorting: { sort: PropertySort }; + pageSize: number; + onTableChange: (nextValues: CriteriaWithPagination) => void; +}; + +export function useEuiTablePersist( + props: EuiTablePersistProps & { initialSort?: undefined } +): { + sorting: true; + pageSize: number; + onTableChange: (nextValues: CriteriaWithPagination) => void; +}; + +export function useEuiTablePersist( + props: EuiTablePersistProps +): { + sorting: true | { sort: PropertySort }; + pageSize: number; + onTableChange: (nextValues: CriteriaWithPagination) => void; +}; + +export function useEuiTablePersist({ tableId, customOnTableChange, initialSort, initialPageSize, pageSizeOptions, -}: EuiTablePersistProps) => { +}: EuiTablePersistProps) { const storage = createStorage(); const storedPersistData = storage.get(tableId, undefined); @@ -55,7 +79,7 @@ export const useEuiTablePersist = ({ const sorting = sort ? { sort } : true; // If sort is undefined, return true to allow sorting const onTableChange = useCallback( - (nextValues: Criteria) => { + (nextValues: CriteriaWithPagination) => { if (customOnTableChange) { customOnTableChange(nextValues); } @@ -92,4 +116,4 @@ export const useEuiTablePersist = ({ ); return { pageSize, sorting, onTableChange }; -}; +} diff --git a/src/plugins/data/public/search/session/sessions_mgmt/components/table/table.tsx b/src/plugins/data/public/search/session/sessions_mgmt/components/table/table.tsx index 211fcdcd50235..3e41263c8750f 100644 --- a/src/plugins/data/public/search/session/sessions_mgmt/components/table/table.tsx +++ b/src/plugins/data/public/search/session/sessions_mgmt/components/table/table.tsx @@ -13,6 +13,7 @@ import { CoreStart } from '@kbn/core/public'; import moment from 'moment'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import useDebounce from 'react-use/lib/useDebounce'; +import { useEuiTablePersist } from '@kbn/shared-ux-table-persist'; import { TableText } from '..'; import { SEARCH_SESSIONS_TABLE_ID } from '../../../../../../common'; import { SearchSessionsMgmtAPI } from '../../lib/api'; @@ -45,7 +46,6 @@ export function SearchSessionsMgmtTable({ const [tableData, setTableData] = useState([]); const [isLoading, setIsLoading] = useState(false); const [debouncedIsLoading, setDebouncedIsLoading] = useState(false); - const [pagination, setPagination] = useState({ pageIndex: 0 }); const showLatestResultsHandler = useRef(); const refreshTimeoutRef = useRef(null); const refreshInterval = useMemo( @@ -53,6 +53,14 @@ export function SearchSessionsMgmtTable({ [config.management.refreshInterval] ); + const { pageSize, sorting, onTableChange } = useEuiTablePersist({ + tableId: 'searchSessionsMgmt', + initialSort: { + field: 'created', + direction: 'desc', + }, + }); + // Debounce rendering the state of the Refresh button useDebounce( () => { @@ -148,12 +156,12 @@ export function SearchSessionsMgmtTable({ searchUsageCollector )} items={tableData} - pagination={pagination} - search={search} - sorting={{ sort: { field: 'created', direction: 'desc' } }} - onTableChange={({ page: { index } }) => { - setPagination({ pageIndex: index }); + pagination={{ + pageSize, }} + search={search} + sorting={sorting} + onTableChange={onTableChange} tableLayout="auto" /> ); diff --git a/src/plugins/data/public/utils/table_inspector_view/components/data_table.tsx b/src/plugins/data/public/utils/table_inspector_view/components/data_table.tsx index ebb97fd210f85..10d95bcc46906 100644 --- a/src/plugins/data/public/utils/table_inspector_view/components/data_table.tsx +++ b/src/plugins/data/public/utils/table_inspector_view/components/data_table.tsx @@ -22,11 +22,17 @@ import { FormattedMessage } from '@kbn/i18n-react'; import { i18n } from '@kbn/i18n'; import { IUiSettingsClient } from '@kbn/core/public'; -import { Datatable, DatatableColumn } from '@kbn/expressions-plugin/public'; +import { Datatable, DatatableColumn, DatatableRow } from '@kbn/expressions-plugin/public'; import { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; import { UiActionsStart } from '@kbn/ui-actions-plugin/public'; +import { + withEuiTablePersist, + type EuiTablePersistInjectedProps, +} from '@kbn/shared-ux-table-persist/src'; import { DataViewRow, DataViewColumn } from '../types'; +const PAGE_SIZE_OPTIONS = [10, 20, 50]; + interface DataTableFormatState { columns: DataViewColumn[]; rows: DataViewRow[]; @@ -49,7 +55,10 @@ interface RenderCellArguments { isFilterable: boolean; } -export class DataTableFormat extends Component { +class DataTableFormatClass extends Component< + DataTableFormatProps & EuiTablePersistInjectedProps, + DataTableFormatState +> { static propTypes = { data: PropTypes.object.isRequired, uiSettings: PropTypes.object.isRequired, @@ -169,7 +178,7 @@ export class DataTableFormat extends Component row[dataColumn.id] === value) || 0; - return DataTableFormat.renderCell({ + return DataTableFormatClass.renderCell({ table: data, columnIndex: index, rowIndex, @@ -186,9 +195,10 @@ export class DataTableFormat extends Component {}, + sorting: { sort: { direction: 'asc' as const, field: 'name' as const } }, + }, +}; + const renderTable = ( { editField } = { editField: () => {}, @@ -87,6 +100,7 @@ const renderTable = ( ) => shallow( { +const PAGE_SIZE_OPTIONS = [5, 10, 25, 50]; + +class TableClass extends PureComponent< + IndexedFieldProps & EuiTablePersistInjectedProps +> { renderBooleanTemplate(value: string, arialLabel: string) { return value ? : ; } @@ -403,11 +411,17 @@ export class Table extends PureComponent { } render() { - const { items, editField, deleteField, indexPattern } = this.props; + const { + items, + editField, + deleteField, + indexPattern, + euiTablePersist: { pageSize, sorting, onTableChange }, + } = this.props; const pagination = { - initialPageSize: 10, - pageSizeOptions: [5, 10, 25, 50], + pageSize, + pageSizeOptions: PAGE_SIZE_OPTIONS, }; const columns: Array> = [ @@ -508,8 +522,18 @@ export class Table extends PureComponent { items={items} columns={columns} pagination={pagination} - sorting={{ sort: { field: 'displayName', direction: 'asc' } }} + sorting={sorting} + onTableChange={onTableChange} /> ); } } + +export const TableWithoutPersist = TableClass; // For testing purposes + +export const Table = withEuiTablePersist(TableClass, { + tableId: 'dataViewsIndexedFields', + pageSizeOptions: PAGE_SIZE_OPTIONS, + initialSort: { field: 'displayName', direction: 'asc' }, + initialPageSize: 10, +}); diff --git a/src/plugins/data_view_management/public/components/edit_index_pattern/relationships_table/relationships_table.tsx b/src/plugins/data_view_management/public/components/edit_index_pattern/relationships_table/relationships_table.tsx index 06991b2081639..5fb2adf8697ab 100644 --- a/src/plugins/data_view_management/public/components/edit_index_pattern/relationships_table/relationships_table.tsx +++ b/src/plugins/data_view_management/public/components/edit_index_pattern/relationships_table/relationships_table.tsx @@ -18,6 +18,7 @@ import { import { CoreStart } from '@kbn/core/public'; import { get } from 'lodash'; import { RedirectAppLinks } from '@kbn/shared-ux-link-redirect-app'; +import { useEuiTablePersist } from '@kbn/shared-ux-table-persist'; import { SavedObjectRelation, @@ -139,12 +140,20 @@ export const RelationshipsTable = ({ ] as SearchFilterConfig[], }; + const { pageSize, onTableChange } = useEuiTablePersist({ + tableId: 'dataViewMgmtRelationships', + initialPageSize: 10, + }); + return ( items={relationships} columns={columns} - pagination={true} + pagination={{ + pageSize, + }} + onTableChange={onTableChange} search={search} rowProps={() => ({ 'data-test-subj': `relationshipsTableRow`, diff --git a/src/plugins/data_view_management/public/components/edit_index_pattern/scripted_fields_table/components/table/__snapshots__/table.test.tsx.snap b/src/plugins/data_view_management/public/components/edit_index_pattern/scripted_fields_table/components/table/__snapshots__/table.test.tsx.snap index 5f8e34d0776ec..f3fee53256c67 100644 --- a/src/plugins/data_view_management/public/components/edit_index_pattern/scripted_fields_table/components/table/__snapshots__/table.test.tsx.snap +++ b/src/plugins/data_view_management/public/components/edit_index_pattern/scripted_fields_table/components/table/__snapshots__/table.test.tsx.snap @@ -75,9 +75,10 @@ exports[`Table should render normally 1`] = ` }, ] } + onTableChange={[Function]} pagination={ Object { - "initialPageSize": 10, + "pageSize": 10, "pageSizeOptions": Array [ 5, 10, @@ -87,7 +88,14 @@ exports[`Table should render normally 1`] = ` } } searchFormat="eql" - sorting={true} + sorting={ + Object { + "sort": Object { + "direction": "asc", + "field": "name", + }, + } + } tableLayout="fixed" /> `; diff --git a/src/plugins/data_view_management/public/components/edit_index_pattern/scripted_fields_table/components/table/table.test.tsx b/src/plugins/data_view_management/public/components/edit_index_pattern/scripted_fields_table/components/table/table.test.tsx index 9ef16a1cb1531..29b51160e4730 100644 --- a/src/plugins/data_view_management/public/components/edit_index_pattern/scripted_fields_table/components/table/table.test.tsx +++ b/src/plugins/data_view_management/public/components/edit_index_pattern/scripted_fields_table/components/table/table.test.tsx @@ -10,7 +10,7 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { Table } from '.'; +import { TableWithoutPersist as Table } from './table'; import { ScriptedFieldItem } from '../../types'; import { DataView } from '@kbn/data-views-plugin/public'; @@ -21,6 +21,14 @@ const items: ScriptedFieldItem[] = [ { name: '2', lang: 'painless', script: '', isUserEditable: false }, ]; +const baseProps = { + euiTablePersist: { + pageSize: 10, + onTableChange: () => {}, + sorting: { sort: { direction: 'asc' as const, field: 'name' as const } }, + }, +}; + describe('Table', () => { let indexPattern: DataView; @@ -37,8 +45,9 @@ describe('Table', () => { }); test('should render normally', () => { - const component = shallow
( + const component = shallow(
{}} @@ -52,6 +61,7 @@ describe('Table', () => { test('should render the format', () => { const component = shallow(
{}} @@ -68,6 +78,7 @@ describe('Table', () => { const component = shallow(
{ const component = shallow(
{}} @@ -100,6 +112,7 @@ describe('Table', () => { test('should not allow edit or deletion for user with only read access', () => { const component = shallow(
{}} diff --git a/src/plugins/data_view_management/public/components/edit_index_pattern/scripted_fields_table/components/table/table.tsx b/src/plugins/data_view_management/public/components/edit_index_pattern/scripted_fields_table/components/table/table.tsx index f1561ed99f8fd..834e6792768c6 100644 --- a/src/plugins/data_view_management/public/components/edit_index_pattern/scripted_fields_table/components/table/table.tsx +++ b/src/plugins/data_view_management/public/components/edit_index_pattern/scripted_fields_table/components/table/table.tsx @@ -13,8 +13,14 @@ import { i18n } from '@kbn/i18n'; import { EuiInMemoryTable, EuiBasicTableColumn } from '@elastic/eui'; import { DataView } from '@kbn/data-views-plugin/public'; +import { + withEuiTablePersist, + type EuiTablePersistInjectedProps, +} from '@kbn/shared-ux-table-persist'; import { ScriptedFieldItem } from '../../types'; +const PAGE_SIZE_OPTIONS = [5, 10, 25, 50]; + interface TableProps { indexPattern: DataView; items: ScriptedFieldItem[]; @@ -22,7 +28,9 @@ interface TableProps { deleteField: (field: ScriptedFieldItem) => void; } -export class Table extends PureComponent { +class TableClass extends PureComponent< + TableProps & EuiTablePersistInjectedProps +> { renderFormatCell = (value: string) => { const { indexPattern } = this.props; const title = get(indexPattern, ['fieldFormatMap', value, 'type', 'title'], ''); @@ -31,7 +39,12 @@ export class Table extends PureComponent { }; render() { - const { items, editField, deleteField } = this.props; + const { + items, + editField, + deleteField, + euiTablePersist: { pageSize, sorting, onTableChange }, + } = this.props; const columns: Array> = [ { @@ -132,12 +145,26 @@ export class Table extends PureComponent { ]; const pagination = { - initialPageSize: 10, - pageSizeOptions: [5, 10, 25, 50], + pageSize, + pageSizeOptions: PAGE_SIZE_OPTIONS, }; return ( - + ); } } + +export const TableWithoutPersist = TableClass; // For testing purposes + +export const Table = withEuiTablePersist(TableClass, { + tableId: 'dataViewsScriptedFields', + pageSizeOptions: PAGE_SIZE_OPTIONS, + initialPageSize: 10, +}); diff --git a/src/plugins/data_view_management/public/components/edit_index_pattern/source_filters_table/components/table/__snapshots__/table.test.tsx.snap b/src/plugins/data_view_management/public/components/edit_index_pattern/source_filters_table/components/table/__snapshots__/table.test.tsx.snap index 7ddd6d34fb089..9469bacfc8a7d 100644 --- a/src/plugins/data_view_management/public/components/edit_index_pattern/source_filters_table/components/table/__snapshots__/table.test.tsx.snap +++ b/src/plugins/data_view_management/public/components/edit_index_pattern/source_filters_table/components/table/__snapshots__/table.test.tsx.snap @@ -79,9 +79,10 @@ exports[`Table should render normally 1`] = ` ] } loading={true} + onTableChange={[Function]} pagination={ Object { - "initialPageSize": 10, + "pageSize": 10, "pageSizeOptions": Array [ 5, 10, @@ -91,7 +92,14 @@ exports[`Table should render normally 1`] = ` } } searchFormat="eql" - sorting={true} + sorting={ + Object { + "sort": Object { + "direction": "asc", + "field": "clientId", + }, + } + } tableLayout="fixed" /> `; diff --git a/src/plugins/data_view_management/public/components/edit_index_pattern/source_filters_table/components/table/table.test.tsx b/src/plugins/data_view_management/public/components/edit_index_pattern/source_filters_table/components/table/table.test.tsx index db766ca9b3e54..695002440754f 100644 --- a/src/plugins/data_view_management/public/components/edit_index_pattern/source_filters_table/components/table/table.test.tsx +++ b/src/plugins/data_view_management/public/components/edit_index_pattern/source_filters_table/components/table/table.test.tsx @@ -10,7 +10,7 @@ import React, { ReactElement } from 'react'; import { shallow, ShallowWrapper } from 'enzyme'; -import { Table, TableProps, TableState } from './table'; +import { TableWithoutPersist as Table } from './table'; import { EuiTableFieldDataColumnType, keys } from '@elastic/eui'; import { DataView } from '@kbn/data-views-plugin/public'; import { SourceFiltersTableFilter } from '../../types'; @@ -20,10 +20,15 @@ const items: SourceFiltersTableFilter[] = [{ value: 'tim*', clientId: '' }]; const getIndexPatternMock = (mockedFields: any = {}) => ({ ...mockedFields } as DataView); -const getTableColumnRender = ( - component: ShallowWrapper, - index: number = 0 -) => { +const baseProps = { + euiTablePersist: { + pageSize: 10, + onTableChange: () => {}, + sorting: { sort: { direction: 'asc' as const, field: 'clientId' as const } }, + }, +}; + +const getTableColumnRender = (component: ShallowWrapper, index: number = 0) => { const columns = component.prop>>('columns'); return { @@ -35,6 +40,7 @@ describe('Table', () => { test('should render normally', () => { const component = shallow(
{}} @@ -48,8 +54,9 @@ describe('Table', () => { }); test('should render filter matches', () => { - const component = shallow
( + const component = shallow(
[{ name: 'time' }, { name: 'value' }], })} @@ -70,11 +77,12 @@ describe('Table', () => { describe('editing', () => { const saveFilter = jest.fn(); const clientId = '1'; - let component: ShallowWrapper; + let component: ShallowWrapper; beforeEach(() => { - component = shallow
( + component = shallow(
{}} @@ -125,6 +133,7 @@ describe('Table', () => { test('should update the matches dynamically as input value is changed', () => { const localComponent = shallow(
[{ name: 'time' }, { name: 'value' }], })} @@ -191,6 +200,7 @@ describe('Table', () => { const component = shallow(
{ const component = shallow(
{}} @@ -251,6 +262,7 @@ describe('Table', () => { const component = shallow(
{}} diff --git a/src/plugins/data_view_management/public/components/edit_index_pattern/source_filters_table/components/table/table.tsx b/src/plugins/data_view_management/public/components/edit_index_pattern/source_filters_table/components/table/table.tsx index 21de13871d03d..d43c72991c136 100644 --- a/src/plugins/data_view_management/public/components/edit_index_pattern/source_filters_table/components/table/table.tsx +++ b/src/plugins/data_view_management/public/components/edit_index_pattern/source_filters_table/components/table/table.tsx @@ -21,6 +21,11 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { DataView } from '@kbn/data-views-plugin/public'; +import { + withEuiTablePersist, + type EuiTablePersistInjectedProps, +} from '@kbn/shared-ux-table-persist'; + import { SourceFiltersTableFilter } from '../../types'; const filterHeader = i18n.translate( @@ -69,6 +74,8 @@ const cancelAria = i18n.translate( } ); +const PAGE_SIZE_OPTIONS = [5, 10, 25, 50]; + export interface TableProps { indexPattern: DataView; items: SourceFiltersTableFilter[]; @@ -83,8 +90,11 @@ export interface TableState { editingFilterValue: string; } -export class Table extends Component { - constructor(props: TableProps) { +class TableClass extends Component< + TableProps & EuiTablePersistInjectedProps, + TableState +> { + constructor(props: TableProps & EuiTablePersistInjectedProps) { super(props); this.state = { editingFilterId: '', @@ -227,11 +237,15 @@ export class Table extends Component { } render() { - const { items, isSaving } = this.props; + const { + items, + isSaving, + euiTablePersist: { pageSize, sorting, onTableChange }, + } = this.props; const columns = this.getColumns(); const pagination = { - initialPageSize: 10, - pageSizeOptions: [5, 10, 25, 50], + pageSize, + pageSizeOptions: PAGE_SIZE_OPTIONS, }; return ( @@ -240,8 +254,17 @@ export class Table extends Component { items={items} columns={columns} pagination={pagination} - sorting={true} + sorting={sorting} + onTableChange={onTableChange} /> ); } } + +export const TableWithoutPersist = TableClass; // For testing purposes + +export const Table = withEuiTablePersist(TableClass, { + tableId: 'dataViewsSourceFilters', + pageSizeOptions: PAGE_SIZE_OPTIONS, + initialPageSize: 10, +}); diff --git a/src/plugins/data_view_management/public/components/index_pattern_table/index_pattern_table.tsx b/src/plugins/data_view_management/public/components/index_pattern_table/index_pattern_table.tsx index 4512cb520c574..daabfe3fe6a9a 100644 --- a/src/plugins/data_view_management/public/components/index_pattern_table/index_pattern_table.tsx +++ b/src/plugins/data_view_management/public/components/index_pattern_table/index_pattern_table.tsx @@ -30,6 +30,8 @@ import { NoDataViewsPromptComponent, useOnTryESQL } from '@kbn/shared-ux-prompt- import type { SpacesContextProps } from '@kbn/spaces-plugin/public'; import { DataViewType } from '@kbn/data-views-plugin/public'; import { RollupDeprecationTooltip } from '@kbn/rollup'; +import { useEuiTablePersist } from '@kbn/shared-ux-table-persist'; + import type { IndexPatternManagmentContext } from '../../types'; import { getListBreadcrumbs } from '../breadcrumbs'; import { type RemoveDataViewProps, removeDataView } from '../edit_index_pattern'; @@ -42,10 +44,7 @@ import { deleteModalMsg } from './delete_modal_msg'; import { NoData } from './no_data'; import { SpacesList } from './spaces_list'; -const pagination = { - initialPageSize: 10, - pageSizeOptions: [5, 10, 25, 50], -}; +const PAGE_SIZE_OPTIONS = [5, 10, 25, 50]; const sorting = { sort: { @@ -123,6 +122,12 @@ export const IndexPatternTable = ({ }; const onTryESQL = useOnTryESQL(useOnTryESQLParams); + const { pageSize, onTableChange } = useEuiTablePersist({ + tableId: 'dataViewsIndexPattern', + initialPageSize: 10, + pageSizeOptions: PAGE_SIZE_OPTIONS, + }); + const handleOnChange = ({ queryText, error }: { queryText: string; error: unknown }) => { if (!error) { setQuery(queryText); @@ -361,8 +366,12 @@ export const IndexPatternTable = ({ itemId="id" items={indexPatterns} columns={columns} - pagination={pagination} + pagination={{ + pageSize, + pageSizeOptions: PAGE_SIZE_OPTIONS, + }} sorting={sorting} + onTableChange={onTableChange} search={search} selection={dataViews.getCanSaveSync() ? selection : undefined} /> diff --git a/src/plugins/data_view_management/tsconfig.json b/src/plugins/data_view_management/tsconfig.json index 9857dd44829fa..879d2dab84da9 100644 --- a/src/plugins/data_view_management/tsconfig.json +++ b/src/plugins/data_view_management/tsconfig.json @@ -46,6 +46,7 @@ "@kbn/react-kibana-mount", "@kbn/rollup", "@kbn/share-plugin", + "@kbn/shared-ux-table-persist", ], "exclude": [ "target/**/*", diff --git a/src/plugins/discover/public/application/main/components/top_nav/__snapshots__/open_search_panel.test.tsx.snap b/src/plugins/discover/public/application/main/components/top_nav/__snapshots__/open_search_panel.test.tsx.snap index 1851856a8739e..b1b399d1bd736 100644 --- a/src/plugins/discover/public/application/main/components/top_nav/__snapshots__/open_search_panel.test.tsx.snap +++ b/src/plugins/discover/public/application/main/components/top_nav/__snapshots__/open_search_panel.test.tsx.snap @@ -22,6 +22,7 @@ exports[`OpenSearchPanel render 1`] = ` ) : ( { diff --git a/src/plugins/saved_objects_finder/public/finder/index.tsx b/src/plugins/saved_objects_finder/public/finder/index.tsx index 28a79391dd0a6..cd985f5235920 100644 --- a/src/plugins/saved_objects_finder/public/finder/index.tsx +++ b/src/plugins/saved_objects_finder/public/finder/index.tsx @@ -12,10 +12,11 @@ import React from 'react'; import { SavedObjectsTaggingApi } from '@kbn/saved-objects-tagging-oss-plugin/public'; import type { ContentClient } from '@kbn/content-management-plugin/public'; import type { IUiSettingsClient } from '@kbn/core-ui-settings-browser'; -import type { SavedObjectFinderProps } from './saved_object_finder'; +import type { HOCProps } from '@kbn/shared-ux-table-persist'; +import type { SavedObjectFinderItem, SavedObjectFinderProps } from './saved_object_finder'; const LazySavedObjectFinder = React.lazy(() => import('./saved_object_finder')); -const SavedObjectFinder = (props: SavedObjectFinderProps) => ( +const SavedObjectFinder = (props: HOCProps) => ( @@ -32,7 +33,7 @@ export const getSavedObjectFinder = ( uiSettings: IUiSettingsClient, savedObjectsTagging?: SavedObjectsTaggingApi ) => { - return (props: SavedObjectFinderProps) => ( + return (props: HOCProps) => ( ); }; diff --git a/src/plugins/saved_objects_finder/public/finder/saved_object_finder.test.tsx b/src/plugins/saved_objects_finder/public/finder/saved_object_finder.test.tsx index d6cce936200d4..ace6f6a9d3661 100644 --- a/src/plugins/saved_objects_finder/public/finder/saved_object_finder.test.tsx +++ b/src/plugins/saved_objects_finder/public/finder/saved_object_finder.test.tsx @@ -28,7 +28,10 @@ import { IconType } from '@elastic/eui'; import { mount, shallow } from 'enzyme'; import React from 'react'; import * as sinon from 'sinon'; -import { SavedObjectFinderUi as SavedObjectFinder } from './saved_object_finder'; +import { + SavedObjectFinderWithoutPersist as SavedObjectFinder, + SavedObjectFinderUi, +} from './saved_object_finder'; import { contentManagementMock } from '@kbn/content-management-plugin/public/mocks'; import { findTestSubject } from '@kbn/test-jest-helpers'; import { SavedObjectsTaggingApi } from '@kbn/saved-objects-tagging-oss-plugin/public'; @@ -72,6 +75,15 @@ describe('SavedObjectsFinder', () => { }, ]; + const baseProps = { + id: 'foo', + euiTablePersist: { + pageSize: 10, + onTableChange: () => {}, + sorting: { sort: { direction: 'asc' as const, field: 'title' as const } }, + }, + }; + const contentManagement = contentManagementMock.createStartContract(); const contentClient = contentManagement.client; beforeEach(() => { @@ -109,6 +121,7 @@ describe('SavedObjectsFinder', () => { const wrapper = shallow( { const wrapper = shallow( @@ -157,6 +171,7 @@ describe('SavedObjectsFinder', () => { const wrapper = mount( { const wrapper = shallow( { const button = Hello; const wrapper = shallow( { const wrapper = mount( @@ -251,6 +269,7 @@ describe('SavedObjectsFinder', () => { const wrapper = mount( @@ -279,6 +298,7 @@ describe('SavedObjectsFinder', () => { const wrapper = mount( @@ -299,6 +319,7 @@ describe('SavedObjectsFinder', () => { const wrapper = mount( @@ -322,6 +343,7 @@ describe('SavedObjectsFinder', () => { const wrapper = mount( @@ -346,6 +368,7 @@ describe('SavedObjectsFinder', () => { const wrapper = mount( @@ -375,6 +398,7 @@ describe('SavedObjectsFinder', () => { const wrapper = shallow( { const wrapper = mount( @@ -430,6 +455,7 @@ describe('SavedObjectsFinder', () => { const wrapper = mount( @@ -453,6 +479,7 @@ describe('SavedObjectsFinder', () => { const wrapper = shallow( { const wrapper = mount( { const wrapper = mount( { const wrapper = mount( { const wrapper = mount( { const wrapper = mount( { const wrapper = mount( { const noItemsMessage = ; const wrapper = mount( { const wrapper = mount( { const wrapper = mount( { ); const wrapper = mount( - ); - wrapper.instance().componentDidMount!(); await nextTick(); wrapper.update(); expect(wrapper.find(EuiInMemoryTable).find('tbody tr')).toHaveLength(15); @@ -774,6 +818,7 @@ describe('SavedObjectsFinder', () => { const wrapper = mount( { const wrapper = mount( { const wrapper = mount( @@ -840,6 +887,7 @@ describe('SavedObjectsFinder', () => { const wrapper = mount( { const wrapper = mount( @@ -884,6 +933,7 @@ describe('SavedObjectsFinder', () => { const wrapper = mount( { const wrapper = mount( @@ -933,6 +984,7 @@ describe('SavedObjectsFinder', () => { const wrapper = mount( @@ -954,6 +1006,7 @@ describe('SavedObjectsFinder', () => { const wrapper = mount( @@ -978,6 +1031,7 @@ describe('SavedObjectsFinder', () => { render( (item.id === doc3.id ? tooltipText : undefined)} @@ -990,7 +1044,7 @@ describe('SavedObjectsFinder', () => { const tooltip = screen.queryByText(tooltipText); if (show) { - expect(tooltip).toBeInTheDocument(); + expect(tooltip)?.toBeInTheDocument(); } else { expect(tooltip).toBeNull(); } diff --git a/src/plugins/saved_objects_finder/public/finder/saved_object_finder.tsx b/src/plugins/saved_objects_finder/public/finder/saved_object_finder.tsx index e9f51a808b335..9ea3472e59d3c 100644 --- a/src/plugins/saved_objects_finder/public/finder/saved_object_finder.tsx +++ b/src/plugins/saved_objects_finder/public/finder/saved_object_finder.tsx @@ -25,15 +25,21 @@ import { EuiToolTip, EuiIconTip, IconType, - PropertySort, Query, SearchFilterConfig, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import type { SavedObjectsTaggingApi } from '@kbn/saved-objects-tagging-oss-plugin/public'; +import { + withEuiTablePersist, + type EuiTablePersistInjectedProps, +} from '@kbn/shared-ux-table-persist/src'; + import { FinderAttributes, SavedObjectCommon, LISTING_LIMIT_SETTING } from '../../common'; +const PAGE_SIZE_OPTIONS = [5, 10, 15, 25]; + export interface SavedObjectMetaData { type: string; name: string; @@ -45,7 +51,7 @@ export interface SavedObjectMetaData; @@ -55,7 +61,6 @@ interface SavedObjectFinderState { items: SavedObjectFinderItem[]; query: Query; isFetchingItems: boolean; - sort?: PropertySort; } interface SavedObjectFinderServices { @@ -65,6 +70,7 @@ interface SavedObjectFinderServices { } interface BaseSavedObjectFinder { + id: string; services: SavedObjectFinderServices; onChoose?: ( id: SavedObjectCommon['id'], @@ -93,8 +99,8 @@ interface SavedObjectFinderInitialPageSize extends BaseSavedObjectFinder { export type SavedObjectFinderProps = SavedObjectFinderFixedPage | SavedObjectFinderInitialPageSize; -export class SavedObjectFinderUi extends React.Component< - SavedObjectFinderProps, +class SavedObjectFinderUiClass extends React.Component< + SavedObjectFinderProps & EuiTablePersistInjectedProps, SavedObjectFinderState > { public static propTypes = { @@ -174,7 +180,7 @@ export class SavedObjectFinderUi extends React.Component< } }, 300); - constructor(props: SavedObjectFinderProps) { + constructor(props: SavedObjectFinderProps & EuiTablePersistInjectedProps) { super(props); this.state = { @@ -211,7 +217,11 @@ export class SavedObjectFinderUi extends React.Component< }; public render() { - const { onChoose, savedObjectMetaData } = this.props; + const { + onChoose, + savedObjectMetaData, + euiTablePersist: { pageSize, sorting, onTableChange }, + } = this.props; const taggingApi = this.props.services.savedObjectsTagging; const originalTagColumn = taggingApi?.ui.getTableColumnDefinition(); const tagColumn: EuiTableFieldDataColumnType | undefined = originalTagColumn @@ -320,16 +330,11 @@ export class SavedObjectFinderUi extends React.Component< ...(tagColumn ? [tagColumn] : []), ]; const pagination = { - initialPageSize: this.props.initialPageSize || this.props.fixedPageSize || 10, - pageSizeOptions: [5, 10, 15, 25], + initialPageSize: !!this.props.fixedPageSize ? this.props.fixedPageSize : pageSize ?? 10, + pageSize: !!this.props.fixedPageSize ? undefined : pageSize, + pageSizeOptions: PAGE_SIZE_OPTIONS, showPerPageOptions: !this.props.fixedPageSize, }; - const sorting = { - sort: this.state.sort ?? { - field: this.state.query?.text ? '' : 'title', - direction: 'asc', - }, - }; const typeFilter: SearchFilterConfig = { type: 'field_value_selection', field: 'type', @@ -382,10 +387,8 @@ export class SavedObjectFinderUi extends React.Component< message={this.props.noItemsMessage} search={search} pagination={pagination} - sorting={sorting} - onTableChange={({ sort }) => { - this.setState({ sort }); - }} + sorting={!!this.state.query?.text ? undefined : sorting} + onTableChange={onTableChange} /> @@ -393,6 +396,16 @@ export class SavedObjectFinderUi extends React.Component< } } +export const SavedObjectFinderUi = withEuiTablePersist(SavedObjectFinderUiClass, { + get: (props) => ({ + tableId: `soFinder-${props.id}`, + pageSizeOptions: PAGE_SIZE_OPTIONS, + initialPageSize: props.initialPageSize ?? props.fixedPageSize ?? 10, + }), +}); + +export const SavedObjectFinderWithoutPersist = SavedObjectFinderUiClass; // For testing + // Needed for React.lazy // eslint-disable-next-line import/no-default-export export default SavedObjectFinderUi; diff --git a/src/plugins/saved_objects_finder/tsconfig.json b/src/plugins/saved_objects_finder/tsconfig.json index cecc9fbdadb61..e32d4f34e68bc 100644 --- a/src/plugins/saved_objects_finder/tsconfig.json +++ b/src/plugins/saved_objects_finder/tsconfig.json @@ -15,6 +15,7 @@ "@kbn/content-management-plugin", "@kbn/content-management-utils", "@kbn/core-ui-settings-browser", + "@kbn/shared-ux-table-persist", ], "exclude": [ "target/**/*", diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/flyout.test.tsx.snap b/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/flyout.test.tsx.snap index af21429a9f7bb..b82a989d32851 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/flyout.test.tsx.snap +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/flyout.test.tsx.snap @@ -102,7 +102,6 @@ exports[`Flyout conflicts should allow conflict resolution 1`] = ` onTableChange={[Function]} pagination={ Object { - "pageIndex": 0, "pageSize": 5, "pageSizeOptions": Array [ 5, @@ -251,10 +250,6 @@ exports[`Flyout conflicts should allow conflict resolution 2`] = ` "newIndexPatternId": "2", }, ], - "unmatchedReferencesTablePagination": Object { - "pageIndex": 0, - "pageSize": 5, - }, }, }, ], diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/relationships.test.tsx.snap b/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/relationships.test.tsx.snap index f4a552f0a2fa2..124f4b4f2e285 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/relationships.test.tsx.snap +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/relationships.test.tsx.snap @@ -110,7 +110,12 @@ exports[`Relationships should render dashboards normally 1`] = ` }, ] } - pagination={true} + onTableChange={[Function]} + pagination={ + Object { + "pageSize": 10, + } + } rowProps={[Function]} search={ Object { @@ -310,7 +315,12 @@ exports[`Relationships should render index patterns normally 1`] = ` }, ] } - pagination={true} + onTableChange={[Function]} + pagination={ + Object { + "pageSize": 10, + } + } rowProps={[Function]} search={ Object { @@ -501,7 +511,12 @@ exports[`Relationships should render invalid relations 1`] = ` ] } items={Array []} - pagination={true} + onTableChange={[Function]} + pagination={ + Object { + "pageSize": 10, + } + } rowProps={[Function]} search={ Object { @@ -652,7 +667,12 @@ exports[`Relationships should render searches normally 1`] = ` }, ] } - pagination={true} + onTableChange={[Function]} + pagination={ + Object { + "pageSize": 10, + } + } rowProps={[Function]} search={ Object { @@ -813,7 +833,12 @@ exports[`Relationships should render visualizations normally 1`] = ` }, ] } - pagination={true} + onTableChange={[Function]} + pagination={ + Object { + "pageSize": 10, + } + } rowProps={[Function]} search={ Object { diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.test.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.test.tsx index 17a25fe3d98b7..4c94812d7de69 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.test.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.test.tsx @@ -12,7 +12,7 @@ import { importFileMock, resolveImportErrorsMock } from './flyout.test.mocks'; import React from 'react'; import { shallowWithI18nProvider } from '@kbn/test-jest-helpers'; import { coreMock, httpServiceMock } from '@kbn/core/public/mocks'; -import { Flyout, FlyoutProps, FlyoutState } from './flyout'; +import { FlyoutClass as Flyout, FlyoutProps, FlyoutState } from './flyout'; import { ShallowWrapper } from 'enzyme'; import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; @@ -21,15 +21,21 @@ const mockFile = { path: '/home/foo.ndjson', } as unknown as File; +const baseProps = { + euiTablePersist: { + pageSize: 5, + onTableChange: () => {}, + sorting: { sort: { direction: 'asc' as const, field: 'foo' as const } }, + }, +}; + describe('Flyout', () => { let defaultProps: FlyoutProps; const shallowRender = (props: FlyoutProps) => { - return shallowWithI18nProvider() as unknown as ShallowWrapper< - FlyoutProps, - FlyoutState, - Flyout - >; + return shallowWithI18nProvider( + + ) as unknown as ShallowWrapper; }; beforeEach(() => { diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.tsx index 7b50cec41fa27..4e29f34dedb73 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.tsx @@ -36,6 +36,10 @@ import { FormattedMessage } from '@kbn/i18n-react'; import { HttpStart, IBasePath } from '@kbn/core/public'; import { ISearchStart } from '@kbn/data-plugin/public'; import type { DataViewsContract, DataView } from '@kbn/data-views-plugin/public'; +import { + withEuiTablePersist, + type EuiTablePersistInjectedProps, +} from '@kbn/shared-ux-table-persist'; import type { SavedObjectManagementTypeInfo } from '../../../../common/types'; import { importFile, @@ -50,6 +54,7 @@ import { ImportSummary } from './import_summary'; const CREATE_NEW_COPIES_DEFAULT = false; const OVERWRITE_ALL_DEFAULT = true; +const PAGE_SIZE_OPTIONS = [5, 10, 25]; export interface FlyoutProps { close: () => void; @@ -65,7 +70,6 @@ export interface FlyoutProps { export interface FlyoutState { unmatchedReferences?: ProcessedImportResponse['unmatchedReferences']; - unmatchedReferencesTablePagination: { pageIndex: number; pageSize: number }; failedImports?: ProcessedImportResponse['failedImports']; successfulImports?: ProcessedImportResponse['successfulImports']; conflictingRecord?: ConflictingRecord; @@ -95,16 +99,15 @@ const getErrorMessage = (e: any) => { }); }; -export class Flyout extends Component { - constructor(props: FlyoutProps) { +export class FlyoutClass extends Component< + FlyoutProps & EuiTablePersistInjectedProps, + FlyoutState +> { + constructor(props: FlyoutProps & EuiTablePersistInjectedProps) { super(props); this.state = { unmatchedReferences: undefined, - unmatchedReferencesTablePagination: { - pageIndex: 0, - pageSize: 5, - }, conflictingRecord: undefined, error: undefined, file: undefined, @@ -275,7 +278,10 @@ export class Flyout extends Component { }; renderUnmatchedReferences() { - const { unmatchedReferences, unmatchedReferencesTablePagination: tablePagination } = this.state; + const { unmatchedReferences } = this.state; + const { + euiTablePersist: { pageSize, onTableChange }, + } = this.props; if (!unmatchedReferences) { return null; @@ -367,8 +373,8 @@ export class Flyout extends Component { ]; const pagination = { - ...tablePagination, - pageSizeOptions: [5, 10, 25], + pageSize, + pageSizeOptions: PAGE_SIZE_OPTIONS, }; return ( @@ -376,16 +382,7 @@ export class Flyout extends Component { items={unmatchedReferences as any[]} columns={columns} pagination={pagination} - onTableChange={({ page }) => { - if (page) { - this.setState({ - unmatchedReferencesTablePagination: { - pageSize: page.size, - pageIndex: page.index, - }, - }); - } - }} + onTableChange={onTableChange} /> ); } @@ -657,3 +654,9 @@ export class Flyout extends Component { ); } } + +export const Flyout = withEuiTablePersist(FlyoutClass, { + tableId: 'savedObjectsMgmtUnmatchedReferences', + pageSizeOptions: PAGE_SIZE_OPTIONS, + initialPageSize: 5, +}); diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/relationships.test.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/relationships.test.tsx index e98c9e7b54223..e963b626552ca 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/relationships.test.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/relationships.test.tsx @@ -11,7 +11,7 @@ import React from 'react'; import { shallowWithI18nProvider } from '@kbn/test-jest-helpers'; import { httpServiceMock } from '@kbn/core/public/mocks'; import type { SavedObjectManagementTypeInfo } from '../../../../common/types'; -import { Relationships, RelationshipsProps } from './relationships'; +import { RelationshipsClass as Relationships, RelationshipsProps } from './relationships'; jest.mock('../../../lib/fetch_export_by_type_and_search', () => ({ fetchExportByTypeAndSearch: jest.fn(), @@ -21,6 +21,14 @@ jest.mock('../../../lib/fetch_export_objects', () => ({ fetchExportObjects: jest.fn(), })); +const baseProps = { + euiTablePersist: { + pageSize: 10, + onTableChange: () => {}, + sorting: { sort: { direction: 'asc' as const, field: 'id' as const } }, + }, +}; + const allowedTypes: SavedObjectManagementTypeInfo[] = [ { name: 'index-pattern', @@ -86,7 +94,7 @@ describe('Relationships', () => { close: jest.fn(), }; - const component = shallowWithI18nProvider(); + const component = shallowWithI18nProvider(); // Make sure we are showing loading expect(component.find('EuiLoadingElastic').length).toBe(1); @@ -155,7 +163,7 @@ describe('Relationships', () => { close: jest.fn(), }; - const component = shallowWithI18nProvider(); + const component = shallowWithI18nProvider(); // Make sure we are showing loading expect(component.find('EuiLoadingElastic').length).toBe(1); @@ -223,7 +231,7 @@ describe('Relationships', () => { close: jest.fn(), }; - const component = shallowWithI18nProvider(); + const component = shallowWithI18nProvider(); // Make sure we are showing loading expect(component.find('EuiLoadingElastic').length).toBe(1); @@ -292,7 +300,7 @@ describe('Relationships', () => { showPlainSpinner: true, }; - const component = shallowWithI18nProvider(); + const component = shallowWithI18nProvider(); // Make sure we are showing loading expect(component.find('EuiLoadingSpinner').length).toBe(1); @@ -332,7 +340,7 @@ describe('Relationships', () => { close: jest.fn(), }; - const component = shallowWithI18nProvider(); + const component = shallowWithI18nProvider(); // Ensure all promises resolve await new Promise((resolve) => process.nextTick(resolve)); @@ -378,7 +386,7 @@ describe('Relationships', () => { close: jest.fn(), }; - const component = shallowWithI18nProvider(); + const component = shallowWithI18nProvider(); // Ensure all promises resolve await new Promise((resolve) => process.nextTick(resolve)); diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/relationships.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/relationships.tsx index 36cb9da9ad436..0d9c71ceae2ff 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/relationships.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/relationships.tsx @@ -27,6 +27,10 @@ import { SearchFilterConfig } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { IBasePath } from '@kbn/core/public'; +import { + withEuiTablePersist, + type EuiTablePersistInjectedProps, +} from '@kbn/shared-ux-table-persist'; import type { SavedObjectManagementTypeInfo } from '../../../../common/types'; import { getDefaultTitle, getSavedObjectLabel } from '../../../lib'; import type { v1 } from '../../../../common'; @@ -83,8 +87,11 @@ const relationshipColumn = { }, }; -export class Relationships extends Component { - constructor(props: RelationshipsProps) { +export class RelationshipsClass extends Component< + RelationshipsProps & EuiTablePersistInjectedProps, + RelationshipsState +> { + constructor(props: RelationshipsProps & EuiTablePersistInjectedProps) { super(props); this.state = { @@ -218,7 +225,14 @@ export class Relationships extends Component ({ 'data-test-subj': `relationshipsTableRow`, @@ -420,3 +435,8 @@ export class Relationships extends Component { { - const panel = await this.testSubjects.find('inspectorPanel'); - await this.find.clickByButtonText('Rows per page: 20', panel); + await this.testSubjects.click('tablePaginationPopoverButton'); // The buttons for setting table page size are in a popover element. This popover // element appears as if it's part of the inspectorPanel but it's really attached // to the body element by a portal. diff --git a/x-pack/plugins/canvas/public/components/embeddable_flyout/flyout.component.tsx b/x-pack/plugins/canvas/public/components/embeddable_flyout/flyout.component.tsx index fbb7b971bfcb4..543367fda127a 100644 --- a/x-pack/plugins/canvas/public/components/embeddable_flyout/flyout.component.tsx +++ b/x-pack/plugins/canvas/public/components/embeddable_flyout/flyout.component.tsx @@ -135,6 +135,7 @@ export const AddEmbeddableFlyout: FC = ({ { onIndexPatternSelected(indexPattern as IndexPatternSavedObject); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/source_selection/source_selection.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/source_selection/source_selection.tsx index 5aa0ccc46a5cd..ff173c47a5320 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/source_selection/source_selection.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/source_selection/source_selection.tsx @@ -125,6 +125,7 @@ export const SourceSelection: FC = () => { )} { = ({ onClose }) => { = ({ = ({