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 95b5c90ca9bc8cfbff08fe44aa71cef4d48c58b6..03db69c5e04e0165d40fca9af32128a9da8171ab 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 77e0771024d17fabf6e3f4cb6f4389884849e9fb..f327549a54e1cec50b76fc72808c3c0e0bcd17d3 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 bafe3939745e996805e3bb2211040b86d4b15ca7..fb6c7b690f855539902fb533f7020ef89b72581b 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 822958c53cd6ddfd73aee4e68fd06462a3ae516b..7fdd95bba02ac95db35fff83e2b0def78607530a 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', () => {