diff --git a/src/gitops/components/application/ApplicationResourcesView.tsx b/src/gitops/components/application/ApplicationResourcesView.tsx index 0e2ea4bb4..1299c95e6 100644 --- a/src/gitops/components/application/ApplicationResourcesView.tsx +++ b/src/gitops/components/application/ApplicationResourcesView.tsx @@ -25,7 +25,11 @@ import { DataViewTh, DataViewTr } from '@patternfly/react-data-view/dist/esm/Dat import { CubesIcon } from '@patternfly/react-icons'; import { Tbody, Td, Tr } from '@patternfly/react-table'; -import { GitOpsDataViewTable, useGitOpsDataViewSort } from '../shared/DataView'; +import { + GitOpsDataViewTable, + useGitOpsDataViewSort, + useGitOpsListPagePagination, +} from '../shared/DataView'; import ResourceActionsCell from '../shared/ResourceActionsCell/ResourceActionsCell'; import { ApplicationGraphView } from './graph/ApplicationGraphView'; @@ -57,7 +61,8 @@ const ApplicationResourcesView: React.FC = ({ [], ); - const { sortBy, direction, getSortParams } = useGitOpsDataViewSort(columnSortConfig); + const { searchParams, sortBy, direction, getSortParams } = + useGitOpsDataViewSort(columnSortConfig); const columnsDV = useResourceColumnsDV(getSortParams); const sortedResources = React.useMemo( () => sortData(resources, sortBy, direction), @@ -79,8 +84,18 @@ const ApplicationResourcesView: React.FC = ({ resourceFilters, ); + const { + pagination, + pagedItems: pagedResources, + itemCount, + } = useGitOpsListPagePagination({ + items: filteredResources, + namespace: application?.metadata?.namespace, + searchParams, + }); + const isEmptyResources = filteredResources.length === 0; - const rows = useResourceRowsDV(filteredResources, application, argoBaseURL); + const rows = useResourceRowsDV(pagedResources, application, argoBaseURL); const isListView = viewType === ApplicationResourcesViewType.list; const empty = ( @@ -141,6 +156,8 @@ const ApplicationResourcesView: React.FC = ({ emptyState={empty} isEmpty={isEmptyResources} activeState={isEmptyResources ? DataViewState.empty : null} + itemCount={itemCount} + pagination={pagination} /> ) : (
diff --git a/src/gitops/components/application/ApplicationSourcesTab.tsx b/src/gitops/components/application/ApplicationSourcesTab.tsx index 0be06e65d..87d80eb74 100644 --- a/src/gitops/components/application/ApplicationSourcesTab.tsx +++ b/src/gitops/components/application/ApplicationSourcesTab.tsx @@ -1,5 +1,6 @@ import * as React from 'react'; import { RouteComponentProps } from 'react-router'; +import { useSearchParams } from 'react-router-dom-v5-compat'; import { useArgoServer } from '@gitops/hooks/useArgoServer'; import { ApplicationKind, ApplicationSource } from '@gitops/models/ApplicationModel'; @@ -23,7 +24,7 @@ import { CubesIcon, GithubIcon } from '@patternfly/react-icons'; import { Tbody, Td, Tr } from '@patternfly/react-table'; import ArgoCDLink from '../shared/ArgoCDLink/ArgoCDLink'; -import { GitOpsDataViewTable } from '../shared/DataView'; +import { GitOpsDataViewTable, useGitOpsListPagePagination } from '../shared/DataView'; type ApplicationDetailsTabProps = RouteComponentProps<{ ns: string; @@ -181,7 +182,17 @@ export const useRowsDV = (sources: ApplicationSource[]): DataViewTr[] => { export const SourceList: React.FC = ({ sources, obj, argoServer }) => { const columns = useColumnsDV(); - const rows = useRowsDV(sources); + const [searchParams] = useSearchParams(); + const { + pagination, + pagedItems: pagedSources, + itemCount, + } = useGitOpsListPagePagination({ + items: sources, + namespace: obj?.metadata?.namespace, + searchParams, + }); + const rows = useRowsDV(pagedSources); const argoUrl = getApplicationArgoUrl(argoServer, obj); const empty = ( @@ -215,7 +226,9 @@ export const SourceList: React.FC = ({ sources, obj, argoServer rows={rows} columns={columns} emptyState={empty} - isEmpty={rows.length === 0} + isEmpty={sources.length === 0} + itemCount={itemCount} + pagination={pagination} /> ); diff --git a/src/gitops/components/application/ApplicationSyncStatusTab.tsx b/src/gitops/components/application/ApplicationSyncStatusTab.tsx index c4052acc1..af050cdc5 100644 --- a/src/gitops/components/application/ApplicationSyncStatusTab.tsx +++ b/src/gitops/components/application/ApplicationSyncStatusTab.tsx @@ -34,7 +34,11 @@ import { CubesIcon } from '@patternfly/react-icons'; import { Tbody, Td, ThProps, Tr } from '@patternfly/react-table'; import { DetailsDescriptionGroup } from '../shared/BaseDetailsSummary/BaseDetailsSummary'; -import { GitOpsDataViewTable, useGitOpsDataViewSort } from '../shared/DataView'; +import { + GitOpsDataViewTable, + useGitOpsDataViewSort, + useGitOpsListPagePagination, +} from '../shared/DataView'; import ResourceActionsCell from '../shared/ResourceActionsCell/ResourceActionsCell'; import { ConditionsPopover } from './Conditions/ConditionsPopover'; @@ -62,13 +66,24 @@ const ApplicationSyncStatusTab: React.FC = ({ obj [], ); - const { sortBy, direction, getSortParams } = useGitOpsDataViewSort(columnSortConfig); + const { searchParams, sortBy, direction, getSortParams } = + useGitOpsDataViewSort(columnSortConfig); const columnsDV = useResourceColumnsDV(getSortParams); const sortedResources = React.useMemo(() => { return sortData(resources, sortBy, direction); }, [resources, sortBy, direction]); - const rows = useResourceRowsDV(sortedResources, obj, argoUrl); + const { + pagination, + pagedItems: pagedResources, + itemCount, + } = useGitOpsListPagePagination({ + items: sortedResources, + namespace: obj?.metadata?.namespace, + searchParams, + }); + + const rows = useResourceRowsDV(pagedResources, obj, argoUrl); const empty = ( @@ -247,6 +262,8 @@ const ApplicationSyncStatusTab: React.FC = ({ obj emptyState={empty} isEmpty={sortedResources.length === 0} activeState={resources.length === 0 ? DataViewState.empty : null} + itemCount={itemCount} + pagination={pagination} />
diff --git a/src/gitops/components/application/History/History.tsx b/src/gitops/components/application/History/History.tsx index bb7619043..410c6f77f 100644 --- a/src/gitops/components/application/History/History.tsx +++ b/src/gitops/components/application/History/History.tsx @@ -14,7 +14,11 @@ import { DataViewTh, DataViewTr } from '@patternfly/react-data-view/dist/esm/Dat import { CubesIcon } from '@patternfly/react-icons'; import { Tbody, Td, ThProps, Tr } from '@patternfly/react-table'; -import { GitOpsDataViewTable, useGitOpsDataViewSort } from '../../shared/DataView'; +import { + GitOpsDataViewTable, + useGitOpsDataViewSort, + useGitOpsListPagePagination, +} from '../../shared/DataView'; import './History.scss'; @@ -30,14 +34,26 @@ const HistoryList: React.FC = ({ history, obj }) => { [], ); - const { sortBy, direction, getSortParams } = useGitOpsDataViewSort(columnSortConfig); + const { searchParams, sortBy, direction, getSortParams } = + useGitOpsDataViewSort(columnSortConfig); const columnsDV = useColumnsDV(getSortParams); - const sortedHistory = React.useMemo(() => { - return sortData(history, sortBy, direction); - }, [history, sortBy, direction]); + const displayHistory = React.useMemo( + () => sortData([...history].reverse(), sortBy, direction), + [history, sortBy, direction], + ); + + const { + pagination, + pagedItems: pagedHistory, + itemCount, + } = useGitOpsListPagePagination({ + items: displayHistory, + namespace: obj?.metadata?.namespace, + searchParams, + }); - const rows = useRowsDV(sortedHistory, obj); + const rows = useRowsDV(pagedHistory, obj); const argoServer = useArgoServer(obj); const argoUrl = getApplicationArgoUrl(argoServer, obj); @@ -75,7 +91,9 @@ const HistoryList: React.FC = ({ history, obj }) => { rows={rows} columns={columnsDV} emptyState={empty} - isEmpty={rows.length === 0} + isEmpty={displayHistory.length === 0} + itemCount={itemCount} + pagination={pagination} /> ); @@ -177,7 +195,7 @@ const useRowsDV = (history: ApplicationHistory[], app: ApplicationKind): DataVie }, ]); }); - return rows.reverse(); + return rows; }; const useColumnsDV = (getSortParams: (columnIndex: number) => ThProps['sort']) => { diff --git a/src/gitops/components/imageupdater/ImageUpdaterRecentUpdatesTab.tsx b/src/gitops/components/imageupdater/ImageUpdaterRecentUpdatesTab.tsx index f3c5c686a..260ffea71 100644 --- a/src/gitops/components/imageupdater/ImageUpdaterRecentUpdatesTab.tsx +++ b/src/gitops/components/imageupdater/ImageUpdaterRecentUpdatesTab.tsx @@ -15,7 +15,11 @@ import { Tbody, Td, ThProps, Tr } from '@patternfly/react-table'; import { ImageUpdaterKind, ImageUpdaterRecentUpdate } from '../../models/ImageUpdaterModel'; import { useGitOpsTranslation } from '../../utils/hooks/useGitOpsTranslation'; -import { GitOpsDataViewTable, useGitOpsDataViewSort } from '../shared/DataView'; +import { + GitOpsDataViewTable, + useGitOpsDataViewSort, + useGitOpsListPagePagination, +} from '../shared/DataView'; type ImageUpdaterRecentUpdatesTabProps = RouteComponentProps<{ ns: string; name: string }> & { obj?: ImageUpdaterKind; @@ -32,7 +36,8 @@ const ImageUpdaterRecentUpdatesTab: React.FC [], ); - const { sortBy, direction, getSortParams } = useGitOpsDataViewSort(columnSortConfig); + const { searchParams, sortBy, direction, getSortParams } = + useGitOpsDataViewSort(columnSortConfig); const columnsDV = useColumnsDV(getSortParams, t); @@ -41,7 +46,17 @@ const ImageUpdaterRecentUpdatesTab: React.FC return sortData(updates, sortBy, direction); }, [obj, sortBy, direction]); - const rows = useRowsDV(sortedUpdates); + const { + pagination, + pagedItems: pagedUpdates, + itemCount, + } = useGitOpsListPagePagination({ + items: sortedUpdates, + namespace: obj?.metadata?.namespace, + searchParams, + }); + + const rows = useRowsDV(pagedUpdates); if (!obj) { return null; @@ -74,7 +89,9 @@ const ImageUpdaterRecentUpdatesTab: React.FC rows={rows} columns={columnsDV} emptyState={empty} - isEmpty={rows.length === 0} + isEmpty={sortedUpdates.length === 0} + itemCount={itemCount} + pagination={pagination} /> diff --git a/src/gitops/components/project/ProjectRolesTab.tsx b/src/gitops/components/project/ProjectRolesTab.tsx index a4db7ae23..4e53d577e 100644 --- a/src/gitops/components/project/ProjectRolesTab.tsx +++ b/src/gitops/components/project/ProjectRolesTab.tsx @@ -19,7 +19,11 @@ import { AppProjectKind, Role } from '../../models/AppProjectModel'; import { ArgoServer, getArgoServerForProject } from '../../utils/gitops'; import { useGitOpsTranslation } from '../../utils/hooks/useGitOpsTranslation'; import { ArgoCDLink } from '../shared/ArgoCDLink/ArgoCDLink'; -import { GitOpsDataViewTable, useGitOpsDataViewSort } from '../shared/DataView'; +import { + GitOpsDataViewTable, + useGitOpsDataViewSort, + useGitOpsListPagePagination, +} from '../shared/DataView'; /** * Parses an Argo CD policy string and returns a formatted React element for tooltip @@ -102,13 +106,24 @@ const ProjectRolesTab: React.FC = ({ obj }) => { [], ); - const { sortBy, direction, getSortParams } = useGitOpsDataViewSort(columnSortConfig); + const { searchParams, sortBy, direction, getSortParams } = + useGitOpsDataViewSort(columnSortConfig); const columnsDV = useRolesColumnsDV(getSortParams, t); const sortedRoles = React.useMemo(() => { return sortRolesData(roles, sortBy, direction); }, [roles, sortBy, direction]); - const rows = useRolesRowsDV(sortedRoles, t); + const { + pagination, + pagedItems: pagedRoles, + itemCount, + } = useGitOpsListPagePagination({ + items: sortedRoles, + namespace: obj?.metadata?.namespace, + searchParams, + }); + + const rows = useRolesRowsDV(pagedRoles, t); if (!obj) return null; @@ -142,6 +157,8 @@ const ProjectRolesTab: React.FC = ({ obj }) => { rows={rows} isEmpty={roles.length === 0} emptyState={empty} + itemCount={itemCount} + pagination={pagination} /> ); diff --git a/src/gitops/components/project/ProjectSyncWindowsTab.tsx b/src/gitops/components/project/ProjectSyncWindowsTab.tsx index 997a9ffa6..3a725e26b 100644 --- a/src/gitops/components/project/ProjectSyncWindowsTab.tsx +++ b/src/gitops/components/project/ProjectSyncWindowsTab.tsx @@ -12,7 +12,11 @@ import { AppProjectKind, SyncWindow } from '../../models/AppProjectModel'; import { ArgoServer, getArgoServerForProject } from '../../utils/gitops'; import { useGitOpsTranslation } from '../../utils/hooks/useGitOpsTranslation'; import { ArgoCDLink } from '../shared/ArgoCDLink/ArgoCDLink'; -import { GitOpsDataViewTable, useGitOpsDataViewSort } from '../shared/DataView'; +import { + GitOpsDataViewTable, + useGitOpsDataViewSort, + useGitOpsListPagePagination, +} from '../shared/DataView'; type ProjectSyncWindowsTabProps = RouteComponentProps<{ ns: string; name: string }> & { obj?: AppProjectKind; @@ -52,13 +56,24 @@ const ProjectSyncWindowsTab: React.FC = ({ obj }) => [], ); - const { sortBy, direction, getSortParams } = useGitOpsDataViewSort(columnSortConfig); + const { searchParams, sortBy, direction, getSortParams } = + useGitOpsDataViewSort(columnSortConfig); const columnsDV = useSyncWindowsColumnsDV(getSortParams, t); const sortedSyncWindows = React.useMemo(() => { return sortSyncWindowsData(syncWindows, sortBy, direction); }, [syncWindows, sortBy, direction]); - const rows = useSyncWindowsRowsDV(sortedSyncWindows, t); + const { + pagination, + pagedItems: pagedSyncWindows, + itemCount, + } = useGitOpsListPagePagination({ + items: sortedSyncWindows, + namespace: obj?.metadata?.namespace, + searchParams, + }); + + const rows = useSyncWindowsRowsDV(pagedSyncWindows, t); if (!obj) return null; @@ -96,6 +111,8 @@ const ProjectSyncWindowsTab: React.FC = ({ obj }) => rows={rows} isEmpty={syncWindows.length === 0} emptyState={empty} + itemCount={itemCount} + pagination={pagination} /> ); diff --git a/src/gitops/components/rollout/components/PodList/PodList.tsx b/src/gitops/components/rollout/components/PodList/PodList.tsx index 7eefea6ba..f944899f2 100644 --- a/src/gitops/components/rollout/components/PodList/PodList.tsx +++ b/src/gitops/components/rollout/components/PodList/PodList.tsx @@ -1,7 +1,11 @@ import * as React from 'react'; import { useTranslation } from 'react-i18next'; -import { GitOpsDataViewTable, useGitOpsDataViewSort } from '@gitops/components/shared/DataView'; +import { + GitOpsDataViewTable, + useGitOpsDataViewSort, + useGitOpsListPagePagination, +} from '@gitops/components/shared/DataView'; import { PodTraffic } from '@gitops/topology/console/pod-traffic'; import { podPhase } from '@gitops/topology/console/PodsOverview'; import { PodKind } from '@gitops/topology/console/types'; @@ -447,6 +451,16 @@ export const PodList: React.FC = ({ rollout, namespace, selector } }); }, [filteredData, searchQuery]); + const { + pagination, + pagedItems: pagedPods, + itemCount, + } = useGitOpsListPagePagination({ + items: filteredBySearch, + namespace, + searchParams, + }); + const empty = ( @@ -461,7 +475,7 @@ export const PodList: React.FC = ({ rollout, namespace, selector } const isEmptyState = !loadError && filteredBySearch.length === 0; - const rows = usePodRowsDV(filteredBySearch, memResults, cpuResults, namespace); + const rows = usePodRowsDV(pagedPods, memResults, cpuResults, namespace); const topologyUrl = rollout?.metadata?.namespace ? '/topology/ns/' + @@ -490,6 +504,8 @@ export const PodList: React.FC = ({ rollout, namespace, selector } isEmpty={isEmptyState} emptyState={empty} isError={!!loadError} + itemCount={itemCount} + pagination={pagination} /> diff --git a/src/gitops/components/shared/DataView/gitOpsDataViewPagination.test.ts b/src/gitops/components/shared/DataView/gitOpsDataViewPagination.test.ts index 5df364c09..2a60a3c5d 100644 --- a/src/gitops/components/shared/DataView/gitOpsDataViewPagination.test.ts +++ b/src/gitops/components/shared/DataView/gitOpsDataViewPagination.test.ts @@ -1,4 +1,9 @@ -import { getGitOpsPaginationResetKey, paginateItems } from './gitOpsDataViewPagination'; +import { + getGitOpsPaginationResetKey, + GITOPS_DEFAULT_PER_PAGE, + GITOPS_PER_PAGE_OPTIONS, + paginateItems, +} from './gitOpsDataViewPagination'; describe('paginateItems', () => { const items = ['a', 'b', 'c', 'd', 'e']; @@ -83,4 +88,94 @@ describe('getGitOpsPaginationResetKey', () => { getGitOpsPaginationResetKey('argocd', labeled), ); }); + + it('resets for Application Resources list filters but not for page changes', () => { + const filtered = new URLSearchParams( + 'page=2&rowFilter-resource-sync=Synced&rowFilter-resource-kind=Deployment', + ); + const nextPage = new URLSearchParams( + 'page=4&rowFilter-resource-sync=Synced&rowFilter-resource-kind=Deployment', + ); + const otherKind = new URLSearchParams( + 'page=2&rowFilter-resource-sync=Synced&rowFilter-resource-kind=Service', + ); + + expect(getGitOpsPaginationResetKey('openshift-gitops', filtered)).toBe( + getGitOpsPaginationResetKey('openshift-gitops', nextPage), + ); + expect(getGitOpsPaginationResetKey('openshift-gitops', filtered)).not.toBe( + getGitOpsPaginationResetKey('openshift-gitops', otherKind), + ); + }); +}); + +describe('GitOps detail-tab pagination contract', () => { + it('uses Console-compatible page size defaults', () => { + expect(GITOPS_DEFAULT_PER_PAGE).toBe(50); + expect(GITOPS_PER_PAGE_OPTIONS.map((option) => option.value)).toEqual([10, 20, 50, 100]); + }); + + it('paginates AppProject roles after sort', () => { + const roles = [ + { name: 'ci-role' }, + { name: 'read-only' }, + { name: 'admin' }, + { name: 'developer' }, + ].sort((a, b) => a.name.localeCompare(b.name)); + + expect(paginateItems(roles, 1, 2).map((role) => role.name)).toEqual(['admin', 'ci-role']); + expect(paginateItems(roles, 2, 2).map((role) => role.name)).toEqual([ + 'developer', + 'read-only', + ]); + }); + + it('paginates AppProject sync windows', () => { + const windows = [ + { kind: 'allow', schedule: '0 9 * * 1-5' }, + { kind: 'deny', schedule: '0 22 * * *' }, + { kind: 'allow', schedule: '0 12 * * 6' }, + ]; + + expect(paginateItems(windows, 1, 2)).toEqual([ + { kind: 'allow', schedule: '0 9 * * 1-5' }, + { kind: 'deny', schedule: '0 22 * * *' }, + ]); + expect(paginateItems(windows, 2, 2)).toEqual([{ kind: 'allow', schedule: '0 12 * * 6' }]); + }); + + it('paginates ImageUpdater recent updates newest-first display lists', () => { + const updates = [ + { alias: 'test-nginx', newVersion: '1.17.11' }, + { alias: 'test-memcached', newVersion: '1.6.12' }, + { alias: 'test-redis', newVersion: '7.2.0' }, + ]; + + expect(paginateItems(updates, 1, 2).map((update) => update.alias)).toEqual([ + 'test-nginx', + 'test-memcached', + ]); + expect(paginateItems(updates, 2, 2).map((update) => update.alias)).toEqual(['test-redis']); + }); + + it('paginates Application history after reversing for newest-first display', () => { + const history = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }]; + // Default (no column sort): reverse source order, then paginate. + const displayHistory = [...history].reverse(); + + expect(paginateItems(displayHistory, 1, 2).map((entry) => entry.id)).toEqual([4, 3]); + expect(paginateItems(displayHistory, 2, 2).map((entry) => entry.id)).toEqual([2, 1]); + }); + + it('keeps an explicit sort order when paginating history (no post-sort reverse)', () => { + const sortedAsc = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }]; + + expect(paginateItems(sortedAsc, 1, 2).map((entry) => entry.id)).toEqual([1, 2]); + expect(paginateItems(sortedAsc, 2, 2).map((entry) => entry.id)).toEqual([3, 4]); + }); + + it('yields no rows for empty detail-tab tables (no pager expected)', () => { + expect(paginateItems([], 1, GITOPS_DEFAULT_PER_PAGE)).toEqual([]); + expect(paginateItems(undefined, 1, GITOPS_DEFAULT_PER_PAGE)).toEqual([]); + }); });