@atomist/sdm-pack-aspect
Version:
an Atomist SDM Extension Pack for visualizing drift across an organization
254 lines (222 loc) • 8.5 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 * as _ from "lodash";
import {
PlantedTree,
SunburstCircleMetadata,
SunburstLeaf,
SunburstTree,
} from "./sunburst";
/**
* Implemented by types that can create JSON usable to back a d3 sunburst
* or other displayable formats from a set of repositories.
*/
export interface ReportBuilder<ROOT> {
/**
* Construct a PlantedTree from the given data.
* @param {() => (ROOT[] | AsyncIterable<ROOT>)} query
* @return {Promise<SunburstTree>}
*/
toPlantedTree(query: () => ROOT[] | AsyncIterable<ROOT>): Promise<PlantedTree>;
}
export type Renderer<T> = (t: T) => SunburstLeaf;
/**
* Access analysis data to emit a sunburst tree. All calculations will
* be performed in memory once an initial cohort of repo analyses is provided.
* All methods cause a tree layer to be emitted except for map
*/
export interface TreeBuilder<ROOT, T> {
/**
* Name of the tree rout
*/
readonly rootName: string;
/**
* Group values in the present layer by classifying each individual value.
* Return undefined to exclude a value
*/
group(groupStep: GroupStep<T>): TreeBuilder<ROOT, T>;
/**
* Group all values in the present layer in one go
* @param {CustomGroupStep<T, Q>} customGroupStep
* @return {TreeBuilder<Q>}
*/
customGroup<Q>(customGroupStep: CustomGroupStep<T, Q>): TreeBuilder<ROOT, Q>;
/**
* Split each T into multiple Qs, emitting a layer
*/
split<Q>(splitStep: SplitStep<T, Q>): TreeBuilder<ROOT, Q>;
/**
* Map or suppress values. Does not emit a layer.
* Can be used to filter as undefined values will be excluded
*/
map<Q>(mapStep: MapStep<ROOT, T, Q>): TreeBuilder<ROOT, Q>;
/**
* Setting the renderer for leaf nodes gives us a ReportBuilder we can
* use to transform passed in data.
* @param {(t: T) => SunburstLeaf} renderer
* @return {SunburstTree}
*/
renderWith(renderer: Renderer<T>): ReportBuilder<ROOT>;
}
/**
* Group records in this layer by a string
*/
export interface GroupStep<T> {
name: string;
by: (t: T) => string | Promise<string>;
flattenSingle?: boolean;
}
export interface CustomGroupStep<T, Q> {
name: string;
to: (t: T[] | AsyncIterable<T>) => Promise<Record<string, Q[]>> | Record<string, Q[]>;
flattenSingle?: boolean;
}
/**
* Map all the n records in this layer to m Qs
*/
export interface MapStep<ROOT, T, Q> {
mapping: (t: AsyncIterable<T> | T[], originalQuery: () => ROOT[] | AsyncIterable<ROOT>) => AsyncIterable<Q>;
}
/**
* Split every record T in this layer into n Qs
*/
export interface SplitStep<T, Q> {
splitter: (t: T) => Promise<Q[]> | Q[];
namer: (t: T) => string;
}
// Add a kind field to help with type determination
type Step = (GroupStep<any> | CustomGroupStep<any, any> | MapStep<any, any, any> | SplitStep<any, any>) &
{ kind: "group" | "split" | "customGroup" | "map", name?: string };
class DefaultTreeBuilder<ROOT, T> implements TreeBuilder<ROOT, T> {
private readonly steps: Step[] = [];
public split<Q>(splitStep: SplitStep<T, Q>): TreeBuilder<ROOT, Q> {
this.steps.push({ ...splitStep, kind: "split" });
return this as any;
}
public group(groupStep: GroupStep<T>): TreeBuilder<ROOT, T> {
this.steps.push({ ...groupStep, kind: "group" });
return this;
}
public customGroup<Q>(customGroupStep: CustomGroupStep<T, Q>): TreeBuilder<ROOT, Q> {
this.steps.push({ ...customGroupStep, kind: "customGroup" });
return this as any;
}
public map<Q>(mapStep: MapStep<ROOT, T, Q>): TreeBuilder<ROOT, Q> {
this.steps.push({ ...mapStep, kind: "map" });
return this as any;
}
/**
* Creates the final tree
* @param {(t: T) => SunburstLeaf} renderer
* @return {SunburstTree}
*/
public renderWith(renderer: Renderer<T>): ReportBuilder<ROOT> {
return {
toPlantedTree: async originalQuery => {
const data: ROOT[] = [];
for await (const root of originalQuery()) {
data.push(root);
}
const tree = await this.treeify(data, renderer);
const circles: SunburstCircleMetadata[] = [
...this.steps
.filter(step => !!step.name)
.map(step => ({
meaning: step.name,
})),
{ meaning: "render" },
];
return { tree, circles };
},
};
}
// Make a single tree from materialized data
private async treeify(data: ROOT[], renderer: Renderer<T>): Promise<SunburstTree> {
return {
name: this.rootName,
children: await layer<ROOT, T>(() => data, data, this.steps, renderer),
};
}
/**
*
* @param {string} rootName
*/
public constructor(public readonly rootName: string) {
}
}
export function treeBuilder<ROOT, T = ROOT>(rootName: string): TreeBuilder<ROOT, T> {
return new DefaultTreeBuilder(rootName);
}
async function layer<ROOT, T>(originalQuery: () => AsyncIterable<ROOT> | ROOT[],
currentLayerData: AsyncIterable<any> | ROOT[],
steps: Step[],
renderer: Renderer<T>): Promise<Array<SunburstTree | SunburstLeaf>> {
if (steps.length === 0) {
const results: SunburstLeaf[] = [];
for await (const l of currentLayerData) {
results.push(renderer(l));
}
return results;
}
const step = steps[0];
switch (step.kind) {
case "customGroup":
case "group" :
let groups;
if (step.kind === "customGroup") {
groups = await (step as CustomGroupStep<any, any>).to(currentLayerData);
} else {
const evaluations: Array<{ e: any, result: string }> = [];
for await (const e of currentLayerData) {
evaluations.push(await Promise.resolve((step as GroupStep<any>).by(e)).then(result => ({
e,
result,
})));
}
groups = _.groupBy(currentLayerData, e => evaluations.find(ev => ev.e === e).result);
}
// Lodash returns the name as the string "undefined"
const groupNames = Object.getOwnPropertyNames(groups).filter(name => name !== "undefined");
if (groupNames.length === 1 && (step as GroupStep<any>).flattenSingle) {
return layer(originalQuery, currentLayerData, steps.slice(1), renderer);
} else {
return Promise.all(groupNames.map(async name => {
return {
name,
children: await layer(originalQuery, await groups[name], steps.slice(1), renderer),
};
}));
}
case "split":
const splitStep = step as SplitStep<any, any>;
const kids: any[] = [];
for await (const t of currentLayerData) {
kids.push({
name: splitStep.namer(t),
children: await layer(originalQuery,
(await splitStep.splitter(t))
.filter(x => !!x),
steps.slice(1), renderer),
});
}
return kids;
case "map":
const mapStep = step as MapStep<any, any, any>;
return layer(originalQuery, mapStep.mapping(currentLayerData, originalQuery), steps.slice(1), renderer);
default:
throw new Error(`Unknown step type '${step.kind}'`);
}
}