cytoscape.use(cytoscapeDagre);
cytoscape.use(cytoscapeKlay);
let cy;
let currentLayout;
const RESOURCE_CATEGORIES = {
'Workloads': ['Pod', 'Deployment', 'ReplicaSet', 'StatefulSet', 'DaemonSet', 'Job', 'CronJob', 'ReplicationController', 'PodTemplate'],
'Networking': ['Service', 'Ingress', 'IngressClass', 'NetworkPolicy', 'Endpoints', 'EndpointSlice', 'NetworkAttachmentDefinition'],
'Storage': ['PersistentVolumeClaim', 'PersistentVolume', 'StorageClass', 'CSIDriver', 'CSINode', 'CSIStorageCapacity', 'VolumeAttachment'],
'Configuration': ['ConfigMap', 'Secret'],
'Access Control': ['ServiceAccount', 'Role', 'RoleBinding', 'ClusterRole', 'ClusterRoleBinding', 'PodSecurityPolicy', 'User', 'Group'],
'Cluster & Ops': ['Node', 'Namespace', 'Event', 'HorizontalPodAutoscaler', 'VerticalPodAutoscaler', 'LimitRange', 'ResourceQuota', 'PodDisruptionBudget', 'PriorityClass', 'RuntimeClass', 'Lease'],
'Extensions': ['CustomResourceDefinition', 'APIService', 'MutatingWebhookConfiguration', 'ValidatingWebhookConfiguration']
};
function closeAllFilterPanels(except) {
document.querySelectorAll('.filter-dropdown.open').forEach(dropdown => {
if (dropdown !== except) dropdown.classList.remove('open');
});
}
function renderFilters() {
const container = document.getElementById('categoryFilters');
if (!container) return;
container.innerHTML = '';
for (const [category, kinds] of Object.entries(RESOURCE_CATEGORIES)) {
const dropdown = document.createElement('div');
dropdown.className = 'filter-dropdown';
const toggleBtn = document.createElement('button');
toggleBtn.type = 'button';
toggleBtn.className = 'filter-dropdown-toggle';
toggleBtn.textContent = category + ' ▾';
toggleBtn.addEventListener('click', (e) => {
e.stopPropagation();
const wasOpen = dropdown.classList.contains('open');
closeAllFilterPanels();
dropdown.classList.toggle('open', !wasOpen);
});
const panel = document.createElement('div');
panel.className = 'filter-dropdown-panel';
panel.addEventListener('click', (e) => e.stopPropagation());
const catLabel = document.createElement('label');
catLabel.className = 'filter-category-label';
const catCheck = document.createElement('input');
catCheck.type = 'checkbox';
catCheck.checked = true;
catCheck.className = 'category-checkbox';
catCheck.value = category;
catLabel.appendChild(catCheck);
catLabel.appendChild(document.createTextNode(' ' + category));
panel.appendChild(catLabel);
kinds.forEach(kind => {
const label = document.createElement('label');
label.className = 'filter-kind-label';
const cb = document.createElement('input');
cb.type = 'checkbox';
cb.checked = true;
cb.value = kind.toLowerCase();
cb.className = `kind-checkbox kind-${kind.toLowerCase()}`;
cb.dataset.category = category;
label.appendChild(cb);
label.appendChild(document.createTextNode(' ' + kind));
panel.appendChild(label);
cb.addEventListener('change', () => {
// If any child is unchecked, uncheck the parent. If all are checked, check it.
const allChecked = Array.from(panel.querySelectorAll('.kind-checkbox')).every(c => c.checked);
catCheck.checked = allChecked;
updateCategoryFilters();
updateDropdownToggleState(dropdown, toggleBtn, category, panel);
});
});
catCheck.addEventListener('change', (e) => {
const isChecked = e.target.checked;
panel.querySelectorAll('.kind-checkbox').forEach(cb => {
cb.checked = isChecked;
});
updateCategoryFilters();
updateDropdownToggleState(dropdown, toggleBtn, category, panel);
});
dropdown.appendChild(toggleBtn);
dropdown.appendChild(panel);
container.appendChild(dropdown);
}
document.addEventListener('click', () => closeAllFilterPanels());
}
function updateDropdownToggleState(dropdown, toggleBtn, category, panel) {
const checkboxes = panel.querySelectorAll('.kind-checkbox');
const checkedCount = Array.from(checkboxes).filter(cb => cb.checked).length;
dropdown.classList.toggle('filter-dropdown-partial', checkedCount > 0 && checkedCount < checkboxes.length);
dropdown.classList.toggle('filter-dropdown-empty', checkedCount === 0);
}
function updateCategoryFilters() {
if (!cy) return;
const kindCheckboxes = document.querySelectorAll('#categoryFilters .kind-checkbox');
const visibleKinds = new Set();
kindCheckboxes.forEach(cb => {
if (cb.checked) visibleKinds.add(cb.value);
});
cy.batch(() => {
cy.nodes().forEach(node => {
if (node.data('group') === 'cluster') return;
const kind = (node.data('kind') || '').toLowerCase();
let isKnown = false;
for (const kinds of Object.values(RESOURCE_CATEGORIES)) {
if (kinds.some(k => k.toLowerCase() === kind)) {
isKnown = true;
break;
}
}
if (isKnown) {
if (visibleKinds.has(kind)) {
node.style('display', 'element');
} else {
node.style('display', 'none');
}
} else {
node.style('display', 'element');
}
});
});
}
// Construit la liste d'éléments Cytoscape à partir d'un objet DOT_JSON
function buildElementsFromDotJson(json) {
const elements = [];
const nodes = json.objects || json.nodes || [];
const edges = json.edges || [];
const sortedNodes = [...nodes].sort((a, b) => {
const aIsCluster = a.nodes || a.subgraphs;
const bIsCluster = b.nodes || b.subgraphs;
if (aIsCluster && !bIsCluster) return -1;
if (!aIsCluster && bIsCluster) return 1;
return 0;
});
addNodesElementsParsedFromNodesJson(elements, sortedNodes);
addNodesEdgesParsedFromEdgesJson(elements, edges);
return elements;
}
// Rend le graphe directement depuis un objet DOT_JSON
window.renderFromDotJson = function (json) {
try {
const elements = buildElementsFromDotJson(json);
load_cytoscape(elements);
} catch (e) {
console.error('renderFromDotJson failed:', e);
}
};
/**
* 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() {
renderFilters();
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);
window.addEventListener('message', (e) => {
const data = e && e.data;
if (!data || data.type !== 'KD_LOAD_DOT_JSON' || !data.payload) return;
try {
window.renderFromDotJson(data.payload);
} catch (err) {
console.error('Failed to handle KD_LOAD_DOT_JSON:', err);
}
}, false);
// === (Optionnel) Autoload via ?dataUrl=... ===
try {
const params = new URLSearchParams(location.search);
const dataUrl = params.get('dataUrl');
if (dataUrl) {
fetch(dataUrl)
.then(r => r.json())
.then(json => window.renderFromDotJson(json))
.catch(err => console.error('Failed to load DOT_JSON via dataUrl:', err));
}
} catch (_) { /* ignore */ }
}
/**
* 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.onload = function(e) {
try {
const content = e.target.result;
const json = JSON.parse(content);
const elements = buildElementsFromDotJson(json);
load_cytoscape(elements);
} catch (err) {
console.error('Invalid DOT_JSON file:', err);
}
};
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);
updateCategoryFilters();
cy.layout(currentLayout).run();
}
function load_layout(layout) {
currentLayout = layout;
cy.layout(layout).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 tooltip = nodesJson[i].tooltip ?? '';
let kindMatch = tooltip.match(/kind:\s*([A-Za-z0-9_]+)/i);
let kind = kindMatch ? kindMatch[1] : '';
let image = nodesJson[i].image ?? '';
let label = nodesJson[i].label ?? '';
if (!image && typeof label === 'string' && label.includes(']+src="([^"]+)"/);
if (imgMatch) {
image = imgMatch[1];
}
// Try to extract only the text part from the table structure for the label
let textMatch = label.match(/