dashboard/shell/mixins/resource-fetch-api-paginati...

369 lines
11 KiB
JavaScript

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 { PaginationParamFilter, PaginationFilterField, PaginationArgs } from '@shell/types/store/pagination.types';
import stevePaginationUtils from '@shell/plugins/steve/steve-pagination-utils';
import { STEVE_WATCH_MODE } from '@shell/types/store/subscribe.types';
/**
* Companion mixin used with `resource-fetch` for `ResourceList` to determine if the user needs to filter the list by a single namespace
*/
export default {
props: {
namespaced: {
type: Boolean,
default: null, // Automatic from schema
},
/**
* Where in the ui this mixin is used. For instance the home page cluster list would be `home`
*/
context: {
type: String,
default: null,
},
},
data() {
return {
forceUpdateLiveAndDelayed: 0,
/**
* This of type `OptPagination`
*/
pPagination: null,
// Avoid scenarios where namespace is updated just before other pagination changes come in
debouncedSetPagination: debounce(this.setPagination, 50),
/**
* Apply these additional filters given the ns / project header selection
*/
requestFilters: {
filters: [],
projectsOrNamespaces: [],
},
paginationFromList: null,
isPaginationManualRefreshEnabled: paginationUtils.isListManualRefreshEnabled({ rootGetters: this.$store.getters }),
};
},
methods: {
/**
* @param {PaginationArgs} pagination
*/
setPagination(pagination) {
if (pagination) {
this.pPagination = pagination;
}
},
/**
* Primary point that handles changes from either a table or the namespace filter
*/
paginationChanged(event) {
if (!event) {
return;
}
this.paginationFromList = event;
const {
page, perPage, filter, sort, descending
} = event;
const searchFilters = filter.searchQuery ? filter.searchFields.map((field) => new PaginationFilterField({
field,
value: filter.searchQuery,
exact: false,
})) : [];
const pagination = new PaginationArgs({
page,
pageSize: perPage,
sort: sort?.map((field) => ({
field,
asc: !descending
})),
projectsOrNamespaces: this.requestFilters.projectsOrNamespaces,
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);
},
/**
* @param {PaginationArgs} neu
* @param {PaginationArgs} old
*/
paginationEqual(neu, old) {
if (!neu.page) {
// Not valid, count as not equal
return false;
}
if (paginationUtils.paginationEqual(neu, old)) {
return true;
}
return false;
},
calcCanPaginate() {
if (!this.resource) {
return false;
}
const args = {
id: this.resource.id || this.resource,
context: this.context,
};
return this.$store.getters[`${ this.overrideInStore || this.inStore }/paginationEnabled`]?.(args);
}
},
computed: {
...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?
*/
paginationNsFilterRequired() {
return this.canPaginate && !this.__validPaginationNsFilter;
},
/**
* Check if the Project/Namespace filter from the header contains a valid ns / project filter
*/
__validPaginationNsFilter() {
return paginationUtils.validateNsProjectFilters(this.namespaceFilters);
},
/**
* 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 exiting earlier if mixin is from parent ResourceList and leave work for CustomList mixins
*/
isResourceList() {
return !!this.hasListComponent;
},
/**
* Is Pagination supported and has the table supplied pagination settings from the table?
*/
pagination() {
if (this.isResourceList) {
return;
}
return this.canPaginate ? this.pPagination : '';
},
/**
* Should this list be paginated via API?
*/
canPaginate() {
if (this.isResourceList) {
return;
}
return this.calcCanPaginate();
},
paginationResult() {
if (this.isResourceList || !this.canPaginate) {
return;
}
return this.havePaginated?.result;
},
havePaginated() {
if (this.isResourceList) {
return;
}
return this.$store.getters[`${ this.overrideInStore || this.inStore }/havePage`](this.resource);
},
/**
* Links to ns.isSystem and covers things like ns with system annotation, hardcoded list, etc
*/
productHidesSystemNamespaces() {
return this.currentProduct?.hideSystemResources;
},
/**
* Links to ns.isObscure and covers things like `c-`, `user-`, etc (see OBSCURE_NAMESPACE_PREFIX)
*/
showDynamicRancherNamespaces() {
return this.$store.getters['prefs/get'](ALL_NAMESPACES);
},
isNamespaced() {
if (this.namespaced !== null) { // null is the default value
// This is an override, but only if it's set
return !!this.namespaced;
}
return this.schema?.attributes?.namespaced;
}
},
watch: {
/**
* Monitor the rows to ensure deleting the last entry in a server-side paginated page doesn't
* result in an empty page
*/
rows(neu) {
if (!this.canPaginate || !this.pagination || this.isResourceList) {
return;
}
if (this.pagination.page > 1 && neu.length === 0) {
this.setPagination({
...this.pagination,
page: this.pagination.page - 1
});
}
},
namespaceFilters: {
immediate: true,
async handler(neu, old) {
if (!this.canPaginate || !this.isNamespaced) {
return;
}
if (this.isResourceList) {
return;
}
// Transitioning from no ns filters to no ns filters should be avoided
const neuEmpty = !neu || neu.length === 0 || neu[0] === NAMESPACE_FILTER_ALL;
const oldEmpty = !old || old.length === 0 || old[0] === NAMESPACE_FILTER_ALL;
if (neuEmpty && oldEmpty) {
const allButHidingSystemResources = this.isAllNamespaces && (!this.showDynamicRancherNamespaces || this.productHidesSystemNamespaces);
// If we're showing all... and not hiding system or obscure ns then don't go through filter process
if (!allButHidingSystemResources) {
return;
}
}
// Transitioning to a ns filter that doesn't affect the list should be avoided
if (neu.length === 1) {
if ([NAMESPACE_FILTER_NAMESPACED_YES, NAMESPACE_FILTER_NAMESPACED_NO].includes(neu[0])) {
return;
}
}
const {
projectsOrNamespaces,
filters
} = stevePaginationUtils.createParamsFromNsFilter({
allNamespaces: this.$store.getters[`${ this.currentProduct?.inStore }/all`](NAMESPACE),
selection: neu,
isAllNamespaces: this.isAllNamespaces,
isLocalCluster: this.$store.getters['currentCluster'].isLocal,
showReservedRancherNamespaces: this.showDynamicRancherNamespaces,
productHidesSystemNamespaces: this.productHidesSystemNamespaces,
});
this.requestFilters.filters = filters;
this.requestFilters.projectsOrNamespaces = projectsOrNamespaces;
}
},
'requestFilters.filters'() {
this.paginationChanged(this.paginationFromList);
},
'requestFilters.projectsOrNamespaces'() {
this.paginationChanged(this.paginationFromList);
},
/**
* 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.canPaginate) {
return;
}
// 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, 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({
canPaginate: this.canPaginate, force: false, page: this.rows, pagResult: this.paginationResult
});
}
},
async beforeUnmount() {
if (this.havePaginated) {
// of type @STEVE_WATCH_PARAMS
const watchArgs = {
type: this.resource,
mode: STEVE_WATCH_MODE.RESOURCE_CHANGES,
};
await this.$store.dispatch(`${ this.overrideInStore || this.inStore }/forgetType`, this.resource, (watchParams) => {
return watchParams.type === watchArgs.type && watchParams.mode === watchArgs.type.mode;
});
}
}
};