From f980283813a608d102b04a2c330f2813027343c8 Mon Sep 17 00:00:00 2001 From: Richard Cox <18697775+richard-cox@users.noreply.github.com> Date: Tue, 7 Jan 2025 22:13:42 +0000 Subject: [PATCH] Add server-side pagination to cluster explorer lists (#11672) * cluster list now uses resource table * New PaginatedResourceTable * WIP * add context to pag setting * tidying up * Add server-side pagination to cluster explorer events and general events list * Servier-side pagination for catalog apps, cluster repo and operation * Servier-side pagination for Service --> HPA, inmgress, services * Server-side pagination for home page clusters list and side bar clusters - Functional Changes - SSP now works after vue3 bump - Home Page Clusters list now uses server-side pagination - Side Bar clusters list now uses server-side pagination - Wire in now supported sorting / filtering by id and name used for table columns - Allow pagination to be enabled given a specific context - Call findPage without persisting to store - New Pagination Tools - PaginatedResourceTable - Convenience Component, wraps ResourceTable with pagination specific props - PaginationWrapper - Convenience class to handle requests for resources and updates to them (avoiding store) - Regressions - Side Nav menu ready state was `mgmtCluster.isReady && !pCluster?.hasError`, now ??? * fixes after merge. apply PagResTable to node list, vue3 fixes * - Fix issue where list components containing PaginatedResourceTables would incorrectly fetch all resources anyway - convert reminaing storage lists * policy section * fix async button in manual refresh mode * fgh * aaaaa * Server-side pagination for home page clusters list and side bar clusters - Functional Changes - SSP now works after vue3 bump - Home Page Clusters list now uses server-side pagination - Side Bar clusters list now uses server-side pagination - Wire in now supported sorting / filtering by id and name used for table columns - Allow pagination to be enabled given a specific context - Call findPage without persisting to store - New Pagination Tools - PaginatedResourceTable - Convenience Component, wraps ResourceTable with pagination specific props - PaginationWrapper - Convenience class to handle requests for resources and updates to them (avoiding store) - Regressions - Side Nav menu ready state was `mgmtCluster.isReady && !pCluster?.hasError`, now ??? * Iteration Note - prov clusters is broken (only fetches local) due to blocking pr. breals - notPinned list * Fix dupe inStore - remove from resource list, put in resource-fetch (used also by pag res table) * Fix dupe inStore - remove from resource list, put in resource-fetch (used also by pag res table) * Two fixes - changes namespaces kicked of side nav cluster requests (thought pinnedIds changed) - fix generic lists re-fetching given ns filter changes (they don't have namespaced arg) * fixes / updates * testing/tidying * WIP - Rest of workload lists * finish off workloads * tidying up * fix non-generic list filtering by namespace * remove comment, backport fix * test fixes * E2E: Ensure we wait for cluster entries to exist before clicking on them * backport fix for local/api filtering * Remove debug code * Changes after review * e2e fixes / debugging * More e2e fixes * More e2e fixes * More e2e fixes * Fix generic pages that filter on pagination * Attempt to fix flaky vai test * Fix after merge from master * Updates following new indexed files * Fix lint and test * Changes given real cluster tests - general fixes - correct issue were sorting prov clusters on mgmt cluster props (issue in master as well...) - bit the bullet, we now don't fetch all mgmt clusters on dashboard visit. - there could be knock on affects, but we'd need to remove it sometime in 2.11.... * Fix issues with diplaying rke1 data in home page - includes https://github.com/rancher/dashboard/pull/12881 * Fix unit tests * remove invalid sort/filter type * Running through new indexed fields * fix unit tests * lint fixes * Fixes after small review * lint * Test for check-plugin-gates pkg build from shell * e2e fixes * Testing new indexed fields with dev image * tidyuing up imports in hope to fix ts error in check-plugins gate * Fix failing check-plugin-gates - caused by shell/scripts/test-plugins-build.sh importing list/catalog.cattle.io.clusterrepo.vue - the component had been updated to a TS component - check-plugin build outputs TS errors for a component file imports - vs code shows no errors for imported file * tidying up a smidge * Many tweaks - Remove final todo's - includes fix for service type clusterip/headless overlap - Removed ununused ENDPOINT column (note ENDPOINT formatter used in other columns) - Testing freshly added index fields * Improve location of new place where componentWillFetch is set * fix lint, comment updated * Fix failing e2e tests - fix cluster dashboard events test - fix and greatly improve flaky events test - fix hpa test - functional fixes - fix sorting/filtering events by object type - fix hpa columns --- cypress/e2e/blueprints/nav/fake-cluster.ts | 2 +- .../po/pages/explorer/cluster-dashboard.po.ts | 2 +- .../side-nav/main-side-menu.spec.ts | 2 +- .../dashboard/cluster-dashboard.spec.ts | 57 +++-- .../pages/explorer/dashboard/events.spec.ts | 188 ++++++++++++----- .../horizontal-pod-autoscalers.spec.ts | 9 +- cypress/globals.d.ts | 8 +- .../support/commands/rancher-api-commands.ts | 41 +++- shell/assets/translations/en-us.yaml | 1 + shell/components/AsyncButton.vue | 12 +- shell/components/PaginatedResourceTable.vue | 5 +- shell/components/ResourceList/index.vue | 6 +- shell/components/ResourceTable.vue | 2 +- shell/components/SortableTable/index.vue | 10 +- .../components/form/ResourceLabeledSelect.vue | 2 +- shell/components/formatter/Endpoints.vue | 2 +- shell/components/formatter/ServiceTargets.vue | 2 +- shell/components/formatter/ServiceType.vue | 36 ++-- shell/components/nav/TopLevelMenu.helper.ts | 2 +- shell/config/pagination-table-headers.js | 10 +- shell/config/product/apps.js | 93 ++++++--- shell/config/product/explorer.js | 196 ++++++++++++++++-- shell/config/settings.ts | 22 +- shell/config/table-headers.js | 25 +-- shell/list/catalog.cattle.io.app.vue | 15 +- shell/list/catalog.cattle.io.clusterrepo.vue | 84 +++++--- shell/list/namespace.vue | 5 +- shell/list/networking.k8s.io.ingress.vue | 25 ++- shell/list/node.vue | 128 ++++++------ shell/list/persistentvolume.vue | 75 +++++-- shell/list/persistentvolumeclaim.vue | 18 +- shell/list/service.vue | 37 ++-- shell/list/workload.vue | 84 ++++---- shell/models/persistentvolume.js | 4 +- shell/models/storage.k8s.io.storageclass.js | 4 + .../pages/c/_cluster/explorer/EventsTable.vue | 74 +++++-- shell/pages/c/_cluster/explorer/index.vue | 19 +- .../pages/c/_cluster/settings/performance.vue | 27 ++- shell/pages/home.vue | 2 +- shell/pages/support/index.vue | 2 +- shell/plugins/steve/steve-pagination-utils.ts | 100 +++++++-- shell/scripts/test-plugins-build.sh | 17 +- shell/scripts/typegen.sh | 1 + shell/store/type-map.utils.ts | 15 +- shell/utils/cluster.js | 4 +- 45 files changed, 996 insertions(+), 479 deletions(-) diff --git a/cypress/e2e/blueprints/nav/fake-cluster.ts b/cypress/e2e/blueprints/nav/fake-cluster.ts index 8391e69528d..de9a85f57be 100644 --- a/cypress/e2e/blueprints/nav/fake-cluster.ts +++ b/cypress/e2e/blueprints/nav/fake-cluster.ts @@ -2474,7 +2474,7 @@ function generateFakeNavClusterData(provClusterId = 'some-prov-cluster-id', mgmt } export function generateFakeClusterDataAndIntercepts(fakeProvClusterId = 'some-prov-cluster-id', fakeMgmtClusterId = 'some-mgmt-cluster-id', addEditClusterCapabilities = false): {} { - const longClusterDescription = 'this-is-some-really-really-really-really-really-really-long-decription'; + const longClusterDescription = 'this-is-some-really-really-really-really-really-really-long-description'; const fakeNavClusterData = generateFakeNavClusterData(fakeProvClusterId, fakeMgmtClusterId, addEditClusterCapabilities); // add cluster to fleet clusters for testing https://github.com/rancher/dashboard/issues/9984 diff --git a/cypress/e2e/po/pages/explorer/cluster-dashboard.po.ts b/cypress/e2e/po/pages/explorer/cluster-dashboard.po.ts index 6eb0c38a2e5..e3e6ad73d71 100644 --- a/cypress/e2e/po/pages/explorer/cluster-dashboard.po.ts +++ b/cypress/e2e/po/pages/explorer/cluster-dashboard.po.ts @@ -57,7 +57,7 @@ export default class ClusterDashboardPagePo extends PagePo { } fullEventsLink() { - return cy.get('.events-table-link').contains('Full events list'); + return cy.get('[data-testid="events-link"]').contains('Full events list'); } fullSecretsList() { diff --git a/cypress/e2e/tests/navigation/side-nav/main-side-menu.spec.ts b/cypress/e2e/tests/navigation/side-nav/main-side-menu.spec.ts index 560a43f17f8..ffc0ca298a8 100644 --- a/cypress/e2e/tests/navigation/side-nav/main-side-menu.spec.ts +++ b/cypress/e2e/tests/navigation/side-nav/main-side-menu.spec.ts @@ -4,7 +4,7 @@ import PagePo from '@/cypress/e2e/po/pages/page.po'; import ProductNavPo from '@/cypress/e2e/po/side-bars/product-side-nav.po'; import { generateFakeClusterDataAndIntercepts } from '@/cypress/e2e/blueprints/nav/fake-cluster'; -const longClusterDescription = 'this-is-some-really-really-really-really-really-really-long-decription'; +const longClusterDescription = 'this-is-some-really-really-really-really-really-really-long-description'; const fakeProvClusterId = 'some-fake-cluster-id'; const fakeMgmtClusterId = 'some-fake-mgmt-id'; diff --git a/cypress/e2e/tests/pages/explorer/dashboard/cluster-dashboard.spec.ts b/cypress/e2e/tests/pages/explorer/dashboard/cluster-dashboard.spec.ts index 739bb22bffa..5ea576f3673 100644 --- a/cypress/e2e/tests/pages/explorer/dashboard/cluster-dashboard.spec.ts +++ b/cypress/e2e/tests/pages/explorer/dashboard/cluster-dashboard.spec.ts @@ -48,7 +48,7 @@ describe('Cluster Dashboard', { testIsolation: 'off', tags: ['@explorer', '@admi clusterDashboard.waitForPage(undefined, 'cluster-events'); - // check if burguer menu nav is highlighted correctly for local cluster + // check if burger menu nav is highlighted correctly for local cluster BurgerMenuPo.checkIfClusterMenuLinkIsHighlighted('local'); }); @@ -257,48 +257,47 @@ describe('Cluster Dashboard', { testIsolation: 'off', tags: ['@explorer', '@admi }); it('can view events table empty if no events', { tags: ['@vai', '@adminUser'] }, () => { - cy.visit(clusterDashboard.urlPath(), { - onBeforeLoad(win) { - cy.stub(win.console, 'error').as('consoleError'); - cy.stub(win.console, 'warn').as('consoleWarn'); - }, - }); - eventsNoDataset(); clusterDashboard.goTo(); - cy.get('@consoleError').should('not.be.called'); // See error lot - cy.get('@consoleWarn').should('not.be.called'); // See warning log (there will be some....) - cy.wait('@eventsNoData'); clusterDashboard.waitForPage(undefined, 'cluster-events'); clusterDashboard.eventsList().resourceTable().sortableTable().checkRowCount(true, 1); - const expectedHeaders = ['Reason', 'Object', 'Message', 'Name', 'Date']; + let expectedHeaders = ['Reason', 'Object', 'Message', 'Name', 'Date']; - clusterDashboard.eventsList().resourceTable().sortableTable().tableHeaderRow() - .within('.table-header-container .content') - .each((el, i) => { - expect(el.text().trim()).to.eq(expectedHeaders[i]); - }); + cy.isVaiCacheEnabled().then((isVaiCacheEnabled) => { + if (isVaiCacheEnabled) { + expectedHeaders = ['Reason', 'Object', 'Message', 'Name', 'First Seen', 'Last Seen', 'Count']; + } - clusterDashboard.fullEventsLink().click(); - cy.wait('@eventsNoData'); - const events = new EventsPagePo('local'); + clusterDashboard.eventsList().resourceTable().sortableTable().tableHeaderRow() + .self() + .scrollIntoView(); + clusterDashboard.eventsList().resourceTable().sortableTable().tableHeaderRow() + .within('.table-header-container .content') + .each((el, i) => { + expect(el.text().trim()).to.eq(expectedHeaders[i]); + }); - events.waitForPage(); + clusterDashboard.fullEventsLink().click(); + cy.wait('@eventsNoData'); + const events = new EventsPagePo('local'); - events.eventslist().resourceTable().sortableTable().checkRowCount(true, 1); + events.waitForPage(); - const expectedFullHeaders = ['State', 'Last Seen', 'Type', 'Reason', 'Object', - 'Subobject', 'Source', 'Message', 'First Seen', 'Count', 'Name', 'Namespace']; + events.eventslist().resourceTable().sortableTable().checkRowCount(true, 1); - events.eventslist().resourceTable().sortableTable().tableHeaderRow() - .within('.table-header-container .content') - .each((el, i) => { - expect(el.text().trim()).to.eq(expectedFullHeaders[i]); - }); + const expectedFullHeaders = ['State', 'Last Seen', 'Type', 'Reason', 'Object', + 'Subobject', 'Source', 'Message', 'First Seen', 'Count', 'Name', 'Namespace']; + + events.eventslist().resourceTable().sortableTable().tableHeaderRow() + .within('.table-header-container .content') + .each((el, i) => { + expect(el.text().trim()).to.eq(expectedFullHeaders[i]); + }); + }); }); describe('Cluster dashboard with limited permissions', () => { diff --git a/cypress/e2e/tests/pages/explorer/dashboard/events.spec.ts b/cypress/e2e/tests/pages/explorer/dashboard/events.spec.ts index 913405b7712..4bac54da0cc 100644 --- a/cypress/e2e/tests/pages/explorer/dashboard/events.spec.ts +++ b/cypress/e2e/tests/pages/explorer/dashboard/events.spec.ts @@ -7,6 +7,27 @@ import SortableTablePo from '@/cypress/e2e/po/components/sortable-table.po'; const cluster = 'local'; const clusterDashboard = new ClusterDashboardPagePo(cluster); const events = new EventsPagePo(cluster); +const pageSize = 10; +// Should be enough to create at least 3 pages of events +const podCount = 15; + +const countHelper = { + setupCount: (vaiCacheEnabled: boolean, initialCount: number) => { + if (vaiCacheEnabled) { + cy.intercept('GET', '/v1/events?*').as('getCount'); + } else { + cy.wrap(initialCount).as('count'); + } + }, + handleCount: (vaiCacheEnabled) => { + if (vaiCacheEnabled) { + cy.wait('@getCount').then((interception) => { + cy.wrap(interception.response.body.count).as('count'); + }); + } + }, + getCount: () => cy.get('@count').then((count) => count as any as number), +}; describe('Events', { testIsolation: 'off', tags: ['@explorer', '@adminUser'] }, () => { before(() => { @@ -20,7 +41,12 @@ describe('Events', { testIsolation: 'off', tags: ['@explorer', '@adminUser'] }, let nsName2: string; before('set up', () => { - cy.updateNamespaceFilter(cluster, 'none', '{\"local\":[]}'); + cy.tableRowsPerPageAndPreferences(pageSize, { + clusterName: cluster, + groupBy: 'none', + namespaceFilter: '{\"local\":[]}', + allNamespaces: 'true', + }); cy.createE2EResourceName('ns1').then((ns1) => { nsName1 = ns1; @@ -30,7 +56,7 @@ describe('Events', { testIsolation: 'off', tags: ['@explorer', '@adminUser'] }, // create pods let i = 0; - while (i < 125) { + while (i < podCount) { const podName = Cypress._.uniqueId(Date.now().toString()); cy.createPod(nsName1, podName, 'nginx:latest', false, { createNameOptions: { prefixContext: true } }).then((resp) => { @@ -52,6 +78,9 @@ describe('Events', { testIsolation: 'off', tags: ['@explorer', '@adminUser'] }, uniquePod = resp.body.metadata.name; }); }); + + // I'm loathed to do this, but the events created from the pods need to settle before we start + cy.wait(20000); // eslint-disable-line cypress/no-unnecessary-waiting }); it('pagination is visible and user is able to navigate through events data', () => { @@ -61,73 +90,113 @@ describe('Events', { testIsolation: 'off', tags: ['@explorer', '@adminUser'] }, EventsPagePo.navTo(); events.waitForPage(); - cy.getRancherResource('v1', 'events').then((resp: Cypress.Response) => { - // Why 500? there's a hardcoded figure to stops ui from storing more than 500 events ... - const count = resp.body.count < 500 ? resp.body.count : 500; - - // Test break down if less than 400... - expect(count).to.be.greaterThan(400); + let vaiCacheEnabled = false; - // pagination is visible - events.sortableTable().pagination().checkVisible(); + cy.isVaiCacheEnabled() + .then((isVaiCacheEnabled) => { + vaiCacheEnabled = isVaiCacheEnabled; - const loadingPo = new LoadingPo('.title .resource-loading-indicator'); + return cy.getRancherResource('v1', 'events'); + }) + .then((resp: Cypress.Response) => { + let initialCount = resp.body.count; - loadingPo.checkNotExists(); + if (!vaiCacheEnabled && resp.body.count > 500) { + // Why 500? there's a hardcoded figure to stops ui from storing more than 500 events ... + initialCount = 500; + } - // basic checks on navigation buttons - events.sortableTable().pagination().beginningButton().isDisabled(); - events.sortableTable().pagination().leftButton().isDisabled(); - events.sortableTable().pagination().rightButton().isEnabled(); - events.sortableTable().pagination().endButton().isEnabled(); + // Test break down if less than 3 pages... + expect(initialCount).to.be.greaterThan(3 * pageSize); - // check text before navigation - events.sortableTable().pagination().paginationText().then((el) => { - expect(el.trim()).to.eq(`1 - 100 of ${ count } Events`); - }); - - // navigate to next page - right button - events.sortableTable().pagination().rightButton().click(); + // pagination is visible + events.sortableTable().pagination().checkVisible(); - // check text and buttons after navigation - events.sortableTable().pagination().paginationText().then((el) => { - expect(el.trim()).to.eq(`101 - 200 of ${ count } Events`); - }); - events.sortableTable().pagination().beginningButton().isEnabled(); - events.sortableTable().pagination().leftButton().isEnabled(); + const loadingPo = new LoadingPo('.title .resource-loading-indicator'); - // navigate to first page - left button - events.sortableTable().pagination().leftButton().click(); + loadingPo.checkNotExists(); - // check text and buttons after navigation - events.sortableTable().pagination().paginationText().then((el) => { - expect(el.trim()).to.eq(`1 - 100 of ${ count } Events`); - }); - events.sortableTable().pagination().beginningButton().isDisabled(); - events.sortableTable().pagination().leftButton().isDisabled(); + // basic checks on navigation buttons + events.sortableTable().pagination().beginningButton().isDisabled(); + events.sortableTable().pagination().leftButton().isDisabled(); + events.sortableTable().pagination().rightButton().isEnabled(); + events.sortableTable().pagination().endButton().isEnabled(); - // navigate to last page - end button - events.sortableTable().pagination().endButton().scrollIntoView() - .click(); + // check text before navigation + events.sortableTable().pagination().self().scrollIntoView(); + events.sortableTable().pagination().paginationText().then((el) => { + expect(el.trim()).to.eq(`1 - ${ pageSize } of ${ initialCount } Events`); + }); - // check row count on last page - events.sortableTable().checkRowCount(false, 100); + // navigate to next page - right button + countHelper.setupCount(vaiCacheEnabled, initialCount); + events.sortableTable().pagination().rightButton().click(); + countHelper.handleCount(vaiCacheEnabled); + + // check text and buttons after navigation + events.sortableTable().pagination().self().scrollIntoView(); + countHelper.getCount().then((count) => { + return events.sortableTable().pagination().paginationText().then((el) => { + expect(el.trim()).to.eq(`${ pageSize + 1 } - ${ 2 * pageSize } of ${ count } Events`); + }); + }); + events.sortableTable().pagination().beginningButton().isEnabled(); + events.sortableTable().pagination().leftButton().isEnabled(); + + // navigate to first page - left button + countHelper.setupCount(vaiCacheEnabled, initialCount); + events.sortableTable().pagination().leftButton().click(); + countHelper.handleCount(vaiCacheEnabled); + + // check text and buttons after navigation + events.sortableTable().pagination().self().scrollIntoView(); + countHelper.getCount().then((count) => { + return events.sortableTable().pagination().paginationText().then((el) => { + expect(el.trim()).to.eq(`1 - ${ pageSize } of ${ count } Events`); + }); + }); - // check text after navigation - events.sortableTable().pagination().paginationText().then((el) => { - expect(el.trim()).to.eq(`401 - ${ count } of ${ count } Events`); - }); + events.sortableTable().pagination().beginningButton().isDisabled(); + events.sortableTable().pagination().leftButton().isDisabled(); + + // navigate to last page - end button + countHelper.setupCount(vaiCacheEnabled, initialCount); + events.sortableTable().pagination().endButton().scrollIntoView() + .click(); + countHelper.handleCount(vaiCacheEnabled); + + // check text after navigation + events.sortableTable().pagination().self().scrollIntoView(); + countHelper.getCount().then((count) => { + return events.sortableTable().pagination().paginationText().then((el) => { + let pages = Math.floor(count / pageSize); + + if (count % pageSize === 0) { + pages--; + } + const from = (pages * pageSize) + 1; + const to = count; + + expect(el.trim()).to.eq(`${ from } - ${ to } of ${ to } Events`); + }); + }); - // navigate to first page - beginning button - events.sortableTable().pagination().beginningButton().click(); + // navigate to first page - beginning button + countHelper.setupCount(vaiCacheEnabled, initialCount); + events.sortableTable().pagination().beginningButton().click(); + countHelper.handleCount(vaiCacheEnabled); + + // check text and buttons after navigation + events.sortableTable().pagination().self().scrollIntoView(); + countHelper.getCount().then((count) => { + events.sortableTable().pagination().paginationText().then((el) => { + expect(el.trim()).to.eq(`1 - ${ pageSize } of ${ count } Events`); + }); + }); - // check text and buttons after navigation - events.sortableTable().pagination().paginationText().then((el) => { - expect(el.trim()).to.eq(`1 - 100 of ${ count } Events`); + events.sortableTable().pagination().beginningButton().isDisabled(); + events.sortableTable().pagination().leftButton().isDisabled(); }); - events.sortableTable().pagination().beginningButton().isDisabled(); - events.sortableTable().pagination().leftButton().isDisabled(); - }); }); it('filter events', () => { @@ -138,7 +207,7 @@ describe('Events', { testIsolation: 'off', tags: ['@explorer', '@adminUser'] }, events.sortableTable().checkVisible(); events.sortableTable().checkLoadingIndicatorNotVisible(); - events.sortableTable().checkRowCount(false, 100); + events.sortableTable().checkRowCount(false, pageSize); // filter by namespace events.sortableTable().filter(nsName2); @@ -201,7 +270,12 @@ describe('Events', { testIsolation: 'off', tags: ['@explorer', '@adminUser'] }, }); after('clean up', () => { - cy.updateNamespaceFilter(cluster, 'none', '{"local":["all://user"]}'); + cy.tableRowsPerPageAndPreferences(100, { + clusterName: cluster, + groupBy: 'none', + namespaceFilter: '{"local":["all://user"]}', + allNamespaces: 'false', + }); // delete namespace (this will also delete all pods in it) cy.deleteRancherResource('v1', 'namespaces', nsName1); diff --git a/cypress/e2e/tests/pages/explorer/service-discovery/horizontal-pod-autoscalers.spec.ts b/cypress/e2e/tests/pages/explorer/service-discovery/horizontal-pod-autoscalers.spec.ts index 2fd6984c29a..d7ba6706b1c 100644 --- a/cypress/e2e/tests/pages/explorer/service-discovery/horizontal-pod-autoscalers.spec.ts +++ b/cypress/e2e/tests/pages/explorer/service-discovery/horizontal-pod-autoscalers.spec.ts @@ -19,8 +19,11 @@ describe('HorizontalPodAutoscalers', { testIsolation: 'off', tags: ['@explorer', horizontalPodAutoscalersPage.waitForPage(); cy.wait('@horizontalpodautoscalerNoData'); - const expectedHeaders = ['State', 'Name', 'Workload', 'Minimum Replicas', 'Maximum Replicas', 'Current Replicas', 'Age']; + const expectedHeaders = ['State', 'Name', 'Namespace', 'Workload', 'Minimum Replicas', 'Maximum Replicas', 'Current Replicas', 'Age']; + horizontalPodAutoscalersPage.list().resourceTable().sortableTable().tableHeaderRow() + .self() + .scrollIntoView(); horizontalPodAutoscalersPage.list().resourceTable().sortableTable().tableHeaderRow() .get('.table-header-container .content') .each((el, i) => { @@ -39,7 +42,7 @@ describe('HorizontalPodAutoscalers', { testIsolation: 'off', tags: ['@explorer', horizontalPodAutoscalersPage.header().selectNamespaceFilterOption('All Namespaces'); // check table headers are visible - const expectedHeaders = ['State', 'Name', 'Workload', 'Minimum Replicas', 'Maximum Replicas', 'Current Replicas', 'Age']; + const expectedHeaders = ['State', 'Name', 'Namespace', 'Workload', 'Minimum Replicas', 'Maximum Replicas', 'Current Replicas', 'Age']; horizontalPodAutoscalersPage.list().resourceTable().sortableTable().tableHeaderRow() .get('.table-header-container .content') @@ -65,7 +68,7 @@ describe('HorizontalPodAutoscalers', { testIsolation: 'off', tags: ['@explorer', horizontalPodAutoscalersPage.list().resourceTable().sortableTable().groupByButtons(1) .click(); - // check table headers are visible + // check table headers are visible (minus namespace given we're now grouped by it) const expectedHeaders = ['State', 'Name', 'Workload', 'Minimum Replicas', 'Maximum Replicas', 'Current Replicas', 'Age']; horizontalPodAutoscalersPage.list().resourceTable().sortableTable().tableHeaderRow() diff --git a/cypress/globals.d.ts b/cypress/globals.d.ts index 8d5b99566de..060c3b6e1eb 100644 --- a/cypress/globals.d.ts +++ b/cypress/globals.d.ts @@ -99,7 +99,8 @@ declare global { deleteRancherResource(prefix: 'v3' | 'v1' | 'k8s', resourceType: string, resourceId: string, failOnStatusCode?: boolean): Chainable; deleteNodeTemplate(nodeTemplateId: string, timeout?: number, failOnStatusCode?: boolean) - tableRowsPerPageAndNamespaceFilter(rows: number, cluster: string, groupBy: string, namespacefilter: string, interation?: number) + tableRowsPerPageAndNamespaceFilter(rows: number, clusterName: string, groupBy: string, namespaceFilter: string) + tableRowsPerPageAndPreferences(rows: number, preferences: { clusterName: string, groupBy: string, namespaceFilter: string, allNamespaces: string}, iteration?: number) /** * update namespace filter @@ -162,6 +163,11 @@ declare global { * Fetch the steve `revision` / timestamp of request */ fetchRevision(): Chainable; + + /** + * Check if the vai FF is enabled + */ + isVaiCacheEnabled(): Chainable; } } } diff --git a/cypress/support/commands/rancher-api-commands.ts b/cypress/support/commands/rancher-api-commands.ts index 70a09fdb084..7afc70efa1a 100644 --- a/cypress/support/commands/rancher-api-commands.ts +++ b/cypress/support/commands/rancher-api-commands.ts @@ -1024,39 +1024,58 @@ Cypress.Commands.add('fetchRevision', () => { }); }); -Cypress.Commands.add('tableRowsPerPageAndNamespaceFilter', (rows: number, clusterName: string, groupBy: string, namespaceFilter: string, iteration = 0) => { +/** + * Check if the vai FF is enabled + */ +Cypress.Commands.add('isVaiCacheEnabled', () => { + return cy.getRancherResource('v1', 'management.cattle.io.features', 'ui-sql-cache', 200) + .then((res) => res.body.spec.value === true || res.body.spec.value === 'true'); +}); + +Cypress.Commands.add('tableRowsPerPageAndPreferences', (rows: number, preferences: { clusterName: string, groupBy: string, namespaceFilter: string, allNamespaces: string}, iteration = 0) => { + const { + clusterName, groupBy, namespaceFilter, allNamespaces + } = preferences; + return cy.getRancherResource('v3', 'users?me=true').then((resp: Cypress.Response) => { const userId = resp.body.data[0].id.trim(); const payload = { id: `${ userId }`, type: 'userpreference', data: { - cluster: clusterName, - 'per-page': `${ rows }`, - 'group-by': groupBy, - 'ns-by-cluster': namespaceFilter + cluster: clusterName, + 'per-page': `${ rows }`, + 'group-by': groupBy, + 'ns-by-cluster': namespaceFilter, + 'all-namespaces': allNamespaces, } }; - cy.log(`tableRowsPerPageAndNamespaceFilter: /v1/userpreferences/${ userId }. Payload: ${ JSON.stringify(payload) }`); + cy.log(`tableRowsPerPageAndPreferences: /v1/userpreferences/${ userId }. Payload: ${ JSON.stringify(payload) }`); cy.setRancherResource('v1', 'userpreferences', userId, payload).then(() => { return cy.waitForRancherResource('v1', 'userpreferences', userId, (resp: any) => compare(resp?.body, payload)) .then((res) => { if (res) { - cy.log(`tableRowsPerPageAndNamespaceFilter: Success!`); + cy.log(`tableRowsPerPageAndPreferences: Success!`); } else { if (iteration < 3) { - cy.log(`tableRowsPerPageAndNamespaceFilter: Failed! Going to retry...`); + cy.log(`tableRowsPerPageAndPreferences: Failed! Going to retry...`); - return cy.tableRowsPerPageAndNamespaceFilter(rows, clusterName, groupBy, namespaceFilter, iteration + 1); + return cy.tableRowsPerPageAndPreferences(rows, preferences, iteration + 1); } - cy.log(`tableRowsPerPageAndNamespaceFilter: Failed! Giving up...`); + cy.log(`tableRowsPerPageAndPreferences: Failed! Giving up...`); - return Promise.reject(new Error('tableRowsPerPageAndNamespaceFilter failed')); + return Promise.reject(new Error('tableRowsPerPageAndPreferences failed')); } }); }); }); }); + +Cypress.Commands.add('tableRowsPerPageAndNamespaceFilter', (rows: number, clusterName: string, groupBy: string, namespaceFilter: string) => { + return cy.tableRowsPerPageAndPreferences(rows, { + clusterName, groupBy, namespaceFilter + }); +}); diff --git a/shell/assets/translations/en-us.yaml b/shell/assets/translations/en-us.yaml index 50e30986c85..758c7a19d37 100644 --- a/shell/assets/translations/en-us.yaml +++ b/shell/assets/translations/en-us.yaml @@ -7583,6 +7583,7 @@ performance: resources: generic: most resources in the cluster's 'More Resources' section all: All Resources + populateDefaults: Populate with latest pagination defaults banner: label: Fixed Banners settingName: Banners diff --git a/shell/components/AsyncButton.vue b/shell/components/AsyncButton.vue index f847dc07df8..e80d265d89a 100644 --- a/shell/components/AsyncButton.vue +++ b/shell/components/AsyncButton.vue @@ -208,6 +208,10 @@ export default defineComponent({ return this.disabled || this.phase === ASYNC_BUTTON_STATES.WAITING; }, + isManualRefresh() { + return this.mode === 'manual-refresh'; + }, + tooltip(): { content: string, hideOnTargetClick: boolean} | null { if ( this.labelAs === TOOLTIP ) { return { @@ -283,12 +287,14 @@ export default defineComponent({ :data-testid="componentTestid + '-async-button'" @click="clicked" > - {{ t('action.refresh') }} + {{ t('action.refresh') }} { this.isLoading = true; - }, 200); // this should be higher than the targetted quick response + }, 200); // this should be higher than the targeted quick response } else { clearTimeout(this._altLoadingDelayTimer); this.isLoading = false; @@ -575,11 +575,13 @@ export default { }, showHeaderRow() { + // All of these are used to show content in the header return this.search || this.tableActions || - this.$slots['header-left']?.() || - this.$slots['header-middle']?.() || - this.$slots['header-right']?.(); + this.$slots['header-left'] || + this.$slots['header-middle'] || + this.$slots['header-right'] || + this.isTooManyItemsToAutoUpdate; }, columns() { diff --git a/shell/components/form/ResourceLabeledSelect.vue b/shell/components/form/ResourceLabeledSelect.vue index dd9e0e16e42..3326a992793 100644 --- a/shell/components/form/ResourceLabeledSelect.vue +++ b/shell/components/form/ResourceLabeledSelect.vue @@ -27,7 +27,7 @@ export interface ResourceLabeledSelectPaginateSettings extends SharedSettings { */ overrideRequest?: LabelSelectPaginateFn, /** - * Override the default settings used in the convience function to fetch a page of results + * Override the default settings used in the convenience function to fetch a page of results */ requestSettings?: PaginateTypeOverridesFn, } diff --git a/shell/components/formatter/Endpoints.vue b/shell/components/formatter/Endpoints.vue index 57035bb1e0d..9742006141d 100644 --- a/shell/components/formatter/Endpoints.vue +++ b/shell/components/formatter/Endpoints.vue @@ -27,7 +27,7 @@ export default { const nodeWithExternal = nodes.find((node) => !!node.externalIp) || {}; const externalIp = nodeWithExternal.externalIp; - if ( this.value && this.value.length ) { + if ( this.value?.length ) { let out ; try { diff --git a/shell/components/formatter/ServiceTargets.vue b/shell/components/formatter/ServiceTargets.vue index 15abb015cfc..87b1c01848b 100644 --- a/shell/components/formatter/ServiceTargets.vue +++ b/shell/components/formatter/ServiceTargets.vue @@ -95,7 +95,7 @@ export default { }, }, }; -> +