mirror of https://github.com/rancher/dashboard.git
546 lines
15 KiB
JavaScript
546 lines
15 KiB
JavaScript
|
|
import { SCHEMA, COUNT, POD } from '@shell/config/types';
|
|
|
|
import { matches } from '@shell/utils/selector';
|
|
import { typeMunge, typeRef, SIMPLE_TYPES } from '@shell/utils/create-yaml';
|
|
import Resource from '@shell/plugins/dashboard-store/resource-class';
|
|
import mutations from './mutations';
|
|
import { keyFieldFor, normalizeType } from './normalize';
|
|
import { lookup } from './model-loader';
|
|
import garbageCollect from '@shell/utils/gc/gc';
|
|
import paginationUtils from '@shell/utils/pagination-utils';
|
|
import { labelSelectorToSelector } from '@shell/utils/selector-typed';
|
|
|
|
/**
|
|
* opt: ActionFindPageArgs
|
|
*/
|
|
export const urlFor = (state, getters) => (type, id, opt) => {
|
|
opt = opt || {};
|
|
type = getters.normalizeType(type);
|
|
let url = opt.url;
|
|
|
|
let schema;
|
|
|
|
if ( !url ) {
|
|
schema = getters.schemaFor(type);
|
|
|
|
if ( !schema ) {
|
|
throw new Error(`Unknown schema for type: ${ type }`);
|
|
}
|
|
|
|
url = schema.links.collection;
|
|
|
|
if ( !url ) {
|
|
throw new Error(`You don't have permission to list this type: ${ type }`);
|
|
}
|
|
|
|
if ( id ) {
|
|
url += `/${ id }`;
|
|
}
|
|
}
|
|
|
|
if ( !url.startsWith('/') && !url.startsWith('http') ) {
|
|
const baseUrl = state.config.baseUrl.replace(/\/$/, '');
|
|
|
|
url = `${ baseUrl }/${ url }`;
|
|
}
|
|
|
|
url = getters.urlOptions(url, opt, schema);
|
|
|
|
return url;
|
|
};
|
|
|
|
function resourceCount(rootGetters, getters, typeObj) {
|
|
let _typeObj = typeObj;
|
|
const { name: type, count } = _typeObj;
|
|
|
|
if (!type) {
|
|
throw new Error(`Resource type required to calc count: ${ JSON.stringify(typeObj) }`);
|
|
}
|
|
|
|
if (!count) {
|
|
const schema = getters.schemaFor(type);
|
|
const counts = getters.all(COUNT)?.[0]?.counts || {};
|
|
const count = counts[type];
|
|
|
|
// This object aligns with `Type.vue` `type`
|
|
_typeObj = {
|
|
count: count ? count.summary.count || 0 : null,
|
|
byNamespace: count ? count.namespaces : {},
|
|
revision: count ? count.revision : null,
|
|
namespaced: schema?.attributes?.namespaced
|
|
};
|
|
}
|
|
|
|
const namespaces = _typeObj?.namespaced && !rootGetters.isAllNamespaces ? Object.keys(rootGetters.activeNamespaceCache || {}) : [];
|
|
|
|
return matchingCounts(_typeObj, namespaces.length ? namespaces : null);
|
|
}
|
|
|
|
/**
|
|
* Find the number of resources given
|
|
* - if the type is namespaced
|
|
* - if there are any counts per namespace
|
|
* - if there are no namespaces
|
|
* - if there is no total count
|
|
*/
|
|
function matchingCounts(typeObj, namespaces) {
|
|
// That was easy
|
|
if ( !typeObj.namespaced || !typeObj.byNamespace || namespaces === null || typeObj.count === null) {
|
|
return typeObj.count;
|
|
}
|
|
|
|
let out = 0;
|
|
|
|
// Otherwise start with 0 and count up
|
|
for ( const namespace of namespaces ) {
|
|
out += typeObj.byNamespace[namespace]?.count || 0;
|
|
}
|
|
|
|
return out;
|
|
}
|
|
|
|
export default {
|
|
|
|
/**
|
|
* Get all entries in the store. This might not mean all entries of this type
|
|
*/
|
|
all: (state, getters, rootState) => (type) => {
|
|
type = getters.normalizeType(type);
|
|
|
|
if ( !getters.typeRegistered(type) ) {
|
|
// Yes this is mutating state in a getter... it's not the end of the world..
|
|
// throw new Error(`All of ${ type } is not loaded`);
|
|
console.warn(`All of ${ type } is not loaded yet`); // eslint-disable-line no-console
|
|
mutations.registerType(state, type);
|
|
}
|
|
|
|
garbageCollect.gcUpdateLastAccessed({
|
|
state, getters, rootState
|
|
}, type);
|
|
|
|
return state.types[type].list;
|
|
},
|
|
|
|
/**
|
|
* This is a _manual_ application of the selector against whatever is in the store
|
|
*
|
|
* The store must be populated with enough resources to make this valid
|
|
*
|
|
* It's like `matching` except
|
|
* - it accepts a kube labelSelector object (see KubeLabelSelector)
|
|
* - if the store already has the selector and only the selector just return it, otherwise run through `matching`
|
|
*/
|
|
matchingLabelSelector: (state, getters, rootState) => (type, labelSelector, namespace) => {
|
|
type = getters.normalizeType(type);
|
|
const selector = labelSelectorToSelector(labelSelector);
|
|
const page = getters['havePage'](type, selector)?.request;
|
|
|
|
// Does the store contain the result of a vai backed api request?
|
|
if (
|
|
page?.namespace === namespace &&
|
|
page?.pagination?.filters?.length === 0 &&
|
|
page?.pagination.labelSelector &&
|
|
selector === labelSelectorToSelector(page?.pagination.labelSelector
|
|
)
|
|
) {
|
|
return getters.all(type);
|
|
}
|
|
|
|
// Does the store contain the result of a specific labelSelector request?
|
|
if (getters['haveSelector'](type, selector)) {
|
|
return getters.all(type);
|
|
}
|
|
|
|
if (getters['havePage'](type)) {
|
|
return getters.all(type);
|
|
}
|
|
|
|
// Does the store have all and we can pretend like it contains a result of a labelSelector?
|
|
if (getters['haveAll'](type)) {
|
|
return getters.matching( type, selector, namespace );
|
|
}
|
|
|
|
return [];
|
|
},
|
|
|
|
/**
|
|
* This is a _manual_ application of the selector against whatever is in the store
|
|
*
|
|
* The store must be populated with enough resources to make this valid
|
|
*/
|
|
matching: (state, getters, rootState) => (type, selector, namespace, config = { skipSelector: false }) => {
|
|
let matching = getters['all'](type);
|
|
|
|
// Filter first by namespace if one is provided, since this is efficient
|
|
if (namespace && typeof namespace === 'string') {
|
|
matching = type === POD ? getters['podsByNamespace'](namespace) : matching.filter((obj) => obj.namespace === namespace);
|
|
}
|
|
|
|
garbageCollect.gcUpdateLastAccessed({
|
|
state, getters, rootState
|
|
}, type);
|
|
|
|
// Looks like a falsy selector is a thing, so if we're not interested in filtering by the selector... explicitly avoid it
|
|
if (config.skipSelector) {
|
|
return matching;
|
|
}
|
|
|
|
return matching.filter((obj) => {
|
|
return matches(obj, selector);
|
|
});
|
|
},
|
|
|
|
byId: (state, getters, rootState) => (type, id) => {
|
|
type = getters.normalizeType(type);
|
|
const entry = state.types[type];
|
|
|
|
if ( entry ) {
|
|
garbageCollect.gcUpdateLastAccessed({
|
|
state, getters, rootState
|
|
}, type);
|
|
|
|
return entry.map.get(id);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Checks a schema for the given path
|
|
*
|
|
* Given that schema are primarily a rancher thing most logic is in the `steve` store
|
|
*/
|
|
pathExistsInSchema: (state, getters) => (type, path) => {
|
|
return false;
|
|
},
|
|
|
|
// @TODO resolve difference between this and schemaFor and have only one of them.
|
|
schema: (state, getters) => (type) => {
|
|
type = getters.normalizeType(type);
|
|
const schemas = state.types[SCHEMA];
|
|
const keyField = getters.keyFieldForType(SCHEMA);
|
|
|
|
return schemas.list.find((x) => {
|
|
const thisOne = getters.normalizeType(x[keyField]);
|
|
|
|
return thisOne === type || thisOne.endsWith(`.${ type }`);
|
|
});
|
|
},
|
|
|
|
// Fuzzy search to find a matching schema name for plugins/lookup
|
|
schemaName: (state, getters) => (type) => {
|
|
type = getters.normalizeType(type);
|
|
const schemas = state.types[SCHEMA];
|
|
const keyField = getters.keyFieldForType(SCHEMA);
|
|
const res = schemas.list.find((x) => {
|
|
const thisOne = getters.normalizeType(x[keyField]);
|
|
|
|
return thisOne === type || thisOne.endsWith(`.${ type }`);
|
|
});
|
|
|
|
if (!res) {
|
|
return;
|
|
}
|
|
const arrayRes = Array.isArray(res) ? res : [res];
|
|
const entries = arrayRes.map((x) => {
|
|
return x[keyField];
|
|
}).sort((a, b) => {
|
|
return a.length - b.length;
|
|
});
|
|
|
|
if ( entries[0] ) {
|
|
return entries[0];
|
|
}
|
|
|
|
return type;
|
|
},
|
|
|
|
// Fuzzy is only for plugins/lookup, do not use in real code
|
|
schemaFor: (state, getters) => (type, fuzzy = false, allowThrow = true) => {
|
|
const schemas = state.types[SCHEMA];
|
|
|
|
type = getters.normalizeType(type);
|
|
|
|
if ( !schemas ) {
|
|
if ( allowThrow ) {
|
|
throw new Error("Schemas aren't loaded yet");
|
|
} else {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
const out = schemas.map.get(type);
|
|
|
|
if ( !out && fuzzy ) {
|
|
const close = getters.schemaName(type);
|
|
|
|
if ( close ) {
|
|
return getters.schemaFor(close);
|
|
}
|
|
}
|
|
|
|
return out;
|
|
},
|
|
|
|
defaultFor: (state, getters) => (type, rootSchema, schemaDefinitions = null) => {
|
|
let resourceFields;
|
|
|
|
if (!schemaDefinitions) {
|
|
// Depth 0. Get the schemaDefinitions that will contain the child schema resourceFields for recursive calls
|
|
|
|
schemaDefinitions = rootSchema.schemaDefinitions || {}; // norman...
|
|
resourceFields = rootSchema.resourceFields || {};
|
|
} else {
|
|
if (rootSchema.requiresResourceFields) {
|
|
resourceFields = schemaDefinitions[type]?.resourceFields || {};
|
|
} else {
|
|
const schema = getters['schemaFor'](type);
|
|
|
|
resourceFields = schema?.resourceFields || {};
|
|
}
|
|
}
|
|
|
|
const out = {};
|
|
|
|
for ( const key in resourceFields ) {
|
|
const field = resourceFields[key];
|
|
|
|
if ( !field ) {
|
|
// Not much to do here...
|
|
continue;
|
|
}
|
|
|
|
const type = typeMunge(field.type);
|
|
const mapOf = typeRef('map', type, field);
|
|
const arrayOf = typeRef('array', type, field);
|
|
const referenceTo = typeRef('reference', type);
|
|
|
|
if ( mapOf || type === 'map' || type === 'json' ) {
|
|
out[key] = getters.defaultFor(type, rootSchema, schemaDefinitions);
|
|
} else if ( arrayOf || type === 'array' ) {
|
|
out[key] = [];
|
|
} else if ( referenceTo ) {
|
|
out[key] = undefined;
|
|
} else if ( SIMPLE_TYPES.includes(type) ) {
|
|
if ( typeof field['default'] === 'undefined' ) {
|
|
out[key] = undefined;
|
|
} else {
|
|
out[key] = field['default'];
|
|
}
|
|
} else {
|
|
out[key] = getters.defaultFor(type, rootSchema, schemaDefinitions);
|
|
}
|
|
}
|
|
|
|
return out;
|
|
},
|
|
|
|
canList: (state, getters) => (type) => {
|
|
const schema = getters.schemaFor(type);
|
|
|
|
return schema && schema.hasLink('collection');
|
|
},
|
|
|
|
typeRegistered: (state, getters) => (type) => {
|
|
type = getters.normalizeType(type);
|
|
|
|
return !!state.types[type];
|
|
},
|
|
|
|
typeEntry: (state, getters) => (type) => {
|
|
type = getters.normalizeType(type);
|
|
|
|
return state.types[type];
|
|
},
|
|
|
|
haveAll: (state, getters) => (type) => {
|
|
type = getters.normalizeType(type);
|
|
const entry = state.types[type];
|
|
|
|
if ( entry ) {
|
|
return entry.haveAll || false;
|
|
}
|
|
|
|
return false;
|
|
},
|
|
|
|
haveAllNamespace: (state, getters) => (type, namespace) => {
|
|
if (!namespace) {
|
|
return false;
|
|
}
|
|
|
|
type = getters.normalizeType(type);
|
|
const entry = state.types[type];
|
|
|
|
if ( entry ) {
|
|
return entry.haveNamespace === namespace;
|
|
}
|
|
|
|
return false;
|
|
},
|
|
|
|
havePaginatedPage: (state, getters) => (type, opt) => {
|
|
if (!opt.pagination) {
|
|
return false;
|
|
}
|
|
|
|
type = getters.normalizeType(type);
|
|
const entry = state.types[type];
|
|
|
|
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;
|
|
},
|
|
|
|
haveNamespace: (state, getters) => (type) => {
|
|
type = getters.normalizeType(type);
|
|
|
|
return state.types[type]?.haveNamespace || null;
|
|
},
|
|
|
|
/**
|
|
* Returns (type: string ) => StorePagination
|
|
*/
|
|
havePage: (state, getters) => (type) => {
|
|
type = getters.normalizeType(type);
|
|
|
|
return state.types[type]?.havePage || null;
|
|
},
|
|
|
|
haveSelector: (state, getters) => (type, selector) => {
|
|
type = getters.normalizeType(type);
|
|
const entry = state.types[type];
|
|
|
|
if ( entry ) {
|
|
return entry.haveSelector[selector] || false;
|
|
}
|
|
|
|
return false;
|
|
},
|
|
|
|
normalizeType: () => (type) => {
|
|
return normalizeType(type);
|
|
},
|
|
|
|
keyFieldForType: () => (type) => {
|
|
return keyFieldFor(type);
|
|
},
|
|
|
|
urlFor,
|
|
|
|
urlOptions: () => (url, opt, schema) => {
|
|
return url;
|
|
},
|
|
|
|
storeName: (state) => {
|
|
return state.config.namespace;
|
|
},
|
|
|
|
defaultModel: () => () => {
|
|
return undefined;
|
|
},
|
|
|
|
classify: (state, getters, rootState) => (obj) => {
|
|
return lookup(state.config.namespace, obj?.type, obj?.metadata?.name, rootState) || Resource;
|
|
},
|
|
|
|
cleanResource: () => (existing, data) => {
|
|
return data;
|
|
},
|
|
|
|
isClusterStore: (state) => {
|
|
return state.config.isClusterStore;
|
|
},
|
|
|
|
// Increment the load counter for a resource type
|
|
// This is used for incremental loading do detect when a page changes occur of the a reload happend
|
|
// While a previous incremental loading operation is still in progress
|
|
loadCounter: (state, getters) => (type) => {
|
|
type = getters.normalizeType(type);
|
|
|
|
if (!!state.types[type]) {
|
|
return state.types[type].loadCounter;
|
|
}
|
|
|
|
return 0;
|
|
},
|
|
|
|
gcIgnoreTypes: () => {
|
|
return {};
|
|
},
|
|
|
|
/**
|
|
* For the given type, and it's settings, find the number of resources associated with it
|
|
*
|
|
* This takes into account if the type is namespaced.
|
|
*
|
|
* Used in currently two places
|
|
* - Type
|
|
* - getTree
|
|
*
|
|
* @param typeObj see inners for properties. must have at least `name` (resource type)
|
|
*
|
|
*/
|
|
count: (state, getters, rootState, rootGetters) => (typeObj) => {
|
|
const subTypes = rootGetters['type-map/optionsFor'](typeObj.name).subTypes || [];
|
|
|
|
if (subTypes.length) {
|
|
return subTypes.reduce((acc, type) => acc + resourceCount(rootGetters, getters, { name: type }), 0);
|
|
}
|
|
|
|
return resourceCount(rootGetters, getters, typeObj);
|
|
},
|
|
|
|
generation: (state, getters) => (type) => {
|
|
type = getters.normalizeType(type);
|
|
const entry = state.types[type];
|
|
|
|
if ( entry ) {
|
|
return entry.generation;
|
|
}
|
|
|
|
return undefined;
|
|
},
|
|
|
|
/**
|
|
* Determine if server-side pagination (SSP) is enabled
|
|
*
|
|
* If args is falsy just check the Feature Flag
|
|
*
|
|
* Otherwise args should reference a resource who's compatibility with SSP will also be checked
|
|
*/
|
|
paginationEnabled: (state, getters, rootState, rootGetters) => (args) => {
|
|
if (!args) {
|
|
return paginationUtils.isSteveCacheEnabled({ rootGetters });
|
|
}
|
|
const id = typeof args === 'object' ? args.id : args;
|
|
const context = typeof args === 'object' ? args.context : undefined;
|
|
|
|
const store = state.config.namespace;
|
|
const resource = id || context ? { id, context } : null;
|
|
|
|
return paginationUtils.isEnabled({ rootGetters, $plugin: rootState.$plugin }, { store, resource });
|
|
},
|
|
|
|
/**
|
|
* Is the url path a rancher steve one?
|
|
*
|
|
* Can be used to change behaviour given steve api
|
|
*/
|
|
isSteveUrl: (state) => () => false,
|
|
|
|
/**
|
|
* 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) => () => false,
|
|
};
|