diff --git a/README.md b/README.md
index c60d437e35..c82108501f 100644
--- a/README.md
+++ b/README.md
@@ -8,6 +8,7 @@ Developer documentation and documentation for our UI components is available her
Rancher Dashboard supports an extension mechanism that allows developers to independently provide additional functionality to Rancher. You can learn more from our [Rancher Extensions Docs](https://rancher.github.io/dashboard/extensions/introduction).
+
# What is it?
Rancher Dashboard provides a sophisticated UI for managing Kubernetes clusters and Workloads.
diff --git a/cypress/e2e/po/pages/explorer/config-map.po.ts b/cypress/e2e/po/pages/explorer/config-map.po.ts
index 93833d0a36..ae0bda5f06 100644
--- a/cypress/e2e/po/pages/explorer/config-map.po.ts
+++ b/cypress/e2e/po/pages/explorer/config-map.po.ts
@@ -1,5 +1,7 @@
import PagePo from '@/cypress/e2e/po/pages/page.po';
import BaseResourceList from '@/cypress/e2e/po/lists/base-resource-list.po';
+import BurgerMenuPo from '@/cypress/e2e/po/side-bars/burger-side-menu.po';
+import ProductNavPo from '@/cypress/e2e/po/side-bars/product-side-nav.po';
export class ConfigMapPagePo extends PagePo {
private static createPath(clusterId: string) {
@@ -10,19 +12,33 @@ export class ConfigMapPagePo extends PagePo {
return super.goTo(ConfigMapPagePo.createPath(clusterId));
}
+ static navTo(clusterId = 'local') {
+ const burgerMenu = new BurgerMenuPo();
+ const sideNav = new ProductNavPo();
+
+ BurgerMenuPo.toggle();
+ burgerMenu.clusters().contains(clusterId).click();
+ sideNav.navToSideMenuGroupByLabel('Storage');
+ sideNav.navToSideMenuEntryByLabel('ConfigMaps');
+ }
+
constructor(clusterId = 'local') {
super(ConfigMapPagePo.createPath(clusterId));
}
- clickCreate() {
- const baseResourceList = new BaseResourceList(this.self());
+ list() {
+ return new BaseResourceList(this.self());
+ }
- return baseResourceList.masthead().actions().eq(0).click();
+ clickCreate() {
+ return this.list().masthead().create();
}
listElementWithName(name:string) {
- const baseResourceList = new BaseResourceList(this.self());
+ return this.list().resourceTable().sortableTable().rowElementWithName(name);
+ }
- return baseResourceList.resourceTable().sortableTable().rowElementWithName(name);
+ searchForConfigMap(name: string) {
+ return this.list().resourceTable().sortableTable().filter(name);
}
}
diff --git a/cypress/e2e/tests/pages/explorer/api/api-services.spec.ts b/cypress/e2e/tests/pages/explorer/api/api-services.spec.ts
index a975316c4a..4bff4f2107 100644
--- a/cypress/e2e/tests/pages/explorer/api/api-services.spec.ts
+++ b/cypress/e2e/tests/pages/explorer/api/api-services.spec.ts
@@ -1,20 +1,17 @@
import { APIServicesPagePo } from '@/cypress/e2e/po/pages/explorer/api-services.po';
+const apiServicesPage = new APIServicesPagePo('local');
+
describe('Cluster Explorer', { tags: ['@explorer', '@adminUser'] }, () => {
beforeEach(() => {
cy.login();
});
describe('API: APIServices', () => {
- let apiServicesPage: APIServicesPagePo;
-
- beforeEach(() => {
- apiServicesPage = new APIServicesPagePo('local');
+ it('Should be able to use shift+j to select rows and the count of selected is correct', () => {
apiServicesPage.goTo();
apiServicesPage.waitForRequests();
- });
- it('Should be able to use shift+j to select rows and the count of selected is correct', () => {
apiServicesPage.title().should('contain', 'APIServices');
const sortableTable = apiServicesPage.sortableTable();
diff --git a/cypress/e2e/tests/pages/explorer/node-list.spec.ts b/cypress/e2e/tests/pages/explorer/node-list.spec.ts
index 624fd400e3..02c4e92446 100644
--- a/cypress/e2e/tests/pages/explorer/node-list.spec.ts
+++ b/cypress/e2e/tests/pages/explorer/node-list.spec.ts
@@ -15,7 +15,7 @@ describe('Nodes list', { tags: ['@explorer', '@adminUser'], testIsolation: 'off'
after(() => {
// Ensure we delete the dummy node
- cy.deleteRancherResource('v1', 'nodes', 'bigip1');
+ cy.deleteRancherResource('v1', 'nodes', dummyNode.metadata.name);
});
it('should show the nodes list page', () => {
@@ -48,7 +48,7 @@ describe('Nodes list', { tags: ['@explorer', '@adminUser'], testIsolation: 'off'
// Check the node names
nodeList.sortableTable().rowNames().should((names: any) => {
expect(names).to.have.length(count);
- expect(names).to.contain('bigip1');
+ expect(names).to.contain(dummyNode.metadata.name);
});
});
diff --git a/cypress/e2e/tests/pages/explorer/storage/configmap.spec.ts b/cypress/e2e/tests/pages/explorer/storage/configmap.spec.ts
index 525e90dac7..9b0bbc5524 100644
--- a/cypress/e2e/tests/pages/explorer/storage/configmap.spec.ts
+++ b/cypress/e2e/tests/pages/explorer/storage/configmap.spec.ts
@@ -1,14 +1,14 @@
import { ConfigMapPagePo } from '@/cypress/e2e/po/pages/explorer/config-map.po';
import ConfigMapPo from '@/cypress/e2e/po/components/storage/config-map.po';
-describe('ConfigMap', { tags: ['@explorer', '@adminUser'] }, () => {
+const configMapPage = new ConfigMapPagePo('local');
+
+describe('ConfigMap', { testIsolation: 'off', tags: ['@explorer', '@adminUser'] }, () => {
beforeEach(() => {
cy.login();
});
it('has the correct title', () => {
- const configMapPage = new ConfigMapPagePo('local');
-
configMapPage.goTo();
cy.title().should('eq', 'Rancher - local - ConfigMaps');
@@ -27,9 +27,7 @@ skipGeometric=true`;
// Visit the main menu and select the 'local' cluster
// Navigate to Service Discovery => ConfigMaps
- const configMapPage = new ConfigMapPagePo('local');
-
- configMapPage.goTo();
+ ConfigMapPagePo.navTo();
// Click on Create
configMapPage.clickCreate();
@@ -40,7 +38,9 @@ skipGeometric=true`;
// Enter ConfigMap description
const configMapPo = new ConfigMapPo();
- configMapPo.nameInput().set('custom-config-map');
+ const configMapName = 'custom-config-map';
+
+ configMapPo.nameInput().set(configMapName);
configMapPo.keyInput().set('managerApiConfiguration.properties');
configMapPo.valueInput().set(expectedValue);
configMapPo.descriptionInput().set('Custom Config Map Description');
@@ -49,14 +49,12 @@ skipGeometric=true`;
configMapPo.saveCreateForm().click();
// Check if the ConfigMap is created successfully
- configMapPage.listElementWithName('custom-config-map').should('exist');
+ configMapPage.waitForPage();
+ configMapPage.searchForConfigMap(configMapName);
+ configMapPage.listElementWithName(configMapName).should('exist');
// Navigate back to the edit page
- configMapPage.listElementWithName('custom-config-map')
- .find(`button[data-testid="sortable-table-0-action-button"]`)
- .click()
- .get(`li[data-testid="action-menu-0-item"]`)
- .click();
+ configMapPage.list().actionMenu(configMapName).getMenuItem('Edit Config').click();
// Assert the current value yaml dumps will append a newline at the end
configMapPo.valueInput().value().should('eq', `${ expectedValue }\n`);
diff --git a/cypress/e2e/tests/pages/generic/home.spec.ts b/cypress/e2e/tests/pages/generic/home.spec.ts
index d5e60d0a7b..6483175222 100644
--- a/cypress/e2e/tests/pages/generic/home.spec.ts
+++ b/cypress/e2e/tests/pages/generic/home.spec.ts
@@ -8,7 +8,7 @@ import { RANCHER_PAGE_EXCEPTIONS, catchTargetPageException } from '~/cypress/sup
const homePage = new HomePagePo();
const homeClusterList = homePage.list();
const provClusterList = new ClusterManagerListPagePo('local');
-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 rowDetails = (text) => text.split('\n').map((r) => r.trim()).filter((f) => f);
diff --git a/shell/components/ResourceList/index.vue b/shell/components/ResourceList/index.vue
index b3078a962e..c248037a13 100644
--- a/shell/components/ResourceList/index.vue
+++ b/shell/components/ResourceList/index.vue
@@ -172,7 +172,7 @@ export default {
* This covers case 1
*/
pagination(neu, old) {
- if (neu && !this.componentWillFetch && this.paginationEqual(neu, old)) {
+ if (neu && !this.componentWillFetch && !this.paginationEqual(neu, old)) {
this.$fetchType(this.resource);
}
},
diff --git a/shell/components/SortableTable/index.vue b/shell/components/SortableTable/index.vue
index 423999381c..034777ed10 100644
--- a/shell/components/SortableTable/index.vue
+++ b/shell/components/SortableTable/index.vue
@@ -29,21 +29,6 @@ import { getParent } from '@shell/utils/dom';
// NOTE: This is populated by a plugin (formatters.js) to avoid issues with plugins
export const FORMATTERS = {};
-export const COLUMN_BREAKPOINTS = {
- /**
- * Only show column if at tablet width or wider
- */
- TABLET: 'tablet',
- /**
- * Only show column if at laptop width or wider
- */
- LAPTOP: 'laptop',
- /**
- * Only show column if at desktop width or wider
- */
- DESKTOP: 'desktop'
-};
-
// @TODO:
// Fixed header/scrolling
diff --git a/shell/config/product/explorer.js b/shell/config/product/explorer.js
index e0fb117825..cdd9c4ecf7 100644
--- a/shell/config/product/explorer.js
+++ b/shell/config/product/explorer.js
@@ -19,7 +19,7 @@ import {
STORAGE_CLASS_PROVISIONER, PERSISTENT_VOLUME_SOURCE,
HPA_REFERENCE, MIN_REPLICA, MAX_REPLICA, CURRENT_REPLICA,
ACCESS_KEY, DESCRIPTION, EXPIRES, EXPIRY_STATE, SUB_TYPE, AGE_NORMAN, SCOPE_NORMAN, PERSISTENT_VOLUME_CLAIM, RECLAIM_POLICY, PV_REASON, WORKLOAD_HEALTH_SCALE, POD_RESTARTS,
- DURATION, MESSAGE, REASON, LAST_SEEN_TIME, EVENT_TYPE, OBJECT, ROLE, SECRET_DATA
+ DURATION, MESSAGE, REASON, LAST_SEEN_TIME, EVENT_TYPE, OBJECT, ROLE, ROLES, VERSION, INTERNAL_EXTERNAL_IP, KUBE_NODE_OS, CPU, RAM, SECRET_DATA
} from '@shell/config/table-headers';
import { DSL } from '@shell/store/type-map';
@@ -27,6 +27,8 @@ import {
STEVE_AGE_COL, STEVE_LIST_GROUPS, STEVE_NAMESPACE_COL, STEVE_NAME_COL, STEVE_STATE_COL
} from '@shell/config/pagination-table-headers';
+import { COLUMN_BREAKPOINTS } from '@shell/types/store/type-map';
+
export const NAME = 'explorer';
export function init(store) {
@@ -233,8 +235,11 @@ export function init(store) {
value: 'metadata.fields.1',
sort: 'metadata.fields.1',
search: 'metadata.fields.1',
+ }, {
+ ...SECRET_DATA,
+ sort: false,
+ search: false,
},
- SECRET_DATA,
STEVE_AGE_COL
]);
@@ -267,6 +272,66 @@ export function init(store) {
STEVE_AGE_COL
]);
+ headers(NODE,
+ [
+ STATE,
+ NAME_COL,
+ ROLES,
+ VERSION,
+ INTERNAL_EXTERNAL_IP,
+ {
+ ...KUBE_NODE_OS,
+ breakpoint: COLUMN_BREAKPOINTS.LAPTOP,
+ getValue: (row) => row.status?.nodeInfo?.operatingSystem
+ },
+ {
+ ...CPU,
+ breakpoint: COLUMN_BREAKPOINTS.LAPTOP,
+ getValue: (row) => row.cpuUsagePercentage
+ }, {
+ ...RAM,
+ breakpoint: COLUMN_BREAKPOINTS.LAPTOP,
+ getValue: (row) => row.ramUsagePercentage
+ },
+ AGE
+ ],
+ [
+ STEVE_STATE_COL,
+ STEVE_NAME_COL,
+ {
+ ...ROLES,
+ sort: false,
+ search: false
+ },
+ {
+ ...VERSION,
+ value: 'status.nodeInfo.kubeletVersion',
+ getValue: undefined,
+ sort: ['status.nodeInfo.kubeletVersion'],
+ search: 'status.nodeInfo.kubeletVersion'
+ }, {
+ ...INTERNAL_EXTERNAL_IP,
+ sort: false,
+ search: false,
+ }, {
+ ...KUBE_NODE_OS,
+ breakpoint: COLUMN_BREAKPOINTS.LAPTOP,
+ getValue: undefined
+ }, {
+ ...CPU,
+ breakpoint: COLUMN_BREAKPOINTS.LAPTOP,
+ getValue: (row) => row.cpuUsagePercentage,
+ sort: false,
+ search: false,
+ }, {
+ ...RAM,
+ breakpoint: COLUMN_BREAKPOINTS.LAPTOP,
+ sort: false,
+ search: false,
+ },
+ STEVE_AGE_COL
+ ]);
+
headers(MANAGEMENT.PSA, [STATE, NAME_COL, {
...DESCRIPTION,
width: undefined
diff --git a/shell/config/table-headers.js b/shell/config/table-headers.js
index 7aa3531abc..98df8540b1 100644
--- a/shell/config/table-headers.js
+++ b/shell/config/table-headers.js
@@ -1,6 +1,6 @@
import { CATTLE_PUBLIC_ENDPOINTS } from '@shell/config/labels-annotations';
import { NODE as NODE_TYPE } from '@shell/config/types';
-import { COLUMN_BREAKPOINTS } from '@shell/components/SortableTable/index.vue';
+import { COLUMN_BREAKPOINTS } from '@shell/types/store/type-map';
// Note: 'id' is always the last sort, so you don't have to specify it here.
diff --git a/shell/list/node.vue b/shell/list/node.vue
index 84c25fd388..4b4d96e0fb 100644
--- a/shell/list/node.vue
+++ b/shell/list/node.vue
@@ -1,21 +1,28 @@
-
@@ -162,12 +301,15 @@ export default {
v-bind="$attrs"
:schema="schema"
:headers="headers"
- :rows="parsedRows"
+ :rows="rows"
:sub-rows="true"
:loading="loading"
:use-query-params-for-simple-filtering="useQueryParamsForSimpleFiltering"
:force-update-live-and-delayed="forceUpdateLiveAndDelayed"
data-testid="cluster-node-list"
+ :external-pagination-enabled="canPaginate"
+ :external-pagination-result="paginationResult"
+ @pagination-changed="paginationChanged"
v-on="$listeners"
>
diff --git a/shell/mixins/fetch.client.js b/shell/mixins/fetch.client.js
index febb4349dd..c35a23a080 100644
--- a/shell/mixins/fetch.client.js
+++ b/shell/mixins/fetch.client.js
@@ -35,15 +35,19 @@ function beforeMount() {
}
}
-function $fetch() {
- if (!this._fetchPromise) {
- this._fetchPromise = $_fetch.call(this)
- .then(() => {
- delete this._fetchPromise;
- });
+function $fetch(cached = true) {
+ if (cached) {
+ if (!this._fetchPromise) {
+ this._fetchPromise = $_fetch.call(this)
+ .then(() => {
+ delete this._fetchPromise;
+ });
+ }
+
+ return this._fetchPromise;
}
- return this._fetchPromise;
+ return $_fetch.call(this);
}
async function $_fetch() { // eslint-disable-line camelcase
diff --git a/shell/mixins/resource-fetch-api-pagination.js b/shell/mixins/resource-fetch-api-pagination.js
index a72490ffe9..d1ecb6f911 100644
--- a/shell/mixins/resource-fetch-api-pagination.js
+++ b/shell/mixins/resource-fetch-api-pagination.js
@@ -1,12 +1,12 @@
import { NAMESPACE_FILTER_NAMESPACED_YES, NAMESPACE_FILTER_NAMESPACED_NO, NAMESPACE_FILTER_ALL } from '@shell/utils/namespace-filter';
+import { NAMESPACE } from '@shell/config/types';
+import { ALL_NAMESPACES } from '@shell/store/prefs';
import { mapGetters } from 'vuex';
import { ResourceListComponentName } from '../components/ResourceList/resource-list.config';
import paginationUtils from '@shell/utils/pagination-utils';
import debounce from 'lodash/debounce';
-import { OptPaginationFilter, OptPaginationFilterField } from '@shell/types/store/dashboard-store.types';
+import { PaginationParamFilter, PaginationFilterField, PaginationArgs } from '@shell/types/store/pagination.types';
import stevePaginationUtils from '@shell/plugins/steve/steve-pagination-utils';
-import { ALL_NAMESPACES } from '@shell/store/prefs';
-import { NAMESPACE } from '@shell/config/types';
/**
* Companion mixin used with `resource-fetch` for `ResourceList` to determine if the user needs to filter the list by a single namespace
@@ -34,18 +34,22 @@ export default {
},
methods: {
+ /**
+ * @param {PaginationArgs} pagination
+ */
setPagination(pagination) {
- this.pPagination = pagination;
+ if (pagination) {
+ this.pPagination = pagination;
+ }
},
paginationChanged(event) {
- const searchFilters = event.filter.searchQuery ? event.filter.searchFields.map((field) => new OptPaginationFilterField({
+ const searchFilters = event.filter.searchQuery ? event.filter.searchFields.map((field) => new PaginationFilterField({
field,
value: event.filter.searchQuery,
})) : [];
- this.debouncedSetPagination({
- ...this.pPagination,
+ const pagination = new PaginationArgs({
page: event.page,
pageSize: event.perPage,
sort: event.sort?.map((field) => ({
@@ -53,11 +57,13 @@ export default {
asc: !event.descending
})),
projectsOrNamespaces: this.requestFilters.projectsOrNamespaces,
- filter: [
- new OptPaginationFilter({ fields: searchFilters }),
+ filters: [
+ new PaginationParamFilter({ fields: searchFilters }),
...this.requestFilters.filters, // Apply the additional filters. these aren't from the user but from ns filtering
]
});
+
+ this.debouncedSetPagination(pagination);
},
namespaceFilterChanged(neu) {
@@ -81,26 +87,40 @@ export default {
this.requestFilters.projectsOrNamespaces = projectsOrNamespaces;
// Kick off a change
- this.debouncedSetPagination({ ...this.pPagination });
+ if (this.pPagination) {
+ this.debouncedSetPagination({ ...this.pPagination });
+ }
},
+ /**
+ * @param {PaginationArgs} neu
+ * @param {PaginationArgs} old
+ */
paginationEqual(neu, old) {
if (!neu.page) {
- // Not valid, don't bother
+ // Not valid, count as not equal
return false;
}
if (paginationUtils.paginationEqual(neu, old)) {
- // Same, nae bother
- return false;
+ return true;
}
- return true;
+ return false;
}
},
computed: {
- ...mapGetters(['currentProduct', 'namespaceFilters', 'isAllNamespaces']),
+ ...mapGetters(['currentProduct', 'isAllNamespaces']),
+
+ /**
+ * Why is this a specific getter and not not in mapGetters?
+ *
+ * Adding it to mapGetters means the kubewarden unit tests fail as they don't mock it....
+ */
+ namespaceFilters() {
+ return this.$store.getters['namespaceFilters'];
+ },
/**
* Does the user need to update the filter to supply valid options?
@@ -120,14 +140,14 @@ export default {
* ResourceList imports resource-fetch --> this mixin
* When there is no custom list this is fine (ResourceList with mixins --> ResourceTable)
* When there is a custom list there are two instances of this mixin (ResourceList with mixins --> CustomList with mixins --> ResourceTable)
- * - In this scenario, reduce churn by existing earlier if mixin is from parent ResourceList and leave work for CustomList mixins
+ * - In this scenario, reduce churn by exiting earlier if mixin is from parent ResourceList and leave work for CustomList mixins
*/
isResourceList() {
return !!this.hasListComponent;
},
/**
- * Is Pagination supported and ready for this list?
+ * Is Pagination supported and has the table supplied pagination settings from the table?
*/
pagination() {
if (this.isResourceList) {
@@ -152,7 +172,7 @@ export default {
},
paginationResult() {
- if (this.isResourceList) {
+ if (this.isResourceList || !this.canPaginate) {
return;
}
@@ -188,7 +208,7 @@ export default {
* result in an empty page
*/
rows(neu) {
- if (!this.pagination || this.isResourceList) {
+ if (!this.canPaginate || !this.pagination || this.isResourceList) {
return;
}
@@ -203,6 +223,10 @@ export default {
namespaceFilters: {
immediate: true,
async handler(neu, old) {
+ if (!this.canPaginate || !this.schema?.attributes?.namespaced) {
+ return;
+ }
+
if (this.isResourceList) {
return;
}
@@ -231,25 +255,52 @@ export default {
}
},
+ /**
+ * When a pagination is required and the user changes page / sort / filter, kick off a new set of API requests
+ *
+ * @param {StorePaginationResult} neu
+ * @param {StorePaginationResult} old
+ */
async pagination(neu, old) {
- if (this.isResourceList) {
+ if (!this.canPaginate) {
return;
}
- // When a pagination is required and the user changes page / sort / filter, kick off a new set of API requests
- //
// ResourceList has two modes
// 1) ResourceList component handles API request to fetch resources
// 2) Custom list component handles API request to fetch resources
//
- // This covers case 2
- if (neu && this.$options.name !== ResourceListComponentName && !!this.$fetch && this.paginationEqual(neu, old)) {
- await this.$fetch();
+ // This covers case 2, so ignore case 1
+ if (this.isResourceList) {
+ return;
+ }
+ if (neu && this.$options.name !== ResourceListComponentName && !!this.$fetch && !this.paginationEqual(neu, old)) {
+ await this.$fetch(false);
// Ensure any live/delayed columns get updated
this.forceUpdateLiveAndDelayed = new Date().getTime();
}
},
+ /**
+ * If the pagination result has changed fetch secondary resources
+ *
+ * Lists should implement fetchPageSecondaryResources to fetch them
+ *
+ * @param {StorePaginationResult} neu
+ * @param {StorePaginationResult} old
+ */
+ async paginationResult(neu, old) {
+ if (!this.fetchPageSecondaryResources || !neu ) { // || neu.timestamp === old?.timestamp
+ return;
+ }
+
+ if (neu.timestamp === old?.timestamp) {
+ // This occurs when the user returns to the page... and pagination hasn't actually changed
+ return;
+ }
+
+ await this.fetchPageSecondaryResources();
+ }
},
};
diff --git a/shell/mixins/resource-fetch.js b/shell/mixins/resource-fetch.js
index fac539d9c1..848b4f6b28 100644
--- a/shell/mixins/resource-fetch.js
+++ b/shell/mixins/resource-fetch.js
@@ -46,6 +46,7 @@ export default {
paginating: null,
};
},
+
beforeDestroy() {
// make sure this only runs once, for the initialized instance
if (this.init) {
@@ -77,14 +78,19 @@ export default {
return this.rows.length ? false : this.$fetchState.pending;
},
},
+
watch: {
- refreshFlag(neu) {
+ async refreshFlag(neu) {
// this is where the data assignment will trigger the update of the list view...
if (this.init && neu) {
- this.$fetch();
+ await this.$fetch();
+ if (this.canPaginate && this.fetchPageSecondaryResources) {
+ this.fetchPageSecondaryResources(true);
+ }
}
}
},
+
methods: {
// this defines all the flags needed for the mechanism
// to work. They should be defined based on the main list view
@@ -127,7 +133,6 @@ export default {
// when pagination is enabled we want to wait for the correct set of initial pagination settings to make the call
return;
}
-
const opt = {
hasManualRefresh: this.hasManualRefresh,
pagination: { ...this.pagination },
@@ -141,7 +146,8 @@ export default {
return this.$store.dispatch(`${ currStore }/findPage`, {
type,
opt
- }).finally(() => Vue.set(that, 'paginating', false));
+ })
+ .finally(() => Vue.set(that, 'paginating', false));
}
let incremental = 0;
diff --git a/shell/models/cluster/node.js b/shell/models/cluster/node.js
index 2d942e980d..da4f364f6f 100644
--- a/shell/models/cluster/node.js
+++ b/shell/models/cluster/node.js
@@ -450,6 +450,10 @@ export default class ClusterNode extends SteveModel {
get provider() {
return this.$rootGetters['currentCluster'].provisioner.toLowerCase();
}
+
+ get displayTaintsAndLabels() {
+ return !!this.spec.taints?.length || !!this.customLabelCount;
+ }
}
function calculatePercentage(allocatable, capacity) {
diff --git a/shell/plugins/dashboard-store/actions.js b/shell/plugins/dashboard-store/actions.js
index 733191381c..6380eebc8a 100644
--- a/shell/plugins/dashboard-store/actions.js
+++ b/shell/plugins/dashboard-store/actions.js
@@ -123,7 +123,7 @@ export default {
}
});
} else {
- // We have everything!
+ // We have everything!
if (opt.hasManualRefresh) {
dispatch('resource-fetch/updateManualRefreshIsLoading', false, { root: true });
}
@@ -145,7 +145,7 @@ export default {
/**
*
* @param {*} ctx
- * @param { {type: string, opt: FindAllOpt} } opt
+ * @param { {type: string, opt: ActionFindPageArgs} } opt
*/
async findAll(ctx, { type, opt }) {
const {
@@ -164,8 +164,7 @@ export default {
!opt.force &&
(
getters['haveAll'](type) ||
- getters['haveAllNamespace'](type, opt.namespaced) ||
- (opt.pagination ? getters['havePaginatedPage'](type, opt.pagination) : false)
+ getters['haveAllNamespace'](type, opt.namespaced)
)
) {
if (opt.watch !== false ) {
@@ -323,8 +322,9 @@ export default {
pagination: opt.pagination ? {
request: opt.pagination,
result: {
- count: out.count,
- pages: out.pages
+ count: out.count,
+ pages: out.pages,
+ timestamp: new Date().getTime()
}
} : undefined,
});
@@ -383,7 +383,7 @@ export default {
}
// No need to request the resources if we have them already
- if (!opt.force && getters['havePaginatedPage'](type, opt.pagination)) {
+ if (!opt.force && getters['havePaginatedPage'](type, opt)) {
return findAllGetter(getters, type, opt);
}
@@ -412,10 +412,14 @@ export default {
type,
data: out.data,
pagination: opt.pagination ? {
- request: opt.pagination,
- result: {
- count: out.count,
- pages: out.pages
+ request: {
+ namespace: opt.namespaced,
+ pagination: opt.pagination
+ },
+ result: {
+ count: out.count,
+ pages: out.pages,
+ timestamp: new Date().getTime()
}
} : undefined,
});
diff --git a/shell/plugins/dashboard-store/getters.js b/shell/plugins/dashboard-store/getters.js
index a2f2872656..0623ceaa38 100644
--- a/shell/plugins/dashboard-store/getters.js
+++ b/shell/plugins/dashboard-store/getters.js
@@ -15,8 +15,10 @@ export const urlFor = (state, getters) => (type, id, opt) => {
type = getters.normalizeType(type);
let url = opt.url;
+ let schema;
+
if ( !url ) {
- const schema = getters.schemaFor(type);
+ schema = getters.schemaFor(type);
if ( !schema ) {
throw new Error(`Unknown schema for type: ${ type }`);
@@ -39,7 +41,7 @@ export const urlFor = (state, getters) => (type, id, opt) => {
url = `${ baseUrl }/${ url }`;
}
- url = getters.urlOptions(url, opt);
+ url = getters.urlOptions(url, opt, schema);
return url;
};
@@ -298,16 +300,22 @@ export default {
return false;
},
- havePaginatedPage: (state, getters) => (type, pagination) => {
- if (!pagination) {
+ havePaginatedPage: (state, getters) => (type, opt) => {
+ if (!opt.pagination) {
return false;
}
type = getters.normalizeType(type);
const entry = state.types[type];
- if ( entry ) {
- return entry.havePage && paginationUtils.paginationEqual(entry.havePage.request, pagination);
+ if ( entry?.havePage ) {
+ const { namespace: aNamespace = undefined, pagination: aPagination } = entry.havePage.request;
+ const { namespace: bNamespace = undefined, pagination: bPagination } = {
+ namespace: opt.namespaced,
+ pagination: opt.pagination
+ };
+
+ return entry.havePage && aNamespace === bNamespace && paginationUtils.paginationEqual(aPagination, bPagination);
}
return false;
@@ -346,7 +354,7 @@ export default {
urlFor,
- urlOptions: () => (url, opt) => {
+ urlOptions: () => (url, opt, schema) => {
return url;
},
diff --git a/shell/plugins/steve/__tests__/getters.test.ts b/shell/plugins/steve/__tests__/getters.test.ts
index 96361915cf..11111d928f 100644
--- a/shell/plugins/steve/__tests__/getters.test.ts
+++ b/shell/plugins/steve/__tests__/getters.test.ts
@@ -20,7 +20,7 @@ describe('steve: getters:', () => {
}
},
// this has its own tests so it just returns the input string
- urlOptions: (url: string, opt: any) => {
+ urlOptions: (url: string, opt: any, type: string) => {
if (opt.addParam) {
url += '?param=true';
}
diff --git a/shell/plugins/steve/accept-or-reject-socket-message.ts b/shell/plugins/steve/accept-or-reject-socket-message.ts
index d1f25966a4..65a1d0fd75 100644
--- a/shell/plugins/steve/accept-or-reject-socket-message.ts
+++ b/shell/plugins/steve/accept-or-reject-socket-message.ts
@@ -3,12 +3,11 @@ import pAndNFiltering from '@shell/plugins/steve/projectAndNamespaceFiltering.ut
type TypeIsCached = { [type: string]: boolean }
/**
- * There are scenarios where we can't subscribe to subsets of a resources
+ * There are scenarios where we can't subscribe to subsets of a resource type
* - Multiple namespaces or projects
* - Result of Pagination (a single page of resources that have been sorted / filtered)
*
* For those scenarios we subscribe to allll changes BUT ignore changes that are not applicable to that subset
- *
*/
class AcceptOrRejectSocketMessage {
typeIsNamespaced({ getters }: any, type: string): boolean {
diff --git a/shell/plugins/steve/getters.js b/shell/plugins/steve/getters.js
index df1c173eba..dd60616ea5 100644
--- a/shell/plugins/steve/getters.js
+++ b/shell/plugins/steve/getters.js
@@ -36,12 +36,12 @@ const GC_IGNORE_TYPES = {
const steveRegEx = new RegExp('(/v1)|(\/k8s\/clusters\/[a-z0-9-]+\/v1)');
export default {
- urlOptions: () => (url, opt) => {
+ urlOptions: () => (url, opt, schema) => {
opt = opt || {};
const parsedUrl = parse(url);
const isSteve = steveRegEx.test(parsedUrl.path);
- const stevePagination = stevePaginationUtils.checkAndCreateParam(opt);
+ const stevePagination = stevePaginationUtils.createParamsForPagination(schema, opt);
if (stevePagination) {
url += `${ (url.includes('?') ? '&' : '?') + stevePagination }`;
@@ -323,7 +323,7 @@ export default {
*/
optionsFor: () => (ctx, { schema, pagination, opts }) => {
if (pagination) {
- // As headers are hardcoded each list should specify the specific default sort option
+ // As headers are hardcoded each list should have specific default sort option
// This avoids the sortable table adding both name and id (which when combined with group would result in 3 sort args, which isn't supported)
const steveOpts = { listMandatorySort: [] };
diff --git a/shell/plugins/steve/projectAndNamespaceFiltering.utils.ts b/shell/plugins/steve/projectAndNamespaceFiltering.utils.ts
index 75079c1586..fce015e59b 100644
--- a/shell/plugins/steve/projectAndNamespaceFiltering.utils.ts
+++ b/shell/plugins/steve/projectAndNamespaceFiltering.utils.ts
@@ -1,6 +1,6 @@
import { NAMESPACE_FILTER_NS_FULL_PREFIX, NAMESPACE_FILTER_P_FULL_PREFIX } from '@shell/utils/namespace-filter';
import { getPerformanceSetting } from '@shell/utils/settings';
-import { FindAllOpt } from '@shell/types/store/dashboard-store.types';
+import { ActionFindAllArgs } from '@shell/types/store/dashboard-store.types';
class ProjectAndNamespaceFiltering {
static param = 'projectsornamespaces'
@@ -8,7 +8,7 @@ class ProjectAndNamespaceFiltering {
/**
* Does the request `opt` definition require resources are fetched from a specific set namespaces/projects?
*/
- isApplicable(opt: FindAllOpt): boolean {
+ isApplicable(opt: ActionFindAllArgs): boolean {
return Array.isArray(opt.namespaced);
}
@@ -36,7 +36,7 @@ class ProjectAndNamespaceFiltering {
/**
* Check if `opt` requires resources from specific ns/projects, if so return the required query param (x=y)
*/
- checkAndCreateParam(opt: FindAllOpt): string {
+ checkAndCreateParam(opt: ActionFindAllArgs): string {
if (!this.isApplicable(opt)) {
return '';
}
diff --git a/shell/plugins/steve/schema.d.ts b/shell/plugins/steve/schema.d.ts
index 758230f358..84e556efec 100644
--- a/shell/plugins/steve/schema.d.ts
+++ b/shell/plugins/steve/schema.d.ts
@@ -1,3 +1,4 @@
+
export interface SchemaAttributeColumn {
description: string,
field: string,
@@ -11,3 +12,11 @@ export interface SchemaAttribute {
columns: SchemaAttributeColumn[],
namespaced: boolean
}
+
+/**
+ * At some point this will be properly typed, until then...
+ */
+export interface Schema {
+ id: string,
+ attributes: SchemaAttribute
+}
diff --git a/shell/plugins/steve/steve-pagination-utils.ts b/shell/plugins/steve/steve-pagination-utils.ts
index f52445af4c..dfc76c4277 100644
--- a/shell/plugins/steve/steve-pagination-utils.ts
+++ b/shell/plugins/steve/steve-pagination-utils.ts
@@ -1,12 +1,16 @@
-import { FindPageOpt, OptPaginationFilter, OptPaginationFilterField, OptPaginationProjectOrNamespace } from '@shell/types/store/dashboard-store.types';
+import { ActionFindPageArgs } from '@shell/types/store/dashboard-store.types';
+import { PaginationParam, PaginationFilterField, PaginationParamProjectOrNamespace, PaginationParamFilter } from '@shell/types/store/pagination.types';
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 { MANAGEMENT, NODE, POD } from '@shell/config/types';
+import { Schema } from 'plugins/steve/schema';
class NamespaceProjectFilters {
/**
* User needs all resources.... except if there's some settings which should remove resources in specific circumstances
*/
- protected handlePrefAndSettingFilter(allNamespaces: Namespace[], showDynamicRancherNamespaces: boolean, productHidesSystemNamespaces: boolean): OptPaginationFilter[] {
+ protected handlePrefAndSettingFilter(allNamespaces: Namespace[], showDynamicRancherNamespaces: boolean, productHidesSystemNamespaces: boolean): PaginationParamFilter[] {
// These are AND'd together
// Not ns 1 AND ns 2
return allNamespaces.reduce((res, ns) => {
@@ -16,15 +20,13 @@ class NamespaceProjectFilters {
const hideSystem = productHidesSystemNamespaces ? ns.isSystem : false;
if (hideObscure || hideSystem) {
- res.push(new OptPaginationFilter({
- fields: [new OptPaginationFilterField({
- field: 'metadata.namespace', value: ns.name, equals: false
- })]
+ res.push(PaginationParamFilter.createSingleField({
+ field: 'metadata.namespace', value: ns.name, equals: false
}));
}
return res;
- }, [] as OptPaginationFilter[]);
+ }, [] as PaginationParamFilter[]);
}
/**
@@ -42,20 +44,16 @@ class NamespaceProjectFilters {
if (isAllSystem) {
// return resources in system ns 1 OR in system ns 2 ...
// &filter=metadata.namespace=system ns 1,metadata.namespace=system ns 2
- return [
- new OptPaginationFilter({
- fields: allSystem.map(
- (ns) => new OptPaginationFilterField({ field: 'metadata.namespace', value: ns.name })
- )
- })
- ];
+ return [PaginationParamFilter.createMultipleFields(
+ allSystem.map(
+ (ns) => new PaginationFilterField({ field: 'metadata.namespace', value: ns.name })
+ )
+ )];
} else { // if isAllUser
// return resources not in system ns 1 AND not in system ns 2 ...
// &filter=metadata.namespace!=system ns 1&filter=metadata.namespace!=system ns 2
- return allSystem.map((ns) => new OptPaginationFilter({
- fields: [{
- field: 'metadata.namespace', value: ns.name, equals: false
- }]
+ return allSystem.map((ns) => PaginationParamFilter.createSingleField({
+ field: 'metadata.namespace', value: ns.name, equals: false
}));
}
}
@@ -68,7 +66,9 @@ class NamespaceProjectFilters {
// return resources in project 1 OR namespace 2
// &projectsornamespaces=project 1,namespace 2
- const projectsOrNamespaces = [new OptPaginationProjectOrNamespace({ fields: neu.map((selection) => new OptPaginationFilterField({ value: selection })) })];
+ const projectsOrNamespaces = [
+ new PaginationParamProjectOrNamespace({ projectOrNamespace: neu })
+ ];
if (isLocalCluster) {
// > As per `handleSystemOrUserFilter` above, we need to be careful of the local cluster where there's namespaces related to projects with the same id
@@ -82,10 +82,8 @@ class NamespaceProjectFilters {
projectsOrNamespaces,
filters: neu
.filter((selection) => selection.startsWith(NAMESPACE_FILTER_P_FULL_PREFIX))
- .map((projects) => new OptPaginationFilter({
- fields: [{
- field: 'metadata.namespace', value: projects.replace(NAMESPACE_FILTER_P_FULL_PREFIX, ''), equals: false
- }]
+ .map((projects) => PaginationParamFilter.createSingleField({
+ field: 'metadata.namespace', value: projects.replace(NAMESPACE_FILTER_P_FULL_PREFIX, ''), equals: false
}))
};
}
@@ -98,6 +96,30 @@ class NamespaceProjectFilters {
* Helper functions for steve pagination
*/
class StevePaginationUtils extends NamespaceProjectFilters {
+ /**
+ * Filtering with the vai cache supports specific fields
+ * 1) Those listed here
+ * 2) Those references in the schema's attributes.fields list (which is used by generic lists)
+ */
+ static VALID_FIELDS: { [type: string]: { field: string, startsWith?: boolean }[]} = {
+ '': [// all types
+ { field: 'metadata.name' },
+ { field: 'metadata.namespace' },
+ { field: 'metadata.state.name' },
+ ],
+ [NODE]: [
+ { field: 'status.nodeInfo.kubeletVersion' },
+ { field: 'status.nodeInfo.operatingSystem' },
+ ],
+ [POD]: [
+ { field: 'spec.containers.image' },
+ { field: 'spec.nodeName' },
+ ],
+ [MANAGEMENT.NODE]: [
+ { field: 'status.nodeName' }
+ ]
+ }
+
/**
* Given the selection of projects or namespaces come up with `filter` and `projectsornamespace` query params
*/
@@ -116,7 +138,7 @@ class StevePaginationUtils extends NamespaceProjectFilters {
*/
isAllNamespaces: boolean,
/**
- * Weird things be happening if the target cluster is local / upstream
+ * Weird things be happening if the target cluster is local / upstream. Uses this to check what cluster we're in
*/
isLocalCluster: boolean,
/**
@@ -128,19 +150,19 @@ class StevePaginationUtils extends NamespaceProjectFilters {
*/
productHidesSystemNamespaces: boolean,
}): {
- projectsOrNamespaces: OptPaginationFilter[],
- filters: OptPaginationFilter[]
+ projectsOrNamespaces: PaginationParamProjectOrNamespace[],
+ filters: PaginationParamFilter[]
} {
// Hold up, why are we doing yet another way to convert the user's project / namespace filter to a set of something?
- // - When doing this for local pagination `getActiveNamespaces` provides a full list of applicable namespaces. Lists then filter resource locally
+ // - When doing this for local pagination `getActiveNamespaces` provides a full list of applicable namespaces.
+ // Lists then filter resource locally using those namespaces
// - Pagination cannot take this approach of 'gimme all resources in these namespaces' primarily for the 'Only User Namespaces' case
// - User could have 2k namespaces. This would result in 2k+ namespaces added to the url (namespace=1,namespace=2,namespace=3, etc)
- // - Instead we do // TODO: RC
+ // - Instead we do
// - All but not given settings - Gimme resources NOT in system or obscure namespaces
// - Only System Namespaces - Gimme resources in the system namespaces (which shouldn't be many namespaces)
// - Only User Namespaces - Gimme resources NOT in system namespaces
// - User selection - Gimme resources in specific Projects or Namespaces
-
if (isAllNamespaces && (showDynamicRancherNamespaces && !productHidesSystemNamespaces)) {
// No-op. Everything is returned
return {
@@ -151,10 +173,10 @@ class StevePaginationUtils extends NamespaceProjectFilters {
// used to return resources in / not in projects/namespaces (entries are checked in both types)
// &projectsornamespaces=project 1,namespace 2
- let projectsOrNamespaces: OptPaginationFilter[] = [];
+ let projectsOrNamespaces: PaginationParamProjectOrNamespace[] = [];
// used to return resources in / not in namespaces
// &filter=metadata.namespace=abc
- let filters: OptPaginationFilter[] = [];
+ let filters: PaginationParamFilter[] = [];
if (!showDynamicRancherNamespaces || productHidesSystemNamespaces) {
// We need to hide dynamic namespaces ('c-', 'p-', etc) OR system namespaces
@@ -181,13 +203,13 @@ class StevePaginationUtils extends NamespaceProjectFilters {
};
}
- checkAndCreateParam(opt: FindPageOpt): string | undefined {
+ public createParamsForPagination(schema: Schema, opt: ActionFindPageArgs): string | undefined {
if (!opt.pagination) {
return;
}
const params: string[] = [];
- const namespaceParam = this.convertPaginationFilter(opt.pagination.projectsOrNamespaces);
+ const namespaceParam = this.convertPaginationParams(schema, opt.pagination.projectsOrNamespaces);
if (namespaceParam) {
params.push(namespaceParam);
@@ -201,8 +223,6 @@ class StevePaginationUtils extends NamespaceProjectFilters {
if (opt.pagination.pageSize) {
params.push(`pagesize=${ opt.pagination.pageSize }`);
- } else {
- throw new Error(`A pagination request is required but no 'page' property provided: ${ JSON.stringify(opt) }`);
}
if (opt.pagination.sort?.length) {
@@ -213,11 +233,11 @@ class StevePaginationUtils extends NamespaceProjectFilters {
params.push(`sort=${ joined }`);
}
- if (opt.pagination.filter?.length) {
- const andFilters = this.convertPaginationFilter(opt.pagination.filter);
+ if (opt.pagination.filters?.length) {
+ const filters = this.convertPaginationParams(schema, opt.pagination.filters);
- if (andFilters) {
- params.push(andFilters);
+ if (filters) {
+ params.push(filters);
}
}
@@ -227,13 +247,64 @@ class StevePaginationUtils extends NamespaceProjectFilters {
return params.join('&');
}
- private convertPaginationFilter(filters: OptPaginationFilter[] = []): string {
- return filters
+ /**
+ * Check if the API supports filtering by this field
+ */
+ private validateField(state: { checked: string[], invalid: string[]}, schema: Schema, field?: string) {
+ if (!field) {
+ return; // no field, so not invalid
+ }
+
+ if (state.checked.includes(field)) {
+ return; // already checked, exit early
+ }
+
+ state.checked.push(field);
+
+ // First check in our hardcoded list of supported filters
+ if ([
+ StevePaginationUtils.VALID_FIELDS[''], // Global
+ StevePaginationUtils.VALID_FIELDS[schema.id], // Type specific
+ ].find((fields) => fields?.find((f) => {
+ if (f.startsWith) {
+ if (field.startsWith(f.field)) {
+ return true;
+ }
+ } else {
+ return field === f.field;
+ }
+ }))) {
+ return;
+ }
+
+ // Then check in schema (the api automatically supports these)
+ if (!!schema?.attributes.columns.find(
+ // This isn't the most performant, but the string is tiny
+ (at) => at.field.replace('$.', '').replace('[', '.').replace(']', '') === field
+ )) {
+ return;
+ }
+
+ state.invalid.push(field);
+ }
+
+ /**
+ * Convert our {@link PaginationParam} definition of params to a set of url params
+ */
+ private convertPaginationParams(schema: Schema, filters: PaginationParam[] = []): string {
+ const validateFields = {
+ checked: new Array(),
+ invalid: new Array(),
+ };
+ const res = filters
.filter((filter) => !!filter.fields.length)
.map((filter) => {
const joined = filter.fields
.map((field) => {
if (field.field) {
+ // Check if the API supports filtering by this field
+ this.validateField(validateFields, schema, field.field);
+
return `${ field.field }${ field.equals ? '=' : '!=' }${ field.value }`;
}
@@ -243,6 +314,12 @@ class StevePaginationUtils extends NamespaceProjectFilters {
return `${ filter.param }${ filter.equals ? '=' : '!=' }${ joined }`;
}).join('&'); // This means AND
+
+ if (validateFields.invalid.length) {
+ console.warn(`Pagination API does not support filtering '${ schema.id }' by the requested fields: ${ uniq(validateFields.invalid).join(', ') }`); // eslint-disable-line no-console
+ }
+
+ return res;
}
}
diff --git a/shell/types/store/dashboard-store.types.ts b/shell/types/store/dashboard-store.types.ts
index 04fba3e9b3..603c31351a 100644
--- a/shell/types/store/dashboard-store.types.ts
+++ b/shell/types/store/dashboard-store.types.ts
@@ -1,169 +1,16 @@
-import { NAMESPACE_FILTER_NS_FULL_PREFIX, NAMESPACE_FILTER_P_FULL_PREFIX } from '@shell/utils/namespace-filter';
+import { PaginationArgs } from '@shell/types/store/pagination.types';
/**
- * Sort the pagination result
- *
- * For more information see https://github.com/rancher/steve?tab=readme-ov-file#sort
+ * Properties on all findX actions
*/
-export interface OptPaginationSort {
- /**
- * Name of field within the object to sort by
- */
- field: string,
- asc: boolean
-}
-
-/**
- * Filter the pagination result by these specific fields
- *
- * For example
- *
- * - metadata.name=test
- * - metadata.namespace!=system
- *
- * For more information see https://github.com/rancher/steve?tab=readme-ov-file#query-parameters
- */
-export class OptPaginationFilterField {
- /**
- * Name of field within the object to sort by
- *
- * This can be optional for some (projectsornamespaces)
- */
- field?: string;
- value: string;
- equals: boolean;
-
- constructor(
- { field, value, equals = true }:
- { field?: string; value: string; equals?: boolean; }
- ) {
- this.field = field;
- this.value = value;
- this.equals = equals;
- }
-}
-
-/**
- * Filter the pagination result by a number of fields
- *
- * OptPaginationFilter can be used in two ways
- *
- * 1) OR'd together
- * - a=1 OR b=2 OR c=3
- - Query Param - filter=a=1,b=2,c=3
- * - Object structure - [
- * [a,1],[b,2],[c,3]
- * ]
- *
- * 2) AND'd together
- *- a=1 AND b=2 AND c=3
- * - Query Param - filter=a=1&filter=b=2&filter=c=3
- * - Object structure - [
- * [a,1]
- * ],
- * [
- * [b,2]
- * ],
- * [
- * [c,3]
- * ]
- *
- * This structure should give enough flexibility to cover all uses
- *
- * For more information see https://github.com/rancher/steve?tab=readme-ov-file#filter
- */
-export class OptPaginationFilter {// implements OptPaginationFilter
- /**
- * Query Param. For example `filter` or `projectsornamespaces`
- */
- param: string;
- /**
- * should fields equal param.
- *
- * For example projectsornamexspaces=x or projectsornamexspaces!=x
- */
- equals: boolean;
- /**
- * Fields to filter by
- *
- * For example metadata.namespace=abc OR metadata.namespace=xyz
- */
- fields: OptPaginationFilterField[];
-
- constructor(
- { param = 'filter', equals = true, fields = [] }:
- { param?: string; equals?: boolean; fields?: OptPaginationFilterField[];
- }) {
- this.param = param;
- this.equals = equals;
- this.fields = fields;
- }
-}
-
-/**
- * This is a convenience class which works some magic, adds defaults and converts to the required OptPaginationFilter format
- *
- * For more information see https://github.com/rancher/steve?tab=readme-ov-file#projectsornamespaces
- */
-export class OptPaginationProjectOrNamespace extends OptPaginationFilter {
- constructor(
- { equals = true, fields = [] }:
- { equals?: boolean; fields?: OptPaginationFilterField[]; }
- ) {
- const safeFields = fields.map((f) => {
- return {
- ...f,
- value: f.value
- .replace(NAMESPACE_FILTER_NS_FULL_PREFIX, '')
- .replace(NAMESPACE_FILTER_P_FULL_PREFIX, '')
- };
- });
-
- super({
- param: 'projectsornamespaces',
- equals,
- fields: safeFields
- });
- }
-}
-
-/**
- * Pagination settings sent to actions and persisted to store
- */
-export interface OptPagination {
- page: number,
- pageSize: number,
- sort: OptPaginationSort[],
- filter: OptPaginationFilter[],
- projectsOrNamespaces: OptPaginationFilter[],
-}
-
-/**
- * Object persisted to store
- */
-export interface StorePagination {
- /**
- * This set of pagination settings that created the result
- */
- request: OptPagination,
- /**
- * Information in the response outside of the actual resources returned
- */
- result: {
- count: number,
- pages: number
- }
-}
-
-export type CoreFindOpt = {
- type: string,
+export type ActionCoreFindArgs = {
force?: boolean,
}
/**
* Args used for findAll action
*/
-export interface FindAllOpt extends CoreFindOpt {
+export interface ActionFindAllArgs extends ActionCoreFindArgs {
watch?: boolean,
namespaced?: string[],
incremental?: boolean,
@@ -174,7 +21,16 @@ export interface FindAllOpt extends CoreFindOpt {
/**
* Args used for findPage action
*/
-export interface FindPageOpt extends CoreFindOpt {
- pagination: OptPagination,
+export interface ActionFindPageArgs extends ActionCoreFindArgs {
+ /**
+ * Set of pagination settings that creates the url.
+ *
+ * This is stored and can be used to compare in new request to determine if we already have this page
+ */
+ pagination: PaginationArgs,
+ /**
+ * The single namespace to filter by (used in url path, not part of pagination params)
+ */
+ namespaced?: string,
hasManualRefresh?: boolean,
}
diff --git a/shell/types/store/pagination.types.ts b/shell/types/store/pagination.types.ts
new file mode 100644
index 0000000000..b9ffb12eeb
--- /dev/null
+++ b/shell/types/store/pagination.types.ts
@@ -0,0 +1,403 @@
+import { NAMESPACE_FILTER_NS_FULL_PREFIX, NAMESPACE_FILTER_P_FULL_PREFIX } from '@shell/utils/namespace-filter';
+
+// Pagination Typing
+// These structures are designed to offer both convenience and flexibility based on a common structure and are
+// converted to the url param format as per https://github.com/rancher/steve.
+//
+// Simple use cases such as filtering by a single param should be easy to use.
+// More complex filtering (and'ing and 'or'ing multiple fields) is also supported.
+//
+// The top level object `PaginationArgs` contains all properties that will be converted to url params
+//
+// The two important / complex params are currently
+// - `filter` https://github.com/rancher/steve?tab=readme-ov-file#filter
+// - represented by `PaginationParamFilter extends PaginationParam`
+// - Examples
+// - filter=metadata.name=123
+// - filter=metadata.name=123,metadata.name=456 (name is 123 OR 456)
+// - filter=metadata.name=123&filter=metadata.namespace=abc (name 123 AND namespace abc)
+// - `projectsornamespaces` https://github.com/rancher/steve?tab=readme-ov-file#projectsornamespaces
+// - represented by `PaginationParamProjectOrNamespace extends PaginationParam`
+// - Examples
+// - projectsornamespaces=123
+// - projectsornamespaces=123,456 (projects or namespaces that have id 123 OR 456)
+//
+//
+// Some of the types below are defined using classes instead of TS types/interfaces
+// - Avoid making complex json objects by using clearer instance constructors
+// - Better documented
+// - Defaults (a lot of the time convenience > utility)
+// - Adds some kind of typing in pure js docs
+// - class ctor links to definition, instead of object just being a random json blob)
+// - helps VSCode jsdoc highlighting
+
+/**
+ * Sort the pagination result
+ *
+ * For more information regarding the API see https://github.com/rancher/steve?tab=readme-ov-file#sort
+ */
+export interface PaginationSort {
+ /**
+ * Name of field within the object to sort by
+ */
+ field: string,
+ asc: boolean
+}
+
+/**
+ * Filter the pagination result by these specific fields
+ *
+ * For example
+ *
+ * - metadata.name=test
+ * - metadata.namespace!=system
+ *
+ * For more information regarding the API see https://github.com/rancher/steve?tab=readme-ov-file#query-parameters
+ */
+export class PaginationFilterField {
+ /**
+ * Name of field within the object to filter by for example the x of x=y
+ *
+ * This can be optional for some (projectsornamespaces)
+ */
+ field?: string;
+ /**
+ * Value of field within the object to filter by for example the y of x=y
+ */
+ value: string;
+ /**
+ * Equality field within the object to filter by for example the `=` or `!=` of x=y
+ */
+ equals: boolean;
+
+ constructor(
+ { field, value, equals = true }:
+ { field?: string; value: string; equals?: boolean; }
+ ) {
+ this.field = field;
+ this.value = value;
+ this.equals = equals;
+ }
+}
+
+/**
+ * Represents filter like params, for example
+ *
+ * - `filter=abc!=xyz&def=123`
+ * - `projectsornamespace!=p-3456`
+ *
+ * ### Params
+ * #### Filter
+ * - For more information regarding the API see https://github.com/rancher/steve?tab=readme-ov-file#filter
+ *
+ * #### Projects Or Namespace
+ * - For more information regarding the API see https://github.com/rancher/steve?tab=readme-ov-file#projectsornamespaces
+ *
+ * ### Combining Params
+ * Params can be combined in two logical ways
+ *
+ * 1) AND
+ * - Used when you would like to filter by something like a=1 AND b=2 AND c=3
+ * - To do this multiple instances of `PaginationParam` are used in an array
+ * - Object Structure
+ * ```
+ * [
+ * PaginationParam,
+ * PaginationParam,
+ * PaginationParam
+ * ]
+ * ```
+ * - Results in url
+ * ```
+ * filter=a=1&filter=b=2&filter=c=3
+ * ```
+ * - Examples
+ * - `filter=metadata.namespace=abc&filter=metadata.name=123,property=123`
+ * 2) OR
+ * - Used when you would like to filter by something like a=1 OR b=2 OR c=3
+ * - To do this multiple fields within a single PaginationParam is used
+ * - Object Structure
+ * ```
+ * [
+ * PaginationParam {
+ * PaginationFilterField,
+ * PaginationFilterField,
+ * PaginationFilterField
+ * }
+ * ]
+ * ```
+ * - Results in url
+ * ```
+ * filter=a=1,b=2,c=3
+ * ```
+ *
+ * - For example `filter=a=1,b=2,c=3`
+ *
+ *
+ * This structure should give enough flexibility to cover all uses.
+ *
+ *
+ */
+export abstract class PaginationParam {
+ /**
+ * Query Param. For example `filter` or `projectsornamespaces`
+ */
+ param: string;
+ /**
+ * should fields equal param.
+ *
+ * For example projectsornamexspaces=x or projectsornamexspaces!=x
+ */
+ equals: boolean;
+ /**
+ * Fields to filter by
+ *
+ * For example metadata.namespace=abc OR metadata.namespace=xyz
+ */
+ fields: PaginationFilterField[];
+
+ constructor(
+ { param, equals = true, fields = [] }:
+ {
+ param: string;
+ /**
+ * should param equal fields
+ *
+ * For definition see {@link PaginationParam} `equals`
+ */
+ equals?: boolean;
+ /**
+ * Collection of fields to filter by
+ *
+ * For definition see {@link PaginationParam} `fields`
+ */
+ fields?: PaginationFilterField[];
+ }) {
+ this.param = param;
+ this.equals = equals;
+ this.fields = fields;
+ }
+}
+
+/**
+ * This is a convenience class for the `filter` param which works some magic, adds defaults and converts to the required PaginationParam format
+ *
+ * See description for {@link PaginationParam} for how multiple of these can be combined together to AND or OR together
+ *
+ * For more information regarding the API see https://github.com/rancher/steve?tab=readme-ov-file#filter
+ */
+export class PaginationParamFilter extends PaginationParam {
+ constructor(
+ { equals = true, fields = [] }:
+ {
+ /**
+ * should param equal fields
+ *
+ * For definition see {@link PaginationParam} `equals`
+ */
+ equals?: boolean;
+ /**
+ * Collection of fields to filter by.
+ *
+ * Fields are ORd together
+ *
+ * For definition see {@link PaginationParam} `fields`
+ */
+ fields?: PaginationFilterField[];
+ }
+ ) {
+ super({
+ param: 'filter',
+ equals,
+ fields
+ });
+ }
+
+ /**
+ * Convenience method when you just want an instance of {@link PaginationParamFilter} with a simple `filter=x=y` param
+ */
+ static createSingleField(field: { field?: string; value: string; equals?: boolean; }): PaginationParam {
+ return new PaginationParamFilter({ fields: [new PaginationFilterField(field)] });
+ }
+
+ /**
+ * Convenience method when you just want an instance of {@link PaginationParamFilter} with a simple `filter=a=1,b=2,c=3` PaginationParam
+ *
+ * These will be OR'd together
+ */
+ static createMultipleFields(fields: PaginationFilterField[]): PaginationParam {
+ return new PaginationParamFilter({ fields });
+ }
+}
+
+/**
+ * This is a convenience class for the `projectsornamespaces` param which works some magic, adds defaults and converts to the required PaginationParam format
+ *
+ * See description for {@link PaginationParam} for how multiple of these can be combined together to AND or OR together
+ *
+ * For more information regarding the API see https://github.com/rancher/steve?tab=readme-ov-file#projectsornamespaces
+ */
+export class PaginationParamProjectOrNamespace extends PaginationParam {
+ constructor(
+ { equals = true, projectOrNamespace = [] }:
+ {
+ /**
+ * should param equal fields
+ * For definition see {@link PaginationParam} `equals`
+ */
+ equals?: boolean;
+ /**
+ * Collection of projects / namespace id's to filter by
+ *
+ * These are OR'd together
+ *
+ * For clarification on definition see {@link PaginationFilterField}
+ */
+ projectOrNamespace?: string[];
+ }
+ ) {
+ const safeFields = projectOrNamespace.map((f) => {
+ return new PaginationFilterField({
+ value: f
+ .replace(NAMESPACE_FILTER_NS_FULL_PREFIX, '')
+ .replace(NAMESPACE_FILTER_P_FULL_PREFIX, '')
+ });
+ });
+
+ super({
+ param: 'projectsornamespaces',
+ equals,
+ fields: safeFields
+ });
+ }
+}
+
+/**
+ * Pagination settings sent to actions and persisted to store
+ */
+export class PaginationArgs {
+ /**
+ * Page number to fetch
+ */
+ page: number;
+ /**
+ * Number of results in the page
+ */
+ pageSize?: number;
+ /**
+ * Sort the results
+ *
+ * For more info see {@link PaginationSort}
+ */
+ sort: PaginationSort[];
+ /**
+ * A collection of `filter` params
+ *
+ * For more info see {@link PaginationParamFilter}
+ */
+ filters: PaginationParamFilter[];
+ /**
+ * A collection of `projectsornamespace` params
+ *
+ * For more info see {@link PaginationParamProjectOrNamespace}
+ */
+ projectsOrNamespaces: PaginationParamProjectOrNamespace[];
+
+ /**
+ * Creates an instance of PaginationArgs.
+ *
+ * Contains defaults to avoid creating complex json objects all the time
+ */
+ constructor({
+ page = 1,
+ pageSize = 10,
+ sort = [],
+ filters = [],
+ projectsOrNamespaces = [],
+ }:
+ // This would be neater as just Partial but we lose all jsdoc
+ {
+ /**
+ * For definition see {@link PaginationArgs} `page`
+ */
+ page?: number,
+ /**
+ * For definition see {@link PaginationArgs} `pageSize`
+ */
+ pageSize?: number,
+ /**
+ * 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[],
+ }) {
+ this.page = page;
+ this.pageSize = pageSize;
+ this.sort = sort;
+ if (filters) {
+ this.filters = Array.isArray(filters) ? filters : [filters];
+ } else {
+ this.filters = [];
+ }
+ if (projectsOrNamespaces) {
+ this.projectsOrNamespaces = Array.isArray(projectsOrNamespaces) ? projectsOrNamespaces : [projectsOrNamespaces];
+ } else {
+ this.projectsOrNamespaces = [];
+ }
+ }
+}
+
+/**
+ * Overall result of a pagination request.
+ *
+ * Does not contain actual resources but overall stats (count, pages, etc)
+ */
+export interface StorePaginationResult {
+ count: number,
+ pages: number,
+ /**
+ * The last time the resource was updated. Used to assist list watching for changes
+ */
+ timestamp: number,
+}
+
+export interface StorePaginationRequest {
+ /**
+ * The single namespace to filter results by (as part of url path, not pagination params)
+ */
+ namespace?: string,
+ /**
+ * The set of pagination args used to create the request
+ */
+ pagination: PaginationArgs
+}
+
+/**
+ * Pagination settings
+ * - what was requested
+ * - what was received (minus actual resources)
+ *
+ * Object is persisted to store
+ */
+export interface StorePagination {
+ /**
+ * Collection of args that is used to make the request
+ */
+ request: StorePaginationRequest,
+
+ /**
+ * Information in the response outside of the actual resources returned
+ */
+ result: StorePaginationResult
+}
diff --git a/shell/types/store/type-map.d.ts b/shell/types/store/type-map.ts
similarity index 50%
rename from shell/types/store/type-map.d.ts
rename to shell/types/store/type-map.ts
index d866f2adcf..58859c3243 100644
--- a/shell/types/store/type-map.d.ts
+++ b/shell/types/store/type-map.ts
@@ -13,3 +13,18 @@ export interface TableColumn {
tooltip?: string,
search?: string | boolean,
}
+
+export const COLUMN_BREAKPOINTS = {
+ /**
+ * Only show column if at tablet width or wider
+ */
+ TABLET: 'tablet',
+ /**
+ * Only show column if at laptop width or wider
+ */
+ LAPTOP: 'laptop',
+ /**
+ * Only show column if at desktop width or wider
+ */
+ DESKTOP: 'desktop'
+};
diff --git a/shell/utils/array.ts b/shell/utils/array.ts
index 18fad61135..faea3ff5f1 100644
--- a/shell/utils/array.ts
+++ b/shell/utils/array.ts
@@ -180,7 +180,7 @@ export function sameContents(aryA: T[], aryB: T[]): boolean {
return xor(aryA, aryB).length === 0;
}
-export function sameArrayObjects(aryA: T[], aryB: T[]): boolean {
+export function sameArrayObjects(aryA: T[], aryB: T[], positionAgnostic = false): boolean {
if (!aryA && !aryB) {
// catch calls from js (where props aren't type checked)
return false;
@@ -190,9 +190,29 @@ export function sameArrayObjects(aryA: T[], aryB: T[]): boolean {
return false;
}
- for (let i = 0; i < aryA.length; i++) {
- if (!isEqual(aryA[i], aryB[i])) {
- return false;
+ if (positionAgnostic) {
+ const consumedB: { [pos: number]: boolean } = {};
+
+ aryB.forEach((_, index) => {
+ consumedB[index] = false;
+ });
+
+ for (let i = 0; i < aryA.length; i++) {
+ const a = aryA[i];
+
+ const validA = aryB.findIndex((arB, index) => isEqual(arB, a) && !consumedB[index] );
+
+ if (validA >= 0) {
+ consumedB[validA] = true;
+ } else {
+ return false;
+ }
+ }
+ } else {
+ for (let i = 0; i < aryA.length; i++) {
+ if (!isEqual(aryA[i], aryB[i])) {
+ return false;
+ }
}
}
diff --git a/shell/utils/pagination-utils.ts b/shell/utils/pagination-utils.ts
index 0d1a62e3c3..74f693a35d 100644
--- a/shell/utils/pagination-utils.ts
+++ b/shell/utils/pagination-utils.ts
@@ -9,7 +9,7 @@ import {
NAMESPACE_FILTER_NS_FULL_PREFIX,
NAMESPACE_FILTER_P_FULL_PREFIX,
} from '@shell/utils/namespace-filter';
-import { OptPagination, OptPaginationFilter, OptPaginationSort } from '@shell/types/store/dashboard-store.types';
+import { PaginationArgs, PaginationParam, PaginationSort } from '@shell/types/store/pagination.types';
import { sameArrayObjects } from '@shell/utils/array';
import { isEqual } from '@shell/utils/object';
@@ -21,7 +21,7 @@ const settings: PaginationSettings = {
resources: {
enableAll: false,
enableSome: {
- enabled: ['configmap', 'secret', 'pod'],
+ enabled: ['configmap', 'secret', 'pod', 'node'],
generic: true,
}
}
@@ -110,15 +110,15 @@ class PaginationUtils {
return this.validNsProjectFilters.includes(nsProjectFilter);
}
- paginationFilterEqual(a: OptPaginationFilter, b: OptPaginationFilter): boolean {
+ paginationFilterEqual(a: PaginationParam, b: PaginationParam): boolean {
if (a.param !== b.param || a.equals !== b.equals) {
return false;
}
- return isEqual(a.fields, b.fields);
+ return sameArrayObjects(a.fields, b.fields, true);
}
- paginationFiltersEqual(a: OptPaginationFilter[], b: OptPaginationFilter[]): boolean {
+ paginationFiltersEqual(a: PaginationParam[], b: PaginationParam[]): boolean {
if (!!a && a?.length !== b?.length) {
return false;
}
@@ -132,18 +132,18 @@ class PaginationUtils {
return true;
}
- paginationEqual(a?: OptPagination, b?: OptPagination): boolean {
+ paginationEqual(a?: PaginationArgs, b?: PaginationArgs): boolean {
const {
- filter: aFilter = [], sort: aSort = [], projectsOrNamespaces: aPN = [], ...aPrimitiveTypes
+ filters: aFilter = [], sort: aSort = [], projectsOrNamespaces: aPN = [], ...aPrimitiveTypes
} = a || {};
const {
- filter: bFilter = [], sort: bSort = [], projectsOrNamespaces: bPN = [], ...bPrimitiveTypes
+ filters: bFilter = [], sort: bSort = [], projectsOrNamespaces: bPN = [], ...bPrimitiveTypes
} = b || {};
return isEqual(aPrimitiveTypes, bPrimitiveTypes) &&
this.paginationFiltersEqual(aFilter, bFilter) &&
this.paginationFiltersEqual(aPN, bPN) &&
- sameArrayObjects(aSort, bSort);
+ sameArrayObjects(aSort, bSort, true);
}
}