mirror of https://github.com/rancher/dashboard.git
566 lines
14 KiB
JavaScript
566 lines
14 KiB
JavaScript
import { toRaw } from 'vue';
|
||
import cloneDeep from 'lodash/cloneDeep';
|
||
import flattenDeep from 'lodash/flattenDeep';
|
||
import compact from 'lodash/compact';
|
||
import { JSONPath } from 'jsonpath-plus';
|
||
import transform from 'lodash/transform';
|
||
import isObject from 'lodash/isObject';
|
||
import isArray from 'lodash/isArray';
|
||
import isEqual from 'lodash/isEqual';
|
||
import difference from 'lodash/difference';
|
||
import mergeWith from 'lodash/mergeWith';
|
||
import { splitObjectPath, joinObjectPath } from '@shell/utils/string';
|
||
import { addObject } from '@shell/utils/array';
|
||
|
||
export function set(obj, path, value) {
|
||
let ptr = obj;
|
||
|
||
if (!ptr) {
|
||
return;
|
||
}
|
||
|
||
const parts = splitObjectPath(path);
|
||
|
||
for (let i = 0; i < parts.length; i++) {
|
||
const key = parts[i];
|
||
|
||
if ( i === parts.length - 1 ) {
|
||
ptr[key] = value;
|
||
} else if ( !ptr[key] ) {
|
||
// Make sure parent keys exist
|
||
ptr[key] = {};
|
||
}
|
||
|
||
ptr = ptr[key];
|
||
}
|
||
|
||
return obj;
|
||
}
|
||
|
||
export function getAllValues(obj, path) {
|
||
const keysInOrder = path.split('.');
|
||
let currentValue = [obj];
|
||
|
||
keysInOrder.forEach((currentKey) => {
|
||
currentValue = currentValue.map((indexValue) => {
|
||
if (Array.isArray(indexValue)) {
|
||
return indexValue.map((arr) => arr[currentKey]).flat();
|
||
} else if (indexValue) {
|
||
return indexValue[currentKey];
|
||
} else {
|
||
return null;
|
||
}
|
||
}).flat();
|
||
});
|
||
|
||
return currentValue.filter((val) => val !== null);
|
||
}
|
||
|
||
export function get(obj, path) {
|
||
if ( !path) {
|
||
throw new Error('Cannot translate an empty input. The t function requires a string.');
|
||
}
|
||
if ( path.startsWith('$') ) {
|
||
try {
|
||
return JSONPath({
|
||
path,
|
||
json: obj,
|
||
wrap: false,
|
||
});
|
||
} catch (e) {
|
||
console.log('JSON Path error', e, path, obj); // eslint-disable-line no-console
|
||
|
||
return '(JSON Path err)';
|
||
}
|
||
}
|
||
if ( !path.includes('.') ) {
|
||
return obj?.[path];
|
||
}
|
||
|
||
const parts = splitObjectPath(path);
|
||
|
||
for (let i = 0; i < parts.length; i++) {
|
||
if (!obj) {
|
||
return;
|
||
}
|
||
|
||
obj = obj[parts[i]];
|
||
}
|
||
|
||
return obj;
|
||
}
|
||
|
||
export function remove(obj, path) {
|
||
const parentAry = splitObjectPath(path);
|
||
|
||
// Remove the very last part of the path
|
||
|
||
if (parentAry.length === 1) {
|
||
obj[path] = undefined;
|
||
delete obj[path];
|
||
} else {
|
||
const leafKey = parentAry.pop();
|
||
const parent = get(obj, joinObjectPath(parentAry));
|
||
|
||
if ( parent ) {
|
||
parent[leafKey] = undefined;
|
||
delete parent[leafKey];
|
||
}
|
||
}
|
||
|
||
return obj;
|
||
}
|
||
|
||
/**
|
||
* `delete` a property at the given path.
|
||
*
|
||
* This is similar to `remove` but doesn't need any fancy kube obj path splitting
|
||
* and doesn't use `Vue.set` (avoids reactivity)
|
||
*/
|
||
export function deleteProperty(obj, path) {
|
||
const pathAr = path.split('.');
|
||
const propToDelete = pathAr.pop();
|
||
|
||
// Walk down path until final prop, then delete final prop
|
||
delete pathAr.reduce((o, k) => o[k] || {}, obj)[propToDelete];
|
||
}
|
||
|
||
export function getter(path) {
|
||
return function(obj) {
|
||
return get(obj, path);
|
||
};
|
||
}
|
||
|
||
export function clone(obj) {
|
||
return cloneDeep(obj);
|
||
}
|
||
|
||
export function isEmpty(obj) {
|
||
if ( !obj ) {
|
||
return true;
|
||
}
|
||
|
||
return !Object.keys(obj).length;
|
||
}
|
||
|
||
/**
|
||
* Checks to see if the object is a simple key value pair where all values are
|
||
* just primitives.
|
||
* @param {any} obj
|
||
*/
|
||
export function isSimpleKeyValue(obj) {
|
||
return obj !== null &&
|
||
!Array.isArray(obj) &&
|
||
typeof obj === 'object' &&
|
||
Object.values(obj || {}).every((v) => typeof v !== 'object');
|
||
}
|
||
|
||
/*
|
||
returns an object with no key/value pairs (including nested) where the value is:
|
||
empty array
|
||
empty object
|
||
null
|
||
undefined
|
||
*/
|
||
export function cleanUp(obj) {
|
||
Object.keys(obj).map((key) => {
|
||
const val = obj[key];
|
||
|
||
if ( Array.isArray(val) ) {
|
||
obj[key] = val.map((each) => {
|
||
if (each !== null && each !== undefined) {
|
||
return cleanUp(each);
|
||
}
|
||
});
|
||
if (obj[key].length === 0) {
|
||
delete obj[key];
|
||
}
|
||
} else if (typeof val === 'undefined' || val === null) {
|
||
delete obj[key];
|
||
} else if ( isObject(val) ) {
|
||
if (isEmpty(val)) {
|
||
delete obj[key];
|
||
}
|
||
obj[key] = cleanUp(val);
|
||
}
|
||
});
|
||
|
||
return obj;
|
||
}
|
||
|
||
export function definedKeys(obj) {
|
||
const keys = Object.keys(obj).map((key) => {
|
||
const val = obj[key];
|
||
|
||
if ( Array.isArray(val) ) {
|
||
return `"${ key }"`;
|
||
} else if ( isObject(val) ) {
|
||
// no need for quotes around the subkey since the recursive call will fill that in via one of the other two statements in the if block
|
||
return ( definedKeys(val) || [] ).map((subkey) => `"${ key }".${ subkey }`);
|
||
} else {
|
||
return `"${ key }"`;
|
||
}
|
||
});
|
||
|
||
return compact(flattenDeep(keys));
|
||
}
|
||
|
||
export function diff(from, to) {
|
||
from = from || {};
|
||
to = to || {};
|
||
|
||
// Copy values in 'to' that are different than from
|
||
const out = transform(to, (res, toVal, k) => {
|
||
const fromVal = from[k];
|
||
|
||
if ( isEqual(toVal, fromVal) ) {
|
||
return;
|
||
}
|
||
|
||
if ( Array.isArray(toVal) || Array.isArray(fromVal) ) {
|
||
// Don't diff arrays, just use the whole value
|
||
res[k] = toVal;
|
||
} else if ( isObject(toVal) && isObject(from[k]) ) {
|
||
res[k] = diff(fromVal, toVal);
|
||
} else {
|
||
res[k] = toVal;
|
||
}
|
||
});
|
||
|
||
const fromKeys = definedKeys(from);
|
||
const toKeys = definedKeys(to);
|
||
|
||
// Return keys that are in 'from' but not 'to.'
|
||
const missing = difference(fromKeys, toKeys);
|
||
|
||
for ( const k of missing ) {
|
||
set(out, k, null);
|
||
}
|
||
|
||
return out;
|
||
}
|
||
|
||
/**
|
||
* Super simple lodash isEqual equivalent.
|
||
*
|
||
* Only checks root properties for strict equality
|
||
*/
|
||
function isEqualBasic(from, to) {
|
||
const fromKeys = Object.keys(from || {});
|
||
const toKeys = Object.keys(to || {});
|
||
|
||
if (fromKeys.length !== toKeys.length) {
|
||
return false;
|
||
}
|
||
|
||
for (let i = 0; i < fromKeys.length; i++) {
|
||
const fromValue = from[fromKeys[i]];
|
||
const toValue = to[fromKeys[i]];
|
||
|
||
if (fromValue !== toValue) {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
export { isEqualBasic as isEqual };
|
||
|
||
export function changeset(from, to, parentPath = []) {
|
||
let out = {};
|
||
|
||
if ( isEqual(from, to) ) {
|
||
return out;
|
||
}
|
||
|
||
for ( const k in from ) {
|
||
const path = joinObjectPath([...parentPath, k]);
|
||
|
||
if ( !(k in to) ) {
|
||
out[path] = { op: 'remove', path };
|
||
} else if ( (isObject(from[k]) && isObject(to[k])) || (isArray(from[k]) && isArray(to[k])) ) {
|
||
out = { ...out, ...changeset(from[k], to[k], [...parentPath, k]) };
|
||
} else if ( !isEqual(from[k], to[k]) ) {
|
||
out[path] = {
|
||
op: 'change', from: from[k], value: to[k]
|
||
};
|
||
}
|
||
}
|
||
|
||
for ( const k in to ) {
|
||
if ( !(k in from) ) {
|
||
const path = joinObjectPath([...parentPath, k]);
|
||
|
||
out[path] = { op: 'add', value: to[k] };
|
||
}
|
||
}
|
||
|
||
return out;
|
||
}
|
||
|
||
export function changesetConflicts(a, b) {
|
||
let keys = Object.keys(a).sort();
|
||
const out = [];
|
||
const seen = {};
|
||
|
||
for ( const k of keys ) {
|
||
let ok = true;
|
||
const aa = a[k];
|
||
const bb = b[k];
|
||
|
||
// If we've seen a change for a parent of this key before (e.g. looking at `spec.replicas` and there's already been a change to `spec`), assume they conflict
|
||
for ( const parentKey of parentKeys(k) ) {
|
||
if ( seen[parentKey] ) {
|
||
ok = false;
|
||
break;
|
||
}
|
||
}
|
||
|
||
seen[k] = true;
|
||
|
||
if ( ok && bb ) {
|
||
switch ( `${ aa.op }-${ bb.op }` ) {
|
||
case 'add-add':
|
||
case 'add-change':
|
||
case 'change-add':
|
||
case 'change-change':
|
||
ok = isEqual(aa.value, bb.value);
|
||
break;
|
||
|
||
case 'add-remove':
|
||
case 'change-remove':
|
||
case 'remove-add':
|
||
case 'remove-change':
|
||
ok = false;
|
||
break;
|
||
|
||
case 'remove-remove':
|
||
default:
|
||
ok = true;
|
||
break;
|
||
}
|
||
}
|
||
|
||
if ( !ok ) {
|
||
addObject(out, k);
|
||
}
|
||
}
|
||
|
||
// Check parent keys going the other way
|
||
keys = Object.keys(b).sort();
|
||
for ( const k of keys ) {
|
||
let ok = true;
|
||
|
||
for ( const parentKey of parentKeys(k) ) {
|
||
if ( seen[parentKey] ) {
|
||
ok = false;
|
||
break;
|
||
}
|
||
}
|
||
|
||
seen[k] = true;
|
||
|
||
if ( !ok ) {
|
||
addObject(out, k);
|
||
}
|
||
}
|
||
|
||
return out.sort();
|
||
|
||
function parentKeys(k) {
|
||
const out = [];
|
||
const parts = splitObjectPath(k);
|
||
|
||
parts.pop();
|
||
|
||
while ( parts.length ) {
|
||
const path = joinObjectPath(parts);
|
||
|
||
out.push(path);
|
||
parts.pop();
|
||
}
|
||
|
||
return out;
|
||
}
|
||
}
|
||
|
||
export function applyChangeset(obj, changeset) {
|
||
let entry;
|
||
|
||
for ( const path in changeset ) {
|
||
entry = changeset[path];
|
||
|
||
if ( entry.op === 'add' || entry.op === 'change' ) {
|
||
set(obj, path, entry.value);
|
||
} else if ( entry.op === 'remove' ) {
|
||
remove(obj, path);
|
||
} else {
|
||
throw new Error(`Unknown operation:${ entry.op }`);
|
||
}
|
||
}
|
||
|
||
return obj;
|
||
}
|
||
|
||
/**
|
||
* Creates an object composed of the `object` properties `predicate` returns
|
||
*/
|
||
export function pickBy(obj = {}, predicate = (value, key) => false) {
|
||
return Object.entries(obj)
|
||
.reduce((res, [key, value]) => {
|
||
if (predicate(value, key)) {
|
||
res[key] = value;
|
||
}
|
||
|
||
return res;
|
||
}, {});
|
||
}
|
||
|
||
/**
|
||
* Convert list to dictionary from a given function
|
||
* @param {*} array
|
||
* @param {*} callback
|
||
* @returns
|
||
*/
|
||
export const toDictionary = (array, callback) => Object.assign(
|
||
{}, ...array.map((item) => ({ [item]: callback(item) }))
|
||
);
|
||
|
||
export function dropKeys(obj, keys) {
|
||
if ( !obj ) {
|
||
return;
|
||
}
|
||
|
||
for ( const k of keys ) {
|
||
delete obj[k];
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Recursively convert a reactive object to a raw object
|
||
* @param {*} obj
|
||
* @param {*} cache
|
||
* @returns
|
||
*/
|
||
export function deepToRaw(obj, cache = new WeakSet()) {
|
||
if (obj === null || typeof obj !== 'object') {
|
||
// If obj is null or a primitive, return it as is
|
||
return obj;
|
||
}
|
||
|
||
// If the object has already been processed, return it to prevent circular references
|
||
if (cache.has(obj)) {
|
||
return obj;
|
||
}
|
||
cache.add(obj);
|
||
|
||
if (Array.isArray(obj)) {
|
||
return obj.map((item) => deepToRaw(item, cache));
|
||
} else {
|
||
const rawObj = toRaw(obj);
|
||
const result = {};
|
||
|
||
for (const key in rawObj) {
|
||
if (typeof rawObj[key] === 'function' || typeof rawObj[key] === 'symbol') {
|
||
result[key] = null;
|
||
} else {
|
||
result[key] = deepToRaw(rawObj[key], cache);
|
||
}
|
||
}
|
||
|
||
return result;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Helper function to alter Lodash merge function default behaviour on merging
|
||
* arrays and objects.
|
||
*
|
||
* In rke2.vue, the syncMachineConfigWithLatest function updates machine pool configuration by
|
||
* merging the latest configuration received from the backend with the current configuration updated by the user.
|
||
* However, Lodash's merge function treats arrays like object so index values are merged and not appended to arrays
|
||
* resulting in undesired outcomes for us, Example:
|
||
*
|
||
* const lastSavedConfigFromBE = { a: ["test"] };
|
||
* const currentConfigByUser = { a: [] };
|
||
* merge(lastSavedConfigFromBE, currentConfigByUser); // returns { a: ["test"] }; but we expect { a: [] };
|
||
*
|
||
* More info: https://github.com/lodash/lodash/issues/1313
|
||
|
||
* This helper function addresses the issue by always replacing the old array with the new array during the merge process.
|
||
*
|
||
* This helper is also used for another case in rke2.vue to handle merging addon chart default values with the user's current values.
|
||
* It fixed https://github.com/rancher/dashboard/issues/12418
|
||
*
|
||
* If `mutateOriginal` is true, the merge is done directly into `obj1` (mutating it).
|
||
* This is useful in cases like:
|
||
* machinePool.config = mergeWithReplace(clonedLatestConfig, clonedCurrentConfig, { mutateOriginal: true })
|
||
* where merging into a new empty object may lead to incomplete structure.
|
||
*
|
||
* Use `mutateOriginal` when you want to preserve obj1’s original shape, references,
|
||
* or when assigning the result directly to an existing object.
|
||
*
|
||
* @param {Object} [obj1={}] - The first object to merge
|
||
* @param {Object} [obj2={}] - The second object to merge
|
||
* @param {Object} [options={}] - Options for customizing merge behavior
|
||
* @param {boolean} [options.mutateOriginal=false] - true: mutates obj1
|
||
* directly. false: returns a new object
|
||
* @param {boolean} [options.replaceArray=true] - true: replaces arrays in obj1
|
||
* with arrays in obj2 when both properties are arrays
|
||
* false: default lodash merge behavior - recursively merges
|
||
* array members
|
||
*/
|
||
export function mergeWithReplace(
|
||
obj1 = {},
|
||
obj2 = {},
|
||
{
|
||
mutateOriginal = false,
|
||
replaceArray = true,
|
||
} = {}
|
||
) {
|
||
const destination = mutateOriginal ? obj1 : {};
|
||
|
||
return mergeWith(destination, obj1, obj2, (obj1Value, obj2Value) => {
|
||
if (replaceArray && Array.isArray(obj1Value) && Array.isArray(obj2Value)) {
|
||
return obj2Value;
|
||
}
|
||
});
|
||
}
|
||
/**
|
||
* Converts Object into a string of a format "key1, val1, key2, val2"
|
||
* @param {Object} input - KV object to convert
|
||
* @returns string
|
||
*/
|
||
export const convertKVToString = (input) => {
|
||
if (!input || typeof input !== 'object') return '';
|
||
|
||
return Object.entries(input)
|
||
.flatMap(([key, value]) => [key, String(value)])
|
||
.join(',');
|
||
};
|
||
|
||
/**
|
||
* Converts kv string into object
|
||
* @param {string} input - a string of a format "key1, val1, key2, val2"
|
||
* @returns
|
||
*/
|
||
export const convertStringToKV = ( input ) => {
|
||
if (!input?.trim()) return {};
|
||
|
||
const parts = input.split(',').map((part) => part.trim());
|
||
const result = {};
|
||
|
||
for (let i = 0; i < parts.length - 1; i += 2) {
|
||
const key = parts[i];
|
||
const value = parts[i + 1];
|
||
|
||
// Accept empty keys but ignore undefined values
|
||
if (typeof key === 'string') {
|
||
result[key] = value ?? '';
|
||
}
|
||
}
|
||
|
||
return result;
|
||
};
|