@atomist/sdm-pack-aspect
Version:
an Atomist SDM Extension Pack for visualizing drift across an organization
267 lines • 9.98 kB
JavaScript
;
/*
* Copyright © 2019 Atomist, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
const automation_client_1 = require("@atomist/automation-client");
const _ = require("lodash");
const sunburst_1 = require("./sunburst");
/**
* Visit all nodes of a tree. May mutate them.
*/
function visit(t, visitor, depth = 0) {
const keepGoing = visitor(t, depth);
if (keepGoing && sunburst_1.isSunburstTree(t)) {
(t.children || []).forEach(c => visit(c, visitor, depth + 1));
}
}
exports.visit = visit;
function visitAsync(t, visitor, depth = 0) {
return __awaiter(this, void 0, void 0, function* () {
const r = yield visitor(t, depth);
if (r && sunburst_1.isSunburstTree(t)) {
yield Promise.all(t.children.map(c => visitAsync(c, visitor, depth + 1)));
}
});
}
exports.visitAsync = visitAsync;
/**
* Suppress branches that meet a condition
* @param tr Tree to transform
* @param shouldEliminate whether this child should be deleted
*/
function killChildren(tr, shouldEliminate) {
const t = _.cloneDeep(tr);
visit(t, (l, depth) => {
if (sunburst_1.isSunburstTree(l)) {
l.children = l.children.filter(c => {
const kill = shouldEliminate(c, depth + 1);
// logger.debug("Kill = %s for %s of depth %d", kill, c.name, depth);
return !kill;
});
return true;
}
return true;
});
return t;
}
exports.killChildren = killChildren;
/**
* Merge siblings into groups by the grouper
*/
function groupSiblings(tr, params) {
const opts = Object.assign({ collapseUnderName: () => false }, params);
const t = _.cloneDeep(tr);
visit(t, l => {
if (sunburst_1.isSunburstTree(l) && opts.parentSelector(l)) {
const grouped = _.groupBy(l.children, opts.childClassifier);
if (Object.values(grouped).every(a => a.length === 1)) {
// Nothing needs merged
return false;
}
l.children = [];
for (const name of Object.keys(grouped)) {
let children = _.flatten(grouped[name]);
if (opts.collapseUnderName(name)) {
children = _.flatten(children.map(childrenOf));
}
const newLayer = {
name,
children,
};
if (opts.groupLayerDecorator) {
opts.groupLayerDecorator(newLayer);
}
l.children.push(newLayer);
}
return false;
}
return true;
});
return t;
}
exports.groupSiblings = groupSiblings;
/**
* Trim the outer rim, replacing the next one with sized leaves
* @param tr tree to work on
* @param test test for which eligible nodes (nodes with only leaves under them) to kill
*/
function trimOuterRim(tr, test = () => true) {
const t = _.cloneDeep(tr);
visit(t, l => {
if (sunburst_1.isSunburstTree(l) && !l.children.some(sunburst_1.isSunburstTree) && test(l)) {
const leafChildren = l.children;
l.size = _.sum(leafChildren.map(c => c.size));
delete (l.children);
}
return true;
});
return t;
}
exports.trimOuterRim = trimOuterRim;
function insertAt(arr, index, item) {
arr.splice(index, 0, item);
return arr;
}
/**
* Introduce a new level splitting by by the given classifier for descendants
*/
function introduceClassificationLayer(pt, how) {
const opts = Object.assign({ descendantFinder: descendants }, how);
const tr = pt.tree;
if (tr.children.length === 0) {
return pt;
}
const circles = insertAt(pt.circles, opts.newLayerDepth, { meaning: opts.newLayerMeaning });
// Find descendants we're introduced in
const descendantPicker = tree => opts.descendantFinder(tree).filter(n => !!opts.descendantClassifier(n));
const t = _.cloneDeep(tr);
visit(t, (node, depth) => {
if (depth === (opts.newLayerDepth - 1) && sunburst_1.isSunburstTree(node)) {
// Split children
const descendantsToClassifyBy = descendantPicker(node);
automation_client_1.logger.debug("Found %d leaves for %s", descendantsToClassifyBy.length, t.name);
if (node === t && descendantsToClassifyBy.length === 0) {
automation_client_1.logger.debug("Nothing to do on %s", t.name);
return false;
}
// Introduce a new node for each classification
const distinctNames = _.uniq(descendantsToClassifyBy.map(d => opts.descendantClassifier(d)));
const oldKids = node.children;
node.children = [];
for (const name of distinctNames.sort()) {
const children = oldKids
.filter(k => sunburst_1.isSunburstTree(k) && descendantPicker(k).some(leaf => opts.descendantClassifier(leaf) === name) ||
!sunburst_1.isSunburstTree(k) && opts.descendantClassifier(k) === name);
if (children.length > 0) {
// Need to take out the children that are trees but don't have a descendant under them
const subTree = {
name,
children,
};
// Kill the children that have a different descendant classification
const prunedSubTree = killChildren(subTree, tt => {
if (sunburst_1.isSunburstTree(tt)) {
return false;
}
const classification = opts.descendantClassifier(tt);
return !!classification && classification !== name;
});
node.children.push(prunedSubTree);
}
}
return false;
}
return true;
});
const result = { tree: t, circles };
validatePlantedTree(result);
return result;
}
exports.introduceClassificationLayer = introduceClassificationLayer;
function pruneLeaves(tr, toPrune) {
const copy = _.cloneDeep(tr);
visit(copy, l => {
if (sunburst_1.isSunburstTree(l) && !l.children.some(sunburst_1.isSunburstTree)) {
l.children = l.children.filter(c => {
const f = sunburst_1.isSunburstTree(c) || !toPrune(c);
return f;
});
return false;
}
return true;
});
return copy;
}
exports.pruneLeaves = pruneLeaves;
/**
* Return all terminals under this level
* @param {SunburstLevel} t
* @return {SunburstLeaf[]}
*/
function leavesUnder(t) {
const leaves = [];
visit(t, l => {
if (!sunburst_1.isSunburstTree(l)) {
leaves.push(l);
}
return true;
});
return leaves;
}
exports.leavesUnder = leavesUnder;
function descendants(t) {
const descs = [];
visit(t, l => {
descs.push(l);
if (sunburst_1.isSunburstTree(l)) {
descs.push(..._.flatten(l.children.map(descendants)));
}
return true;
});
return _.uniq(descs);
}
exports.descendants = descendants;
function childCount(l) {
return sunburst_1.isSunburstTree(l) ? l.children.length : 0;
}
exports.childCount = childCount;
function childrenOf(l) {
return sunburst_1.isSunburstTree(l) ? l.children : [];
}
exports.childrenOf = childrenOf;
function validatePlantedTree(pt) {
checkNullChildrenInvariant(pt);
checkDepthInvariant(pt);
}
exports.validatePlantedTree = validatePlantedTree;
function checkDepthInvariant(pt) {
let depth = 0;
visit(pt.tree, (l, d) => {
if (d > depth) {
depth = d;
}
return true;
});
// the tree counts depth from zero
if ((depth + 1) !== pt.circles.length) {
automation_client_1.logger.error("Data: " + JSON.stringify(pt, undefined, 2));
automation_client_1.logger.error(`Expected a depth of ${pt.circles.length} but saw a tree of depth ${depth + 1}`);
}
}
function checkNullChildrenInvariant(pt) {
const haveNullChildren = [];
visit(pt.tree, (l, d) => {
if (sunburst_1.isSunburstTree(l) && l.children === null) {
haveNullChildren.push(l);
}
return true;
});
// the tree counts depth from zero
if (haveNullChildren.length > 0) {
automation_client_1.logger.error("Tree: " + JSON.stringify(pt.tree, undefined, 2));
throw new Error(`${haveNullChildren.length} tree nodes have null children: ${JSON.stringify(haveNullChildren)}`);
}
}
//# sourceMappingURL=treeUtils.js.map