@atomist/sdm-pack-aspect
Version:
an Atomist SDM Extension Pack for visualizing drift across an organization
249 lines (226 loc) • 8.43 kB
text/typescript
/*
* 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;
});
}