UNPKG

@atomist/sdm-pack-aspect

Version:

an Atomist SDM Extension Pack for visualizing drift across an organization

249 lines (226 loc) 8.43 kB
/* * 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. */ import { ConcreteIdeal, FP, Ideal, } from "@atomist/sdm-pack-fingerprint"; import { isConcreteIdeal } from "@atomist/sdm-pack-fingerprint/lib/machine/Ideal"; import { AspectRegistry } from "../aspect/AspectRegistry"; import { isSunburstTree, PlantedTree, SunburstTree, } from "../tree/sunburst"; import { groupSiblings, introduceClassificationLayer, killChildren, trimOuterRim, visit, visitAsync, } from "../tree/treeUtils"; import { Aspect } from "@atomist/sdm-pack-fingerprint/lib/machine/Aspect"; import * as _ from "lodash"; import { ProjectAnalysisResultStore } from "../analysis/offline/persist/ProjectAnalysisResultStore"; import { addRepositoryViewUrl, splitByOrg, } from "./support/treeMunging"; /** * Return a tree from fingerprint name -> instances -> repos * @return {Promise<PlantedTree>} */ export async function buildFingerprintTree( world: { aspectRegistry: AspectRegistry, store: ProjectAnalysisResultStore, }, params: { workspaceId: string, fingerprintName: string, fingerprintType: string, byName: boolean, otherLabel: string, byOrg: boolean, trim: boolean, showProgress: boolean, }): Promise<PlantedTree> { const { workspaceId, byName, fingerprintName, fingerprintType, otherLabel, byOrg, trim, showProgress } = params; const showPresence = !!otherLabel; const { store, aspectRegistry } = world; // Get the tree and then perform post processing on it let pt = await store.fingerprintsToReposTree({ workspaceId, byName, otherLabel, rootName: fingerprintName, aspectName: fingerprintType, }); // logger.debug("Returning fingerprint tree '%s': %j", fingerprintName, pt); await decorateProblemFingerprints(aspectRegistry, pt); const aspect = aspectRegistry.aspectOf(fingerprintType); if (!byName) { // Show all fingerprints in one aspect, splitting by fingerprint name pt = introduceClassificationLayer<{ data: any, type: string }>(pt, { descendantClassifier: l => { if (!(l as any).sha) { return undefined; } const aspect2: Aspect = aspectRegistry.aspectOf(l.type); return !aspect2 || !aspect2.toDisplayableFingerprintName ? l.name : aspect2.toDisplayableFingerprintName(l.name); }, newLayerDepth: 1, newLayerMeaning: "fingerprint name", }); if (!!aspect) { pt.tree.name = aspect.displayName; } } else { // We are showing a particular fingerprint if (!!aspect) { pt.tree.name = aspect.toDisplayableFingerprintName ? aspect.toDisplayableFingerprintName(fingerprintName) : fingerprintName; } } resolveAspectNames(aspectRegistry, pt.tree); // if (!showPresence) { // // Suppress branches from aspects that use name "None" for not found // pt.tree = killChildren(pt.tree, c => c.name === "None"); // } if (byOrg) { pt = splitByOrg(pt); } if (showPresence) { pt.tree = groupSiblings(pt.tree, { parentSelector: parent => parent.children.some(c => (c as any).sha), childClassifier: kid => (kid as any).sha && kid.name !== "None" ? "Present" : "Absent", collapseUnderName: name => name === "Absent", }); } else if (showProgress) { const ideal = await aspectRegistry.idealStore.loadIdeal(workspaceId, fingerprintType, fingerprintName); if (!ideal || !isConcreteIdeal(ideal)) { throw new Error(`No ideal to aspire to for ${fingerprintType}/${fingerprintName} in workspace '${workspaceId}'`); } decorateToShowProgressToIdeal(aspectRegistry, pt, ideal); } if (!showPresence) { // Don't do this if we are looking at presence, as sized nodes will swamp absent nodes with default 1 applyTerminalSizing(aspect, pt.tree); } pt.tree = addRepositoryViewUrl(pt.tree); // Group all fingerprint nodes by their name at the first level pt.tree = groupSiblings(pt.tree, { parentSelector: parent => parent.children.some(c => (c as any).sha), childClassifier: l => l.name, collapseUnderName: () => true, }); if (trim) { pt.tree = trimOuterRim(pt.tree); } else { putRepoPathInNameOfRepoLeaves(pt); } return pt; } function resolveAspectNames(aspectRegistry: AspectRegistry, t: SunburstTree): void { visit(t, l => { if ((l as any).sha) { const fp = l as any as FP; // It's a fingerprint name const aspect = aspectRegistry.aspectOf(fp.type); if (aspect) { fp.name = aspect.toDisplayableFingerprint ? aspect.toDisplayableFingerprint(fp) : fp.data; } else if (!!fp.data && !!fp.data.displayValue) { fp.name = fp.data.displayValue; } else if (!!fp.displayValue) { fp.name = fp.displayValue; } } return true; }); } /** * Size terminal nodes by aspect stat if available */ function applyTerminalSizing(aspect: Aspect, t: SunburstTree): void { if (aspect && aspect.stats && aspect.stats.basicStatsPath) { visit(t, l => { if (isSunburstTree(l) && l.children.every(c => !isSunburstTree(c) && (c as any).owner)) { l.children.forEach(c => (c as any).size = _.get(l, "data." + aspect.stats.basicStatsPath, 1)); } return true; }); } } async function decorateProblemFingerprints(aspectRegistry: AspectRegistry, pt: PlantedTree): Promise<void> { const usageChecker = await aspectRegistry.undesirableUsageCheckerFor("local"); // Flag bad fingerprints with a special color await visitAsync(pt.tree, async l => { if ((l as any).sha) { const problems = usageChecker ? usageChecker.check(l as any, "local") : undefined; if (problems && problems.length > 0) { (l as any).color = "#810325"; (l as any).problems = problems.map(problem => ({ // Need to dispense with the fingerprint, which would make this circular description: problem.description, severity: problem.severity, authority: problem.authority, url: problem.url, })); } } return true; }); } function decorateToShowProgressToIdeal(aspectRegistry: AspectRegistry, pt: PlantedTree, ideal: ConcreteIdeal): void { pt.tree = groupSiblings(pt.tree, { parentSelector: parent => parent.children.some(c => (c as any).sha), childClassifier: kid => (kid as any).sha === ideal.ideal.sha ? "Ideal" : "No", groupLayerDecorator: l => { if (l.name === "Ideal") { (l as any).color = "#168115"; } else { (l as any).color = "#811824"; } }, }); } /** * Show virtual repos * @param {PlantedTree} pt */ export function putRepoPathInNameOfRepoLeaves(pt: PlantedTree): void { interface EndNode { name: string; size: number; path?: string; url?: string; } visit(pt.tree, l => { const en = l as EndNode; if (!isSunburstTree(en) && en.name && en.url && en.path) { // It's an eligible end node en.name = en.name + "/" + en.path; } return true; }); }