UNPKG

@atomist/sdm-pack-aspect

Version:

an Atomist SDM Extension Pack for visualizing drift across an organization

267 lines 9.98 kB
"use strict"; /* * 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