mirror of https://github.com/rancher/dashboard.git
598 lines
16 KiB
JavaScript
598 lines
16 KiB
JavaScript
import { reactive } from 'vue';
|
|
import { addObject, addObjects, clear, removeObject } from '@shell/utils/array';
|
|
import { SCHEMA, COUNT } from '@shell/config/types';
|
|
import { normalizeType, keyFieldFor } from '@shell/plugins/dashboard-store/normalize';
|
|
import { addSchemaIndexFields } from '@shell/plugins/steve/schema.utils';
|
|
import { classify } from '@shell/plugins/dashboard-store/classify';
|
|
import garbageCollect from '@shell/utils/gc/gc';
|
|
|
|
function registerType(state, type) {
|
|
let cache = state.types[type];
|
|
|
|
if ( !cache ) {
|
|
cache = {
|
|
list: [],
|
|
haveAll: false,
|
|
haveSelector: {},
|
|
/**
|
|
* If the cached list only contains resources for a namespace, this will contain the ns name
|
|
*/
|
|
haveNamespace: undefined,
|
|
/**
|
|
* If the cached list only contains resources from a pagination request, this will contain the pagination settings (`StorePagination`)
|
|
*/
|
|
havePage: undefined,
|
|
/**
|
|
* The highest known resourceVersion from the server for this type
|
|
*/
|
|
revision: 0,
|
|
/**
|
|
* Updated every time something is loaded for this type
|
|
*/
|
|
generation: 0,
|
|
/**
|
|
* Used to cancel incremental loads if the page changes during load
|
|
*/
|
|
loadCounter: 0,
|
|
|
|
// Not enumerable so they don't get sent back to the client for SSR
|
|
map: new Map(),
|
|
};
|
|
|
|
state.types[type] = cache;
|
|
}
|
|
|
|
return cache;
|
|
}
|
|
|
|
export function replace(existing, data) {
|
|
const existingPropertyMap = {};
|
|
|
|
for ( const k of Object.keys(existing) ) {
|
|
delete existing[k];
|
|
existingPropertyMap[k] = true;
|
|
}
|
|
|
|
let newProperty = false;
|
|
|
|
for ( const k of Object.keys(data) ) {
|
|
if (!newProperty && !existingPropertyMap[k]) {
|
|
newProperty = true;
|
|
}
|
|
|
|
existing[k] = data[k];
|
|
}
|
|
|
|
return newProperty ? reactive(existing) : existing;
|
|
}
|
|
|
|
function replaceResource(existing, data, getters) {
|
|
data = getters.cleanResource(existing, data);
|
|
|
|
return replace(existing, data);
|
|
}
|
|
|
|
/**
|
|
* `load` can be called as part of a loop. to avoid common look ups create them up front and pass as `cachedArgs`
|
|
*/
|
|
export function createLoadArgs(ctx, dataType) {
|
|
const { getters } = ctx;
|
|
const type = normalizeType(dataType);
|
|
const keyField = getters.keyFieldForType(type);
|
|
const opts = ctx.rootGetters[`type-map/optionsFor`](type);
|
|
|
|
return {
|
|
type, keyField, opts
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Add or update the given resource in the store
|
|
*
|
|
* invalidatePageCache
|
|
* - if something calls `load` then the cache no longer has a page so we invalidate it
|
|
* - however on resource create or remove this can lead to lists showing nothing... before the new page is fetched
|
|
* - for those cases avoid invaliding the page cache
|
|
*/
|
|
export function load(state, {
|
|
data, ctx, existing, cachedArgs, invalidatePageCache = true,
|
|
}) {
|
|
const { getters } = ctx;
|
|
// Optimisation. This can run once per resource loaded.., so pass in from parent
|
|
const { type: cachedType, keyField, opts } = cachedArgs || createLoadArgs(ctx, data.type);
|
|
let type = cachedType;
|
|
|
|
const limit = opts?.limit;
|
|
|
|
// Inject special fields for indexing schemas
|
|
if ( type === SCHEMA ) {
|
|
addSchemaIndexFields(data);
|
|
}
|
|
|
|
const id = data[keyField];
|
|
|
|
let cache = registerType(state, type);
|
|
|
|
cache.generation++;
|
|
|
|
let entry = cache.map.get(id);
|
|
const inMap = !!entry;
|
|
|
|
//
|
|
// Determine the `entry` that should be in the local map and list cache
|
|
//
|
|
if ( existing && !existing.id ) {
|
|
// A specific proxy instance to use was passed in (for create -> save), use it instead of making a new proxy
|
|
// `existing` is a classified resource created locally that is most probably not in the store (unless a slow connection means it's added by socket before the API responds)
|
|
// Note - `existing` has no `id` because the resource was created locally and not supplied by Rancher API
|
|
|
|
// Get the latest and greatest version of the resource
|
|
const latestEntry = replaceResource(existing, data, getters);
|
|
|
|
if (inMap) {
|
|
// There's already an entry in the store, so merge changes into it. The list entry is a reference to the map (and vice versa)
|
|
entry = replaceResource(entry, latestEntry, getters);
|
|
} else {
|
|
// There's no entry, using existing proxy
|
|
entry = latestEntry;
|
|
}
|
|
} else {
|
|
if (inMap) {
|
|
// In theory cached `entry` should match provided `existing`, so changes merged from `data` into `entry` should be reflected in `existing`.
|
|
// However.. there's a disconnect happening somewhere so merge data into `existing` before merging into `entry`
|
|
const latestEntry = existing && entry !== existing ? replaceResource(existing, data, getters) : data;
|
|
|
|
// There's already an entry in the store, so merge changes into it. The list entry is a reference to the map (and vice versa)
|
|
entry = replaceResource(entry, latestEntry, getters);
|
|
} else {
|
|
// There's no entry, make a new proxy
|
|
entry = reactive(classify(ctx, data));
|
|
}
|
|
}
|
|
|
|
//
|
|
// Ensure the `entry` is in both both list and cache
|
|
// Note - We should be safe assuming the two collections have parity (not in map means not in list)
|
|
//
|
|
if (!inMap) {
|
|
cache.list.push(entry);
|
|
cache.map.set(id, entry);
|
|
}
|
|
|
|
// If there is a limit to the number of resources we can store for this type then
|
|
// remove the first one to keep the list size to that limit
|
|
if (limit && cache.list.length > limit) {
|
|
const rm = cache.list.shift();
|
|
|
|
cache.map.delete(rm.id);
|
|
}
|
|
|
|
if ( data.baseType ) {
|
|
type = normalizeType(data.baseType);
|
|
cache = state.types[type];
|
|
if ( cache ) {
|
|
addObject(cache.list, entry);
|
|
cache.map.set(id, entry);
|
|
}
|
|
}
|
|
|
|
// see `invalidatePageCache` description above
|
|
cache.havePage = invalidatePageCache ? false : cache.havePage;
|
|
|
|
return entry;
|
|
}
|
|
|
|
export function forgetType(state, type) {
|
|
const cache = state.types[type];
|
|
|
|
if ( cache ) {
|
|
cache.haveAll = false;
|
|
cache.haveSelector = {};
|
|
cache.haveNamespace = undefined;
|
|
cache.havePage = undefined;
|
|
cache.revision = 0;
|
|
cache.generation = 0;
|
|
clear(cache.list);
|
|
cache.map.clear();
|
|
delete state.types[type];
|
|
|
|
garbageCollect.gcResetType(state, type);
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
export function resetStore(state, commit) {
|
|
// eslint-disable-next-line no-console
|
|
console.log('Reset store: ', state.config.namespace);
|
|
|
|
for ( const type of Object.keys(state.types) ) {
|
|
commit(`${ state.config.namespace }/forgetType`, type);
|
|
}
|
|
|
|
garbageCollect.gcResetStore(state);
|
|
}
|
|
|
|
export function remove(state, obj, getters) {
|
|
if (obj) {
|
|
let type = normalizeType(obj.type);
|
|
const keyField = getters[`${ state.config.namespace }/keyFieldForType`](type);
|
|
const id = obj[keyField];
|
|
|
|
let entry = state.types[type];
|
|
|
|
if ( entry ) {
|
|
removeObject(entry.list, obj);
|
|
entry.map.delete(id);
|
|
}
|
|
|
|
if ( obj.baseType ) {
|
|
type = normalizeType(obj.baseType);
|
|
entry = state.types[type];
|
|
|
|
if ( entry ) {
|
|
removeObject(entry.list, obj);
|
|
entry.map.delete(id);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
export function batchChanges(state, { ctx, batch }) {
|
|
const batchTypes = Object.keys(batch);
|
|
const combinedBatch = {};
|
|
|
|
batchTypes.forEach((batchType) => {
|
|
combinedBatch[batchType] = batch[batchType];
|
|
const typeOption = ctx.rootGetters['type-map/optionsFor'](batchType);
|
|
|
|
if (typeOption?.alias?.length > 0) {
|
|
const alias = typeOption?.alias || [];
|
|
|
|
alias.forEach((aliasType) => {
|
|
combinedBatch[aliasType] = {};
|
|
for (const [key, value] of Object.entries(batch[batchType])) {
|
|
combinedBatch[aliasType][key] = {
|
|
...value,
|
|
type: aliasType
|
|
};
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
const combinedBatchTypes = Object.keys(combinedBatch);
|
|
|
|
combinedBatchTypes.forEach((type) => {
|
|
const normalizedType = normalizeType(type === 'counts' ? COUNT : type);
|
|
const keyField = keyFieldFor(normalizedType);
|
|
const typeCache = registerType(state, normalizedType);
|
|
|
|
// making a map for every resource's location in the list is gonna ensure we only have to loop through the big list once.
|
|
const typeCacheIndexMap = {};
|
|
|
|
typeCache.list.forEach((resource, index) => {
|
|
typeCacheIndexMap[resource[keyField]] = index;
|
|
});
|
|
|
|
const removeAtIndexes = [];
|
|
|
|
// looping through the batch, executing changes, deferring creates and removes since they change the array length
|
|
Object.keys(combinedBatch[normalizedType]).forEach((id) => {
|
|
const index = typeCacheIndexMap[id];
|
|
const resource = combinedBatch[normalizedType][id];
|
|
|
|
// an empty resource passed into batch changes is how we'll signal which ones to delete
|
|
if (Object.keys(resource).length === 0 && index !== undefined) {
|
|
typeCache.map.delete(id);
|
|
removeAtIndexes.push(index);
|
|
} else if (Object.keys(resource).length === 0) {
|
|
// No op. We're removing it... but we don't have it in the cache
|
|
} else {
|
|
if (normalizedType === SCHEMA) {
|
|
addSchemaIndexFields(resource);
|
|
}
|
|
const classyResource = reactive(classify(ctx, resource));
|
|
|
|
if (index === undefined) {
|
|
typeCache.list.push(classyResource);
|
|
typeCache.map.set(id, classyResource);
|
|
|
|
typeCacheIndexMap[classyResource[keyField]] = typeCache.list.length - 1;
|
|
} else {
|
|
replaceResource(typeCache.list[index], resource, ctx.getters);
|
|
}
|
|
}
|
|
});
|
|
|
|
// looping through the removeAtIndexes, making sure to offset by iteration so the array changing doesn't mess us up
|
|
removeAtIndexes.sort().forEach((cacheIndex, loopIndex) => {
|
|
typeCache.list.splice(cacheIndex - loopIndex, 1);
|
|
});
|
|
|
|
const opts = ctx.rootGetters[`type-map/optionsFor`](type);
|
|
const limit = opts?.limit;
|
|
|
|
// If there is a limit to the number of resources we can store for this type then
|
|
// remove the first one to keep the list size to that limit
|
|
if (limit && typeCache.list.length > limit) {
|
|
const rm = typeCache.list.shift();
|
|
|
|
typeCache.map.delete(rm.id);
|
|
}
|
|
|
|
typeCache.generation++;
|
|
});
|
|
}
|
|
|
|
export function loadAll(state, {
|
|
type,
|
|
data,
|
|
ctx,
|
|
skipHaveAll,
|
|
namespace,
|
|
revision
|
|
}) {
|
|
const { getters } = ctx;
|
|
|
|
if (!data) {
|
|
return;
|
|
}
|
|
|
|
const opts = ctx.rootGetters[`type-map/optionsFor`](type);
|
|
const limit = opts?.limit;
|
|
|
|
// If there is a limit, only store the last elements from the list to keep to that limit
|
|
if (limit) {
|
|
data = data.slice(-limit);
|
|
}
|
|
|
|
const keyField = getters.keyFieldForType(type);
|
|
const proxies = reactive(data.map((x) => classify(ctx, x)));
|
|
const cache = registerType(state, type);
|
|
|
|
clear(cache.list);
|
|
cache.map.clear();
|
|
cache.revision = revision || 0;
|
|
cache.generation++;
|
|
|
|
addObjects(cache.list, proxies);
|
|
|
|
for ( let i = 0 ; i < proxies.length ; i++ ) {
|
|
cache.map.set(proxies[i][keyField], proxies[i]);
|
|
}
|
|
|
|
// Allow requester to skip setting that everything has loaded
|
|
if (!skipHaveAll) {
|
|
if (namespace) {
|
|
cache.havePage = false;
|
|
cache.haveNamespace = namespace;
|
|
cache.haveAll = false;
|
|
} else {
|
|
cache.havePage = false;
|
|
cache.haveNamespace = false;
|
|
cache.haveAll = true;
|
|
}
|
|
}
|
|
|
|
return proxies;
|
|
}
|
|
|
|
/**
|
|
* Add a set of resources to the store for a given type
|
|
*
|
|
* Don't mark the 'haveAll' field - this is used for incremental loading
|
|
*/
|
|
export function loadAdd(state, { type, data: allLatest, ctx }) {
|
|
const { getters } = ctx;
|
|
const keyField = getters.keyFieldForType(type);
|
|
const cachedArgs = createLoadArgs(ctx, allLatest?.[0]?.type);
|
|
|
|
allLatest.forEach((entry) => {
|
|
const existing = state.types[type].map.get(entry[keyField]);
|
|
|
|
load(state, {
|
|
data: entry, ctx, existing, cachedArgs
|
|
});
|
|
});
|
|
}
|
|
|
|
export default {
|
|
registerType,
|
|
load,
|
|
|
|
applyConfig(state, config) {
|
|
if ( !state.config ) {
|
|
state.config = {};
|
|
}
|
|
|
|
Object.assign(state.config, config);
|
|
},
|
|
|
|
/**
|
|
* Load multiple different types of resources
|
|
*/
|
|
loadMulti(state, { data, ctx }) {
|
|
// console.log('### Mutation loadMulti', data?.length);
|
|
|
|
for ( const entry of data ) {
|
|
load(state, { data: entry, ctx });
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Load the results of a request that used a selector (like label)
|
|
*/
|
|
loadSelector(state, {
|
|
type, entries, ctx, selector, revision
|
|
}) {
|
|
const keyField = ctx.getters.keyFieldForType(type);
|
|
const cache = registerType(state, type);
|
|
const proxies = reactive(entries.map((x) => classify(ctx, x)));
|
|
|
|
clear(cache.list);
|
|
cache.map.clear();
|
|
cache.generation++;
|
|
|
|
addObjects(cache.list, proxies);
|
|
|
|
for ( let i = 0 ; i < proxies.length ; i++ ) {
|
|
cache.map.set(proxies[i][keyField], proxies[i]);
|
|
}
|
|
|
|
cache.haveSelector[selector] = true;
|
|
cache.revision = revision || 0;
|
|
},
|
|
|
|
/**
|
|
* Load the results of a request to fetch all resources or all resources in a namespace
|
|
*/
|
|
loadAll,
|
|
|
|
/**
|
|
* Handles changes (add, update, remove) to multiple resources for multiple types
|
|
*/
|
|
batchChanges,
|
|
|
|
loadMerge(state, { type, data: allLatest, ctx }) {
|
|
const { commit, getters } = ctx;
|
|
// const allLatest = await dispatch('findAll', { type, opt: { force: true, load, _NONE } });
|
|
// const allExisting = getters.all({type});
|
|
const keyField = getters.keyFieldForType(type);
|
|
const cache = state.types[type];
|
|
const cachedArgs = createLoadArgs(ctx, allLatest?.[0].type);
|
|
|
|
allLatest.forEach((entry) => {
|
|
const existing = state.types[type].map.get(entry[keyField]);
|
|
|
|
load(state, {
|
|
data: entry, ctx, existing, cachedArgs
|
|
});
|
|
});
|
|
cache.list.forEach((entry) => {
|
|
if (!allLatest.find((toLoadEntry) => toLoadEntry.id === entry.id)) {
|
|
commit('remove', entry);
|
|
}
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Load resources, but don't set `haveAll`
|
|
*/
|
|
loadAdd,
|
|
|
|
/**
|
|
* Load the results of a request for a page. Often used to exercise advanced filtering
|
|
*/
|
|
loadPage(state, {
|
|
type,
|
|
data,
|
|
ctx,
|
|
pagination,
|
|
revision
|
|
}) {
|
|
if (!data) {
|
|
return;
|
|
}
|
|
// We loop over data three times in this mutator, which is bad
|
|
// However we're only dealing with pageSize worth of data and splitting out into three loops improves legibility
|
|
|
|
const keyField = ctx.getters.keyFieldForType(type);
|
|
|
|
// Why don't we just replace the map? Because we
|
|
// 1. nav to list, subscribe to changes
|
|
// 2. nav to resource in list
|
|
// 3. update to page comes over
|
|
// 4. need to update the reference the detail list uses
|
|
const proxiesMap = {};
|
|
const proxies = reactive(data.map((x) => {
|
|
proxiesMap[x[keyField]] = true;
|
|
|
|
return classify(ctx, x);
|
|
}));
|
|
const cache = registerType(state, type);
|
|
|
|
cache.generation++;
|
|
|
|
// Update list
|
|
clear(cache.list);
|
|
addObjects(cache.list, proxies);
|
|
|
|
// Update Map (remove stale)
|
|
cache.map.forEach((value, key) => {
|
|
if (!proxiesMap[value[keyField]]) {
|
|
cache.map.delete(key);
|
|
}
|
|
});
|
|
|
|
// Update Map (update existing / add latest)
|
|
for ( let i = 0 ; i < proxies.length ; i++ ) {
|
|
// This could probably be merged with the first loop above
|
|
const existing = cache.map.get(proxies[i][keyField]);
|
|
const latest = proxies[i];
|
|
|
|
if (existing) {
|
|
replaceResource(existing, latest, ctx.getters);
|
|
} else {
|
|
cache.map.set(latest[keyField], latest);
|
|
}
|
|
}
|
|
|
|
// havePage is of type `StorePagination`
|
|
cache.havePage = pagination;
|
|
cache.haveNamespace = undefined;
|
|
cache.haveAll = undefined;
|
|
cache.revision = revision;
|
|
|
|
return proxies;
|
|
},
|
|
|
|
forgetAll(state, { type }) {
|
|
const cache = registerType(state, type);
|
|
|
|
clear(cache.list);
|
|
cache.map.clear();
|
|
cache.generation++;
|
|
},
|
|
|
|
setHaveAll(state, { type }) {
|
|
const cache = registerType(state, type);
|
|
|
|
cache.haveAll = true;
|
|
},
|
|
|
|
setHaveNamespace(state, { type, namespace }) {
|
|
const cache = registerType(state, type);
|
|
|
|
cache.haveNamespace = namespace;
|
|
},
|
|
|
|
loadedAll(state, { type }) {
|
|
const cache = registerType(state, type);
|
|
|
|
cache.generation++;
|
|
cache.haveAll = true;
|
|
},
|
|
|
|
remove(state, obj) {
|
|
if (obj) {
|
|
remove(state, obj, this.getters);
|
|
}
|
|
},
|
|
|
|
reset(state) {
|
|
resetStore(state, this.commit);
|
|
},
|
|
|
|
forgetType,
|
|
|
|
incrementLoadCounter(state, type) {
|
|
const typeData = state.types[type];
|
|
|
|
if (typeData) {
|
|
typeData.loadCounter++;
|
|
}
|
|
},
|
|
|
|
};
|