// This file has 2 sections that control getting and using specializations of particular types // over the generic info that is specified or generated from schemas. // // 1) Getting info about types // // labelFor(schema, count) Get the display label for a schema. Count is (in English) 1 or not-1 for pluralizing // groupLabelFor(schema) Get the label for the API group of this schema's type // isIgnored(schema) Returns true if this type should be hidden from the tree // groupForBasicType(schema) Returns the group a type should be shown in basic view, or false-y if it shouldn't be shown. // typeWeightFor(type, forBasic) Get the weight value for a particular type label // groupWeightFor(group, forBasic) Get the weight value for a particular group // headersFor(schema) Returns the column definitions for a type to give to SortableTable // activeProducts() Returns the list of products that are installed and should be shown // // 2) Detecting and using custom list/detail/edit/header components // // hasCustomList(type) Does type have a custom list implementation? // hasCustomDetail(type[,subType]) Does type have a custom detail implementation? // hasCustomEdit(type[,subType]) Does type have a custom edit implementation? // importList(type) Returns a promise that resolves to the list component for type // importDetail(type[,subType]) Returns a promise that resolves to the detail component for type // importEdit(type[,subType]) Returns a promise that resolves to the edit component for type // optionsFor(schemaOrType) Return the configured options for a type (from configureType) // // 3) Changing specialization info about a type // For all: // let { thingYouWant } = DSL(instanceOfTheStore, 'product'); // // product( Add a product into the nav // removable, -- Is the product removable (true) or built-in (false). // weight, -- Sort order and divider sections in the product menu. 3=global (fleet, ecm), 2=always on (apps, explorer) 1=other // showClusterSwitcher, -- Show the cluster switcher in the header (default true) // showNamespaceFilter, -- Show the namespace filter in the header (default false) // showWorkspaceSwitcher, -- Show the workspace switcher in the header (conflicts with namespace) (default false) // ifHave, -- Show this product only if the given capability is available // ifHaveGroup, -- Show this product only if the given group exists in the store [inStore] // ifHaveType, -- Show this product only if the given type exists in the store [inStore], This can also be specified as an object { type: TYPE, store: 'management' } if the type isn't in the current [inStore] // ifHaveVerb, -- In combination with ifHaveTYpe, show it only if the type also has this collectionMethod // inStore, -- Which store to look at for if* above and the left-nav, defaults to "cluster" // public, -- If true, show to all users. If false, only show when the Developer Tools pref is on (default true) // category, -- Group to show the product in for the nav hamburger menu // typeStoreMap, -- An object mapping types to the store that should be used to retrieve information about the type // }) // // externalLink(stringOrFn) The product has an external page (function gets context object // // virtualType(obj) Add an item to the tree that goes to a route instead of an actual type. // -- obj can contain anything in the objects getTree returns. // -- obj must have a `name` that is unique among all virtual types. // -- `cluster` is automatically added to route.params if it exists. // // spoofedType(obj) Create a fake type that can be treated like a normal type // // basicType( Mark type(s) as always shown in the top of the nav // type(s), -- Type name or array of type names // group -- Group to show the type(s) under; false-y for top-level. // ) // basicType( Mark all types in group as always shown in the top of the nav // group, -- Group to show // asLabel -- Label to display the group as; false-y for top-level. // ) // ignoreType(type) Never show type // weightType( Set the weight (sorting) order of one or more types // typeOrArrayOfTypes, // weight, -- Higher numbers are shown first/higher up on the nav tree // forBasic -- Apply to basic type instead of regular type tree // ) // mapType( Remap a type id to a display name // matchRegexOrString, -- Type to match, or regex that matches types // replacementStringOrFn, -- String to replace the type with, or // -- sync function: (type, capturedString, schemaObj => { return 'new-type'; } // mapWeight, -- Priority for apply this mapping (higher numbers applied first) // continueOnMatch -- If true, continue applying to hit other rules that might match the new type. // ) // moveType( Change the group a type is in // matchRegexOrString, -- Type to match, or regex that matches types // newGroup, -- Group to move the type into // mapWeight, -- Priority for apply this mapping (higher numbers applied first) // ) // componentForType( Map matching types to a single component name // ( (this is helpful if multiple types should be rendered by a single component) // matchRegexOrString, -- Type to match, or regex that matches types // replacementString -- String to replace the type with // ) // configureType( Display options for a particular type // type, -- Type to apply to // options -- Object of options. Defaults/Supported: { // isCreatable: true, -- If false, disable create even if schema says it's allowed // isEditable: true, -- Ditto, for edit // isRemovable: true, -- Ditto, for remove/delete // showState: true, -- If false, hide state in columns and masthead // showAge: true, -- If false, hide age in columns and masthead // canYaml: true, // resource: undefined -- Use this resource in ResourceDetails instead // resourceDetail: undefined -- Use this resource specifically for ResourceDetail's detail component // resourceEdit: undefined -- Use this resource specifically for ResourceDetail's edit component // } // ) // ignoreGroup(group): Never show group or any types in it // weightGroup( Set the weight (sorting) of one or more groups // groupOrArrayOfGroups, -- see weightType... // weight // ) // setGroupDefaultType( Set the default child type to show when the group is expanded // groupOrArrayOfGroups, -- see setGroupDefaultType... // defaultType // ) // mapGroup( Remap a group name to a display name // matchRegexOrString, -- see mapType... // replacementString, // mapWeight, // continueOnMatch // ) import { AGE, NAME, NAMESPACE, STATE } from '@/config/table-headers'; import { COUNT, SCHEMA, MANAGEMENT } from '@/config/types'; import { DEV, EXPANDED_GROUPS, FAVORITE_TYPES } from '@/store/prefs'; import { addObject, findBy, insertAt, isArray, removeObject, filterBy } from '@/utils/array'; import { clone, get } from '@/utils/object'; import { ensureRegex, escapeHtml, escapeRegex, ucFirst, pluralize } from '@/utils/string'; import { importList, importDetail, importEdit, loadProduct, importCustomPromptRemove } from '@/utils/dynamic-importer'; import { NAME as EXPLORER } from '@/config/product/explorer'; import isObject from 'lodash/isObject'; import { normalizeType } from '@/plugins/steve/normalize'; import { sortBy } from '@/utils/sort'; import { haveV1Monitoring, haveV2Monitoring } from '@/utils/monitoring'; export const NAMESPACED = 'namespaced'; export const CLUSTER_LEVEL = 'cluster'; export const BOTH = 'both'; export const ALL = 'all'; export const BASIC = 'basic'; export const FAVORITE = 'favorite'; export const USED = 'used'; export const ROOT = 'root'; export const SPOOFED_PREFIX = '__[[spoofed]]__'; export const SPOOFED_API_PREFIX = '__[[spoofedapi]]__'; const instanceMethods = {}; export const IF_HAVE = { V1_MONITORING: 'v1-monitoring', V2_MONITORING: 'v2-monitoring', PROJECT: 'project', NO_PROJECT: 'no-project', NOT_V1_ISTIO: 'not-v1-istio', MULTI_CLUSTER: 'multi-cluster', HARVESTER_SINGLE_CLUSTER: 'harv-multi-cluster', }; export function DSL(store, product, module = 'type-map') { // store.commit(`${ module }/product`, { name: product }); return { product(inOpt) { const opt = { name: product, weight: 1, inStore: 'cluster', removable: true, showClusterSwitcher: true, showNamespaceFilter: false, public: true, filterMode: 'namespaces', ...inOpt }; for ( const k of ['ifHaveGroup', 'ifHaveType'] ) { if ( opt[k] ) { opt[k] = regexToString(ensureRegex(opt[k])); } } store.commit(`${ module }/product`, opt); }, basicType(types, group) { // Support passing in a map of types and using just the values if ( !isArray(types) && types && isObject(types) ) { types = Object.values(types); } store.commit(`${ module }/basicType`, { product, types, group }); }, // Type- and Group-dependent groupBy(type, field) { store.commit(`${ module }/groupBy`, { type, field }); }, headers(type, headers) { store.commit(`${ module }/headers`, { type, headers }); }, hideBulkActions(type, field) { store.commit(`${ module }/hideBulkActions`, { type, field }); }, configureType(match, options) { store.commit(`${ module }/configureType`, { ...options, match }); }, componentForType(match, replace) { store.commit(`${ module }/componentForType`, { match, replace }); }, ignoreType(regexOrString) { store.commit(`${ module }/ignoreType`, regexOrString); }, ignoreGroup(regexOrString) { store.commit(`${ module }/ignoreGroup`, regexOrString); }, weightGroup(input, weight, forBasic) { if ( isArray(input) ) { store.commit(`${ module }/weightGroup`, { groups: input, weight, forBasic }); } else { store.commit(`${ module }/weightGroup`, { group: input, weight, forBasic }); } }, setGroupDefaultType(input, defaultType) { if ( isArray(input) ) { store.commit(`${ module }/setGroupDefaultType`, { groups: input, defaultType }); } else { store.commit(`${ module }/setGroupDefaultType`, { group: input, defaultType }); } }, weightType(input, weight, forBasic) { if ( isArray(input) ) { store.commit(`${ module }/weightType`, { types: input, weight, forBasic }); } else { store.commit(`${ module }/weightType`, { type: input, weight, forBasic }); } }, mapGroup(match, replace, weight = 5, continueOnMatch = false) { store.commit(`${ module }/mapGroup`, { match, replace, weight, continueOnMatch }); }, mapType(match, replace, weight = 5, continueOnMatch = false) { store.commit(`${ module }/mapType`, { match, replace, weight, continueOnMatch }); }, moveType(match, group, weight = 5, continueOnMatch = false) { store.commit(`${ module }/moveType`, { match, group, weight, }); }, virtualType(obj) { store.commit(`${ module }/virtualType`, { product, obj }); }, spoofedType(obj) { store.commit(`${ module }/spoofedType`, { product, obj }); } }; } let called = false; export async function applyProducts(store) { if (called) { return; } called = true; const ctx = require.context('@/config/product', true, /.*/); const products = ctx.keys().filter(path => !path.endsWith('.js')).map(path => path.substr(2)); for ( const product of products ) { const impl = await loadProduct(product); if ( impl?.init ) { impl.init(store); } } } export const state = function() { return { products: [], virtualTypes: {}, spoofedTypes: {}, basicTypes: {}, groupIgnore: [], groupWeights: {}, groupDefaultTypes: {}, basicGroupWeights: { [ROOT]: 1000 }, groupMappings: [], typeIgnore: [], basicTypeWeights: {}, typeWeights: {}, typeMappings: [], typeMoveMappings: [], typeToComponentMappings: [], typeOptions: [], groupBy: {}, headers: {}, hideBulkActions: {}, schemaGeneration: 1, cache: { typeMove: {}, groupLabel: {}, ignore: {}, list: {}, detail: {}, edit: {}, componentFor: {}, promptRemove: {}, }, }; }; export const getters = { // ---------------------------------------------------------------------------- // 1 ) Getting info // ---------------------------------------------------------------------------- // Turns a type name into a display label (e.g. management.cattle.io.cluster -> Cluster) labelFor(state, getters, rootState, rootGetters) { return (schema, count = 1) => { return _applyMapping(schema, state.typeMappings, 'id', false, () => { const key = `typeLabel."${ schema.id.toLowerCase() }"`; if ( rootGetters['i18n/exists'](key) ) { return rootGetters['i18n/t'](key, { count }).trim(); } const out = schema?.attributes?.kind || schema.id || '?'; // Add spaces, but breaks typing names into jump menu naturally // out = ucFirst(out.replace(/([a-z])([A-Z])/g,'$1 $2')); if ( count === 1 ) { return out; } // This works for most things... if you don't like it, put in a typeLabel translation for above. return pluralize(out); }); }; }, // Turns a group name into a display label (e.g. management.cattle.io.v3.cluster -> Cluster) groupLabelFor(state) { return (schemaOrName) => { let group = schemaOrName; if ( typeof schemaOrName === 'object' ) { let moved = false; for ( const rule of state.typeMoveMappings ) { const re = stringToRegex(rule.match); if ( schemaOrName.id.match(re) ) { moved = true; group = rule.replace; } } if ( !moved ) { group = group.attributes.group; } } if ( typeof group !== 'string' ) { return null; } const out = _applyMapping(group, state.groupMappings, null, state.cache.groupLabel, (group) => { const match = group.match(/^(.*)\.k8s\.io$/); if ( match ) { return match[1].split(/\./).map(x => ucFirst(x)).join('.'); } return group; }); return out; }; }, groupForBasicType(state) { return (product, schemaId) => { return state.basicTypes?.[product]?.[schemaId]; }; }, optionsFor(state) { const def = { isCreatable: true, isEditable: true, isRemovable: true, showState: true, showAge: true, canYaml: true, namespaced: null, }; return (schemaOrType) => { if (!schemaOrType) { return {}; } const type = (typeof schemaOrType === 'object' ? schemaOrType.id : schemaOrType); const found = state.typeOptions.find((entry) => { const re = stringToRegex(entry.match); return re.test(type); }); const opts = Object.assign({}, def, found || {}); return opts; }; }, isFavorite(state, getters, rootState, rootGetters) { return (schemaId) => { return rootGetters['prefs/get'](FAVORITE_TYPES).includes(schemaId) || false; }; }, typeWeightFor(state) { return (type, forBasic) => { type = type?.toLowerCase(); if ( forBasic ) { return state.basicTypeWeights[type] || 0; } else { return state.typeWeights[type] || 0; } }; }, groupWeightFor(state) { return (group, forBasic) => { group = group.toLowerCase(); if ( forBasic ) { return state.basicGroupWeights[group] || 0; } else { return state.groupWeights[group] || 0; } }; }, groupDefaultTypeFor(state) { return (group) => { group = group.toLowerCase(); return state.groupDefaultTypes[group]; }; }, getTree(state, getters, rootState, rootGetters) { return (productId, mode, allTypes, clusterId, namespaceMode, namespaces, currentType, search) => { // modes: basic, used, all, favorite // namespaceMode: 'namespaced', 'cluster', or 'both' // namespaces: null means all, otherwise it will be an array of specific namespaces to include const isBasic = mode === BASIC; let searchRegex; if ( search ) { searchRegex = new RegExp(`^(.*)(${ escapeRegex(search) })(.*)$`, 'i'); } const root = { children: [] }; // Add types from shortest to longest so that parents // get added before children const keys = Object.keys(allTypes).sort((a, b) => a.length - b.length); for ( const type of keys ) { const typeObj = allTypes[type]; if ( typeObj.schema && getters.isIgnored(typeObj.schema) ) { // Skip ignored groups & types continue; } const namespaced = typeObj.namespaced; if ( (namespaceMode === NAMESPACED && !namespaced ) || (namespaceMode === CLUSTER_LEVEL && namespaced) ) { // Skip types that are not the right namespace mode continue; } const count = _matchingCounts(typeObj, namespaces); const groupForBasicType = getters.groupForBasicType(productId, typeObj.name); if ( typeObj.id === currentType ) { // If this is the type currently being shown, always show it } else if ( isBasic && !groupForBasicType ) { // If we want the basic tree only return basic types; continue; } else if ( mode === USED && count <= 0 ) { // If there's none of this type, ignore this entry when viewing only in-use types // Note: count is sometimes null, which is <= 0. continue; } const label = typeObj.labelKey ? rootGetters['i18n/t'](typeObj.labelKey) || typeObj.label : typeObj.label; const virtual = !!typeObj.virtual; let icon = typeObj.icon; if ( (!virtual || typeObj.isSpoofed ) && !icon ) { if ( namespaced ) { icon = 'folder'; } else { icon = 'globe'; } } const labelDisplay = highlightLabel(label, icon); if ( !labelDisplay ) { // Search happens in highlight and returns null if not found continue; } let group; if ( isBasic ) { group = _ensureGroup(root, groupForBasicType, true); } else if ( mode === FAVORITE ) { group = _ensureGroup(root, 'starred'); group.weight = 1000; } else if ( mode === USED ) { group = _ensureGroup(root, `inUse::${ getters.groupLabelFor(typeObj.schema) }`); } else { group = _ensureGroup(root, typeObj.schema || typeObj.group || ROOT); } let route = typeObj.route; // Make the default route if one isn't set if (!route ) { route = { name: 'c-cluster-product-resource', params: { product: productId, cluster: clusterId, resource: typeObj.name, } }; typeObj.route = route; } // Cluster ID and Product should always be set if ( route && typeof route === 'object' ) { route.params = route.params || {}; route.params.cluster = clusterId; route.params.product = productId; } group.children.push({ label, labelDisplay, mode: typeObj.mode, count, exact: typeObj.exact || false, namespaced, route, name: typeObj.name, weight: typeObj.weight || getters.typeWeightFor(typeObj.schema?.id || label, isBasic), overview: !!typeObj.overview, }); } // Recursively sort the groups _sortGroup(root, mode); return root.children; // ---------------------- function _ensureGroup(tree, schemaOrName, forBasic = false) { let name = getters.groupLabelFor(schemaOrName); const isRoot = ( name === ROOT || name.startsWith(`${ ROOT }::`) ); if ( name && name.includes('::') ) { let parent; [parent, name] = name.split('::', 2); tree = _ensureGroup(tree, parent); } // Translate if an entry exists let label = name; const key = `nav.group."${ name }"`; if ( rootGetters['i18n/exists'](key) ) { label = rootGetters['i18n/t'](key); } let group = findBy(tree.children, 'name', name); if ( !group ) { group = { name, label, weight: getters.groupWeightFor(name, forBasic), defaultType: getters.groupDefaultTypeFor(name), }; tree.children.push(group); } if ( isRoot ) { group.isRoot = true; } if ( !group.children ) { group.children = []; } return group; } function highlightLabel(original, icon) { let label = escapeHtml(original); if ( searchRegex ) { const match = label.match(searchRegex); if ( match ) { label = `${ escapeHtml(match[1]) }${ escapeHtml(match[2]) }${ escapeHtml(match[3]) }`; } else { return null; } } if ( icon ) { label = `${ label }`; } return label; } }; }, isSpoofed(state, getters, rootState, rootGetters) { return (type, product) => { product = product || rootGetters['productId']; const productSpoofedTypes = state.spoofedTypes[product] || []; return productSpoofedTypes.some(st => st.type === type); }; }, getSpoofedInstances(state, getters, rootState, rootGetters) { return async(type, product) => { product = product || rootGetters['productId']; const getInstances = instanceMethods[product]?.[type] || (() => []); const instances = await getInstances(); instances.forEach((instance) => { const type = instance.type; const id = instance.id; const link = `/${ SPOOFED_PREFIX }/${ type }/${ id }`; const apiLink = `/${ SPOOFED_API_PREFIX }/${ type }/${ id }`; instance.links = { remove: instance.links?.remove || link, self: instance.links?.self || link, update: instance.links?.update || link, view: instance.links?.view || apiLink, }; instance.isSpoofed = true; }); return instances; }; }, getSpoofedInstance(state, getters, rootState, rootGetters) { return async(type, id, product) => { const productInstances = await getters.getSpoofedInstances(type, product); return productInstances.find( instance => instance.id === id); }; }, allSpoofedTypes(state, getters, rootState, rootGetters) { return Object.values(state.spoofedTypes).flat(); }, allSpoofedSchemas(state, getters, rootState, rootGetters) { return getters.allSpoofedTypes.flatMap((type) => { const schemas = type.schemas || []; return schemas.map(schema => ({ ...schema, isSpoofed: true })); }); }, allTypes(state, getters, rootState, rootGetters) { return (product, mode = ALL) => { const module = findBy(state.products, 'name', product).inStore; const schemas = rootGetters[`${ module }/all`](SCHEMA); const counts = rootGetters[`${ module }/all`](COUNT)?.[0]?.counts || {}; const isDev = rootGetters['prefs/get'](DEV); const isBasic = mode === BASIC; const out = {}; for ( const schema of schemas ) { const attrs = schema.attributes || {}; const count = counts[schema.id]; const label = getters.labelFor(schema, count); const weight = getters.typeWeightFor(schema?.id || label, isBasic); const typeOptions = getters['optionsFor'](schema); if ( isBasic ) { // These are separate ifs so that things with no kind can still be basic if ( !getters.groupForBasicType(product, schema.id) ) { continue; } } else if ( mode === FAVORITE && !getters.isFavorite(schema.id) ) { continue; } else if ( !attrs.kind ) { // Skip the schemas that aren't top-level types continue; } else if ( typeof typeOptions.ifRancherCluster !== 'undefined' && typeOptions.ifRancherCluster !== rootGetters.isRancher ) { continue; } out[schema.id] = { label, mode, weight, schema, name: schema.id, namespaced: typeOptions.namespaced === null ? attrs.namespaced : typeOptions.namespaced, count: count ? count.summary.count || 0 : null, byNamespace: count ? count.namespaces : {}, revision: count ? count.revision : null, }; } // Add virtual and spoofed types if ( mode !== USED ) { const virtualTypes = state.virtualTypes[product] || []; const spoofedTypes = state.spoofedTypes[product] || []; const allTypes = [...virtualTypes, ...spoofedTypes]; for ( const type of allTypes ) { const item = clone(type); const id = item.name; const weight = type.weight || getters.typeWeightFor(item.label, isBasic); // Is there a virtual/spoofed type override for schema type? // Currently used by harvester, this should be investigated and removed if possible if (out[id]) { delete out[id]; } if ( item['public'] === false && !isDev ) { continue; } if (item.ifHave && !ifHave(rootGetters, item.ifHave)) { continue; } if ( item.ifHaveType ) { const targetedSchemas = typeof item.ifHaveType === 'string' ? schemas : rootGetters[`${ item.ifHaveType.store }/all`](SCHEMA); const type = typeof item.ifHaveType === 'string' ? item.ifHaveType : item.ifHaveType?.type; const haveIds = filterBy(targetedSchemas, 'id', normalizeType(type)).map(s => s.id); if (!haveIds.length) { continue; } if (item.ifHaveVerb && !ifHaveVerb(rootGetters, module, item.ifHaveVerb, haveIds)) { continue; } } if ( item.ifHaveSubTypes ) { const hasSome = (item.ifHaveSubTypes || []).some((type) => { return !!findBy(schemas, 'id', normalizeType(type)); }); if (!hasSome) { continue; } } if ( typeof item.ifRancherCluster !== 'undefined' && item.ifRancherCluster !== rootGetters.isRancher ) { continue; } if ( isBasic && !getters.groupForBasicType(product, id) ) { continue; } else if ( mode === FAVORITE && !getters.isFavorite(id) ) { continue; } item.mode = mode; item.weight = weight; item.label = item.label || item.name; out[id] = item; } } return out; }; }, groupByFor(state) { return (schema) => { return state.groupBy[schema.id]; }; }, hideBulkActionsFor(state) { return (schema) => { return state.hideBulkActions[schema.id]; }; }, headersFor(state, getters, rootState, rootGetters) { return (schema) => { const attributes = schema.attributes || {}; const columns = attributes.columns || []; const typeOptions = getters['optionsFor'](schema); // A specific list has been provided if ( state.headers[schema.id] ) { return state.headers[schema.id].map((entry) => { if ( typeof entry === 'string' ) { const col = findBy(columns, 'name', entry); if ( col ) { return fromSchema(col, rootGetters); } else { return null; } } else { return entry; } }).filter(col => !!col); } // Otherwise make one up from schema const out = typeOptions.showState ? [STATE] : []; const namespaced = attributes.namespaced || false; let hasName = false; for ( const col of columns ) { if ( col.format === 'name' ) { hasName = true; out.push(NAME); if ( namespaced ) { out.push(NAMESPACE); } } else { out.push(fromSchema(col, rootGetters)); } } if ( !hasName ) { insertAt(out, 1, NAME); if ( namespaced ) { insertAt(out, 2, NAMESPACE); } } // Age always goes last if ( out.includes(AGE) ) { removeObject(out, AGE); if ( typeOptions.showAge ) { out.push(AGE); } } return out; function fromSchema(col, rootGetters) { let formatter, width, formatterOpts; if ( (col.format === '' || col.format === 'date') && col.name === 'Age' ) { return AGE; } if ( col.format === 'date' || col.type === 'date' ) { formatter = 'Date'; width = 120; formatterOpts = { multiline: true }; } if ( col.type === 'number' || col.type === 'int' ) { formatter = 'Number'; } const exists = rootGetters['i18n/exists']; const t = rootGetters['i18n/t']; const labelKey = `tableHeaders.${ col.name }`; const description = col.description || ''; const tooltip = description && description[description.length - 1] === '.' ? description.slice(0, -1) : description; return { name: col.name.toLowerCase(), label: exists(labelKey) ? t(labelKey) : col.name, value: col.field.startsWith('.') ? `$${ col.field }` : col.field, sort: [col.field], formatter, formatterOpts, width, tooltip }; } }; }, // ------------------------------------ // Custom list/detail/edit/header component detection // // Note: you can't refactor these into one function that does `@/${kind}/${type}`, // because babel needs some hardcoded idea where to look for the dependency. // // Note 2: Yes these are editing state in a getter for caching... it's ok, probably. // ------------------------------------ hasCustomList(state, getters) { return (rawType) => { const type = getters.componentFor(rawType); const cache = state.cache.list; if ( cache[type] !== undefined ) { return cache[type]; } try { require.resolve(`@/list/${ type }`); cache[type] = true; } catch (e) { cache[type] = false; } return cache[type]; }; }, hasCustomDetail(state, getters) { return (rawType, subType) => { const key = getters.componentFor(rawType, subType); const cache = state.cache.detail; if ( cache[key] !== undefined ) { return cache[key]; } try { require.resolve(`@/detail/${ key }`); cache[key] = true; } catch (e) { cache[key] = false; } return cache[key]; }; }, hasCustomEdit(state, getters) { return (rawType, subType) => { const key = getters.componentFor(rawType, subType); const cache = state.cache.edit; if ( cache[key] !== undefined ) { return cache[key]; } try { require.resolve(`@/edit/${ key }`); cache[key] = true; } catch (e) { cache[key] = false; } return cache[key]; }; }, hasComponent(state, getters) { return (path) => { try { require.resolve(`@/edit/${ path }`); return true; } catch (e) { return false; } }; }, hasCustomPromptRemove(state, getters) { return (rawType) => { const type = getters.componentFor(rawType); const cache = state.cache.promptRemove; if ( cache[type] !== undefined ) { return cache[type]; } try { require.resolve(`@/promptRemove/${ type }`); cache[type] = true; } catch (e) { cache[type] = false; } return cache[type]; }; }, importComponent(state, getters) { return (path) => { return importEdit(path); }; }, importList(state, getters) { return (rawType) => { const type = getters.componentFor(rawType); return importList(type); }; }, importDetail(state, getters) { return (rawType, subType) => { const key = getters.componentFor(rawType, subType); return importDetail(key); }; }, importEdit(state, getters) { return (rawType, subType) => { const key = getters.componentFor(rawType, subType); return importEdit(key); }; }, importCustomPromptRemove(state, getters) { return (rawType) => { const type = getters.componentFor(rawType); return importCustomPromptRemove(type); }; }, componentFor(state, getters) { return (type, subType) => { let key = type; if ( subType ) { key = `${ type }/${ subType }`; } if ( state.cache.componentFor[key] !== undefined ) { return state.cache.componentFor[key]; } let out = type; const mapping = state.typeToComponentMappings.find((mapping) => { const re = stringToRegex(mapping.match); return re.test(key); }); if ( mapping ) { out = mapping.replace; } else if ( subType ) { // Try again without the subType out = getters.componentFor(type); } state.cache.componentFor[key] = out; return out; }; }, isIgnored(state) { return (schema) => { if ( state.cache.ignore[schema.id] !== undefined ) { return state.cache.ignore[schema.id]; } let out = false; for ( const rule of state.groupIgnore ) { const group = schema?.attributes?.group; if ( group && group.match(stringToRegex(rule)) ) { out = true; break; } } if ( !out ) { for ( const rule of state.typeIgnore ) { if ( schema.id.match(stringToRegex(rule)) ) { out = true; break; } } } state.cache.ignore[schema.id] = out; return out; }; }, activeProducts(state, getters, rootState, rootGetters) { const knownTypes = {}; const knownGroups = {}; const isDev = rootGetters['prefs/get'](DEV); if ( state.schemaGeneration < 0 ) { // This does nothing, but makes activeProducts depend on schemaGeneration // so that it can be used to update the product list on schema change. return; } return state.products.filter((p) => { const module = p.inStore; if ( p['public'] === false && !isDev ) { return false; } if ( p.ifGetter && !rootGetters[p.ifGetter] ) { return false; } if ( !knownTypes[module] ) { const schemas = rootGetters[`${ module }/all`](SCHEMA); knownTypes[module] = []; knownGroups[module] = []; for ( const s of schemas ) { knownTypes[module].push(s._id); if ( s._group ) { addObject(knownGroups[module], s._group); } } } if ( p.ifFeature) { const features = Array.isArray(p.ifFeature) ? p.ifFeature : [p.ifFeature]; for (const f of features) { if (!rootGetters['features/get'](f)) { return false; } } } if ( p.ifHave && !ifHave(rootGetters, p.ifHave)) { return false; } if ( p.ifHaveType ) { const haveIds = knownTypes[module].filter(t => t.match(stringToRegex(p.ifHaveType)) ); if ( !haveIds.length ) { return false; } if ( p.ifHaveVerb && !ifHaveVerb(rootGetters, module, p.ifHaveVerb, haveIds)) { return false; } } if ( p.ifHaveGroup && !knownGroups[module].find(t => t.match(stringToRegex(p.ifHaveGroup)) ) ) { return false; } return true; }); }, isProductActive(state, getters) { return (name) => { if ( findBy(getters['activeProducts'], 'name', name) ) { return true; } return false; }; }, }; export const mutations = { schemaChanged(state) { state.schemaGeneration = state.schemaGeneration + 1; }, product(state, obj) { const existing = findBy(state.products, 'name', obj.name); if ( existing ) { Object.assign(existing, obj); } else { addObject(state.products, obj); } }, virtualType(state, { product, obj }) { if ( !state.virtualTypes[product] ) { state.virtualTypes[product] = []; } const copy = clone(obj); copy.virtual = true; const existing = findBy(state.virtualTypes[product], 'name', copy.name); if ( existing ) { Object.assign(existing, copy); } else { addObject(state.virtualTypes[product], copy); } }, spoofedType(state, { product, obj }) { if ( !state.spoofedTypes[product] ) { state.spoofedTypes[product] = []; } const copy = clone(obj); instanceMethods[product] = instanceMethods[product] || {}; instanceMethods[product][copy.type] = copy.getInstances; delete copy.getInstances; copy.name = copy.type; copy.isSpoofed = true; copy.virtual = true; copy.schemas.forEach((schema) => { schema.links = { collection: `/${ SPOOFED_PREFIX }/${ schema.id }`, ...(schema.links || {}) }; }); const existing = findBy(state.spoofedTypes[product], 'type', copy.type); if ( existing ) { Object.assign(existing, copy); } else { addObject(state.spoofedTypes[product], copy); } }, basicType(state, { product, group, types }) { if ( !product ) { product = EXPLORER; } if ( !group ) { group = ROOT; } if ( !isArray(types) ) { types = [types]; } if ( !state.basicTypes[product] ) { state.basicTypes[product] = {}; } for ( const t of types ) { state.basicTypes[product][t] = group; } }, ignoreGroup(state, match) { match = ensureRegex(match); // State shouldn't contain actual RegExp objects, because they don't serialize state.groupIgnore.push(regexToString(match)); }, ignoreType(state, match) { match = ensureRegex(match); state.typeIgnore.push(regexToString(match)); }, groupBy(state, { type, field }) { state.groupBy[type] = field; }, headers(state, { type, headers }) { state.headers[type] = headers; }, hideBulkActions(state, { type, field }) { state.hideBulkActions[type] = field; }, // weightGroup({group: 'core', weight: 99}); -- higher groups are shown first // These operate on group names *after* mapping but *before* translation weightGroup(state, { group, groups, weight, forBasic }) { if ( !groups ) { groups = []; } if ( group ) { groups.push(group); } const map = forBasic ? state.basicGroupWeights : state.groupWeights; for ( const g of groups ) { map[g.toLowerCase()] = weight; } }, // setGroupDefaultType({group: 'core', defaultType: 'name'}); // By default when a group is clicked, the first item is selected - this allows // this behaviour to be changed and a named child type can be chosen // These operate on group names *after* mapping but *before* translation setGroupDefaultType(state, { group, groups, defaultType }) { if ( !groups ) { groups = []; } if ( group ) { groups.push(group); } for ( const g of groups ) { state.groupDefaultTypes[g.toLowerCase()] = defaultType; } }, // weightType('Cluster' 99); -- higher groups are shown first // These operate on *schema* type names, before mapping weightType(state, { type, types, weight, forBasic }) { if ( !types ) { types = []; } if ( type ) { types.push(type); } const map = forBasic ? state.basicTypeWeights : state.typeWeights; for ( const t of types ) { map[t.toLowerCase()] = weight; } }, // mapGroup('ugly.thing', 'Nice Thing', 1); // mapGroup(/ugly.thing.(stuff)', '$1', 2); // mapGroup(/ugly.thing.(stuff)', function(groupStr, ruleObj, regexMatch, typeObj) { return ucFirst(group.id) } , 2); mapGroup(state, { match, replace, weight = 5, continueOnMatch = false }) { _addMapping(state.groupMappings, match, replace, weight, continueOnMatch); }, mapType(state, { match, replace, weight = 5, continueOnMatch = false }) { _addMapping(state.typeMappings, match, replace, weight, continueOnMatch); }, // moveType('apps.deployment', 'Core'); moveType(state, { match, group, weight = 5 }) { _addMapping(state.typeMoveMappings, match, group, weight); }, componentForType(state, { match, replace }) { match = ensureRegex(match); match = regexToString(match); state.typeToComponentMappings.push({ match, replace }); }, configureType(state, options) { const match = regexToString(ensureRegex(options.match)); const idx = state.typeOptions.findIndex(obj => obj.match === match); let obj = { ...options, match }; if ( idx >= 0 ) { obj = Object.assign(obj, state.typeOptions[idx]); state.typeOptions.splice(idx, 1, obj); } else { const obj = Object.assign({}, options, { match }); state.typeOptions.push(obj); } }, }; export const actions = { addFavorite({ dispatch, rootGetters }, type) { const types = rootGetters['prefs/get'](FAVORITE_TYPES) || []; addObject(types, type); dispatch('prefs/set', { key: FAVORITE_TYPES, value: types }, { root: true }); }, removeFavorite({ dispatch, rootGetters }, type) { const types = rootGetters['prefs/get'](FAVORITE_TYPES) || []; removeObject(types, type); dispatch('prefs/set', { key: FAVORITE_TYPES, value: types }, { root: true }); }, toggleGroup({ dispatch, rootGetters }, { group, expanded }) { const groups = rootGetters['prefs/get'](EXPANDED_GROUPS); if ( expanded ) { addObject(groups, group); } else { removeObject(groups, group); } dispatch('prefs/set', { key: EXPANDED_GROUPS, value: groups }, { root: true }); }, configureType({ commit }, options) { commit('configureType', options); } }; function _sortGroup(tree, mode) { const by = ['weight:desc', 'namespaced', 'label']; tree.children = sortBy(tree.children, by); for (const entry of tree.children ) { if ( entry.children ) { _sortGroup(entry, mode); } } } 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; } function _applyMapping(objOrValue, mappings, keyField, cache, defaultFn) { let key = objOrValue; let found = false; if ( keyField ) { if ( typeof objOrValue !== 'object' ) { return objOrValue; } key = get(objOrValue, keyField); if ( typeof key !== 'string' ) { return null; } } if ( key && cache && cache[key] ) { return cache[key]; } let out = `${ key }`; for ( const rule of mappings ) { const re = stringToRegex(rule.match); const captured = out.match(re); if ( captured && rule.replace ) { out = out.replace(re, rule.replace); found = true; if ( !rule.continueOnMatch ) { break; } } } if ( !found && defaultFn ) { out = defaultFn(out, objOrValue); } if ( cache ) { cache[key] = out; } return out; } function _addMapping(mappings, match, replace, weight, continueOnMatch) { match = regexToString(ensureRegex(match)); mappings.push({ match, replace, weight, continueOnMatch, insertIndex: mappings.length, }); // Re-sort the list by weight (highest first) and insert time (oldest first) mappings.sort((a, b) => { const pri = b.weight - a.weight; if ( pri ) { return pri; } return a.insertIndex - b.insertIndex; }); } // Regexes can't be represented in state because they don't serialize to JSON.. const regexCache = {}; function regexToString(regex) { return regex.source; } function stringToRegex(str) { let out = regexCache[str]; if ( !out ) { out = new RegExp(str); regexCache[str] = out; } return out; } function ifHave(getters, option) { switch (option) { case IF_HAVE.V2_MONITORING: { return haveV2Monitoring(getters); } case IF_HAVE.V1_MONITORING: { return haveV1Monitoring(getters); } case IF_HAVE.PROJECT: { return !!project(getters); } case IF_HAVE.NO_PROJECT: { return !project(getters); } case IF_HAVE.NOT_V1_ISTIO: { return !isV1Istio(getters); } case IF_HAVE.MULTI_CLUSTER: { return getters.isMultiCluster; } case IF_HAVE.HARVESTER_SINGLE_CLUSTER: { return getters.isSingleVirtualCluster; } default: return false; } } // Is V1 Istio installed? function isV1Istio(getters) { const cluster = getters['currentCluster']; return !!cluster?.status?.istioEnabled; } function ifHaveVerb(rootGetters, module, verb, haveIds) { for ( const haveId of haveIds ) { const schema = rootGetters[`${ module }/schemaFor`](haveId); const want = verb.toLowerCase(); const collectionMethods = schema.collectionMethods || []; const resourceMethods = schema.resourceMethods || []; const have = [...collectionMethods, ...resourceMethods].map(x => x.toLowerCase()); if ( !have.includes(want) && !have.includes(`blocked-${ want }`) ) { return false; } } return true; } // Look at the namespace filters to determine if a project is selected export function project(getters) { const clusterId = getters['currentCluster']?.id; if ( !clusterId ) { return null; } const filters = getters['namespaceFilters']; const namespaces = []; let projectName = null; for (const filter of filters) { const [type, id] = filter.split('://', 2); if (type === 'project') { if (projectName !== null) { // More than one project selected return null; } projectName = id; } else if (type === 'ns') { namespaces.push(id); } else { // Something other than project or namespace return null; } } // No project found? if (!projectName) { return null; } // We have one project and a set of namespaces // Check that all of the namespaces belong to the project const project = getters['management/byId'](MANAGEMENT.PROJECT, `${ clusterId }/${ projectName }`); // No additional namespaces means just the project is selected if (namespaces.length === 0) { return project; } // Convert the project namespaces into a map so we can check existence easily const prjNamespaceMap = project.namespaces.reduce((m, ns) => { m[ns.metadata.name] = true; return m; }, {}); // All of the namespace filters must belong to the project const found = namespaces.reduce((total, ns) => { return prjNamespaceMap[ns] ? total + 1 : 0; }, 0); if (found !== namespaces.length) { return null; } return project; }