From acbdfa86a51d0690dd37887cc9d02e3c7aae808a Mon Sep 17 00:00:00 2001 From: Anna Vovchenko Date: Wed, 22 Oct 2025 18:53:13 +0300 Subject: [PATCH] Fix pagination issue with the pods list Previously, the page is updated to the first page if any changes occur to the pods list. With the change, the page will remain if it exists. If the page is no longer available, it will navigate to previous. Changelog: fixed --- .../components/kubernetes/kubernetes_pods.vue | 7 ++ .../components/workload_layout.vue | 2 + .../components/workload_table.vue | 11 ++- .../kubernetes/kubernetes_pods_spec.js | 27 ++++++ .../components/workload_layout_spec.js | 12 +++ .../components/workload_table_spec.js | 83 +++++++++++++++++++ 6 files changed, 141 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/environments/environment_details/components/kubernetes/kubernetes_pods.vue b/app/assets/javascripts/environments/environment_details/components/kubernetes/kubernetes_pods.vue index 95b5c90ca9bc8c..03db69c5e04e01 100644 --- a/app/assets/javascripts/environments/environment_details/components/kubernetes/kubernetes_pods.vue +++ b/app/assets/javascripts/environments/environment_details/components/kubernetes/kubernetes_pods.vue @@ -157,6 +157,12 @@ export default { k8sPods() { this.$emit('update-cluster-state', this.podsHealthStatus); }, + podsSearch() { + this.$refs.workloadTable?.resetPagination(); + }, + statusFilter() { + this.$refs.workloadTable?.resetPagination(); + }, }, methods: { search(searchTerm, podName) { @@ -222,6 +228,7 @@ export default { this.totalPages && this.totalPages > 0) { + this.currentPage = this.totalPages; + } }, }, methods: { @@ -66,6 +71,10 @@ export default { return actions.find((action) => action.name === 'delete-pod') || null; }, + // eslint-disable-next-line vue/no-unused-properties -- triggered from outside of the component + resetPagination() { + this.currentPage = 1; + }, }, i18n: { emptyText: __('No results found'), diff --git a/spec/frontend/environments/environment_details/components/kubernetes/kubernetes_pods_spec.js b/spec/frontend/environments/environment_details/components/kubernetes/kubernetes_pods_spec.js index 77e0771024d17f..f327549a54e1ce 100644 --- a/spec/frontend/environments/environment_details/components/kubernetes/kubernetes_pods_spec.js +++ b/spec/frontend/environments/environment_details/components/kubernetes/kubernetes_pods_spec.js @@ -2,6 +2,7 @@ import Vue, { nextTick } from 'vue'; import VueApollo from 'vue-apollo'; import { GlLoadingIcon, GlTab, GlSearchBoxByType, GlSprintf } from '@gitlab/ui'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import { stubComponent } from 'helpers/stub_component'; import waitForPromises from 'helpers/wait_for_promises'; import createMockApollo from 'helpers/mock_apollo_helper'; import KubernetesPods from '~/environments/environment_details/components/kubernetes/kubernetes_pods.vue'; @@ -28,6 +29,8 @@ describe('~/environments/environment_details/components/kubernetes/kubernetes_po }, }; + const resetPaginationSpy = jest.fn(); + const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); const findTab = () => wrapper.findComponent(GlTab); const findWorkloadStats = () => wrapper.findComponent(WorkloadStats); @@ -52,6 +55,11 @@ describe('~/environments/environment_details/components/kubernetes/kubernetes_po stubs: { GlTab, GlSprintf, + WorkloadTable: stubComponent(WorkloadTable, { + methods: { + resetPagination: resetPaginationSpy, + }, + }), }, }); }; @@ -151,6 +159,17 @@ describe('~/environments/environment_details/components/kubernetes/kubernetes_po expect(findWorkloadTable().props('items')).toMatchObject(filteredPods); }); + it('resets pagination when status filter changes', async () => { + createWrapper(); + await waitForPromises(); + + const status = 'Failed'; + findWorkloadStats().vm.$emit('select', status); + await nextTick(); + + expect(resetPaginationSpy).toHaveBeenCalled(); + }); + describe('searching pods', () => { beforeEach(async () => { createWrapper(); @@ -178,6 +197,14 @@ describe('~/environments/environment_details/components/kubernetes/kubernetes_po expect(findWorkloadTable().props('items')).toMatchObject(filteredPods); }); + it('resets pagination when search changes', async () => { + const searchTerm = 'pod-2'; + findSearchBox().vm.$emit('input', searchTerm); + await nextTick(); + + expect(resetPaginationSpy).toHaveBeenCalled(); + }); + it('shows the correct pod counters in the workload stats', async () => { const searchTerm = 'pod-4'; diff --git a/spec/frontend/kubernetes_dashboard/components/workload_layout_spec.js b/spec/frontend/kubernetes_dashboard/components/workload_layout_spec.js index bafe3939745e99..fb6c7b690f8555 100644 --- a/spec/frontend/kubernetes_dashboard/components/workload_layout_spec.js +++ b/spec/frontend/kubernetes_dashboard/components/workload_layout_spec.js @@ -16,6 +16,7 @@ const defaultProps = { }; const toggleDetailsDrawerSpy = jest.fn(); +const resetPaginationSpy = jest.fn(); const createWrapper = (propsData = {}) => { wrapper = shallowMount(WorkloadLayout, { @@ -27,6 +28,9 @@ const createWrapper = (propsData = {}) => { WorkloadDetailsDrawer: stubComponent(WorkloadDetailsDrawer, { methods: { toggle: toggleDetailsDrawerSpy }, }), + WorkloadTable: stubComponent(WorkloadTable, { + methods: { resetPagination: resetPaginationSpy }, + }), }, }); }; @@ -125,6 +129,14 @@ describe('Workload layout component', () => { const filteredItems = mockPodsTableItems.filter((item) => item.status === status); expect(findWorkloadTable().props('items')).toMatchObject(filteredItems); }); + + it('resets pagination when filter changes', async () => { + const status = 'Failed'; + findWorkloadStats().vm.$emit('select', status); + await nextTick(); + + expect(resetPaginationSpy).toHaveBeenCalled(); + }); }); describe('drawer', () => { diff --git a/spec/frontend/kubernetes_dashboard/components/workload_table_spec.js b/spec/frontend/kubernetes_dashboard/components/workload_table_spec.js index 822958c53cd6dd..7fdd95bba02ac9 100644 --- a/spec/frontend/kubernetes_dashboard/components/workload_table_spec.js +++ b/spec/frontend/kubernetes_dashboard/components/workload_table_spec.js @@ -215,6 +215,89 @@ describe('Workload table component', () => { expect(findTable().props('currentPage')).toBe(2); }); + + describe('pagination behavior on items change', () => { + const largeDataset = Array.from({ length: 25 }, (_, i) => ({ + ...mockPodsTableItems[0], + name: `pod-${i}`, + })); + + beforeEach(() => { + createWrapper({ items: largeDataset, pageSize: 10 }, true); + }); + + it('preserves current page when items are updated but page is still valid', async () => { + findPagination().vm.$emit('input', 2); + await nextTick(); + + expect(findTable().props('currentPage')).toBe(2); + + const updatedItems = largeDataset.map((item) => ({ ...item, status: 'Running' })); + await wrapper.setProps({ items: updatedItems }); + + expect(findTable().props('currentPage')).toBe(2); + }); + + it('navigates to last page when current page becomes invalid due to fewer items', async () => { + findPagination().vm.$emit('input', 3); + await nextTick(); + expect(findTable().props('currentPage')).toBe(3); + + const reducedItems = largeDataset.slice(0, 15); + await wrapper.setProps({ items: reducedItems }); + + expect(findTable().props('currentPage')).toBe(2); + }); + + it('navigates to page 1 when all items are significantly reduced', async () => { + findPagination().vm.$emit('input', 3); + await nextTick(); + expect(findTable().props('currentPage')).toBe(3); + + const reducedItems = largeDataset.slice(0, 5); + await wrapper.setProps({ items: reducedItems }); + + expect(findTable().props('currentPage')).toBe(1); + }); + + it('preserves current page when items are added', async () => { + findPagination().vm.$emit('input', 2); + await nextTick(); + expect(findTable().props('currentPage')).toBe(2); + + const expandedItems = [ + ...largeDataset, + ...Array.from({ length: 10 }, (_, i) => ({ + ...mockPodsTableItems[0], + name: `new-pod-${i}`, + })), + ]; + await wrapper.setProps({ items: expandedItems }); + + expect(findTable().props('currentPage')).toBe(2); + }); + }); + + describe('resetPagination method', () => { + it('resets current page to 1 when called', async () => { + const largeDataset = Array.from({ length: 25 }, (_, i) => ({ + ...mockPodsTableItems[0], + name: `pod-${i}`, + })); + + createWrapper({ items: largeDataset, pageSize: 10 }, true); + + findPagination().vm.$emit('input', 2); + await nextTick(); + expect(findTable().props('currentPage')).toBe(2); + + // Note: This method can only be triggered from outside of the component + wrapper.vm.resetPagination(); + await nextTick(); + + expect(findTable().props('currentPage')).toBe(1); + }); + }); }); describe('item selection', () => { -- GitLab