mirror of https://github.com/rancher/dashboard.git
Merge pull request #10761 from richard-cox/pagination-node
Update Node list to support server-side pagination
This commit is contained in:
commit
01219b751e
|
|
@ -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).
|
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?
|
# What is it?
|
||||||
|
|
||||||
Rancher Dashboard provides a sophisticated UI for managing Kubernetes clusters and Workloads.
|
Rancher Dashboard provides a sophisticated UI for managing Kubernetes clusters and Workloads.
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
import PagePo from '@/cypress/e2e/po/pages/page.po';
|
import PagePo from '@/cypress/e2e/po/pages/page.po';
|
||||||
import BaseResourceList from '@/cypress/e2e/po/lists/base-resource-list.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 {
|
export class ConfigMapPagePo extends PagePo {
|
||||||
private static createPath(clusterId: string) {
|
private static createPath(clusterId: string) {
|
||||||
|
|
@ -10,19 +12,33 @@ export class ConfigMapPagePo extends PagePo {
|
||||||
return super.goTo(ConfigMapPagePo.createPath(clusterId));
|
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') {
|
constructor(clusterId = 'local') {
|
||||||
super(ConfigMapPagePo.createPath(clusterId));
|
super(ConfigMapPagePo.createPath(clusterId));
|
||||||
}
|
}
|
||||||
|
|
||||||
clickCreate() {
|
list() {
|
||||||
const baseResourceList = new BaseResourceList(this.self());
|
return new BaseResourceList(this.self());
|
||||||
|
}
|
||||||
|
|
||||||
return baseResourceList.masthead().actions().eq(0).click();
|
clickCreate() {
|
||||||
|
return this.list().masthead().create();
|
||||||
}
|
}
|
||||||
|
|
||||||
listElementWithName(name:string) {
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,17 @@
|
||||||
import { APIServicesPagePo } from '@/cypress/e2e/po/pages/explorer/api-services.po';
|
import { APIServicesPagePo } from '@/cypress/e2e/po/pages/explorer/api-services.po';
|
||||||
|
|
||||||
|
const apiServicesPage = new APIServicesPagePo('local');
|
||||||
|
|
||||||
describe('Cluster Explorer', { tags: ['@explorer', '@adminUser'] }, () => {
|
describe('Cluster Explorer', { tags: ['@explorer', '@adminUser'] }, () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.login();
|
cy.login();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('API: APIServices', () => {
|
describe('API: APIServices', () => {
|
||||||
let apiServicesPage: APIServicesPagePo;
|
it('Should be able to use shift+j to select rows and the count of selected is correct', () => {
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
apiServicesPage = new APIServicesPagePo('local');
|
|
||||||
apiServicesPage.goTo();
|
apiServicesPage.goTo();
|
||||||
apiServicesPage.waitForRequests();
|
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');
|
apiServicesPage.title().should('contain', 'APIServices');
|
||||||
|
|
||||||
const sortableTable = apiServicesPage.sortableTable();
|
const sortableTable = apiServicesPage.sortableTable();
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ describe('Nodes list', { tags: ['@explorer', '@adminUser'], testIsolation: 'off'
|
||||||
|
|
||||||
after(() => {
|
after(() => {
|
||||||
// Ensure we delete the dummy node
|
// 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', () => {
|
it('should show the nodes list page', () => {
|
||||||
|
|
@ -48,7 +48,7 @@ describe('Nodes list', { tags: ['@explorer', '@adminUser'], testIsolation: 'off'
|
||||||
// Check the node names
|
// Check the node names
|
||||||
nodeList.sortableTable().rowNames().should((names: any) => {
|
nodeList.sortableTable().rowNames().should((names: any) => {
|
||||||
expect(names).to.have.length(count);
|
expect(names).to.have.length(count);
|
||||||
expect(names).to.contain('bigip1');
|
expect(names).to.contain(dummyNode.metadata.name);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
import { ConfigMapPagePo } from '@/cypress/e2e/po/pages/explorer/config-map.po';
|
import { ConfigMapPagePo } from '@/cypress/e2e/po/pages/explorer/config-map.po';
|
||||||
import ConfigMapPo from '@/cypress/e2e/po/components/storage/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(() => {
|
beforeEach(() => {
|
||||||
cy.login();
|
cy.login();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('has the correct title', () => {
|
it('has the correct title', () => {
|
||||||
const configMapPage = new ConfigMapPagePo('local');
|
|
||||||
|
|
||||||
configMapPage.goTo();
|
configMapPage.goTo();
|
||||||
|
|
||||||
cy.title().should('eq', 'Rancher - local - ConfigMaps');
|
cy.title().should('eq', 'Rancher - local - ConfigMaps');
|
||||||
|
|
@ -27,9 +27,7 @@ skipGeometric=true`;
|
||||||
|
|
||||||
// Visit the main menu and select the 'local' cluster
|
// Visit the main menu and select the 'local' cluster
|
||||||
// Navigate to Service Discovery => ConfigMaps
|
// Navigate to Service Discovery => ConfigMaps
|
||||||
const configMapPage = new ConfigMapPagePo('local');
|
ConfigMapPagePo.navTo();
|
||||||
|
|
||||||
configMapPage.goTo();
|
|
||||||
|
|
||||||
// Click on Create
|
// Click on Create
|
||||||
configMapPage.clickCreate();
|
configMapPage.clickCreate();
|
||||||
|
|
@ -40,7 +38,9 @@ skipGeometric=true`;
|
||||||
// Enter ConfigMap description
|
// Enter ConfigMap description
|
||||||
const configMapPo = new ConfigMapPo();
|
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.keyInput().set('managerApiConfiguration.properties');
|
||||||
configMapPo.valueInput().set(expectedValue);
|
configMapPo.valueInput().set(expectedValue);
|
||||||
configMapPo.descriptionInput().set('Custom Config Map Description');
|
configMapPo.descriptionInput().set('Custom Config Map Description');
|
||||||
|
|
@ -49,14 +49,12 @@ skipGeometric=true`;
|
||||||
configMapPo.saveCreateForm().click();
|
configMapPo.saveCreateForm().click();
|
||||||
|
|
||||||
// Check if the ConfigMap is created successfully
|
// 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
|
// Navigate back to the edit page
|
||||||
configMapPage.listElementWithName('custom-config-map')
|
configMapPage.list().actionMenu(configMapName).getMenuItem('Edit Config').click();
|
||||||
.find(`button[data-testid="sortable-table-0-action-button"]`)
|
|
||||||
.click()
|
|
||||||
.get(`li[data-testid="action-menu-0-item"]`)
|
|
||||||
.click();
|
|
||||||
|
|
||||||
// Assert the current value yaml dumps will append a newline at the end
|
// Assert the current value yaml dumps will append a newline at the end
|
||||||
configMapPo.valueInput().value().should('eq', `${ expectedValue }\n`);
|
configMapPo.valueInput().value().should('eq', `${ expectedValue }\n`);
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import { RANCHER_PAGE_EXCEPTIONS, catchTargetPageException } from '~/cypress/sup
|
||||||
const homePage = new HomePagePo();
|
const homePage = new HomePagePo();
|
||||||
const homeClusterList = homePage.list();
|
const homeClusterList = homePage.list();
|
||||||
const provClusterList = new ClusterManagerListPagePo('local');
|
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);
|
const rowDetails = (text) => text.split('\n').map((r) => r.trim()).filter((f) => f);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -172,7 +172,7 @@ export default {
|
||||||
* This covers case 1
|
* This covers case 1
|
||||||
*/
|
*/
|
||||||
pagination(neu, old) {
|
pagination(neu, old) {
|
||||||
if (neu && !this.componentWillFetch && this.paginationEqual(neu, old)) {
|
if (neu && !this.componentWillFetch && !this.paginationEqual(neu, old)) {
|
||||||
this.$fetchType(this.resource);
|
this.$fetchType(this.resource);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -29,21 +29,6 @@ import { getParent } from '@shell/utils/dom';
|
||||||
// NOTE: This is populated by a plugin (formatters.js) to avoid issues with plugins
|
// NOTE: This is populated by a plugin (formatters.js) to avoid issues with plugins
|
||||||
export const FORMATTERS = {};
|
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:
|
// @TODO:
|
||||||
// Fixed header/scrolling
|
// Fixed header/scrolling
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ import {
|
||||||
STORAGE_CLASS_PROVISIONER, PERSISTENT_VOLUME_SOURCE,
|
STORAGE_CLASS_PROVISIONER, PERSISTENT_VOLUME_SOURCE,
|
||||||
HPA_REFERENCE, MIN_REPLICA, MAX_REPLICA, CURRENT_REPLICA,
|
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,
|
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';
|
} from '@shell/config/table-headers';
|
||||||
|
|
||||||
import { DSL } from '@shell/store/type-map';
|
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
|
STEVE_AGE_COL, STEVE_LIST_GROUPS, STEVE_NAMESPACE_COL, STEVE_NAME_COL, STEVE_STATE_COL
|
||||||
} from '@shell/config/pagination-table-headers';
|
} from '@shell/config/pagination-table-headers';
|
||||||
|
|
||||||
|
import { COLUMN_BREAKPOINTS } from '@shell/types/store/type-map';
|
||||||
|
|
||||||
export const NAME = 'explorer';
|
export const NAME = 'explorer';
|
||||||
|
|
||||||
export function init(store) {
|
export function init(store) {
|
||||||
|
|
@ -233,8 +235,11 @@ export function init(store) {
|
||||||
value: 'metadata.fields.1',
|
value: 'metadata.fields.1',
|
||||||
sort: 'metadata.fields.1',
|
sort: 'metadata.fields.1',
|
||||||
search: 'metadata.fields.1',
|
search: 'metadata.fields.1',
|
||||||
|
}, {
|
||||||
|
...SECRET_DATA,
|
||||||
|
sort: false,
|
||||||
|
search: false,
|
||||||
},
|
},
|
||||||
SECRET_DATA,
|
|
||||||
STEVE_AGE_COL
|
STEVE_AGE_COL
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
@ -267,6 +272,66 @@ export function init(store) {
|
||||||
STEVE_AGE_COL
|
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, {
|
headers(MANAGEMENT.PSA, [STATE, NAME_COL, {
|
||||||
...DESCRIPTION,
|
...DESCRIPTION,
|
||||||
width: undefined
|
width: undefined
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { CATTLE_PUBLIC_ENDPOINTS } from '@shell/config/labels-annotations';
|
import { CATTLE_PUBLIC_ENDPOINTS } from '@shell/config/labels-annotations';
|
||||||
import { NODE as NODE_TYPE } from '@shell/config/types';
|
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.
|
// Note: 'id' is always the last sort, so you don't have to specify it here.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,28 @@
|
||||||
<script>
|
<script lang="ts">
|
||||||
import ResourceTable from '@shell/components/ResourceTable';
|
import ResourceTable from '@shell/components/ResourceTable.vue';
|
||||||
import Tag from '@shell/components/Tag';
|
import Tag from '@shell/components/Tag.vue';
|
||||||
import { Banner } from '@components/Banner';
|
import { Banner } from '@components/Banner';
|
||||||
import {
|
import { PODS } from '@shell/config/table-headers';
|
||||||
STATE, NAME, ROLES, VERSION, INTERNAL_EXTERNAL_IP, CPU, RAM, PODS, AGE, KUBE_NODE_OS
|
|
||||||
} from '@shell/config/table-headers';
|
|
||||||
import metricPoller from '@shell/mixins/metric-poller';
|
import metricPoller from '@shell/mixins/metric-poller';
|
||||||
|
|
||||||
|
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 {
|
import {
|
||||||
CAPI,
|
CAPI,
|
||||||
MANAGEMENT, METRIC, NODE, NORMAN, POD
|
MANAGEMENT, METRIC, NODE, NORMAN, POD
|
||||||
} from '@shell/config/types';
|
} from '@shell/config/types';
|
||||||
import { allHash } from '@shell/utils/promise';
|
import { allHash } from '@shell/utils/promise';
|
||||||
import { GROUP_RESOURCES, mapPref } from '@shell/store/prefs';
|
import { GROUP_RESOURCES, mapPref } from '@shell/store/prefs';
|
||||||
import { COLUMN_BREAKPOINTS } from '@shell/components/SortableTable/index.vue';
|
import { COLUMN_BREAKPOINTS } from '@shell/types/store/type-map';
|
||||||
|
|
||||||
import ResourceFetch from '@shell/mixins/resource-fetch';
|
import ResourceFetch from '@shell/mixins/resource-fetch';
|
||||||
export default {
|
import { mapGetters } from 'vuex';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
name: 'ListNode',
|
name: 'ListNode',
|
||||||
components: {
|
components: {
|
||||||
ResourceTable,
|
ResourceTable,
|
||||||
|
|
@ -36,94 +43,92 @@ export default {
|
||||||
useQueryParamsForSimpleFiltering: {
|
useQueryParamsForSimpleFiltering: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false
|
default: false
|
||||||
|
},
|
||||||
|
|
||||||
|
listComponent: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async fetch() {
|
async fetch() {
|
||||||
this.$initializeFetchData(this.resource);
|
this.$initializeFetchData(this.resource);
|
||||||
|
|
||||||
const hash = { kubeNodes: this.$fetchType(this.resource) };
|
await allHash({
|
||||||
|
kubeNodes: this.$fetchType(this.resource),
|
||||||
this.canViewPods = this.$store.getters[`cluster/schemaFor`](POD);
|
...this.fetchSecondaryResources(),
|
||||||
|
});
|
||||||
if (this.$store.getters[`management/schemaFor`](MANAGEMENT.NODE)) {
|
|
||||||
// Required for Drain/Cordon action
|
|
||||||
hash.normanNodes = this.$fetchType(NORMAN.NODE, [], 'rancher');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.$store.getters[`rancher/schemaFor`](NORMAN.NODE)) {
|
|
||||||
hash.mgmtNodes = this.$fetchType(MANAGEMENT.NODE, [], 'management');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.$store.getters[`management/schemaFor`](CAPI.MACHINE)) {
|
|
||||||
// Required for ssh / download key actions
|
|
||||||
hash.machines = this.$fetchType(CAPI.MACHINE, [], 'management');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.canViewPods) {
|
|
||||||
// Used for running pods metrics - we don't need to block on this to show the list of nodes
|
|
||||||
this.$fetchType(POD);
|
|
||||||
}
|
|
||||||
|
|
||||||
await allHash(hash);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return { canViewPods: false };
|
return {
|
||||||
|
// Pods required for `Pods` column's running pods metrics
|
||||||
|
// podConsumedUsage = podConsumed / podConsumedUsage. podConsumed --> pods. allPods.filter((pod) => pod.spec.nodeName === this.name)
|
||||||
|
canViewPods: !!this.$store.getters[`cluster/schemaFor`](POD),
|
||||||
|
// Norman node required for Drain/Cordon/Uncordon action
|
||||||
|
canViewNormanNodes: !!this.$store.getters[`rancher/schemaFor`](NORMAN.NODE),
|
||||||
|
// Mgmt Node required to find Norman node
|
||||||
|
canViewMgmtNodes: !!this.$store.getters[`management/schemaFor`](MANAGEMENT.NODE),
|
||||||
|
// Required for ssh / download key actions
|
||||||
|
canViewMachines: !!this.$store.getters[`management/schemaFor`](CAPI.MACHINE),
|
||||||
|
// Required for CPU and RAM columns
|
||||||
|
canViewNodeMetrics: !!this.$store.getters['cluster/schemaFor'](METRIC.NODE),
|
||||||
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
// Stop watching pods, nodes and node metrics
|
// Stop watching pods, nodes and node metrics
|
||||||
this.$store.dispatch('cluster/forgetType', POD);
|
if (this.canViewPods) {
|
||||||
|
this.$store.dispatch('cluster/forgetType', POD);
|
||||||
|
}
|
||||||
|
|
||||||
this.$store.dispatch('cluster/forgetType', NODE);
|
this.$store.dispatch('cluster/forgetType', NODE);
|
||||||
this.$store.dispatch('cluster/forgetType', METRIC.NODE);
|
this.$store.dispatch('cluster/forgetType', METRIC.NODE);
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
|
...mapGetters(['currentCluster']),
|
||||||
hasWindowsNodes() {
|
hasWindowsNodes() {
|
||||||
return (this.rows || []).some((node) => node.status.nodeInfo.operatingSystem === 'windows');
|
// Note if server side pagination is used this is only applicable to the current page
|
||||||
|
return (this.rows || []).some((node: any) => node.status.nodeInfo.operatingSystem === 'windows');
|
||||||
},
|
},
|
||||||
|
|
||||||
tableGroup: mapPref(GROUP_RESOURCES),
|
tableGroup: mapPref(GROUP_RESOURCES),
|
||||||
|
|
||||||
parsedRows() {
|
|
||||||
this.rows.forEach((row) => {
|
|
||||||
row.displayTaintsAndLabels = (row.spec.taints && row.spec.taints.length) || !!row.customLabelCount;
|
|
||||||
});
|
|
||||||
|
|
||||||
return this.rows;
|
|
||||||
},
|
|
||||||
|
|
||||||
headers() {
|
headers() {
|
||||||
const headers = [
|
// This is all about adding the pods column... if the user can see pods
|
||||||
STATE,
|
|
||||||
NAME,
|
if (this.canPaginate) {
|
||||||
ROLES,
|
const paginationHeaders = [...this.$store.getters['type-map/headersFor'](this.schema, true)];
|
||||||
VERSION,
|
|
||||||
INTERNAL_EXTERNAL_IP,
|
if (paginationHeaders) {
|
||||||
{
|
if (this.canViewPods) {
|
||||||
...KUBE_NODE_OS,
|
paginationHeaders.splice(paginationHeaders.length - 1, 0, {
|
||||||
breakpoint: COLUMN_BREAKPOINTS.LAPTOP,
|
...PODS,
|
||||||
getValue: (row) => row.status?.nodeInfo?.operatingSystem
|
breakpoint: COLUMN_BREAKPOINTS.DESKTOP,
|
||||||
},
|
sort: false,
|
||||||
{
|
search: false,
|
||||||
...CPU,
|
getValue: (row: any) => row.podConsumedUsage
|
||||||
breakpoint: COLUMN_BREAKPOINTS.LAPTOP,
|
});
|
||||||
getValue: (row) => row.cpuUsagePercentage
|
}
|
||||||
}, {
|
|
||||||
...RAM,
|
return paginationHeaders;
|
||||||
breakpoint: COLUMN_BREAKPOINTS.LAPTOP,
|
} else {
|
||||||
getValue: (row) => row.ramUsagePercentage
|
console.warn('Nodes list expects pagination headers but none found'); // eslint-disable-line no-console
|
||||||
}];
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const headers = [...this.$store.getters['type-map/headersFor'](this.schema, false)];
|
||||||
|
|
||||||
if (this.canViewPods) {
|
if (this.canViewPods) {
|
||||||
headers.push({
|
headers.splice(headers.length - 1, 0, {
|
||||||
...PODS,
|
...PODS,
|
||||||
breakpoint: COLUMN_BREAKPOINTS.DESKTOP,
|
breakpoint: COLUMN_BREAKPOINTS.DESKTOP,
|
||||||
getValue: (row) => row.podConsumedUsage
|
getValue: (row: any) => row.podConsumedUsage
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
headers.push(AGE);
|
|
||||||
|
|
||||||
return headers;
|
return headers;
|
||||||
},
|
},
|
||||||
|
|
@ -131,24 +136,158 @@ export default {
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
async loadMetrics() {
|
async loadMetrics() {
|
||||||
const schema = this.$store.getters['cluster/schemaFor'](METRIC.NODE);
|
if (!this.canViewNodeMetrics) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (schema) {
|
if (this.canPaginate) {
|
||||||
|
if (!this.rows.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const opt: ActionFindPageArgs = {
|
||||||
|
force: true,
|
||||||
|
pagination: new PaginationArgs({
|
||||||
|
page: -1,
|
||||||
|
filters: new PaginationParamFilter({
|
||||||
|
fields: this.rows.map((r: any) => new PaginationFilterField({
|
||||||
|
field: 'metadata.name',
|
||||||
|
value: r.id
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
await this.$store.dispatch('cluster/findPage', {
|
||||||
|
type: METRIC.NODE,
|
||||||
|
opt
|
||||||
|
});
|
||||||
|
} else {
|
||||||
await this.$store.dispatch('cluster/findAll', {
|
await this.$store.dispatch('cluster/findAll', {
|
||||||
type: METRIC.NODE,
|
type: METRIC.NODE,
|
||||||
opt: { force: true }
|
opt: { force: true }
|
||||||
});
|
});
|
||||||
|
|
||||||
this.$forceUpdate();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.$forceUpdate();
|
||||||
},
|
},
|
||||||
|
|
||||||
toggleLabels(row) {
|
toggleLabels(row: any) {
|
||||||
this.$set(row, 'displayLabels', !row.displayLabels);
|
this.$set(row, 'displayLabels', !row.displayLabels);
|
||||||
},
|
},
|
||||||
}
|
|
||||||
|
|
||||||
};
|
fetchSecondaryResources(): { [key: string]: Promise<any>} {
|
||||||
|
if (this.canPaginate) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const hash: { [key: string]: Promise<any>} = {};
|
||||||
|
|
||||||
|
if (this.canViewMgmtNodes) {
|
||||||
|
hash.mgmtNodes = this.$fetchType(MANAGEMENT.NODE, [], 'management');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.canViewNormanNodes) {
|
||||||
|
hash.normanNodes = this.$fetchType(NORMAN.NODE, [], 'rancher');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.canViewMachines) {
|
||||||
|
hash.machines = this.$fetchType(CAPI.MACHINE, [], 'management');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.canViewPods) {
|
||||||
|
// No need to block on this
|
||||||
|
this.$fetchType(POD);
|
||||||
|
}
|
||||||
|
|
||||||
|
return hash;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Nodes columns need other resources in order to show data in some columns
|
||||||
|
*
|
||||||
|
* In the paginated world we want to resrict the fetch of those resources to only the one's we need
|
||||||
|
*
|
||||||
|
* So when we have a page.... use those entries as filters when fetching the other resources
|
||||||
|
*/
|
||||||
|
async fetchPageSecondaryResources(force = false) {
|
||||||
|
if (!this.rows?.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.canViewMgmtNodes && this.canViewNormanNodes) {
|
||||||
|
// We only fetch mgmt node to get norman node. We only fetch node to get node actions
|
||||||
|
// See https://github.com/rancher/dashboard/issues/10743
|
||||||
|
const opt: ActionFindPageArgs = {
|
||||||
|
force,
|
||||||
|
pagination: new PaginationArgs({
|
||||||
|
page: -1,
|
||||||
|
filters: PaginationParamFilter.createMultipleFields(this.rows.map((r: any) => new PaginationFilterField({
|
||||||
|
field: 'status.nodeName',
|
||||||
|
value: r.id
|
||||||
|
}))),
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
this.$store.dispatch(`management/findPage`, { type: MANAGEMENT.NODE, opt })
|
||||||
|
.then(() => {
|
||||||
|
this.$store.dispatch(`rancher/findAll`, { type: NORMAN.NODE, opt: { force } });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.canViewMachines) {
|
||||||
|
const namespace = this.currentCluster.provClusterId?.split('/')[0];
|
||||||
|
|
||||||
|
if (namespace) {
|
||||||
|
const opt: ActionFindPageArgs = {
|
||||||
|
force,
|
||||||
|
namespaced: namespace,
|
||||||
|
pagination: new PaginationArgs({
|
||||||
|
page: -1,
|
||||||
|
filters: PaginationParamFilter.createMultipleFields(
|
||||||
|
this.rows.reduce((res: PaginationFilterField[], r: any ) => {
|
||||||
|
const name = r.metadata?.annotations?.[CAPI_ANNOTATIONS.MACHINE_NAME];
|
||||||
|
|
||||||
|
if (name) {
|
||||||
|
res.push(new PaginationFilterField({
|
||||||
|
field: 'metadata.name',
|
||||||
|
value: name,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}, [])
|
||||||
|
)
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
this.$store.dispatch(`management/findPage`, { type: CAPI.MACHINE, opt });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.canViewPods) {
|
||||||
|
// 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,
|
||||||
|
filters: PaginationParamFilter.createMultipleFields(
|
||||||
|
this.rows.map((r: any) => new PaginationFilterField({
|
||||||
|
field: 'spec.nodeName',
|
||||||
|
value: r.id,
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
this.$store.dispatch(`cluster/findPage`, { type: POD, opt });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch metrics given the current page
|
||||||
|
this.loadMetrics();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
@ -162,12 +301,15 @@ export default {
|
||||||
v-bind="$attrs"
|
v-bind="$attrs"
|
||||||
:schema="schema"
|
:schema="schema"
|
||||||
:headers="headers"
|
:headers="headers"
|
||||||
:rows="parsedRows"
|
:rows="rows"
|
||||||
:sub-rows="true"
|
:sub-rows="true"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
:use-query-params-for-simple-filtering="useQueryParamsForSimpleFiltering"
|
:use-query-params-for-simple-filtering="useQueryParamsForSimpleFiltering"
|
||||||
:force-update-live-and-delayed="forceUpdateLiveAndDelayed"
|
:force-update-live-and-delayed="forceUpdateLiveAndDelayed"
|
||||||
data-testid="cluster-node-list"
|
data-testid="cluster-node-list"
|
||||||
|
:external-pagination-enabled="canPaginate"
|
||||||
|
:external-pagination-result="paginationResult"
|
||||||
|
@pagination-changed="paginationChanged"
|
||||||
v-on="$listeners"
|
v-on="$listeners"
|
||||||
>
|
>
|
||||||
<template #sub-row="{fullColspan, row, onRowMouseEnter, onRowMouseLeave}">
|
<template #sub-row="{fullColspan, row, onRowMouseEnter, onRowMouseLeave}">
|
||||||
|
|
|
||||||
|
|
@ -35,15 +35,19 @@ function beforeMount() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function $fetch() {
|
function $fetch(cached = true) {
|
||||||
if (!this._fetchPromise) {
|
if (cached) {
|
||||||
this._fetchPromise = $_fetch.call(this)
|
if (!this._fetchPromise) {
|
||||||
.then(() => {
|
this._fetchPromise = $_fetch.call(this)
|
||||||
delete this._fetchPromise;
|
.then(() => {
|
||||||
});
|
delete this._fetchPromise;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return this._fetchPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
return this._fetchPromise;
|
return $_fetch.call(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function $_fetch() { // eslint-disable-line camelcase
|
async function $_fetch() { // eslint-disable-line camelcase
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
import { NAMESPACE_FILTER_NAMESPACED_YES, NAMESPACE_FILTER_NAMESPACED_NO, NAMESPACE_FILTER_ALL } from '@shell/utils/namespace-filter';
|
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 { mapGetters } from 'vuex';
|
||||||
import { ResourceListComponentName } from '../components/ResourceList/resource-list.config';
|
import { ResourceListComponentName } from '../components/ResourceList/resource-list.config';
|
||||||
import paginationUtils from '@shell/utils/pagination-utils';
|
import paginationUtils from '@shell/utils/pagination-utils';
|
||||||
import debounce from 'lodash/debounce';
|
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 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
|
* 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: {
|
methods: {
|
||||||
|
/**
|
||||||
|
* @param {PaginationArgs} pagination
|
||||||
|
*/
|
||||||
setPagination(pagination) {
|
setPagination(pagination) {
|
||||||
this.pPagination = pagination;
|
if (pagination) {
|
||||||
|
this.pPagination = pagination;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
paginationChanged(event) {
|
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,
|
field,
|
||||||
value: event.filter.searchQuery,
|
value: event.filter.searchQuery,
|
||||||
})) : [];
|
})) : [];
|
||||||
|
|
||||||
this.debouncedSetPagination({
|
const pagination = new PaginationArgs({
|
||||||
...this.pPagination,
|
|
||||||
page: event.page,
|
page: event.page,
|
||||||
pageSize: event.perPage,
|
pageSize: event.perPage,
|
||||||
sort: event.sort?.map((field) => ({
|
sort: event.sort?.map((field) => ({
|
||||||
|
|
@ -53,11 +57,13 @@ export default {
|
||||||
asc: !event.descending
|
asc: !event.descending
|
||||||
})),
|
})),
|
||||||
projectsOrNamespaces: this.requestFilters.projectsOrNamespaces,
|
projectsOrNamespaces: this.requestFilters.projectsOrNamespaces,
|
||||||
filter: [
|
filters: [
|
||||||
new OptPaginationFilter({ fields: searchFilters }),
|
new PaginationParamFilter({ fields: searchFilters }),
|
||||||
...this.requestFilters.filters, // Apply the additional filters. these aren't from the user but from ns filtering
|
...this.requestFilters.filters, // Apply the additional filters. these aren't from the user but from ns filtering
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.debouncedSetPagination(pagination);
|
||||||
},
|
},
|
||||||
|
|
||||||
namespaceFilterChanged(neu) {
|
namespaceFilterChanged(neu) {
|
||||||
|
|
@ -81,26 +87,40 @@ export default {
|
||||||
this.requestFilters.projectsOrNamespaces = projectsOrNamespaces;
|
this.requestFilters.projectsOrNamespaces = projectsOrNamespaces;
|
||||||
|
|
||||||
// Kick off a change
|
// Kick off a change
|
||||||
this.debouncedSetPagination({ ...this.pPagination });
|
if (this.pPagination) {
|
||||||
|
this.debouncedSetPagination({ ...this.pPagination });
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {PaginationArgs} neu
|
||||||
|
* @param {PaginationArgs} old
|
||||||
|
*/
|
||||||
paginationEqual(neu, old) {
|
paginationEqual(neu, old) {
|
||||||
if (!neu.page) {
|
if (!neu.page) {
|
||||||
// Not valid, don't bother
|
// Not valid, count as not equal
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (paginationUtils.paginationEqual(neu, old)) {
|
if (paginationUtils.paginationEqual(neu, old)) {
|
||||||
// Same, nae bother
|
return true;
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
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?
|
* Does the user need to update the filter to supply valid options?
|
||||||
|
|
@ -120,14 +140,14 @@ export default {
|
||||||
* ResourceList imports resource-fetch --> this mixin
|
* ResourceList imports resource-fetch --> this mixin
|
||||||
* When there is no custom list this is fine (ResourceList with mixins --> ResourceTable)
|
* 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)
|
* 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() {
|
isResourceList() {
|
||||||
return !!this.hasListComponent;
|
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() {
|
pagination() {
|
||||||
if (this.isResourceList) {
|
if (this.isResourceList) {
|
||||||
|
|
@ -152,7 +172,7 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
paginationResult() {
|
paginationResult() {
|
||||||
if (this.isResourceList) {
|
if (this.isResourceList || !this.canPaginate) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -188,7 +208,7 @@ export default {
|
||||||
* result in an empty page
|
* result in an empty page
|
||||||
*/
|
*/
|
||||||
rows(neu) {
|
rows(neu) {
|
||||||
if (!this.pagination || this.isResourceList) {
|
if (!this.canPaginate || !this.pagination || this.isResourceList) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -203,6 +223,10 @@ export default {
|
||||||
namespaceFilters: {
|
namespaceFilters: {
|
||||||
immediate: true,
|
immediate: true,
|
||||||
async handler(neu, old) {
|
async handler(neu, old) {
|
||||||
|
if (!this.canPaginate || !this.schema?.attributes?.namespaced) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.isResourceList) {
|
if (this.isResourceList) {
|
||||||
return;
|
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) {
|
async pagination(neu, old) {
|
||||||
if (this.isResourceList) {
|
if (!this.canPaginate) {
|
||||||
return;
|
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
|
// ResourceList has two modes
|
||||||
// 1) ResourceList component handles API request to fetch resources
|
// 1) ResourceList component handles API request to fetch resources
|
||||||
// 2) Custom list component handles API request to fetch resources
|
// 2) Custom list component handles API request to fetch resources
|
||||||
//
|
//
|
||||||
// This covers case 2
|
// This covers case 2, so ignore case 1
|
||||||
if (neu && this.$options.name !== ResourceListComponentName && !!this.$fetch && this.paginationEqual(neu, old)) {
|
if (this.isResourceList) {
|
||||||
await this.$fetch();
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (neu && this.$options.name !== ResourceListComponentName && !!this.$fetch && !this.paginationEqual(neu, old)) {
|
||||||
|
await this.$fetch(false);
|
||||||
// Ensure any live/delayed columns get updated
|
// Ensure any live/delayed columns get updated
|
||||||
this.forceUpdateLiveAndDelayed = new Date().getTime();
|
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();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,7 @@ export default {
|
||||||
paginating: null,
|
paginating: null,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
// make sure this only runs once, for the initialized instance
|
// make sure this only runs once, for the initialized instance
|
||||||
if (this.init) {
|
if (this.init) {
|
||||||
|
|
@ -77,14 +78,19 @@ export default {
|
||||||
return this.rows.length ? false : this.$fetchState.pending;
|
return this.rows.length ? false : this.$fetchState.pending;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
refreshFlag(neu) {
|
async refreshFlag(neu) {
|
||||||
// this is where the data assignment will trigger the update of the list view...
|
// this is where the data assignment will trigger the update of the list view...
|
||||||
if (this.init && neu) {
|
if (this.init && neu) {
|
||||||
this.$fetch();
|
await this.$fetch();
|
||||||
|
if (this.canPaginate && this.fetchPageSecondaryResources) {
|
||||||
|
this.fetchPageSecondaryResources(true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
// this defines all the flags needed for the mechanism
|
// this defines all the flags needed for the mechanism
|
||||||
// to work. They should be defined based on the main list view
|
// 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
|
// when pagination is enabled we want to wait for the correct set of initial pagination settings to make the call
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const opt = {
|
const opt = {
|
||||||
hasManualRefresh: this.hasManualRefresh,
|
hasManualRefresh: this.hasManualRefresh,
|
||||||
pagination: { ...this.pagination },
|
pagination: { ...this.pagination },
|
||||||
|
|
@ -141,7 +146,8 @@ export default {
|
||||||
return this.$store.dispatch(`${ currStore }/findPage`, {
|
return this.$store.dispatch(`${ currStore }/findPage`, {
|
||||||
type,
|
type,
|
||||||
opt
|
opt
|
||||||
}).finally(() => Vue.set(that, 'paginating', false));
|
})
|
||||||
|
.finally(() => Vue.set(that, 'paginating', false));
|
||||||
}
|
}
|
||||||
|
|
||||||
let incremental = 0;
|
let incremental = 0;
|
||||||
|
|
|
||||||
|
|
@ -450,6 +450,10 @@ export default class ClusterNode extends SteveModel {
|
||||||
get provider() {
|
get provider() {
|
||||||
return this.$rootGetters['currentCluster'].provisioner.toLowerCase();
|
return this.$rootGetters['currentCluster'].provisioner.toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get displayTaintsAndLabels() {
|
||||||
|
return !!this.spec.taints?.length || !!this.customLabelCount;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function calculatePercentage(allocatable, capacity) {
|
function calculatePercentage(allocatable, capacity) {
|
||||||
|
|
|
||||||
|
|
@ -123,7 +123,7 @@ export default {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// We have everything!
|
// We have everything!
|
||||||
if (opt.hasManualRefresh) {
|
if (opt.hasManualRefresh) {
|
||||||
dispatch('resource-fetch/updateManualRefreshIsLoading', false, { root: true });
|
dispatch('resource-fetch/updateManualRefreshIsLoading', false, { root: true });
|
||||||
}
|
}
|
||||||
|
|
@ -145,7 +145,7 @@ export default {
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {*} ctx
|
* @param {*} ctx
|
||||||
* @param { {type: string, opt: FindAllOpt} } opt
|
* @param { {type: string, opt: ActionFindPageArgs} } opt
|
||||||
*/
|
*/
|
||||||
async findAll(ctx, { type, opt }) {
|
async findAll(ctx, { type, opt }) {
|
||||||
const {
|
const {
|
||||||
|
|
@ -164,8 +164,7 @@ export default {
|
||||||
!opt.force &&
|
!opt.force &&
|
||||||
(
|
(
|
||||||
getters['haveAll'](type) ||
|
getters['haveAll'](type) ||
|
||||||
getters['haveAllNamespace'](type, opt.namespaced) ||
|
getters['haveAllNamespace'](type, opt.namespaced)
|
||||||
(opt.pagination ? getters['havePaginatedPage'](type, opt.pagination) : false)
|
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
if (opt.watch !== false ) {
|
if (opt.watch !== false ) {
|
||||||
|
|
@ -323,8 +322,9 @@ export default {
|
||||||
pagination: opt.pagination ? {
|
pagination: opt.pagination ? {
|
||||||
request: opt.pagination,
|
request: opt.pagination,
|
||||||
result: {
|
result: {
|
||||||
count: out.count,
|
count: out.count,
|
||||||
pages: out.pages
|
pages: out.pages,
|
||||||
|
timestamp: new Date().getTime()
|
||||||
}
|
}
|
||||||
} : undefined,
|
} : undefined,
|
||||||
});
|
});
|
||||||
|
|
@ -383,7 +383,7 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
// No need to request the resources if we have them already
|
// 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);
|
return findAllGetter(getters, type, opt);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -412,10 +412,14 @@ export default {
|
||||||
type,
|
type,
|
||||||
data: out.data,
|
data: out.data,
|
||||||
pagination: opt.pagination ? {
|
pagination: opt.pagination ? {
|
||||||
request: opt.pagination,
|
request: {
|
||||||
result: {
|
namespace: opt.namespaced,
|
||||||
count: out.count,
|
pagination: opt.pagination
|
||||||
pages: out.pages
|
},
|
||||||
|
result: {
|
||||||
|
count: out.count,
|
||||||
|
pages: out.pages,
|
||||||
|
timestamp: new Date().getTime()
|
||||||
}
|
}
|
||||||
} : undefined,
|
} : undefined,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -15,8 +15,10 @@ export const urlFor = (state, getters) => (type, id, opt) => {
|
||||||
type = getters.normalizeType(type);
|
type = getters.normalizeType(type);
|
||||||
let url = opt.url;
|
let url = opt.url;
|
||||||
|
|
||||||
|
let schema;
|
||||||
|
|
||||||
if ( !url ) {
|
if ( !url ) {
|
||||||
const schema = getters.schemaFor(type);
|
schema = getters.schemaFor(type);
|
||||||
|
|
||||||
if ( !schema ) {
|
if ( !schema ) {
|
||||||
throw new Error(`Unknown schema for type: ${ type }`);
|
throw new Error(`Unknown schema for type: ${ type }`);
|
||||||
|
|
@ -39,7 +41,7 @@ export const urlFor = (state, getters) => (type, id, opt) => {
|
||||||
url = `${ baseUrl }/${ url }`;
|
url = `${ baseUrl }/${ url }`;
|
||||||
}
|
}
|
||||||
|
|
||||||
url = getters.urlOptions(url, opt);
|
url = getters.urlOptions(url, opt, schema);
|
||||||
|
|
||||||
return url;
|
return url;
|
||||||
};
|
};
|
||||||
|
|
@ -298,16 +300,22 @@ export default {
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
|
|
||||||
havePaginatedPage: (state, getters) => (type, pagination) => {
|
havePaginatedPage: (state, getters) => (type, opt) => {
|
||||||
if (!pagination) {
|
if (!opt.pagination) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
type = getters.normalizeType(type);
|
type = getters.normalizeType(type);
|
||||||
const entry = state.types[type];
|
const entry = state.types[type];
|
||||||
|
|
||||||
if ( entry ) {
|
if ( entry?.havePage ) {
|
||||||
return entry.havePage && paginationUtils.paginationEqual(entry.havePage.request, pagination);
|
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;
|
return false;
|
||||||
|
|
@ -346,7 +354,7 @@ export default {
|
||||||
|
|
||||||
urlFor,
|
urlFor,
|
||||||
|
|
||||||
urlOptions: () => (url, opt) => {
|
urlOptions: () => (url, opt, schema) => {
|
||||||
return url;
|
return url;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ describe('steve: getters:', () => {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// this has its own tests so it just returns the input string
|
// 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) {
|
if (opt.addParam) {
|
||||||
url += '?param=true';
|
url += '?param=true';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,11 @@ import pAndNFiltering from '@shell/plugins/steve/projectAndNamespaceFiltering.ut
|
||||||
type TypeIsCached = { [type: string]: boolean }
|
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
|
* - Multiple namespaces or projects
|
||||||
* - Result of Pagination (a single page of resources that have been sorted / filtered)
|
* - 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
|
* For those scenarios we subscribe to allll changes BUT ignore changes that are not applicable to that subset
|
||||||
*
|
|
||||||
*/
|
*/
|
||||||
class AcceptOrRejectSocketMessage {
|
class AcceptOrRejectSocketMessage {
|
||||||
typeIsNamespaced({ getters }: any, type: string): boolean {
|
typeIsNamespaced({ getters }: any, type: string): boolean {
|
||||||
|
|
|
||||||
|
|
@ -36,12 +36,12 @@ const GC_IGNORE_TYPES = {
|
||||||
const steveRegEx = new RegExp('(/v1)|(\/k8s\/clusters\/[a-z0-9-]+\/v1)');
|
const steveRegEx = new RegExp('(/v1)|(\/k8s\/clusters\/[a-z0-9-]+\/v1)');
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
urlOptions: () => (url, opt) => {
|
urlOptions: () => (url, opt, schema) => {
|
||||||
opt = opt || {};
|
opt = opt || {};
|
||||||
const parsedUrl = parse(url);
|
const parsedUrl = parse(url);
|
||||||
const isSteve = steveRegEx.test(parsedUrl.path);
|
const isSteve = steveRegEx.test(parsedUrl.path);
|
||||||
|
|
||||||
const stevePagination = stevePaginationUtils.checkAndCreateParam(opt);
|
const stevePagination = stevePaginationUtils.createParamsForPagination(schema, opt);
|
||||||
|
|
||||||
if (stevePagination) {
|
if (stevePagination) {
|
||||||
url += `${ (url.includes('?') ? '&' : '?') + stevePagination }`;
|
url += `${ (url.includes('?') ? '&' : '?') + stevePagination }`;
|
||||||
|
|
@ -323,7 +323,7 @@ export default {
|
||||||
*/
|
*/
|
||||||
optionsFor: () => (ctx, { schema, pagination, opts }) => {
|
optionsFor: () => (ctx, { schema, pagination, opts }) => {
|
||||||
if (pagination) {
|
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)
|
// 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: [] };
|
const steveOpts = { listMandatorySort: [] };
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { NAMESPACE_FILTER_NS_FULL_PREFIX, NAMESPACE_FILTER_P_FULL_PREFIX } from '@shell/utils/namespace-filter';
|
import { NAMESPACE_FILTER_NS_FULL_PREFIX, NAMESPACE_FILTER_P_FULL_PREFIX } from '@shell/utils/namespace-filter';
|
||||||
import { getPerformanceSetting } from '@shell/utils/settings';
|
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 {
|
class ProjectAndNamespaceFiltering {
|
||||||
static param = 'projectsornamespaces'
|
static param = 'projectsornamespaces'
|
||||||
|
|
@ -8,7 +8,7 @@ class ProjectAndNamespaceFiltering {
|
||||||
/**
|
/**
|
||||||
* Does the request `opt` definition require resources are fetched from a specific set namespaces/projects?
|
* 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);
|
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)
|
* 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)) {
|
if (!this.isApplicable(opt)) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
|
||||||
export interface SchemaAttributeColumn {
|
export interface SchemaAttributeColumn {
|
||||||
description: string,
|
description: string,
|
||||||
field: string,
|
field: string,
|
||||||
|
|
@ -11,3 +12,11 @@ export interface SchemaAttribute {
|
||||||
columns: SchemaAttributeColumn[],
|
columns: SchemaAttributeColumn[],
|
||||||
namespaced: boolean
|
namespaced: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* At some point this will be properly typed, until then...
|
||||||
|
*/
|
||||||
|
export interface Schema {
|
||||||
|
id: string,
|
||||||
|
attributes: SchemaAttribute
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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_FILTER_ALL_SYSTEM, NAMESPACE_FILTER_ALL_USER, NAMESPACE_FILTER_P_FULL_PREFIX } from '@shell/utils/namespace-filter';
|
||||||
import Namespace from '@shell/models/namespace';
|
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 {
|
class NamespaceProjectFilters {
|
||||||
/**
|
/**
|
||||||
* User needs all resources.... except if there's some settings which should remove resources in specific circumstances
|
* 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
|
// These are AND'd together
|
||||||
// Not ns 1 AND ns 2
|
// Not ns 1 AND ns 2
|
||||||
return allNamespaces.reduce((res, ns) => {
|
return allNamespaces.reduce((res, ns) => {
|
||||||
|
|
@ -16,15 +20,13 @@ class NamespaceProjectFilters {
|
||||||
const hideSystem = productHidesSystemNamespaces ? ns.isSystem : false;
|
const hideSystem = productHidesSystemNamespaces ? ns.isSystem : false;
|
||||||
|
|
||||||
if (hideObscure || hideSystem) {
|
if (hideObscure || hideSystem) {
|
||||||
res.push(new OptPaginationFilter({
|
res.push(PaginationParamFilter.createSingleField({
|
||||||
fields: [new OptPaginationFilterField({
|
field: 'metadata.namespace', value: ns.name, equals: false
|
||||||
field: 'metadata.namespace', value: ns.name, equals: false
|
|
||||||
})]
|
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
}, [] as OptPaginationFilter[]);
|
}, [] as PaginationParamFilter[]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -42,20 +44,16 @@ class NamespaceProjectFilters {
|
||||||
if (isAllSystem) {
|
if (isAllSystem) {
|
||||||
// return resources in system ns 1 OR in system ns 2 ...
|
// return resources in system ns 1 OR in system ns 2 ...
|
||||||
// &filter=metadata.namespace=system ns 1,metadata.namespace=system ns 2
|
// &filter=metadata.namespace=system ns 1,metadata.namespace=system ns 2
|
||||||
return [
|
return [PaginationParamFilter.createMultipleFields(
|
||||||
new OptPaginationFilter({
|
allSystem.map(
|
||||||
fields: allSystem.map(
|
(ns) => new PaginationFilterField({ field: 'metadata.namespace', value: ns.name })
|
||||||
(ns) => new OptPaginationFilterField({ field: 'metadata.namespace', value: ns.name })
|
)
|
||||||
)
|
)];
|
||||||
})
|
|
||||||
];
|
|
||||||
} else { // if isAllUser
|
} else { // if isAllUser
|
||||||
// return resources not in system ns 1 AND not in system ns 2 ...
|
// 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
|
// &filter=metadata.namespace!=system ns 1&filter=metadata.namespace!=system ns 2
|
||||||
return allSystem.map((ns) => new OptPaginationFilter({
|
return allSystem.map((ns) => PaginationParamFilter.createSingleField({
|
||||||
fields: [{
|
field: 'metadata.namespace', value: ns.name, equals: false
|
||||||
field: 'metadata.namespace', value: ns.name, equals: false
|
|
||||||
}]
|
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -68,7 +66,9 @@ class NamespaceProjectFilters {
|
||||||
|
|
||||||
// return resources in project 1 OR namespace 2
|
// return resources in project 1 OR namespace 2
|
||||||
// &projectsornamespaces=project 1,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) {
|
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
|
// > 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,
|
projectsOrNamespaces,
|
||||||
filters: neu
|
filters: neu
|
||||||
.filter((selection) => selection.startsWith(NAMESPACE_FILTER_P_FULL_PREFIX))
|
.filter((selection) => selection.startsWith(NAMESPACE_FILTER_P_FULL_PREFIX))
|
||||||
.map((projects) => new OptPaginationFilter({
|
.map((projects) => PaginationParamFilter.createSingleField({
|
||||||
fields: [{
|
field: 'metadata.namespace', value: projects.replace(NAMESPACE_FILTER_P_FULL_PREFIX, ''), equals: false
|
||||||
field: 'metadata.namespace', value: projects.replace(NAMESPACE_FILTER_P_FULL_PREFIX, ''), equals: false
|
|
||||||
}]
|
|
||||||
}))
|
}))
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -98,6 +96,30 @@ class NamespaceProjectFilters {
|
||||||
* Helper functions for steve pagination
|
* Helper functions for steve pagination
|
||||||
*/
|
*/
|
||||||
class StevePaginationUtils extends NamespaceProjectFilters {
|
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
|
* 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,
|
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,
|
isLocalCluster: boolean,
|
||||||
/**
|
/**
|
||||||
|
|
@ -128,19 +150,19 @@ class StevePaginationUtils extends NamespaceProjectFilters {
|
||||||
*/
|
*/
|
||||||
productHidesSystemNamespaces: boolean,
|
productHidesSystemNamespaces: boolean,
|
||||||
}): {
|
}): {
|
||||||
projectsOrNamespaces: OptPaginationFilter[],
|
projectsOrNamespaces: PaginationParamProjectOrNamespace[],
|
||||||
filters: OptPaginationFilter[]
|
filters: PaginationParamFilter[]
|
||||||
} {
|
} {
|
||||||
// Hold up, why are we doing yet another way to convert the user's project / namespace filter to a set of something?
|
// 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
|
// - 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)
|
// - 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
|
// - 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 System Namespaces - Gimme resources in the system namespaces (which shouldn't be many namespaces)
|
||||||
// - Only User Namespaces - Gimme resources NOT in system namespaces
|
// - Only User Namespaces - Gimme resources NOT in system namespaces
|
||||||
// - User selection - Gimme resources in specific Projects or Namespaces
|
// - User selection - Gimme resources in specific Projects or Namespaces
|
||||||
|
|
||||||
if (isAllNamespaces && (showDynamicRancherNamespaces && !productHidesSystemNamespaces)) {
|
if (isAllNamespaces && (showDynamicRancherNamespaces && !productHidesSystemNamespaces)) {
|
||||||
// No-op. Everything is returned
|
// No-op. Everything is returned
|
||||||
return {
|
return {
|
||||||
|
|
@ -151,10 +173,10 @@ class StevePaginationUtils extends NamespaceProjectFilters {
|
||||||
|
|
||||||
// used to return resources in / not in projects/namespaces (entries are checked in both types)
|
// used to return resources in / not in projects/namespaces (entries are checked in both types)
|
||||||
// &projectsornamespaces=project 1,namespace 2
|
// &projectsornamespaces=project 1,namespace 2
|
||||||
let projectsOrNamespaces: OptPaginationFilter[] = [];
|
let projectsOrNamespaces: PaginationParamProjectOrNamespace[] = [];
|
||||||
// used to return resources in / not in namespaces
|
// used to return resources in / not in namespaces
|
||||||
// &filter=metadata.namespace=abc
|
// &filter=metadata.namespace=abc
|
||||||
let filters: OptPaginationFilter[] = [];
|
let filters: PaginationParamFilter[] = [];
|
||||||
|
|
||||||
if (!showDynamicRancherNamespaces || productHidesSystemNamespaces) {
|
if (!showDynamicRancherNamespaces || productHidesSystemNamespaces) {
|
||||||
// We need to hide dynamic namespaces ('c-', 'p-', etc) OR system namespaces
|
// 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) {
|
if (!opt.pagination) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const params: string[] = [];
|
const params: string[] = [];
|
||||||
const namespaceParam = this.convertPaginationFilter(opt.pagination.projectsOrNamespaces);
|
const namespaceParam = this.convertPaginationParams(schema, opt.pagination.projectsOrNamespaces);
|
||||||
|
|
||||||
if (namespaceParam) {
|
if (namespaceParam) {
|
||||||
params.push(namespaceParam);
|
params.push(namespaceParam);
|
||||||
|
|
@ -201,8 +223,6 @@ class StevePaginationUtils extends NamespaceProjectFilters {
|
||||||
|
|
||||||
if (opt.pagination.pageSize) {
|
if (opt.pagination.pageSize) {
|
||||||
params.push(`pagesize=${ 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) {
|
if (opt.pagination.sort?.length) {
|
||||||
|
|
@ -213,11 +233,11 @@ class StevePaginationUtils extends NamespaceProjectFilters {
|
||||||
params.push(`sort=${ joined }`);
|
params.push(`sort=${ joined }`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (opt.pagination.filter?.length) {
|
if (opt.pagination.filters?.length) {
|
||||||
const andFilters = this.convertPaginationFilter(opt.pagination.filter);
|
const filters = this.convertPaginationParams(schema, opt.pagination.filters);
|
||||||
|
|
||||||
if (andFilters) {
|
if (filters) {
|
||||||
params.push(andFilters);
|
params.push(filters);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -227,13 +247,64 @@ class StevePaginationUtils extends NamespaceProjectFilters {
|
||||||
return params.join('&');
|
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<string>(),
|
||||||
|
invalid: new Array<string>(),
|
||||||
|
};
|
||||||
|
const res = filters
|
||||||
.filter((filter) => !!filter.fields.length)
|
.filter((filter) => !!filter.fields.length)
|
||||||
.map((filter) => {
|
.map((filter) => {
|
||||||
const joined = filter.fields
|
const joined = filter.fields
|
||||||
.map((field) => {
|
.map((field) => {
|
||||||
if (field.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 }`;
|
return `${ field.field }${ field.equals ? '=' : '!=' }${ field.value }`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -243,6 +314,12 @@ class StevePaginationUtils extends NamespaceProjectFilters {
|
||||||
|
|
||||||
return `${ filter.param }${ filter.equals ? '=' : '!=' }${ joined }`;
|
return `${ filter.param }${ filter.equals ? '=' : '!=' }${ joined }`;
|
||||||
}).join('&'); // This means AND
|
}).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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
* Properties on all findX actions
|
||||||
*
|
|
||||||
* For more information see https://github.com/rancher/steve?tab=readme-ov-file#sort
|
|
||||||
*/
|
*/
|
||||||
export interface OptPaginationSort {
|
export type ActionCoreFindArgs = {
|
||||||
/**
|
|
||||||
* 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,
|
|
||||||
force?: boolean,
|
force?: boolean,
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Args used for findAll action
|
* Args used for findAll action
|
||||||
*/
|
*/
|
||||||
export interface FindAllOpt extends CoreFindOpt {
|
export interface ActionFindAllArgs extends ActionCoreFindArgs {
|
||||||
watch?: boolean,
|
watch?: boolean,
|
||||||
namespaced?: string[],
|
namespaced?: string[],
|
||||||
incremental?: boolean,
|
incremental?: boolean,
|
||||||
|
|
@ -174,7 +21,16 @@ export interface FindAllOpt extends CoreFindOpt {
|
||||||
/**
|
/**
|
||||||
* Args used for findPage action
|
* Args used for findPage action
|
||||||
*/
|
*/
|
||||||
export interface FindPageOpt extends CoreFindOpt {
|
export interface ActionFindPageArgs extends ActionCoreFindArgs {
|
||||||
pagination: OptPagination,
|
/**
|
||||||
|
* 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,
|
hasManualRefresh?: boolean,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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<PaginationArgs> 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
|
||||||
|
}
|
||||||
|
|
@ -13,3 +13,18 @@ export interface TableColumn {
|
||||||
tooltip?: string,
|
tooltip?: string,
|
||||||
search?: string | boolean,
|
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'
|
||||||
|
};
|
||||||
|
|
@ -180,7 +180,7 @@ export function sameContents<T>(aryA: T[], aryB: T[]): boolean {
|
||||||
return xor(aryA, aryB).length === 0;
|
return xor(aryA, aryB).length === 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function sameArrayObjects<T>(aryA: T[], aryB: T[]): boolean {
|
export function sameArrayObjects<T>(aryA: T[], aryB: T[], positionAgnostic = false): boolean {
|
||||||
if (!aryA && !aryB) {
|
if (!aryA && !aryB) {
|
||||||
// catch calls from js (where props aren't type checked)
|
// catch calls from js (where props aren't type checked)
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -190,9 +190,29 @@ export function sameArrayObjects<T>(aryA: T[], aryB: T[]): boolean {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let i = 0; i < aryA.length; i++) {
|
if (positionAgnostic) {
|
||||||
if (!isEqual(aryA[i], aryB[i])) {
|
const consumedB: { [pos: number]: boolean } = {};
|
||||||
return false;
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import {
|
||||||
NAMESPACE_FILTER_NS_FULL_PREFIX,
|
NAMESPACE_FILTER_NS_FULL_PREFIX,
|
||||||
NAMESPACE_FILTER_P_FULL_PREFIX,
|
NAMESPACE_FILTER_P_FULL_PREFIX,
|
||||||
} from '@shell/utils/namespace-filter';
|
} 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 { sameArrayObjects } from '@shell/utils/array';
|
||||||
import { isEqual } from '@shell/utils/object';
|
import { isEqual } from '@shell/utils/object';
|
||||||
|
|
||||||
|
|
@ -21,7 +21,7 @@ const settings: PaginationSettings = {
|
||||||
resources: {
|
resources: {
|
||||||
enableAll: false,
|
enableAll: false,
|
||||||
enableSome: {
|
enableSome: {
|
||||||
enabled: ['configmap', 'secret', 'pod'],
|
enabled: ['configmap', 'secret', 'pod', 'node'],
|
||||||
generic: true,
|
generic: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -110,15 +110,15 @@ class PaginationUtils {
|
||||||
return this.validNsProjectFilters.includes(nsProjectFilter);
|
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) {
|
if (a.param !== b.param || a.equals !== b.equals) {
|
||||||
return false;
|
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) {
|
if (!!a && a?.length !== b?.length) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -132,18 +132,18 @@ class PaginationUtils {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
paginationEqual(a?: OptPagination, b?: OptPagination): boolean {
|
paginationEqual(a?: PaginationArgs, b?: PaginationArgs): boolean {
|
||||||
const {
|
const {
|
||||||
filter: aFilter = [], sort: aSort = [], projectsOrNamespaces: aPN = [], ...aPrimitiveTypes
|
filters: aFilter = [], sort: aSort = [], projectsOrNamespaces: aPN = [], ...aPrimitiveTypes
|
||||||
} = a || {};
|
} = a || {};
|
||||||
const {
|
const {
|
||||||
filter: bFilter = [], sort: bSort = [], projectsOrNamespaces: bPN = [], ...bPrimitiveTypes
|
filters: bFilter = [], sort: bSort = [], projectsOrNamespaces: bPN = [], ...bPrimitiveTypes
|
||||||
} = b || {};
|
} = b || {};
|
||||||
|
|
||||||
return isEqual(aPrimitiveTypes, bPrimitiveTypes) &&
|
return isEqual(aPrimitiveTypes, bPrimitiveTypes) &&
|
||||||
this.paginationFiltersEqual(aFilter, bFilter) &&
|
this.paginationFiltersEqual(aFilter, bFilter) &&
|
||||||
this.paginationFiltersEqual(aPN, bPN) &&
|
this.paginationFiltersEqual(aPN, bPN) &&
|
||||||
sameArrayObjects<OptPaginationSort>(aSort, bSort);
|
sameArrayObjects<PaginationSort>(aSort, bSort, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue