From c03aad4a55e336d741ff51e515a6f5763e4eacdb Mon Sep 17 00:00:00 2001 From: Sweta Agarwalla Date: Tue, 13 Oct 2026 10:30:51 +0530 Subject: [PATCH 01/27] Standardize UI pagination: 15/25/50 for lists, 12/24/48 for card grids Paged lists and tables default to 15 with a 15/25/50 picker; paged card grids default to 12 with 12/24/48. Pages with a table/card toggle take the scale of the current view and return to page 1 when it changes. - Add LIST/GRID page-size option constants and view helpers in PaginationUtils. - Core PaginationCardWithControls, TableV2 client paging and the atoms listing hooks default to 15 instead of 10. - Lists: Connections list view, App Live Indexing, Reindex failures, glossary update conflict table, Context Center memories, service version history (now sends its page size to the API). - Grids: Personas, installed Applications, Applications marketplace. - Table/card: Domains, Sub-domains, Data Products, Metrics, Learning Resources. - Fix pre-existing type errors in the touched files (AppLiveIndexing unsupported TableV2 props, useDataFetching default transform, Domains.spec.ts). - Update Playwright waits to the new sizes and record the rule in ADR:2026-10-13-ui-page-sizes-lists-15-25-50-grids-12-24-48. --- ...age-sizes-lists-15-25-50-grids-12-24-48.md | 37 ++++++++++++++ .../application/pagination/pagination.tsx | 4 +- .../ui/src/stories/Pagination.stories.tsx | 2 +- .../e2e/Pages/AppStopRunModal.spec.ts | 6 +-- .../DataInsightReportApplication.spec.ts | 2 +- .../e2e/Pages/DataInsightSettings.spec.ts | 2 +- .../ui/playwright/e2e/Pages/Domains.spec.ts | 18 ++++--- .../e2e/Pages/SearchIndexApplication.spec.ts | 2 +- .../e2e/Pages/SubDomainPagination.spec.ts | 2 +- .../DataProduct/DataProductListPage.tsx | 23 +++++++-- .../hooks/useDataProductListingData.tsx | 10 ++-- .../SubDomainsTable.component.tsx | 19 +++++-- .../hooks/useSubdomainListingData.tsx | 3 ++ .../DomainListing/DomainListPage.tsx | 23 +++++++-- .../hooks/useDomainListingData.tsx | 4 +- .../GlossaryUpdateConfirmationModal.tsx | 7 ++- .../AppLiveIndexing.component.tsx | 18 +++---- .../ReindexFailures.component.tsx | 9 ++-- .../src/components/common/Table/TableV2.tsx | 3 +- .../atoms/compositions/useListingData.tsx | 3 +- .../common/atoms/data/useDataFetching.tsx | 5 +- .../common/atoms/data/useUrlState.tsx | 3 +- .../domain/compositions/useDomainListing.tsx | 4 +- .../ConnectionsListView.test.tsx | 6 +-- .../ConnectionsPage/ConnectionsListView.tsx | 6 ++- .../ConnectionsPage.constants.ts | 3 -- .../useConnectionsData.test.ts | 2 +- .../ConnectionsPage/useConnectionsData.ts | 2 +- .../src/constants/ContextCenter.constants.ts | 5 +- .../ui/src/constants/Learning.constants.ts | 1 - .../resources/ui/src/constants/constants.ts | 10 +++- .../src/pages/Application/ApplicationPage.tsx | 9 +++- .../LearningResourcesPage.tsx | 39 ++++++++++----- .../pages/MarketPlacePage/MarketPlacePage.tsx | 7 ++- .../MetricListPage/MetricListPage.test.tsx | 9 +++- .../MetricListPage/MetricListPage.tsx | 26 ++++++---- .../Persona/PersonaListPage/PersonaPage.tsx | 8 +-- .../ServiceVersionMainTabContent.interface.ts | 1 + .../ServiceVersionMainTabContent.tsx | 4 +- .../ServiceVersionPage/ServiceVersionPage.tsx | 5 +- .../ui/src/utils/PaginationUtils.test.ts | 50 ++++++++++++++++++- .../resources/ui/src/utils/PaginationUtils.ts | 23 +++++++++ 42 files changed, 318 insertions(+), 107 deletions(-) create mode 100644 docs/decisions/2026-10-13-ui-page-sizes-lists-15-25-50-grids-12-24-48.md diff --git a/docs/decisions/2026-10-13-ui-page-sizes-lists-15-25-50-grids-12-24-48.md b/docs/decisions/2026-10-13-ui-page-sizes-lists-15-25-50-grids-12-24-48.md new file mode 100644 index 000000000000..f2e75bb58f10 --- /dev/null +++ b/docs/decisions/2026-10-13-ui-page-sizes-lists-15-25-50-grids-12-24-48.md @@ -0,0 +1,37 @@ +# Paged UI lists step 15/25/50 and paged card grids step 12/24/48 + +- **Status:** Accepted +- **Revisions:** v1 2026-10-13 (initial) +- **Deciders:** Sweta Agarwalla +- **Guard:** `PaginationUtils.test.ts` (`view page sizes` describe block) +- **Related:** — + +## Context +Page sizes had drifted page by page: 10, 9, 20 and 5 sat beside the app-wide 15/25/50, a card grid +offered 15/25/50 (rows that never fill), and one list offered 12/24/48. The core-components +`PaginationCardWithControls` defaulted to 10 with 10/25/50, so any page that did not pass its own +options showed a scale no other page used. The `pageSize` URL param and the `globalPageSize` +preference are shared across pages, so a size from one scale could land in a picker with no +matching entry. + +## Decision +- A paged **list or table** defaults to 15 and offers 15/25/50 (`LIST_PAGE_SIZE_OPTIONS`, + `PAGE_SIZE_BASE` in `src/constants/constants.ts`). +- A paged **card grid** defaults to 12 and offers 12/24/48 (`GRID_PAGE_SIZE_OPTIONS`, + `GRID_PAGE_SIZE_BASE`), so rows of 2, 3 or 4 cards fill evenly. +- A page with a table/card toggle takes its scale from the view it is in + (`getViewPageSizeOptions` / `getViewPageSize` / `resolveViewPageSize` in `utils/PaginationUtils.ts`) + and returns to page 1 when the view changes. +- A grid page passes `GRID_PAGE_SIZE_OPTIONS` to `usePaging` so a list-scale size from the shared + URL or preference is reconciled to its own scale, and the global preference only ever stores a + list-scale size. +- Shared defaults (core `PaginationCardWithControls`, `TableV2` client paging, the atoms listing + hooks) are 15. +- Out of scope: fetch limits that are not a paged view — typeahead/suggestion searches, tree-select + batches, count-only queries, bulk fetches, and "show more" previews. + +## Consequences +A new paged view picks one of the two scales instead of inventing a size. The two scales share no +value, so switching a page between list and grid always resets its size to the new scale's first +step. Playwright specs that wait on a list request's `limit`/`size` must use the scale of the page +they open. Adding a third scale, or a page size outside these, means amending this record. diff --git a/openmetadata-ui-core-components/src/main/resources/ui/src/components/application/pagination/pagination.tsx b/openmetadata-ui-core-components/src/main/resources/ui/src/components/application/pagination/pagination.tsx index 202c48404f81..59c1d684afcc 100644 --- a/openmetadata-ui-core-components/src/main/resources/ui/src/components/application/pagination/pagination.tsx +++ b/openmetadata-ui-core-components/src/main/resources/ui/src/components/application/pagination/pagination.tsx @@ -417,8 +417,8 @@ interface PaginationCardWithControlsProps extends PaginationProps { export const PaginationCardWithControls = ({ page = 1, total = 10, - pageSize = 10, - pageSizeOptions = [10, 25, 50], + pageSize = 15, + pageSizeOptions = [15, 25, 50], className, minimal = false, onPageChange, diff --git a/openmetadata-ui-core-components/src/main/resources/ui/src/stories/Pagination.stories.tsx b/openmetadata-ui-core-components/src/main/resources/ui/src/stories/Pagination.stories.tsx index d40b7ad3c229..ea9ebaa7ab3f 100644 --- a/openmetadata-ui-core-components/src/main/resources/ui/src/stories/Pagination.stories.tsx +++ b/openmetadata-ui-core-components/src/main/resources/ui/src/stories/Pagination.stories.tsx @@ -113,7 +113,7 @@ export const ButtonGroup: StoryObj = { export const CardWithControls: StoryObj = { render: () => { const [page, setPage] = useState(1); - const [pageSize, setPageSize] = useState(10); + const [pageSize, setPageSize] = useState(15); const totalPages = Math.max(Math.ceil(TOTAL_ITEMS / pageSize), 1); return ( diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/AppStopRunModal.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/AppStopRunModal.spec.ts index c6bdafca60bf..699d95da6074 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/AppStopRunModal.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/AppStopRunModal.spec.ts @@ -81,7 +81,7 @@ test.describe('App Stop Run Modal', () => { }); const appsResponse = page.waitForResponse( - `**/api/v1/apps?limit=15&include=non-deleted` + `**/api/v1/apps?limit=12&include=non-deleted` ); await settingClick(page, GlobalSettingOptions.APPLICATIONS); await appsResponse; @@ -141,7 +141,7 @@ test.describe('App Stop Run Modal', () => { }); const appsResponse = page.waitForResponse( - `**/api/v1/apps?limit=15&include=non-deleted` + `**/api/v1/apps?limit=12&include=non-deleted` ); await settingClick(page, GlobalSettingOptions.APPLICATIONS); await appsResponse; @@ -195,7 +195,7 @@ test.describe('App Stop Run Modal', () => { }); const appsResponse = page.waitForResponse( - `**/api/v1/apps?limit=15&include=non-deleted` + `**/api/v1/apps?limit=12&include=non-deleted` ); await settingClick(page, GlobalSettingOptions.APPLICATIONS); await appsResponse; diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/DataInsightReportApplication.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/DataInsightReportApplication.spec.ts index ae42d04447d2..74fe4c0e502c 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/DataInsightReportApplication.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/DataInsightReportApplication.spec.ts @@ -51,7 +51,7 @@ test.describe.serial('Data Insight Report Application', () => { await redirectToHomePage(page); const appsResponse = page.waitForResponse( - `/api/v1/apps?limit=15&include=non-deleted` + `/api/v1/apps?limit=12&include=non-deleted` ); await settingClick(page, GlobalSettingOptions.APPLICATIONS); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/DataInsightSettings.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/DataInsightSettings.spec.ts index 4d93c031764f..3f27099c350f 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/DataInsightSettings.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/DataInsightSettings.spec.ts @@ -40,7 +40,7 @@ test.describe.serial( await redirectToHomePage(page); const appsResponse = page.waitForResponse( - `/api/v1/apps?limit=15&include=non-deleted` + `/api/v1/apps?limit=12&include=non-deleted` ); await settingClick(page, GlobalSettingOptions.APPLICATIONS); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Domains.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Domains.spec.ts index c75cb90d5cec..fdf5e26a4d44 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Domains.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Domains.spec.ts @@ -583,7 +583,7 @@ test.describe('Domains', () => { await Promise.all([ createSubDomain(page, subDomain.data), page.waitForResponse( - '/api/v1/search/query?q=&index=domain&from=0&size=9&deleted=false*' + '/api/v1/search/query?q=&index=domain&from=0&size=15&deleted=false*' ), ]); @@ -631,7 +631,7 @@ test.describe('Domains', () => { await selectDomain(page, domain.data); const subDomainApiRes1 = page.waitForResponse( - '/api/v1/search/query?q=&index=domain&from=0&size=9&deleted=false*' + '/api/v1/search/query?q=&index=domain&from=0&size=15&deleted=false*' ); // Create new sub domain under the existing sub domain @@ -1350,7 +1350,7 @@ test.describe('Domains', () => { }); await test.step('Fill subdomain form', async () => { - await fillDomainForm(page, subDomain.data, false); + await fillDomainForm(page, subDomain.data); }); await test.step('Search and select tag via TagSuggestion', async () => { @@ -3072,7 +3072,7 @@ test.describe('Domain Access with hasDomain() Rule', () => { // Navigate to the domain table const domainTableFqn = testResources.domainTable.entityResponseData.fullyQualifiedName; - await userPage.goto(`/table/${encodeURIComponent(domainTableFqn)}`); + await userPage.goto(`/table/${encodeURIComponent(domainTableFqn ?? '')}`); await waitForAllLoadersToDisappear(userPage); // Verify no permission error @@ -3088,7 +3088,9 @@ test.describe('Domain Access with hasDomain() Rule', () => { // Navigate to the subdomain table const subDomainTableFqn = testResources.subDomainTable.entityResponseData.fullyQualifiedName; - await userPage.goto(`/table/${encodeURIComponent(subDomainTableFqn)}`); + await userPage.goto( + `/table/${encodeURIComponent(subDomainTableFqn ?? '')}` + ); // Verify no permission error await expect( @@ -3136,7 +3138,7 @@ test.describe('Domain Access with noDomain() Rule', () => { await test.step('Verify user can access domain-assigned table', async () => { const domainTableFqn = testResources.domainTable.entityResponseData.fullyQualifiedName; - await userPage.goto(`/table/${encodeURIComponent(domainTableFqn)}`); + await userPage.goto(`/table/${encodeURIComponent(domainTableFqn ?? '')}`); await waitForAllLoadersToDisappear(userPage); // Verify no permission error @@ -3151,7 +3153,9 @@ test.describe('Domain Access with noDomain() Rule', () => { await test.step('Verify user gets permission error for table without domain', async () => { const noDomainTableFqn = testResources.noDomainTable.entityResponseData.fullyQualifiedName; - await userPage.goto(`/table/${encodeURIComponent(noDomainTableFqn)}`); + await userPage.goto( + `/table/${encodeURIComponent(noDomainTableFqn ?? '')}` + ); await waitForAllLoadersToDisappear(userPage); // Verify permission error is shown diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/SearchIndexApplication.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/SearchIndexApplication.spec.ts index 1438da9f9a5b..639838051270 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/SearchIndexApplication.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/SearchIndexApplication.spec.ts @@ -47,7 +47,7 @@ interface AppRunRecordResponse { */ const installSearchIndexApplication = async (page: Page) => { const getMarketPlaceResponse = page.waitForResponse( - '/api/v1/apps/marketplace?limit=15' + '/api/v1/apps/marketplace?limit=12' ); await page.click('[data-testid="add-application"]'); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/SubDomainPagination.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/SubDomainPagination.spec.ts index 72a6e8e8622f..063097443a83 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/SubDomainPagination.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/SubDomainPagination.spec.ts @@ -30,7 +30,7 @@ test.use({ storageState: 'playwright/.auth/admin.json' }); const domain = new Domain(); const subDomains: SubDomain[] = []; const SUBDOMAIN_COUNT = 60; -const PAGE_SIZE = 9; +const PAGE_SIZE = 15; test.describe('SubDomain Pagination', PLAYWRIGHT_BASIC_TEST_TAG_OBJ, () => { test.beforeAll('Setup domain and subdomains', async ({ browser }) => { diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataProduct/DataProductListPage.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataProduct/DataProductListPage.tsx index 7b95c4495ccd..96c7e939e88b 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataProduct/DataProductListPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataProduct/DataProductListPage.tsx @@ -49,6 +49,7 @@ import { useMarketplaceStore } from '../../hooks/useMarketplaceStore'; import dataProductClassBase from '../../utils/DataProduct/DataProductClassBase'; import { getEntityName } from '../../utils/EntityNameUtils'; import { getEntityAvatarProps } from '../../utils/IconUtils'; +import { getViewPageSize } from '../../utils/PaginationUtils'; import { renderBreakableTooltip } from '../../utils/TooltipUtils'; import { useDelete } from '../common/atoms/actions/useDelete'; import { @@ -153,7 +154,22 @@ const DataProductListPage = ({ const { isMarketplace, dataProductBasePath } = useMarketplaceStore(); const { t } = useTranslation(); const isAiMode = useIsAiMode(); - const dataProductListing = useDataProductListingData({ enableNlq: isAiMode }); + const personaView = usePersonaViewMode(VIEW_MODE_PAGE.DataProducts); + const [selectedView, setView] = useState(); + const view = selectedView ?? personaView; + const dataProductListing = useDataProductListingData({ + enableNlq: isAiMode, + pageSize: getViewPageSize(view), + }); + // Card and table views page at different sizes, so the current page may not exist in the other. + const { handlePageChange } = dataProductListing; + const handleViewChange = useCallback( + (nextView: PageViewMode) => { + setView(nextView); + handlePageChange(1); + }, + [handlePageChange] + ); const { permissions } = usePermissionProvider(); const { quickFilters, defaultFilters } = useDataProductFilters({ aggregations: dataProductListing.aggregations || undefined, @@ -233,9 +249,6 @@ const DataProductListPage = ({ loading: dataProductListing.loading, }); - const personaView = usePersonaViewMode(VIEW_MODE_PAGE.DataProducts); - const [selectedView, setView] = useState(); - const view = selectedView ?? personaView; const { renderDataProductCard } = useDomainCardTemplates(); const extraColumns = dataProductClassBase.getListingExtraColumns(); @@ -457,7 +470,7 @@ const DataProductListPage = ({ )} {quickFilters} - + {deleteIconButton} {filterSelectionDisplay} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataProduct/hooks/useDataProductListingData.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataProduct/hooks/useDataProductListingData.tsx index 0ea35431c75a..902d66a284c1 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataProduct/hooks/useDataProductListingData.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataProduct/hooks/useDataProductListingData.tsx @@ -13,7 +13,7 @@ import { Avatar, Typography } from '@openmetadata/ui-core-components'; import { useCallback, useMemo } from 'react'; -import { TABLE_CARD_PAGE_SIZE } from '../../../constants/constants'; +import { PAGE_SIZE_BASE } from '../../../constants/constants'; import { SearchIndex } from '../../../enums/search.enum'; import { DataProduct } from '../../../generated/entity/domains/dataProduct'; import { useMarketplaceStore } from '../../../hooks/useMarketplaceStore'; @@ -33,7 +33,11 @@ import { export const useDataProductListingData = ({ enableNlq, -}: { enableNlq?: boolean } = {}): ListingData => { + pageSize = PAGE_SIZE_BASE, +}: { + enableNlq?: boolean; + pageSize?: number; +} = {}): ListingData => { const { dataProductBasePath } = useMarketplaceStore(); // Memoized because a ClassBase override may build these per call, and // `filterConfigs` feeds a useMemo dep list down in useUrlState. @@ -117,7 +121,7 @@ export const useDataProductListingData = ({ const listingData = useListingData({ searchIndex: SearchIndex.DATA_PRODUCT, baseFilter: '', // No parent filter for data products - pageSize: TABLE_CARD_PAGE_SIZE, + pageSize, filterKeys, filterConfigs, columns, diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Domain/SubDomainsTable/SubDomainsTable.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Domain/SubDomainsTable/SubDomainsTable.component.tsx index 295a32312fbc..bb09d6252978 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Domain/SubDomainsTable/SubDomainsTable.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Domain/SubDomainsTable/SubDomainsTable.component.tsx @@ -26,6 +26,7 @@ import { VIEW_MODE_PAGE } from '../../../constants/platform/personaAppLayout.con import { ERROR_PLACEHOLDER_TYPE } from '../../../enums/common.enum'; import { PageViewMode } from '../../../generated/type/personaPreferences'; import { usePersonaViewMode } from '../../../hooks/platform/usePersonaViewMode'; +import { getViewPageSize } from '../../../utils/PaginationUtils'; import { useDelete } from '../../common/atoms/actions/useDelete'; import { useDomainCardTemplates } from '../../common/atoms/domain/ui/useDomainCardTemplates'; import { useDomainFilters } from '../../common/atoms/domain/ui/useDomainFilters'; @@ -47,9 +48,22 @@ const SubDomainsTable = ({ onDeleteSubDomain, }: SubDomainsTableProps) => { const { t } = useTranslation(); + const personaView = usePersonaViewMode(VIEW_MODE_PAGE.SubDomains); + const [selectedView, setView] = useState(); + const view = selectedView ?? personaView; const subdomainListing = useSubdomainListingData({ parentDomainFqn: domainFqn, + pageSize: getViewPageSize(view), }); + // Card and table views page at different sizes, so the current page may not exist in the other. + const { handlePageChange } = subdomainListing; + const handleViewChange = useCallback( + (nextView: PageViewMode) => { + setView(nextView); + handlePageChange(1); + }, + [handlePageChange] + ); const { quickFilters, defaultFilters } = useDomainFilters({ isSubDomain: true, @@ -85,9 +99,6 @@ const SubDomainsTable = ({ }; }, [debouncedSearch]); - const personaView = usePersonaViewMode(VIEW_MODE_PAGE.SubDomains); - const [selectedView, setView] = useState(); - const view = selectedView ?? personaView; const { renderDomainCard } = useDomainCardTemplates(); const { columns: subDomainColumns, renderCell: renderSubDomainCell } = @@ -229,7 +240,7 @@ const SubDomainsTable = ({ /> {quickFilters} - + {deleteIconButton} {filterSelectionDisplay} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Domain/SubDomainsTable/hooks/useSubdomainListingData.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Domain/SubDomainsTable/hooks/useSubdomainListingData.tsx index 8752e1c2b4ed..3b30ea63c54e 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Domain/SubDomainsTable/hooks/useSubdomainListingData.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Domain/SubDomainsTable/hooks/useSubdomainListingData.tsx @@ -18,10 +18,12 @@ import { useMarketplaceStore } from '../../../../hooks/useMarketplaceStore'; interface UseSubdomainListingDataProps { parentDomainFqn: string; + pageSize?: number; } export const useSubdomainListingData = ({ parentDomainFqn, + pageSize, }: UseSubdomainListingDataProps): ListingData => { const { domainBasePath } = useMarketplaceStore(); @@ -45,5 +47,6 @@ export const useSubdomainListingData = ({ basePath: domainBasePath, isSubDomain: true, searchKey: 'sub-domain', + pageSize, }); }; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DomainListing/DomainListPage.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DomainListing/DomainListPage.tsx index 0014fd62ab1e..e9e8315cbc49 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DomainListing/DomainListPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DomainListing/DomainListPage.tsx @@ -51,6 +51,7 @@ import ViewToggle from '../common/ViewToggle/ViewToggle'; import MarketplaceSearchInput from '../DataMarketplace/MarketplaceSearchInput/MarketplaceSearchInput.component'; import PageLayoutV1 from '../PageLayoutV1/PageLayoutV1'; import DomainTreeView from './components/DomainTreeView'; +import { getViewPageSize } from '../../utils/PaginationUtils'; import { DomainListPageProps } from './DomainListPage.interface'; import { useDomainCreateDrawer } from './hooks/useDomainCreateDrawer'; import { useDomainListingData } from './hooks/useDomainListingData'; @@ -59,7 +60,22 @@ const DomainListPage = ({ renderPageHeader }: DomainListPageProps) => { const { isMarketplace, domainBasePath } = useMarketplaceStore(); const { t } = useTranslation(); const isAiMode = useIsAiMode(); - const domainListing = useDomainListingData({ enableNlq: isAiMode }); + const personaView = usePersonaViewMode(VIEW_MODE_PAGE.Domains); + const [selectedView, setView] = useState(); + const view = selectedView ?? personaView; + const domainListing = useDomainListingData({ + enableNlq: isAiMode, + pageSize: getViewPageSize(view), + }); + // Card and table views page at different sizes, so the current page may not exist in the other. + const { handlePageChange } = domainListing; + const handleViewChange = useCallback( + (nextView: PageViewMode) => { + setView(nextView); + handlePageChange(1); + }, + [handlePageChange] + ); const { permissions } = usePermissionProvider(); const [treeRefreshToken, setTreeRefreshToken] = useState(0); @@ -151,9 +167,6 @@ const DomainListPage = ({ renderPageHeader }: DomainListPageProps) => { loading: domainListing.loading, }); - const personaView = usePersonaViewMode(VIEW_MODE_PAGE.Domains); - const [selectedView, setView] = useState(); - const view = selectedView ?? personaView; const isTreeView = view === PageViewMode.Tree; const { renderDomainCard } = useDomainCardTemplates(); @@ -352,7 +365,7 @@ const DomainListPage = ({ renderPageHeader }: DomainListPageProps) => { {deleteIconButton} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DomainListing/hooks/useDomainListingData.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DomainListing/hooks/useDomainListingData.tsx index 2c24f2f3fca9..d3a2ce7ef693 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DomainListing/hooks/useDomainListingData.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DomainListing/hooks/useDomainListingData.tsx @@ -21,7 +21,8 @@ import { QueryFieldInterface } from '../../../pages/ExplorePage/ExplorePage.inte export const useDomainListingData = ({ enableNlq, -}: { enableNlq?: boolean } = {}): ListingData => { + pageSize, +}: { enableNlq?: boolean; pageSize?: number } = {}): ListingData => { const { domainBasePath } = useMarketplaceStore(); const { userDomains, isDomainRestricted } = useDomainStore(); @@ -63,5 +64,6 @@ export const useDomainListingData = ({ nameLabelKey: 'label.domain', basePath: domainBasePath, enableNlq, + pageSize, }); }; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryUpdateConfirmationModal/GlossaryUpdateConfirmationModal.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryUpdateConfirmationModal/GlossaryUpdateConfirmationModal.tsx index ca3ff8f8cbdf..328ffc4f4ae5 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryUpdateConfirmationModal/GlossaryUpdateConfirmationModal.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryUpdateConfirmationModal/GlossaryUpdateConfirmationModal.tsx @@ -27,6 +27,10 @@ import { AxiosError } from 'axios'; import { useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Link } from 'react-router-dom'; +import { + LIST_PAGE_SIZE_OPTIONS, + PAGE_SIZE_BASE, +} from '../../../constants/constants'; import { ClientErrors } from '../../../enums/Axios.enum'; import { EntityType } from '../../../enums/entity.enum'; import { GlossaryTerm } from '../../../generated/entity/data/glossaryTerm'; @@ -101,7 +105,8 @@ const renderFailedContent = ( columns={columns} dataSource={failedStatus?.failedRequest ?? []} pagination={{ - pageSize: 5, + pageSize: PAGE_SIZE_BASE, + pageSizeOptions: LIST_PAGE_SIZE_OPTIONS, showSizeChanger: true, }} rowKey={(record) => record.request?.id} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Applications/AppLiveIndexing/AppLiveIndexing.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Applications/AppLiveIndexing/AppLiveIndexing.component.tsx index daab03c26b28..a22e58bcf541 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Applications/AppLiveIndexing/AppLiveIndexing.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Applications/AppLiveIndexing/AppLiveIndexing.component.tsx @@ -15,7 +15,10 @@ import { Typography } from '@openmetadata/ui-core-components'; import { AxiosError } from 'axios'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { NO_DATA_PLACEHOLDER } from '../../../../constants/constants'; +import { + LIST_PAGE_SIZE_OPTIONS, + NO_DATA_PLACEHOLDER, +} from '../../../../constants/constants'; import { Paging } from '../../../../generated/type/paging'; import { usePaging } from '../../../../hooks/paging/usePaging'; import { useFqn } from '../../../../hooks/useFqn'; @@ -40,8 +43,6 @@ const STATUS_TYPE_MAP: Record = { FAILED: StatusType.Failure, }; -const PAGE_SIZE = 15; - const AppLiveIndexing = ({ appData: _appData }: AppLiveIndexingProps) => { const { t } = useTranslation(); const { fqn } = useFqn(); @@ -55,7 +56,7 @@ const AppLiveIndexing = ({ appData: _appData }: AppLiveIndexingProps) => { handlePagingChange, handlePageChange, handlePageSizeChange, - } = usePaging(PAGE_SIZE); + } = usePaging(); const fetchRetryQueue = useCallback( async (offset = 0) => { @@ -164,7 +165,6 @@ const AppLiveIndexing = ({ appData: _appData }: AppLiveIndexingProps) => { { ), }} - pageSize={pageSize} pagination={{ total: paging.total, pageSize, current: currentPage, + hideOnSinglePage: true, showSizeChanger: true, - pageSizeOptions: ['10', '15', '25', '50'], - onChange: (page) => - handlePageChangeHandler({ currentPage: page, cursorType: '' }), + pageSizeOptions: LIST_PAGE_SIZE_OPTIONS, + onChange: (page) => handlePageChangeHandler({ currentPage: page }), onShowSizeChange: (_, size) => { handlePageSizeChange(size); fetchRetryQueue(0); }, }} - paginationVisible={records.length > 0 && paging.total > pageSize} rowKey={(record) => `${record.entityId}-${record.entityFqn}`} size="small" /> diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Applications/AppLogsViewer/ReindexFailures.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Applications/AppLogsViewer/ReindexFailures.component.tsx index 5af02763a008..27b21f0cfc3b 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Applications/AppLogsViewer/ReindexFailures.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Applications/AppLogsViewer/ReindexFailures.component.tsx @@ -29,6 +29,7 @@ import { } from 'react'; import { useTranslation } from 'react-i18next'; import { RDF_INDEX_APP_NAME } from '../../../../constants/Applications.constant'; +import { PAGE_SIZE_BASE } from '../../../../constants/constants'; import { getRdfReindexFailures } from '../../../../rest/rdfAPI'; import { getReindexFailures } from '../../../../rest/searchAPI'; import { formatDateTimeWithTimezone } from '../../../../utils/date-time/DateTimeUtils'; @@ -41,8 +42,6 @@ import { ReindexFailuresProps, } from './ReindexFailures.interface'; -const PAGE_SIZE = 20; - const ErrorMessage = ({ text }: { text: string }) => { const { t } = useTranslation(); const textRef = useRef(null); @@ -107,8 +106,8 @@ const ReindexFailures = ({ ? getRdfReindexFailures : getReindexFailures; const response = await fetcher({ - offset: (page - 1) * PAGE_SIZE, - limit: PAGE_SIZE, + offset: (page - 1) * PAGE_SIZE_BASE, + limit: PAGE_SIZE_BASE, entityType, }); if (requestId !== requestSequence.current) { @@ -277,7 +276,7 @@ const ReindexFailures = ({ loading={loading} pagination={{ current: currentPage, - pageSize: PAGE_SIZE, + pageSize: PAGE_SIZE_BASE, showSizeChanger: false, total, }} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/Table/TableV2.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/Table/TableV2.tsx index 25b4459f6e7b..07e0bf633b45 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/Table/TableV2.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/Table/TableV2.tsx @@ -74,6 +74,7 @@ import { } from 'react-aria-components'; import { useTranslation } from 'react-i18next'; import { twMerge } from 'tailwind-merge'; +import { PAGE_SIZE_BASE } from '../../../constants/constants'; import { useCurrentUserPreferences } from '../../../hooks/currentUserStore/useCurrentUserStore'; import { getCustomizeColumnDetails, @@ -161,7 +162,7 @@ type TableV2Props = Omit< PaginationContract & TableV2ExtensionProps; -const DEFAULT_PAGE_SIZE = 10; +const DEFAULT_PAGE_SIZE = PAGE_SIZE_BASE; const DEFAULT_INDENT_PX = 12; const EXPANDER_GUTTER_PX = 16; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/atoms/compositions/useListingData.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/atoms/compositions/useListingData.tsx index 25045d980b15..179791a3154f 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/atoms/compositions/useListingData.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/atoms/compositions/useListingData.tsx @@ -12,6 +12,7 @@ */ import { useCallback, useEffect, useRef } from 'react'; +import { PAGE_SIZE_BASE } from '../../../../constants/constants'; import { useIsRouteVisible } from '../../../../context/RouteVisibilityProvider/RouteVisibilityProvider'; import { SearchIndex } from '../../../../enums/search.enum'; import { useQuickFilterLabels } from '../../../../hooks/useQuickFilterLabels'; @@ -50,7 +51,7 @@ export const useListingData = < const { searchIndex, baseFilter = '', - pageSize = 10, + pageSize = PAGE_SIZE_BASE, filterKeys, filterConfigs, columns, diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/atoms/data/useDataFetching.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/atoms/data/useDataFetching.tsx index a9f4538c5b45..6e1024270727 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/atoms/data/useDataFetching.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/atoms/data/useDataFetching.tsx @@ -13,6 +13,7 @@ import { AxiosError } from 'axios'; import { useCallback, useRef, useState } from 'react'; +import { PAGE_SIZE_BASE } from '../../../../constants/constants'; import { SearchIndex } from '../../../../enums/search.enum'; import { Aggregations, @@ -57,7 +58,7 @@ export const useDataFetching = ( const { searchIndex, baseFilter = '', - pageSize = 10, + pageSize = PAGE_SIZE_BASE, transform, useNlq = false, } = config; @@ -65,7 +66,7 @@ export const useDataFetching = ( // Default transform function const defaultTransform = useCallback( (data: SearchResponse) => - data.hits?.hits?.map((hit) => hit._source) || [], + (data.hits?.hits?.map((hit) => hit._source) ?? []) as unknown as T[], [] ); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/atoms/data/useUrlState.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/atoms/data/useUrlState.tsx index d07831970e94..44f39fb35479 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/atoms/data/useUrlState.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/atoms/data/useUrlState.tsx @@ -13,6 +13,7 @@ import { useCallback, useMemo } from 'react'; import { useSearchParams } from 'react-router-dom'; +import { PAGE_SIZE_BASE } from '../../../../constants/constants'; import { ExploreQuickFilterField } from '../../../Explore/ExplorePage.interface'; import { UrlState, UrlStateConfig, UrlStateHook } from '../shared/types'; @@ -26,7 +27,7 @@ export const useUrlState = ( filterKeys, pageKey = 'page', pageSizeKey = 'size', - defaultPageSize = 10, + defaultPageSize = PAGE_SIZE_BASE, filterConfigs, } = config; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/atoms/domain/compositions/useDomainListing.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/atoms/domain/compositions/useDomainListing.tsx index e0bd356e504d..1be112349b65 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/atoms/domain/compositions/useDomainListing.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/atoms/domain/compositions/useDomainListing.tsx @@ -12,7 +12,7 @@ */ import { useMemo } from 'react'; -import { TABLE_CARD_PAGE_SIZE } from '../../../../../constants/constants'; +import { PAGE_SIZE_BASE } from '../../../../../constants/constants'; import { SearchIndex } from '../../../../../enums/search.enum'; import { Domain } from '../../../../../generated/entity/domains/domain'; import domainClassBase from '../../../../../utils/Domain/DomainClassBase'; @@ -46,7 +46,7 @@ export const useDomainListing = ( baseFilter = '', enableNlq = false, nameLabelKey = 'label.domain', - pageSize = TABLE_CARD_PAGE_SIZE, + pageSize = PAGE_SIZE_BASE, basePath = '/domain', includeOwners = true, includeGlossaryTerms = true, diff --git a/openmetadata-ui/src/main/resources/ui/src/components/integration/ConnectionsPage/ConnectionsListView.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/integration/ConnectionsPage/ConnectionsListView.test.tsx index 24ece142f390..1d2964283b8e 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/integration/ConnectionsPage/ConnectionsListView.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/integration/ConnectionsPage/ConnectionsListView.test.tsx @@ -23,7 +23,7 @@ import ConnectionsListView, { import { GRID_PAGE_SIZE_OPTIONS, LIST_PAGE_SIZE_OPTIONS, -} from './ConnectionsPage.constants'; +} from '../../../constants/constants'; import { ConnectionsCategory } from './useConnectionsData'; const mockNavigate = jest.fn(); @@ -449,7 +449,7 @@ const defaultData = { serviceTypeOptions: [{ count: 24, value: 'Mysql' }], serviceTypesInTab: ['Mysql'], page: 1, - pageSize: 10, + pageSize: LIST_PAGE_SIZE_OPTIONS[0], rows: [row], setPage: mockSetPage, setPageSize: mockSetPageSize, @@ -1271,7 +1271,7 @@ describe('ConnectionsListView', () => { ); }); - it('uses twelve cards per grid page and ten rows per list page', () => { + it('uses the grid scale for cards and the list scale for rows', () => { renderView('grid'); expect(mockUseConnectionsData).toHaveBeenLastCalledWith( diff --git a/openmetadata-ui/src/main/resources/ui/src/components/integration/ConnectionsPage/ConnectionsListView.tsx b/openmetadata-ui/src/main/resources/ui/src/components/integration/ConnectionsPage/ConnectionsListView.tsx index daa9bdc6dc37..a4753e8f3015 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/integration/ConnectionsPage/ConnectionsListView.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/integration/ConnectionsPage/ConnectionsListView.tsx @@ -30,6 +30,10 @@ import React, { } from 'react'; import { useTranslation } from 'react-i18next'; import { useNavigate, useSearchParams } from 'react-router-dom'; +import { + GRID_PAGE_SIZE_OPTIONS, + LIST_PAGE_SIZE_OPTIONS, +} from '../../../constants/constants'; import { SERVICE_EMPTY_STATE_ICON_CLASS } from '../../../constants/ServiceEmptyState.constant'; import { ServiceHealth } from '../../../generated/api/services/servicesOverview'; import { TagSource } from '../../../generated/type/tagLabel'; @@ -53,9 +57,7 @@ import { DELETED_PARAM, ENTITY_TYPE_TO_CATEGORY, getConnectionsEmptyState, - GRID_PAGE_SIZE_OPTIONS, HEALTH_PARAM, - LIST_PAGE_SIZE_OPTIONS, SERVICE_TYPE_PARAM, } from './ConnectionsPage.constants'; import ConnectionsPageSkeleton from './ConnectionsPageSkeleton'; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/integration/ConnectionsPage/ConnectionsPage.constants.ts b/openmetadata-ui/src/main/resources/ui/src/components/integration/ConnectionsPage/ConnectionsPage.constants.ts index 1dbc56a559f8..7dc4c3870d17 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/integration/ConnectionsPage/ConnectionsPage.constants.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/integration/ConnectionsPage/ConnectionsPage.constants.ts @@ -107,9 +107,6 @@ export const CATEGORY_TO_ENTITY_TYPE = Object.fromEntries( // The mode is derived from the response's own `total`, never guessed. export const SERVICES_ESTATE_LIMIT = 500; -export const GRID_PAGE_SIZE_OPTIONS = [12, 24, 48]; -// Same steps as the grid so switching layout keeps the page you were on rather than resizing it. -export const LIST_PAGE_SIZE_OPTIONS = [12, 24, 48]; export const VIEW_MODE_PARAM = 'viewMode'; export const CATEGORY_PARAM = 'category'; // Health filter, set either by this page's own dropdown or by the landing Platform Health diff --git a/openmetadata-ui/src/main/resources/ui/src/components/integration/ConnectionsPage/useConnectionsData.test.ts b/openmetadata-ui/src/main/resources/ui/src/components/integration/ConnectionsPage/useConnectionsData.test.ts index 5a9a5802e8cc..dd2d435ce340 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/integration/ConnectionsPage/useConnectionsData.test.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/integration/ConnectionsPage/useConnectionsData.test.ts @@ -20,7 +20,7 @@ import { queryClient } from '../../../queryClient'; import { GRID_PAGE_SIZE_OPTIONS, LIST_PAGE_SIZE_OPTIONS, -} from './ConnectionsPage.constants'; +} from '../../../constants/constants'; import { ConnectionsCategory, useConnectionsData } from './useConnectionsData'; const mockGetServicesOverview = jest.fn(); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/integration/ConnectionsPage/useConnectionsData.ts b/openmetadata-ui/src/main/resources/ui/src/components/integration/ConnectionsPage/useConnectionsData.ts index 88096a2ad257..f22061145850 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/integration/ConnectionsPage/useConnectionsData.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/integration/ConnectionsPage/useConnectionsData.ts @@ -18,6 +18,7 @@ import { } from '@tanstack/react-query'; import { debounce } from 'lodash'; import { useEffect, useMemo, useRef, useState } from 'react'; +import { LIST_PAGE_SIZE_OPTIONS } from '../../../constants/constants'; import { ServiceHealth, ServiceSummary, @@ -30,7 +31,6 @@ import { ConnectionsServiceCategory, CONNECTIONS_ENTITY_TYPES, ENTITY_TYPE_TO_CATEGORY, - LIST_PAGE_SIZE_OPTIONS, SERVICES_ESTATE_LIMIT, } from './ConnectionsPage.constants'; import { diff --git a/openmetadata-ui/src/main/resources/ui/src/constants/ContextCenter.constants.ts b/openmetadata-ui/src/main/resources/ui/src/constants/ContextCenter.constants.ts index 133c104f645c..648b06328591 100644 --- a/openmetadata-ui/src/main/resources/ui/src/constants/ContextCenter.constants.ts +++ b/openmetadata-ui/src/main/resources/ui/src/constants/ContextCenter.constants.ts @@ -13,6 +13,7 @@ // ─── File type badge ────────────────────────────────────────────────────────── import { EntityFields } from '../enums/AdvancedSearch.enum'; +import { PAGE_SIZE_BASE } from './constants'; import { ContextMemoryStatus, MemoryType, @@ -73,7 +74,7 @@ export const ARTICLE_SORT_OPTIONS: ArticleSortOption[] = [ export const DEFAULT_ARTICLE_SORT_OPTION = ARTICLE_SORT_OPTIONS[0]; -export const ARCHIVE_PAGE_SIZE = 15; +export const ARCHIVE_PAGE_SIZE = PAGE_SIZE_BASE; export const RECENT_DASHBOARD_ARTICLES_LIMIT = 3; export const RECENT_DASHBOARD_DOCUMENTS_LIMIT = 3; export const RECENT_DASHBOARD_MEMORIES_LIMIT = 3; @@ -133,7 +134,7 @@ export const VISIBILITY_OPTIONS = [ }, ]; -export const MEMORIES_PER_PAGE = 10; +export const MEMORIES_PER_PAGE = PAGE_SIZE_BASE; export const MEMORY_FIELDS = 'owners,tags,domains,primaryEntity,relatedEntities,derivedEntities,sourceEntity'; diff --git a/openmetadata-ui/src/main/resources/ui/src/constants/Learning.constants.ts b/openmetadata-ui/src/main/resources/ui/src/constants/Learning.constants.ts index b83de92bc2a4..656c792c4466 100644 --- a/openmetadata-ui/src/main/resources/ui/src/constants/Learning.constants.ts +++ b/openmetadata-ui/src/main/resources/ui/src/constants/Learning.constants.ts @@ -22,7 +22,6 @@ export { LearningResourceStatus, ResourceCategory, ResourceType }; export const MAX_VISIBLE_TAGS = 2; export const MAX_VISIBLE_CONTEXTS = 2; -export const DEFAULT_PAGE_SIZE = 10; export const MAX_CHAIN_PAGES = 25; diff --git a/openmetadata-ui/src/main/resources/ui/src/constants/constants.ts b/openmetadata-ui/src/main/resources/ui/src/constants/constants.ts index be23bbc036e9..aa3e488ac634 100644 --- a/openmetadata-ui/src/main/resources/ui/src/constants/constants.ts +++ b/openmetadata-ui/src/main/resources/ui/src/constants/constants.ts @@ -53,10 +53,18 @@ export const INITIAL_PAGING_VALUE = 1; export const JSON_TAB_SIZE = 2; export const KNOWLEDGE_LIST_LENGTH = 8; export const PAGE_SIZE = 10; -export const TABLE_CARD_PAGE_SIZE = 9; export const PAGE_SIZE_BASE = 15; export const PAGE_SIZE_MEDIUM = 25; export const PAGE_SIZE_LARGE = 50; +// Paged lists/tables step 15/25/50; paged card grids step 12/24/48 so rows of 2, 3 or 4 fill evenly. +// ADR:2026-10-13-ui-page-sizes-lists-15-25-50-grids-12-24-48 +export const LIST_PAGE_SIZE_OPTIONS = [ + PAGE_SIZE_BASE, + PAGE_SIZE_MEDIUM, + PAGE_SIZE_LARGE, +]; +export const GRID_PAGE_SIZE_BASE = 12; +export const GRID_PAGE_SIZE_OPTIONS = [GRID_PAGE_SIZE_BASE, 24, 48]; export const PAGE_SIZE_EXTRA_LARGE = 100; export const AGGREGATE_PAGE_SIZE_LARGE = 1000; export const ES_MAX_PAGE_SIZE = 10000; diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/Application/ApplicationPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/Application/ApplicationPage.tsx index c39e158deb27..bf1ebfbc3662 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/Application/ApplicationPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/Application/ApplicationPage.tsx @@ -31,7 +31,11 @@ import { TitleBreadcrumbProps } from '../../components/common/TitleBreadcrumb/Ti import PageHeader from '../../components/PageHeader/PageHeader.component'; import PageLayoutV1 from '../../components/PageLayoutV1/PageLayoutV1'; import ApplicationCard from '../../components/Settings/Applications/ApplicationCard/ApplicationCard.component'; -import { ROUTES } from '../../constants/constants'; +import { + GRID_PAGE_SIZE_BASE, + GRID_PAGE_SIZE_OPTIONS, + ROUTES, +} from '../../constants/constants'; import { GlobalSettingsMenuCategory } from '../../constants/GlobalSettings.constants'; import { LEARNING_PAGE_IDS } from '../../constants/Learning.constants'; import { PAGE_HEADERS } from '../../constants/PageHeaders.constant'; @@ -61,7 +65,7 @@ const ApplicationPage = () => { handlePageSizeChange, showPagination, pagingCursor, - } = usePaging(); + } = usePaging(GRID_PAGE_SIZE_BASE, GRID_PAGE_SIZE_OPTIONS); const navigate = useNavigate(); const [isLoading, setIsLoading] = useState(true); const [applicationData, setApplicationData] = useState(); @@ -261,6 +265,7 @@ const ApplicationPage = () => { currentPage={currentPage} isLoading={isLoading} pageSize={pageSize} + pageSizeOptions={GRID_PAGE_SIZE_OPTIONS} paging={paging} pagingHandler={handleApplicationPageChange} onShowSizeChange={handlePageSizeChange} diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/LearningResourcesPage/LearningResourcesPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/LearningResourcesPage/LearningResourcesPage.tsx index c4a8be3f53de..3c07809eb9c5 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/LearningResourcesPage/LearningResourcesPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/LearningResourcesPage/LearningResourcesPage.tsx @@ -41,11 +41,6 @@ import { LearningResourceCard } from '../../components/Learning/LearningResource import { ResourcePlayerModal } from '../../components/Learning/ResourcePlayer/ResourcePlayerModal.component'; import { ResourceTypeIcon } from '../../components/Learning/ResourceTypeIcon/ResourceTypeIcon'; import PageLayoutV1 from '../../components/PageLayoutV1/PageLayoutV1'; -import { - PAGE_SIZE_BASE, - PAGE_SIZE_LARGE, - PAGE_SIZE_MEDIUM, -} from '../../constants/constants'; import { GlobalSettingsMenuCategory } from '../../constants/GlobalSettings.constants'; import { MAX_VISIBLE_CONTEXTS, @@ -56,6 +51,10 @@ import { VIEW_MODE_PAGE } from '../../constants/platform/personaAppLayout.consta import { PageViewMode } from '../../generated/type/personaPreferences'; import { usePersonaViewMode } from '../../hooks/platform/usePersonaViewMode'; import { LearningResource } from '../../rest/learningResourceAPI'; +import { + getViewPageSizeOptions, + resolveViewPageSize, +} from '../../utils/PaginationUtils'; import { getSettingPath } from '../../utils/RouterUtils'; import { useLearningResourceActions } from './hooks/useLearningResourceActions'; import { @@ -85,7 +84,13 @@ export const LearningResourcesPage: React.FC = () => { {} ); const [currentPage, setCurrentPage] = useState(1); - const [pageSize, setPageSize] = useState(PAGE_SIZE_BASE); + const [selectedPageSize, setPageSize] = useState(); + + const personaView = usePersonaViewMode(VIEW_MODE_PAGE.LearningResources); + const [selectedView, setView] = useState(); + const view = selectedView ?? personaView; + const pageSizeOptions = getViewPageSizeOptions(view); + const pageSize = resolveViewPageSize(view, selectedPageSize); const { resources, paging, isLoading, refetch } = useLearningResources({ searchText, @@ -112,10 +117,6 @@ export const LearningResourcesPage: React.FC = () => { handlePlayerClose, } = useLearningResourceActions({ onRefetch: refetch }); - const personaView = usePersonaViewMode(VIEW_MODE_PAGE.LearningResources); - const [selectedView, setView] = useState(); - const view = selectedView ?? personaView; - const [searchInputValue, setSearchInputValue] = useState(searchText); const debouncedSetSearchText = useMemo( @@ -143,6 +144,11 @@ export const LearningResourcesPage: React.FC = () => { setCurrentPage(1); }, []); + const handleViewChange = useCallback((nextView: PageViewMode) => { + setView(nextView); + setCurrentPage(1); + }, []); + const breadcrumbs = useMemo( () => [ { name: t('label.setting-plural'), url: getSettingPath() }, @@ -175,10 +181,17 @@ export const LearningResourcesPage: React.FC = () => { currentPage, isNumberBased: true, isLoading, - pageSizeOptions: [PAGE_SIZE_BASE, PAGE_SIZE_MEDIUM, PAGE_SIZE_LARGE], + pageSizeOptions, onShowSizeChange: handlePageSizeChange, }), - [paging.total, pageSize, currentPage, isLoading, handlePageSizeChange] + [ + paging.total, + pageSize, + pageSizeOptions, + currentPage, + isLoading, + handlePageSizeChange, + ] ); const renderCategoriesCell = (record: LearningResource) => { @@ -354,7 +367,7 @@ export const LearningResourcesPage: React.FC = () => { /> {quickFilters} - + {filterSelectionDisplay} diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/MarketPlacePage/MarketPlacePage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/MarketPlacePage/MarketPlacePage.tsx index 8651d895437d..7c6bf676e7db 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/MarketPlacePage/MarketPlacePage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/MarketPlacePage/MarketPlacePage.tsx @@ -26,6 +26,10 @@ import TitleBreadcrumb from '../../components/common/TitleBreadcrumb/TitleBreadc import PageHeader from '../../components/PageHeader/PageHeader.component'; import PageLayoutV1 from '../../components/PageLayoutV1/PageLayoutV1'; import ApplicationCard from '../../components/Settings/Applications/ApplicationCard/ApplicationCard.component'; +import { + GRID_PAGE_SIZE_BASE, + GRID_PAGE_SIZE_OPTIONS, +} from '../../constants/constants'; import { GlobalSettingOptions } from '../../constants/GlobalSettings.constants'; import { LEARNING_PAGE_IDS } from '../../constants/Learning.constants'; import { PAGE_HEADERS } from '../../constants/PageHeaders.constant'; @@ -54,7 +58,7 @@ const MarketPlacePage = () => { handlePageSizeChange, showPagination, pagingCursor, - } = usePaging(); + } = usePaging(GRID_PAGE_SIZE_BASE, GRID_PAGE_SIZE_OPTIONS); const navigate = useNavigate(); const [isLoading, setIsLoading] = useState(true); const [applicationData, setApplicationData] = @@ -208,6 +212,7 @@ const MarketPlacePage = () => { currentPage={currentPage} isLoading={isLoading} pageSize={pageSize} + pageSizeOptions={GRID_PAGE_SIZE_OPTIONS} paging={paging} pagingHandler={handleMarketPlacePageChange} onShowSizeChange={handlePageSizeChange} diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/MetricsPage/MetricListPage/MetricListPage.test.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/MetricsPage/MetricListPage/MetricListPage.test.tsx index 44c852d665f1..b191d65ccad5 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/MetricsPage/MetricListPage/MetricListPage.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/MetricsPage/MetricListPage/MetricListPage.test.tsx @@ -19,6 +19,7 @@ import { within, } from '@testing-library/react'; import { MemoryRouter } from 'react-router-dom'; +import { PAGE_SIZE_BASE } from '../../../constants/constants'; import { EntityType } from '../../../enums/entity.enum'; import { EntityStatus, @@ -544,7 +545,7 @@ const setHierarchy = ({ mockUseMetricHierarchy.mockImplementation(() => ({ topLevelNodes, buildRows: () => rows, - paging: { total, offset: 0, limit: 20 }, + paging: { total, offset: 0, limit: PAGE_SIZE_BASE }, isPending, isFetching: isPending, error, @@ -1131,7 +1132,11 @@ describe('MetricListPage', () => { await waitFor(() => expect(mockUseMetricHierarchy).toHaveBeenLastCalledWith( - expect.objectContaining({ page: 2, pageSize: 20, query: '' }) + expect.objectContaining({ + page: 2, + pageSize: PAGE_SIZE_BASE, + query: '', + }) ) ); diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/MetricsPage/MetricListPage/MetricListPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/MetricsPage/MetricListPage/MetricListPage.tsx index 6171b37698b3..63548f450a10 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/MetricsPage/MetricListPage/MetricListPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/MetricsPage/MetricListPage/MetricListPage.tsx @@ -123,6 +123,7 @@ import { MetricTreeNode, VisibleMetricTableRow, } from '../../../utils/MetricEntityUtils/MetricHierarchyUtils'; +import { getViewPageSize } from '../../../utils/PaginationUtils'; import { getDerivedPermissionFlags } from '../../../utils/PermissionDerivation'; import { DEFAULT_ENTITY_PERMISSION } from '../../../utils/PermissionsUtils'; import { getEntityDetailsPath } from '../../../utils/RouterUtils'; @@ -152,7 +153,6 @@ const METRIC_TABLE_PREFERENCE_KEY = 'metricList'; const METRIC_STATIC_COLUMNS = ['name']; const METRIC_VIEW_STORAGE_KEY = 'metricsList.viewMode.v1'; const METRIC_SEARCH_DEBOUNCE_MS = 500; -const METRIC_PAGE_SIZE = 20; const DEFAULT_VISIBLE_METRIC_COLUMNS: MetricColumnId[] = [ 'description', @@ -263,6 +263,7 @@ const MetricListPage = () => { // Owned by TableV2's Customize menu; card view only reads it. const visibleColumns = getVisibleMetricColumns(selectedEntityTableColumns); const [viewMode, setViewMode] = useState(getInitialViewMode); + const metricPageSize = getViewPageSize(viewMode); const [isExporting, setIsExporting] = useState(false); const [isMetricActionsOpen, setIsMetricActionsOpen] = useState(false); const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); @@ -306,14 +307,14 @@ const MetricListPage = () => { search: debouncedSearch, status: statusFilter, page, - size: METRIC_PAGE_SIZE, + size: metricPageSize, }, ], queryFn: () => searchQuery({ query: debouncedSearch, pageNumber: page, - pageSize: METRIC_PAGE_SIZE, + pageSize: metricPageSize, searchIndex: SearchIndex.METRIC, trackTotalHits: true, queryFilter: statusFilter @@ -346,7 +347,7 @@ const MetricListPage = () => { } = useMetricHierarchy({ enabled: [hasViewPermission, isTreeMode].every(Boolean), page, - pageSize: METRIC_PAGE_SIZE, + pageSize: metricPageSize, query: debouncedSearch, }); @@ -452,7 +453,7 @@ const MetricListPage = () => { const totalMetrics = isTreeMode ? treePaging.total : searchResponse?.hits.total.value ?? 0; - const totalPages = Math.max(1, Math.ceil(totalMetrics / METRIC_PAGE_SIZE)); + const totalPages = Math.max(1, Math.ceil(totalMetrics / metricPageSize)); const listingError = permissionError ?? (isTreeMode ? treeError : searchError); const listingErrorMessage = permissionError @@ -530,10 +531,15 @@ const MetricListPage = () => { [setCurrentPage] ); - const handleViewModeChange = useCallback((nextMode: MetricViewMode) => { - setViewMode(nextMode); - localStorage.setItem(METRIC_VIEW_STORAGE_KEY, nextMode); - }, []); + // Card and table views page at different sizes, so the current page may not exist in the other. + const handleViewModeChange = useCallback( + (nextMode: MetricViewMode) => { + setViewMode(nextMode); + setCurrentPage(1); + localStorage.setItem(METRIC_VIEW_STORAGE_KEY, nextMode); + }, + [setCurrentPage] + ); const handleImport = useCallback(() => { setIsMetricActionsOpen(false); @@ -1050,7 +1056,7 @@ const MetricListPage = () => { ); const renderPagination = () => - totalMetrics > METRIC_PAGE_SIZE ? ( + totalMetrics > metricPageSize ? ( { const { isAdminUser } = useAuth(); const { t } = useTranslation(); @@ -57,7 +59,7 @@ export const PersonaPage = ({ pageTitle }: { pageTitle: string }) => { paging, pagingCursor, showPagination, - } = usePaging(PERSONA_PAGE_SIZE); + } = usePaging(GRID_PAGE_SIZE_BASE, GRID_PAGE_SIZE_OPTIONS); const breadcrumbs: TitleBreadcrumbProps['titleLinks'] = useMemo( () => getSettingPageEntityBreadCrumb(GlobalSettingsMenuCategory.PERSONA), diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/ServiceVersionPage/ServiceVersionMainTabContent.interface.ts b/openmetadata-ui/src/main/resources/ui/src/pages/ServiceVersionPage/ServiceVersionMainTabContent.interface.ts index 7e0d9b08b12f..8234a0cdcdde 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/ServiceVersionPage/ServiceVersionMainTabContent.interface.ts +++ b/openmetadata-ui/src/main/resources/ui/src/pages/ServiceVersionPage/ServiceVersionMainTabContent.interface.ts @@ -24,6 +24,7 @@ export interface ServiceVersionMainTabContentProps { data: ServicePageData[]; isServiceLoading: boolean; paging: Paging; + pageSize: number; currentPage: number; pagingHandler: NextPreviousProps['pagingHandler']; entityType: EntityType; diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/ServiceVersionPage/ServiceVersionMainTabContent.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/ServiceVersionPage/ServiceVersionMainTabContent.tsx index a14743213089..b38a90809e02 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/ServiceVersionPage/ServiceVersionMainTabContent.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/ServiceVersionPage/ServiceVersionMainTabContent.tsx @@ -24,7 +24,6 @@ import { ColumnsType } from '../../components/common/Table/Table.interface'; import Table from '../../components/common/Table/TableV2'; import TagsContainerV2 from '../../components/Tag/TagsContainerV2/TagsContainerV2'; import { DisplayType } from '../../components/Tag/TagsViewer/TagsViewer.interface'; -import { PAGE_SIZE } from '../../constants/constants'; import { TABLE_SCROLL_VALUE } from '../../constants/Table.constants'; import { TagSource } from '../../generated/type/tagLabel'; import { useFqn } from '../../hooks/useFqn'; @@ -40,6 +39,7 @@ function ServiceVersionMainTabContent({ data, isServiceLoading, paging, + pageSize, pagingHandler, currentPage, serviceDetails, @@ -106,7 +106,7 @@ function ServiceVersionMainTabContent({ showPagination: Boolean(!isNil(paging.after) || !isNil(paging.before)) && !isEmpty(data), - pageSize: PAGE_SIZE, + pageSize, paging, pagingHandler, }} diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/ServiceVersionPage/ServiceVersionPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/ServiceVersionPage/ServiceVersionPage.tsx index bfa1631a9a39..c66a656318fc 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/ServiceVersionPage/ServiceVersionPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/ServiceVersionPage/ServiceVersionPage.tsx @@ -334,7 +334,8 @@ function ServiceVersionPage() { ); const getOtherDetails = useCallback( - async (paging?: PagingWithoutTotal) => { + async (cursor?: PagingWithoutTotal) => { + const paging = { ...cursor, limit: pageSize }; try { setIsOtherDataLoading(true); switch (serviceCategory) { @@ -394,6 +395,7 @@ function ServiceVersionPage() { }, [ serviceCategory, + pageSize, fetchDatabases, fetchTopics, fetchDashboards, @@ -482,6 +484,7 @@ function ServiceVersionPage() { data={data} entityType={entityType} isServiceLoading={isOtherDataLoading} + pageSize={pageSize} paging={paging} pagingHandler={pagingHandler} serviceDetails={currentVersionData} diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/PaginationUtils.test.ts b/openmetadata-ui/src/main/resources/ui/src/utils/PaginationUtils.test.ts index 0552cc68d0ce..e21a9b6710ed 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/PaginationUtils.test.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/PaginationUtils.test.ts @@ -1,4 +1,14 @@ -import { computeTotalPages } from './PaginationUtils'; +import { + GRID_PAGE_SIZE_OPTIONS, + LIST_PAGE_SIZE_OPTIONS, +} from '../constants/constants'; +import { PageViewMode } from '../generated/type/personaPreferences'; +import { + computeTotalPages, + getViewPageSize, + getViewPageSizeOptions, + resolveViewPageSize, +} from './PaginationUtils'; /* * Copyright 2024 Collate. @@ -41,3 +51,41 @@ describe('computeTotalPages', () => { expect(computeTotalPages(20, 10)).toBe(1); }); }); + +// ADR:2026-10-13-ui-page-sizes-lists-15-25-50-grids-12-24-48 +describe('view page sizes', () => { + it('pages lists at 15/25/50 and card grids at 12/24/48', () => { + expect(LIST_PAGE_SIZE_OPTIONS).toEqual([15, 25, 50]); + expect(GRID_PAGE_SIZE_OPTIONS).toEqual([12, 24, 48]); + }); + + it('uses the grid scale only for the card view', () => { + expect(getViewPageSizeOptions(PageViewMode.Card)).toBe( + GRID_PAGE_SIZE_OPTIONS + ); + expect(getViewPageSizeOptions('card')).toBe(GRID_PAGE_SIZE_OPTIONS); + expect(getViewPageSizeOptions(PageViewMode.Table)).toBe( + LIST_PAGE_SIZE_OPTIONS + ); + expect(getViewPageSizeOptions(PageViewMode.Tree)).toBe( + LIST_PAGE_SIZE_OPTIONS + ); + expect(getViewPageSizeOptions(undefined)).toBe(LIST_PAGE_SIZE_OPTIONS); + }); + + it('starts each view on the first step of its scale', () => { + expect(getViewPageSize(PageViewMode.Card)).toBe(12); + expect(getViewPageSize(PageViewMode.Table)).toBe(15); + }); + + it('keeps a picked size that the view offers', () => { + expect(resolveViewPageSize(PageViewMode.Table, 50)).toBe(50); + expect(resolveViewPageSize(PageViewMode.Card, 24)).toBe(24); + }); + + it('falls back to the first step when the picked size belongs to the other scale', () => { + expect(resolveViewPageSize(PageViewMode.Card, 25)).toBe(12); + expect(resolveViewPageSize(PageViewMode.Table, 24)).toBe(15); + expect(resolveViewPageSize(PageViewMode.Table)).toBe(15); + }); +}); diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/PaginationUtils.ts b/openmetadata-ui/src/main/resources/ui/src/utils/PaginationUtils.ts index 084f32a8a2b4..b6e07ed76c44 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/PaginationUtils.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/PaginationUtils.ts @@ -11,6 +11,29 @@ * limitations under the License. */ +import { + GRID_PAGE_SIZE_OPTIONS, + LIST_PAGE_SIZE_OPTIONS, +} from '../constants/constants'; +import { PageViewMode } from '../generated/type/personaPreferences'; + export const computeTotalPages = (pSize: number, total: number) => { return Math.ceil(total / pSize); }; + +type ViewMode = PageViewMode | `${PageViewMode}`; + +export const getViewPageSizeOptions = (view?: ViewMode) => + view === PageViewMode.Card ? GRID_PAGE_SIZE_OPTIONS : LIST_PAGE_SIZE_OPTIONS; + +export const getViewPageSize = (view?: ViewMode) => + getViewPageSizeOptions(view)[0]; + +// The two scales share no size, so a size picked in one view falls back to the other's first step. +export const resolveViewPageSize = (view?: ViewMode, selectedSize?: number) => { + const options = getViewPageSizeOptions(view); + + return selectedSize && options.includes(selectedSize) + ? selectedSize + : options[0]; +}; From de9cb2629efc226321eb77f6fa24ce881ec462f2 Mon Sep 17 00:00:00 2001 From: Sweta Agarwalla Date: Tue, 13 Oct 2026 11:00:18 +0530 Subject: [PATCH 02/27] Reset Connections to page 1 when the layout changes scale; fix checkstyle - Connections grid (12/24/48) and list (15/25/50) now page at different sizes, so switching layout resets to page 1 instead of keeping a page number that can fall past the last row. Add a regression test. - ServiceVersionPage: add pageSize to the pagingHandler and tabs dependency lists so a size change reaches the table. - Apply organize-imports ordering flagged by UI checkstyle. --- .../components/DomainListing/DomainListPage.tsx | 2 +- .../ConnectionsListView.test.tsx | 8 ++++---- .../ConnectionsPage/useConnectionsData.test.ts | 17 +++++++++++++++-- .../ConnectionsPage/useConnectionsData.ts | 4 +++- .../ui/src/constants/ContextCenter.constants.ts | 2 +- .../ServiceVersionPage/ServiceVersionPage.tsx | 3 ++- 6 files changed, 26 insertions(+), 10 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DomainListing/DomainListPage.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DomainListing/DomainListPage.tsx index e9e8315cbc49..463c0eba8a25 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DomainListing/DomainListPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DomainListing/DomainListPage.tsx @@ -35,6 +35,7 @@ import { PageViewMode } from '../../generated/type/personaPreferences'; import { usePersonaViewMode } from '../../hooks/platform/usePersonaViewMode'; import { useIsAiMode } from '../../hooks/useAppMode'; import { useMarketplaceStore } from '../../hooks/useMarketplaceStore'; +import { getViewPageSize } from '../../utils/PaginationUtils'; import { useDelete } from '../common/atoms/actions/useDelete'; import { useDomainCardTemplates } from '../common/atoms/domain/ui/useDomainCardTemplates'; import { useDomainFilters } from '../common/atoms/domain/ui/useDomainFilters'; @@ -51,7 +52,6 @@ import ViewToggle from '../common/ViewToggle/ViewToggle'; import MarketplaceSearchInput from '../DataMarketplace/MarketplaceSearchInput/MarketplaceSearchInput.component'; import PageLayoutV1 from '../PageLayoutV1/PageLayoutV1'; import DomainTreeView from './components/DomainTreeView'; -import { getViewPageSize } from '../../utils/PaginationUtils'; import { DomainListPageProps } from './DomainListPage.interface'; import { useDomainCreateDrawer } from './hooks/useDomainCreateDrawer'; import { useDomainListingData } from './hooks/useDomainListingData'; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/integration/ConnectionsPage/ConnectionsListView.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/integration/ConnectionsPage/ConnectionsListView.test.tsx index 1d2964283b8e..d16fcc62781f 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/integration/ConnectionsPage/ConnectionsListView.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/integration/ConnectionsPage/ConnectionsListView.test.tsx @@ -13,6 +13,10 @@ import { fireEvent, render, screen } from '@testing-library/react'; import React from 'react'; +import { + GRID_PAGE_SIZE_OPTIONS, + LIST_PAGE_SIZE_OPTIONS, +} from '../../../constants/constants'; import { ALL_SERVICES_CATEGORY } from '../../../constants/Services.constant'; import { ServiceCategory } from '../../../enums/service.enum'; import { EXTENSION_POINTS } from '../../../utils/ExtensionPointTypes'; @@ -20,10 +24,6 @@ import ConnectionsListView, { NAME_COLUMN_WIDTH, SECONDARY_COLUMN_WIDTH, } from './ConnectionsListView'; -import { - GRID_PAGE_SIZE_OPTIONS, - LIST_PAGE_SIZE_OPTIONS, -} from '../../../constants/constants'; import { ConnectionsCategory } from './useConnectionsData'; const mockNavigate = jest.fn(); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/integration/ConnectionsPage/useConnectionsData.test.ts b/openmetadata-ui/src/main/resources/ui/src/components/integration/ConnectionsPage/useConnectionsData.test.ts index dd2d435ce340..700d0b7640ce 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/integration/ConnectionsPage/useConnectionsData.test.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/integration/ConnectionsPage/useConnectionsData.test.ts @@ -15,12 +15,12 @@ import { QueryClientProvider } from '@tanstack/react-query'; import { act, renderHook, waitFor } from '@testing-library/react'; import React from 'react'; import { MemoryRouter } from 'react-router-dom'; -import { ServiceHealth } from '../../../generated/api/services/servicesOverview'; -import { queryClient } from '../../../queryClient'; import { GRID_PAGE_SIZE_OPTIONS, LIST_PAGE_SIZE_OPTIONS, } from '../../../constants/constants'; +import { ServiceHealth } from '../../../generated/api/services/servicesOverview'; +import { queryClient } from '../../../queryClient'; import { ConnectionsCategory, useConnectionsData } from './useConnectionsData'; const mockGetServicesOverview = jest.fn(); @@ -432,6 +432,19 @@ describe('useConnectionsData', () => { await waitFor(() => expect(result.current.page).toBe(1)); }); + it('returns to page 1 when the layout switches to the other page-size scale', async () => { + const { result, rerender } = renderConnections(); + await goToPageThree(result, rerender); + + const gridArgs = { + ...defaultArgs, + pageSizeOptions: GRID_PAGE_SIZE_OPTIONS, + }; + rerender(gridArgs); + + await waitFor(() => expect(result.current.page).toBe(1)); + }); + it('keeps the page when only the sort order changes', async () => { const { result, rerender } = renderConnections(); await goToPageThree(result, rerender); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/integration/ConnectionsPage/useConnectionsData.ts b/openmetadata-ui/src/main/resources/ui/src/components/integration/ConnectionsPage/useConnectionsData.ts index f22061145850..63405fd7031b 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/integration/ConnectionsPage/useConnectionsData.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/integration/ConnectionsPage/useConnectionsData.ts @@ -187,8 +187,10 @@ export const useConnectionsData = ({ // currentPage from the URL rather than resetting it, so without this, switching tab or filter // from page 3 stays on page 3: client mode slices past the end and shows an empty list beside a // non-zero count, and server mode asks for an offset the narrowed result set no longer has. - // Sort order is deliberately absent — reordering does not change which rows match. + // Sort order is deliberately absent — reordering does not change which rows match. The page-size + // scale is present: grid and list page at different sizes, so a page number does not carry over. const queryKey = [ + pageSizeOptions.join(','), category, String(showDeleted), searchTerm.trim(), diff --git a/openmetadata-ui/src/main/resources/ui/src/constants/ContextCenter.constants.ts b/openmetadata-ui/src/main/resources/ui/src/constants/ContextCenter.constants.ts index 648b06328591..892f71129f68 100644 --- a/openmetadata-ui/src/main/resources/ui/src/constants/ContextCenter.constants.ts +++ b/openmetadata-ui/src/main/resources/ui/src/constants/ContextCenter.constants.ts @@ -13,13 +13,13 @@ // ─── File type badge ────────────────────────────────────────────────────────── import { EntityFields } from '../enums/AdvancedSearch.enum'; -import { PAGE_SIZE_BASE } from './constants'; import { ContextMemoryStatus, MemoryType, ShareVisibility, } from '../generated/entity/context/contextMemory'; import { ExploreQuickFilterField } from '../interface/quickFilter.interface'; +import { PAGE_SIZE_BASE } from './constants'; export const DOCUMENT_MAX_FILE_SIZE = 5 * 1024 * 1024; diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/ServiceVersionPage/ServiceVersionPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/ServiceVersionPage/ServiceVersionPage.tsx index c66a656318fc..bd0a5859c942 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/ServiceVersionPage/ServiceVersionPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/ServiceVersionPage/ServiceVersionPage.tsx @@ -465,7 +465,7 @@ function ServiceVersionPage() { ); } }, - [paging, getOtherDetails, handlePageChange] + [paging, pageSize, getOtherDetails, handlePageChange] ); const tabs: DetailsTabItem[] = useMemo(() => { @@ -503,6 +503,7 @@ function ServiceVersionPage() { currentVersionData, serviceCategory, paging, + pageSize, data, isOtherDataLoading, getOtherDetails, From bd83735a8a00d507ff19102bf5850fcd0182ea52 Mon Sep 17 00:00:00 2001 From: Sweta Agarwalla Date: Tue, 13 Oct 2026 12:50:30 +0530 Subject: [PATCH 03/27] Re-expand metric groups after a view switch; fix Sonar issues - MetricListPage: card and table views page at different sizes, and useMetricHierarchy clears expansion when the size changes. Include the page size in the default-expansion scope so groups re-expand after a view switch (the desktop visual-regression test found "Gross Margin" hidden inside a collapsed group). - ContextCenter.constants: re-export PAGE_SIZE_BASE with export...from (S7763). - LearningResourcesPage: name the setter after its state value (S6754). - TableV2: `customPaginationProps?: never` instead of the redundant `?: undefined` (S4782). --- .../ui/src/components/common/Table/TableV2.tsx | 2 +- .../ui/src/constants/ContextCenter.constants.ts | 7 ++++--- .../LearningResourcesPage/LearningResourcesPage.tsx | 4 ++-- .../MetricsPage/MetricListPage/MetricListPage.test.tsx | 10 +++++----- .../MetricsPage/MetricListPage/MetricListPage.tsx | 3 ++- 5 files changed, 14 insertions(+), 12 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/Table/TableV2.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/Table/TableV2.tsx index 07e0bf633b45..98031e73f708 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/Table/TableV2.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/Table/TableV2.tsx @@ -150,7 +150,7 @@ type CustomPaginationProps = NonNullable< */ type PaginationContract = | { - customPaginationProps?: undefined; + customPaginationProps?: never; pagination?: TableComponentProps['pagination']; } | { customPaginationProps: CustomPaginationProps; pagination: false }; diff --git a/openmetadata-ui/src/main/resources/ui/src/constants/ContextCenter.constants.ts b/openmetadata-ui/src/main/resources/ui/src/constants/ContextCenter.constants.ts index 892f71129f68..6fdc565f3610 100644 --- a/openmetadata-ui/src/main/resources/ui/src/constants/ContextCenter.constants.ts +++ b/openmetadata-ui/src/main/resources/ui/src/constants/ContextCenter.constants.ts @@ -19,7 +19,6 @@ import { ShareVisibility, } from '../generated/entity/context/contextMemory'; import { ExploreQuickFilterField } from '../interface/quickFilter.interface'; -import { PAGE_SIZE_BASE } from './constants'; export const DOCUMENT_MAX_FILE_SIZE = 5 * 1024 * 1024; @@ -74,7 +73,10 @@ export const ARTICLE_SORT_OPTIONS: ArticleSortOption[] = [ export const DEFAULT_ARTICLE_SORT_OPTION = ARTICLE_SORT_OPTIONS[0]; -export const ARCHIVE_PAGE_SIZE = PAGE_SIZE_BASE; +export { + PAGE_SIZE_BASE as ARCHIVE_PAGE_SIZE, + PAGE_SIZE_BASE as MEMORIES_PER_PAGE, +} from './constants'; export const RECENT_DASHBOARD_ARTICLES_LIMIT = 3; export const RECENT_DASHBOARD_DOCUMENTS_LIMIT = 3; export const RECENT_DASHBOARD_MEMORIES_LIMIT = 3; @@ -134,7 +136,6 @@ export const VISIBILITY_OPTIONS = [ }, ]; -export const MEMORIES_PER_PAGE = PAGE_SIZE_BASE; export const MEMORY_FIELDS = 'owners,tags,domains,primaryEntity,relatedEntities,derivedEntities,sourceEntity'; diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/LearningResourcesPage/LearningResourcesPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/LearningResourcesPage/LearningResourcesPage.tsx index 3c07809eb9c5..923f388e30ea 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/LearningResourcesPage/LearningResourcesPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/LearningResourcesPage/LearningResourcesPage.tsx @@ -84,7 +84,7 @@ export const LearningResourcesPage: React.FC = () => { {} ); const [currentPage, setCurrentPage] = useState(1); - const [selectedPageSize, setPageSize] = useState(); + const [selectedPageSize, setSelectedPageSize] = useState(); const personaView = usePersonaViewMode(VIEW_MODE_PAGE.LearningResources); const [selectedView, setView] = useState(); @@ -140,7 +140,7 @@ export const LearningResourcesPage: React.FC = () => { }, [searchText, filterState]); const handlePageSizeChange = useCallback((size: number) => { - setPageSize(size); + setSelectedPageSize(size); setCurrentPage(1); }, []); diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/MetricsPage/MetricListPage/MetricListPage.test.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/MetricsPage/MetricListPage/MetricListPage.test.tsx index b191d65ccad5..1495acbff77a 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/MetricsPage/MetricListPage/MetricListPage.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/MetricsPage/MetricListPage/MetricListPage.test.tsx @@ -1013,7 +1013,7 @@ describe('MetricListPage', () => { ).not.toBeInTheDocument(); }); - it('expands group banners once by default without expanding metric variants or overriding collapse', async () => { + it('expands group banners once per page scope without expanding metric variants', async () => { const groupRow = { id: 'group:group-1', isGroupRow: true, @@ -1061,12 +1061,12 @@ describe('MetricListPage', () => { { row: groupRow, groupId: 'group-1', members: [groupedMetric] }, ], }); + // Card view pages at a different size, and useMetricHierarchy clears expansion when the size + // changes, so the default expansion is applied again for the new scope. fireEvent.click(cardViewButton); - expect(hierarchyActions.expandAll).toHaveBeenCalledTimes(1); - expect(screen.getByTestId('metric-group-profitability')).toHaveAttribute( - 'aria-expanded', - 'false' + await waitFor(() => + expect(hierarchyActions.expandAll).toHaveBeenCalledTimes(2) ); }); diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/MetricsPage/MetricListPage/MetricListPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/MetricsPage/MetricListPage/MetricListPage.tsx index 63548f450a10..5af7efc1de97 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/MetricsPage/MetricListPage/MetricListPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/MetricsPage/MetricListPage/MetricListPage.tsx @@ -410,7 +410,8 @@ const MetricListPage = () => { }); }, [selectableMetricIds]); - const hierarchyExpansionScope = `${page}:${debouncedSearch}`; + // useMetricHierarchy clears expansion when page, size or query changes, so each must re-expand. + const hierarchyExpansionScope = `${page}:${metricPageSize}:${debouncedSearch}`; useEffect(() => { const hasExpandableGroup = topLevelNodes.some(({ groupId }) => groupId); From 8a64931c88c077740715bac0be25e7e3d8366bed Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 13 Oct 2026 09:46:06 +0000 Subject: [PATCH 04/27] chore(playwright): auto-refresh impact-map.generated.json --- .github/playwright/impact-map.generated.json | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/playwright/impact-map.generated.json b/.github/playwright/impact-map.generated.json index ae584fa889a2..a016a9cdd31c 100644 --- a/.github/playwright/impact-map.generated.json +++ b/.github/playwright/impact-map.generated.json @@ -6327,6 +6327,14 @@ "playwright/e2e/VersionPages/TestCaseVersionPage.spec.ts" ] }, + { + "sources": [ + "openmetadata-ui/src/main/resources/ui/src/components/DataQuality/AddDataQualityTest/components/TestCaseFormDrawer.tsx" + ], + "specs": [ + "playwright/e2e/Features/DataQuality/AddTestCaseNewFlow.spec.ts" + ] + }, { "sources": [ "openmetadata-ui/src/main/resources/ui/src/components/DataQuality/AddDataQualityTest/components/TestCaseSchedulerSection.tsx" @@ -7148,7 +7156,6 @@ ], "specs": [ "playwright/e2e/Features/ContextCenterArticles.spec.ts", - "playwright/e2e/Features/DataQuality/AddTestCaseNewFlow.spec.ts", "playwright/e2e/Features/Glossary/GlossaryRelationsGraph.spec.ts", "playwright/e2e/Features/Glossary/GlossaryTermRelationsGraph.spec.ts", "playwright/e2e/Features/MetricAssets.spec.ts", From 0be431b63baee299a7f3881debe27ef25e43157f Mon Sep 17 00:00:00 2001 From: Sweta Agarwalla Date: Tue, 13 Oct 2026 18:52:32 +0530 Subject: [PATCH 05/27] Page Settings Services cards at 12/24/48; keep stale sizes out of usePaging - Services: ListView's card/table view is now owned by Services, which pages cards at 12/24/48 and the table at 15/25/50 and returns to page 1 (clearing the cursor) when the view changes. - usePaging: a caller that declares no options is on the list scale, so a grid size left in the shared pageSize param or an off-scale size an older build stored in the preference never reaches a 15/25/50 picker. - usePaging: hand out the resolved size in the render where the options change. The stored size caught up a render late, so switching to cards fetched 25 under a picker reading 12. - Record the usePaging default in the page-size ADR. --- ...age-sizes-lists-15-25-50-grids-12-24-48.md | 3 ++ ...tRecentEventsTab.desync.real-hook.test.tsx | 23 ++++---- .../Settings/Services/Services.test.tsx | 47 +++++++++++++++- .../components/Settings/Services/Services.tsx | 24 ++++++++- .../common/ListView/ListView.component.tsx | 9 ++-- .../common/ListView/ListView.interface.ts | 2 + .../common/ListView/ListView.test.tsx | 32 ++++++++--- .../paging/usePaging.integration.test.tsx | 54 +++++++++++++++++-- .../ui/src/hooks/paging/usePaging.ts | 40 +++++++------- 9 files changed, 185 insertions(+), 49 deletions(-) diff --git a/docs/decisions/2026-10-13-ui-page-sizes-lists-15-25-50-grids-12-24-48.md b/docs/decisions/2026-10-13-ui-page-sizes-lists-15-25-50-grids-12-24-48.md index f2e75bb58f10..fb310f130930 100644 --- a/docs/decisions/2026-10-13-ui-page-sizes-lists-15-25-50-grids-12-24-48.md +++ b/docs/decisions/2026-10-13-ui-page-sizes-lists-15-25-50-grids-12-24-48.md @@ -25,6 +25,9 @@ matching entry. - A grid page passes `GRID_PAGE_SIZE_OPTIONS` to `usePaging` so a list-scale size from the shared URL or preference is reconciled to its own scale, and the global preference only ever stores a list-scale size. +- `usePaging` treats a caller that declares no options as on the list scale, so a grid size left + in the shared URL param, or an off-scale size an older build stored in the preference, never + reaches a 15/25/50 picker. - Shared defaults (core `PaginationCardWithControls`, `TableV2` client paging, the atoms listing hooks) are 15. - Out of scope: fetch limits that are not a paged view — typeahead/suggestion searches, tree-select diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Alerts/AlertDetails/AlertRecentEventsTab/AlertRecentEventsTab.desync.real-hook.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Alerts/AlertDetails/AlertRecentEventsTab/AlertRecentEventsTab.desync.real-hook.test.tsx index a56a31dfc36d..b8c04e8ff3a8 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Alerts/AlertDetails/AlertRecentEventsTab/AlertRecentEventsTab.desync.real-hook.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Alerts/AlertDetails/AlertRecentEventsTab/AlertRecentEventsTab.desync.real-hook.test.tsx @@ -24,9 +24,10 @@ import AlertRecentEventsTab from './AlertRecentEventsTab'; // Only useCurrentUserPreferences is stubbed: the real usePaging hook, its // useTableFilters URL-write path, and the real NextPreviousWithOffset // component are all exercised under MemoryRouter. +// The preference must be a size on the list scale (15/25/50): usePaging replaces any other value. jest.mock('../../../../hooks/currentUserStore/useCurrentUserStore', () => ({ useCurrentUserPreferences: () => ({ - preferences: { globalPageSize: 5 }, + preferences: { globalPageSize: 25 }, setPreference: jest.fn(), }), })); @@ -103,34 +104,34 @@ describe('AlertRecentEventsTab pagination/filter reset (real usePaging + real Ne it('resets currentPage to 1 on filter change and keeps Next navigation contiguous', async () => { await renderWithRouter(); - expect(indicator()).toHaveTextContent('1/20'); + expect(indicator()).toHaveTextContent('1/4'); await clickNext(); - expect(indicator()).toHaveTextContent('2/20'); + expect(indicator()).toHaveTextContent('2/4'); await clickNext(); - expect(indicator()).toHaveTextContent('3/20'); - expect(lastCallParams().paginationOffset).toBe(10); + expect(indicator()).toHaveTextContent('3/4'); + expect(lastCallParams().paginationOffset).toBe(50); await selectFilter('label.successful'); expect(await screen.findByTestId('applied-filter-text')).toHaveTextContent( ': label.successful' ); - expect(indicator()).toHaveTextContent('1/20'); + expect(indicator()).toHaveTextContent('1/4'); expect(lastCallParams().paginationOffset).toBe(0); expect(lastCallParams().status).toBe(AlertRecentEventFilters.SUCCESSFUL); await clickNext(); - expect(indicator()).toHaveTextContent('2/20'); - expect(lastCallParams().paginationOffset).toBe(5); + expect(indicator()).toHaveTextContent('2/4'); + expect(lastCallParams().paginationOffset).toBe(25); await clickPrevious(); - expect(indicator()).toHaveTextContent('1/20'); + expect(indicator()).toHaveTextContent('1/4'); expect(lastCallParams().paginationOffset).toBe(0); }); @@ -161,7 +162,7 @@ describe('AlertRecentEventsTab pagination/filter reset (real usePaging + real Ne // fetches offset 0 (the documented mount desync, out of scope here). await renderWithRouter('/alert?currentPage=3'); - expect(indicator()).toHaveTextContent('3/20'); + expect(indicator()).toHaveTextContent('3/4'); expect(lastCallParams().paginationOffset).toBe(0); // Changing the filter resets the page to 1 and rewrites the URL. @@ -170,7 +171,7 @@ describe('AlertRecentEventsTab pagination/filter reset (real usePaging + real Ne expect(await screen.findByTestId('applied-filter-text')).toHaveTextContent( ': label.successful' ); - expect(indicator()).toHaveTextContent('1/20'); + expect(indicator()).toHaveTextContent('1/4'); expect(lastCallParams().paginationOffset).toBe(0); expect(lastCallParams().status).toBe(AlertRecentEventFilters.SUCCESSFUL); expect(screen.getByTestId('location-search').textContent).toContain( diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Services.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Services.test.tsx index 6d9248fceddb..73b3459df1ce 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Services.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Services.test.tsx @@ -12,16 +12,23 @@ */ import { act, fireEvent, render, screen } from '@testing-library/react'; import React, { ReactNode } from 'react'; -import { DISABLED } from '../../../constants/constants'; +import { + DISABLED, + GRID_PAGE_SIZE_BASE, + GRID_PAGE_SIZE_OPTIONS, + LIST_PAGE_SIZE_OPTIONS, +} from '../../../constants/constants'; import { PAGE_HEADERS } from '../../../constants/PageHeaders.constant'; import { PIPELINE_SERVICE_PLATFORM } from '../../../constants/Services.constant'; import { useAirflowStatus } from '../../../context/AirflowStatusProvider/AirflowStatusProvider'; import { ServiceCategory } from '../../../enums/service.enum'; import { PipelineServiceType } from '../../../generated/entity/data/pipeline'; import LimitWrapper from '../../../hoc/LimitWrapper'; +import { usePaging } from '../../../hooks/paging/usePaging'; import { getServices, searchService } from '../../../rest/serviceAPI'; import { checkPermission } from '../../../utils/PermissionsUtils'; import { ListView } from '../../common/ListView/ListView.component'; +import { ListViewOptions } from '../../common/ListView/ListView.interface'; import { ColumnsType } from '../../common/Table/Table.interface'; import Services from './Services'; @@ -203,8 +210,19 @@ jest.mock('../../common/ListView/ListView.component', () => ({ tableProps, searchProps, handleDeletedSwitchChange, + onViewChange, }) => (
+ -
@@ -116,7 +116,7 @@ describe('PersonasLanding', () => { ); expect(getAllPersonas).toHaveBeenCalledWith({ - limit: 15, + limit: 12, fields: 'users', after: undefined, before: undefined, @@ -184,7 +184,7 @@ describe('PersonasLanding', () => { ); - expect(await screen.findByTestId('pagination')).toHaveTextContent('1/3'); + expect(await screen.findByTestId('pagination')).toHaveTextContent('1/4'); (getAllPersonas as jest.Mock).mockResolvedValue({ data: PERSONAS, @@ -192,7 +192,7 @@ describe('PersonasLanding', () => { }); fireEvent.click(screen.getByText('next')); - expect(mockSetPage).toHaveBeenCalledWith(2, 'after', 'cursor-2', 15); + expect(mockSetPage).toHaveBeenCalledWith(2, 'after', 'cursor-2', 12); await waitFor(() => expect(getAllPersonas).toHaveBeenLastCalledWith( @@ -205,11 +205,11 @@ describe('PersonasLanding', () => { fireEvent.click(screen.getByText('next')); - expect(mockSetPage).toHaveBeenLastCalledWith(3, 'after', 'cursor-3', 15); + expect(mockSetPage).toHaveBeenLastCalledWith(3, 'after', 'cursor-3', 12); fireEvent.click(screen.getByText('previous')); - expect(mockSetPage).toHaveBeenLastCalledWith(1, undefined, undefined, 15); + expect(mockSetPage).toHaveBeenLastCalledWith(1, undefined, undefined, 12); await waitFor(() => expect(getAllPersonas).toHaveBeenLastCalledWith( @@ -221,7 +221,7 @@ describe('PersonasLanding', () => { it('resumes from a cursor in the hash on mount', async () => { mockPagingParams({ page: 3, - pageSize: 25, + pageSize: 24, cursor: 'cursor-x', cursorType: 'before', }); @@ -230,7 +230,7 @@ describe('PersonasLanding', () => { await screen.findByTestId('persona-card-analyst'); expect(getAllPersonas).toHaveBeenCalledWith({ - limit: 25, + limit: 24, fields: 'users', after: undefined, before: 'cursor-x', @@ -243,7 +243,7 @@ describe('PersonasLanding', () => { await screen.findByTestId('persona-card-analyst'); - expect(mockSetPage).toHaveBeenCalledWith(1, undefined, undefined, 15); + expect(mockSetPage).toHaveBeenCalledWith(1, undefined, undefined, 12); expect(getAllPersonas).toHaveBeenCalledWith( expect.objectContaining({ after: undefined, before: undefined }) ); @@ -258,6 +258,6 @@ describe('PersonasLanding', () => { fireEvent.click(await screen.findByText('size')); - expect(mockSetPage).toHaveBeenCalledWith(1, undefined, undefined, 25); + expect(mockSetPage).toHaveBeenCalledWith(1, undefined, undefined, 24); }); }); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/Profile/tabs/personas/PersonasLanding.tsx b/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/Profile/tabs/personas/PersonasLanding.tsx index 4f3e1a652708..baf5689b3e60 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/Profile/tabs/personas/PersonasLanding.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/Profile/tabs/personas/PersonasLanding.tsx @@ -23,14 +23,11 @@ import { import { AxiosError } from 'axios'; import { useEffect, useMemo, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { - PAGE_SIZE_BASE, - PAGE_SIZE_LARGE, - PAGE_SIZE_MEDIUM, -} from '../../../../../../constants/constants'; +import { GRID_PAGE_SIZE_OPTIONS } from '../../../../../../constants/constants'; import { TabSpecificField } from '../../../../../../enums/entity.enum'; import { Persona } from '../../../../../../generated/entity/teams/persona'; import { Paging } from '../../../../../../generated/type/paging'; +import { useSavedPageSize } from '../../../../../../hooks/paging/useSavedPageSize'; import { useHashPagingParams } from '../../../../../../hooks/useSettingsHash'; import { getAllPersonas } from '../../../../../../rest/PersonaAPI'; import { getEntityName } from '../../../../../../utils/EntityNameUtils'; @@ -53,7 +50,14 @@ const PersonasLanding = ({ onNavigate }: PersonasLandingProps) => { cursorType: hashCursorType, setPage: setHashPage, } = useHashPagingParams(); - const pageSize = hashPageSize || PAGE_SIZE_BASE; + // Personas render as a card grid, so they page on the grid scale. + const { savedPageSize, savePageSize } = useSavedPageSize( + GRID_PAGE_SIZE_OPTIONS + ); + const pageSize = + hashPageSize && GRID_PAGE_SIZE_OPTIONS.includes(hashPageSize) + ? hashPageSize + : savedPageSize; const [personas, setPersonas] = useState([]); const [isLoading, setIsLoading] = useState(false); @@ -140,6 +144,7 @@ const PersonasLanding = ({ onNavigate }: PersonasLandingProps) => { }; const handlePageSizeChange = (size: number) => { + savePageSize(size); setHashPage(1, undefined, undefined, size); setCursorCache(new Map()); }; @@ -260,7 +265,7 @@ const PersonasLanding = ({ onNavigate }: PersonasLandingProps) => { (); + const view = selectedView ?? personaView; + // Table and cards page on their own scales, each starting from the size last picked on it. + const pageSizeOptions = getViewPageSizeOptions(view); + const { savedPageSize: pageSize, savePageSize } = + useSavedPageSize(pageSizeOptions); + const handleViewChange = useCallback((nextView: PageViewMode) => { + setSelectedView(nextView); + setCurrentPage(1); + }, []); const { resources, paging, isLoading, refetch } = useLearningResources({ searchText, @@ -141,10 +155,6 @@ const LearningResourcesSettings = ({ onFilterChange: setFilterState, }); - const personaView = usePersonaViewMode(VIEW_MODE_PAGE.LearningResources); - const [selectedView, setSelectedView] = useState(); - const view = selectedView ?? personaView; - const debouncedSearch = useMemo( () => debounce(setSearchText, SEARCH_DEBOUNCE_MS), [] @@ -279,11 +289,11 @@ const LearningResourcesSettings = ({ currentPage={currentPage} isLoading={isLoading} pageSize={pageSize} - pageSizeOptions={[PAGE_SIZE_BASE, PAGE_SIZE_MEDIUM, PAGE_SIZE_LARGE]} + pageSizeOptions={pageSizeOptions} paging={{ total: paging.total }} pagingHandler={({ currentPage: page }) => setCurrentPage(page)} onShowSizeChange={(size) => { - setPageSize(size); + savePageSize(size); setCurrentPage(1); }} /> @@ -310,7 +320,7 @@ const LearningResourcesSettings = ({ /> {quickFilters} - + {filterSelectionDisplay}
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/integration/ConnectionsPage/useConnectionsData.test.ts b/openmetadata-ui/src/main/resources/ui/src/components/integration/ConnectionsPage/useConnectionsData.test.ts index 700d0b7640ce..740a5b220206 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/integration/ConnectionsPage/useConnectionsData.test.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/integration/ConnectionsPage/useConnectionsData.test.ts @@ -50,7 +50,9 @@ jest.mock('../../../hooks/paging/usePaging', () => ({ .usePaging(defaultPageSize, pageSizeOptions); const { useCallback, useState } = jest.requireActual('react'); const [currentPage, setCurrentPage] = useState(1); - const [pageSize, setPageSize] = useState(defaultPageSize ?? 10); + const [pageSize, setPageSize] = useState( + defaultPageSize ?? pageSizeOptions?.[0] ?? 15 + ); const handlePageChange = useCallback( (page: number | ((page: number) => number)) => { setCurrentPage((current: number) => diff --git a/openmetadata-ui/src/main/resources/ui/src/components/integration/ConnectionsPage/useConnectionsData.ts b/openmetadata-ui/src/main/resources/ui/src/components/integration/ConnectionsPage/useConnectionsData.ts index 63405fd7031b..ef795992b6a5 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/integration/ConnectionsPage/useConnectionsData.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/integration/ConnectionsPage/useConnectionsData.ts @@ -180,7 +180,7 @@ export const useConnectionsData = ({ pageSize, handlePageChange, handlePageSizeChange, - } = usePaging(pageSizeOptions[0], pageSizeOptions); + } = usePaging(undefined, pageSizeOptions); const [sortOrder, setSortOrder] = useState('asc'); // Any change to what is being queried invalidates the current page number. usePaging restores diff --git a/openmetadata-ui/src/main/resources/ui/src/hooks/currentUserStore/useCurrentUserStore.ts b/openmetadata-ui/src/main/resources/ui/src/hooks/currentUserStore/useCurrentUserStore.ts index efea69202fed..b6096bb6601b 100644 --- a/openmetadata-ui/src/main/resources/ui/src/hooks/currentUserStore/useCurrentUserStore.ts +++ b/openmetadata-ui/src/main/resources/ui/src/hooks/currentUserStore/useCurrentUserStore.ts @@ -16,7 +16,7 @@ import { RecentlySearchedData, RecentlyViewedData } from 'Models'; import { useCallback, useMemo } from 'react'; import { create } from 'zustand'; import { createJSONStorage, persist } from 'zustand/middleware'; -import { PAGE_SIZE_BASE } from '../../constants/constants'; +import { GRID_PAGE_SIZE_BASE, PAGE_SIZE_BASE } from '../../constants/constants'; import { deleteUserPreference, putUserPreference } from '../../rest/userAPI'; import { showErrorToast } from '../../utils/ToastUtils'; import { useApplicationStore } from '../useApplicationStore'; @@ -30,6 +30,8 @@ export interface UserPreferences { isSidebarCollapsed: boolean; selectedEntityTableColumns: Record; globalPageSize: number; + /** The card-grid counterpart of `globalPageSize`, on the 12/24/48 scale. */ + globalGridPageSize: number; recentlyViewed: RecentlyViewedData[]; recentlySearched: RecentlySearchedData[]; recentlyViewedQuickLinks: RecentlyViewedData[]; @@ -64,6 +66,7 @@ const defaultPreferences: UserPreferences = { isSidebarCollapsed: false, selectedEntityTableColumns: {}, globalPageSize: PAGE_SIZE_BASE, + globalGridPageSize: GRID_PAGE_SIZE_BASE, recentlyViewed: [], recentlySearched: [], recentlyViewedQuickLinks: [], diff --git a/openmetadata-ui/src/main/resources/ui/src/hooks/paging/usePaging.integration.test.tsx b/openmetadata-ui/src/main/resources/ui/src/hooks/paging/usePaging.integration.test.tsx index 0632e5bcacfc..0905c0b220fa 100644 --- a/openmetadata-ui/src/main/resources/ui/src/hooks/paging/usePaging.integration.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/hooks/paging/usePaging.integration.test.tsx @@ -58,18 +58,33 @@ describe('usePaging (integration)', () => { expect(result.current.handlePageSizeChange).toBe(firstHandlePageSizeChange); }); - it('keeps a size off the app-wide scale out of the shared preference', () => { - const { result } = renderHook(() => usePaging(12, GRID_SIZES), { + it('saves a grid size to the grid preference, leaving the list one alone', () => { + const { result } = renderHook(() => usePaging(undefined, GRID_SIZES), { wrapper: wrapperAt('/connections'), }); act(() => result.current.handlePageSizeChange(24)); - // Persisting 24 would hand it to every page that reads the preference rather than declaring - // options — landing in a picker with no entry for it, and discarding the size chosen there. - expect( - usePersistentStorage.getState().preferences['paging-user'] - ).toBeUndefined(); + // Saving 24 as the list size would hand it to list pages, whose pickers have no 24. + const saved = usePersistentStorage.getState().preferences['paging-user']; + + expect(saved.globalGridPageSize).toBe(24); + expect(saved.globalPageSize).toBe(15); + }); + + it('remembers the size last picked on each scale', () => { + usePersistentStorage.getState().setUserPreference('paging-user', { + globalGridPageSize: 24, + globalPageSize: 25, + }); + + const grid = renderHook(() => usePaging(undefined, GRID_SIZES), { + wrapper, + }); + const list = renderHook(() => usePaging(), { wrapper }); + + expect(grid.result.current.pageSize).toBe(24); + expect(list.result.current.pageSize).toBe(25); }); it('starts from the size the caller asked for, whatever the preference holds', () => { diff --git a/openmetadata-ui/src/main/resources/ui/src/hooks/paging/usePaging.ts b/openmetadata-ui/src/main/resources/ui/src/hooks/paging/usePaging.ts index c2c2dcad9687..1d0f668c9577 100644 --- a/openmetadata-ui/src/main/resources/ui/src/hooks/paging/usePaging.ts +++ b/openmetadata-ui/src/main/resources/ui/src/hooks/paging/usePaging.ts @@ -21,13 +21,12 @@ import { import { INITIAL_PAGING_VALUE, LIST_PAGE_SIZE_OPTIONS, - PAGE_SIZE_BASE, pagingObject, } from '../../constants/constants'; import { CursorType } from '../../enums/pagination.enum'; import { Paging } from '../../generated/type/paging'; -import { useCurrentUserPreferences } from '../currentUserStore/useCurrentUserStore'; import { useTableFilters } from '../useTableFilters'; +import { useSavedPageSize } from './useSavedPageSize'; type FilterState = Record< string, @@ -62,15 +61,8 @@ export interface UsePagingInterface { } /** - * The sizes every page on the app-wide scale offers, and therefore the only ones `globalPageSize` - * may hold: it is read by pages that declare no options of their own, so a size from some page's - * bespoke scale would land in a picker with no matching entry. - */ -const GLOBAL_PAGE_SIZES = new Set(LIST_PAGE_SIZE_OPTIONS); - -/** - * @param defaultPageSize where to start when the URL carries no size; falls back to the app-wide - * `globalPageSize` preference. + * @param defaultPageSize where to start when the URL carries no size; falls back to the size the + * user last picked on this page's scale (`useSavedPageSize`). * @param pageSizeOptions the sizes this page's picker offers; defaults to the app-wide list scale. * A size from the shared URL param or preference is reconciled to this set, so a grid's 48 left * in the URL, or an off-scale size an older build stored in the preference, cannot reach a @@ -82,12 +74,9 @@ export const usePaging = ( defaultPageSize?: number, pageSizeOptions: number[] = LIST_PAGE_SIZE_OPTIONS ): UsePagingInterface => { - const { - preferences: { globalPageSize }, - setPreference, - } = useCurrentUserPreferences(); + const { savedPageSize, savePageSize } = useSavedPageSize(pageSizeOptions); - const fallbackPageSize = defaultPageSize ?? globalPageSize; + const fallbackPageSize = defaultPageSize ?? savedPageSize; const processedPageSize = !pageSizeOptions.includes(fallbackPageSize) ? pageSizeOptions[0] : fallbackPageSize; @@ -148,22 +137,10 @@ export const usePaging = ( ] ); - // A page on its own scale keeps its size to itself. Persisting a 24 here would hand it to every - // page that reads the preference instead of declaring options, where it has no matching entry — - // and would overwrite whatever size the user had chosen on those pages. - const persistGlobalPageSize = useCallback( - (size: number) => { - if (GLOBAL_PAGE_SIZES.has(size)) { - setPreference({ globalPageSize: size }); - } - }, - [setPreference] - ); - const handlePageSize = useCallback( (page: number) => { setPageSize(page); - persistGlobalPageSize(page); + savePageSize(page); setCurrentPage(INITIAL_PAGING_VALUE); // Update URL params, removing cursor data since they're invalid with new page size @@ -174,7 +151,7 @@ export const usePaging = ( cursorValue: null, }); }, - [setPageSize, persistGlobalPageSize, setCurrentPage, updateUrlParams] + [setPageSize, savePageSize, setCurrentPage, updateUrlParams] ); const paginationVisible = useMemo(() => { @@ -183,9 +160,16 @@ export const usePaging = ( return ( hasCursorPagination || paging.total > pageSize || - pageSize !== (defaultPageSize ?? PAGE_SIZE_BASE) + pageSize !== (defaultPageSize ?? pageSizeOptions[0]) ); - }, [defaultPageSize, paging.after, paging.before, paging.total, pageSize]); + }, [ + defaultPageSize, + pageSizeOptions, + paging.after, + paging.before, + paging.total, + pageSize, + ]); const handlePageChange = useCallback( ( @@ -208,12 +192,12 @@ export const usePaging = ( if (pageSize) { urlUpdate.pageSize = pageSize; - persistGlobalPageSize(pageSize); + savePageSize(pageSize); } updateUrlParams(urlUpdate as FilterState); }, - [currentPage, setCurrentPage, persistGlobalPageSize, updateUrlParams] + [currentPage, setCurrentPage, savePageSize, updateUrlParams] ); return { diff --git a/openmetadata-ui/src/main/resources/ui/src/hooks/paging/useSavedPageSize.ts b/openmetadata-ui/src/main/resources/ui/src/hooks/paging/useSavedPageSize.ts new file mode 100644 index 000000000000..8f0a9ecae299 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/hooks/paging/useSavedPageSize.ts @@ -0,0 +1,55 @@ +/* + * Copyright 2026 Collate. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { useCallback } from 'react'; +import { + GRID_PAGE_SIZE_OPTIONS, + LIST_PAGE_SIZE_OPTIONS, +} from '../../constants/constants'; +import { useCurrentUserPreferences } from '../currentUserStore/useCurrentUserStore'; + +/** + * The page size the user last picked on this picker's scale, and a setter that saves a new pick. + * Lists (15/25/50) and card grids (12/24/48) each keep their own preference, so a page that + * switches between the two views comes back to the size last chosen in each. + * ADR:2026-10-13-ui-page-sizes-lists-15-25-50-grids-12-24-48 + */ +export const useSavedPageSize = ( + pageSizeOptions: number[] = LIST_PAGE_SIZE_OPTIONS +) => { + const { + preferences: { globalPageSize, globalGridPageSize }, + setPreference, + } = useCurrentUserPreferences(); + + const saved = GRID_PAGE_SIZE_OPTIONS.includes(pageSizeOptions[0]) + ? globalGridPageSize + : globalPageSize; + // A preference from an older build can hold a size neither scale offers. + const savedPageSize = pageSizeOptions.includes(saved) + ? saved + : pageSizeOptions[0]; + + const savePageSize = useCallback( + (size: number) => { + if (GRID_PAGE_SIZE_OPTIONS.includes(size)) { + setPreference({ globalGridPageSize: size }); + } else if (LIST_PAGE_SIZE_OPTIONS.includes(size)) { + setPreference({ globalPageSize: size }); + } + }, + [setPreference] + ); + + return { savedPageSize, savePageSize }; +}; diff --git a/openmetadata-ui/src/main/resources/ui/src/hooks/useCursorPagedList.test.ts b/openmetadata-ui/src/main/resources/ui/src/hooks/useCursorPagedList.test.ts index feb7c2b6e696..5acc2baba85d 100644 --- a/openmetadata-ui/src/main/resources/ui/src/hooks/useCursorPagedList.test.ts +++ b/openmetadata-ui/src/main/resources/ui/src/hooks/useCursorPagedList.test.ts @@ -38,8 +38,10 @@ describe('useCursorPagedList', () => { expect(result.current.showPagination).toBe(true); }); - it('starts at the page size its caller passes', async () => { - const { result } = renderHook(() => useCursorPagedList(fetchPage, 12)); + it('starts at the first step of the scale its caller passes', async () => { + const { result } = renderHook(() => + useCursorPagedList(fetchPage, [12, 24, 48]) + ); await waitFor(() => expect(result.current.isLoading).toBe(false)); diff --git a/openmetadata-ui/src/main/resources/ui/src/hooks/useCursorPagedList.ts b/openmetadata-ui/src/main/resources/ui/src/hooks/useCursorPagedList.ts index 62aadb7e4427..cce7dffddae5 100644 --- a/openmetadata-ui/src/main/resources/ui/src/hooks/useCursorPagedList.ts +++ b/openmetadata-ui/src/main/resources/ui/src/hooks/useCursorPagedList.ts @@ -13,9 +13,10 @@ import { AxiosError } from 'axios'; import { useCallback, useEffect, useRef, useState } from 'react'; -import { PAGE_SIZE_BASE } from '../constants/constants'; +import { LIST_PAGE_SIZE_OPTIONS } from '../constants/constants'; import { Paging } from '../generated/type/paging'; import { showErrorToast } from '../utils/ToastUtils'; +import { useSavedPageSize } from './paging/useSavedPageSize'; export interface CursorPageParams { after?: string; @@ -27,20 +28,21 @@ export interface CursorPageParams { * Page-number pagination over a cursor-paged list endpoint. Moving forward * follows `after`, moving back follows `before`, so (like the other settings * lists) only adjacent pages are reachable by cursor. A new `fetchPage` - * identity — e.g. a changed filter — restarts from page 1. `initialPageSize` - * is the first step of the caller's page-size scale. + * identity — e.g. a changed filter — restarts from page 1. It starts at, and + * saves, the size last picked on the `pageSizeOptions` scale. */ export const useCursorPagedList = ( fetchPage: (params: CursorPageParams) => Promise<{ data: T[]; paging: Paging; }>, - initialPageSize = PAGE_SIZE_BASE + pageSizeOptions: number[] = LIST_PAGE_SIZE_OPTIONS ) => { + const { savedPageSize, savePageSize } = useSavedPageSize(pageSizeOptions); const [items, setItems] = useState([]); const [paging, setPaging] = useState({ total: 0 }); const [page, setPage] = useState(1); - const [pageSize, setPageSize] = useState(initialPageSize); + const [pageSize, setPageSize] = useState(savedPageSize); const [isLoading, setIsLoading] = useState(true); // Only the latest request may write state: a slow earlier response (e.g. the // list before a filter flipped) must not overwrite the newer one. @@ -74,6 +76,14 @@ export const useCursorPagedList = ( void load(); }, [load]); + const onPageSizeChange = useCallback( + (size: number) => { + setPageSize(size); + savePageSize(size); + }, + [savePageSize] + ); + const onPageChange = (nextPage: number) => { const cursorType = nextPage > page ? 'after' : 'before'; setPage(nextPage); @@ -87,8 +97,9 @@ export const useCursorPagedList = ( pageSize, totalPages: Math.ceil((paging.total ?? 0) / pageSize), showPagination: - Boolean(paging.after || paging.before) || paging.total > initialPageSize, + Boolean(paging.after || paging.before) || + paging.total > pageSizeOptions[0], onPageChange, - onPageSizeChange: setPageSize, + onPageSizeChange, }; }; diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/Application/ApplicationPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/Application/ApplicationPage.tsx index c5884ee0d366..539590752eb3 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/Application/ApplicationPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/Application/ApplicationPage.tsx @@ -32,11 +32,7 @@ import { TitleBreadcrumbProps } from '../../components/common/TitleBreadcrumb/Ti import PageHeader from '../../components/PageHeader/PageHeader.component'; import PageLayoutV1 from '../../components/PageLayoutV1/PageLayoutV1'; import ApplicationCard from '../../components/Settings/Applications/ApplicationCard/ApplicationCard.component'; -import { - GRID_PAGE_SIZE_BASE, - GRID_PAGE_SIZE_OPTIONS, - ROUTES, -} from '../../constants/constants'; +import { GRID_PAGE_SIZE_OPTIONS, ROUTES } from '../../constants/constants'; import { GlobalSettingsMenuCategory } from '../../constants/GlobalSettings.constants'; import { LEARNING_PAGE_IDS } from '../../constants/Learning.constants'; import { PAGE_HEADERS } from '../../constants/PageHeaders.constant'; @@ -66,7 +62,7 @@ const ApplicationPage = () => { handlePageSizeChange, showPagination, pagingCursor, - } = usePaging(GRID_PAGE_SIZE_BASE, GRID_PAGE_SIZE_OPTIONS); + } = usePaging(undefined, GRID_PAGE_SIZE_OPTIONS); const navigate = useNavigate(); const [isLoading, setIsLoading] = useState(true); const [applicationData, setApplicationData] = useState(); diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/LearningResourcesPage/LearningResourcesPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/LearningResourcesPage/LearningResourcesPage.tsx index 923f388e30ea..3263973a6135 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/LearningResourcesPage/LearningResourcesPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/LearningResourcesPage/LearningResourcesPage.tsx @@ -49,12 +49,10 @@ import { } from '../../constants/Learning.constants'; import { VIEW_MODE_PAGE } from '../../constants/platform/personaAppLayout.constants'; import { PageViewMode } from '../../generated/type/personaPreferences'; +import { useSavedPageSize } from '../../hooks/paging/useSavedPageSize'; import { usePersonaViewMode } from '../../hooks/platform/usePersonaViewMode'; import { LearningResource } from '../../rest/learningResourceAPI'; -import { - getViewPageSizeOptions, - resolveViewPageSize, -} from '../../utils/PaginationUtils'; +import { getViewPageSizeOptions } from '../../utils/PaginationUtils'; import { getSettingPath } from '../../utils/RouterUtils'; import { useLearningResourceActions } from './hooks/useLearningResourceActions'; import { @@ -84,13 +82,13 @@ export const LearningResourcesPage: React.FC = () => { {} ); const [currentPage, setCurrentPage] = useState(1); - const [selectedPageSize, setSelectedPageSize] = useState(); const personaView = usePersonaViewMode(VIEW_MODE_PAGE.LearningResources); const [selectedView, setView] = useState(); const view = selectedView ?? personaView; const pageSizeOptions = getViewPageSizeOptions(view); - const pageSize = resolveViewPageSize(view, selectedPageSize); + const { savedPageSize: pageSize, savePageSize } = + useSavedPageSize(pageSizeOptions); const { resources, paging, isLoading, refetch } = useLearningResources({ searchText, @@ -139,10 +137,13 @@ export const LearningResourcesPage: React.FC = () => { setCurrentPage(1); }, [searchText, filterState]); - const handlePageSizeChange = useCallback((size: number) => { - setSelectedPageSize(size); - setCurrentPage(1); - }, []); + const handlePageSizeChange = useCallback( + (size: number) => { + savePageSize(size); + setCurrentPage(1); + }, + [savePageSize] + ); const handleViewChange = useCallback((nextView: PageViewMode) => { setView(nextView); diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/MarketPlacePage/MarketPlacePage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/MarketPlacePage/MarketPlacePage.tsx index 7c6bf676e7db..78c5f00f2073 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/MarketPlacePage/MarketPlacePage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/MarketPlacePage/MarketPlacePage.tsx @@ -26,10 +26,7 @@ import TitleBreadcrumb from '../../components/common/TitleBreadcrumb/TitleBreadc import PageHeader from '../../components/PageHeader/PageHeader.component'; import PageLayoutV1 from '../../components/PageLayoutV1/PageLayoutV1'; import ApplicationCard from '../../components/Settings/Applications/ApplicationCard/ApplicationCard.component'; -import { - GRID_PAGE_SIZE_BASE, - GRID_PAGE_SIZE_OPTIONS, -} from '../../constants/constants'; +import { GRID_PAGE_SIZE_OPTIONS } from '../../constants/constants'; import { GlobalSettingOptions } from '../../constants/GlobalSettings.constants'; import { LEARNING_PAGE_IDS } from '../../constants/Learning.constants'; import { PAGE_HEADERS } from '../../constants/PageHeaders.constant'; @@ -58,7 +55,7 @@ const MarketPlacePage = () => { handlePageSizeChange, showPagination, pagingCursor, - } = usePaging(GRID_PAGE_SIZE_BASE, GRID_PAGE_SIZE_OPTIONS); + } = usePaging(undefined, GRID_PAGE_SIZE_OPTIONS); const navigate = useNavigate(); const [isLoading, setIsLoading] = useState(true); const [applicationData, setApplicationData] = diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/Persona/PersonaListPage/PersonaPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/Persona/PersonaListPage/PersonaPage.tsx index dc9389e7bf79..3f63c4f51db6 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/Persona/PersonaListPage/PersonaPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/Persona/PersonaListPage/PersonaPage.tsx @@ -28,10 +28,7 @@ import { AddEditPersonaForm } from '../../../components/MyData/Persona/AddEditPe import { PersonaDetailsCard } from '../../../components/MyData/Persona/PersonaDetailsCard/PersonaDetailsCard'; import PageHeader from '../../../components/PageHeader/PageHeader.component'; import PageLayoutV1 from '../../../components/PageLayoutV1/PageLayoutV1'; -import { - GRID_PAGE_SIZE_BASE, - GRID_PAGE_SIZE_OPTIONS, -} from '../../../constants/constants'; +import { GRID_PAGE_SIZE_OPTIONS } from '../../../constants/constants'; import { GlobalSettingsMenuCategory } from '../../../constants/GlobalSettings.constants'; import { PAGE_HEADERS } from '../../../constants/PageHeaders.constant'; import { ERROR_PLACEHOLDER_TYPE } from '../../../enums/common.enum'; @@ -62,7 +59,7 @@ export const PersonaPage = ({ pageTitle }: { pageTitle: string }) => { paging, pagingCursor, showPagination, - } = usePaging(GRID_PAGE_SIZE_BASE, GRID_PAGE_SIZE_OPTIONS); + } = usePaging(undefined, GRID_PAGE_SIZE_OPTIONS); const breadcrumbs: TitleBreadcrumbProps['titleLinks'] = useMemo( () => getSettingPageEntityBreadCrumb(GlobalSettingsMenuCategory.PERSONA), diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/WorkflowDefinitions/WorkflowsPage/WorkflowsPage.test.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/WorkflowDefinitions/WorkflowsPage/WorkflowsPage.test.tsx index eb6f57e6be71..8a5dfa36cc98 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/WorkflowDefinitions/WorkflowsPage/WorkflowsPage.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/WorkflowDefinitions/WorkflowsPage/WorkflowsPage.test.tsx @@ -14,6 +14,8 @@ import { render, screen } from '@testing-library/react'; import React from 'react'; import { MemoryRouter } from 'react-router-dom'; +import NextPrevious from '../../../components/common/NextPrevious/NextPrevious'; +import { GRID_PAGE_SIZE_OPTIONS } from '../../../constants/constants'; import { getWorkflowDefinitions } from '../../../rest/workflowDefinitionsAPI'; import WorkflowsPage from './WorkflowsPage'; @@ -89,4 +91,31 @@ describe('WorkflowsPage on OSS', () => { screen.queryByTestId('create-workflow-button') ).not.toBeInTheDocument(); }); + + it('pages the workflow cards on the grid scale', async () => { + mockGetWorkflowDefinitions.mockResolvedValue({ + data: [ + { + id: 'w1', + name: 'pw-grid-workflow', + fullyQualifiedName: 'pw-grid-workflow', + }, + ], + paging: { total: 1 }, + }); + + renderPage(); + + expect(await screen.findByTestId('pw-grid-workflow')).toBeInTheDocument(); + expect(mockGetWorkflowDefinitions).toHaveBeenCalledWith( + expect.objectContaining({ limit: 12 }) + ); + expect(NextPrevious).toHaveBeenLastCalledWith( + expect.objectContaining({ + pageSize: 12, + pageSizeOptions: GRID_PAGE_SIZE_OPTIONS, + }), + expect.anything() + ); + }); }); diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/WorkflowDefinitions/WorkflowsPage/WorkflowsPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/WorkflowDefinitions/WorkflowsPage/WorkflowsPage.tsx index 0696b88cc2d1..24c455670ac6 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/WorkflowDefinitions/WorkflowsPage/WorkflowsPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/WorkflowDefinitions/WorkflowsPage/WorkflowsPage.tsx @@ -43,15 +43,14 @@ import PageHeader from '../../../components/PageHeader/PageHeader.component'; import PageLayoutV1 from '../../../components/PageLayoutV1/PageLayoutV1'; import WorkflowCard from '../../../components/WorkflowDefinitions/WorkflowCard/WorkflowCard.component'; import { + GRID_PAGE_SIZE_OPTIONS, INITIAL_PAGING_VALUE, - PAGE_SIZE_BASE, - PAGE_SIZE_LARGE, - PAGE_SIZE_MEDIUM, } from '../../../constants/constants'; import { LEARNING_PAGE_IDS } from '../../../constants/Learning.constants'; import { CursorType } from '../../../enums/pagination.enum'; import { WorkflowDefinition } from '../../../generated/governance/workflows/workflowDefinition'; import { Paging } from '../../../generated/type/paging'; +import { useSavedPageSize } from '../../../hooks/paging/useSavedPageSize'; import { useIsAiMode } from '../../../hooks/useAppMode'; import { createWorkflowDefinition, @@ -80,7 +79,10 @@ const WorkflowsPage = () => { total: 0, }); const [currentPage, setCurrentPage] = useState(INITIAL_PAGING_VALUE); - const [pageSize, setPageSize] = useState(PAGE_SIZE_MEDIUM); + // Workflows render as a card grid, so they page on the grid scale. + const { savedPageSize: pageSize, savePageSize } = useSavedPageSize( + GRID_PAGE_SIZE_OPTIONS + ); const [isModalOpen, setIsModalOpen] = useState(false); const [workflowName, setWorkflowName] = useState(''); const [description, setDescription] = useState(''); @@ -380,14 +382,10 @@ const WorkflowsPage = () => { currentPage={currentPage} isLoading={loading} pageSize={pageSize} - pageSizeOptions={[ - PAGE_SIZE_BASE, - PAGE_SIZE_MEDIUM, - PAGE_SIZE_LARGE, - ]} + pageSizeOptions={GRID_PAGE_SIZE_OPTIONS} paging={paging} pagingHandler={handlePageChange} - onShowSizeChange={setPageSize} + onShowSizeChange={savePageSize} /> diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/PaginationUtils.test.ts b/openmetadata-ui/src/main/resources/ui/src/utils/PaginationUtils.test.ts index e21a9b6710ed..684618362b53 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/PaginationUtils.test.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/PaginationUtils.test.ts @@ -7,7 +7,6 @@ import { computeTotalPages, getViewPageSize, getViewPageSizeOptions, - resolveViewPageSize, } from './PaginationUtils'; /* @@ -77,15 +76,4 @@ describe('view page sizes', () => { expect(getViewPageSize(PageViewMode.Card)).toBe(12); expect(getViewPageSize(PageViewMode.Table)).toBe(15); }); - - it('keeps a picked size that the view offers', () => { - expect(resolveViewPageSize(PageViewMode.Table, 50)).toBe(50); - expect(resolveViewPageSize(PageViewMode.Card, 24)).toBe(24); - }); - - it('falls back to the first step when the picked size belongs to the other scale', () => { - expect(resolveViewPageSize(PageViewMode.Card, 25)).toBe(12); - expect(resolveViewPageSize(PageViewMode.Table, 24)).toBe(15); - expect(resolveViewPageSize(PageViewMode.Table)).toBe(15); - }); }); diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/PaginationUtils.ts b/openmetadata-ui/src/main/resources/ui/src/utils/PaginationUtils.ts index b6e07ed76c44..bcb20eb9c7f3 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/PaginationUtils.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/PaginationUtils.ts @@ -28,12 +28,3 @@ export const getViewPageSizeOptions = (view?: ViewMode) => export const getViewPageSize = (view?: ViewMode) => getViewPageSizeOptions(view)[0]; - -// The two scales share no size, so a size picked in one view falls back to the other's first step. -export const resolveViewPageSize = (view?: ViewMode, selectedSize?: number) => { - const options = getViewPageSizeOptions(view); - - return selectedSize && options.includes(selectedSize) - ? selectedSize - : options[0]; -}; From 6ad0b4c1fcbbb76ecd74771c1d74e8c8d2c04d73 Mon Sep 17 00:00:00 2001 From: Sweta Agarwalla Date: Thu, 15 Oct 2026 12:26:56 +0530 Subject: [PATCH 08/27] Hide the pager when a list has no data Settings Services and App Live Indexing show their pager whenever there is data, even a single page, and hide it when there is none, so an empty list shows only its empty state. --- .../AppLiveIndexing.component.tsx | 25 ++++++++++++------- .../Settings/Services/Services.test.tsx | 24 +++++++++++++++++- .../components/Settings/Services/Services.tsx | 5 ++-- 3 files changed, 42 insertions(+), 12 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Applications/AppLiveIndexing/AppLiveIndexing.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Applications/AppLiveIndexing/AppLiveIndexing.component.tsx index e9582746cb60..5a1e42a9fdbc 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Applications/AppLiveIndexing/AppLiveIndexing.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Applications/AppLiveIndexing/AppLiveIndexing.component.tsx @@ -18,6 +18,7 @@ import { } from '@openmetadata/ui-core-components'; import { CheckCircle } from '@openmetadata/ui-core-components/icons'; import { AxiosError } from 'axios'; +import { isEmpty } from 'lodash'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { @@ -184,15 +185,21 @@ const AppLiveIndexing = ({ appData }: AppLiveIndexingProps) => { ), }} - pagination={{ - total: paging.total, - pageSize, - current: currentPage, - showSizeChanger: true, - pageSizeOptions: LIST_PAGE_SIZE_OPTIONS, - onChange: (page) => handlePageChangeHandler({ currentPage: page }), - onShowSizeChange: (_, size) => handlePageSizeChange(size), - }} + // Shown whenever there are records, even a single page of them; hidden when there are none. + pagination={ + isEmpty(records) + ? false + : { + total: paging.total, + pageSize, + current: currentPage, + showSizeChanger: true, + pageSizeOptions: LIST_PAGE_SIZE_OPTIONS, + onChange: (page) => + handlePageChangeHandler({ currentPage: page }), + onShowSizeChange: (_, size) => handlePageSizeChange(size), + } + } rowKey={(record) => `${record.entityId}-${record.entityFqn}`} size="small" /> diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Services.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Services.test.tsx index b54cb85c2ee5..5c2bdaa262b1 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Services.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Services.test.tsx @@ -361,7 +361,10 @@ describe('Services', () => { ); }); - it('shows the pager even when every service fits on one page', async () => { + it('shows the pager when there are services, even a single page of them', async () => { + (getServices as jest.Mock).mockImplementationOnce(() => + Promise.resolve({ data: mockGetServicesData, paging: { total: 1 } }) + ); const pagingState = (usePaging as jest.Mock)(); (usePaging as jest.Mock).mockReturnValue({ ...pagingState, @@ -384,6 +387,25 @@ describe('Services', () => { (usePaging as jest.Mock).mockReturnValue(pagingState); }); + it('hides the pager when there are no services', async () => { + (getServices as jest.Mock).mockImplementationOnce(() => + Promise.resolve({ data: [], paging: { total: 0 } }) + ); + + await act(async () => { + render(); + }); + + expect(ListView).toHaveBeenLastCalledWith( + expect.objectContaining({ + customPaginationProps: expect.objectContaining({ + showPagination: false, + }), + }), + expect.anything() + ); + }); + it('should render Services', async () => { await act(async () => { render(); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Services.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Services.tsx index 07e21c9626c4..b4549614e203 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Services.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Services.tsx @@ -378,10 +378,10 @@ const Services = ({ serviceName }: ServicesProps) => { })); }, [serviceName]); - // The pager stays visible even when every service fits on one page, so the page size can be changed. + // Shown whenever there are services, even a single page of them, so the page size can be changed. const customPaginationTableProps = useMemo( () => ({ - showPagination: true, + showPagination: !isEmpty(serviceDetails), currentPage, isLoading, isNumberBased: !isEmpty(searchTerm) || !isEmpty(serviceTypeFilter), @@ -392,6 +392,7 @@ const Services = ({ serviceName }: ServicesProps) => { onShowSizeChange: handlePageSizeChange, }), [ + serviceDetails, currentPage, isLoading, searchTerm, From 163c81ac6d8c36cda53cb10f7b6a93c201494f96 Mon Sep 17 00:00:00 2001 From: Sweta Agarwalla Date: Thu, 15 Oct 2026 14:26:58 +0530 Subject: [PATCH 09/27] Start every paged view from the saved page size No page sets a fixed starting size any more; each starts from the size last picked on its scale (useSavedPageSize), and a size in the URL or settings hash still wins. - Local-state pickers now read and save it: Audit Logs, profile Audit Logs, Roles, Policies, Notification Alerts and Recent Events, Bots, Governance glossaries, Search preview, Glossary Term Relations, Column Bulk Operations, Incident Groups (useIncidentPaging). - usePaging callers drop their fixed defaults: schema, column profiler, data model, glossary terms and lineage tables (50), table queries, data product ports, test definitions, Context Center archive, AI alert events (15), data contract schema (25). - Views with no picker follow it too: Domains, Data Products, Sub-domains and Metrics (per view scale), KPIs, Reindex failures, Context Center memories. - Remove getViewPageSize, INCIDENT_GROUPS_PAGE_SIZE, ARCHIVE_PAGE_SIZE and MEMORIES_PER_PAGE, now unused; record the rule in the ADR. --- ...age-sizes-lists-15-25-50-grids-12-24-48.md | 5 ++++ .../ModelTab/ModelTab.component.tsx | 3 +-- .../ContractScehmaFormTab.tsx | 7 ++--- .../DataProduct/DataProductListPage.tsx | 8 ++++-- .../PortsListView/PortsListView.component.tsx | 3 +-- .../ColumnProfileTable/ColumnProfileTable.tsx | 3 +-- .../SchemaTable/SchemaTable.component.tsx | 3 +-- .../Database/SchemaTable/SchemaTable.test.tsx | 4 +-- .../Database/TableQueries/TableQueries.tsx | 7 ++--- .../SubDomainsTable.component.tsx | 8 ++++-- .../DomainListing/DomainListPage.tsx | 8 ++++-- .../GlossaryTermTab.component.tsx | 2 +- .../LineageTable/LineageTable.test.tsx | 3 ++- .../components/LineageTable/LineageTable.tsx | 2 +- .../ReindexFailures.component.tsx | 11 ++++---- .../useTestDefinitionListPage.ts | 7 ++--- .../AccessControlAuditLogsPanel.tsx | 12 ++++++--- .../AccessControlPoliciesPanel.tsx | 5 +++- .../AccessControlRolesPanel.tsx | 5 +++- .../Profile/tabs/bots/BotsListPanel.tsx | 7 +++-- .../governance/GovernanceGlossaryList.tsx | 5 +++- .../NotificationAlertsPanel.test.tsx | 1 + .../notification/NotificationAlertsPanel.tsx | 5 +++- .../NotificationRecentEvents.test.tsx | 1 + .../notification/NotificationRecentEvents.tsx | 7 +++-- .../search/SearchPreviewPanel.tsx | 6 +++-- .../Alerts/AlertAiRecentEventsTab.tsx | 2 +- .../IncidentGroups/IncidentGroupDetail.tsx | 2 -- .../IncidentGroups/IncidentGroupDrawer.tsx | 6 +---- .../IncidentGroups.constants.ts | 2 -- .../useIncidentGroupIncidents.ts | 3 ++- .../IncidentGroups/useIncidentGroups.ts | 4 +-- .../IncidentGroups/useIncidentPaging.ts | 18 ++++++++++--- .../src/constants/ContextCenter.constants.ts | 4 --- .../src/pages/AuditLogsPage/AuditLogsPage.tsx | 16 +++++++---- .../hooks/useColumnGridListingData.tsx | 27 ++++++++++--------- .../ContextCenterArchivePage.tsx | 3 +-- .../ContextCenterMemoriesPage.tsx | 10 ++++--- .../ui/src/pages/DataInsightPage/KPIList.tsx | 14 +++++----- .../GlossaryTermRelationSettings.tsx | 5 +++- .../MetricListPage/MetricListPage.tsx | 7 +++-- .../ui/src/utils/PaginationUtils.test.ts | 11 +------- .../resources/ui/src/utils/PaginationUtils.ts | 3 --- 43 files changed, 154 insertions(+), 121 deletions(-) diff --git a/docs/decisions/2026-10-13-ui-page-sizes-lists-15-25-50-grids-12-24-48.md b/docs/decisions/2026-10-13-ui-page-sizes-lists-15-25-50-grids-12-24-48.md index d790433026a9..c2c422fb7788 100644 --- a/docs/decisions/2026-10-13-ui-page-sizes-lists-15-25-50-grids-12-24-48.md +++ b/docs/decisions/2026-10-13-ui-page-sizes-lists-15-25-50-grids-12-24-48.md @@ -27,6 +27,11 @@ matching entry. - The size the user picks is remembered per scale (`useSavedPageSize`): list sizes in `globalPageSize`, grid sizes in `globalGridPageSize`. A page that switches views comes back to the size last picked in each, and a grid size never lands in a list picker. +- Every paged view starts from that saved size; no page sets a fixed starting size of its own. + This includes views with no size picker (Metrics, KPIs, Domains, Data Products, reindex + failures, Context Center memories). A size carried in the URL (`pageSize` param or settings + hash) still wins, so a shared link opens at its own size. Fixed fetch batches that are not a + paged view (e.g. loading all glossary terms, approval tasks) are out of scope. - `usePaging` treats a caller that declares no options as on the list scale, so a grid size left in the shared URL param, or an off-scale size an older build stored in the preference, never reaches a 15/25/50 picker. diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DataModel/DataModels/ModelTab/ModelTab.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DataModel/DataModels/ModelTab/ModelTab.component.tsx index ef5ddaf71900..b189269b63b7 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DataModel/DataModels/ModelTab/ModelTab.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DataModel/DataModels/ModelTab/ModelTab.component.tsx @@ -15,7 +15,6 @@ import { groupBy, isEmpty, omit, uniqBy } from 'lodash'; import { EntityTags, TagFilterOptions } from 'Models'; import { lazy, useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { PAGE_SIZE_LARGE } from '../../../../../constants/constants'; import { COMMON_STATIC_TABLE_VISIBLE_COLUMNS, DEFAULT_DASHBOARD_DATA_MODEL_VISIBLE_COLUMNS, @@ -91,7 +90,7 @@ const ModelTab = () => { showPagination, paging, handlePagingChange, - } = usePaging(PAGE_SIZE_LARGE); + } = usePaging(); const { data: dataModel, permissions } = useGenericContext(); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataContract/ContractSchemaFormTab/ContractScehmaFormTab.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataContract/ContractSchemaFormTab/ContractScehmaFormTab.tsx index c9dd736e0b61..9ac1510d32bc 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataContract/ContractSchemaFormTab/ContractScehmaFormTab.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataContract/ContractSchemaFormTab/ContractScehmaFormTab.tsx @@ -15,10 +15,7 @@ import { isEmpty, pick, uniqBy } from 'lodash'; import { Key, useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { FQN_SEPARATOR_CHAR } from '../../../constants/char.constants'; -import { - NO_DATA_PLACEHOLDER, - PAGE_SIZE_MEDIUM, -} from '../../../constants/constants'; +import { NO_DATA_PLACEHOLDER } from '../../../constants/constants'; import { TABLE_COLUMNS_KEYS } from '../../../constants/TableKeys.constants'; import { EntityType, @@ -97,7 +94,7 @@ export const ContractSchemaFormTab: React.FC<{ handlePageSizeChange, handlePagingChange, showPagination, - } = usePaging(PAGE_SIZE_MEDIUM); + } = usePaging(); const handleChangeTable = useCallback( (selectedRowKeys: Key[]) => { setSelectedKeys(selectedRowKeys as string[]); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataProduct/DataProductListPage.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataProduct/DataProductListPage.tsx index 96c7e939e88b..3c3c93a73555 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataProduct/DataProductListPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataProduct/DataProductListPage.tsx @@ -43,13 +43,14 @@ import { VIEW_MODE_PAGE } from '../../constants/platform/personaAppLayout.consta import { usePermissionProvider } from '../../context/PermissionProvider/PermissionProvider'; import { DataProduct } from '../../generated/entity/domains/dataProduct'; import { PageViewMode } from '../../generated/type/personaPreferences'; +import { useSavedPageSize } from '../../hooks/paging/useSavedPageSize'; import { usePersonaViewMode } from '../../hooks/platform/usePersonaViewMode'; import { useIsAiMode } from '../../hooks/useAppMode'; import { useMarketplaceStore } from '../../hooks/useMarketplaceStore'; import dataProductClassBase from '../../utils/DataProduct/DataProductClassBase'; import { getEntityName } from '../../utils/EntityNameUtils'; import { getEntityAvatarProps } from '../../utils/IconUtils'; -import { getViewPageSize } from '../../utils/PaginationUtils'; +import { getViewPageSizeOptions } from '../../utils/PaginationUtils'; import { renderBreakableTooltip } from '../../utils/TooltipUtils'; import { useDelete } from '../common/atoms/actions/useDelete'; import { @@ -157,9 +158,12 @@ const DataProductListPage = ({ const personaView = usePersonaViewMode(VIEW_MODE_PAGE.DataProducts); const [selectedView, setView] = useState(); const view = selectedView ?? personaView; + const { savedPageSize: listingPageSize } = useSavedPageSize( + getViewPageSizeOptions(view) + ); const dataProductListing = useDataProductListingData({ enableNlq: isAiMode, - pageSize: getViewPageSize(view), + pageSize: listingPageSize, }); // Card and table views page at different sizes, so the current page may not exist in the other. const { handlePageChange } = dataProductListing; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataProducts/InputOutputPortsTab/PortsListView/PortsListView.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataProducts/InputOutputPortsTab/PortsListView/PortsListView.component.tsx index 1b1e2ccd06f5..e6d6e0ecbb8c 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataProducts/InputOutputPortsTab/PortsListView/PortsListView.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataProducts/InputOutputPortsTab/PortsListView/PortsListView.component.tsx @@ -25,7 +25,6 @@ import { } from 'react'; import { useTranslation } from 'react-i18next'; import { ReactComponent as DeleteIcon } from '../../../../assets/svg/ic-delete.svg'; -import { PAGE_SIZE_BASE } from '../../../../constants/constants'; import { EntityReference } from '../../../../generated/type/entityReference'; import { usePaging } from '../../../../hooks/paging/usePaging'; import { @@ -74,7 +73,7 @@ const PortsListView = forwardRef( handlePageSizeChange, handlePagingChange, showPagination, - } = usePaging(PAGE_SIZE_BASE); + } = usePaging(); const fetchPorts = useCallback( async (page = 1) => { diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Database/Profiler/TableProfiler/ColumnProfileTable/ColumnProfileTable.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Database/Profiler/TableProfiler/ColumnProfileTable/ColumnProfileTable.tsx index 60a3a4b09c36..68245ea2542c 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Database/Profiler/TableProfiler/ColumnProfileTable/ColumnProfileTable.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Database/Profiler/TableProfiler/ColumnProfileTable/ColumnProfileTable.tsx @@ -22,7 +22,6 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; import type { SortDescriptor } from 'react-aria-components'; import { useTranslation } from 'react-i18next'; import { Link, useParams } from 'react-router-dom'; -import { PAGE_SIZE_LARGE } from '../../../../../constants/constants'; import { ERROR_PLACEHOLDER_TYPE } from '../../../../../enums/common.enum'; import { EntityTabs, @@ -101,7 +100,7 @@ const ColumnProfileTable = () => { handlePageChange, handlePageSizeChange, handlePagingChange, - } = usePaging(PAGE_SIZE_LARGE); + } = usePaging(); const tableDetailsWithColumns = useMemo( () => ({ ...tableProfiler, columns: data as Column[] } as TableType), diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Database/SchemaTable/SchemaTable.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Database/SchemaTable/SchemaTable.component.tsx index c0fde233b131..1e58cf8c71a2 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Database/SchemaTable/SchemaTable.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Database/SchemaTable/SchemaTable.component.tsx @@ -34,7 +34,6 @@ import { ReactComponent as IconSort } from '../../../assets/svg/ic-sort-both.svg import { INITIAL_PAGING_VALUE, NO_DATA_PLACEHOLDER, - PAGE_SIZE_LARGE, } from '../../../constants/constants'; import { COLUMN_CONSTRAINT_TYPE_OPTIONS, @@ -155,7 +154,7 @@ const SchemaTable = () => { showPagination, paging, handlePagingChange, - } = usePaging(PAGE_SIZE_LARGE); + } = usePaging(); const { filters, setFilters } = useTableFilters({ columnSearch: undefined as string | undefined, diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Database/SchemaTable/SchemaTable.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Database/SchemaTable/SchemaTable.test.tsx index a253334fd58e..6bc956ac67e2 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Database/SchemaTable/SchemaTable.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Database/SchemaTable/SchemaTable.test.tsx @@ -429,7 +429,7 @@ describe('Test EntityTable Component', () => { MOCK_TABLE.fullyQualifiedName, { fields: 'tags,customMetrics,extension', - limit: 50, + limit: 15, offset: 0, sortBy: 'name', sortOrder: 'asc', @@ -454,7 +454,7 @@ describe('Test EntityTable Component', () => { MOCK_TABLE.fullyQualifiedName, { fields: 'tags,customMetrics,extension', - limit: 50, + limit: 15, offset: 0, sortBy: 'name', sortOrder: 'asc', diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Database/TableQueries/TableQueries.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Database/TableQueries/TableQueries.tsx index ca12a234821f..be3c709b77fa 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Database/TableQueries/TableQueries.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Database/TableQueries/TableQueries.tsx @@ -31,10 +31,7 @@ import { FC, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; import { WILD_CARD_CHAR } from '../../../constants/char.constants'; -import { - INITIAL_PAGING_VALUE, - PAGE_SIZE_BASE, -} from '../../../constants/constants'; +import { INITIAL_PAGING_VALUE } from '../../../constants/constants'; import { USAGE_DOCS } from '../../../constants/docs.constants'; import { NO_PERMISSION_FOR_ACTION } from '../../../constants/HelperTextUtil'; import { @@ -150,7 +147,7 @@ const TableQueries: FC = ({ paging, handlePagingChange, showPagination, - } = usePaging(PAGE_SIZE_BASE); + } = usePaging(); const { getEntityPermission, permissions } = usePermissionProvider(); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Domain/SubDomainsTable/SubDomainsTable.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Domain/SubDomainsTable/SubDomainsTable.component.tsx index 3f5da522d557..9c2f45707808 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Domain/SubDomainsTable/SubDomainsTable.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Domain/SubDomainsTable/SubDomainsTable.component.tsx @@ -23,8 +23,9 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { VIEW_MODE_PAGE } from '../../../constants/platform/personaAppLayout.constants'; import { PageViewMode } from '../../../generated/type/personaPreferences'; +import { useSavedPageSize } from '../../../hooks/paging/useSavedPageSize'; import { usePersonaViewMode } from '../../../hooks/platform/usePersonaViewMode'; -import { getViewPageSize } from '../../../utils/PaginationUtils'; +import { getViewPageSizeOptions } from '../../../utils/PaginationUtils'; import { useDelete } from '../../common/atoms/actions/useDelete'; import { useDomainCardTemplates } from '../../common/atoms/domain/ui/useDomainCardTemplates'; import { useDomainFilters } from '../../common/atoms/domain/ui/useDomainFilters'; @@ -51,9 +52,12 @@ const SubDomainsTable = ({ const personaView = usePersonaViewMode(VIEW_MODE_PAGE.SubDomains); const [selectedView, setView] = useState(); const view = selectedView ?? personaView; + const { savedPageSize: listingPageSize } = useSavedPageSize( + getViewPageSizeOptions(view) + ); const subdomainListing = useSubdomainListingData({ parentDomainFqn: domainFqn, - pageSize: getViewPageSize(view), + pageSize: listingPageSize, }); // Card and table views page at different sizes, so the current page may not exist in the other. const { handlePageChange } = subdomainListing; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DomainListing/DomainListPage.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DomainListing/DomainListPage.tsx index 463c0eba8a25..cf1f611e033c 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DomainListing/DomainListPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DomainListing/DomainListPage.tsx @@ -32,10 +32,11 @@ import { LEARNING_PAGE_IDS } from '../../constants/Learning.constants'; import { VIEW_MODE_PAGE } from '../../constants/platform/personaAppLayout.constants'; import { usePermissionProvider } from '../../context/PermissionProvider/PermissionProvider'; import { PageViewMode } from '../../generated/type/personaPreferences'; +import { useSavedPageSize } from '../../hooks/paging/useSavedPageSize'; import { usePersonaViewMode } from '../../hooks/platform/usePersonaViewMode'; import { useIsAiMode } from '../../hooks/useAppMode'; import { useMarketplaceStore } from '../../hooks/useMarketplaceStore'; -import { getViewPageSize } from '../../utils/PaginationUtils'; +import { getViewPageSizeOptions } from '../../utils/PaginationUtils'; import { useDelete } from '../common/atoms/actions/useDelete'; import { useDomainCardTemplates } from '../common/atoms/domain/ui/useDomainCardTemplates'; import { useDomainFilters } from '../common/atoms/domain/ui/useDomainFilters'; @@ -63,9 +64,12 @@ const DomainListPage = ({ renderPageHeader }: DomainListPageProps) => { const personaView = usePersonaViewMode(VIEW_MODE_PAGE.Domains); const [selectedView, setView] = useState(); const view = selectedView ?? personaView; + const { savedPageSize: listingPageSize } = useSavedPageSize( + getViewPageSizeOptions(view) + ); const domainListing = useDomainListingData({ enableNlq: isAiMode, - pageSize: getViewPageSize(view), + pageSize: listingPageSize, }); // Card and table views page at different sizes, so the current page may not exist in the other. const { handlePageChange } = domainListing; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTermTab/GlossaryTermTab.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTermTab/GlossaryTermTab.component.tsx index 5ff7f63c5747..d246b53c193e 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTermTab/GlossaryTermTab.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTermTab/GlossaryTermTab.component.tsx @@ -621,7 +621,7 @@ const GlossaryTermTab = ({ isGlossary, className }: GlossaryTermTabProps) => { const [confirmCheckboxChecked, setConfirmCheckboxChecked] = useState(false); const [totalTermsCount, setTotalTermsCount] = useState(0); - const { paging, handlePagingChange, pageSize } = usePaging(PAGE_SIZE_LARGE); + const { paging, handlePagingChange, pageSize } = usePaging(); const [currentPage, setCurrentPage] = useState(1); const [loadingChildren, setLoadingChildren] = useState< Record diff --git a/openmetadata-ui/src/main/resources/ui/src/components/LineageTable/LineageTable.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/LineageTable/LineageTable.test.tsx index 263e42df7acb..2a013ed9790a 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/LineageTable/LineageTable.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/LineageTable/LineageTable.test.tsx @@ -579,7 +579,8 @@ describe('LineageTable', () => { render(, { wrapper: MemoryRouter }); - expect(mockUsePaging).toHaveBeenCalledWith(50); + // No fixed default: the table starts from the user's saved page size. + expect(mockUsePaging).toHaveBeenCalledWith(); }); }); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/LineageTable/LineageTable.tsx b/openmetadata-ui/src/main/resources/ui/src/components/LineageTable/LineageTable.tsx index 8ce6d8ecb02b..3e497627a04b 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/LineageTable/LineageTable.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/LineageTable/LineageTable.tsx @@ -153,7 +153,7 @@ const LineageTable: FC<{ entity: SourceType }> = ({ entity }) => { handlePageChange, handlePagingChange, handlePageSizeChange, - } = usePaging(PAGE_SIZE_LARGE); + } = usePaging(); const paginationInfoKeyRef = useRef(null); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Applications/AppLogsViewer/ReindexFailures.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Applications/AppLogsViewer/ReindexFailures.component.tsx index 601d8e856bb7..0b7f303d8bec 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Applications/AppLogsViewer/ReindexFailures.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Applications/AppLogsViewer/ReindexFailures.component.tsx @@ -30,7 +30,7 @@ import { } from 'react'; import { useTranslation } from 'react-i18next'; import { RDF_INDEX_APP_NAME } from '../../../../constants/Applications.constant'; -import { PAGE_SIZE_BASE } from '../../../../constants/constants'; +import { useSavedPageSize } from '../../../../hooks/paging/useSavedPageSize'; import { getRdfReindexFailures } from '../../../../rest/rdfAPI'; import { getReindexFailures } from '../../../../rest/searchAPI'; import { formatDateTimeWithTimezone } from '../../../../utils/date-time/DateTimeUtils'; @@ -90,6 +90,7 @@ const ReindexFailures = ({ const [data, setData] = useState([]); const [total, setTotal] = useState(0); const [currentPage, setCurrentPage] = useState(1); + const { savedPageSize: pageSize } = useSavedPageSize(); const [entityTypeFilter, setEntityTypeFilter] = useState( undefined ); @@ -109,8 +110,8 @@ const ReindexFailures = ({ ? getRdfReindexFailures : getReindexFailures; const response = await fetcher({ - offset: (page - 1) * PAGE_SIZE_BASE, - limit: PAGE_SIZE_BASE, + offset: (page - 1) * pageSize, + limit: pageSize, entityType, }); if (requestId !== requestSequence.current) { @@ -133,7 +134,7 @@ const ReindexFailures = ({ } } }, - [appName] + [appName, pageSize] ); useEffect(() => { @@ -289,7 +290,7 @@ const ReindexFailures = ({ loading={loading} pagination={{ current: currentPage, - pageSize: PAGE_SIZE_BASE, + pageSize, showSizeChanger: false, total, }} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/TestLibrary/TestDefinitionList/useTestDefinitionListPage.ts b/openmetadata-ui/src/main/resources/ui/src/components/TestLibrary/TestDefinitionList/useTestDefinitionListPage.ts index c53b2bf6188b..0f74eeade26b 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/TestLibrary/TestDefinitionList/useTestDefinitionListPage.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/TestLibrary/TestDefinitionList/useTestDefinitionListPage.ts @@ -11,10 +11,7 @@ * limitations under the License. */ import { useMemo } from 'react'; -import { - INITIAL_PAGING_VALUE, - PAGE_SIZE_BASE, -} from '../../../constants/constants'; +import { INITIAL_PAGING_VALUE } from '../../../constants/constants'; import { TEST_DEFINITION_COLUMN_BY_SORT_FIELD } from '../../../constants/TestDefinition.constants'; import { usePaging } from '../../../hooks/paging/usePaging'; import { PagingHandlerParams } from '../../common/NextPrevious/NextPrevious.interface'; @@ -39,7 +36,7 @@ export const useTestDefinitionListPage = () => { handlePageSizeChange, showPagination, pagingCursor, - } = usePaging(PAGE_SIZE_BASE); + } = usePaging(); const { urlParams, diff --git a/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/Profile/tabs/access-control/AccessControlAuditLogsPanel.tsx b/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/Profile/tabs/access-control/AccessControlAuditLogsPanel.tsx index 4b11558157aa..5bccccbef9ad 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/Profile/tabs/access-control/AccessControlAuditLogsPanel.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/Profile/tabs/access-control/AccessControlAuditLogsPanel.tsx @@ -53,6 +53,7 @@ import { } from '../../../../../../constants/constants'; import { useWebSocketConnector } from '../../../../../../context/WebSocketProvider/WebSocketProvider'; import { Paging } from '../../../../../../generated/type/paging'; +import { useSavedPageSize } from '../../../../../../hooks/paging/useSavedPageSize'; import { useHashPagingParams } from '../../../../../../hooks/useSettingsHash'; import { exportAuditLogs, @@ -127,8 +128,9 @@ async function walkToPageCursor( return { cursor, discoveredCursors }; } -function resolvePageSize(hash: number | undefined): number { - return hash || PAGE_SIZE_MEDIUM; +// A size in the hash (a shared or reloaded link) wins over the size the user last picked. +function resolvePageSize(hash: number | undefined, saved: number): number { + return hash || saved; } interface AccessControlAuditLogsPanelProps { @@ -149,7 +151,8 @@ const AccessControlAuditLogsPanel: React.FC< cursorType: hashCursorType, setPage: setHashPage, } = useHashPagingParams(); - const pageSize = resolvePageSize(hashPageSize); + const { savedPageSize, savePageSize } = useSavedPageSize(); + const pageSize = resolvePageSize(hashPageSize, savedPageSize); const [logs, setLogs] = useState([]); const [paging, setPaging] = useState(INITIAL_PAGING); @@ -240,9 +243,10 @@ const AccessControlAuditLogsPanel: React.FC< const handlePageSizeChange = useCallback( (size: number) => { pageCursorsRef.current = {}; + savePageSize(size); setHashPage(1, undefined, undefined, size); }, - [setHashPage] + [savePageSize, setHashPage] ); const handleFiltersChange = useCallback( diff --git a/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/Profile/tabs/access-control/AccessControlPoliciesPanel.tsx b/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/Profile/tabs/access-control/AccessControlPoliciesPanel.tsx index 812889d4a1ba..b92aa223dc99 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/Profile/tabs/access-control/AccessControlPoliciesPanel.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/Profile/tabs/access-control/AccessControlPoliciesPanel.tsx @@ -53,6 +53,7 @@ import { } from '../../../../../../generated/entity/policies/policy'; import { EntityReference } from '../../../../../../generated/entity/type'; import { Paging } from '../../../../../../generated/type/paging'; +import { useSavedPageSize } from '../../../../../../hooks/paging/useSavedPageSize'; import { useHashPagingParams } from '../../../../../../hooks/useSettingsHash'; import { getPolicies } from '../../../../../../rest/rolesAPIV1'; import { hardDeleteEntity } from '../../../../../../utils/DeleteWidget/DeleteWidgetUtils'; @@ -87,7 +88,8 @@ const AccessControlPoliciesPanel: React.FC = ({ cursorType: hashCursorType, setPage: setHashPage, } = useHashPagingParams(); - const pageSize = hashPageSize || PAGE_SIZE_BASE; + const { savedPageSize, savePageSize } = useSavedPageSize(); + const pageSize = hashPageSize || savedPageSize; const [policies, setPolicies] = useState([]); const [isLoading, setIsLoading] = useState(true); @@ -283,6 +285,7 @@ const AccessControlPoliciesPanel: React.FC = ({ }; const handlePageSizeChange = (size: number) => { + savePageSize(size); setHashPage(1, undefined, undefined, size); setCursorCache(new Map()); }; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/Profile/tabs/access-control/AccessControlRolesPanel.tsx b/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/Profile/tabs/access-control/AccessControlRolesPanel.tsx index 35876ee555cb..daa4d179e1f7 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/Profile/tabs/access-control/AccessControlRolesPanel.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/Profile/tabs/access-control/AccessControlRolesPanel.tsx @@ -51,6 +51,7 @@ import { Operation } from '../../../../../../generated/entity/policies/policy'; import { Role } from '../../../../../../generated/entity/teams/role'; import { EntityReference } from '../../../../../../generated/entity/type'; import { Paging } from '../../../../../../generated/type/paging'; +import { useSavedPageSize } from '../../../../../../hooks/paging/useSavedPageSize'; import { useHashPagingParams } from '../../../../../../hooks/useSettingsHash'; import { getRoles } from '../../../../../../rest/rolesAPIV1'; import { hardDeleteEntity } from '../../../../../../utils/DeleteWidget/DeleteWidgetUtils'; @@ -85,7 +86,8 @@ const AccessControlRolesPanel: React.FC = ({ cursorType: hashCursorType, setPage: setHashPage, } = useHashPagingParams(); - const pageSize = hashPageSize || PAGE_SIZE_BASE; + const { savedPageSize, savePageSize } = useSavedPageSize(); + const pageSize = hashPageSize || savedPageSize; const [roles, setRoles] = useState([]); const [isLoading, setIsLoading] = useState(false); @@ -283,6 +285,7 @@ const AccessControlRolesPanel: React.FC = ({ }; const handlePageSizeChange = (size: number) => { + savePageSize(size); setHashPage(1, undefined, undefined, size); setCursorCache(new Map()); }; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/Profile/tabs/bots/BotsListPanel.tsx b/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/Profile/tabs/bots/BotsListPanel.tsx index 368ecc2a539c..079fcc7eaa98 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/Profile/tabs/bots/BotsListPanel.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/Profile/tabs/bots/BotsListPanel.tsx @@ -51,6 +51,7 @@ import { User } from '../../../../../../generated/entity/teams/user'; import { Include } from '../../../../../../generated/type/include'; import { Paging } from '../../../../../../generated/type/paging'; import { useAuth } from '../../../../../../hooks/authHooks'; +import { useSavedPageSize } from '../../../../../../hooks/paging/useSavedPageSize'; import { useSettingsHash } from '../../../../../../hooks/useSettingsHash'; import { getBots } from '../../../../../../rest/botsAPI'; import { searchQuery } from '../../../../../../rest/searchAPI'; @@ -103,8 +104,9 @@ const BotsListPanel: React.FC = ({ const [currentPage, setCurrentPage] = useState( Number(hashState.params.page) || 1 ); + const { savedPageSize, savePageSize } = useSavedPageSize(); const [pageSize, setPageSize] = useState( - Number(hashState.params.pageSize) || PAGE_SIZE_BASE + Number(hashState.params.pageSize) || savedPageSize ); const [reloadKey, setReloadKey] = useState(0); const fetchRequestIdRef = useRef(0); @@ -345,6 +347,7 @@ const BotsListPanel: React.FC = ({ const handlePageSizeChange = useCallback( (newSize: number) => { setPageSize(newSize); + savePageSize(newSize); setCurrentPage(1); updateParams({ page: '1', @@ -353,7 +356,7 @@ const BotsListPanel: React.FC = ({ pageSize: String(newSize), }); }, - [updateParams] + [savePageSize, updateParams] ); const handleShowDeletedToggle = useCallback( diff --git a/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/Profile/tabs/governance/GovernanceGlossaryList.tsx b/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/Profile/tabs/governance/GovernanceGlossaryList.tsx index cdf64c85c3d1..07e20a2381a6 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/Profile/tabs/governance/GovernanceGlossaryList.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/Profile/tabs/governance/GovernanceGlossaryList.tsx @@ -31,6 +31,7 @@ import { PAGE_SIZE_MEDIUM, } from '../../../../../../constants/constants'; import { RelationshipType } from '../../../../../../generated/entity/data/relationshipType'; +import { useSavedPageSize } from '../../../../../../hooks/paging/useSavedPageSize'; import RelationshipTypeTable from '../../../../../../pages/GlossaryTermRelationSettings/RelationshipTypeTable'; import { getRelationTypeUsageCounts } from '../../../../../../rest/glossaryAPI'; import { @@ -78,7 +79,8 @@ const GovernanceGlossaryList: FC = ({ const [usageCounts, setUsageCounts] = useState>({}); const [deleteTarget, setDeleteTarget] = useState(); const [currentPage, setCurrentPage] = useState(1); - const [pageSize, setPageSize] = useState(PAGE_SIZE_BASE); + const { savedPageSize, savePageSize } = useSavedPageSize(); + const [pageSize, setPageSize] = useState(savedPageSize); const fetchData = useCallback(async () => { setIsLoading(true); @@ -181,6 +183,7 @@ const GovernanceGlossaryList: FC = ({ onPageChange={setCurrentPage} onPageSizeChange={(next) => { setPageSize(next); + savePageSize(next); setCurrentPage(1); }} /> diff --git a/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/Profile/tabs/notification/NotificationAlertsPanel.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/Profile/tabs/notification/NotificationAlertsPanel.test.tsx index 15058e08d07c..7166eeab4913 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/Profile/tabs/notification/NotificationAlertsPanel.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/Profile/tabs/notification/NotificationAlertsPanel.test.tsx @@ -192,6 +192,7 @@ jest.mock( ); jest.mock('../../../../../../constants/constants', () => ({ + ...jest.requireActual('../../../../../../constants/constants'), PAGE_SIZE_BASE: 10, PAGE_SIZE_MEDIUM: 25, PAGE_SIZE_LARGE: 50, diff --git a/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/Profile/tabs/notification/NotificationAlertsPanel.tsx b/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/Profile/tabs/notification/NotificationAlertsPanel.tsx index 7e35ca6daeda..82865f5dab89 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/Profile/tabs/notification/NotificationAlertsPanel.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/Profile/tabs/notification/NotificationAlertsPanel.tsx @@ -50,6 +50,7 @@ import { ProviderType, } from '../../../../../../generated/events/eventSubscription'; import { Paging } from '../../../../../../generated/type/paging'; +import { useSavedPageSize } from '../../../../../../hooks/paging/useSavedPageSize'; import { useHashPagingParams } from '../../../../../../hooks/useSettingsHash'; import { getAlertsFromName, @@ -83,7 +84,8 @@ const NotificationAlertsPanel: React.FC = ({ cursorType: hashCursorType, setPage: setHashPage, } = useHashPagingParams(); - const pageSize = hashPageSize || PAGE_SIZE_BASE; + const { savedPageSize, savePageSize } = useSavedPageSize(); + const pageSize = hashPageSize || savedPageSize; const [alerts, setAlerts] = useState([]); const [isLoading, setIsLoading] = useState(false); const [selectedAlert, setSelectedAlert] = useState(); @@ -327,6 +329,7 @@ const NotificationAlertsPanel: React.FC = ({ }; const handlePageSizeChange = (size: number) => { + savePageSize(size); setHashPage(1, undefined, undefined, size); setCursorCache(new Map()); }; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/Profile/tabs/notification/NotificationRecentEvents.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/Profile/tabs/notification/NotificationRecentEvents.test.tsx index ffe7b2555ec2..cfcff62a603e 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/Profile/tabs/notification/NotificationRecentEvents.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/Profile/tabs/notification/NotificationRecentEvents.test.tsx @@ -136,6 +136,7 @@ jest.mock('../../../../../../assets/svg/ic-filter-off.svg', () => ({ })); jest.mock('../../../../../../constants/constants', () => ({ + ...jest.requireActual('../../../../../../constants/constants'), PAGE_SIZE_BASE: 10, PAGE_SIZE_MEDIUM: 25, PAGE_SIZE_LARGE: 50, diff --git a/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/Profile/tabs/notification/NotificationRecentEvents.tsx b/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/Profile/tabs/notification/NotificationRecentEvents.tsx index 054230b36ba3..5f74a89851a6 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/Profile/tabs/notification/NotificationRecentEvents.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/Profile/tabs/notification/NotificationRecentEvents.tsx @@ -45,6 +45,7 @@ import { } from '../../../../../../generated/events/api/typedEvent'; import { EventSubscription } from '../../../../../../generated/events/eventSubscription'; import { Paging } from '../../../../../../generated/type/paging'; +import { useSavedPageSize } from '../../../../../../hooks/paging/useSavedPageSize'; import { useSettingsHash } from '../../../../../../hooks/useSettingsHash'; import { getAlertEventsFromId } from '../../../../../../rest/alertsAPI'; import { getAlertStatusIcon } from '../../../../../../utils/Alerts/AlertsUtil'; @@ -88,7 +89,8 @@ function NotificationRecentEvents({ ); const { state: hashState, updateParams } = useSettingsHash(); const initialPage = Number(hashState.params.page) || 1; - const initialPageSize = Number(hashState.params.pageSize) || PAGE_SIZE_BASE; + const { savedPageSize, savePageSize } = useSavedPageSize(); + const initialPageSize = Number(hashState.params.pageSize) || savedPageSize; const [currentPage, setCurrentPage] = useState(initialPage); const [pageSize, setPageSize] = useState(initialPageSize); @@ -154,10 +156,11 @@ function NotificationRecentEvents({ (size: number) => { setCurrentPage(1); setPageSize(size); + savePageSize(size); updateParams({ page: '1', pageSize: String(size) }); getAlertRecentEvents(0, size); }, - [updateParams, getAlertRecentEvents] + [savePageSize, updateParams, getAlertRecentEvents] ); const showPagination = useMemo( diff --git a/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/Profile/tabs/platform-settings/search/SearchPreviewPanel.tsx b/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/Profile/tabs/platform-settings/search/SearchPreviewPanel.tsx index 63c315fd70c6..c0e5e4501154 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/Profile/tabs/platform-settings/search/SearchPreviewPanel.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/Profile/tabs/platform-settings/search/SearchPreviewPanel.tsx @@ -23,9 +23,9 @@ import { AxiosError } from 'axios'; import { debounce } from 'lodash'; import { useEffect, useMemo, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { PAGE_SIZE_BASE } from '../../../../../../../constants/constants'; import { ENTITY_PATH_TO_SEARCH_INDEX } from '../../../../../../../constants/SearchSettings.constant'; import { SearchSettings } from '../../../../../../../generated/configuration/searchSettings'; +import { useSavedPageSize } from '../../../../../../../hooks/paging/useSavedPageSize'; import { searchPreview } from '../../../../../../../rest/searchAPI'; import { showErrorToast } from '../../../../../../../utils/ToastUtils'; import ExploreSearchCard from '../../../../../../ExploreV1/ExploreSearchCard/ExploreSearchCard'; @@ -51,7 +51,8 @@ const SearchPreviewPanel = ({ const [query, setQuery] = useState(''); const [searchTerm, setSearchTerm] = useState(''); const [page, setPage] = useState(1); - const [pageSize, setPageSize] = useState(PAGE_SIZE_BASE); + const { savedPageSize, savePageSize } = useSavedPageSize(); + const [pageSize, setPageSize] = useState(savedPageSize); const [showRankingDetails, setShowRankingDetails] = useState(false); const [hits, setHits] = useState([]); const [total, setTotal] = useState(0); @@ -200,6 +201,7 @@ const SearchPreviewPanel = ({ onPageChange={setPage} onPageSizeChange={(next) => { setPageSize(next); + savePageSize(next); setPage(1); }} /> diff --git a/openmetadata-ui/src/main/resources/ui/src/components/observability/Alerts/AlertAiRecentEventsTab.tsx b/openmetadata-ui/src/main/resources/ui/src/components/observability/Alerts/AlertAiRecentEventsTab.tsx index bc5592b6e202..62bcafa3deb6 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/observability/Alerts/AlertAiRecentEventsTab.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/observability/Alerts/AlertAiRecentEventsTab.tsx @@ -190,7 +190,7 @@ const AlertAiRecentEventsTab = ({ pageSize, paging, showPagination, - } = usePaging(PAGE_SIZE_BASE, PAGE_SIZE_OPTIONS); + } = usePaging(undefined, PAGE_SIZE_OPTIONS); const { id } = alertDetails; const isFiltered = filter !== AlertRecentEventFilters.ALL; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/observability/IncidentManager/IncidentGroups/IncidentGroupDetail.tsx b/openmetadata-ui/src/main/resources/ui/src/components/observability/IncidentManager/IncidentGroups/IncidentGroupDetail.tsx index 0907a9e3af52..972477fce4a4 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/observability/IncidentManager/IncidentGroups/IncidentGroupDetail.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/observability/IncidentManager/IncidentGroups/IncidentGroupDetail.tsx @@ -28,7 +28,6 @@ import { Transi18next } from '../../../../utils/i18next/LocalUtil'; import { computeTotalPages } from '../../../../utils/PaginationUtils'; import IncidentGroupRelatedBadge from './IncidentGroupRelatedBadge'; import { - INCIDENT_GROUPS_PAGE_SIZE, INCIDENT_GROUPS_PAGE_SIZE_OPTIONS, INCIDENT_GROUP_SEPARATOR, } from './IncidentGroups.constants'; @@ -71,7 +70,6 @@ const IncidentGroupDetail = ({ } = useIncidentGroupIncidents({ group, filters, - defaultPageSize: INCIDENT_GROUPS_PAGE_SIZE, }); const name = getIncidentGroupName( group, diff --git a/openmetadata-ui/src/main/resources/ui/src/components/observability/IncidentManager/IncidentGroups/IncidentGroupDrawer.tsx b/openmetadata-ui/src/main/resources/ui/src/components/observability/IncidentManager/IncidentGroups/IncidentGroupDrawer.tsx index b55f15cfb3cc..2a0c6f5342b6 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/observability/IncidentManager/IncidentGroups/IncidentGroupDrawer.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/observability/IncidentManager/IncidentGroups/IncidentGroupDrawer.tsx @@ -23,10 +23,7 @@ import { NO_DATA_PLACEHOLDER } from '../../../../constants/constants'; import { formatDate } from '../../../../utils/date-time/DateTimeUtils'; import { computeTotalPages } from '../../../../utils/PaginationUtils'; import IncidentGroupRelatedBadge from './IncidentGroupRelatedBadge'; -import { - INCIDENT_GROUPS_PAGE_SIZE, - INCIDENT_GROUPS_PAGE_SIZE_OPTIONS, -} from './IncidentGroups.constants'; +import { INCIDENT_GROUPS_PAGE_SIZE_OPTIONS } from './IncidentGroups.constants'; import { IncidentGroupDrawerProps, IncidentGroupStatProps, @@ -96,7 +93,6 @@ const IncidentGroupDrawer = ({ } = useIncidentGroupIncidents({ group, filters, - defaultPageSize: INCIDENT_GROUPS_PAGE_SIZE, }); return ( diff --git a/openmetadata-ui/src/main/resources/ui/src/components/observability/IncidentManager/IncidentGroups/IncidentGroups.constants.ts b/openmetadata-ui/src/main/resources/ui/src/components/observability/IncidentManager/IncidentGroups/IncidentGroups.constants.ts index b841c56ea2a1..ea5f96a2497e 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/observability/IncidentManager/IncidentGroups/IncidentGroups.constants.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/observability/IncidentManager/IncidentGroups/IncidentGroups.constants.ts @@ -63,8 +63,6 @@ export const INCIDENT_GROUP_DETAIL_PARAM = 'group'; /** Dimension the page opens with when the URL does not carry a valid one. */ export const DEFAULT_INCIDENT_GROUP_BY = IncidentGroupBy.TestDefinition; -export const INCIDENT_GROUPS_PAGE_SIZE = PAGE_SIZE_BASE; - /** The page sizes the application's paged lists offer. */ export const INCIDENT_GROUPS_PAGE_SIZE_OPTIONS = [ PAGE_SIZE_BASE, diff --git a/openmetadata-ui/src/main/resources/ui/src/components/observability/IncidentManager/IncidentGroups/useIncidentGroupIncidents.ts b/openmetadata-ui/src/main/resources/ui/src/components/observability/IncidentManager/IncidentGroups/useIncidentGroupIncidents.ts index e882f821b05c..6cb9d5ec7dc6 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/observability/IncidentManager/IncidentGroups/useIncidentGroupIncidents.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/observability/IncidentManager/IncidentGroups/useIncidentGroupIncidents.ts @@ -32,7 +32,8 @@ interface UseIncidentGroupIncidentsProps { group?: TestCaseIncidentGroup; /** The filters the group was fetched with, so the list matches its count. */ filters: IncidentGroupFilters; - defaultPageSize: number; + /** Overrides the size the user last picked; used by tests that page small sets. */ + defaultPageSize?: number; } const NO_INCIDENTS: TestCaseResolutionStatus[] = []; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/observability/IncidentManager/IncidentGroups/useIncidentGroups.ts b/openmetadata-ui/src/main/resources/ui/src/components/observability/IncidentManager/IncidentGroups/useIncidentGroups.ts index a3ba1e41d14c..94ed911f7c80 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/observability/IncidentManager/IncidentGroups/useIncidentGroups.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/observability/IncidentManager/IncidentGroups/useIncidentGroups.ts @@ -37,7 +37,6 @@ import { import { showErrorToast } from '../../../../utils/ToastUtils'; import { DEFAULT_INCIDENT_GROUP_SORT, - INCIDENT_GROUPS_PAGE_SIZE, INCIDENT_GROUP_BY_PARAM, INCIDENT_GROUP_DETAIL_PARAM, INCIDENT_GROUP_FILTER_KEYS, @@ -286,8 +285,7 @@ export const useIncidentGroups = () => { DEFAULT_INCIDENT_GROUP_SORT ); const { currentPage, pageSize, setPageSize, goToPage } = useIncidentPaging( - `${groupBy}|${sort.field}|${sort.type}|${domain}|${filtersSearch}`, - INCIDENT_GROUPS_PAGE_SIZE + `${groupBy}|${sort.field}|${sort.type}|${domain}|${filtersSearch}` ); const listParams = { diff --git a/openmetadata-ui/src/main/resources/ui/src/components/observability/IncidentManager/IncidentGroups/useIncidentPaging.ts b/openmetadata-ui/src/main/resources/ui/src/components/observability/IncidentManager/IncidentGroups/useIncidentPaging.ts index 4fdaaf5648d8..380f9c135e76 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/observability/IncidentManager/IncidentGroups/useIncidentPaging.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/observability/IncidentManager/IncidentGroups/useIncidentPaging.ts @@ -12,6 +12,7 @@ */ import { useCallback, useState } from 'react'; +import { useSavedPageSize } from '../../../../hooks/paging/useSavedPageSize'; interface PageState { /** The query the page belongs to; a page of another query is page 1. */ @@ -23,13 +24,24 @@ interface PageState { * Page state for the incident listings, which take a 1-based page. Any change * to the query — or to the page size — reads as the first page again. That * reset is derived rather than set from an effect, so the stale page is never - * fetched on the way. + * fetched on the way. The page size starts from `defaultPageSize` when given, + * otherwise from the size the user last picked on the list scale. */ export const useIncidentPaging = ( queryKey: string, - defaultPageSize: number + defaultPageSize?: number ) => { - const [pageSize, setPageSize] = useState(defaultPageSize); + const { savedPageSize, savePageSize } = useSavedPageSize(); + const [pageSize, setPageSizeState] = useState( + defaultPageSize ?? savedPageSize + ); + const setPageSize = useCallback( + (size: number) => { + setPageSizeState(size); + savePageSize(size); + }, + [savePageSize] + ); const pagedKey = `${queryKey}|${pageSize}`; const [page, setPage] = useState({ queryKey: pagedKey, diff --git a/openmetadata-ui/src/main/resources/ui/src/constants/ContextCenter.constants.ts b/openmetadata-ui/src/main/resources/ui/src/constants/ContextCenter.constants.ts index 908841b8f633..0cc44c313dbd 100644 --- a/openmetadata-ui/src/main/resources/ui/src/constants/ContextCenter.constants.ts +++ b/openmetadata-ui/src/main/resources/ui/src/constants/ContextCenter.constants.ts @@ -73,10 +73,6 @@ export const ARTICLE_SORT_OPTIONS: ArticleSortOption[] = [ export const DEFAULT_ARTICLE_SORT_OPTION = ARTICLE_SORT_OPTIONS[0]; -export { - PAGE_SIZE_BASE as ARCHIVE_PAGE_SIZE, - PAGE_SIZE_BASE as MEMORIES_PER_PAGE, -} from './constants'; export const RECENT_DASHBOARD_ARTICLES_LIMIT = 3; export const RECENT_DASHBOARD_DOCUMENTS_LIMIT = 3; export const RECENT_DASHBOARD_MEMORIES_LIMIT = 3; diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/AuditLogsPage/AuditLogsPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/AuditLogsPage/AuditLogsPage.tsx index 026753a0e44d..027649b18d0c 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/AuditLogsPage/AuditLogsPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/AuditLogsPage/AuditLogsPage.tsx @@ -51,6 +51,7 @@ import { PAGE_HEADERS } from '../../constants/PageHeaders.constant'; import { useWebSocketConnector } from '../../context/WebSocketProvider/WebSocketProvider'; import { CursorType } from '../../enums/pagination.enum'; import { Paging } from '../../generated/type/paging'; +import { useSavedPageSize } from '../../hooks/paging/useSavedPageSize'; import { exportAuditLogs, getAuditLogExportJob, @@ -104,7 +105,8 @@ const AuditLogsPage = () => { {} ); const filterParamsRef = useRef>({}); - const [pageSize, setPageSize] = useState(PAGE_SIZE_MEDIUM); + const { savedPageSize, savePageSize } = useSavedPageSize(); + const [pageSize, setPageSize] = useState(savedPageSize); const [isExportModalOpen, setIsExportModalOpen] = useState(false); const [exportDateRange, setExportDateRange] = useState< @@ -161,10 +163,14 @@ const AuditLogsPage = () => { [fetchAuditLogs, paging] ); - const handlePageSizeChange = useCallback((size: number) => { - setPageSize(size); - setCurrentPage(1); - }, []); + const handlePageSizeChange = useCallback( + (size: number) => { + setPageSize(size); + savePageSize(size); + setCurrentPage(1); + }, + [savePageSize] + ); const handleFiltersChange = useCallback( (filters: AuditLogActiveFilter[], params: Partial) => { diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/ColumnBulkOperations/ColumnGrid/hooks/useColumnGridListingData.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/ColumnBulkOperations/ColumnGrid/hooks/useColumnGridListingData.tsx index 689f956608bd..4591a17ad5fb 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/ColumnBulkOperations/ColumnGrid/hooks/useColumnGridListingData.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/ColumnBulkOperations/ColumnGrid/hooks/useColumnGridListingData.tsx @@ -23,10 +23,10 @@ import { ListingData, } from '../../../../components/common/atoms/shared/types'; import { ExploreQuickFilterField } from '../../../../components/Explore/ExplorePage.interface'; -import { PAGE_SIZE_MEDIUM } from '../../../../constants/constants'; import { EntityFields } from '../../../../enums/AdvancedSearch.enum'; import { ColumnGridItem } from '../../../../generated/api/data/columnGridResponse'; import { TagLabel } from '../../../../generated/type/tagLabel'; +import { useSavedPageSize } from '../../../../hooks/paging/useSavedPageSize'; import { getColumnGrid } from '../../../../rest/columnAPI'; import { showErrorToast } from '../../../../utils/ToastUtils'; import { ColumnGridFilters, ColumnGridRowData } from '../ColumnGrid.interface'; @@ -38,8 +38,6 @@ import { MAX_REFETCH_CHAIN_PAGES, } from '../constants/ColumnGrid.constants'; -const PAGE_SIZE = PAGE_SIZE_MEDIUM; - interface UseColumnGridListingDataProps { externalFilters?: ColumnGridFilters; transformGridItemsToRows: ( @@ -96,7 +94,8 @@ export const useColumnGridListingData = ( // Local pagination state (cursor-based, not in URL) const [currentPage, setCurrentPage] = useState(1); - const [pageSize, setPageSize] = useState(PAGE_SIZE); + const { savedPageSize, savePageSize } = useSavedPageSize(); + const [pageSize, setPageSize] = useState(savedPageSize); // URL state management (search and filters only, pagination is local) const urlState = useMemo(() => { @@ -412,14 +411,18 @@ export const useColumnGridListingData = ( setCurrentPage(page); }, []); - const handlePageSizeChange = useCallback((size: number) => { - setPageSize(size); - setCurrentPage(1); - cursorsByPageRef.current = new Map(); - itemsByPageRef.current = new Map(); - totalUniqueColumnsRef.current = 0; - totalOccurrencesRef.current = 0; - }, []); + const handlePageSizeChange = useCallback( + (size: number) => { + setPageSize(size); + savePageSize(size); + setCurrentPage(1); + cursorsByPageRef.current = new Map(); + itemsByPageRef.current = new Map(); + totalUniqueColumnsRef.current = 0; + totalOccurrencesRef.current = 0; + }, + [savePageSize] + ); /** * Refetch after bulk update (WebSocket job complete). diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/ContextCenterPage/ContextCenterArchivePage/ContextCenterArchivePage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/ContextCenterPage/ContextCenterArchivePage/ContextCenterArchivePage.tsx index 30a4684c51c8..b9f7540e7007 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/ContextCenterPage/ContextCenterArchivePage/ContextCenterArchivePage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/ContextCenterPage/ContextCenterArchivePage/ContextCenterArchivePage.tsx @@ -22,7 +22,6 @@ import ArchiveView from '../../../components/ContextCenter/ArchiveView/ArchiveVi import { ArchiveItem } from '../../../components/ContextCenter/ArchiveView/ArchiveView.interface'; import ContextCenterHeader from '../../../components/ContextCenter/ContextCenterHeader/ContextCenterHeader.component'; import { useContextCenterPageLayout } from '../../../components/ContextCenter/ContextCenterLayout/useContextCenterPageLayout'; -import { ARCHIVE_PAGE_SIZE } from '../../../constants/ContextCenter.constants'; import { usePermissionProvider } from '../../../context/PermissionProvider/PermissionProvider'; import { OperationPermission } from '../../../context/PermissionProvider/PermissionProvider.interface'; import { ResourceEntity } from '../../../enums/permissions.enum'; @@ -52,7 +51,7 @@ const ContextCenterArchivePage: FC = () => { const pageLayoutClassNames = useContextCenterPageLayout(); const { currentUser } = useApplicationStore(); const { getResourcePermission } = usePermissionProvider(); - const { paging, pageSize, handlePagingChange } = usePaging(ARCHIVE_PAGE_SIZE); + const { paging, pageSize, handlePagingChange } = usePaging(); const [items, setItems] = useState([]); const [isLoading, setIsLoading] = useState(true); const [isLoadingMore, setIsLoadingMore] = useState(false); diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/ContextCenterPage/ContextCenterMemoriesPage/ContextCenterMemoriesPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/ContextCenterPage/ContextCenterMemoriesPage/ContextCenterMemoriesPage.tsx index 08fd251e7bb2..243b0ea8d279 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/ContextCenterPage/ContextCenterMemoriesPage/ContextCenterMemoriesPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/ContextCenterPage/ContextCenterMemoriesPage/ContextCenterMemoriesPage.tsx @@ -58,7 +58,6 @@ import { INBOX_OPEN_TASK_COUNT_QUERY_KEY } from '../../../components/discovery/p import { DEFAULT_MEMORY_STATUSES, FILTER_TABS, - MEMORIES_PER_PAGE, MEMORY_FIELDS, MEMORY_STATUS_LABEL_KEYS, } from '../../../constants/ContextCenter.constants'; @@ -70,6 +69,7 @@ import { ContextMemoryStatus, } from '../../../generated/entity/context/contextMemory'; import { useMemoryConflictReviews } from '../../../hooks/discovery/context-center/useMemoryConflictReviews'; +import { useSavedPageSize } from '../../../hooks/paging/useSavedPageSize'; import { useApplicationStore } from '../../../hooks/useApplicationStore'; import { queryClient } from '../../../queryClient'; import { @@ -339,6 +339,7 @@ const ContextCenterMemoriesPage: FC = () => { ContextMemoryStatus[] >(DEFAULT_MEMORY_STATUSES); const [currentPage, setCurrentPage] = useState(1); + const { savedPageSize: memoriesPerPage } = useSavedPageSize(); const [selectedAsset, setSelectedAsset] = useState(); const [selectedAuthor, setSelectedAuthor] = useState(); const [authorOptions, setAuthorOptions] = useState([]); @@ -437,8 +438,8 @@ const ContextCenterMemoriesPage: FC = () => { ? currentUser?.id ?? currentUser?.name : selectedAuthor?.id; const response = await getListContextMemories({ - limit: MEMORIES_PER_PAGE, - offset: (currentPage - 1) * MEMORIES_PER_PAGE, + limit: memoriesPerPage, + offset: (currentPage - 1) * memoriesPerPage, fields: MEMORY_FIELDS, q: debouncedSearch.trim() || undefined, assets: selectedAsset?.id, @@ -463,6 +464,7 @@ const ContextCenterMemoriesPage: FC = () => { activeFilter, applyMemoryListResponse, currentPage, + memoriesPerPage, currentUser?.id, currentUser?.name, debouncedSearch, @@ -604,7 +606,7 @@ const ContextCenterMemoriesPage: FC = () => { fetchMemoryCounts(); }, [fetchMemoryCounts]); - const totalPages = Math.max(1, Math.ceil(totalMemories / MEMORIES_PER_PAGE)); + const totalPages = Math.max(1, Math.ceil(totalMemories / memoriesPerPage)); const { hasActiveFilters, diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/DataInsightPage/KPIList.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/DataInsightPage/KPIList.tsx index 53693af655ed..2b2065f0b0e1 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/DataInsightPage/KPIList.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/DataInsightPage/KPIList.tsx @@ -29,11 +29,7 @@ import { PagingHandlerParams } from '../../components/common/NextPrevious/NextPr import { ColumnsType } from '../../components/common/Table/Table.interface'; import Table from '../../components/common/Table/TableV2'; import { EmptyGraphPlaceholder } from '../../components/DataInsight/EmptyGraphPlaceholder'; -import { - INITIAL_PAGING_VALUE, - PAGE_SIZE_MEDIUM, - pagingObject, -} from '../../constants/constants'; +import { INITIAL_PAGING_VALUE, pagingObject } from '../../constants/constants'; import { usePermissionProvider } from '../../context/PermissionProvider/PermissionProvider'; import { ERROR_PLACEHOLDER_TYPE } from '../../enums/common.enum'; import { EntityType, TabSpecificField } from '../../enums/entity.enum'; @@ -41,6 +37,7 @@ import { ResourceEntity } from '../../enums/permissions.enum'; import { Kpi, KpiTargetType } from '../../generated/dataInsight/kpi/kpi'; import { Operation } from '../../generated/entity/policies/policy'; import { Paging } from '../../generated/type/paging'; +import { useSavedPageSize } from '../../hooks/paging/useSavedPageSize'; import { useApplicationStore } from '../../hooks/useApplicationStore'; import { getListKPIs } from '../../rest/KpiAPI'; import { formatDateTime } from '../../utils/date-time/DateTimeUtils'; @@ -54,6 +51,7 @@ const KPIList = () => { const navigate = useNavigate(); const { currentUser } = useApplicationStore(); const isAdminUser = currentUser?.isAdmin ?? false; + const { savedPageSize: kpiPageSize } = useSavedPageSize(); const { t } = useTranslation(); const { permissions } = usePermissionProvider(); const viewKPIPermission = useMemo( @@ -78,7 +76,7 @@ const KPIList = () => { TabSpecificField.DATA_INSIGHT_CHART, TabSpecificField.METRIC_TYPE, ], - limit: PAGE_SIZE_MEDIUM, + limit: kpiPageSize, before: param && param.before, after: param && param.after, }); @@ -254,8 +252,8 @@ const KPIList = () => { customPaginationProps={{ currentPage: kpiPage, isLoading, - showPagination: kpiList.length > PAGE_SIZE_MEDIUM, - pageSize: PAGE_SIZE_MEDIUM, + showPagination: kpiList.length > kpiPageSize, + pageSize: kpiPageSize, paging: kpiPaging, pagingHandler: kpiPagingHandler, }} diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/GlossaryTermRelationSettings/GlossaryTermRelationSettings.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/GlossaryTermRelationSettings/GlossaryTermRelationSettings.tsx index c7ffbf4db6ca..bc9964e8be95 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/GlossaryTermRelationSettings/GlossaryTermRelationSettings.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/GlossaryTermRelationSettings/GlossaryTermRelationSettings.tsx @@ -36,6 +36,7 @@ import { import { GlobalSettingsMenuCategory } from '../../constants/GlobalSettings.constants'; import { RelationshipType } from '../../generated/entity/data/relationshipType'; import { useAuth } from '../../hooks/authHooks'; +import { useSavedPageSize } from '../../hooks/paging/useSavedPageSize'; import { getRelationTypeUsageCounts } from '../../rest/glossaryAPI'; import { createRelationshipType, @@ -132,7 +133,8 @@ const GlossaryTermRelationSettingsPage = () => { ); const [formErrors, setFormErrors] = useState>({}); const [currentPage, setCurrentPage] = useState(1); - const [pageSize, setPageSize] = useState(PAGE_SIZE_BASE); + const { savedPageSize, savePageSize } = useSavedPageSize(); + const [pageSize, setPageSize] = useState(savedPageSize); const breadcrumbs: TitleBreadcrumbProps['titleLinks'] = useMemo( () => @@ -348,6 +350,7 @@ const GlossaryTermRelationSettingsPage = () => { onPageChange={setCurrentPage} onPageSizeChange={(nextPageSize) => { setPageSize(nextPageSize); + savePageSize(nextPageSize); setCurrentPage(1); }} /> diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/MetricsPage/MetricListPage/MetricListPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/MetricsPage/MetricListPage/MetricListPage.tsx index 1a2b60fea5cc..a33a8d58a2d9 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/MetricsPage/MetricListPage/MetricListPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/MetricsPage/MetricListPage/MetricListPage.tsx @@ -93,6 +93,7 @@ import type { TagLabel } from '../../../generated/type/tagLabel'; import { TagSource } from '../../../generated/type/tagLabel'; import LimitWrapper from '../../../hoc/LimitWrapper'; import { useCurrentUserPreferences } from '../../../hooks/currentUserStore/useCurrentUserStore'; +import { useSavedPageSize } from '../../../hooks/paging/useSavedPageSize'; import { useIsAiMode } from '../../../hooks/useAppMode'; import { useMetricHierarchy } from '../../../hooks/useMetricHierarchy'; import { @@ -123,7 +124,7 @@ import { MetricTreeNode, VisibleMetricTableRow, } from '../../../utils/MetricEntityUtils/MetricHierarchyUtils'; -import { getViewPageSize } from '../../../utils/PaginationUtils'; +import { getViewPageSizeOptions } from '../../../utils/PaginationUtils'; import { getDerivedPermissionFlags } from '../../../utils/PermissionDerivation'; import { DEFAULT_ENTITY_PERMISSION } from '../../../utils/PermissionsUtils'; import { getEntityDetailsPath } from '../../../utils/RouterUtils'; @@ -263,7 +264,9 @@ const MetricListPage = () => { // Owned by TableV2's Customize menu; card view only reads it. const visibleColumns = getVisibleMetricColumns(selectedEntityTableColumns); const [viewMode, setViewMode] = useState(getInitialViewMode); - const metricPageSize = getViewPageSize(viewMode); + const { savedPageSize: metricPageSize } = useSavedPageSize( + getViewPageSizeOptions(viewMode) + ); const [isExporting, setIsExporting] = useState(false); const [isMetricActionsOpen, setIsMetricActionsOpen] = useState(false); const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/PaginationUtils.test.ts b/openmetadata-ui/src/main/resources/ui/src/utils/PaginationUtils.test.ts index 684618362b53..5bdd7c13ee32 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/PaginationUtils.test.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/PaginationUtils.test.ts @@ -3,11 +3,7 @@ import { LIST_PAGE_SIZE_OPTIONS, } from '../constants/constants'; import { PageViewMode } from '../generated/type/personaPreferences'; -import { - computeTotalPages, - getViewPageSize, - getViewPageSizeOptions, -} from './PaginationUtils'; +import { computeTotalPages, getViewPageSizeOptions } from './PaginationUtils'; /* * Copyright 2024 Collate. @@ -71,9 +67,4 @@ describe('view page sizes', () => { ); expect(getViewPageSizeOptions(undefined)).toBe(LIST_PAGE_SIZE_OPTIONS); }); - - it('starts each view on the first step of its scale', () => { - expect(getViewPageSize(PageViewMode.Card)).toBe(12); - expect(getViewPageSize(PageViewMode.Table)).toBe(15); - }); }); diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/PaginationUtils.ts b/openmetadata-ui/src/main/resources/ui/src/utils/PaginationUtils.ts index bcb20eb9c7f3..a577dc9dde86 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/PaginationUtils.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/PaginationUtils.ts @@ -25,6 +25,3 @@ type ViewMode = PageViewMode | `${PageViewMode}`; export const getViewPageSizeOptions = (view?: ViewMode) => view === PageViewMode.Card ? GRID_PAGE_SIZE_OPTIONS : LIST_PAGE_SIZE_OPTIONS; - -export const getViewPageSize = (view?: ViewMode) => - getViewPageSizeOptions(view)[0]; From 51ed4a5337e6198af6d213d58ac845c90ab0f889 Mon Sep 17 00:00:00 2001 From: Sweta Agarwalla Date: Fri, 16 Oct 2026 11:47:39 +0530 Subject: [PATCH 10/27] Keep the pager on an empty later page; fix Sonar issues - Settings Services and App Live Indexing hide the pager only on an empty first page. A later page that comes back empty (a drained retry queue, a stale link) keeps it so the user can page back. - Sonar: name state setters as value/setter pairs (usePaging, useIncidentPaging); NonNullable for TableV2's optional pagination; core Select value/onChange instead of the deprecated selectedKey/onSelectionChange (SchemaTable); drop the redundant click handler and presentation role around TableQueries' date picker, which already opens itself; mark fire-and-forget promises with void (Context Center memories). --- .../SchemaTable/SchemaTable.component.tsx | 4 +-- .../Database/TableQueries/TableQueries.tsx | 7 +---- .../AppLiveIndexing.component.tsx | 5 ++-- .../Settings/Services/Services.test.tsx | 26 +++++++++++++++++++ .../components/Settings/Services/Services.tsx | 4 ++- .../src/components/common/Table/TableV2.tsx | 2 +- .../IncidentGroups/useIncidentPaging.ts | 10 +++---- .../ui/src/hooks/paging/usePaging.ts | 9 ++++--- .../ContextCenterMemoriesPage.tsx | 12 ++++----- 9 files changed, 51 insertions(+), 28 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Database/SchemaTable/SchemaTable.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Database/SchemaTable/SchemaTable.component.tsx index 1e58cf8c71a2..2bb1a6e4c6ac 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Database/SchemaTable/SchemaTable.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Database/SchemaTable/SchemaTable.component.tsx @@ -920,8 +920,8 @@ const SchemaTable = () => { entity: t('label.constraint'), }), })} - selectedKey={editConstraint ?? null} - onSelectionChange={(key) => + value={editConstraint ?? null} + onChange={(key) => setEditConstraint( key === NO_CONSTRAINT_KEY ? undefined : (key as Constraint) ) diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Database/TableQueries/TableQueries.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Database/TableQueries/TableQueries.tsx index be3c709b77fa..e5b796368ea3 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Database/TableQueries/TableQueries.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Database/TableQueries/TableQueries.tsx @@ -630,12 +630,7 @@ const TableQueries: FC = ({ onGetInitialOptions={getInitialTagsOptions} onSearch={handleTagsSearch} /> - { - setIsClickedCalendar(true); - }}> + {t('label.created-date')} { ), }} - // Shown whenever there are records, even a single page of them; hidden when there are none. + // Shown whenever there are records, even a single page of them. Hidden only on an empty first + // page: the retry queue drains, so a later page can come back empty and still needs a way back. pagination={ - isEmpty(records) + isEmpty(records) && currentPage === 1 ? false : { total: paging.total, diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Services.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Services.test.tsx index 107509c62f23..c264a1f246ec 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Services.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Services.test.tsx @@ -391,6 +391,32 @@ describe('Services', () => { (usePaging as jest.Mock).mockReturnValue(pagingState); }); + it('keeps the pager on an empty later page so the user can page back', async () => { + (getServices as jest.Mock).mockImplementationOnce(() => + Promise.resolve({ data: [], paging: { total: 20 } }) + ); + const pagingState = (usePaging as jest.Mock)(); + (usePaging as jest.Mock).mockReturnValue({ + ...pagingState, + currentPage: 2, + }); + + await act(async () => { + render(); + }); + + expect(ListView).toHaveBeenLastCalledWith( + expect.objectContaining({ + customPaginationProps: expect.objectContaining({ + showPagination: true, + }), + }), + expect.anything() + ); + + (usePaging as jest.Mock).mockReturnValue(pagingState); + }); + it('hides the pager when there are no services', async () => { (getServices as jest.Mock).mockImplementationOnce(() => Promise.resolve({ data: [], paging: { total: 0 } }) diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Services.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Services.tsx index 929f3fedf4f1..39a3213eaf2f 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Services.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Services.tsx @@ -380,9 +380,11 @@ const Services = ({ serviceName }: ServicesProps) => { }, [serviceName]); // Shown whenever there are services, even a single page of them, so the page size can be changed. + // Hidden only on an empty first page: an empty later page (a stale link, deleted services) keeps it + // so the user can page back. const customPaginationTableProps = useMemo( () => ({ - showPagination: !isEmpty(serviceDetails), + showPagination: !isEmpty(serviceDetails) || currentPage > 1, currentPage, isLoading, isNumberBased: !isEmpty(searchTerm) || !isEmpty(serviceTypeFilter), diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/Table/TableV2.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/Table/TableV2.tsx index 2c4472f5e031..c03cc0a51059 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/Table/TableV2.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/Table/TableV2.tsx @@ -151,7 +151,7 @@ type CustomPaginationProps = NonNullable< type PaginationContract = | { customPaginationProps?: never; - pagination?: TableComponentProps['pagination']; + pagination?: NonNullable['pagination']>; } | { customPaginationProps: CustomPaginationProps; pagination: false }; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/observability/IncidentManager/IncidentGroups/useIncidentPaging.ts b/openmetadata-ui/src/main/resources/ui/src/components/observability/IncidentManager/IncidentGroups/useIncidentPaging.ts index 380f9c135e76..9ef431de7eb2 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/observability/IncidentManager/IncidentGroups/useIncidentPaging.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/observability/IncidentManager/IncidentGroups/useIncidentPaging.ts @@ -32,12 +32,10 @@ export const useIncidentPaging = ( defaultPageSize?: number ) => { const { savedPageSize, savePageSize } = useSavedPageSize(); - const [pageSize, setPageSizeState] = useState( - defaultPageSize ?? savedPageSize - ); - const setPageSize = useCallback( + const [pageSize, setPageSize] = useState(defaultPageSize ?? savedPageSize); + const changePageSize = useCallback( (size: number) => { - setPageSizeState(size); + setPageSize(size); savePageSize(size); }, [savePageSize] @@ -55,5 +53,5 @@ export const useIncidentPaging = ( [pagedKey] ); - return { currentPage, pageSize, setPageSize, goToPage }; + return { currentPage, pageSize, setPageSize: changePageSize, goToPage }; }; diff --git a/openmetadata-ui/src/main/resources/ui/src/hooks/paging/usePaging.ts b/openmetadata-ui/src/main/resources/ui/src/hooks/paging/usePaging.ts index 1d0f668c9577..97a6592a8edd 100644 --- a/openmetadata-ui/src/main/resources/ui/src/hooks/paging/usePaging.ts +++ b/openmetadata-ui/src/main/resources/ui/src/hooks/paging/usePaging.ts @@ -101,7 +101,8 @@ export const usePaging = ( const [paging, setPaging] = useState(pagingObject); const [currentPage, setCurrentPage] = useState(initialCurrentPage); - const [storedPageSize, setPageSize] = useState(resolvedPageSize); + const [storedPageSize, setStoredPageSize] = + useState(resolvedPageSize); // The sync effect below corrects the stored size a render late. When the options change (a page // switching between its list and grid scale), callers would fetch with the old scale's size in // that render, so hand out the resolved size until the stored one catches up. @@ -117,7 +118,7 @@ export const usePaging = ( setCurrentPage((currentPage) => currentPage === nextCurrentPage ? currentPage : nextCurrentPage ); - setPageSize((pageSize) => + setStoredPageSize((pageSize) => pageSize === resolvedPageSize ? pageSize : resolvedPageSize ); }, [resolvedPageSize, urlParams.currentPage]); @@ -139,7 +140,7 @@ export const usePaging = ( const handlePageSize = useCallback( (page: number) => { - setPageSize(page); + setStoredPageSize(page); savePageSize(page); setCurrentPage(INITIAL_PAGING_VALUE); @@ -151,7 +152,7 @@ export const usePaging = ( cursorValue: null, }); }, - [setPageSize, savePageSize, setCurrentPage, updateUrlParams] + [setStoredPageSize, savePageSize, setCurrentPage, updateUrlParams] ); const paginationVisible = useMemo(() => { diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/ContextCenterPage/ContextCenterMemoriesPage/ContextCenterMemoriesPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/ContextCenterPage/ContextCenterMemoriesPage/ContextCenterMemoriesPage.tsx index d8a7efc700f2..63690a98f4a1 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/ContextCenterPage/ContextCenterMemoriesPage/ContextCenterMemoriesPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/ContextCenterPage/ContextCenterMemoriesPage/ContextCenterMemoriesPage.tsx @@ -704,7 +704,7 @@ const ContextCenterMemoriesPage: FC = () => { t('server.entity-deleted-success', { entity: t('label.memory') }) ); setMemoryToDelete(undefined); - refreshMemoryReviews(); + void refreshMemoryReviews(); await fetchMemories(); await fetchMemoryCounts(); } catch (err) { @@ -748,14 +748,14 @@ const ContextCenterMemoriesPage: FC = () => { ); const handleReviewResolved = useCallback(() => { - refreshMemoryReviews(); - queryClient.invalidateQueries({ + void refreshMemoryReviews(); + void queryClient.invalidateQueries({ queryKey: CONTEXT_CENTER_MEMORIES_COUNT_QUERY_KEY, }); - queryClient.invalidateQueries({ + void queryClient.invalidateQueries({ queryKey: INBOX_OPEN_TASK_COUNT_QUERY_KEY, }); - fetchMemoryCounts(); + void fetchMemoryCounts(); }, [fetchMemoryCounts]); const handleModalClose = useCallback(() => { @@ -809,7 +809,7 @@ const ContextCenterMemoriesPage: FC = () => { const handleModalSuccess = useCallback(() => { handleModalClose(); - refreshMemoryReviews(); + void refreshMemoryReviews(); fetchMemories(); fetchMemoryCounts(); }, [handleModalClose, fetchMemories, fetchMemoryCounts]); From 49f835917480dae6d192bcafbce1e96a120c1a42 Mon Sep 17 00:00:00 2001 From: Sweta Agarwalla Date: Fri, 16 Oct 2026 14:38:01 +0530 Subject: [PATCH 11/27] Address review: KPI pager, view-scale URL sizes, hash size checks - KPIs: show the pager from the server cursors (the fetched page never exceeds the page size, so the old row-count check hid it and the remaining KPIs were unreachable), and refetch from page 1 when the saved size changes. - Domains, Data Products, Sub-domains: useListingData takes the view's page-size options and ignores a URL size the view does not offer, so a table's size=25 no longer fetches 25 cards. - useSavedPageSize.resolvePageSize: a URL or hash size wins only if the picker offers it. Used by Roles, Policies, Notification Alerts and Recent Events, profile Audit Logs, Bots and Personas. - Connections: queryClient.query(...).catch(noop) instead of the deprecated prefetchQuery. - useIncidentPaging drops its test-only defaultPageSize; memories marks its remaining fire-and-forget calls with void. - Tests: useSavedPageSize, KPI pager, off-scale URL size in useListingData, Audit Logs opening at the saved size. --- .../DataProduct/DataProductListPage.tsx | 4 +- .../hooks/useDataProductListingData.tsx | 3 + .../SubDomainsTable.component.tsx | 4 +- .../hooks/useSubdomainListingData.tsx | 3 + .../DomainListing/DomainListPage.tsx | 4 +- .../hooks/useDomainListingData.tsx | 8 +- .../compositions/useListingData.test.tsx | 35 +++++++ .../atoms/compositions/useListingData.tsx | 14 ++- .../domain/compositions/useDomainListing.tsx | 3 + .../AccessControlAuditLogsPanel.tsx | 9 +- .../AccessControlPoliciesPanel.tsx | 4 +- .../AccessControlRolesPanel.tsx | 4 +- .../Profile/tabs/bots/BotsListPanel.tsx | 4 +- .../notification/NotificationAlertsPanel.tsx | 4 +- .../notification/NotificationRecentEvents.tsx | 4 +- .../Profile/tabs/personas/PersonasLanding.tsx | 7 +- .../ConnectionsPage/useConnectionsData.ts | 8 +- .../useIncidentGroupIncidents.test.tsx | 10 +- .../useIncidentGroupIncidents.ts | 6 +- .../IncidentGroups/useIncidentPaging.test.ts | 13 +-- .../IncidentGroups/useIncidentPaging.ts | 11 +-- .../src/hooks/paging/useSavedPageSize.test.ts | 92 +++++++++++++++++++ .../ui/src/hooks/paging/useSavedPageSize.ts | 12 ++- .../AuditLogsPage/AuditLogsPage.test.tsx | 29 ++++++ .../ContextCenterMemoriesPage.tsx | 4 +- .../pages/DataInsightPage/KPIList.test.tsx | 24 +++++ .../ui/src/pages/DataInsightPage/KPIList.tsx | 8 +- 27 files changed, 274 insertions(+), 57 deletions(-) create mode 100644 openmetadata-ui/src/main/resources/ui/src/hooks/paging/useSavedPageSize.test.ts diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataProduct/DataProductListPage.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataProduct/DataProductListPage.tsx index 3c3c93a73555..cb0e7be08dba 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataProduct/DataProductListPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataProduct/DataProductListPage.tsx @@ -158,12 +158,14 @@ const DataProductListPage = ({ const personaView = usePersonaViewMode(VIEW_MODE_PAGE.DataProducts); const [selectedView, setView] = useState(); const view = selectedView ?? personaView; + const listingPageSizeOptions = getViewPageSizeOptions(view); const { savedPageSize: listingPageSize } = useSavedPageSize( - getViewPageSizeOptions(view) + listingPageSizeOptions ); const dataProductListing = useDataProductListingData({ enableNlq: isAiMode, pageSize: listingPageSize, + pageSizeOptions: listingPageSizeOptions, }); // Card and table views page at different sizes, so the current page may not exist in the other. const { handlePageChange } = dataProductListing; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataProduct/hooks/useDataProductListingData.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataProduct/hooks/useDataProductListingData.tsx index 902d66a284c1..bef03adeb2d9 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataProduct/hooks/useDataProductListingData.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataProduct/hooks/useDataProductListingData.tsx @@ -34,9 +34,11 @@ import { export const useDataProductListingData = ({ enableNlq, pageSize = PAGE_SIZE_BASE, + pageSizeOptions, }: { enableNlq?: boolean; pageSize?: number; + pageSizeOptions?: number[]; } = {}): ListingData => { const { dataProductBasePath } = useMarketplaceStore(); // Memoized because a ClassBase override may build these per call, and @@ -122,6 +124,7 @@ export const useDataProductListingData = ({ searchIndex: SearchIndex.DATA_PRODUCT, baseFilter: '', // No parent filter for data products pageSize, + pageSizeOptions, filterKeys, filterConfigs, columns, diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Domain/SubDomainsTable/SubDomainsTable.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Domain/SubDomainsTable/SubDomainsTable.component.tsx index 9c2f45707808..5781bc4510d4 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Domain/SubDomainsTable/SubDomainsTable.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Domain/SubDomainsTable/SubDomainsTable.component.tsx @@ -52,12 +52,14 @@ const SubDomainsTable = ({ const personaView = usePersonaViewMode(VIEW_MODE_PAGE.SubDomains); const [selectedView, setView] = useState(); const view = selectedView ?? personaView; + const listingPageSizeOptions = getViewPageSizeOptions(view); const { savedPageSize: listingPageSize } = useSavedPageSize( - getViewPageSizeOptions(view) + listingPageSizeOptions ); const subdomainListing = useSubdomainListingData({ parentDomainFqn: domainFqn, pageSize: listingPageSize, + pageSizeOptions: listingPageSizeOptions, }); // Card and table views page at different sizes, so the current page may not exist in the other. const { handlePageChange } = subdomainListing; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Domain/SubDomainsTable/hooks/useSubdomainListingData.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Domain/SubDomainsTable/hooks/useSubdomainListingData.tsx index 3b30ea63c54e..29f1d178bd5a 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Domain/SubDomainsTable/hooks/useSubdomainListingData.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Domain/SubDomainsTable/hooks/useSubdomainListingData.tsx @@ -19,11 +19,13 @@ import { useMarketplaceStore } from '../../../../hooks/useMarketplaceStore'; interface UseSubdomainListingDataProps { parentDomainFqn: string; pageSize?: number; + pageSizeOptions?: number[]; } export const useSubdomainListingData = ({ parentDomainFqn, pageSize, + pageSizeOptions, }: UseSubdomainListingDataProps): ListingData => { const { domainBasePath } = useMarketplaceStore(); @@ -48,5 +50,6 @@ export const useSubdomainListingData = ({ isSubDomain: true, searchKey: 'sub-domain', pageSize, + pageSizeOptions, }); }; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DomainListing/DomainListPage.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DomainListing/DomainListPage.tsx index cf1f611e033c..8a97032f068a 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DomainListing/DomainListPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DomainListing/DomainListPage.tsx @@ -64,12 +64,14 @@ const DomainListPage = ({ renderPageHeader }: DomainListPageProps) => { const personaView = usePersonaViewMode(VIEW_MODE_PAGE.Domains); const [selectedView, setView] = useState(); const view = selectedView ?? personaView; + const listingPageSizeOptions = getViewPageSizeOptions(view); const { savedPageSize: listingPageSize } = useSavedPageSize( - getViewPageSizeOptions(view) + listingPageSizeOptions ); const domainListing = useDomainListingData({ enableNlq: isAiMode, pageSize: listingPageSize, + pageSizeOptions: listingPageSizeOptions, }); // Card and table views page at different sizes, so the current page may not exist in the other. const { handlePageChange } = domainListing; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DomainListing/hooks/useDomainListingData.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DomainListing/hooks/useDomainListingData.tsx index d3a2ce7ef693..4afb8eff73ea 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DomainListing/hooks/useDomainListingData.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DomainListing/hooks/useDomainListingData.tsx @@ -22,7 +22,12 @@ import { QueryFieldInterface } from '../../../pages/ExplorePage/ExplorePage.inte export const useDomainListingData = ({ enableNlq, pageSize, -}: { enableNlq?: boolean; pageSize?: number } = {}): ListingData => { + pageSizeOptions, +}: { + enableNlq?: boolean; + pageSize?: number; + pageSizeOptions?: number[]; +} = {}): ListingData => { const { domainBasePath } = useMarketplaceStore(); const { userDomains, isDomainRestricted } = useDomainStore(); @@ -65,5 +70,6 @@ export const useDomainListingData = ({ basePath: domainBasePath, enableNlq, pageSize, + pageSizeOptions, }); }; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/atoms/compositions/useListingData.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/atoms/compositions/useListingData.test.tsx index 6cc7717522e8..885a320deacc 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/atoms/compositions/useListingData.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/atoms/compositions/useListingData.test.tsx @@ -234,6 +234,41 @@ describe('useListingData', () => { }); describe('effectivePageSize fallback', () => { + it('ignores a URL size the current view does not offer', () => { + // `size=25` was left by the table view; the card view pages on 12/24/48. + mockUseUrlState.mockReturnValue({ + urlState: { + searchQuery: '', + filters: {}, + currentPage: 1, + pageSize: 25, + }, + parsedFilters: [], + setSearchQuery: mockSetSearchQuery, + setFilters: mockSetFilters, + setCurrentPage: mockSetCurrentPage, + setPageSize: mockSetPageSize, + resetFilters: jest.fn(), + resetAll: jest.fn(), + }); + + renderHook( + () => + useListingData({ + searchIndex: SearchIndex.TABLE, + filterKeys: [], + columns: [], + pageSize: 12, + pageSizeOptions: [12, 24, 48], + }), + { wrapper } + ); + + expect(mockUseDataFetching).toHaveBeenLastCalledWith( + expect.objectContaining({ pageSize: 12 }) + ); + }); + it('should use urlState.pageSize when available', () => { mockUseUrlState.mockReturnValueOnce({ urlState: { diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/atoms/compositions/useListingData.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/atoms/compositions/useListingData.tsx index 179791a3154f..aed43bd20d99 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/atoms/compositions/useListingData.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/atoms/compositions/useListingData.tsx @@ -29,6 +29,8 @@ interface UseListingDataProps { searchIndex: SearchIndex; baseFilter?: string; pageSize?: number; + /** The sizes the current view offers; a URL size outside them falls back to `pageSize`. */ + pageSizeOptions?: number[]; filterKeys: string[]; filterConfigs?: ExploreQuickFilterField[]; columns: ColumnConfig[]; @@ -52,6 +54,7 @@ export const useListingData = < searchIndex, baseFilter = '', pageSize = PAGE_SIZE_BASE, + pageSizeOptions, filterKeys, filterConfigs, columns, @@ -86,7 +89,12 @@ export const useListingData = < resetAll, } = urlStateHook; - const effectivePageSize = urlState.pageSize || pageSize; + // A size left in the URL by the other view's scale (25 from the table on the card view) is not + // one this view offers, so it falls back to the view's own size. + const effectivePageSize = + pageSizeOptions && !pageSizeOptions.includes(urlState.pageSize) + ? pageSize + : urlState.pageSize || pageSize; const dataFetching = useDataFetching({ searchIndex, @@ -131,7 +139,7 @@ export const useListingData = < urlState.currentPage, urlState.searchQuery, urlState.filters, - urlState.pageSize, + effectivePageSize, ]); if (signature === lastFetchedRef.current) { return; @@ -147,7 +155,7 @@ export const useListingData = < urlState.currentPage, urlState.searchQuery, urlState.filters, - urlState.pageSize, + effectivePageSize, // `useNlq` is left out on purpose: flipping the NLQ toggle must not run a // query by itself. As on Explore, NLQ runs when the user submits. isRouteVisible, diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/atoms/domain/compositions/useDomainListing.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/atoms/domain/compositions/useDomainListing.tsx index 1be112349b65..1ffce40d2fd4 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/atoms/domain/compositions/useDomainListing.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/atoms/domain/compositions/useDomainListing.tsx @@ -27,6 +27,7 @@ interface UseDomainListingConfig { enableNlq?: boolean; nameLabelKey?: string; pageSize?: number; + pageSizeOptions?: number[]; basePath?: string; includeOwners?: boolean; includeGlossaryTerms?: boolean; @@ -47,6 +48,7 @@ export const useDomainListing = ( enableNlq = false, nameLabelKey = 'label.domain', pageSize = PAGE_SIZE_BASE, + pageSizeOptions, basePath = '/domain', includeOwners = true, includeGlossaryTerms = true, @@ -103,6 +105,7 @@ export const useDomainListing = ( searchIndex: SearchIndex.DOMAIN, baseFilter, pageSize, + pageSizeOptions, filterKeys, filterConfigs, columns, diff --git a/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/Profile/tabs/access-control/AccessControlAuditLogsPanel.tsx b/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/Profile/tabs/access-control/AccessControlAuditLogsPanel.tsx index 5bccccbef9ad..73e5eecaa1e1 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/Profile/tabs/access-control/AccessControlAuditLogsPanel.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/Profile/tabs/access-control/AccessControlAuditLogsPanel.tsx @@ -128,11 +128,6 @@ async function walkToPageCursor( return { cursor, discoveredCursors }; } -// A size in the hash (a shared or reloaded link) wins over the size the user last picked. -function resolvePageSize(hash: number | undefined, saved: number): number { - return hash || saved; -} - interface AccessControlAuditLogsPanelProps { /** Callback to inject action buttons into the page header. */ onSetHeaderActions?: (actions: React.ReactNode) => void; @@ -151,8 +146,8 @@ const AccessControlAuditLogsPanel: React.FC< cursorType: hashCursorType, setPage: setHashPage, } = useHashPagingParams(); - const { savedPageSize, savePageSize } = useSavedPageSize(); - const pageSize = resolvePageSize(hashPageSize, savedPageSize); + const { savePageSize, resolvePageSize } = useSavedPageSize(); + const pageSize = resolvePageSize(hashPageSize); const [logs, setLogs] = useState([]); const [paging, setPaging] = useState(INITIAL_PAGING); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/Profile/tabs/access-control/AccessControlPoliciesPanel.tsx b/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/Profile/tabs/access-control/AccessControlPoliciesPanel.tsx index b92aa223dc99..47da51268ab6 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/Profile/tabs/access-control/AccessControlPoliciesPanel.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/Profile/tabs/access-control/AccessControlPoliciesPanel.tsx @@ -88,8 +88,8 @@ const AccessControlPoliciesPanel: React.FC = ({ cursorType: hashCursorType, setPage: setHashPage, } = useHashPagingParams(); - const { savedPageSize, savePageSize } = useSavedPageSize(); - const pageSize = hashPageSize || savedPageSize; + const { savePageSize, resolvePageSize } = useSavedPageSize(); + const pageSize = resolvePageSize(hashPageSize); const [policies, setPolicies] = useState([]); const [isLoading, setIsLoading] = useState(true); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/Profile/tabs/access-control/AccessControlRolesPanel.tsx b/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/Profile/tabs/access-control/AccessControlRolesPanel.tsx index daa4d179e1f7..ad9ed0a032fb 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/Profile/tabs/access-control/AccessControlRolesPanel.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/Profile/tabs/access-control/AccessControlRolesPanel.tsx @@ -86,8 +86,8 @@ const AccessControlRolesPanel: React.FC = ({ cursorType: hashCursorType, setPage: setHashPage, } = useHashPagingParams(); - const { savedPageSize, savePageSize } = useSavedPageSize(); - const pageSize = hashPageSize || savedPageSize; + const { savePageSize, resolvePageSize } = useSavedPageSize(); + const pageSize = resolvePageSize(hashPageSize); const [roles, setRoles] = useState([]); const [isLoading, setIsLoading] = useState(false); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/Profile/tabs/bots/BotsListPanel.tsx b/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/Profile/tabs/bots/BotsListPanel.tsx index 079fcc7eaa98..8cece3b4423c 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/Profile/tabs/bots/BotsListPanel.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/Profile/tabs/bots/BotsListPanel.tsx @@ -104,9 +104,9 @@ const BotsListPanel: React.FC = ({ const [currentPage, setCurrentPage] = useState( Number(hashState.params.page) || 1 ); - const { savedPageSize, savePageSize } = useSavedPageSize(); + const { savePageSize, resolvePageSize } = useSavedPageSize(); const [pageSize, setPageSize] = useState( - Number(hashState.params.pageSize) || savedPageSize + resolvePageSize(Number(hashState.params.pageSize)) ); const [reloadKey, setReloadKey] = useState(0); const fetchRequestIdRef = useRef(0); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/Profile/tabs/notification/NotificationAlertsPanel.tsx b/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/Profile/tabs/notification/NotificationAlertsPanel.tsx index 82865f5dab89..1851c4572b77 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/Profile/tabs/notification/NotificationAlertsPanel.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/Profile/tabs/notification/NotificationAlertsPanel.tsx @@ -84,8 +84,8 @@ const NotificationAlertsPanel: React.FC = ({ cursorType: hashCursorType, setPage: setHashPage, } = useHashPagingParams(); - const { savedPageSize, savePageSize } = useSavedPageSize(); - const pageSize = hashPageSize || savedPageSize; + const { savePageSize, resolvePageSize } = useSavedPageSize(); + const pageSize = resolvePageSize(hashPageSize); const [alerts, setAlerts] = useState([]); const [isLoading, setIsLoading] = useState(false); const [selectedAlert, setSelectedAlert] = useState(); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/Profile/tabs/notification/NotificationRecentEvents.tsx b/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/Profile/tabs/notification/NotificationRecentEvents.tsx index 5f74a89851a6..420f189a0dff 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/Profile/tabs/notification/NotificationRecentEvents.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/Profile/tabs/notification/NotificationRecentEvents.tsx @@ -89,8 +89,8 @@ function NotificationRecentEvents({ ); const { state: hashState, updateParams } = useSettingsHash(); const initialPage = Number(hashState.params.page) || 1; - const { savedPageSize, savePageSize } = useSavedPageSize(); - const initialPageSize = Number(hashState.params.pageSize) || savedPageSize; + const { savePageSize, resolvePageSize } = useSavedPageSize(); + const initialPageSize = resolvePageSize(Number(hashState.params.pageSize)); const [currentPage, setCurrentPage] = useState(initialPage); const [pageSize, setPageSize] = useState(initialPageSize); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/Profile/tabs/personas/PersonasLanding.tsx b/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/Profile/tabs/personas/PersonasLanding.tsx index d05664dfb97b..49cff66b10fd 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/Profile/tabs/personas/PersonasLanding.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/Profile/tabs/personas/PersonasLanding.tsx @@ -51,13 +51,10 @@ const PersonasLanding = ({ onNavigate }: PersonasLandingProps) => { setPage: setHashPage, } = useHashPagingParams(); // Personas render as a card grid, so they page on the grid scale. - const { savedPageSize, savePageSize } = useSavedPageSize( + const { savePageSize, resolvePageSize } = useSavedPageSize( GRID_PAGE_SIZE_OPTIONS ); - const pageSize = - hashPageSize && GRID_PAGE_SIZE_OPTIONS.includes(hashPageSize) - ? hashPageSize - : savedPageSize; + const pageSize = resolvePageSize(hashPageSize); const [personas, setPersonas] = useState([]); const [isLoading, setIsLoading] = useState(false); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/integration/ConnectionsPage/useConnectionsData.ts b/openmetadata-ui/src/main/resources/ui/src/components/integration/ConnectionsPage/useConnectionsData.ts index ef795992b6a5..bc3d470a5b02 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/integration/ConnectionsPage/useConnectionsData.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/integration/ConnectionsPage/useConnectionsData.ts @@ -16,7 +16,7 @@ import { useQuery, useQueryClient, } from '@tanstack/react-query'; -import { debounce } from 'lodash'; +import { debounce, noop } from 'lodash'; import { useEffect, useMemo, useRef, useState } from 'react'; import { LIST_PAGE_SIZE_OPTIONS } from '../../../constants/constants'; import { @@ -387,9 +387,9 @@ export const useConnectionsData = ({ return; } - void queryClient.prefetchQuery( - getServicesOverviewQueryOptions({ ...ESTATE_PARAMS, include }) - ); + queryClient + .query(getServicesOverviewQueryOptions({ ...ESTATE_PARAMS, include })) + .catch(noop); }); const toggleSortOrder = () => { diff --git a/openmetadata-ui/src/main/resources/ui/src/components/observability/IncidentManager/IncidentGroups/useIncidentGroupIncidents.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/observability/IncidentManager/IncidentGroups/useIncidentGroupIncidents.test.tsx index a046f2f61dee..bfe3e9440da2 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/observability/IncidentManager/IncidentGroups/useIncidentGroupIncidents.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/observability/IncidentManager/IncidentGroups/useIncidentGroupIncidents.test.tsx @@ -32,6 +32,15 @@ jest.mock('../../../../rest/incidentManagerAPI', () => ({ getListTestCaseIncidentStatus: jest.fn(), })); +// Small pages keep the paging cases short; the real hook reads the user's saved list size. +jest.mock('../../../../hooks/paging/useSavedPageSize', () => ({ + useSavedPageSize: () => ({ + savedPageSize: 4, + savePageSize: jest.fn(), + resolvePageSize: jest.fn(), + }), +})); + jest.mock('../../../../utils/ToastUtils', () => ({ showErrorToast: jest.fn(), })); @@ -68,7 +77,6 @@ const renderIncidents = (group?: TestCaseIncidentGroup) => useIncidentGroupIncidents({ group: current, filters: FILTERS, - defaultPageSize: 4, }), { initialProps: { current: group }, wrapper: createQueryWrapper() } ); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/observability/IncidentManager/IncidentGroups/useIncidentGroupIncidents.ts b/openmetadata-ui/src/main/resources/ui/src/components/observability/IncidentManager/IncidentGroups/useIncidentGroupIncidents.ts index 6cb9d5ec7dc6..d1ecba42d7d1 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/observability/IncidentManager/IncidentGroups/useIncidentGroupIncidents.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/observability/IncidentManager/IncidentGroups/useIncidentGroupIncidents.ts @@ -32,8 +32,6 @@ interface UseIncidentGroupIncidentsProps { group?: TestCaseIncidentGroup; /** The filters the group was fetched with, so the list matches its count. */ filters: IncidentGroupFilters; - /** Overrides the size the user last picked; used by tests that page small sets. */ - defaultPageSize?: number; } const NO_INCIDENTS: TestCaseResolutionStatus[] = []; @@ -50,7 +48,6 @@ const NO_INCIDENTS: TestCaseResolutionStatus[] = []; export const useIncidentGroupIncidents = ({ group, filters, - defaultPageSize, }: UseIncidentGroupIncidentsProps) => { const { activeDomain } = useDomainStore(); const domain = @@ -59,8 +56,7 @@ export const useIncidentGroupIncidents = ({ ? `${group.groupBy}|${getIncidentGroupKey(group)}` : ''; const { currentPage, pageSize, setPageSize, goToPage } = useIncidentPaging( - `${groupKey}|${domain}|${JSON.stringify(filters)}`, - defaultPageSize + `${groupKey}|${domain}|${JSON.stringify(filters)}` ); // The group object changes with every re-read of the groups; the read is // keyed on its identity, and takes the latest object when it runs. diff --git a/openmetadata-ui/src/main/resources/ui/src/components/observability/IncidentManager/IncidentGroups/useIncidentPaging.test.ts b/openmetadata-ui/src/main/resources/ui/src/components/observability/IncidentManager/IncidentGroups/useIncidentPaging.test.ts index 3f2d931f31f0..db26db0f7517 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/observability/IncidentManager/IncidentGroups/useIncidentPaging.test.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/observability/IncidentManager/IncidentGroups/useIncidentPaging.test.ts @@ -15,15 +15,16 @@ import { act, renderHook } from '@testing-library/react'; import { useIncidentPaging } from './useIncidentPaging'; describe('useIncidentPaging', () => { - it('should start on the first page in the default size', () => { - const { result } = renderHook(() => useIncidentPaging('query', 10)); + it('should start on the first page at the saved page size', () => { + const { result } = renderHook(() => useIncidentPaging('query')); + // No size saved yet, so the list scale's first step. expect(result.current.currentPage).toBe(1); - expect(result.current.pageSize).toBe(10); + expect(result.current.pageSize).toBe(15); }); it('should go straight to any page asked for', () => { - const { result } = renderHook(() => useIncidentPaging('query', 10)); + const { result } = renderHook(() => useIncidentPaging('query')); act(() => result.current.goToPage(7)); @@ -36,7 +37,7 @@ describe('useIncidentPaging', () => { it('should start over when the query changes', () => { const { result, rerender } = renderHook( - ({ queryKey }) => useIncidentPaging(queryKey, 10), + ({ queryKey }) => useIncidentPaging(queryKey), { initialProps: { queryKey: 'query' } } ); @@ -47,7 +48,7 @@ describe('useIncidentPaging', () => { }); it('should start over in the new size when the page size changes', () => { - const { result } = renderHook(() => useIncidentPaging('query', 10)); + const { result } = renderHook(() => useIncidentPaging('query')); act(() => result.current.goToPage(3)); act(() => result.current.setPageSize(25)); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/observability/IncidentManager/IncidentGroups/useIncidentPaging.ts b/openmetadata-ui/src/main/resources/ui/src/components/observability/IncidentManager/IncidentGroups/useIncidentPaging.ts index 9ef431de7eb2..310f6735c8f8 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/observability/IncidentManager/IncidentGroups/useIncidentPaging.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/observability/IncidentManager/IncidentGroups/useIncidentPaging.ts @@ -24,15 +24,12 @@ interface PageState { * Page state for the incident listings, which take a 1-based page. Any change * to the query — or to the page size — reads as the first page again. That * reset is derived rather than set from an effect, so the stale page is never - * fetched on the way. The page size starts from `defaultPageSize` when given, - * otherwise from the size the user last picked on the list scale. + * fetched on the way. The page size starts from the size the user last picked + * on the list scale. */ -export const useIncidentPaging = ( - queryKey: string, - defaultPageSize?: number -) => { +export const useIncidentPaging = (queryKey: string) => { const { savedPageSize, savePageSize } = useSavedPageSize(); - const [pageSize, setPageSize] = useState(defaultPageSize ?? savedPageSize); + const [pageSize, setPageSize] = useState(savedPageSize); const changePageSize = useCallback( (size: number) => { setPageSize(size); diff --git a/openmetadata-ui/src/main/resources/ui/src/hooks/paging/useSavedPageSize.test.ts b/openmetadata-ui/src/main/resources/ui/src/hooks/paging/useSavedPageSize.test.ts new file mode 100644 index 000000000000..66924c4c853e --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/hooks/paging/useSavedPageSize.test.ts @@ -0,0 +1,92 @@ +/* + * Copyright 2026 Collate. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { act, renderHook } from '@testing-library/react'; +import { + GRID_PAGE_SIZE_OPTIONS, + LIST_PAGE_SIZE_OPTIONS, +} from '../../constants/constants'; +import { usePersistentStorage } from '../currentUserStore/useCurrentUserStore'; +import { useSavedPageSize } from './useSavedPageSize'; + +// Exercises the real preferences store; only the current user is stubbed. +jest.mock('../useApplicationStore', () => ({ + useApplicationStore: jest.fn((selector) => + selector({ currentUser: { name: 'saved-size-user' } }) + ), +})); + +const savePreference = (preference: { + globalPageSize?: number; + globalGridPageSize?: number; +}) => + usePersistentStorage + .getState() + .setUserPreference('saved-size-user', preference); + +describe('useSavedPageSize', () => { + beforeEach(() => + usePersistentStorage.getState().clearUserPreference('saved-size-user') + ); + + it('starts each scale at its first step when nothing is saved', () => { + const list = renderHook(() => useSavedPageSize()); + const grid = renderHook(() => useSavedPageSize(GRID_PAGE_SIZE_OPTIONS)); + + expect(list.result.current.savedPageSize).toBe(15); + expect(grid.result.current.savedPageSize).toBe(12); + }); + + it('reads each scale from its own preference', () => { + savePreference({ globalPageSize: 50, globalGridPageSize: 24 }); + + const list = renderHook(() => useSavedPageSize(LIST_PAGE_SIZE_OPTIONS)); + const grid = renderHook(() => useSavedPageSize(GRID_PAGE_SIZE_OPTIONS)); + + expect(list.result.current.savedPageSize).toBe(50); + expect(grid.result.current.savedPageSize).toBe(24); + }); + + it('falls back to the first step when an older build saved an off-scale size', () => { + savePreference({ globalPageSize: 10 }); + + const { result } = renderHook(() => useSavedPageSize()); + + expect(result.current.savedPageSize).toBe(15); + }); + + it('saves a pick to the preference of the scale it belongs to', () => { + const { result } = renderHook(() => useSavedPageSize()); + + act(() => result.current.savePageSize(24)); + act(() => result.current.savePageSize(50)); + + const saved = + usePersistentStorage.getState().preferences['saved-size-user']; + + expect(saved.globalGridPageSize).toBe(24); + expect(saved.globalPageSize).toBe(50); + }); + + it('lets a size from the URL or hash win only when the picker offers it', () => { + savePreference({ globalPageSize: 25 }); + + const { result } = renderHook(() => useSavedPageSize()); + + // A shared link's size is honoured... + expect(result.current.resolvePageSize(50)).toBe(50); + // ...but an old link's 10, a grid's 24, or no size at all use the saved size. + expect(result.current.resolvePageSize(10)).toBe(25); + expect(result.current.resolvePageSize(24)).toBe(25); + expect(result.current.resolvePageSize(undefined)).toBe(25); + }); +}); diff --git a/openmetadata-ui/src/main/resources/ui/src/hooks/paging/useSavedPageSize.ts b/openmetadata-ui/src/main/resources/ui/src/hooks/paging/useSavedPageSize.ts index 8f0a9ecae299..f75b8f53149c 100644 --- a/openmetadata-ui/src/main/resources/ui/src/hooks/paging/useSavedPageSize.ts +++ b/openmetadata-ui/src/main/resources/ui/src/hooks/paging/useSavedPageSize.ts @@ -51,5 +51,15 @@ export const useSavedPageSize = ( [setPreference] ); - return { savedPageSize, savePageSize }; + // A size from the URL or settings hash (a shared or reloaded link) wins, but only if this + // picker offers it: an old link's 10, or a grid's 24 on a list, falls back to the saved size. + const resolvePageSize = useCallback( + (requested?: number) => + requested && pageSizeOptions.includes(requested) + ? requested + : savedPageSize, + [pageSizeOptions, savedPageSize] + ); + + return { savedPageSize, savePageSize, resolvePageSize }; }; diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/AuditLogsPage/AuditLogsPage.test.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/AuditLogsPage/AuditLogsPage.test.tsx index 33b235397ef7..695ba978d7bd 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/AuditLogsPage/AuditLogsPage.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/AuditLogsPage/AuditLogsPage.test.tsx @@ -22,6 +22,9 @@ import { import { AxiosError } from 'axios'; import { MemoryRouter } from 'react-router-dom'; import { SOCKET_EVENTS } from '../../constants/constants'; +import { User } from '../../generated/entity/teams/user'; +import { usePersistentStorage } from '../../hooks/currentUserStore/useCurrentUserStore'; +import { useApplicationStore } from '../../hooks/useApplicationStore'; import { exportAuditLogs, getAuditLogExportJob, @@ -336,6 +339,32 @@ describe('AuditLogsPage', () => { await waitFor(() => expect(mockGetAuditLogs).toHaveBeenCalledTimes(1)); }); + it('starts from the page size the user last picked', async () => { + // Real stores: a signed-in user whose saved list size is 50, not the page's old fixed 25. + const previousUser = useApplicationStore.getState().currentUser; + useApplicationStore.setState({ + currentUser: { ...previousUser, name: 'audit-user' } as User, + }); + usePersistentStorage + .getState() + .setUserPreference('audit-user', { globalPageSize: 50 }); + + render( + + + + ); + + await waitFor(() => + expect(mockGetAuditLogs).toHaveBeenCalledWith( + expect.objectContaining({ limit: 50 }) + ) + ); + + usePersistentStorage.getState().clearUserPreference('audit-user'); + useApplicationStore.setState({ currentUser: previousUser }); + }); + it('displays loading state initially', async () => { mockGetAuditLogs.mockImplementation( () => diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/ContextCenterPage/ContextCenterMemoriesPage/ContextCenterMemoriesPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/ContextCenterPage/ContextCenterMemoriesPage/ContextCenterMemoriesPage.tsx index 63690a98f4a1..6f852a030bb0 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/ContextCenterPage/ContextCenterMemoriesPage/ContextCenterMemoriesPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/ContextCenterPage/ContextCenterMemoriesPage/ContextCenterMemoriesPage.tsx @@ -810,8 +810,8 @@ const ContextCenterMemoriesPage: FC = () => { const handleModalSuccess = useCallback(() => { handleModalClose(); void refreshMemoryReviews(); - fetchMemories(); - fetchMemoryCounts(); + void fetchMemories(); + void fetchMemoryCounts(); }, [handleModalClose, fetchMemories, fetchMemoryCounts]); const countCards = useMemo( diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/DataInsightPage/KPIList.test.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/DataInsightPage/KPIList.test.tsx index 53f5a539ad2b..cbd12d94327d 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/DataInsightPage/KPIList.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/DataInsightPage/KPIList.test.tsx @@ -16,6 +16,7 @@ import React from 'react'; import { MemoryRouter } from 'react-router-dom'; import { act } from 'react-test-renderer'; import { mockUserData } from '../../components/Settings/Users/mocks/User.mocks'; +import { getListKPIs } from '../../rest/KpiAPI'; import KPIList from './KPIList'; import { KPI_DATA } from './mocks/KPIList'; @@ -90,6 +91,29 @@ jest.mock('../../context/PermissionProvider/PermissionProvider', () => ({ })); describe('KPI list component', () => { + it('asks for the saved page size and shows the pager when the server has more KPIs', async () => { + (getListKPIs as jest.Mock).mockResolvedValueOnce({ + data: KPI_DATA, + paging: { after: 'next-page', total: 20 }, + }); + + render(, { wrapper: MemoryRouter }); + + // The fetched page never holds more rows than the page size, so only the cursor can say a + // second page exists. + expect(await screen.findByTestId('next')).toBeInTheDocument(); + expect(getListKPIs).toHaveBeenCalledWith( + expect.objectContaining({ limit: 15 }) + ); + }); + + it('hides the pager when every KPI fits on one page', async () => { + render(, { wrapper: MemoryRouter }); + + expect(await screen.findByText('Description KPI')).toBeInTheDocument(); + expect(screen.queryByTestId('next')).not.toBeInTheDocument(); + }); + it('Should render the kpi list', async () => { render(, { wrapper: MemoryRouter }); diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/DataInsightPage/KPIList.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/DataInsightPage/KPIList.tsx index 2b2065f0b0e1..9c65b64b06c5 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/DataInsightPage/KPIList.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/DataInsightPage/KPIList.tsx @@ -206,9 +206,11 @@ const KPIList = () => { } }; + // Refetches from page 1 when the saved page size changes; the page can stay mounted in app mode. useEffect(() => { + setKpiPage(INITIAL_PAGING_VALUE); fetchKpiList(); - }, []); + }, [kpiPageSize]); const handleAfterDeleteAction = useCallback(() => { fetchKpiList(); @@ -252,7 +254,9 @@ const KPIList = () => { customPaginationProps={{ currentPage: kpiPage, isLoading, - showPagination: kpiList.length > kpiPageSize, + // The fetch asks for one page, so the row count never exceeds it; the cursors tell whether + // there are more KPIs to page to. + showPagination: Boolean(kpiPaging.after || kpiPaging.before), pageSize: kpiPageSize, paging: kpiPaging, pagingHandler: kpiPagingHandler, From 82b5ba81b5551d95376261bcb6c12b0818e6fb74 Mon Sep 17 00:00:00 2001 From: Sweta Agarwalla Date: Fri, 16 Oct 2026 15:11:54 +0530 Subject: [PATCH 12/27] Drop a link's page size when a page switches between table and cards Grid and list views page on different scales, so a size carried in the URL applies only to the view it was opened in. A view switch now drops the page and page size from the URL and the new view (and any later switch back) starts at the saved size for its scale. - useUrlState.resetPaging clears page and size in one update; Domains, Data Products and Sub-domains call it on a view switch. - Connections drops currentPage, pageSize and the cursor params when the grid/list view changes. - Record the rule in the page-size ADR; tests for resetPaging and the Connections switch. --- ...age-sizes-lists-15-25-50-grids-12-24-48.md | 7 ++++-- .../DataProduct/DataProductListPage.tsx | 9 +++---- .../SubDomainsTable.component.tsx | 9 +++---- .../DomainListing/DomainListPage.tsx | 9 +++---- .../compositions/useListingData.test.tsx | 5 ++++ .../atoms/compositions/useListingData.tsx | 2 ++ .../common/atoms/data/useUrlState.test.tsx | 24 +++++++++++++++++++ .../common/atoms/data/useUrlState.tsx | 5 ++++ .../components/common/atoms/shared/types.ts | 4 ++++ .../useConnectionsViewMode.test.ts | 21 ++++++++++++++++ .../ConnectionsPage/useConnectionsViewMode.ts | 6 +++++ 11 files changed, 87 insertions(+), 14 deletions(-) diff --git a/docs/decisions/2026-10-13-ui-page-sizes-lists-15-25-50-grids-12-24-48.md b/docs/decisions/2026-10-13-ui-page-sizes-lists-15-25-50-grids-12-24-48.md index c2c422fb7788..91aa7d145976 100644 --- a/docs/decisions/2026-10-13-ui-page-sizes-lists-15-25-50-grids-12-24-48.md +++ b/docs/decisions/2026-10-13-ui-page-sizes-lists-15-25-50-grids-12-24-48.md @@ -30,8 +30,11 @@ matching entry. - Every paged view starts from that saved size; no page sets a fixed starting size of its own. This includes views with no size picker (Metrics, KPIs, Domains, Data Products, reindex failures, Context Center memories). A size carried in the URL (`pageSize` param or settings - hash) still wins, so a shared link opens at its own size. Fixed fetch batches that are not a - paged view (e.g. loading all glossary terms, approval tasks) are out of scope. + hash) still wins, so a shared link opens at its own size, but only for the view it was opened + in: a table/card switch drops it from the URL, and the new view (and any later switch back) + starts at the saved size for its scale. A size the current view does not offer is ignored. Fixed + fetch batches that are not a paged view (e.g. loading all glossary terms, approval tasks) are + out of scope. - `usePaging` treats a caller that declares no options as on the list scale, so a grid size left in the shared URL param, or an off-scale size an older build stored in the preference, never reaches a 15/25/50 picker. diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataProduct/DataProductListPage.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataProduct/DataProductListPage.tsx index cb0e7be08dba..716255076cf1 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataProduct/DataProductListPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataProduct/DataProductListPage.tsx @@ -167,14 +167,15 @@ const DataProductListPage = ({ pageSize: listingPageSize, pageSizeOptions: listingPageSizeOptions, }); - // Card and table views page at different sizes, so the current page may not exist in the other. - const { handlePageChange } = dataProductListing; + // Card and table views page at different sizes, so a switch starts over on page 1 at the new + // view's saved size; a size from a link applies only to the view it was opened in. + const { resetPaging } = dataProductListing; const handleViewChange = useCallback( (nextView: PageViewMode) => { setView(nextView); - handlePageChange(1); + resetPaging?.(); }, - [handlePageChange] + [resetPaging] ); const { permissions } = usePermissionProvider(); const { quickFilters, defaultFilters } = useDataProductFilters({ diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Domain/SubDomainsTable/SubDomainsTable.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Domain/SubDomainsTable/SubDomainsTable.component.tsx index 5781bc4510d4..c5315a5dbfbb 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Domain/SubDomainsTable/SubDomainsTable.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Domain/SubDomainsTable/SubDomainsTable.component.tsx @@ -61,14 +61,15 @@ const SubDomainsTable = ({ pageSize: listingPageSize, pageSizeOptions: listingPageSizeOptions, }); - // Card and table views page at different sizes, so the current page may not exist in the other. - const { handlePageChange } = subdomainListing; + // Card and table views page at different sizes, so a switch starts over on page 1 at the new + // view's saved size; a size from a link applies only to the view it was opened in. + const { resetPaging } = subdomainListing; const handleViewChange = useCallback( (nextView: PageViewMode) => { setView(nextView); - handlePageChange(1); + resetPaging?.(); }, - [handlePageChange] + [resetPaging] ); const { quickFilters, defaultFilters } = useDomainFilters({ diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DomainListing/DomainListPage.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DomainListing/DomainListPage.tsx index 8a97032f068a..5fc8608137dc 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DomainListing/DomainListPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DomainListing/DomainListPage.tsx @@ -73,14 +73,15 @@ const DomainListPage = ({ renderPageHeader }: DomainListPageProps) => { pageSize: listingPageSize, pageSizeOptions: listingPageSizeOptions, }); - // Card and table views page at different sizes, so the current page may not exist in the other. - const { handlePageChange } = domainListing; + // Card and table views page at different sizes, so a switch starts over on page 1 at the new + // view's saved size; a size from a link applies only to the view it was opened in. + const { resetPaging } = domainListing; const handleViewChange = useCallback( (nextView: PageViewMode) => { setView(nextView); - handlePageChange(1); + resetPaging?.(); }, - [handlePageChange] + [resetPaging] ); const { permissions } = usePermissionProvider(); const [treeRefreshToken, setTreeRefreshToken] = useState(0); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/atoms/compositions/useListingData.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/atoms/compositions/useListingData.test.tsx index 885a320deacc..5c1ebb666441 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/atoms/compositions/useListingData.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/atoms/compositions/useListingData.test.tsx @@ -84,6 +84,7 @@ describe('useListingData', () => { setPageSize: mockSetPageSize, resetFilters: jest.fn(), resetAll: jest.fn(), + resetPaging: jest.fn(), }); mockUseDataFetching.mockReturnValue({ @@ -205,6 +206,7 @@ describe('useListingData', () => { setPageSize: mockSetPageSize, resetFilters: jest.fn(), resetAll: jest.fn(), + resetPaging: jest.fn(), }); act(() => { @@ -250,6 +252,7 @@ describe('useListingData', () => { setPageSize: mockSetPageSize, resetFilters: jest.fn(), resetAll: jest.fn(), + resetPaging: jest.fn(), }); renderHook( @@ -284,6 +287,7 @@ describe('useListingData', () => { setPageSize: mockSetPageSize, resetFilters: jest.fn(), resetAll: jest.fn(), + resetPaging: jest.fn(), }); mockUsePaginationState.mockReturnValueOnce({ @@ -328,6 +332,7 @@ describe('useListingData', () => { setPageSize: mockSetPageSize, resetFilters: jest.fn(), resetAll: jest.fn(), + resetPaging: jest.fn(), }); mockUsePaginationState.mockReturnValueOnce({ diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/atoms/compositions/useListingData.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/atoms/compositions/useListingData.tsx index aed43bd20d99..aa8c6beee04d 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/atoms/compositions/useListingData.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/atoms/compositions/useListingData.tsx @@ -87,6 +87,7 @@ export const useListingData = < setCurrentPage, setPageSize, resetAll, + resetPaging, } = urlStateHook; // A size left in the URL by the other view's scale (25 from the table on the card view) is not @@ -249,6 +250,7 @@ export const useListingData = < handleClearAll: resetAll, handlePageChange, handlePageSizeChange, + resetPaging, refetch, }; }; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/atoms/data/useUrlState.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/atoms/data/useUrlState.test.tsx index 863e7ba55efc..65b0dc2289de 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/atoms/data/useUrlState.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/atoms/data/useUrlState.test.tsx @@ -176,4 +176,28 @@ describe('useUrlState', () => { expect(newParams.get('page')).toBe('1'); }); }); + + describe('resetPaging', () => { + it('drops the page and page size but keeps search and filters', () => { + const { result } = renderHook( + () => useUrlState({ filterKeys: ['owner'], defaultPageSize: 12 }), + { wrapper } + ); + + act(() => { + result.current.resetPaging(); + }); + + const updateFn = mockSetSearchParams.mock.calls[0][0]; + const newParams = updateFn( + new URLSearchParams('q=sales&owner=alice&page=3&size=25') + ); + + // The listing then starts on page 1 at its own default size. + expect(newParams.get('page')).toBeNull(); + expect(newParams.get('size')).toBeNull(); + expect(newParams.get('q')).toBe('sales'); + expect(newParams.get('owner')).toBe('alice'); + }); + }); }); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/atoms/data/useUrlState.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/atoms/data/useUrlState.tsx index 44f39fb35479..cd19c9854e8a 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/atoms/data/useUrlState.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/atoms/data/useUrlState.tsx @@ -171,6 +171,10 @@ export const useUrlState = ( updateUrlParams(updates); }, [searchKey, pageKey, filterKeys, updateUrlParams]); + const resetPaging = useCallback(() => { + updateUrlParams({ [pageKey]: null, [pageSizeKey]: null }); + }, [pageKey, pageSizeKey, updateUrlParams]); + return { urlState, parsedFilters, @@ -180,5 +184,6 @@ export const useUrlState = ( setPageSize, resetFilters, resetAll, + resetPaging, }; }; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/atoms/shared/types.ts b/openmetadata-ui/src/main/resources/ui/src/components/common/atoms/shared/types.ts index 5700899fe2f1..5cc887f99b2b 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/atoms/shared/types.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/atoms/shared/types.ts @@ -44,6 +44,8 @@ export interface UrlStateHook { setPageSize: (pageSize: number) => void; resetFilters: () => void; resetAll: () => void; + /** Drops the page and page size from the URL, so the listing starts on page 1 at its own size. */ + resetPaging: () => void; } export interface DataFetchingConfig { @@ -154,6 +156,8 @@ export interface ListingData { handleClearAll: () => void; handlePageChange: (page: number) => void; handlePageSizeChange?: (pageSize: number) => void; + /** Starts over on page 1 at the listing's own size, e.g. when a table/card toggle changes scale. */ + resetPaging?: () => void; refetch: (options?: { silent?: boolean }) => void | Promise; } diff --git a/openmetadata-ui/src/main/resources/ui/src/components/integration/ConnectionsPage/useConnectionsViewMode.test.ts b/openmetadata-ui/src/main/resources/ui/src/components/integration/ConnectionsPage/useConnectionsViewMode.test.ts index 0d0b633eea81..06bfda5388cc 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/integration/ConnectionsPage/useConnectionsViewMode.test.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/integration/ConnectionsPage/useConnectionsViewMode.test.ts @@ -86,4 +86,25 @@ describe('useConnectionsViewMode', () => { connectionsViewMode: 'list', }); }); + + it('drops the paging params so the new view starts at its own saved size', () => { + const { result } = renderHook(() => useConnectionsViewMode()); + + act(() => result.current.setViewMode('grid')); + + const [updater] = mockSetSearchParams.mock.calls[0]; + const next = updater( + new URLSearchParams( + 'category=all&pageSize=50¤tPage=3&cursorType=after&cursorValue=abc' + ) + ); + + // A list link's 50 must not come back when the user returns to the list. + expect(next.get('pageSize')).toBeNull(); + expect(next.get('currentPage')).toBeNull(); + expect(next.get('cursorType')).toBeNull(); + expect(next.get('cursorValue')).toBeNull(); + expect(next.get('category')).toBe('all'); + expect(next.get('viewMode')).toBe('grid'); + }); }); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/integration/ConnectionsPage/useConnectionsViewMode.ts b/openmetadata-ui/src/main/resources/ui/src/components/integration/ConnectionsPage/useConnectionsViewMode.ts index a4a474779062..32b6a5a0aea2 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/integration/ConnectionsPage/useConnectionsViewMode.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/integration/ConnectionsPage/useConnectionsViewMode.ts @@ -22,6 +22,11 @@ const isConnectionsViewMode = ( value: string | null | undefined ): value is ConnectionsViewMode => value === 'grid' || value === 'list'; +// usePaging's URL params. Grid and list page on different scales, so a switch drops them and the +// new view starts on page 1 at its saved size; a size from a link applies only to the view it was +// opened in. +const PAGING_PARAMS = ['currentPage', 'pageSize', 'cursorType', 'cursorValue']; + export const useConnectionsViewMode = () => { const [searchParams, setSearchParams] = useSearchParams(); const { @@ -43,6 +48,7 @@ export const useConnectionsViewMode = () => { (previous) => { const next = new URLSearchParams(previous); next.set(VIEW_MODE_PARAM, nextViewMode); + PAGING_PARAMS.forEach((param) => next.delete(param)); return next; }, From 62f45c9dcee1c2211fd619471b24437905b9afe5 Mon Sep 17 00:00:00 2001 From: Sweta Agarwalla Date: Fri, 16 Oct 2026 15:40:36 +0530 Subject: [PATCH 13/27] Memoize fetchKpiList so the KPI refetch effect has complete deps The refetch-on-page-size effect added a react-hooks/exhaustive-deps warning; keying fetchKpiList on the page size and running the effect on it removes that warning and the older one about fetchKpiList changing on every render. --- .../ui/src/pages/DataInsightPage/KPIList.tsx | 56 ++++++++++--------- 1 file changed, 30 insertions(+), 26 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/DataInsightPage/KPIList.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/DataInsightPage/KPIList.tsx index 9c65b64b06c5..d6c299354c2d 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/DataInsightPage/KPIList.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/DataInsightPage/KPIList.tsx @@ -65,30 +65,33 @@ const KPIList = () => { const [selectedKpi, setSelectedKpi] = useState(); const [isDeleting, setIsDeleting] = useState(false); - const fetchKpiList = async (param?: Record) => { - try { - setIsLoading(true); - const response = await getListKPIs({ - fields: [ - TabSpecificField.START_DATE, - TabSpecificField.END_DATE, - TabSpecificField.TARGET_VALUE, - TabSpecificField.DATA_INSIGHT_CHART, - TabSpecificField.METRIC_TYPE, - ], - limit: kpiPageSize, - before: param && param.before, - after: param && param.after, - }); - setKpiList(response.data); - setKpiPaging(response.paging); - } catch { - setKpiList([]); - setKpiPaging(pagingObject); - } finally { - setIsLoading(false); - } - }; + const fetchKpiList = useCallback( + async (param?: Record) => { + try { + setIsLoading(true); + const response = await getListKPIs({ + fields: [ + TabSpecificField.START_DATE, + TabSpecificField.END_DATE, + TabSpecificField.TARGET_VALUE, + TabSpecificField.DATA_INSIGHT_CHART, + TabSpecificField.METRIC_TYPE, + ], + limit: kpiPageSize, + before: param && param.before, + after: param && param.after, + }); + setKpiList(response.data); + setKpiPaging(response.paging); + } catch { + setKpiList([]); + setKpiPaging(pagingObject); + } finally { + setIsLoading(false); + } + }, + [kpiPageSize] + ); const columns = useMemo(() => { const col: ColumnsType = [ @@ -206,11 +209,12 @@ const KPIList = () => { } }; - // Refetches from page 1 when the saved page size changes; the page can stay mounted in app mode. + // fetchKpiList changes with the saved page size, so this refetches from page 1 when it changes; + // the page can stay mounted in app mode. useEffect(() => { setKpiPage(INITIAL_PAGING_VALUE); fetchKpiList(); - }, [kpiPageSize]); + }, [fetchKpiList]); const handleAfterDeleteAction = useCallback(() => { fetchKpiList(); From 6636e78acb644d815604d3085d51afddf82482ae Mon Sep 17 00:00:00 2001 From: Sweta Agarwalla Date: Fri, 16 Oct 2026 15:56:51 +0530 Subject: [PATCH 14/27] Open an off-scale page-size link on page 1 A settings-hash link can carry a size the picker no longer offers (pageSize=10 from before the scales). Replacing only the size kept the link's page and cursor, which were counted at the old size, so an older Recent Events link asked for an offset past the data and showed an empty page with no pager. - useHashPagingParams(pageSizeOptions): when the hash size is off scale, report page 1 with no cursor and rewrite the hash to match. - Roles, Policies, Notification Alerts, profile Audit Logs (list scale) and Personas (grid scale) pass their options; Bots and Recent Events read their starting page, size and cursor through it. - Tests on the real hook: an on-scale link keeps its page and size, an off-scale one opens on page 1 with the hash cleared. --- .../AccessControlAuditLogsPanel.tsx | 3 +- .../AccessControlPoliciesPanel.tsx | 3 +- .../AccessControlRolesPanel.tsx | 3 +- .../Profile/tabs/bots/BotsListPanel.test.tsx | 7 +++ .../Profile/tabs/bots/BotsListPanel.tsx | 30 ++++++---- .../Profile/tabs/bots/BotsPanel.test.tsx | 7 +++ .../notification/NotificationAlertsPanel.tsx | 3 +- .../NotificationRecentEvents.test.tsx | 7 +++ .../notification/NotificationRecentEvents.tsx | 15 +++-- .../Profile/tabs/personas/PersonasLanding.tsx | 2 +- .../ui/src/hooks/useSettingsHash.test.tsx | 55 ++++++++++++++++++- .../resources/ui/src/hooks/useSettingsHash.ts | 54 ++++++++++++++++-- 12 files changed, 164 insertions(+), 25 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/Profile/tabs/access-control/AccessControlAuditLogsPanel.tsx b/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/Profile/tabs/access-control/AccessControlAuditLogsPanel.tsx index 73e5eecaa1e1..0ff0b93ea543 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/Profile/tabs/access-control/AccessControlAuditLogsPanel.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/Profile/tabs/access-control/AccessControlAuditLogsPanel.tsx @@ -46,6 +46,7 @@ import '../../../../../../components/common/atoms/filters/FilterSelection.less'; import Banner from '../../../../../../components/common/Banner/Banner'; import { CSVExportWebsocketResponse } from '../../../../../../components/Entity/EntityExportModalProvider/EntityExportModalProvider.interface'; import { + LIST_PAGE_SIZE_OPTIONS, PAGE_SIZE_BASE, PAGE_SIZE_LARGE, PAGE_SIZE_MEDIUM, @@ -145,7 +146,7 @@ const AccessControlAuditLogsPanel: React.FC< cursor: hashCursor, cursorType: hashCursorType, setPage: setHashPage, - } = useHashPagingParams(); + } = useHashPagingParams(LIST_PAGE_SIZE_OPTIONS); const { savePageSize, resolvePageSize } = useSavedPageSize(); const pageSize = resolvePageSize(hashPageSize); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/Profile/tabs/access-control/AccessControlPoliciesPanel.tsx b/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/Profile/tabs/access-control/AccessControlPoliciesPanel.tsx index 47da51268ab6..c058863346ca 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/Profile/tabs/access-control/AccessControlPoliciesPanel.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/Profile/tabs/access-control/AccessControlPoliciesPanel.tsx @@ -36,6 +36,7 @@ import React, { } from 'react'; import { useTranslation } from 'react-i18next'; import { + LIST_PAGE_SIZE_OPTIONS, PAGE_SIZE_BASE, PAGE_SIZE_LARGE, PAGE_SIZE_MEDIUM, @@ -87,7 +88,7 @@ const AccessControlPoliciesPanel: React.FC = ({ cursor: hashCursor, cursorType: hashCursorType, setPage: setHashPage, - } = useHashPagingParams(); + } = useHashPagingParams(LIST_PAGE_SIZE_OPTIONS); const { savePageSize, resolvePageSize } = useSavedPageSize(); const pageSize = resolvePageSize(hashPageSize); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/Profile/tabs/access-control/AccessControlRolesPanel.tsx b/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/Profile/tabs/access-control/AccessControlRolesPanel.tsx index ad9ed0a032fb..c3cab08f7cdf 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/Profile/tabs/access-control/AccessControlRolesPanel.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/Profile/tabs/access-control/AccessControlRolesPanel.tsx @@ -36,6 +36,7 @@ import React, { } from 'react'; import { useTranslation } from 'react-i18next'; import { + LIST_PAGE_SIZE_OPTIONS, PAGE_SIZE_BASE, PAGE_SIZE_LARGE, PAGE_SIZE_MEDIUM, @@ -85,7 +86,7 @@ const AccessControlRolesPanel: React.FC = ({ cursor: hashCursor, cursorType: hashCursorType, setPage: setHashPage, - } = useHashPagingParams(); + } = useHashPagingParams(LIST_PAGE_SIZE_OPTIONS); const { savePageSize, resolvePageSize } = useSavedPageSize(); const pageSize = resolvePageSize(hashPageSize); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/Profile/tabs/bots/BotsListPanel.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/Profile/tabs/bots/BotsListPanel.test.tsx index 6908c7ba5bf0..567fd9b7689c 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/Profile/tabs/bots/BotsListPanel.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/Profile/tabs/bots/BotsListPanel.test.tsx @@ -39,6 +39,13 @@ jest.mock('../../../../../../hooks/authHooks', () => ({ })); jest.mock('../../../../../../hooks/useSettingsHash', () => ({ + useHashPagingParams: () => ({ + page: 1, + pageSize: undefined, + cursor: undefined, + cursorType: undefined, + setPage: jest.fn(), + }), useSettingsHash: () => ({ state: { tab: 'bots', subPath: '', params: {} }, updateParams: mockUpdateParams, diff --git a/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/Profile/tabs/bots/BotsListPanel.tsx b/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/Profile/tabs/bots/BotsListPanel.tsx index 8cece3b4423c..8266fdb809c6 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/Profile/tabs/bots/BotsListPanel.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/Profile/tabs/bots/BotsListPanel.tsx @@ -39,6 +39,7 @@ import React, { } from 'react'; import { useTranslation } from 'react-i18next'; import { + LIST_PAGE_SIZE_OPTIONS, PAGE_SIZE_BASE, PAGE_SIZE_LARGE, PAGE_SIZE_MEDIUM, @@ -52,7 +53,10 @@ import { Include } from '../../../../../../generated/type/include'; import { Paging } from '../../../../../../generated/type/paging'; import { useAuth } from '../../../../../../hooks/authHooks'; import { useSavedPageSize } from '../../../../../../hooks/paging/useSavedPageSize'; -import { useSettingsHash } from '../../../../../../hooks/useSettingsHash'; +import { + useHashPagingParams, + useSettingsHash, +} from '../../../../../../hooks/useSettingsHash'; import { getBots } from '../../../../../../rest/botsAPI'; import { searchQuery } from '../../../../../../rest/searchAPI'; import { formatUsersResponse } from '../../../../../../utils/APIUtils'; @@ -92,7 +96,7 @@ const BotsListPanel: React.FC = ({ const { t } = useTranslation(); const { isAdminUser } = useAuth(); const { getResourceLimit } = useLimitStore(); - const { state: hashState, updateParams } = useSettingsHash(); + const { updateParams } = useSettingsHash(); const [bots, setBots] = useState([]); const [searchedData, setSearchedData] = useState([]); @@ -101,13 +105,16 @@ const BotsListPanel: React.FC = ({ const [showDeleted, setShowDeleted] = useState(false); const [searchTerm, setSearchTerm] = useState(''); const [paging, setPaging] = useState({ total: 0 }); - const [currentPage, setCurrentPage] = useState( - Number(hashState.params.page) || 1 - ); + // Page and size from the hash, with a link's off-scale size (and the page counted at it) dropped. + const { + page: hashPage, + pageSize: hashPageSize, + cursor: hashCursor, + cursorType: hashCursorType, + } = useHashPagingParams(LIST_PAGE_SIZE_OPTIONS); + const [currentPage, setCurrentPage] = useState(hashPage); const { savePageSize, resolvePageSize } = useSavedPageSize(); - const [pageSize, setPageSize] = useState( - resolvePageSize(Number(hashState.params.pageSize)) - ); + const [pageSize, setPageSize] = useState(resolvePageSize(hashPageSize)); const [reloadKey, setReloadKey] = useState(0); const fetchRequestIdRef = useRef(0); const latestSearchRequest = useRef(0); @@ -373,12 +380,15 @@ const BotsListPanel: React.FC = ({ ); const initialFetchRef = useRef(true); - const initialParamsRef = useRef(hashState.params); + const initialCursorRef = useRef({ + cursor: hashCursor, + cursorType: hashCursorType, + }); useEffect(() => { if (initialFetchRef.current) { initialFetchRef.current = false; - const { cursor, cursorType } = initialParamsRef.current; + const { cursor, cursorType } = initialCursorRef.current; fetchBots(cursor && cursorType ? { [cursorType]: cursor } : undefined); } else { fetchBots(); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/Profile/tabs/bots/BotsPanel.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/Profile/tabs/bots/BotsPanel.test.tsx index a809e2ad8fa7..e98895be5940 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/Profile/tabs/bots/BotsPanel.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/Profile/tabs/bots/BotsPanel.test.tsx @@ -20,6 +20,13 @@ const mockClearHash = jest.fn(); const mockUpdateParams = jest.fn(); jest.mock('../../../../../../hooks/useSettingsHash', () => ({ + useHashPagingParams: () => ({ + page: 1, + pageSize: undefined, + cursor: undefined, + cursorType: undefined, + setPage: jest.fn(), + }), useSettingsHash: () => ({ state: { tab: 'bots', subPath: '', params: {} }, setHash: mockSetHash, diff --git a/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/Profile/tabs/notification/NotificationAlertsPanel.tsx b/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/Profile/tabs/notification/NotificationAlertsPanel.tsx index 1851c4572b77..462c2487a4fb 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/Profile/tabs/notification/NotificationAlertsPanel.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/Profile/tabs/notification/NotificationAlertsPanel.tsx @@ -37,6 +37,7 @@ import React, { } from 'react'; import { useTranslation } from 'react-i18next'; import { + LIST_PAGE_SIZE_OPTIONS, PAGE_SIZE_BASE, PAGE_SIZE_LARGE, PAGE_SIZE_MEDIUM, @@ -83,7 +84,7 @@ const NotificationAlertsPanel: React.FC = ({ cursor: hashCursor, cursorType: hashCursorType, setPage: setHashPage, - } = useHashPagingParams(); + } = useHashPagingParams(LIST_PAGE_SIZE_OPTIONS); const { savePageSize, resolvePageSize } = useSavedPageSize(); const pageSize = resolvePageSize(hashPageSize); const [alerts, setAlerts] = useState([]); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/Profile/tabs/notification/NotificationRecentEvents.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/Profile/tabs/notification/NotificationRecentEvents.test.tsx index cfcff62a603e..444a5a028f9b 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/Profile/tabs/notification/NotificationRecentEvents.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/Profile/tabs/notification/NotificationRecentEvents.test.tsx @@ -81,6 +81,13 @@ jest.mock('../../../../../../rest/alertsAPI', () => ({ })); jest.mock('../../../../../../hooks/useSettingsHash', () => ({ + useHashPagingParams: () => ({ + page: 1, + pageSize: undefined, + cursor: undefined, + cursorType: undefined, + setPage: jest.fn(), + }), useSettingsHash: () => ({ state: { params: {} }, updateParams: jest.fn(), diff --git a/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/Profile/tabs/notification/NotificationRecentEvents.tsx b/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/Profile/tabs/notification/NotificationRecentEvents.tsx index 420f189a0dff..61c0e218750b 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/Profile/tabs/notification/NotificationRecentEvents.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/Profile/tabs/notification/NotificationRecentEvents.tsx @@ -32,6 +32,7 @@ import { lazy, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { ReactComponent as FilterOffIcon } from '../../../../../../assets/svg/ic-filter-off.svg'; import { + LIST_PAGE_SIZE_OPTIONS, PAGE_SIZE_BASE, PAGE_SIZE_LARGE, PAGE_SIZE_MEDIUM, @@ -46,7 +47,10 @@ import { import { EventSubscription } from '../../../../../../generated/events/eventSubscription'; import { Paging } from '../../../../../../generated/type/paging'; import { useSavedPageSize } from '../../../../../../hooks/paging/useSavedPageSize'; -import { useSettingsHash } from '../../../../../../hooks/useSettingsHash'; +import { + useHashPagingParams, + useSettingsHash, +} from '../../../../../../hooks/useSettingsHash'; import { getAlertEventsFromId } from '../../../../../../rest/alertsAPI'; import { getAlertStatusIcon } from '../../../../../../utils/Alerts/AlertsUtil'; import { @@ -87,10 +91,13 @@ function NotificationRecentEvents({ const [filter, setFilter] = useState( AlertRecentEventFilters.ALL ); - const { state: hashState, updateParams } = useSettingsHash(); - const initialPage = Number(hashState.params.page) || 1; + const { updateParams } = useSettingsHash(); + // Page and size from the hash, with a link's off-scale size (and the page counted at it) dropped. + const { page: initialPage, pageSize: hashPageSize } = useHashPagingParams( + LIST_PAGE_SIZE_OPTIONS + ); const { savePageSize, resolvePageSize } = useSavedPageSize(); - const initialPageSize = resolvePageSize(Number(hashState.params.pageSize)); + const initialPageSize = resolvePageSize(hashPageSize); const [currentPage, setCurrentPage] = useState(initialPage); const [pageSize, setPageSize] = useState(initialPageSize); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/Profile/tabs/personas/PersonasLanding.tsx b/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/Profile/tabs/personas/PersonasLanding.tsx index 49cff66b10fd..a956ad1a4f09 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/Profile/tabs/personas/PersonasLanding.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/discovery/personal-space/Profile/tabs/personas/PersonasLanding.tsx @@ -49,7 +49,7 @@ const PersonasLanding = ({ onNavigate }: PersonasLandingProps) => { cursor: hashCursor, cursorType: hashCursorType, setPage: setHashPage, - } = useHashPagingParams(); + } = useHashPagingParams(GRID_PAGE_SIZE_OPTIONS); // Personas render as a card grid, so they page on the grid scale. const { savePageSize, resolvePageSize } = useSavedPageSize( GRID_PAGE_SIZE_OPTIONS diff --git a/openmetadata-ui/src/main/resources/ui/src/hooks/useSettingsHash.test.tsx b/openmetadata-ui/src/main/resources/ui/src/hooks/useSettingsHash.test.tsx index 630e841ea7d4..43eab5954d77 100644 --- a/openmetadata-ui/src/main/resources/ui/src/hooks/useSettingsHash.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/hooks/useSettingsHash.test.tsx @@ -13,7 +13,8 @@ import { act, renderHook } from '@testing-library/react'; import { ReactNode } from 'react'; import { MemoryRouter, useLocation, useNavigate } from 'react-router-dom'; -import { useSettingsHash } from './useSettingsHash'; +import { LIST_PAGE_SIZE_OPTIONS } from '../constants/constants'; +import { useHashPagingParams, useSettingsHash } from './useSettingsHash'; // Real MemoryRouter (no react-router mock): the hook drives navigation through // react-router and reads the live URL hash from `globalThis.location`, so tests @@ -206,3 +207,55 @@ describe('useSettingsHash', () => { act(() => second.result.current.clearHash()); }); }); + +describe('useHashPagingParams', () => { + beforeEach(() => { + globalThis.location.hash = ''; + const { result, unmount } = renderHook(() => useSettingsHash(), { + wrapper, + }); + act(() => result.current.clearHash()); + unmount(); + }); + + const renderPaging = (options?: number[]) => + renderHook( + () => ({ paging: useHashPagingParams(options), hash: useSettingsHash() }), + { wrapper } + ); + + it('keeps the page, cursor and size of a link on the picker scale', () => { + globalThis.location.hash = + '#notification/events?page=3&pageSize=25&cursorType=after&cursor=abc'; + + const { result } = renderPaging(LIST_PAGE_SIZE_OPTIONS); + + expect(result.current.paging.page).toBe(3); + expect(result.current.paging.pageSize).toBe(25); + expect(result.current.paging.cursor).toBe('abc'); + }); + + it('opens an off-scale link on page 1 and rewrites the hash to match', () => { + // An older link: its page 3 was counted at 10 per page, so at the saved 15 it would request an + // offset past the data and show an empty page with no pager to page back with. + globalThis.location.hash = + '#notification/events?page=3&pageSize=10&cursorType=after&cursor=abc'; + + const { result } = renderPaging(LIST_PAGE_SIZE_OPTIONS); + + expect(result.current.paging.page).toBe(1); + expect(result.current.paging.pageSize).toBeUndefined(); + expect(result.current.paging.cursor).toBeUndefined(); + expect(result.current.paging.cursorType).toBeUndefined(); + expect(result.current.hash.state.params).toEqual({}); + }); + + it('accepts any size when the caller declares no options', () => { + globalThis.location.hash = '#notification/events?page=2&pageSize=10'; + + const { result } = renderPaging(); + + expect(result.current.paging.page).toBe(2); + expect(result.current.paging.pageSize).toBe(10); + }); +}); diff --git a/openmetadata-ui/src/main/resources/ui/src/hooks/useSettingsHash.ts b/openmetadata-ui/src/main/resources/ui/src/hooks/useSettingsHash.ts index a93cefaf9819..809cd7b0ce24 100644 --- a/openmetadata-ui/src/main/resources/ui/src/hooks/useSettingsHash.ts +++ b/openmetadata-ui/src/main/resources/ui/src/hooks/useSettingsHash.ts @@ -276,17 +276,61 @@ export const useSettingsHash = () => { return { state, setHash, clearHash, updateParams, goTo }; }; +// A link can carry a size the picker no longer offers (pageSize=10 from before the page-size +// scales). Its page and cursor were counted at that size, so they go too: the page opens on page 1 +// at the user's saved size instead of at an offset past the data, and the caller rewrites the hash. +// ADR:2026-10-13-ui-page-sizes-lists-15-25-50-grids-12-24-48 +interface HashPaging { + page: number; + cursorType?: string; + cursor?: string; + pageSize?: number; + isPageSizeRejected: boolean; +} + +function readHashPaging( + params: Record, + pageSizeOptions?: number[] +): HashPaging { + const requestedPageSize = Number(params.pageSize) || undefined; + const isPageSizeRejected = Boolean( + requestedPageSize && + pageSizeOptions && + !pageSizeOptions.includes(requestedPageSize) + ); + + if (isPageSizeRejected) { + return { page: 1, isPageSizeRejected }; + } + + return { + page: Number(params.page) || 1, + cursorType: params.cursorType || undefined, + cursor: params.cursor || undefined, + pageSize: requestedPageSize, + isPageSizeRejected, + }; +} + /** * Hook to read/write pagination params from the hash. * Drop-in replacement for usePaging when inside the settings modal. */ -export const useHashPagingParams = () => { +export const useHashPagingParams = (pageSizeOptions?: number[]) => { const { state, updateParams } = useSettingsHash(); + const { page, cursorType, cursor, pageSize, isPageSizeRejected } = + readHashPaging(state.params, pageSizeOptions); - const page = Number(state.params.page) || 1; - const cursorType = state.params.cursorType || undefined; - const cursor = state.params.cursor || undefined; - const pageSize = Number(state.params.pageSize) || undefined; + useEffect(() => { + if (isPageSizeRejected) { + updateParams({ + page: undefined, + cursorType: undefined, + cursor: undefined, + pageSize: undefined, + }); + } + }, [isPageSizeRejected, updateParams]); const setPage = useCallback( ( From 6f4c0d9f36f1794de7aa1dcf359e3e9746e149f8 Mon Sep 17 00:00:00 2001 From: Sweta Agarwalla Date: Fri, 16 Oct 2026 18:09:28 +0530 Subject: [PATCH 15/27] Drop changes unrelated to page sizes Restore main's code where the PR only fixed pre-existing type errors or Sonar findings on lines pagination does not touch. --- .../ui/playwright/e2e/Pages/Domains.spec.ts | 14 +++++--------- .../SchemaTable/SchemaTable.component.tsx | 4 ++-- .../Database/TableQueries/TableQueries.tsx | 7 ++++++- .../ui/src/components/common/Table/TableV2.tsx | 4 ++-- .../common/atoms/data/useDataFetching.tsx | 2 +- .../ContextCenterMemoriesPage.tsx | 16 ++++++++-------- 6 files changed, 24 insertions(+), 23 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Domains.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Domains.spec.ts index e1a7941157e0..a50cb1308530 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Domains.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Domains.spec.ts @@ -1350,7 +1350,7 @@ test.describe('Domains', () => { }); await test.step('Fill subdomain form', async () => { - await fillDomainForm(page, subDomain.data); + await fillDomainForm(page, subDomain.data, false); }); await test.step('Search and select tag via TagSuggestion', async () => { @@ -3072,7 +3072,7 @@ test.describe('Domain Access with hasDomain() Rule', () => { // Navigate to the domain table const domainTableFqn = testResources.domainTable.entityResponseData.fullyQualifiedName; - await userPage.goto(`/table/${encodeURIComponent(domainTableFqn ?? '')}`); + await userPage.goto(`/table/${encodeURIComponent(domainTableFqn)}`); await waitForAllLoadersToDisappear(userPage); // Verify no permission error @@ -3088,9 +3088,7 @@ test.describe('Domain Access with hasDomain() Rule', () => { // Navigate to the subdomain table const subDomainTableFqn = testResources.subDomainTable.entityResponseData.fullyQualifiedName; - await userPage.goto( - `/table/${encodeURIComponent(subDomainTableFqn ?? '')}` - ); + await userPage.goto(`/table/${encodeURIComponent(subDomainTableFqn)}`); // Verify no permission error await expect( @@ -3138,7 +3136,7 @@ test.describe('Domain Access with noDomain() Rule', () => { await test.step('Verify user can access domain-assigned table', async () => { const domainTableFqn = testResources.domainTable.entityResponseData.fullyQualifiedName; - await userPage.goto(`/table/${encodeURIComponent(domainTableFqn ?? '')}`); + await userPage.goto(`/table/${encodeURIComponent(domainTableFqn)}`); await waitForAllLoadersToDisappear(userPage); // Verify no permission error @@ -3153,9 +3151,7 @@ test.describe('Domain Access with noDomain() Rule', () => { await test.step('Verify user gets permission error for table without domain', async () => { const noDomainTableFqn = testResources.noDomainTable.entityResponseData.fullyQualifiedName; - await userPage.goto( - `/table/${encodeURIComponent(noDomainTableFqn ?? '')}` - ); + await userPage.goto(`/table/${encodeURIComponent(noDomainTableFqn)}`); await waitForAllLoadersToDisappear(userPage); // Verify permission error is shown diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Database/SchemaTable/SchemaTable.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Database/SchemaTable/SchemaTable.component.tsx index 2bb1a6e4c6ac..1e58cf8c71a2 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Database/SchemaTable/SchemaTable.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Database/SchemaTable/SchemaTable.component.tsx @@ -920,8 +920,8 @@ const SchemaTable = () => { entity: t('label.constraint'), }), })} - value={editConstraint ?? null} - onChange={(key) => + selectedKey={editConstraint ?? null} + onSelectionChange={(key) => setEditConstraint( key === NO_CONSTRAINT_KEY ? undefined : (key as Constraint) ) diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Database/TableQueries/TableQueries.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Database/TableQueries/TableQueries.tsx index c86979613670..c431e6714a62 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Database/TableQueries/TableQueries.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Database/TableQueries/TableQueries.tsx @@ -630,7 +630,12 @@ const TableQueries: FC = ({ onGetInitialOptions={getInitialTagsOptions} onSearch={handleTagsSearch} /> - + { + setIsClickedCalendar(true); + }}> {t('label.created-date')} = | { - customPaginationProps?: never; - pagination?: NonNullable['pagination']>; + customPaginationProps?: undefined; + pagination?: TableComponentProps['pagination']; } | { customPaginationProps: CustomPaginationProps; pagination: false }; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/atoms/data/useDataFetching.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/atoms/data/useDataFetching.tsx index 6e1024270727..3e009fa30b01 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/atoms/data/useDataFetching.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/atoms/data/useDataFetching.tsx @@ -66,7 +66,7 @@ export const useDataFetching = ( // Default transform function const defaultTransform = useCallback( (data: SearchResponse) => - (data.hits?.hits?.map((hit) => hit._source) ?? []) as unknown as T[], + data.hits?.hits?.map((hit) => hit._source) || [], [] ); diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/ContextCenterPage/ContextCenterMemoriesPage/ContextCenterMemoriesPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/ContextCenterPage/ContextCenterMemoriesPage/ContextCenterMemoriesPage.tsx index 6f852a030bb0..d8a7efc700f2 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/ContextCenterPage/ContextCenterMemoriesPage/ContextCenterMemoriesPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/ContextCenterPage/ContextCenterMemoriesPage/ContextCenterMemoriesPage.tsx @@ -704,7 +704,7 @@ const ContextCenterMemoriesPage: FC = () => { t('server.entity-deleted-success', { entity: t('label.memory') }) ); setMemoryToDelete(undefined); - void refreshMemoryReviews(); + refreshMemoryReviews(); await fetchMemories(); await fetchMemoryCounts(); } catch (err) { @@ -748,14 +748,14 @@ const ContextCenterMemoriesPage: FC = () => { ); const handleReviewResolved = useCallback(() => { - void refreshMemoryReviews(); - void queryClient.invalidateQueries({ + refreshMemoryReviews(); + queryClient.invalidateQueries({ queryKey: CONTEXT_CENTER_MEMORIES_COUNT_QUERY_KEY, }); - void queryClient.invalidateQueries({ + queryClient.invalidateQueries({ queryKey: INBOX_OPEN_TASK_COUNT_QUERY_KEY, }); - void fetchMemoryCounts(); + fetchMemoryCounts(); }, [fetchMemoryCounts]); const handleModalClose = useCallback(() => { @@ -809,9 +809,9 @@ const ContextCenterMemoriesPage: FC = () => { const handleModalSuccess = useCallback(() => { handleModalClose(); - void refreshMemoryReviews(); - void fetchMemories(); - void fetchMemoryCounts(); + refreshMemoryReviews(); + fetchMemories(); + fetchMemoryCounts(); }, [handleModalClose, fetchMemories, fetchMemoryCounts]); const countCards = useMemo( From 791d6863d65346189f4b7e82f7814181c871caa9 Mon Sep 17 00:00:00 2001 From: Sweta Agarwalla Date: Fri, 16 Oct 2026 20:13:03 +0530 Subject: [PATCH 16/27] Hide the pager on an empty list at a non-default page size A saved size other than the page's default kept the pager visible so the size could be changed back, even when the list had no records. --- .../ui/src/hooks/paging/usePaging.test.ts | 17 +++++++++++++++++ .../resources/ui/src/hooks/paging/usePaging.ts | 4 +++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/openmetadata-ui/src/main/resources/ui/src/hooks/paging/usePaging.test.ts b/openmetadata-ui/src/main/resources/ui/src/hooks/paging/usePaging.test.ts index b10b297fd5b8..d692d5ded3f6 100644 --- a/openmetadata-ui/src/main/resources/ui/src/hooks/paging/usePaging.test.ts +++ b/openmetadata-ui/src/main/resources/ui/src/hooks/paging/usePaging.test.ts @@ -61,6 +61,23 @@ describe('usePaging', () => { expect(result.current.showPagination).toBe(true); }); + it('hides pagination at a non-default page size only while there are no records', () => { + mockUrlParams.pageSize = '25'; + const { result } = renderHook(() => usePaging()); + + act(() => { + result.current.handlePagingChange({ total: 0 }); + }); + + expect(result.current.showPagination).toBe(false); + + act(() => { + result.current.handlePagingChange({ total: 3 }); + }); + + expect(result.current.showPagination).toBe(true); + }); + it('syncs pagination state when url paging params are updated externally', () => { const { result, rerender } = renderHook(() => usePaging()); diff --git a/openmetadata-ui/src/main/resources/ui/src/hooks/paging/usePaging.ts b/openmetadata-ui/src/main/resources/ui/src/hooks/paging/usePaging.ts index 97a6592a8edd..8336aa568abf 100644 --- a/openmetadata-ui/src/main/resources/ui/src/hooks/paging/usePaging.ts +++ b/openmetadata-ui/src/main/resources/ui/src/hooks/paging/usePaging.ts @@ -157,11 +157,13 @@ export const usePaging = ( const paginationVisible = useMemo(() => { const hasCursorPagination = Boolean(paging.before || paging.after); + const isNonDefaultSize = + pageSize !== (defaultPageSize ?? pageSizeOptions[0]); return ( hasCursorPagination || paging.total > pageSize || - pageSize !== (defaultPageSize ?? pageSizeOptions[0]) + (paging.total > 0 && isNonDefaultSize) ); }, [ defaultPageSize, From c50ff7308468fd45ca3074d5e5da4795f01d45cc Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 16 Oct 2026 14:55:16 +0000 Subject: [PATCH 17/27] chore(playwright): auto-refresh impact-map.generated.json --- .github/playwright/impact-map.generated.json | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/.github/playwright/impact-map.generated.json b/.github/playwright/impact-map.generated.json index 96fccae632bd..a0886d587914 100644 --- a/.github/playwright/impact-map.generated.json +++ b/.github/playwright/impact-map.generated.json @@ -804,6 +804,7 @@ "playwright/e2e/Features/DataQuality/Profiler.spec.ts", "playwright/e2e/Features/DataQuality/ProfilerIngestionForm.spec.ts", "playwright/e2e/Features/DataQuality/RunTestCase.spec.ts", + "playwright/e2e/Features/DataQuality/TableTestCaseExportFilters.spec.ts", "playwright/e2e/Features/DataQuality/TableTestCasePagination.spec.ts", "playwright/e2e/Features/DataQuality/TestCaseDetailsPage.spec.ts", "playwright/e2e/Features/DataQuality/TestCaseImportExportBasic.spec.ts", @@ -1721,6 +1722,7 @@ "playwright/e2e/Features/DataQuality/ProfilerSettingsAfterReindex.spec.ts", "playwright/e2e/Features/DataQuality/RunTestCase.spec.ts", "playwright/e2e/Features/DataQuality/TableLevelTests.spec.ts", + "playwright/e2e/Features/DataQuality/TableTestCaseExportFilters.spec.ts", "playwright/e2e/Features/DataQuality/TableTestCasePagination.spec.ts", "playwright/e2e/Features/DataQuality/TestCaseCrudAfterReindex.spec.ts", "playwright/e2e/Features/DataQuality/TestCaseDetailsPage.spec.ts", @@ -2035,6 +2037,7 @@ "playwright/e2e/Features/DataAssetRulesEnabled.spec.ts", "playwright/e2e/Features/DataQuality/ProfilerIngestionForm.spec.ts", "playwright/e2e/Features/DataQuality/TableLevelTests.spec.ts", + "playwright/e2e/Features/DataQuality/TableTestCaseExportFilters.spec.ts", "playwright/e2e/Features/ExploreQuickFilters.spec.ts", "playwright/e2e/Features/IngestionListNameSorting.spec.ts", "playwright/e2e/Features/LandingPageWidgets/LandingPagePlatformHealth.spec.ts", @@ -3085,6 +3088,7 @@ "playwright/e2e/Features/DataQuality/Profiler.spec.ts", "playwright/e2e/Features/DataQuality/ProfilerIngestionForm.spec.ts", "playwright/e2e/Features/DataQuality/RunTestCase.spec.ts", + "playwright/e2e/Features/DataQuality/TableTestCaseExportFilters.spec.ts", "playwright/e2e/Features/DataQuality/TableTestCasePagination.spec.ts", "playwright/e2e/Features/DataQuality/TestCaseDetailsPage.spec.ts", "playwright/e2e/Features/DataQuality/TestCaseImportExportBasic.spec.ts", @@ -3307,6 +3311,7 @@ "playwright/e2e/Features/AdvancedSearch.spec.ts", "playwright/e2e/Features/ContextCenterDashboard.spec.ts", "playwright/e2e/Features/DataQuality/IncidentManagerSqlQuery.spec.ts", + "playwright/e2e/Features/DataQuality/TableTestCaseExportFilters.spec.ts", "playwright/e2e/Features/DomainFilterQueryFilter.spec.ts", "playwright/e2e/Features/LandingPageWidgets/LandingPageContextCenter.spec.ts", "playwright/e2e/Features/LandingPageWidgets/LandingPageCriticalAnnouncements.spec.ts", @@ -3497,6 +3502,7 @@ "playwright/e2e/Features/DataQuality/ProfilerSettingsAfterReindex.spec.ts", "playwright/e2e/Features/DataQuality/RunTestCase.spec.ts", "playwright/e2e/Features/DataQuality/TableLevelTests.spec.ts", + "playwright/e2e/Features/DataQuality/TableTestCaseExportFilters.spec.ts", "playwright/e2e/Features/DataQuality/TableTestCasePagination.spec.ts", "playwright/e2e/Features/DataQuality/TestCaseCrudAfterReindex.spec.ts", "playwright/e2e/Features/DataQuality/TestCaseDetailsPage.spec.ts", @@ -4113,6 +4119,7 @@ "playwright/e2e/Features/DataQuality/DataQualityListAfterReindex.spec.ts", "playwright/e2e/Features/DataQuality/DataQualityPermissions.spec.ts", "playwright/e2e/Features/DataQuality/IncidentManagerDateFilter.spec.ts", + "playwright/e2e/Features/DataQuality/TableTestCaseExportFilters.spec.ts", "playwright/e2e/Features/DataQuality/TableTestCasePagination.spec.ts", "playwright/e2e/Features/DataQuality/TestCaseDetailsPage.spec.ts", "playwright/e2e/Features/DataQuality/TestCaseImportExportBasic.spec.ts", @@ -4845,6 +4852,7 @@ "playwright/e2e/Features/AIMode/ConnectionServiceDetails.spec.ts", "playwright/e2e/Features/Announcements/SystemAnnouncements.spec.ts", "playwright/e2e/Features/BulkEditEntity.spec.ts", + "playwright/e2e/Features/DataQuality/TableTestCaseExportFilters.spec.ts", "playwright/e2e/Features/OnlineUsers.spec.ts", "playwright/e2e/Features/TestSuitePipelineRedeploy.spec.ts", "playwright/e2e/Pages/AIProfile/ProfileMembers.spec.ts", @@ -5173,6 +5181,7 @@ "playwright/e2e/Features/DataQuality/Profiler.spec.ts", "playwright/e2e/Features/DataQuality/RunTestCase.spec.ts", "playwright/e2e/Features/DataQuality/TableLevelTests.spec.ts", + "playwright/e2e/Features/DataQuality/TableTestCaseExportFilters.spec.ts", "playwright/e2e/Features/DataQuality/TableTestCasePagination.spec.ts", "playwright/e2e/Features/DataQuality/TestCaseCrudAfterReindex.spec.ts", "playwright/e2e/Features/DataQuality/TestCaseDetailsPage.spec.ts", @@ -5281,6 +5290,7 @@ "playwright/e2e/Features/DataQuality/DataQuality.spec.ts", "playwright/e2e/Features/DataQuality/IncidentManagerDateFilter.spec.ts", "playwright/e2e/Features/DataQuality/IncidentManagerSqlQuery.spec.ts", + "playwright/e2e/Features/DataQuality/TableTestCaseExportFilters.spec.ts", "playwright/e2e/Features/DataQuality/TableTestCasePagination.spec.ts", "playwright/e2e/Features/EntitySuggestions.spec.ts", "playwright/e2e/Features/FailedTestCaseSampleData.spec.ts", @@ -6794,6 +6804,14 @@ "playwright/e2e/Features/DataQuality/ProfilerSettingsAfterReindex.spec.ts" ] }, + { + "sources": [ + "openmetadata-ui/src/main/resources/ui/src/components/Database/Profiler/TableProfiler/QualityTab/QualityTab.component.tsx" + ], + "specs": [ + "playwright/e2e/Features/DataQuality/TableTestCaseExportFilters.spec.ts" + ] + }, { "sources": [ "openmetadata-ui/src/main/resources/ui/src/components/Database/Profiler/TestSummary/RunSummaryTiles/RunSummaryTiles.tsx" @@ -12002,6 +12020,7 @@ "playwright/e2e/Features/DataQuality/ProfilerSettingsAfterReindex.spec.ts", "playwright/e2e/Features/DataQuality/RunTestCase.spec.ts", "playwright/e2e/Features/DataQuality/TableLevelTests.spec.ts", + "playwright/e2e/Features/DataQuality/TableTestCaseExportFilters.spec.ts", "playwright/e2e/Features/DataQuality/TableTestCasePagination.spec.ts", "playwright/e2e/Features/DataQuality/TestCaseCrudAfterReindex.spec.ts", "playwright/e2e/Features/DataQuality/TestCaseDetailsPage.spec.ts", From 270c7429eb00e91091cd4e93db59a82b802b68ff Mon Sep 17 00:00:00 2001 From: Sweta Agarwalla Date: Fri, 16 Oct 2026 20:39:19 +0530 Subject: [PATCH 18/27] Add the page-size picker to the glossary terms pager The glossary terms list read the saved page size but offered no way to change it, unlike the classification tags pager. --- .../GlossaryTermTab.component.tsx | 10 +++--- .../GlossaryTermTab/GlossaryTermTab.test.tsx | 33 +++++++++++++++++++ 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTermTab/GlossaryTermTab.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTermTab/GlossaryTermTab.component.tsx index 98a79c045001..608d8c5de35d 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTermTab/GlossaryTermTab.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTermTab/GlossaryTermTab.component.tsx @@ -621,7 +621,8 @@ const GlossaryTermTab = ({ isGlossary, className }: GlossaryTermTabProps) => { const [confirmCheckboxChecked, setConfirmCheckboxChecked] = useState(false); const [totalTermsCount, setTotalTermsCount] = useState(0); - const { paging, handlePagingChange, pageSize } = usePaging(); + const { paging, handlePagingChange, pageSize, handlePageSizeChange } = + usePaging(); const [currentPage, setCurrentPage] = useState(1); const [loadingChildren, setLoadingChildren] = useState< Record @@ -1953,8 +1954,8 @@ const GlossaryTermTab = ({ isGlossary, className }: GlossaryTermTabProps) => { }; }, [isDraggingTerm, moveDraggedGlossaryTermToRoot]); - // Trigger new fetch when search term or status filter changes. Both reset - // pagination back to the first page. + // Trigger new fetch when the search term, status filter or page size + // changes. Each resets pagination back to the first page. useEffect(() => { if ( activeGlossary && @@ -1968,7 +1969,7 @@ const GlossaryTermTab = ({ isGlossary, className }: GlossaryTermTabProps) => { })); fetchAllTerms(); } - }, [searchTerm, selectedStatus]); + }, [searchTerm, selectedStatus, pageSize]); // Check if this is due to search or filter returning no results const isSearchActive = hasActiveSearchTerm(searchTerm); @@ -2089,6 +2090,7 @@ const GlossaryTermTab = ({ isGlossary, className }: GlossaryTermTabProps) => { pageSize={pageSize} paging={paging} pagingHandler={handleGlossaryTermPageChange} + onShowSizeChange={handlePageSizeChange} /> )} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTermTab/GlossaryTermTab.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTermTab/GlossaryTermTab.test.tsx index ae4373b597c2..cadf7f4caf31 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTermTab/GlossaryTermTab.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTermTab/GlossaryTermTab.test.tsx @@ -1139,6 +1139,39 @@ describe('Test GlossaryTermTab component', () => { }); }); + it('reloads the terms with the page size picked in the pager', async () => { + mockUseGlossaryStore.glossaryChildTerms = mockedGlossaryTerms; + mockGetFirstLevelGlossaryTermsPaginated.mockResolvedValue({ + data: mockedGlossaryTerms, + paging: { after: null }, + }); + + render(, { + wrapper: MemoryRouter, + }); + + const pageSizeButton = await screen.findByTestId( + 'page-size-selection-dropdown' + ); + fireEvent.keyDown(pageSizeButton, { key: 'Enter' }); + fireEvent.keyUp(pageSizeButton, { key: 'Enter' }); + fireEvent.click( + await screen.findByRole('menuitem', { name: '50 / label.page' }) + ); + + await waitFor(() => { + expect( + mockGetFirstLevelGlossaryTermsPaginated + ).toHaveBeenLastCalledWith( + expect.anything(), + 50, + undefined, + expect.anything(), + undefined + ); + }); + }); + it('should stop loading when no more terms are available', async () => { mockGetFirstLevelGlossaryTermsPaginated.mockResolvedValue({ data: mockedGlossaryTerms, From b3017a6f4132fe062626e4d123cbba5663c1a6ad Mon Sep 17 00:00:00 2001 From: Sweta Agarwalla Date: Fri, 16 Oct 2026 22:18:12 +0530 Subject: [PATCH 19/27] Address review: old listing links, Services paging test, constants file - An off-scale size in a listing URL now also opens page 1 and is cleared from the URL, so an old ?page=3&size=9 link no longer lands past the last result with no pager to page back. - Replace the Services view-switch test that asserted on mocked hooks with one that runs the real paging hooks, ListView and router and checks the request limit, URL and displayed page. - Move NO_URL_FILTERS to Services.constants.ts; it needs a stable reference so useTableFilters keeps its parsed filters memoized. --- .../Settings/Services/Services.constants.ts | 14 ++ .../Services/Services.paging.test.tsx | 120 ++++++++++++++++++ .../Settings/Services/Services.test.tsx | 55 +------- .../components/Settings/Services/Services.tsx | 4 +- .../atoms/compositions/useListingData.tsx | 32 +++-- .../compositions/useListingData.url.test.tsx | 82 ++++++++++++ 6 files changed, 237 insertions(+), 70 deletions(-) create mode 100644 openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Services.constants.ts create mode 100644 openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Services.paging.test.tsx create mode 100644 openmetadata-ui/src/main/resources/ui/src/components/common/atoms/compositions/useListingData.url.test.tsx diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Services.constants.ts b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Services.constants.ts new file mode 100644 index 000000000000..a53d70487507 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Services.constants.ts @@ -0,0 +1,14 @@ +/* + * Copyright 2026 Collate. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export const NO_URL_FILTERS = {}; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Services.paging.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Services.paging.test.tsx new file mode 100644 index 000000000000..8eacef5570a5 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Services.paging.test.tsx @@ -0,0 +1,120 @@ +/* + * Copyright 2026 Collate. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { + act, + fireEvent, + render, + screen, + waitFor, +} from '@testing-library/react'; +import { ReactNode } from 'react'; +import { MemoryRouter, useLocation } from 'react-router-dom'; +import { PIPELINE_SERVICE_PLATFORM } from '../../../constants/Services.constant'; +import { ServiceCategory } from '../../../enums/service.enum'; +import { getServices } from '../../../rest/serviceAPI'; +import Services from './Services'; + +jest.mock('../../../rest/serviceAPI', () => ({ + getServices: jest.fn().mockResolvedValue({ + data: [ + { + id: 'service-1', + name: 'airbyte', + fullyQualifiedName: 'airbyte', + serviceType: 'Airbyte', + }, + ], + paging: { total: 40, after: 'next-cursor' }, + }), + searchService: jest.fn(), +})); + +jest.mock('../../../context/PermissionProvider/PermissionProvider', () => ({ + usePermissionProvider: () => ({ permissions: {} }), +})); + +jest.mock( + '../../../context/AirflowStatusProvider/AirflowStatusProvider', + () => ({ + useAirflowStatus: () => ({ + isFetchingStatus: false, + isAirflowAvailable: true, + fetchAirflowStatus: jest.fn(), + platform: PIPELINE_SERVICE_PLATFORM, + }), + }) +); + +jest.mock('../../../utils/PermissionsUtils', () => ({ + checkPermission: () => true, +})); + +jest.mock('../../../hoc/LimitWrapper', () => + jest.fn(({ children }: { children: ReactNode }) => <>{children}) +); + +jest.mock('../../PageHeader/PageHeader.component', () => + jest.fn(() =>
) +); + +jest.mock('../../common/DomainFilterChip/DomainFilterChip.component', () => ({ + DomainFilterChip: () => null, +})); + +const mockGetServices = getServices as jest.Mock; + +const LocationProbe = () => { + const { search } = useLocation(); + + return
{search}
; +}; + +const renderServices = (url: string) => + render( + + + + + ); + +describe('Services paging across a view switch', () => { + beforeEach(() => { + mockGetServices.mockClear(); + }); + + it('pages the card view on the grid scale from page 1 and drops the list paging from the URL', async () => { + await act(async () => { + renderServices('/settings/services/pipelines?currentPage=2&pageSize=25'); + }); + + expect(mockGetServices).toHaveBeenLastCalledWith( + expect.objectContaining({ limit: 25 }) + ); + + await act(async () => { + fireEvent.click(screen.getByTestId('grid')); + }); + + await waitFor(() => { + expect(mockGetServices).toHaveBeenLastCalledWith( + expect.objectContaining({ limit: 12 }) + ); + }); + + const search = screen.getByTestId('location-search').textContent ?? ''; + + expect(search).not.toContain('currentPage'); + expect(search).not.toContain('pageSize'); + expect(screen.getByTestId('page-indicator')).toHaveTextContent('1'); + }); +}); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Services.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Services.test.tsx index c264a1f246ec..11a7dce3bf39 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Services.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Services.test.tsx @@ -12,11 +12,7 @@ */ import { act, fireEvent, render, screen } from '@testing-library/react'; import React, { ReactNode } from 'react'; -import { - DISABLED, - GRID_PAGE_SIZE_OPTIONS, - LIST_PAGE_SIZE_OPTIONS, -} from '../../../constants/constants'; +import { DISABLED } from '../../../constants/constants'; import { PAGE_HEADERS } from '../../../constants/PageHeaders.constant'; import { PIPELINE_SERVICE_PLATFORM } from '../../../constants/Services.constant'; import { useAirflowStatus } from '../../../context/AirflowStatusProvider/AirflowStatusProvider'; @@ -27,7 +23,6 @@ import { usePaging } from '../../../hooks/paging/usePaging'; import { getServices, searchService } from '../../../rest/serviceAPI'; import { checkPermission } from '../../../utils/PermissionsUtils'; import { ListView } from '../../common/ListView/ListView.component'; -import { ListViewOptions } from '../../common/ListView/ListView.interface'; import { ColumnsType } from '../../common/Table/Table.interface'; import Services from './Services'; @@ -119,12 +114,10 @@ jest.mock('../../../context/PermissionProvider/PermissionProvider', () => ({ }), })); -const mockSetPagingUrlParams = jest.fn(); - jest.mock('../../../hooks/useTableFilters', () => ({ useTableFilters: () => ({ filters: {}, - setFilters: mockSetPagingUrlParams, + setFilters: jest.fn(), }), })); @@ -222,19 +215,8 @@ jest.mock('../../common/ListView/ListView.component', () => ({ tableProps, searchProps, handleDeletedSwitchChange, - onViewChange, }) => (
-