dashboard/shell/plugins/steve/getters.js

378 lines
12 KiB
JavaScript

import { isArray } from '@shell/utils/array';
import { BY_TYPE } from '@shell/plugins/dashboard-store/classify';
import { lookup } from '@shell/plugins/dashboard-store/model-loader';
import { NAMESPACE, SCHEMA, COUNT, UI } from '@shell/config/types';
import SteveModel from './steve-class';
import HybridModel, { cleanHybridResources } from './hybrid-class';
import NormanModel from './norman-class';
import { urlFor } from '@shell/plugins/dashboard-store/getters';
import { normalizeType } from '@shell/plugins/dashboard-store/normalize';
import pAndNFiltering from '@shell/plugins/steve/projectAndNamespaceFiltering.utils';
import stevePaginationUtils from '@shell/plugins/steve/steve-pagination-utils';
import { parse } from '@shell/utils/url';
import { splitObjectPath } from '@shell/utils/string';
import { parseType } from '@shell/models/schema';
import {
STEVE_AGE_COL,
STEVE_ID_COL, STEVE_LIST_GROUPS, STEVE_NAME_COL, STEVE_NAMESPACE_COL, STEVE_STATE_COL
} from '@shell/config/pagination-table-headers';
import { createHeaders } from '@shell/store/type-map.utils';
import paginationUtils from '@shell/utils/pagination-utils';
export const STEVE_MODEL_TYPES = {
NORMAN: 'norman',
STEVE: 'steve',
BY_TYPE: 'byType'
};
const GC_IGNORE_TYPES = {
[COUNT]: true,
[NAMESPACE]: true,
[SCHEMA]: true,
[UI.NAV_LINK]: true,
};
// Include calls to /v1 AND /k8s/clusters/<cluster id>/v1
const steveRegEx = new RegExp('(/v1)|(\/k8s\/clusters\/[a-z0-9-]+\/v1)');
export default {
/**
* Is the url path a rancher steve one?
*
* Can be used to change behaviour given steve api
*/
isSteveUrl: () => (urlPath) => steveRegEx.test(urlPath),
/**
* Is the url path a rancher steve one AND the steve cache is enabled?
*
* Can be used to change behaviour given steve cache api functionality
*/
isSteveCacheUrl: (state, getters, rootState, rootGetters) => (urlPath) => getters.isSteveUrl(urlPath) && paginationUtils.isSteveCacheEnabled({ rootGetters }),
/**
* opt: ActionFindPageArgs
*/
urlOptions: (state, getters) => (url, opt, schema) => {
opt = opt || {};
const parsedUrl = parse(url || '');
const isSteveUrl = getters.isSteveUrl(parsedUrl.path);
const stevePagination = stevePaginationUtils.createParamsForPagination({ schema, opt });
if (stevePagination) {
url += `${ (url.includes('?') ? '&' : '?') + stevePagination }`;
} else {
const isSteveCacheUrl = getters.isSteveCacheUrl(parsedUrl.path);
// labelSelector
if ( opt.labelSelector ) {
url += `${ url.includes('?') ? '&' : '?' }labelSelector=${ opt.labelSelector }`;
}
// End: labelSelector
// Filter
if ( opt.filter ) {
// When ui-sql-cache is always on we should look to replace the usages of this with findPage (basically using the new filter definitions)
url += `${ (url.includes('?') ? '&' : '?') }`;
const keys = Object.keys(opt.filter);
keys.forEach((key) => {
let vals = opt.filter[key];
if ( !isArray(vals) ) {
vals = [vals];
}
if (isSteveUrl) {
url += `${ (url.includes('filter=') ? '&' : 'filter=') }`;
}
const filterStrings = vals.map((val) => {
return `${ encodeURI(key) }${ isSteveCacheUrl ? '~' : '=' }${ encodeURI(val) }`;
});
const urlEnding = url.charAt(url.length - 1);
const nextStringConnector = ['&', '?', '='].includes(urlEnding) ? '' : '&';
url += `${ nextStringConnector }${ filterStrings.join('&') }`;
});
}
// `opt.namespaced` is either
// - a string representing a single namespace - add restriction to the url
// - an array of namespaces or projects - add restriction as a param
const namespaceProjectFilter = pAndNFiltering.checkAndCreateParam(opt);
if (namespaceProjectFilter) {
url += `${ (url.includes('?') ? '&' : '?') + namespaceProjectFilter }`;
}
// End: Filter
// Limit
const limit = opt.limit;
if ( limit ) {
url += `${ url.includes('?') ? '&' : '?' }limit=${ limit }`;
}
// End: Limit
// Page Size
if (isSteveCacheUrl && opt.isCollection) {
// This is a steve url and the new cache is being used.
// Pre-cache there was always a max page size (given kube proxy). With cache there's not.
// So ensure we don't go backwards (and fetch crazy high resource counts) by adding a default
url += `${ url.includes('?') ? '&' : '?' }pagesize=${ paginationUtils.defaultPageSize }`;
}
// End: Page Size
// Sort
// Steve's sort options supports multi-column sorting and column specific sort orders, not implemented yet #9341
const sortBy = opt.sortBy;
const orderBy = opt.sortOrder;
if ( sortBy ) {
if (isSteveUrl) {
url += `${ url.includes('?') ? '&' : '?' }sort=${ (orderBy === 'desc' ? '-' : '') + encodeURI(sortBy) }`;
} else {
url += `${ url.includes('?') ? '&' : '?' }sort=${ encodeURI(sortBy) }`;
if ( orderBy ) {
url += `${ url.includes('?') ? '&' : '?' }order=${ encodeURI(orderBy) }`;
}
}
}
// End: Sort
}
// Exclude
// excludeFields should be an array of strings representing the paths of the fields to exclude
// only works on Steve but is ignored without error by Norman
if (isSteveUrl) {
if (!Array.isArray(opt?.excludeFields)) {
const excludeFields = ['metadata.managedFields'];
// for some resources, we might want to include fields, excluded by default.
opt.excludeFields = Array.isArray(opt?.omitExcludeFields) ? excludeFields.filter((f) => !f.includes(opt.omitExcludeFields)) : excludeFields;
}
if (opt.excludeFields.length) {
const excludeParamsString = opt.excludeFields.map((field) => `exclude=${ field }`).join('&');
url += `${ url.includes('?') ? '&' : '?' }${ excludeParamsString }`;
}
if (opt.revision) {
url += `${ url.includes('?') ? '&' : '?' }${ `revision=${ opt.revision }` }`;
}
}
// End: Exclude
return url;
},
urlFor: (state, getters) => (type, id, opt) => {
let url = urlFor(state, getters)(type, id, opt);
// `namespaced` is either
// - a string representing a single namespace - add restriction to the url
// - an array of namespaces or projects - add restriction as a param
if (!opt?.url && opt?.namespaced && !pAndNFiltering.isApplicable(opt)) {
// Update path to include `namespace`, but take into account
// - if there is an id
// - if there are query params
// Construct a url so query params / fragments are avoided
const urlObj = new URL(url);
const path = urlObj.pathname;
if (!!path?.length && path[path.length - 1] === '/') {
urlObj.pathname = path.substring(0, path.length - 1);
}
const parts = urlObj.pathname.split('/');
if (id) {
// namespace should go before the id in the path
parts.splice(parts.length - 1, 0, opt.namespaced);
urlObj.pathname = parts.join('/');
} else {
// namespace should go at the end of the path
urlObj.pathname = `${ urlObj.pathname.split('/').join('/') }/${ opt.namespaced }`;
}
url = urlObj.toString();
}
return url;
},
defaultModel: (state) => (obj) => {
const which = state.config.modelBaseClass || STEVE_MODEL_TYPES.BY_TYPE.STEVE;
if ( which === STEVE_MODEL_TYPES.BY_TYPE ) {
if ( obj?.type?.startsWith('management.cattle.io.') || obj?.type?.startsWith('project.cattle.io.')) {
return HybridModel;
} else {
return SteveModel;
}
} else if ( which === STEVE_MODEL_TYPES.NORMAN ) {
return NormanModel;
} else {
return SteveModel;
}
},
classify: (state, getters, rootState) => (obj) => {
const customModel = lookup(state.config.namespace, obj?.type, obj?.metadata?.name, rootState);
if (customModel) {
return customModel;
}
const which = state.config.modelBaseClass || BY_TYPE;
if ( which === BY_TYPE ) {
if ( obj?.type?.startsWith('management.cattle.io.') || obj?.type?.startsWith('project.cattle.io.')) {
return HybridModel;
} else {
return SteveModel;
}
} else if ( which === STEVE_MODEL_TYPES.NORMAN ) {
return NormanModel;
} else {
return SteveModel;
}
},
cleanResource: () => (existing, data) => {
/**
* Resource counts are contained within a single 'count' resource with a 'counts' field that is a map of resource types
* When counts are updated through the websocket, only the resources that changed are sent so we can't load the new 'count' resource into the store as we would another resource
*/
if (data?.type === COUNT && existing) {
data.counts = { ...existing.counts, ...data.counts };
return data;
}
// If the existing model has a cleanResource method, use it
if (existing?.cleanResource && typeof existing.cleanResource === 'function') {
return existing.cleanResource(data);
}
const typeSuperClass = Object.getPrototypeOf(Object.getPrototypeOf(existing))?.constructor;
return typeSuperClass === HybridModel ? cleanHybridResources(data) : data;
},
// Return all the pods for a given namespace
podsByNamespace: (state) => (namespace) => {
const map = state.podsByNamespace[namespace];
return map?.list || [];
},
gcIgnoreTypes: () => {
return GC_IGNORE_TYPES;
},
currentGeneration: (state) => (type) => {
type = normalizeType(type);
const cache = state.types[type];
if ( !cache ) {
return null;
}
return cache.generation;
},
/**
* Checks the norman or steve schema resourceFields for the given path
*/
pathExistsInSchema: (state, getters) => (type, path) => {
const schema = getters.schemaFor(type);
if (schema.requiresResourceFields && !schema.hasResourceFields) {
console.warn(`pathExistsInSchema requires schema ${ schema.id } to have resources fields via schema definition but none were found. has the schema 'fetchResourceFields' been called?`); // eslint-disable-line no-console
return false;
}
const schemaDefinitions = schema.requiresResourceFields ? schema.schemaDefinitions : null;
const parts = splitObjectPath(path);
let schemaOrSchemaDefinition = schema;
// Iterate down the parts (properties) until there are no parts left (success) or the path cannot be found (failure)
while ( parts.length ) {
const key = parts.shift();
const field = schemaOrSchemaDefinition.resourceFields?.[key];
type = field?.type;
if ( !type ) {
return false;
}
if ( parts.length ) {
type = parseType(type, field).pop(); // Get the main part of array[map[something]] => something
schemaOrSchemaDefinition = schemaDefinitions ? schemaDefinitions?.[type] : getters.schemaFor(type);
if ( !schema ) {
return false;
}
}
}
return true;
},
/*
* Override the vanilla type-map headersFor. This allows custom columns
*/
headersFor: (state, getters, rootState, rootGetters) => ({
getters: typeMapGetters,
state: typeMapState,
}, { schema, pagination }) => {
if (!pagination ) {
return;
}
return createHeaders({
state: typeMapState, getters: typeMapGetters, rootGetters
}, {
headers: typeMapState.paginationHeaders,
typeOptions: typeMapGetters['optionsFor'](schema, true),
schema,
columns: {
name: STEVE_NAME_COL,
state: STEVE_STATE_COL,
namespace: STEVE_NAMESPACE_COL,
age: STEVE_AGE_COL,
id: STEVE_ID_COL
}
});
},
/**
* Override the vanilla type-map optionsFor. This allows custom list values
*/
optionsFor: () => (ctx, { schema, pagination, opts }) => {
if (pagination) {
// 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: [] };
if (!opts.listGroupsWillOverride && schema.attributes.namespaced) {
// There's no pre-configured settings... and we're paginating... so use pagination specific groups
steveOpts.listGroups = STEVE_LIST_GROUPS;
steveOpts.listGroupsWillOverride = true;
}
return steveOpts;
}
},
};