285 lines
12 KiB
JavaScript
285 lines
12 KiB
JavaScript
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
class Info {
|
|
constructor(id, tooltip, name, sources) {
|
|
this.id = id;
|
|
this.tooltip = tooltip;
|
|
this.name = name;
|
|
this.sources = sources;
|
|
this.forcedEdges = new Set();
|
|
this.defaultDests = new Set();
|
|
this.blockAssetDests = new Set();
|
|
this.importance = 0;
|
|
}
|
|
update(branch, getId) {
|
|
const edgeId = (alias) => branch.def.name + '->' + getId(alias);
|
|
// shuld really do the same for default and block assets
|
|
this.forcedEdges = new Set([...this.forcedEdges, ...branch.def.forceFlowTo.map(edgeId)]);
|
|
this.defaultDests = new Set([...this.defaultDests, ...branch.def.defaultFlow.map(edgeId)]);
|
|
this.blockAssetDests = new Set([...this.blockAssetDests, ...branch.def.blockAssetTargets.map(edgeId)]);
|
|
}
|
|
}
|
|
|
|
function decoratedAlias(bot, alias) {
|
|
return bot + ':' + alias;
|
|
}
|
|
function aliasForBranch(branch) {
|
|
return decoratedAlias(branch.bot, branch.def.upperName);
|
|
}
|
|
|
|
const EDGE_STYLES = {
|
|
roboshelf: [['color', 'purple'], ['arrowhead', 'diamond']],
|
|
forced: [],
|
|
gated: [['color', 'darkorange']],
|
|
defaultFlow: [['color', 'blue']],
|
|
onRequest: [['color', 'darkgray'], ['style', 'dashed']],
|
|
blockAssets: [['color', 'darkgray'], ['style', 'dashed'], ['arrowhead', 'odiamond']]
|
|
};
|
|
|
|
class Graph {
|
|
constructor(allBranches, options) {
|
|
this.allBranches = allBranches;
|
|
this.options = options;
|
|
this.nodeLabels = new Map();
|
|
this.links = [];
|
|
this.aliases = new Map();
|
|
this.allInfos = [];
|
|
this.linkOutward = new Set();
|
|
this.linkInward = new Set();
|
|
this.connectedNodes = new Set();
|
|
this.branchList = [];
|
|
}
|
|
|
|
singleBot(botName) {
|
|
this.branchList = this.allBranches.filter(b => b.bot === botName);
|
|
this.addNodesAndEdges(false);
|
|
const title = this.options.aliases.length > 0 ? botName + ' (' + this.options.aliases.join(", ") + ')': botName
|
|
return this.makeGraph(title, botName + ' integration paths');
|
|
}
|
|
|
|
allBots() {
|
|
this.branchList = this.options.botsToShow
|
|
? this.allBranches.filter(b => this.options.botsToShow.indexOf(b.bot) >= 0)
|
|
: this.allBranches;
|
|
this.findSharedNodes();
|
|
this.addNodesAndEdges(!this.options.noGroups);
|
|
|
|
let title = 'All bots';
|
|
if (this.options.botsToShow)
|
|
{
|
|
title = `Bots: ${this.options.botsToShow.join(',')}`;
|
|
}
|
|
|
|
return this.makeGraph(title, 'Flow including shared bots');
|
|
}
|
|
|
|
addNodesAndEdges(grouped) {
|
|
for (const branchStatus of this.branchList) {
|
|
const botTitle = grouped
|
|
? this.options.aliases && this.options.aliases.get(branchStatus.bot).length > 0
|
|
? branchStatus.bot + ' (' + this.options.aliases.get(branchStatus.bot).join(", ") + ')'
|
|
: branchStatus.bot
|
|
: null;
|
|
|
|
this.addBranch(branchStatus, grouped ? branchStatus.bot : null, botTitle);
|
|
}
|
|
for (const info of this.allInfos) {
|
|
for (const sourceBranch of info.sources) {
|
|
info.update(sourceBranch, this.getIdForBot(sourceBranch.bot));
|
|
}
|
|
}
|
|
for (const branchStatus of this.branchList) {
|
|
for (const flow of branchStatus.def.flowsTo) {
|
|
this.addEdge(branchStatus, flow);
|
|
}
|
|
}
|
|
}
|
|
makeGraph(title, tooltip) {
|
|
const lines = [
|
|
'digraph robomerge {',
|
|
'fontname="sans-serif"; labelloc=top; fontsize=16;',
|
|
'edge [penwidth=2]; nodesep=.7; ranksep=1.2;',
|
|
`label = "${title}";`,
|
|
`tooltip="${tooltip}";`,
|
|
`node [shape=box, style=filled, fontname="sans-serif", fillcolor=moccasin];`,
|
|
];
|
|
const nodeGroups = [];
|
|
for (const [groupName, v] of this.nodeLabels) {
|
|
const nodeInfo = [];
|
|
nodeGroups.push([groupName, nodeInfo]);
|
|
for (const info of v) {
|
|
const { id, tooltip, name, importance } = info;
|
|
let factor = (Math.min(importance, 10) - 1) / 9;
|
|
nodeInfo.push({
|
|
importance: factor,
|
|
marginX: (.2 * (1 - factor) + .4 * factor).toPrecision(1),
|
|
marginY: (.1 * (1 - factor) + .25 * factor).toPrecision(1),
|
|
fontSize: (14 * (1 - factor) + 20 * factor).toPrecision(2),
|
|
info: info
|
|
});
|
|
}
|
|
}
|
|
for (const [groupName, nodeInfo] of nodeGroups) {
|
|
if (groupName !== 'nogroup') {
|
|
lines.push(`subgraph cluster_${groupName} {
|
|
label="${nodeInfo[0].info.title}";
|
|
`);
|
|
}
|
|
for (const info of nodeInfo) {
|
|
if (this.options.hideDisconnected && !this.connectedNodes.has(info.info.id)) {
|
|
continue;
|
|
}
|
|
const attrs = [
|
|
['label', `"${info.info.name}"`],
|
|
['tooltip', `"${info.info.tooltip}"`],
|
|
['margin', `"${info.marginX},${info.marginY}"`],
|
|
['fontsize', info.fontSize],
|
|
];
|
|
if (info.importance > .5) {
|
|
attrs.push(['style', '"filled,bold"']);
|
|
}
|
|
if (info.info.graphNodeColor) {
|
|
attrs.push(['fillcolor', `"${info.info.graphNodeColor}"`]);
|
|
}
|
|
const attrStrs = attrs.map(([key, value]) => `${key}=${value}`);
|
|
lines.push(`${info.info.id} [${attrStrs.join(', ')}];`);
|
|
}
|
|
if (groupName !== 'nogroup') {
|
|
lines.push('}');
|
|
}
|
|
}
|
|
for (const link of this.links) {
|
|
// when there's forced/unforced pair, only the former is a constrait
|
|
// (this makes flow )
|
|
const combo = link.dst + '->' + link.src;
|
|
if (combo && this.linkOutward.has(combo) && this.linkInward.has(combo)) {
|
|
link.styles.push(['constraint', 'false']);
|
|
}
|
|
const styleStrs = link.styles.map(([key, value]) => `${key}=${value}`);
|
|
const suffix = styleStrs.length === 0 ? '' : ` [${styleStrs.join(', ')}]`;
|
|
lines.push(`${link.src} -> ${link.dst}${suffix} [tooltip="${link.tooltip}"];`);
|
|
}
|
|
lines.push('}');
|
|
return lines;
|
|
}
|
|
addBranch(branchStatus, group = null, title = null) {
|
|
const branch = branchStatus.def;
|
|
if (this.aliases.has(decoratedAlias(branchStatus.bot, branch.upperName)))
|
|
return;
|
|
let tooltip = `${branchStatus.bot} - ${branch.rootPath}`;
|
|
if (branch.aliases && branch.aliases.length !== 0) {
|
|
tooltip += ` (${branch.aliases.join(', ')})`;
|
|
}
|
|
const info = new Info(`_${branchStatus.bot}_${branch.upperName.replace(/[^\w]+/g, '_')}`, tooltip, branch.name, [branchStatus]);
|
|
info.title = title
|
|
this.allInfos.push(info);
|
|
if (branch.config.graphNodeColor) {
|
|
info.graphNodeColor = branch.config.graphNodeColor;
|
|
}
|
|
if (branch.upperName === 'MAIN') {
|
|
// special case branches named Main
|
|
info.importance = 10;
|
|
}
|
|
// note: branch.upperName is always in branch.aliases
|
|
for (const alias of branch.aliases) {
|
|
this.aliases.set(decoratedAlias(branchStatus.bot, alias), info);
|
|
}
|
|
setDefault(this.nodeLabels, group || 'nogroup', []).push(info);
|
|
}
|
|
// dstName
|
|
addEdge(srcBranchStatus, dst) {
|
|
const src = aliasForBranch(srcBranchStatus);
|
|
const srcInfo = this.aliases.get(src);
|
|
const dstInfo = this.aliases.get(decoratedAlias(srcBranchStatus.bot, dst));
|
|
++srcInfo.importance;
|
|
++dstInfo.importance;
|
|
const hasEdge = (edges, target) => edges.has(srcBranchStatus.def.name + '->' + target.id);
|
|
const isForced = hasEdge(srcInfo.forcedEdges, dstInfo);
|
|
if (!isForced && this.options.showOnlyForced) {
|
|
return;
|
|
}
|
|
const isGated = new Map(srcBranchStatus.def.edgeProperties).get(dst).lastGoodCLPath
|
|
this.connectedNodes.add(srcInfo.id);
|
|
this.connectedNodes.add(dstInfo.id);
|
|
const edgeStyle = isForced ? (isGated ? 'gated' : 'forced') :
|
|
hasEdge(srcInfo.defaultDests, dstInfo) ? 'defaultFlow' :
|
|
hasEdge(srcInfo.blockAssetDests, dstInfo) ? 'blockAssets' : 'onRequest';
|
|
const styles = [...EDGE_STYLES[edgeStyle]];
|
|
|
|
// prepare the tool tip of the edge to be as informative as possible
|
|
let suffix = '';
|
|
if(edgeStyle !== 'defaultFlow'){
|
|
suffix = `(${edgeStyle})`;
|
|
}
|
|
|
|
let tooltipcontent = `${srcInfo.name} -> ${dstInfo.name} ${suffix}\n`;
|
|
tooltipcontent += `\n${srcInfo.name}:\n${srcInfo.tooltip}\n`;
|
|
tooltipcontent += `\n${dstInfo.name}:\n${dstInfo.tooltip}\n`;
|
|
if (isGated)
|
|
{
|
|
tooltipcontent += `\nlast good CL path:\n${isGated}`
|
|
}
|
|
|
|
const link = { src: srcInfo.id, dst: dstInfo.id, styles, tooltip: tooltipcontent };
|
|
this.links.push(link);
|
|
if (isForced) {
|
|
this.linkOutward.add(link.src + '->' + link.dst);
|
|
}
|
|
else if (edgeStyle === 'blockAssets' || edgeStyle === 'onRequest') {
|
|
this.linkInward.add(link.dst + '->' + link.src);
|
|
}
|
|
}
|
|
findSharedNodes() {
|
|
const streamMap = new Map();
|
|
for (const branchStatus of this.branchList) {
|
|
let key = branchStatus.def.rootPath;
|
|
if(branchStatus.def.uniqueBranch)
|
|
{
|
|
key = branchStatus.def.name;
|
|
}
|
|
|
|
const [color, streams] = setDefault(streamMap, key, [null, []]);
|
|
streams.push(branchStatus);
|
|
const nodeColor = branchStatus.def.config.graphNodeColor;
|
|
if (!color && nodeColor) {
|
|
streamMap.set(key, [nodeColor, streams]);
|
|
}
|
|
}
|
|
|
|
// create a shared info for each set of nodes monitoring the same stream
|
|
for (const [k, v] of streamMap) {
|
|
const [color, streams] = v;
|
|
if (streams.length > 1) {
|
|
|
|
// create a tooltip with information from all the branches
|
|
let globalToolTip = '';
|
|
for (const branchStatus of streams) {
|
|
let tooltip = `${branchStatus.bot} - ${branchStatus.def.rootPath}`;
|
|
if (branchStatus.def.aliases && branchStatus.def.aliases.length !== 0) {
|
|
tooltip += ` (${branchStatus.def.aliases.join(', ')})`;
|
|
}
|
|
globalToolTip += tooltip + "\n";
|
|
}
|
|
globalToolTip = globalToolTip.trim();
|
|
|
|
const info = new Info(k.replace(/[^\w]+/g, '_'), globalToolTip, k, streams);
|
|
this.allInfos.push(info);
|
|
if (color) {
|
|
info.graphNodeColor = color;
|
|
}
|
|
for (const branchStatus of streams) {
|
|
for (const alias of branchStatus.def.aliases) {
|
|
this.aliases.set(decoratedAlias(branchStatus.bot, alias), info);
|
|
}
|
|
}
|
|
setDefault(this.nodeLabels, 'nogroup', []).push(info);
|
|
}
|
|
}
|
|
}
|
|
getIdForBot(bot) {
|
|
return (alias) => {
|
|
const info = this.aliases.get(decoratedAlias(bot, alias));
|
|
return info ? info.id : alias;
|
|
};
|
|
}
|
|
} |