mirror of https://github.com/rancher/dashboard.git
336 lines
6.8 KiB
JavaScript
336 lines
6.8 KiB
JavaScript
import cloneDeep from 'lodash/cloneDeep';
|
|
import flattenDeep from 'lodash/flattenDeep';
|
|
import compact from 'lodash/compact';
|
|
import { JSONPath } from 'jsonpath-plus';
|
|
import Vue from 'vue';
|
|
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 { splitObjectPath, joinObjectPath } from '@/utils/string';
|
|
import { addObject } from '@/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 ) {
|
|
Vue.set(ptr, key, value);
|
|
} else if ( !ptr[key] ) {
|
|
// Make sure parent keys exist
|
|
Vue.set(ptr, key, {});
|
|
}
|
|
|
|
ptr = ptr[key];
|
|
}
|
|
|
|
return obj;
|
|
}
|
|
|
|
export function get(obj, path) {
|
|
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);
|
|
const leafKey = parentAry.pop();
|
|
|
|
const parent = get(obj, joinObjectPath(parentAry));
|
|
|
|
if ( parent ) {
|
|
Vue.set(parent, leafKey, undefined);
|
|
delete parent[leafKey];
|
|
}
|
|
|
|
return obj;
|
|
}
|
|
|
|
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] = compact(val.map((each) => {
|
|
if (each) {
|
|
const cleaned = cleanUp(each);
|
|
|
|
if (!isEmpty(cleaned)) {
|
|
return cleaned;
|
|
}
|
|
}
|
|
}));
|
|
if (compact(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) ) {
|
|
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);
|
|
const missing = difference(fromKeys, toKeys);
|
|
|
|
for ( const k of missing ) {
|
|
set(out, k, null);
|
|
}
|
|
|
|
return out;
|
|
}
|
|
|
|
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;
|
|
}
|