mirror of
https://github.com/philippemerle/KubeDiagrams.git
synced 2026-05-18 09:46:33 +00:00
Merge pull request #40 from AngelHely/main/interactive_viewer
KubeDiagrams Interactive Viewer
This commit is contained in:
BIN
interactive_viewer/src/KubeDiagrams.png
Normal file
BIN
interactive_viewer/src/KubeDiagrams.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 152 KiB |
46
interactive_viewer/src/index.html
Normal file
46
interactive_viewer/src/index.html
Normal file
@@ -0,0 +1,46 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>KubeDiagrams Interactive Viewer</title>
|
||||
<!-- cytoscape -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/cytoscape@3.32.0/dist/cytoscape.min.js"></script>
|
||||
|
||||
<!-- layout dagre -->
|
||||
<script src=https://cdn.jsdelivr.net/npm/dagre@0.8.5/dist/dagre.min.js></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/cytoscape-dagre@2.5.0/cytoscape-dagre.min.js"></script>
|
||||
|
||||
<!-- layout klay -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/klayjs@0.4.1/klay.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/cytoscape-klay@3.1.4/cytoscape-klay.min.js"></script>
|
||||
|
||||
<!-- cytoscape-context-menus -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/cytoscape-context-menus@4.2.1/cytoscape-context-menus.min.js"></script>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/cytoscape-context-menus@4.2.1/cytoscape-context-menus.min.css">
|
||||
|
||||
<!-- KubeDiagrams Interactive Viewer -->
|
||||
<script src="./script/defaultStyle.js"></script>
|
||||
<script src="./script/layout.js"></script>
|
||||
<script src="./script/itemAndFunctionMenus.js"></script>
|
||||
<script src="./script/main.js"></script>
|
||||
<link rel="stylesheet" href="./style.css">
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<h1><img style="vertical-align: middle;" src="KubeDiagrams.png" height="80"/> <text style="color: #326CE5;">Interactive Viewer</text></h1>
|
||||
<div class="buttons">
|
||||
Select a .dot_json file:
|
||||
<input type="file" id="fileInput"/>
|
||||
Choose a layout
|
||||
<div id="layoutButtons"></div>
|
||||
Save as
|
||||
<div id="saveButtons">
|
||||
<button id="savePNG">PNG</button>
|
||||
<button id="saveJPG">JPG</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="paper"></div>
|
||||
<pre id="tooltip"></pre>
|
||||
</body>
|
||||
</html>
|
||||
92
interactive_viewer/src/script/defaultStyle.js
Normal file
92
interactive_viewer/src/script/defaultStyle.js
Normal file
@@ -0,0 +1,92 @@
|
||||
let clusterStyleList = [];
|
||||
let nodeStyleList = [];
|
||||
let edgeStyleList = [];
|
||||
|
||||
function getDefaultGlobalNodeStyleFromNodeValues(node) {
|
||||
return {
|
||||
selector : ".nodeStyle" + nodeStyleList.length,
|
||||
style : {
|
||||
'color': node.data.fontcolor,
|
||||
'font-size': node.data.fontsize,
|
||||
'font-family': node.data.fontfamily,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function getDefaultClusterStyleFromClusterValues(cluster) {
|
||||
return {
|
||||
selector : ".clusterStyle" + clusterStyleList.length,
|
||||
style : {
|
||||
'border-style': cluster.data.bs,
|
||||
'border-color': cluster.data.bc,
|
||||
'background-color': cluster.data.bgcolor,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function getDefaultEdgeStyleFromEdgeValues(edge) {
|
||||
return {
|
||||
selector : ".edgeStyle" + edgeStyleList.length,
|
||||
style : {
|
||||
'line-color': edge.data.color,
|
||||
'target-arrow-color': edge.data.color,
|
||||
'source-arrow-color': edge.data.color,
|
||||
'line-style': edge.data.line_style,
|
||||
'color': edge.data.fontcolor,
|
||||
'font-size': edge.data.fontsize,
|
||||
'font-family': edge.data.fontfamily,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const clusterClosedStyle = {
|
||||
'text-wrap': 'wrap',
|
||||
'text-max-width': '100px',
|
||||
'text-valign': 'center',
|
||||
'text-halign': 'center',
|
||||
'text-margin-y': 0,
|
||||
'width': 50,
|
||||
'height': 20,
|
||||
}
|
||||
|
||||
const clusterOpenStyle = {
|
||||
'text-valign': 'top',
|
||||
'text-wrap': 'none',
|
||||
'text-margin-y': 15,
|
||||
'padding': '15px',
|
||||
}
|
||||
|
||||
const defaultGroupNodeStyle = {
|
||||
'background-fit': 'cover',
|
||||
'background-image': 'data(image)',
|
||||
'background-opacity': 0,
|
||||
'background-clip': 'node',
|
||||
'width': 100,
|
||||
'height': 100,
|
||||
'text-valign' : 'bottom',
|
||||
'text-wrap': 'wrap',
|
||||
}
|
||||
|
||||
const defaultGlobalNodeStyle = {
|
||||
'content': 'data(label)',
|
||||
'min-zoomed-font-size': '8'
|
||||
}
|
||||
|
||||
const defaultEdgeStyle = {
|
||||
'source-endpoint': 'outside-to-node-or-label',
|
||||
'target-endpoint': 'outside-to-node-or-label',
|
||||
'width': 1,
|
||||
'curve-style': 'bezier',
|
||||
'label': 'data(xlabel)',
|
||||
'min-zoomed-font-size': '8',
|
||||
}
|
||||
|
||||
const defaultEdgeDirForward = {
|
||||
'target-arrow-shape': 'triangle',
|
||||
'target-arrow-fill': 'filled',
|
||||
}
|
||||
|
||||
const defaultEdgeDirBack = {
|
||||
'source-arrow-shape': 'triangle',
|
||||
'source-arrow-fill': 'filled',
|
||||
}
|
||||
35
interactive_viewer/src/script/itemAndFunctionMenus.js
Normal file
35
interactive_viewer/src/script/itemAndFunctionMenus.js
Normal file
@@ -0,0 +1,35 @@
|
||||
const itemOpenClose = {
|
||||
id: 'co',
|
||||
content: 'close/open',
|
||||
tooltipText: 'close/open',
|
||||
selector: 'node[group = "cluster"]',
|
||||
onClickFunction: function (event) {
|
||||
const cluster = event.target;
|
||||
if (cluster.data('isClose')) {
|
||||
openCluster(cluster);
|
||||
}
|
||||
else {
|
||||
closeCluster(cluster);
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a cluster
|
||||
* @param {*} cluster
|
||||
*/
|
||||
function openCluster(cluster) {
|
||||
cluster.children().style('display', 'element');
|
||||
cluster.data('isClose', false);
|
||||
cluster.style(clusterOpenStyle);
|
||||
}
|
||||
|
||||
/**
|
||||
* Close a cluster
|
||||
* @param {*} cluster
|
||||
*/
|
||||
function closeCluster(cluster) {
|
||||
cluster.children().style('display', 'none');
|
||||
cluster.data('isClose', true);
|
||||
cluster.style(clusterClosedStyle);
|
||||
}
|
||||
22
interactive_viewer/src/script/layout.js
Normal file
22
interactive_viewer/src/script/layout.js
Normal file
@@ -0,0 +1,22 @@
|
||||
const layoutDagre = {name: 'dagre',
|
||||
rankDir: 'TB',
|
||||
ranker: 'network-simplex',
|
||||
nodeSep: 25,
|
||||
avoidOverlap: true,
|
||||
fit: true,
|
||||
}
|
||||
|
||||
const layoutKlay = {
|
||||
name: 'klay',
|
||||
klay: {
|
||||
direction: 'DOWN',
|
||||
layoutHierarchy: true,
|
||||
spacing: 50,
|
||||
edgeSpacingFactor: 0,
|
||||
fixedAlignment: 'RIGHTUP',
|
||||
nodeLayering:'INTERACTIVE',
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const layoutList = [layoutDagre, layoutKlay];
|
||||
362
interactive_viewer/src/script/main.js
Normal file
362
interactive_viewer/src/script/main.js
Normal file
@@ -0,0 +1,362 @@
|
||||
cytoscape.use(cytoscapeDagre);
|
||||
cytoscape.use(cytoscapeKlay);
|
||||
|
||||
let cy;
|
||||
let currentLayout;
|
||||
|
||||
/**
|
||||
* initialise a cytoscape instance, create layout selector buttons, add event listeners on save buttons and add
|
||||
* an event listener on the file input button to load the cytoscape graph.
|
||||
*/
|
||||
function setUp() {
|
||||
cy = getCyGraph();
|
||||
currentLayout = layoutList[0];
|
||||
createLayoutSelectorButton();
|
||||
document.getElementById("savePNG").addEventListener("click", () => { saveFile("png")});
|
||||
document.getElementById("saveJPG").addEventListener("click", () => { saveFile("jpg")});
|
||||
document.getElementById('fileInput').addEventListener('change', readFileAndloadCytoscapeGraph);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* return a cytoscape graph.
|
||||
* @returns
|
||||
*/
|
||||
function getCyGraph() {
|
||||
return cytoscape({
|
||||
container: document.getElementById('paper'),
|
||||
elements: undefined,
|
||||
hideEdgesOnViewport: true,
|
||||
pixelRatio: window.devicePixelRatio,
|
||||
style: [
|
||||
{
|
||||
selector: 'node',
|
||||
style: defaultGlobalNodeStyle
|
||||
},
|
||||
{
|
||||
selector: 'node[group = "cluster"]',
|
||||
style: clusterOpenStyle
|
||||
},
|
||||
{
|
||||
selector: 'node[group = "node"]',
|
||||
style: defaultGroupNodeStyle
|
||||
},
|
||||
{
|
||||
selector: 'edge',
|
||||
style: defaultEdgeStyle
|
||||
},
|
||||
{
|
||||
selector: 'edge[dir = "forward"]',
|
||||
style: defaultEdgeDirForward
|
||||
},
|
||||
{
|
||||
selector: 'edge[dir = "back"]',
|
||||
style: defaultEdgeDirBack
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a dot_json file to add nodes and edges in a elements list used to load a cytoscape instance.
|
||||
* @param {*} event
|
||||
*/
|
||||
function readFileAndloadCytoscapeGraph(event) {
|
||||
const file = event.target.files[0];
|
||||
const reader = new FileReader();
|
||||
const elements = [];
|
||||
|
||||
reader.onload = function(e) {
|
||||
let content = e.target.result;
|
||||
const json = JSON.parse(content);
|
||||
const nodes = json.objects;
|
||||
const edges = json.edges;
|
||||
addNodesElementsParsedFromNodesJson(elements, nodes);
|
||||
addNodesEdgesParsedFromEdgesJson(elements, edges);
|
||||
load_cytoscape(elements);
|
||||
};
|
||||
reader.readAsText(file);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove precedent elements before to add the new elements then create tool tip and context menus for the
|
||||
* cytoscape instance.
|
||||
* @param {*} elements
|
||||
*/
|
||||
function load_cytoscape(elements) {
|
||||
cy.nodes().remove();
|
||||
cy.add(elements);
|
||||
createTooltip("node");
|
||||
createTooltip("edge");
|
||||
createAndGetContextMenu(cy);
|
||||
cy.layout(currentLayout).run();
|
||||
}
|
||||
|
||||
function load_layout(layout) {
|
||||
currentLayout = layout;
|
||||
cy.layout(currentLayout).run();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add nodes elements created from nodesJson data list in the elements list.
|
||||
* @param {*} elements
|
||||
* @param {*} nodesJson - nodes data list
|
||||
* @returns
|
||||
*/
|
||||
function addNodesElementsParsedFromNodesJson(elements, nodesJson) {
|
||||
let parent = {};
|
||||
|
||||
for (let i in nodesJson) {
|
||||
let node = {
|
||||
data: {
|
||||
id: nodesJson[i]._gvid,
|
||||
group: (nodesJson[i].nodes) ? 'cluster' : 'node',
|
||||
isClose : false,
|
||||
label: (nodesJson[i].label.includes('<') && nodesJson[i].label.includes('>')) ? nodesJson[i].tooltip : nodesJson[i].label,
|
||||
bs: getCorrespondingBorderStyle(nodesJson[i].style),
|
||||
bgcolor: nodesJson[i].bgcolor ?? 'blue',
|
||||
bc: nodesJson[i].pencolor ?? 'gray',
|
||||
parent: parent[nodesJson[i]._gvid] ?? '',
|
||||
fontsize: nodesJson[i].fontsize ?? '',
|
||||
fontfamily: nodesJson[i].fontname ?? '',
|
||||
fontcolor: nodesJson[i].fontcolor ?? '',
|
||||
image: (nodesJson[i].image) ? getURLImage(nodesJson[i].image) : '',
|
||||
tooltip: nodesJson[i].tooltip ?? ''
|
||||
}
|
||||
}
|
||||
|
||||
if (nodesJson[i].nodes) {
|
||||
for (let child of nodesJson[i].nodes) {
|
||||
parent[child] = nodesJson[i]._gvid;
|
||||
}
|
||||
}
|
||||
|
||||
if (nodesJson[i].subgraphs) {
|
||||
for (let sub of nodesJson[i].subgraphs) {
|
||||
parent[sub] = nodesJson[i]._gvid;
|
||||
}
|
||||
}
|
||||
addClassToElement(node);
|
||||
elements.push(node);
|
||||
}
|
||||
|
||||
return elements;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add nodes elements created from edgesJson data list in the elements list.
|
||||
* @param {*} elements
|
||||
* @param {*} edgesJson
|
||||
*/
|
||||
function addNodesEdgesParsedFromEdgesJson(elements, edgesJson) {
|
||||
for (let e in edgesJson) {
|
||||
let edge = {
|
||||
data: {
|
||||
id: 'e' + edgesJson[e]._gvid,
|
||||
group: 'edge',
|
||||
dir: edgesJson[e].dir,
|
||||
source: edgesJson[e].tail,
|
||||
target: edgesJson[e].head,
|
||||
color: edgesJson[e].color,
|
||||
line_style: edgesJson[e].style ?? 'solid',
|
||||
xlabel: edgesJson[e].xlabel ?? '',
|
||||
fontsize: edgesJson[e].fontsize ?? '',
|
||||
fontfamily: edgesJson[e].fontname ?? '',
|
||||
fontcolor: edgesJson[e].fontcolor ?? '',
|
||||
tooltip: edgesJson[e].tooltip ?? '',
|
||||
}
|
||||
}
|
||||
addClassToElement(edge);
|
||||
elements.push(edge);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get border style based on the what's in the style parameter.
|
||||
* @param {*} style
|
||||
* @returns
|
||||
*/
|
||||
function getCorrespondingBorderStyle(style) {
|
||||
if (style.includes('dashed')) {
|
||||
return 'dashed';
|
||||
}
|
||||
else {
|
||||
return 'solid';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the corresponding url image based on the local url of the image parameter get from a dot_jon file.
|
||||
* @param {*} image
|
||||
* @returns
|
||||
*/
|
||||
function getURLImage(image) {
|
||||
let idx = image.indexOf("resources\/")
|
||||
if(idx != -1) {
|
||||
return "https://raw.githubusercontent.com/mingrammer/diagrams/refs/heads/master/" + image.slice(image.indexOf("resources"))
|
||||
} else {
|
||||
idx = image.indexOf("bin\/icons\/")
|
||||
if(idx != -1) {
|
||||
return "https://raw.githubusercontent.com/philippemerle/KubeDiagrams/refs/heads/main/" + image.slice(image.indexOf("bin\/icons\/"))
|
||||
} else {
|
||||
return image
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a HTML tooltip with the events of the cytoscape instance
|
||||
* @param {*} elementType
|
||||
*/
|
||||
function createTooltip(elementType) {
|
||||
const tooltip = document.getElementById('tooltip');
|
||||
let to;
|
||||
|
||||
cy.on('mouseover', elementType, function(event) {
|
||||
const element = event.target;
|
||||
to = setTimeout(() => {
|
||||
tooltip.style.display = 'block';
|
||||
tooltip.innerText = element.data('tooltip');
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
cy.on('mouseout', elementType, function(event) {
|
||||
clearTimeout(to);
|
||||
tooltip.style.display = 'none';
|
||||
});
|
||||
|
||||
cy.on('mousemove', function(event) {
|
||||
tooltip.style.left = (event.originalEvent.pageX + 10) + 'px';
|
||||
tooltip.style.top = (event.originalEvent.pageY + 10) + 'px';
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create layout selector buttons after the fileInput
|
||||
*/
|
||||
function createLayoutSelectorButton() {
|
||||
const layoutButtons = document.getElementById("layoutButtons");
|
||||
for (let layout of layoutList) {
|
||||
let button = document.createElement("button")
|
||||
button.id = layout.name;
|
||||
button.textContent = layout.name;
|
||||
button.addEventListener("click", () => {load_layout(layout)} );
|
||||
layoutButtons.appendChild(button);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save file in png or jpg format.
|
||||
* @param {*} format
|
||||
*/
|
||||
function saveFile(format) {
|
||||
const img = getFileAs(format);
|
||||
|
||||
const ele = document.createElement('a');
|
||||
ele.href = img;
|
||||
ele.download = 'graph.' + format;
|
||||
ele.click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a representation of the graph as an image of the requested format
|
||||
* @param {*} format
|
||||
* @returns
|
||||
*/
|
||||
function getFileAs(format) {
|
||||
switch (format) {
|
||||
case "png": return cy.png({full : true});
|
||||
case "jpg": return cy.jpg({full : true});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and return a context menus for the cytoscape instance
|
||||
* @param {*} cy
|
||||
* @returns
|
||||
*/
|
||||
function createAndGetContextMenu(cy) {
|
||||
return cy.contextMenus({
|
||||
evtType: 'cxttap',
|
||||
menuItems: [itemOpenClose]
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the element's group and then add a style class based on the group and his values.
|
||||
* @param {*} element
|
||||
*/
|
||||
function addClassToElement(element) {
|
||||
let classesName = [];
|
||||
let style;
|
||||
if (element.data.group == "edge") {
|
||||
style = getDefaultEdgeStyleFromEdgeValues(element);
|
||||
classesName.push(findAndGetClassStyle(style, edgeStyleList).selector.replace(/^\./, ''));
|
||||
}
|
||||
else {
|
||||
style = getDefaultGlobalNodeStyleFromNodeValues(element);
|
||||
classesName.push(findAndGetClassStyle(style, nodeStyleList).selector.replace(/^\./, ''));
|
||||
if (element.data.group == "cluster") {
|
||||
style = getDefaultClusterStyleFromClusterValues(element);
|
||||
classesName.push(findAndGetClassStyle(style, clusterStyleList).selector.replace(/^\./, ''))
|
||||
}
|
||||
}
|
||||
element.classes = classesName.join(' ');
|
||||
}
|
||||
|
||||
/**
|
||||
* Look for a style class similar to the styleTarget in the styleList. if a style class is founded,
|
||||
* so the method return the style class founded otherwise add the styleTarget in the cytoscape instance and
|
||||
* in the styleList then return the styleTarget
|
||||
* @param {*} styleTarget
|
||||
* @param {*} styleList
|
||||
* @returns
|
||||
*/
|
||||
function findAndGetClassStyle(styleTarget, styleList) {
|
||||
for (let style of styleList) {
|
||||
if (equals(styleTarget.style, style.style)) {
|
||||
return style;
|
||||
}
|
||||
}
|
||||
addStyleToCytoscapeGraph(styleTarget);
|
||||
styleList.push(styleTarget);
|
||||
return styleTarget;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new style in the style attribute of the cytoscape instance.
|
||||
* @param {*} style
|
||||
*/
|
||||
function addStyleToCytoscapeGraph(style) {
|
||||
const existingStyle = cy.style().json();
|
||||
cy.style([...existingStyle, style]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if two object are similar.
|
||||
* @param {*} o1
|
||||
* @param {*} o2
|
||||
* @returns
|
||||
*/
|
||||
function equals(o1, o2) {
|
||||
if (o1 === o2) return true;
|
||||
if (typeof o1 !== "object" || typeof o2 !== "object" || o1 == null || o2 == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const k1 = Object.keys(o1);
|
||||
const k2 = Object.keys(o2);
|
||||
|
||||
if (k1.length !== k2.length) return false;
|
||||
|
||||
for (let k of k1) {
|
||||
if (!k2.includes(k) || !equals(o1[k], o2[k])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
window.addEventListener("load", setUp);
|
||||
24
interactive_viewer/src/style.css
Normal file
24
interactive_viewer/src/style.css
Normal file
@@ -0,0 +1,24 @@
|
||||
#paper {
|
||||
width: 100%;
|
||||
height: 80vh;
|
||||
display: block;
|
||||
margin: auto;
|
||||
border: solid;
|
||||
}
|
||||
.buttons {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
#layoutButtons {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
#tooltip {
|
||||
position: absolute;
|
||||
display: none;
|
||||
background: white;
|
||||
border: solid;
|
||||
padding: 5px 10px;
|
||||
border-radius: 4px;
|
||||
font-size: 15px;
|
||||
}
|
||||
Reference in New Issue
Block a user