Skip to content

Commit f63bd38

Browse files
committed
feat: support fn::sub, add filtering and deafult to mxHierarchicalLayout
1 parent 008cfa3 commit f63bd38

File tree

9 files changed

+912
-320
lines changed

9 files changed

+912
-320
lines changed

README.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,7 @@ cfn-dia generate -t template.yaml
3030
* Select only the resource types you want to see. This lets you skip granlar things like roles and policies that might not add to the overview you want to see
3131
* Navigate through a new differnet layouts
3232
* Works for both JSON and YAML templates
33+
* Filter on resource type and/or resource names
3334

3435
## Known issues
3536
* Some icons are missing. Working on completing the coverage.
36-
* Default layouts get quite messy. In the draw.io menu, use the Arrange -> Layout menu for better options
37-
* Connections between resources are limited to `Ref` and `Fn::GetAtt` intrinsic functions. `Fn::Sub` is coming soon.

index.js

Lines changed: 42 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,13 @@ const inquirer = require("inquirer");
99
const prompt = inquirer.createPromptModule();
1010

1111
const package = JSON.parse(fs.readFileSync(path.join(__dirname, "package.json")));
12+
13+
const actionOption = {
14+
FilterResourceTypes: "Filter resources by type",
15+
FilterResourceName: "Filter resources by name",
16+
ChangeLayout: "Change layout"
17+
}
18+
1219
program.version(package.version, "-v, --vers", "output the current version");
1320
program
1421
.command("generate")
@@ -34,33 +41,50 @@ program
3441
? JSON.parse
3542
: YAML.yamlParse;
3643
const template = parser(templateString);
37-
const resources = Object.keys(template.Resources);
44+
const resources = [...Object.keys(template.Resources)].sort();
3845
let types = [];
3946
for (const resource of resources) {
4047
types.push(template.Resources[resource].Type);
4148
}
4249
types = [...new Set(types)].sort();
4350
let resourceTypes = { answer: types };
44-
const filterResources = "Filter resources";
51+
let resourceNames = { answer: resources };
52+
let actionChoice = {};
4553
while (true) {
46-
let layoutChoice = {};
47-
while (layoutChoice.answer !== filterResources) {
48-
const xml = mxGenerator.renderTemplate(template, resourceTypes.answer, layoutChoice.answer);
49-
fs.writeFileSync(cmd.outputFile, xml);
50-
layoutChoice = await prompt({
51-
message: "Select layout",
52-
choices: [{name: filterResources, value: filterResources}, ...mxGenerator.layouts],
53-
type: "list",
54-
name: "answer",
55-
});
56-
57-
}
58-
resourceTypes = await prompt({
59-
message: "Select resource types to include in diagram",
60-
choices: types,
61-
type: "checkbox",
54+
const xml = mxGenerator.renderTemplate(template, resourceTypes.answer, resourceNames.answer);
55+
fs.writeFileSync(cmd.outputFile, xml);
56+
57+
actionChoice = await prompt({
58+
message: "Options",
59+
choices: [
60+
actionOption.FilterResourceTypes,
61+
actionOption.FilterResourceName,
62+
//actionOption.ChangeLayout
63+
],
64+
type: "list",
6265
name: "answer",
6366
});
67+
68+
switch (actionChoice.answer) {
69+
case actionOption.FilterResourceTypes:
70+
resourceTypes = await prompt({
71+
message: "Select resource types to include",
72+
choices: types,
73+
default: resourceTypes.answer,
74+
type: "checkbox",
75+
name: "answer",
76+
});
77+
break;
78+
case actionOption.FilterResourceName:
79+
resourceNames = await prompt({
80+
message: "Select resources to include",
81+
choices: resources,
82+
default: resourceNames.answer,
83+
type: "checkbox",
84+
name: "answer",
85+
});
86+
break;
87+
}
6488
}
6589
});
6690

mxgraph/MxGenerator.js

Lines changed: 137 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,12 @@ const { JSDOM } = jsdom;
44
const dom = new JSDOM();
55
const jsonUtil = require("../resources/JsonUtil");
66
const iconMap = require("../resources/IconMap");
7-
87
global.window = dom.window;
98
global.document = window.document;
109
global.XMLSerializer = window.XMLSerializer;
1110
global.navigator = window.navigator;
1211

1312
const mxgraph = require("mxgraph")({});
14-
1513
const { mxGraph, mxCodec, mxUtils } = mxgraph;
1614

1715
const layouts = [
@@ -21,120 +19,173 @@ const layouts = [
2119
{ name: "Radial Tree", value: "mxRadialTreeLayout" },
2220
];
2321

24-
function makeGraph(template, resourceTypesToInclude, layoutChoice) {
25-
const resources = Object.keys(template.Resources);
22+
const graph = new mxGraph();
23+
24+
let currentLayout = "mxHierarchicalLayout";
2625

27-
const graph = new mxGraph();
28-
const layout = new mxgraph[layoutChoice || "mxFastOrganicLayout"](graph);
29-
layout.radius = 300;
30-
layout.forceConstant = 120;
31-
const parent = graph.getDefaultParent();
32-
const vertices = [];
26+
let vertices = [];
27+
let forceLayoutRender = true;
28+
let locationCache = {};
29+
const parent = graph.getDefaultParent();
30+
function makeGraph(template, resourceTypesToInclude, resourceNamesToInclude) {
31+
let layout = new mxgraph[currentLayout](graph, true, 500);
32+
const resources = Object.keys(template.Resources);
33+
layout.orientation = "west";
34+
layout.intraCellSpacing = 50;
35+
layout.interRankCellSpacing = 200;
36+
layout.interHierarchySpacing = 100;
37+
layout.parallelEdgeSpacing = 20;
38+
layout.leftMargin = 200;
39+
layout.resizeParent = true;
3340
graph.getModel().beginUpdate();
3441
try {
3542
for (const resource of resources) {
3643
const type = template.Resources[resource].Type;
37-
if (!resourceTypesToInclude.includes(type)) {
44+
if (
45+
!resourceTypesToInclude.includes(type) ||
46+
!resourceNamesToInclude.includes(resource)
47+
) {
48+
updateFilters(type, resource);
3849
continue;
3950
}
40-
const dependencies = [];
41-
jsonUtil.findAllValues(template.Resources[resource], dependencies, "Ref");
42-
jsonUtil.findAllValues(
43-
template.Resources[resource],
44-
dependencies,
45-
"Fn::GetAtt"
46-
);
4751

48-
for (const dependency of dependencies) {
49-
dependency.value = dependency.value.filter(
50-
(p) =>
51-
template.Resources[p] &&
52-
resourceTypesToInclude.includes(template.Resources[p].Type)
53-
);
54-
}
52+
const dependencies = getDependencies(
53+
template,
54+
resource,
55+
resourceTypesToInclude
56+
);
5557

56-
vertices.push({
57-
name: resource,
58-
dependencies: dependencies,
59-
vertex: graph.insertVertex(
60-
parent,
61-
null,
62-
resource,
63-
70,
64-
1,
65-
50,
66-
50,
67-
iconMap.getIcon(type)
68-
),
69-
});
58+
addVertices(resource, dependencies, type);
7059
}
7160

72-
for (const vertex of vertices) {
73-
for (const dependencyNode of vertex.dependencies) {
61+
for (const sourceVertex of vertices) {
62+
for (const dependencyNode of sourceVertex.dependencies) {
7463
for (const dependency of dependencyNode.value) {
75-
const target = vertices.filter((p) => p.name === dependency)[0];
76-
let from = vertex.vertex;
77-
let to = target.vertex;
78-
79-
if (from && to) {
80-
if (dependencyNode.path.indexOf("Properties.Events") > 0) {
81-
graph.insertEdge(
82-
parent,
83-
null,
84-
"Event",
85-
to,
86-
from,
87-
"edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;fillColor=#1ba1e2;strokeColor=#006EAF;labelBackgroundColor=none;fontColor=#7EA6E0;"
88-
);
89-
} else {
90-
const edges = graph.getEdges(from);
91-
const existing = edges.filter((p) => p.target.value === to.value);
92-
if (existing.length > 0) {
93-
const edgeValue = pathToDescriptor(dependencyNode.path);
94-
if (edgeValue !== null) {
95-
existing[0].setValue(
96-
`${
97-
existing[0].getValue()
98-
? existing[0].getValue() + "\n"
99-
: ""
100-
}${edgeValue}`
101-
);
102-
}
103-
} else {
104-
graph.insertEdge(
105-
parent,
106-
null,
107-
pathToDescriptor(dependencyNode.path),
108-
from,
109-
to,
110-
"edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;labelBackgroundColor=none;fontColor=#EA6B66;"
111-
);
112-
}
113-
}
64+
const targets = vertices.filter((p) => p.name === dependency);
65+
const targetVertex = targets[0];
66+
if (!targetVertex) {
67+
continue;
11468
}
69+
let from = sourceVertex.vertex;
70+
let to = targetVertex.vertex;
71+
addEdges(from, to, dependencyNode);
11572
}
11673
}
11774
}
11875
} catch (err) {
11976
console.log(err);
12077
} finally {
78+
layout.execute(parent);
79+
forceLayoutRender = false;
12180
graph.getModel().endUpdate();
12281
}
123-
124-
layout.execute(parent);
12582
return graph;
12683
}
12784

85+
function addEdges(from, to, dependencyNode) {
86+
if (from && to) {
87+
const existingEdges = Object.keys(graph.model.cells).filter(
88+
(c) => c === edgeId(to, from)
89+
);
90+
if (existingEdges.length > 0) {
91+
const existingEdge = graph.model.cells[existingEdges[0]];
92+
if (!existingEdge.value.includes(pathToDescriptor(dependencyNode.path))) {
93+
existingEdge.value += `\n${pathToDescriptor(dependencyNode.path)}`;
94+
}
95+
return;
96+
}
97+
if (dependencyNode.path.indexOf("Properties.Events") > 0) {
98+
graph.insertEdge(
99+
parent,
100+
edgeId(to, from),
101+
"Invoke",
102+
to,
103+
from,
104+
"edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;fillColor=#1ba1e2;strokeColor=#006EAF;labelBackgroundColor=none;fontColor=#7EA6E0;"
105+
);
106+
} else {
107+
graph.insertEdge(
108+
parent,
109+
edgeId(to, from),
110+
pathToDescriptor(dependencyNode.path),
111+
from,
112+
to,
113+
"edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;labelBackgroundColor=none;fontColor=#EA6B66;"
114+
);
115+
}
116+
}
117+
}
118+
119+
function addVertices(resource, dependencies, type) {
120+
if (vertices.filter((p) => p.name === resource).length === 0) {
121+
vertices.push({
122+
name: resource,
123+
dependencies: dependencies,
124+
type: type,
125+
vertex: graph.insertVertex(
126+
parent,
127+
null,
128+
resource,
129+
locationCache[resource] ? locationCache[resource].x : 70,
130+
locationCache[resource] ? locationCache[resource].y : 0,
131+
50,
132+
50,
133+
iconMap.getIcon(type)
134+
),
135+
});
136+
}
137+
}
138+
139+
function getDependencies(template, resource, resourceTypesToInclude) {
140+
const dependencies = [];
141+
jsonUtil.findAllValues(template.Resources[resource], dependencies, "Ref");
142+
jsonUtil.findAllValues(
143+
template.Resources[resource],
144+
dependencies,
145+
"Fn::GetAtt"
146+
);
147+
for (const dependency of dependencies) {
148+
dependency.value = dependency.value.filter(
149+
(p) =>
150+
template.Resources[p] &&
151+
resourceTypesToInclude.includes(template.Resources[p].Type)
152+
);
153+
}
154+
return dependencies;
155+
}
156+
157+
function updateFilters(type, resource) {
158+
const cells = graph.model.cells;
159+
const keys = Object.keys(cells);
160+
keys.map(
161+
(p) =>
162+
(locationCache[cells[p].value] = cells[p].geometry
163+
? { x: cells[p].geometry.x, y: cells[p].geometry.y }
164+
: null)
165+
);
166+
if (vertices.filter((p) => p.type === type).length) {
167+
const item = vertices.filter((p) => p.name === resource)[0];
168+
if (item) {
169+
graph.removeCells([item.vertex], true);
170+
}
171+
vertices = vertices.filter((p) => p.name != resource);
172+
}
173+
}
174+
175+
function edgeId(to, from) {
176+
return `${to.value}|${from.value}`; //|${pathToDescriptor(dependencyNode.path)}`;
177+
}
178+
128179
function pathToDescriptor(path) {
129180
if (path.startsWith("$.Properties.Environment")) {
130-
//return "Variable";
181+
return path.split(".").slice(-1)[0];
131182
}
132183

133184
if (path.startsWith("$.Properties.Policies")) {
134185
const split = path.split(".");
135186
return split[3];
136187
}
137-
return null;
188+
return "";
138189
}
139190

140191
function graphToXML(graph) {
@@ -143,8 +194,8 @@ function graphToXML(graph) {
143194
return mxUtils.getXml(result);
144195
}
145196

146-
function renderTemplate(template, resources, layout) {
147-
const xml = graphToXML(makeGraph(template, resources, layout));
197+
function renderTemplate(template, resourceTypes, resourceNames) {
198+
const xml = graphToXML(makeGraph(template, resourceTypes, resourceNames));
148199
return xml;
149200
}
150201

0 commit comments

Comments
 (0)