From aaf2d69130a6539472b3aa010678759bfd369ef9 Mon Sep 17 00:00:00 2001 From: Richard Cox <18697775+richard-cox@users.noreply.github.com> Date: Tue, 11 Jun 2024 17:44:04 +0100 Subject: [PATCH] Pagination changes given new vai cache backed steve api (#11110) * Changes for new design - New visuals - Pagination controls --> load more - finished testing of label select with pagination off # Conflicts: # shell/edit/provisioning.cattle.io.cluster/__tests__/Basics.tests.ts * Changes following review * Update Node list to support server-side pagination - Setup pagination headers for the node type - Define a pattern for fetching custom list secondary resources - Major improvements to the way pagination settings are defined and created - Lots of docs improvements - Handle calling fetch again once fetch is in progress (nuxt caches running request) - Validate filter fields (not all are supported by the vai cache - General pagination fixes * Lint / test / fixes * Improvements to configmap e2e test & Improve pagination disabled * Beef up validation * Fix missing name column in non-server-side paginated node list * Fix PR automation actions - fix syntax - catch scenario where a pr has no fixed issue > There's duplication between files, see https://github.com/rancher/dashboard/pull/10534 * CI bump * Fixes post merge * Wire in 2.9.0 settings for server-side pagination - Everything is gated on `on-disk-steve-cache` feature flag - There's a backend in progress item to resolve a `revision` issue, until then disable watching a resource given it - Global Settings - Performance - Added new setting to enable server side pagination - this is incompatible with two other performance settings * Integrate pagination with configmaps in cis clusterscanbenchmark edit form Also - improved labeled select pagination - gate label select pagination functinality on steve cache being enabled * - harvester machine-config - project monitoring (and bug fixes) * Disable workload screen if vai cache is on - temp step until we get new overview * TODOs and TEST * Conditionally remove fetch of all secrets from SelectOrCreateAuthSecret * TODOs and TEST * Update SimpleSecretSelector - only used in monitoring.coreos.com.alertmanagerconfig context * View and Edit ingress - secrets * node detail page - pods list * Backup/Restore: Secrets (WIP) * Backup/Restore: Secrets, and other usages of SimpleSecretSelector / SelectOrCreateAuthSecret * Edit: Service account * Add comments for remaining items * Paginate Secret selection for logging providers - Allow `None` option in Paginationed LabelSelect - Optionally classify pagination response * WIP * fixes arfter merge * Don't suggest container names, not practical - previously all pods were fetched... and we scrapped all container names from them - this is a scaling nightmare, user now must just enter the name/s to match * Avoid findAll secrets in SimpleSecretSelector * tidying up * Move LabeledSelect/index.vue back to LabeledSelect.vue to not break extensions * changes after self review... 1 * changes after self review... 2 * ooof * changes after self review... 3 * fix formatting * Link new paginated label select with pagination setting * Work around failing kubewarden unit tests in check-plugins gate * Fix backup.spec e2e test * fix formatting, paginationUtils.isSteveCacheEnabled --> paginationUtils.isEnabled * Don't fetch all secrets on cloud creds page * Fix backup.spec e2e test * TODO tidying / tracking * don't getch ALL workloads for hacky way to get a link to a service's workload * Fix bad merge * Updates after working with vai cache image * test fixes * Create a convienence wrapper called ResourceLabelSelector that hides most of the complexity * fix unit test * Updates following review * Remove workload health until #10417 is resolved * Updates following review * changes following self review * Fix bottom bar of edit backup, edit restore pages * revert temp change * changes following self review * Workaround for kubewarden unit tests in check plugin gate * bump * Fix e2e * Fix linting * type fixing * - improve filtering without pagination - update allowed fields given latest backend changes - enable on by default exact filter string matches (disable for lists * remove temp code * fix linting * Changes following review * Fixes for vai cache feature flag - name was changed from on-disk-steve-cache to ui-sql-cache - fix timing issue - don't watch resources until we know the vai cache feature flag * Changes following review * Fix two sketchy tests - new exception in docs page - don't nav to page via button click and then goto same page * More test improvement - force user to go to tab which is source of route guard issue - move setup stuff to a test for cypress to re-retry --- cypress/e2e/po/pages/extensions.po.ts | 5 +- .../side-nav/main-side-menu.spec.ts | 3 + .../tests/pages/extensions/extensions.spec.ts | 10 +-- .../machine-config/harvester.vue | 8 +-- .../form/SelectOrCreateAuthSecret.vue | 7 +- shell/config/pagination-table-headers.js | 4 +- shell/detail/networking.k8s.io.ingress.vue | 7 +- shell/detail/node.vue | 10 ++- .../edit/networking.k8s.io.ingress/index.vue | 7 +- shell/edit/resources.cattle.io.backup.vue | 7 +- shell/edit/resources.cattle.io.restore.vue | 7 +- shell/edit/serviceaccount.vue | 7 +- shell/list/node.vue | 14 ++-- shell/list/workload.vue | 22 +++++++ shell/mixins/resource-fetch-api-pagination.js | 1 + shell/plugins/steve/steve-pagination-utils.ts | 48 +++++++++++--- shell/store/features.js | 6 +- shell/store/index.js | 30 +++++++-- shell/types/store/pagination.types.ts | 66 +++++++++++++++++-- shell/utils/pagination-utils.ts | 21 +----- 20 files changed, 197 insertions(+), 93 deletions(-) diff --git a/cypress/e2e/po/pages/extensions.po.ts b/cypress/e2e/po/pages/extensions.po.ts index aa0b5d0b816..3bd496f8988 100644 --- a/cypress/e2e/po/pages/extensions.po.ts +++ b/cypress/e2e/po/pages/extensions.po.ts @@ -54,8 +54,9 @@ export default class ExtensionsPagePo extends PagePo { // create a new clusterrepo const appRepoList = new RepositoriesPagePo('local', 'apps'); - // appRepoList.waitForPage(); - appRepoList.waitForGoTo('/v1/catalog.cattle.io.clusterrepos?exclude=metadata.managedFields'); + appRepoList.waitForPage(); + appRepoList.list().checkVisible(); + appRepoList.create(); const appRepoCreate = new ChartRepositoriesCreateEditPo('local', 'apps'); 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 b31ae3335ab..433e3be9d72 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,6 +4,7 @@ import PagePo from '@/cypress/e2e/po/pages/page.po'; import ProductNavPo from '@/cypress/e2e/po/side-bars/product-side-nav.po'; import ClusterManagerListPagePo from '@/cypress/e2e/po/pages/cluster-manager/cluster-manager-list.po'; import { generateFakeClusterDataAndIntercepts } from '@/cypress/e2e/blueprints/nav/fake-cluster'; +import { RANCHER_PAGE_EXCEPTIONS, catchTargetPageException } from '@/cypress/support/utils/exception-utils'; const longClusterDescription = 'this-is-some-really-really-really-really-really-really-long-decription'; const fakeProvClusterId = 'some-fake-cluster-id'; @@ -47,6 +48,8 @@ describe('Side Menu: main', () => { // testing https://github.com/rancher/dashboard/issues/10192 it('"documentation" link in editing a cluster should open in a new tab', { tags: ['@navigation', '@adminUser'] }, () => { + catchTargetPageException(RANCHER_PAGE_EXCEPTIONS); + const page = new PagePo(''); const clusterList = new ClusterManagerListPagePo('_'); diff --git a/cypress/e2e/tests/pages/extensions/extensions.spec.ts b/cypress/e2e/tests/pages/extensions/extensions.spec.ts index c8d1c195cc9..68566b7f4a5 100644 --- a/cypress/e2e/tests/pages/extensions/extensions.spec.ts +++ b/cypress/e2e/tests/pages/extensions/extensions.spec.ts @@ -10,22 +10,22 @@ const UI_PLUGINS_PARTNERS_REPO_URL = 'https://github.com/rancher/partner-extensi const UI_PLUGINS_PARTNERS_REPO_NAME = 'partner-extensions'; describe('Extensions page', { tags: ['@extensions', '@adminUser'] }, () => { - before(() => { + beforeEach(() => { cy.login(); + }); + it('add repository', () => { + // This should be in a `before` however is flaky. Move it to an `it` to let cypress retry const extensionsPo = new ExtensionsPagePo(); extensionsPo.goTo(); extensionsPo.waitForPage(); + extensionsPo.extensionTabInstalledClick(); // Avoid nav guard failures that probably auto move user to this tab // install the rancher plugin examples extensionsPo.addExtensionsRepository('https://github.com/rancher/ui-plugin-examples', 'main', 'rancher-plugin-examples'); }); - beforeEach(() => { - cy.login(); - }); - it('has the correct title', () => { const extensionsPo = new ExtensionsPagePo(); diff --git a/pkg/harvester-manager/machine-config/harvester.vue b/pkg/harvester-manager/machine-config/harvester.vue index da4c8528a09..b312ad4d9cb 100644 --- a/pkg/harvester-manager/machine-config/harvester.vue +++ b/pkg/harvester-manager/machine-config/harvester.vue @@ -40,7 +40,7 @@ import { stringify, exceptionToErrorsArray } from '@shell/utils/error'; import { isValidMac } from '@shell/utils/validators/cidr'; import { HCI as HCI_ANNOTATIONS, STORAGE } from '@shell/config/labels-annotations'; import { isEqual } from 'lodash'; -import { PaginationArgs, PaginationFilterField, PaginationParamFilter } from '~/shell/types/store/pagination.types'; +import { FilterArgs, PaginationFilterField, PaginationParamFilter } from '@shell/types/store/pagination.types'; const STORAGE_NETWORK = 'storage-network.settings.harvesterhci.io'; @@ -135,10 +135,8 @@ export default { let configMapsUrl = `${ url }/${ CONFIG_MAP }s`; if (this.$store.getters[`cluster/paginationEnabled`](CONFIG_MAP)) { - const pagination = new PaginationArgs({ - page: 1, - pageSize: -1, - filters: [ + const pagination = new FilterArgs({ + filters: [ PaginationParamFilter.createMultipleFields([ new PaginationFilterField({ field: `metadata.label["${ HCI_ANNOTATIONS.CLOUD_INIT }"]`, value: 'user' }), new PaginationFilterField({ field: `metadata.label["${ HCI_ANNOTATIONS.CLOUD_INIT }"]`, value: 'network' }) diff --git a/shell/components/form/SelectOrCreateAuthSecret.vue b/shell/components/form/SelectOrCreateAuthSecret.vue index 4116772c250..3398d352930 100644 --- a/shell/components/form/SelectOrCreateAuthSecret.vue +++ b/shell/components/form/SelectOrCreateAuthSecret.vue @@ -8,7 +8,7 @@ import { base64Encode } from '@shell/utils/crypto'; import { addObjects, insertAt } from '@shell/utils/array'; import { sortBy } from '@shell/utils/sort'; import { - PaginationArgs, + FilterArgs, PaginationFilterField, PaginationParamFilter, } from '@shell/types/store/pagination.types'; @@ -411,9 +411,8 @@ export default { const findPageArgs = { // Of type ActionFindPageArgs namespaced: this.filterByNamespace ? this.namespace : '', - pagination: new PaginationArgs({ - pageSize: -1, - filters: [ + pagination: new FilterArgs({ + filters: [ PaginationParamFilter.createMultipleFields( this.secretTypes.map( (t) => new PaginationFilterField({ diff --git a/shell/config/pagination-table-headers.js b/shell/config/pagination-table-headers.js index 74f59690517..d0455c25d0f 100644 --- a/shell/config/pagination-table-headers.js +++ b/shell/config/pagination-table-headers.js @@ -25,8 +25,8 @@ export const STEVE_STATE_COL = { ...STATE, // value: 'metadata.state.name', Use the state as defined by the resource rather than converted via the model. // This means we'll show something different to what we sort and filter on. - sort: ['metadata.state.name'], - search: 'metadata.state.name', + sort: [], // ['metadata.state.name'], // Pending API support + search: false, // 'metadata.state.name', // Pending API support }; export const STEVE_AGE_COL = { diff --git a/shell/detail/networking.k8s.io.ingress.vue b/shell/detail/networking.k8s.io.ingress.vue index 9a7c7dc637c..febd0cc30dc 100644 --- a/shell/detail/networking.k8s.io.ingress.vue +++ b/shell/detail/networking.k8s.io.ingress.vue @@ -6,7 +6,7 @@ import Rules from '@shell/edit/networking.k8s.io.ingress/Rules'; import ResourceTabs from '@shell/components/form/ResourceTabs'; import Tab from '@shell/components/Tabbed/Tab'; import { SECRET_TYPES as TYPES } from '@shell/config/secret'; -import { PaginationArgs, PaginationParamFilter } from '@shell/types/store/pagination.types'; +import { FilterArgs, PaginationParamFilter } from '@shell/types/store/pagination.types'; export default { name: 'CRUIngress', @@ -25,9 +25,8 @@ export default { if (this.$store.getters[`cluster/paginationEnabled`](SECRET)) { const findPageArgs = { // Of type ActionFindPageArgs namespaced: this.value.metadata.namespace, - pagination: new PaginationArgs({ - pageSize: -1, - filters: PaginationParamFilter.createSingleField({ + pagination: new FilterArgs({ + filters: PaginationParamFilter.createSingleField({ field: 'metadata.fields.1', value: TYPES.TLS }) diff --git a/shell/detail/node.vue b/shell/detail/node.vue index ac728b9ef7a..9c30668afef 100644 --- a/shell/detail/node.vue +++ b/shell/detail/node.vue @@ -19,7 +19,7 @@ import { mapGetters } from 'vuex'; import { allDashboardsExist } from '@shell/utils/grafana'; import Loading from '@shell/components/Loading'; import metricPoller from '@shell/mixins/metric-poller'; -import { PaginationArgs, PaginationParamFilter } from '@shell/types/store/pagination.types'; +import { FilterArgs, PaginationParamFilter } from '@shell/types/store/pagination.types'; const NODE_METRICS_DETAIL_URL = '/api/v1/namespaces/cattle-monitoring-system/services/http:rancher-monitoring-grafana:80/proxy/d/rancher-node-detail-1/rancher-node-detail?orgId=1'; const NODE_METRICS_SUMMARY_URL = '/api/v1/namespaces/cattle-monitoring-system/services/http:rancher-monitoring-grafana:80/proxy/d/rancher-node-1/rancher-node?orgId=1'; @@ -52,11 +52,9 @@ export default { if (this.filterByApi) { // Only get pods associated with this node. The actual values used are from a get all in node model `pods` getter (this works as it just gets all...) const opt = { // Of type ActionFindPageArgs - pagination: new PaginationArgs({ - page: -1, - pageSize: -1, - sort: [{ field: 'metadata.name', asc: true }], - filters: PaginationParamFilter.createSingleField({ + pagination: new FilterArgs({ + sort: [{ field: 'metadata.name', asc: true }], + filters: PaginationParamFilter.createSingleField({ field: 'spec.nodeName', value: this.value.id, }) diff --git a/shell/edit/networking.k8s.io.ingress/index.vue b/shell/edit/networking.k8s.io.ingress/index.vue index 6be7926e671..c4ae62d7759 100644 --- a/shell/edit/networking.k8s.io.ingress/index.vue +++ b/shell/edit/networking.k8s.io.ingress/index.vue @@ -16,7 +16,7 @@ import Certificates from './Certificates'; import Rules from './Rules'; import IngressClass from './IngressClass'; import Loading from '@shell/components/Loading'; -import { PaginationArgs, PaginationParamFilter } from '@shell/types/store/pagination.types'; +import { FilterArgs, PaginationParamFilter } from '@shell/types/store/pagination.types'; export default { name: 'CRUIngress', @@ -211,9 +211,8 @@ export default { filterSecretsByApi() { const findPageArgs = { // Of type ActionFindPageArgs namespaced: this.value.metadata.namespace, - pagination: new PaginationArgs({ - pageSize: -1, - filters: PaginationParamFilter.createSingleField({ + pagination: new FilterArgs({ + filters: PaginationParamFilter.createSingleField({ field: 'metadata.fields.1', value: TYPES.TLS }) diff --git a/shell/edit/resources.cattle.io.backup.vue b/shell/edit/resources.cattle.io.backup.vue index e0556aff8d3..77f37425a19 100644 --- a/shell/edit/resources.cattle.io.backup.vue +++ b/shell/edit/resources.cattle.io.backup.vue @@ -17,7 +17,7 @@ import { NAMESPACE, _VIEW } from '@shell/config/query-params'; import { sortBy } from '@shell/utils/sort'; import { get } from '@shell/utils/object'; import { formatEncryptionSecretNames } from '@shell/utils/formatter'; -import { PaginationArgs, PaginationParamFilter } from '@shell/types/store/pagination.types'; +import { FilterArgs, PaginationParamFilter } from '@shell/types/store/pagination.types'; import { SECRET_TYPES } from '@shell/config/secret'; export default { @@ -65,9 +65,8 @@ export default { if (this.$store.getters[`cluster/paginationEnabled`](SECRET)) { const findPageArgs = { // Of type ActionFindPageArgs namespaced: this.chartNamespace, - pagination: new PaginationArgs({ - pageSize: -1, - filters: PaginationParamFilter.createSingleField({ + pagination: new FilterArgs({ + filters: PaginationParamFilter.createSingleField({ field: 'metadata.fields.1', value: SECRET_TYPES.OPAQUE }) diff --git a/shell/edit/resources.cattle.io.restore.vue b/shell/edit/resources.cattle.io.restore.vue index f988074afa2..198b39928d9 100644 --- a/shell/edit/resources.cattle.io.restore.vue +++ b/shell/edit/resources.cattle.io.restore.vue @@ -14,7 +14,7 @@ import { allHash } from '@shell/utils/promise'; import { get } from '@shell/utils/object'; import { _CREATE } from '@shell/config/query-params'; import { formatEncryptionSecretNames } from '@shell/utils/formatter'; -import { PaginationArgs, PaginationParamFilter } from '@shell/types/store/pagination.types'; +import { FilterArgs, PaginationParamFilter } from '@shell/types/store/pagination.types'; import { SECRET_TYPES } from '@shell/config/secret'; export default { @@ -62,9 +62,8 @@ export default { if (this.$store.getters[`cluster/paginationEnabled`](SECRET)) { const findPageArgs = { // Of type ActionFindPageArgs namespaced: this.chartNamespace, - pagination: new PaginationArgs({ - pageSize: -1, - filters: PaginationParamFilter.createSingleField({ + pagination: new FilterArgs({ + filters: PaginationParamFilter.createSingleField({ field: 'metadata.fields.1', value: SECRET_TYPES.OPAQUE }) diff --git a/shell/edit/serviceaccount.vue b/shell/edit/serviceaccount.vue index 876a54612e7..2c82b7ea08a 100644 --- a/shell/edit/serviceaccount.vue +++ b/shell/edit/serviceaccount.vue @@ -9,7 +9,7 @@ import { Checkbox } from '@components/Form/Checkbox'; import { SECRET } from '@shell/config/types'; import { TYPES as SECRET_TYPES } from '@shell/models/secret'; import LabeledSelect from '@shell/components/form/LabeledSelect'; -import { PaginationArgs, PaginationParamFilter } from '@shell/types/store/pagination.types'; +import { FilterArgs, PaginationParamFilter } from '@shell/types/store/pagination.types'; export default { name: 'ServiceAccount', @@ -70,9 +70,8 @@ export default { filterSecretsByApi() { const findPageArgs = { // Of type ActionFindPageArgs namespaced: this.value.metadata.namespace, - pagination: new PaginationArgs({ - pageSize: -1, - filters: PaginationParamFilter.createMultipleFields(this.secretTypes.map((t) => ({ + pagination: new FilterArgs({ + filters: PaginationParamFilter.createMultipleFields(this.secretTypes.map((t) => ({ field: 'metadata.fields.1', value: t, equals: true diff --git a/shell/list/node.vue b/shell/list/node.vue index 4b4d96e0fbb..6349996aa97 100644 --- a/shell/list/node.vue +++ b/shell/list/node.vue @@ -9,7 +9,7 @@ import { CAPI as CAPI_ANNOTATIONS } from '@shell/config/labels-annotations.js'; import { defineComponent } from 'vue'; import { ActionFindPageArgs } from '@shell/types/store/dashboard-store.types'; -import { PaginationArgs, PaginationFilterField, PaginationParamFilter } from '@shell/types/store/pagination.types'; +import { FilterArgs, PaginationFilterField, PaginationParamFilter } from '@shell/types/store/pagination.types'; import { CAPI, @@ -147,8 +147,7 @@ export default defineComponent({ const opt: ActionFindPageArgs = { force: true, - pagination: new PaginationArgs({ - page: -1, + pagination: new FilterArgs({ filters: new PaginationParamFilter({ fields: this.rows.map((r: any) => new PaginationFilterField({ field: 'metadata.name', @@ -220,8 +219,7 @@ export default defineComponent({ // See https://github.com/rancher/dashboard/issues/10743 const opt: ActionFindPageArgs = { force, - pagination: new PaginationArgs({ - page: -1, + pagination: new FilterArgs({ filters: PaginationParamFilter.createMultipleFields(this.rows.map((r: any) => new PaginationFilterField({ field: 'status.nodeName', value: r.id @@ -242,8 +240,7 @@ export default defineComponent({ const opt: ActionFindPageArgs = { force, namespaced: namespace, - pagination: new PaginationArgs({ - page: -1, + pagination: new FilterArgs({ filters: PaginationParamFilter.createMultipleFields( this.rows.reduce((res: PaginationFilterField[], r: any ) => { const name = r.metadata?.annotations?.[CAPI_ANNOTATIONS.MACHINE_NAME]; @@ -269,8 +266,7 @@ export default defineComponent({ // Note - fetching pods for current page could be a LOT still (probably max of 3k - 300 pods per node x 100 nodes in a page) const opt: ActionFindPageArgs = { force, - pagination: new PaginationArgs({ - page: -1, + pagination: new FilterArgs({ filters: PaginationParamFilter.createMultipleFields( this.rows.map((r: any) => new PaginationFilterField({ field: 'spec.nodeName', diff --git a/shell/list/workload.vue b/shell/list/workload.vue index 5940ea1da88..e8373b79e4b 100644 --- a/shell/list/workload.vue +++ b/shell/list/workload.vue @@ -4,6 +4,7 @@ import { WORKLOAD_TYPES, SCHEMA, NODE, POD, LIST_WORKLOAD_TYPES } from '@shell/config/types'; import ResourceFetch from '@shell/mixins/resource-fetch'; +import { WORKLOAD_HEALTH_SCALE } from '@shell/config/table-headers'; const schema = { id: 'workload', @@ -93,6 +94,10 @@ export default { return this.$route.params.resource === schema.id; }, + paginationEnabled() { + return !this.allTypes && this.$store.getters[`cluster/paginationEnabled`](); + }, + schema() { const { params:{ resource:type } } = this.$route; @@ -120,6 +125,17 @@ export default { return out; }, + + headers() { + const headers = this.$store.getters['type-map/headersFor'](this.schema, false); + + if (this.paginationEnabled) { + // See https://github.com/rancher/dashboard/issues/10417, health comes from selectors applied locally to all pods (bad) + return headers.filter((h) => h.name !== WORKLOAD_HEALTH_SCALE.name); + } + + return headers; + } }, // All of the resources that we will load that we need for the loading indicator @@ -129,6 +145,11 @@ export default { methods: { loadHeathResources() { + // See https://github.com/rancher/dashboard/issues/10417, health comes from selectors applied locally to all pods (bad) + if (this.paginationEnabled) { + return; + } + // Fetch these in the background to populate workload health if ( this.allTypes ) { this.$fetchType(POD); @@ -167,6 +188,7 @@ export default { new PaginationFilterField({ field, value: event.filter.searchQuery, + exact: false, })) : []; const pagination = new PaginationArgs({ diff --git a/shell/plugins/steve/steve-pagination-utils.ts b/shell/plugins/steve/steve-pagination-utils.ts index ae4c86931d7..cb683221222 100644 --- a/shell/plugins/steve/steve-pagination-utils.ts +++ b/shell/plugins/steve/steve-pagination-utils.ts @@ -3,7 +3,9 @@ import { PaginationParam, PaginationFilterField, PaginationParamProjectOrNamespa import { NAMESPACE_FILTER_ALL_SYSTEM, NAMESPACE_FILTER_ALL_USER, NAMESPACE_FILTER_P_FULL_PREFIX } from '@shell/utils/namespace-filter'; import Namespace from '@shell/models/namespace'; import { uniq } from '@shell/utils/array'; -import { CONFIG_MAP, MANAGEMENT, NODE, POD } from '@shell/config/types'; +import { + CONFIG_MAP, MANAGEMENT, NAMESPACE, NODE, POD +} from '@shell/config/types'; import { Schema } from 'plugins/steve/schema'; class NamespaceProjectFilters { @@ -105,7 +107,9 @@ class StevePaginationUtils extends NamespaceProjectFilters { '': [// all types { field: 'metadata.name' }, { field: 'metadata.namespace' }, - { field: 'metadata.state.name' }, + // { field: 'id' }, // Pending API support + // { field: 'metadata.state.name' }, // Pending API support + { field: 'metadata.creationTimestamp' }, ], [NODE]: [ { field: 'status.nodeInfo.kubeletVersion' }, @@ -116,13 +120,28 @@ class StevePaginationUtils extends NamespaceProjectFilters { { field: 'spec.nodeName' }, ], [MANAGEMENT.NODE]: [ - { field: 'status.nodeName' } + { field: 'status.nodeName' }, ], [CONFIG_MAP]: [ - { field: 'metadata.labels' } + { field: 'metadata.labels[harvesterhci.io/cloud-init-template]' } + ], + [NAMESPACE]: [ + { field: 'metadata.labels[field.cattle.io/projectId]' } ] } + private convertArrayPath(path: string): string { + if (path.startsWith('metadata.fields.')) { + return `metadata.fields[${ path.substring(16) }]`; + } + + return path; + } + + public createSortForPagination(sortByPath: string): string { + return this.convertArrayPath(sortByPath); + } + /** * Given the selection of projects or namespaces come up with `filter` and `projectsornamespace` query params */ @@ -220,8 +239,6 @@ class StevePaginationUtils extends NamespaceProjectFilters { if (opt.pagination.page) { params.push(`page=${ opt.pagination.page }`); - } else { - throw new Error(`A pagination request is required but no 'page' property provided: ${ JSON.stringify(opt) }`); } if (opt.pagination.pageSize) { @@ -229,11 +246,24 @@ class StevePaginationUtils extends NamespaceProjectFilters { } if (opt.pagination.sort?.length) { + const validateFields = { + checked: new Array(), + invalid: new Array(), + }; + const joined = opt.pagination.sort - .map((s) => `${ s.asc ? '' : '-' }${ s.field }`) + .map((s) => { + this.validateField(validateFields, schema, s.field); + + return `${ s.asc ? '' : '-' }${ this.convertArrayPath(s.field) }`; + }) .join(','); params.push(`sort=${ joined }`); + + if (validateFields.invalid.length) { + console.warn(`Pagination API does not support sorting '${ schema.id }' by the requested fields: ${ uniq(validateFields.invalid).join(', ') }`); // eslint-disable-line no-console + } } if (opt.pagination.filters?.length) { @@ -308,7 +338,9 @@ class StevePaginationUtils extends NamespaceProjectFilters { // Check if the API supports filtering by this field this.validateField(validateFields, schema, field.field); - return `${ field.field }${ field.equals ? '=' : '!=' }${ field.value }`; + const exactPartial = field.exact ? `'${ field.value }'` : field.value; + + return `${ this.convertArrayPath(field.field) }${ field.equals ? '=' : '!=' }${ exactPartial }`; } return field.value; diff --git a/shell/store/features.js b/shell/store/features.js index 87a87c5a982..6789ecfd8a5 100644 --- a/shell/store/features.js +++ b/shell/store/features.js @@ -33,7 +33,7 @@ export const FLEET = create('continuous-delivery', true); export const HARVESTER = create('harvester', true); export const HARVESTER_CONTAINER = create('harvester-baremetal-container-workload', false); export const FLEET_WORKSPACE_BACK = create('provisioningv2-fleet-workspace-back-population', false); -export const STEVE_CACHE = create('on-disk-steve-cache', false); +export const STEVE_CACHE = create('ui-sql-cache', false); export const UIEXTENSION = create('uiextension', true); // Not currently used.. no point defining ones we don't use @@ -63,9 +63,9 @@ export const getters = { }; export const actions = { - loadServer({ rootGetters, dispatch }) { + async loadServer({ rootGetters, dispatch }) { if ( rootGetters['management/canList'](MANAGEMENT.FEATURE) ) { - return dispatch('management/findAll', { type: MANAGEMENT.FEATURE }, { root: true }); + return await dispatch('management/findAll', { type: MANAGEMENT.FEATURE, opt: { watch: false } }, { root: true }); } }, }; diff --git a/shell/store/index.js b/shell/store/index.js index 4157d514f60..0d78b5c27f3 100644 --- a/shell/store/index.js +++ b/shell/store/index.js @@ -769,14 +769,24 @@ export const actions = { rancherSchemas: dispatch('rancher/loadSchemas', true), }); + // Note - why aren't we watching anything fetched in the `promises` object? + // To watch we need feature flags to know that the vai cache is enabled. + // So to work around this we won't watch anything initially... and then watch once we have feature flags + // The alternative is simpler (fetch features up front) but would add another blocking request in + const promises = { // Clusters guaranteed always available or your money back - clusters: dispatch('management/findAll', { type: MANAGEMENT.CLUSTER }), + clusters: dispatch('management/findAll', { type: MANAGEMENT.CLUSTER, opt: { watch: false } }), // Features checks on its own if they are available features: dispatch('features/loadServer'), }; + const toWatch = [ + MANAGEMENT.CLUSTER, + MANAGEMENT.FEATURE, + ]; + const isRancher = res.rancherSchemas.status === 'fulfilled' && !!getters['management/schemaFor'](MANAGEMENT.PROJECT); if ( isRancher ) { @@ -785,24 +795,34 @@ export const actions = { } if ( getters['management/schemaFor'](COUNT) ) { - promises['counts'] = dispatch('management/findAll', { type: COUNT }); + promises['counts'] = dispatch('management/findAll', { type: COUNT, opt: { watch: false } }); + toWatch.push(COUNT); } if ( getters['management/canList'](MANAGEMENT.SETTING) ) { - promises['settings'] = dispatch('management/findAll', { type: MANAGEMENT.SETTING }); + promises['settings'] = dispatch('management/findAll', { type: MANAGEMENT.SETTING, opt: { watch: false } }); + toWatch.push(MANAGEMENT.SETTING); } if ( getters['management/schemaFor'](NAMESPACE) ) { - promises['namespaces'] = dispatch('management/findAll', { type: NAMESPACE }); + promises['namespaces'] = dispatch('management/findAll', { type: NAMESPACE, opt: { watch: false } }); + toWatch.push(NAMESPACE); } const fleetSchema = getters['management/schemaFor'](FLEET.WORKSPACE); if (fleetSchema?.links?.collection) { - promises['workspaces'] = dispatch('management/findAll', { type: FLEET.WORKSPACE }); + promises['workspaces'] = dispatch('management/findAll', { type: FLEET.WORKSPACE, opt: { watch: false } }); + toWatch.push(FLEET.WORKSPACE); } res = await allHash(promises); + + // See comment above. Now that we have feature flags we can watch resources + toWatch.forEach((type) => { + dispatch('management/watch', { type }); + }); + const isMultiCluster = getters['isMultiCluster']; // If the local cluster is a Harvester cluster and 'rancher-manager-support' is true, it means that the embedded Rancher is being used. diff --git a/shell/types/store/pagination.types.ts b/shell/types/store/pagination.types.ts index b9ffb12eeb0..02da890fb10 100644 --- a/shell/types/store/pagination.types.ts +++ b/shell/types/store/pagination.types.ts @@ -69,14 +69,25 @@ export class PaginationFilterField { * Equality field within the object to filter by for example the `=` or `!=` of x=y */ equals: boolean; + /** + * Match the field exactly. False for partial matches + * + * Value: pod1 + * Exact: true. "p" no, "pod", no, "pod1" yes + * Exact: false. "p" yes, "pod", yes, "pod1" yes + */ + exact: boolean; constructor( - { field, value, equals = true }: - { field?: string; value: string; equals?: boolean; } + { + field, value, equals = true, exact = true + }: + { field?: string; value: string; equals?: boolean; exact?: boolean;} ) { this.field = field; this.value = value; this.equals = equals; + this.exact = exact; } } @@ -274,16 +285,18 @@ export class PaginationParamProjectOrNamespace extends PaginationParam { /** * Pagination settings sent to actions and persisted to store + * + * Use this for making pagination requests that utilise the new vai cache backed API */ export class PaginationArgs { /** * Page number to fetch */ - page: number; + page: number | null; /** * Number of results in the page */ - pageSize?: number; + pageSize?: number | null; /** * Sort the results * @@ -320,11 +333,11 @@ export class PaginationArgs { /** * For definition see {@link PaginationArgs} `page` */ - page?: number, + page?: number | null, /** * For definition see {@link PaginationArgs} `pageSize` */ - pageSize?: number, + pageSize?: number | null, /** * For definition see {@link PaginationArgs} `sort` */ @@ -358,6 +371,47 @@ export class PaginationArgs { } } +/** + * Wrapper around {@link PaginationArgs} + * + * Use this for making requests that utilise filtering backed by the new vai cache backed API + */ +export class FilterArgs extends PaginationArgs { + /** + * Creates an instance of PaginationArgs. + * + * Contains defaults to avoid creating complex json objects all the time + */ + constructor({ + sort = [], + filters = [], + projectsOrNamespaces = [], + }: + // This would be neater as just Partial but we lose all jsdoc + { + /** + * For definition see {@link PaginationArgs} `sort` + */ + sort?: PaginationSort[], + /** + * Automatically wrap if not an array + * + * For definition see {@link PaginationArgs} `filters` + */ + filters?: PaginationParamFilter | PaginationParamFilter[], + /** + * Automatically wrap if not an array + * + * For definition see {@link PaginationArgs} `projectsOrNamespaces` + */ + projectsOrNamespaces?: PaginationParamProjectOrNamespace | PaginationParamProjectOrNamespace[], + }) { + super({ + page: null, pageSize: null, sort, filters, projectsOrNamespaces + }); + } +} + /** * Overall result of a pagination request. * diff --git a/shell/utils/pagination-utils.ts b/shell/utils/pagination-utils.ts index 49d3af09707..44fc155c1a2 100644 --- a/shell/utils/pagination-utils.ts +++ b/shell/utils/pagination-utils.ts @@ -14,25 +14,13 @@ import { sameArrayObjects } from '@shell/utils/array'; import { isEqual } from '@shell/utils/object'; import { STEVE_CACHE } from '@shell/store/features'; import { getPerformanceSetting } from '@shell/utils/settings'; -import { DEFAULT_PERF_SETTING } from '@shell/config/settings'; - -/** - * Given the vai cache changes haven't merged, work around the settings that are blocked by it - * - * Once cache is merged (pre 2.9.0) this will be removed - */ -const TEMP_VAI_CACHE_MERGED = false; -/** - * Given above, just a dev thing - */ -const TEMP_PERF_ENABLED = false; /** * Helper functions for server side pagination */ class PaginationUtils { /** - * When a ns filter isn't one or more projects/namespaces... what the the valid values? + * When a ns filter isn't one or more projects/namespaces... what are the valid values? * * This basically blocks 'Not in a Project'.. which would involve a projectsornamespaces param with every ns not in a project. */ @@ -46,7 +34,7 @@ class PaginationUtils { isSteveCacheEnabled({ rootGetters }: any): boolean { // We always get Feature flags as part of start up (see `dispatch('features/loadServer')` in loadManagement) - return TEMP_VAI_CACHE_MERGED || rootGetters['features/get']?.(STEVE_CACHE); + return rootGetters['features/get']?.(STEVE_CACHE); } /** @@ -63,10 +51,7 @@ class PaginationUtils { return false; } - const settings = TEMP_PERF_ENABLED ? { - ...DEFAULT_PERF_SETTING.serverPagination, - enabled: true, - } : this.getSettings({ rootGetters }); + const settings = this.getSettings({ rootGetters }); // No setting, not enabled if (!settings?.enabled) {