-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathindex.html
698 lines (627 loc) · 25.5 KB
/
index.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
<!DOCTYPE html>
<!--
This page provides an interactive tree view of a Kubernetes Helm chart and its dependencies.
See https://github.com/melahn/helm-inspector for details
-->
<meta charset="UTF-8">
<style>
.svg-background {
background-color: #607D8B
}
/* Style for the inspector view */
.inspector {
fill: #FFEBEE;
stroke: black;
stroke-width: 3px;
height: 250px;
width: 400px;
}
/* Style for the rectangles */
.node rect {
fill: #B0BEC5;
stroke: black;
stroke-width: 3px;
height: 80px;
width: 200px;
}
/* Style for text using for chart or image titles */
.node text {
font: 12px sans-serif;
}
/* Style for the text used in the node details */
.detailstext {
font: 12px sans-serif;
}
/* Style for the separator used in the node details */
hr {
border-top: 1px solid grey;
}
/* Style for the links between nodes */
.link {
fill: none;
stroke: #ccc;
stroke-width: 2px;
}
/* Styles for the selector text */
.selector {
font: 18px sans-serif;
}
/* Style for the selector text */
.selector-bold {
font: 18px sans-serif;
font-weight: bold;
color: black;
}
</style>
<body>
<!-- load the d3.js V5 library -->
<script src="https://d3js.org/d3.v5.min.js"></script>
<script>
// setup the parameters of the tree
var margin = {top: 100, right: 90, bottom: 30, left: 120},
width = 1800 - margin.left - margin.right,
height = 3000 - margin.top - margin.bottom;
// create a place holder to show the mode and some instructions
d3.select("body").append("div").append("p").attr("id", "selectorMsg");
// create an svg for the tree to live in
var svg = d3.select("body").append("svg")
.attr("class", "svg-background")
.attr("width", width + margin.right + margin.left)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate("
+ margin.left + "," + margin.top + ")");
// 'Navigate' mode is when the user can expand the tree elements by clicking on them
// 'Inspect' mode is when the user can hover over a node to see the details
const mode = {
NAVIGATE: 'Navigate',
INSPECT: 'Inspect',
}
// Start out in 'Navigate' mode
var currentMode = mode.NAVIGATE;
var baseChartName;
var chartCount = 0, imageCount = 0, depthCount = 1, widthCount = 1;
// There are two ways for selecting the mode:
// Using the keyboard: pressing the 'i' or 'n' key
// Using the mouse: double clicking
d3.select('body')
.on("keypress", function () {
d3.event.key === "i" ? currentMode = mode.INSPECT : currentMode = mode.NAVIGATE;
writeModeMsg();
})
.on("dblclick", function () {
if (!skipDblClick) { // if the mouse is over a node, ignore it
currentMode === mode.INSPECT ? currentMode = mode.NAVIGATE : currentMode = mode.INSPECT;
writeModeMsg();
}
skipDblClick = false;
});
var i = 0, // used for node id's
filename = "helm-data.json", // the name of the file containing the helm data
// which can be overridden with a http parm
duration = 500, // controls the speed at which a tree node opens
// need to be careful to make sure that this is set in such a
// way that the mouse is not detected as the tree expands
detailsHeight = 290, // controls the height of the details shown in 'inspection node'
detailsWidth = 400, // controls the width of the details shown in 'inspection node'
detailsTopMargin = 60, // controls the offset of the details shown in 'inspection node'
detailsLeftMargin = 28, // controls the offset of the details shown in 'inspection node'
root, // the root of the tree
MAXNAMELENGTH = 30, // max name of chart or image to be displayed in details
treeData, // where the data is loaded
skipDblClick = false; // used to ignore dblClick when the mouse is over a node
// declares a tree layout and assigns the size
var treeMap = d3.tree().size([height, width]);
d3.json(getFilename()).then(function (treeData) { // get the json data can drop into a function
// to process it
// Get some stats about the shape of the tree
var treeLevels = new Map(); // This is a map of how may elements are at each level
treeLevels.set(1, 1) // level 1 always has one element in it
treeStats(treeData, 1); // recursively get tree stats
// Save the base name of the chart to make it easy to reference and
// write some details about mode
baseChartName = treeData["name"];
writeNavigateMsg();
// Assign the tree root and position it in the middle of
// svg
root = d3.hierarchy(treeData, function (d) {
return d.children;
});
root.x0 = height / 2;
root.y0 = 0;
// The tree starts up with the first two levels shown
// TODO Think more whether users want to see the whole tree first ... most are pretty shallow
//root.children.forEach(collapse);
// The inspector node is what surfaces when hovering over a node
// when in inspect mode
var inspectorNode = d3.select('svg')
.append('g')
.attr("id", "g2")
.append('rect')
.attr('id', 'i1')
.attr('x', -800)
.attr('y', -800)
.attr('rx',8)
.attr('ry',8)
.attr('class', 'inspector')
.style('opacity', '0');
// When the user hovers over the inspector set the opacity so it pops
d3.select("#i1")
.on('mouseover', function (d) {
if (currentMode === mode.INSPECT) {
d3.select("#i1").style('opacity', 1.0);
d3.select(this).style("cursor", "pointer")
}
})
.attr('y', 55);
/**
* Collapse a node and all its children
*
* @param {object} d the node data
* @returns null
*/
function collapse(d) {
if (d.children) {
d._children = d.children;
d._children.forEach(collapse);
d.children = null
}
}
/**
* Collect some stats about the tree
*
* @param {int} t the tree node at this iteration
* @param (int} l the current depth
* @returns null
*/
function treeStats(t, l) {
var p = l; // the current depth (plumb)
if (p > depthCount) {
depthCount = p; // remember the max depth
}
if (t["type"] === "chart") {
chartCount++
} else {
imageCount++
}
var i = 0; // need to reference this later
for (; i < t["children"].length; i++) {
if (i == 0) {
p++;
}
treeStats(t["children"][i], p);
}
// we are at depth p and found i nodes
// update the map that keeps track of how
// many nodes are at each level
var j = treeLevels.get(p);
j = (j == null) ? j = i : j = i + j;
treeLevels.set(p, j);
if (j > widthCount) {
widthCount = j;
}
}
// update the tree starting with the root node
update(root);
/**
* Update the tree starting at some point in the tree
*
* @param {object} source the node at which point update is done
* @returns null
*/
function update(source) {
var treeData = treeMap(root);
var nodes = treeData.descendants(),
links = treeData.descendants().slice(1);
// each parent-child layer in the tree is a fixed distance
nodes.forEach(function (d) {
d.y = d.depth * 270 // TODO derive from style sheets rect width value
});
// update the nodes with an id
var node = svg.selectAll('g.node')
.data(nodes, function (d) {
return d.id || (d.id = ++i)
});
var nodeEnter = node.enter().append('g')
.attr('class', 'node')
.attr("transform", function (d) {
return "translate(" + source.y0 + "," + source.x0 + ")";
})
.on('click', click)
.on("dblclick", function () {
// ignore dblClick if the mouse is over a node
skipDblClick = true;
});
// add a rectangle for each new tree node and add the mouse functions
nodeEnter.append('rect')
// if the user hovers over the rectangle then change the
// pointer and surface the inspector view and format the details
// if in inspection mode
.attr("x", -60)
.attr("y", -40)
.attr("rx", 8)
.attr("ry", 8)
.on("mouseover", function (d) {
d3.select(this).style("cursor", "pointer");
if (currentMode === mode.INSPECT) {
var t = d3.select(this);
inspectorNode.style('opacity', 1.0)
.attr('x', d.y0 + detailsLeftMargin)
.attr('y', d.x0 - detailsTopMargin);
formatDetails(d3.select("#g2"), d)
}
})
// When leaving the rectangle restore the mouse pointer
.on("mouseout", function (d) {
d3.select(this).style("cursor", "default")
})
// use the Helm chart name or image name for the node label
nodeEnter.append('text')
.attr("y", 5)
.attr("x", -42)
.text(function (d) {
return truncateName(d.data.name, MAXNAMELENGTH);
});
var nodeUpdate = nodeEnter.merge(node);
// move the updated nodes to where they should be
nodeUpdate.transition()
.duration(duration)
.attr("transform", function (d) {
return "translate(" + d.y + "," + d.x + ")"
})
// set the rectangle color to provide an indicator of whether a node
// has visible children or not so the user knows whether there is more
// information available by clicking on the node in navigation mode.
nodeUpdate.select("rect")
.style("fill", function (d) {
console.log(d.type);
return d._children ? "#B0BEC5" :
(d.data.type == "chart")? "#ECEFF1" : "#FFE0B2";
})
// remove any nodes that should disappear because of collapsing
var nodeExit = node.exit().transition()
.duration(duration)
.attr("transform", function (d) {
return "translate(" + source.y + "," + source.x + ")";
})
.remove()
var link = svg.selectAll('path.link')
.data(links, function (d) {
return d.id;
});
// attach any new nodes that have appeared
var linkEnter = link.enter().insert('path', "g")
.attr("class", "link")
.attr('d', function (d) {
var o = {x: source.x0, y: source.y0};
return diagonal(o, o)
});
var linkUpdate = linkEnter.merge(link);
linkUpdate.transition()
.duration(duration)
.attr('d', function (d) {
return diagonal(d, d.parent)
});
// remove any links that should disappear because of collapsing
var linkExit = link.exit().transition()
.duration(duration)
.attr('d', function (d) {
var o = {x: source.x, y: source.y};
return diagonal(o, o)
})
.remove();
nodes.forEach(function (d) {
d.x0 = d.x;
d.y0 = d.y;
});
/**
* Compute a path that will produce a pleasing
* smooth line
*
* @param {object} s coordinates
* @param {object} d coordinates
* @returns null
*/
function diagonal(s, d) {
path = `M ${s.y} ${s.x}
C ${(s.y + d.y) / 2} ${s.x},
${(s.y + d.y) / 2} ${d.x},
${d.y} ${d.x}`;
return path
}
/**
* Handle click event by toggling the children's visibility
* _children is the pointer to hidden children
* children is the pointer to visible children
*
* @param {object} d the node data
* @returns null
*/
function click(d) {
if (d.children) {
d._children = d.children;
d.children = null;
} else {
d.children = d._children;
d._children = null;
}
update(d);
}
/**
* Formats the description for a single node in the tree when in
* inspection mode
*
* @param {object} t the element to append to. Note this is an element
* that is selected using d3.select, not a DOM element
* @param {object} d the data object
* @return null
*/
function formatDetails(t, d) {
// create a foreign object so html can be used for text formatting
// of the node details
var browserAdjust = (detectUserAgent() === "safari" || isTouch())? 17:0; // adjust for diffs in vertical
// spacing
console.log("browser adjust = " + browserAdjust);
fo = t.append('foreignObject')
.attr("id", "fo")
.attr('x', d.y0 + detailsLeftMargin + 2)
.attr('y', d.x0 - (detailsTopMargin + 7 - browserAdjust))
.attr("height", detailsHeight)
.attr("width", detailsWidth - 4)
// on mouse out move the inspector out of the way and then remove the foreign object itself
.on("mouseout", function (d) {
d3.select("#i1")
.style('opacity', 0)
.attr('x', -800)
.attr('y', -800);
d3.select("#fo")
.remove()
})
var div = fo.append('xhtml:div')
.append('div');
var p = div.append("p")
.attr('class', 'detailstext');
// format the details using html
var s = "<h2 style=\"text-align:center\">" + d.data["name"] + "<hr></h2>";
var i = 0; // line counter of actual text
// the reason I don't just iterate through the array of values in the JSON
// file is that order of those elements is random and I want them in a
// prescribed order; I also want to filter the properties by type
var details;
if (d.data["type"] === "chart") {
details = formatChartDetails(d)
} else {
details = formatImageDetails(d)
}
s += details["details"];
i += details["lines"];
// pad the rectangle with spaces so the mouse doesn't get confused
// by the underlying rectangle
while (++i < 13) {
for (var j = 0, z = ""; j < 74; j++) {
z += " "
}
s += "<br/>" + z
}
p.html(s)
}
/**
* Formats the description for a chart
*
* @param {object} d the data for the element
* @return {object} object containing the details and the number of lines
*/
function formatChartDetails(d) {
// if the name is really long then truncate. The full name is in the header anyway
var s = " " + "name" + ": " + d.data["name"] + "<br/>";
s += " " + "type" + ": " + d.data["type"] + "<br/>";
s += " " + "version" + ": " + d.data["version"] + "<br/>";
s += " " + "description" + ": " + formatValue(d.data["description"]) + "<br/>";
s += " " + "maintainers" + ": " + formatMaintainers(d.data["maintainers"]) + "<br/>";
s += " " + "keywords" + ": " + formatValue(d.data["keywords"]) + "<br/>";
return {"details": s, "lines": 6}
}
/**
* Formats the description for an image
*
* @param {object} d the data for the element
* @return {object} object containing the details and the number of lines
*/
function formatImageDetails(d) {
// if the name is really long then truncate. The full name is in the header anyway
var s = " " + "name" + ": " + d.data["name"] + "<br/>";
s += " " + "type" + ": " + d.data["type"] + "<br/>";
s += " " + "version" + ": " + formatValue(d.data["version"]) + "<br/>";
s += " " + "repoHost" + ": " + formatValue(d.data["repoHost"]) + "<br/>";
return {"details": s, "lines": 4}
}
/**
* Provides a readable version of a value by, for example,
* checking for null
*
* @param {object} s the string to be formatted
* @return string containing more readable version of the string
*/
function formatValue(s) {
if (s == null) {
return "not specified"
}
return s
}
/**
* Provides a readable version of the maintainers property
* checking for null
*
* @param {object} a an array of maintainers
* @return string containing more readable version of the string
*/
function formatMaintainers(a) {
if (a == null) {
return "not specified";
}
// otherwise this is array of maintainers objects which
// may be just email or both name and email
var s = " ";
for (var i in a) {
if (i > 0) {
s += ","
}
var name = a[i]["name"];
var email = a[i]["email"];
if (email != null) {
s += name + "(" + email + ")";
} else {
s += name;
}
}
return s
}
/**
* Shortens a name, adding an ellipsis if needed
*
* @param {string} s the string to be shortened
* @param {int} n max size of the string before shortening will be done
* @return string containing more readable version of the string
*/
function truncateName(s, n) {
var t;
var e = "...";
if (s.length > n) {
t = s.substring(0, n - e.length);
t += e;
return t;
}
return s;
}
}
})
/**
* Writes the mode message
*
* @returns {string} currentMode
*/
function writeModeMsg() {
currentMode === mode.NAVIGATE ? writeNavigateMsg() : writeInspectMsg();
return currentMode;
}
/**
* Writes some information about the Navigation mode
*
* @returns {String} mode.NAVIGATE
*/
function writeNavigateMsg() {
var s = writeChartInfo();
s += "<p>Current Mode: <span class=\"selector-bold\">Navigate</span>";
s += "<p>Click on a node to expand or collapse it.";
s += "<p>Double-click to switch modes. Alternatively, ";
s += "press the <span class=\"selector-bold\">'i'</span> key to go into 'Inspect' mode</p></span>";
writeHtml("selectorMsg", s);
return mode.NAVIGATE;
}
/**
* Writes some information about the Inspect mode
*
* @returns {String} mode.INSPECT
*/
function writeInspectMsg() {
var s = writeChartInfo();
s += "<p>Current Mode: <span class=\"selector-bold\">Inspect</span>";
s += "<p>Hover over a node to see details about it.";
s += "<p>Double-click to switch modes. Alternatively, ";
s += "press the <span class=\"selector-bold\">'n'</span> key to go into 'Navigate' mode</p></span>";
writeHtml("selectorMsg", s);
return mode.INSPECT;
}
/**
* Creates a line of html with some information about the Chart.
*
* @returns {string} a line of html with some information about the Chart
*/
function writeChartInfo() {
var s = "<span class=\"selector\"><p>Base Chart: " + "<span class=\"selector-bold\">" + baseChartName + "</span>";
var i = " "
s += "<br>" + i + "Number of charts: " + chartCount;
s += "<br>" + i + "Number of images: " + imageCount;
s += "<br>" + i + "Depth: " + depthCount;
s += "<br>" + i + "Width: " + widthCount;
s += "</div>";
return s;
}
/**
* Writes some html
*
* @param {object} i the id of the object to which the html will
* be attached
* @param {string} h the html
* @returns null
*/
function writeHtml(i, h) {
// check if there was html here before and remove if so
var fo = d3.select("#" + "selectorMsg" + "fo");
if (!fo.empty()) {
fo.remove();
}
// add a p with html
var p = d3.select("#" + i)
.append("foreignObject")
.attr("id", "selectorMsg" + "fo")
.append('div')
.append("p")
.attr('class', 'detailstext');
p.html(h)
}
/**
* Returns the name of the file containing JSON data for
* the chart. First a url parm named 'chart' is looked for.
* That is returned if found. Otherwise the default filename
* is returned.
*
* @returns {string} the name of the file containing JSON data for
* the chart
*/
function getFilename() {
var f = filename; // use the default name unless an http parm named 'chart' is found
for (var i = 0, q = window.location.search.substring(1), v = q.split("&"); i < v.length; i++) {
var p = v[i].split("=");
if (p[0] === "chart") {
f = p[1] + ".json";
break;
}
}
return (f);
}
/**
* Detects the current browser
* Note: Derived from duck type method from https://stackoverflow.com/questions/9847580/how-to-detect-safari-chrome-ie-firefox-and-opera-browser
*
* @returns {string} the browser that was detected
*/
function detectUserAgent() {
var isOpera = (!!window.opr && !!opr.addons) || !!window.opera || navigator.userAgent.indexOf(' OPR/') >= 0;
var isFirefox = typeof InstallTrigger !== 'undefined';
var isSafari = /constructor/i.test(window.HTMLElement) || (function (p) {
return p.toString() === "[object SafariRemoteNotification]";
})(!window['safari'] || (typeof safari !== 'undefined' && safari.pushNotification));
var isIE = /*@cc_on!@*/false || !!document.documentMode;
var isEdge = !isIE && !!window.StyleMedia;
var isChrome = !!window.chrome && (!!window.chrome.webstore || !!window.chrome.runtime);
var isEdgeChromium = isChrome && (navigator.userAgent.indexOf("Edg") != -1);
var isBlink = (isChrome || isOpera) && !!window.CSS;
return (isOpera) ? "opera" :
(isFirefox) ? "firefox" :
(isSafari) ? "safari" :
(isIE) ? "ie" :
(isEdge || isEdgeChromium) ? "edge" :
(isChrome) ? "chrome" :
(isBlink) ? "blink" : "unknown";
}
/**
* Detects whether the device is touch aware (usually mobile)
*
* @returns {boolean} true if the device is touch aware, false otherwise
*/
function isTouch() {
return navigator.maxTouchPoints > 1;
}
</script>
</body>