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