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//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; } }, };