diff --git a/dist/dist.zip b/dist/dist.zip index 4dbb0235c..38fc2a072 100644 Binary files a/dist/dist.zip and b/dist/dist.zip differ diff --git a/dist/html-static/css/cy-style-dark.json b/dist/html-static/css/cy-style-dark.json index 368263b89..60d4b4540 100644 --- a/dist/html-static/css/cy-style-dark.json +++ b/dist/html-static/css/cy-style-dark.json @@ -197,10 +197,129 @@ "style": { "width": "8", "height": "8", - "background-image": "images/clab-bridge-light-grey.png", + "background-image": "images/clab-bridge-light-blue.png", "background-fit": "cover" } }, + + { + "selector": "node[topoViewerRole=\"pe\"][editor=\"true\"]", + "style": { + "width": "14", + "height": "14", + "background-image": "images/clab-pe-dark-blue.png", + "background-fit": "cover", + "border-width": "0.5px", + "border-color": "#32CD32" + } + }, + { + "selector": "node[topoViewerRole=\"p\"][editor=\"true\"]", + "style": { + "width": "14", + "height": "14", + "background-image": "images/clab-pe-dark-blue.png", + "background-fit": "cover", + "border-width": "0.5px", + "border-color": "#32CD32" + } + }, + { + "selector": "node[topoViewerRole=\"controller\"][editor=\"true\"]", + "style": { + "width": "14", + "height": "14", + "background-image": "images/clab-controller-light-blue.png", + "background-fit": "cover", + "border-width": "0.5px", + "border-color": "#32CD32" + } + }, + { + "selector": "node[topoViewerRole=\"pon\"][editor=\"true\"]", + "style": { + "width": "12", + "height": "12", + "background-image": "images/clab-pon-dark-blue.png", + "background-fit": "cover", + "border-width": "0.5px", + "border-color": "#32CD32" + } + }, + { + "selector": "node[topoViewerRole=\"dcgw\"][editor=\"true\"]", + "style": { + "width": "12", + "height": "12", + "background-image": "images/clab-dcgw-dark-blue.png", + "background-fit": "cover", + "border-width": "0.5px", + "border-color": "#32CD32" + } + }, + { + "selector": "node[topoViewerRole=\"leaf\"][editor=\"true\"]", + "style": { + "background-image": "images/clab-leaf-light-blue.png", + "background-fit": "cover", + "border-width": "0.5px", + "border-color": "#32CD32" + } + }, + { + "selector": "node[topoViewerRole=\"rgw\"][editor=\"true\"]", + "style": { + "background-image": "images/clab-rgw-light-blue.png", + "background-fit": "cover", + "border-width": "0.5px", + "border-color": "#32CD32" + } + }, + { + "selector": "node[topoViewerRole=\"super-spine\"][editor=\"true\"]", + "style": { + "width": "12", + "height": "12", + "background-image": "images/clab-spine-dark-blue.png", + "background-fit": "cover", + "border-width": "0.5px", + "border-color": "#32CD32" + } + }, + { + "selector": "node[topoViewerRole=\"spine\"][editor=\"true\"]", + "style": { + "width": "12", + "height": "12", + "background-image": "images/clab-spine-light-blue.png", + "background-fit": "cover", + "border-width": "0.5px", + "border-color": "#32CD32" + } + }, + { + "selector": "node[topoViewerRole=\"server\"][editor=\"true\"]", + "style": { + "width": "12", + "height": "12", + "background-image": "images/clab-server-dark-blue.png", + "background-fit": "cover", + "border-width": "0.5px", + "border-color": "#32CD32" + } + }, + { + "selector": "node[topoViewerRole=\"bridge\"][editor=\"true\"]", + "style": { + "width": "8", + "height": "8", + "background-image": "images/clab-bridge-light-blue.png", + "background-fit": "cover", + "border-width": "0.5px", + "border-color": "#32CD32" + } + }, + { "selector": "edge", "style": { diff --git a/dist/html-static/css/style.css b/dist/html-static/css/style.css index bf82ece60..73f2355a7 100755 --- a/dist/html-static/css/style.css +++ b/dist/html-static/css/style.css @@ -154,6 +154,26 @@ body { position: fixed; } +#panel-node-editor { + bottom: 10px; + left: 40px; + width: 340px; + max-height: 700px; + overflow-y: auto; + overflow-x: hidden; + position: fixed; +} + +#panel-node-kind-dropdown-content { + max-height: 120px; /* Adjust height to show about 5 items */ + overflow-y: auto; /* Enables vertical scrolling */ +} + +#panel-node-topoviewerrole-dropdown-content { + max-height: 120px; /* Adjust height to show about 5 items */ + overflow-y: auto; /* Enables vertical scrolling */ +} + #panel-link { top: 85px; right: 40px; diff --git a/dist/html-static/js/clabEditor copy monaco.js b/dist/html-static/js/clabEditor copy monaco.js new file mode 100644 index 000000000..b0bb63b04 --- /dev/null +++ b/dist/html-static/js/clabEditor copy monaco.js @@ -0,0 +1,518 @@ +// Declare global variables at the top +var yamlTopoContent; + +// Create a Promise to track when the Monaco Editor is ready +let monacoEditorReady = new Promise((resolve) => { + // Configure Monaco Editor paths + require.config({ paths: { 'vs': ' https://cdn.jsdelivr.net/npm/monaco-editor@0.50.0/min/vs' }}); + + + require(['vs/editor/editor.main'], function() { + // Initialize the Monaco Editor + window.monacoEditor = monaco.editor.create(document.getElementById('panel-clab-editor-text-area'), { + value: '', // Initial content will be set later + language: 'yaml', // Set the language mode + theme: 'vs-dark', // Optional: Set editor theme + automaticLayout: true // Adjust layout automatically + }); + resolve(); // Resolve the Promise when the editor is ready + }); +}); + + +// CLAB EDITOR +function showPanelContainerlabEditor(event) { + // Wait until the Monaco Editor is initialized + monacoEditorReady; + + // Get the YAML content from backend + getYamlTopoContent(yamlTopoContent); + + // Get all elements with the class "panel-overlay" + var panelOverlays = document.getElementsByClassName("panel-overlay"); + // Loop through each element and set its display to 'none' + for (var i = 0; i < panelOverlays.length; i++) { + panelOverlays[i].style.display = "none"; + } + document.getElementById("panel-clab-editor").style.display = "block"; +} + +// Close button event listener +document.getElementById("panel-clab-editor-close-button").addEventListener("click", () => { + document.getElementById("panel-clab-editor").style.display = "none"; +}); + +// Function to load a file into the editor +function clabEditorLoadFile() { + const fileInput = document.getElementById('panel-clab-editor-file-input'); + + // Trigger the file input's file browser dialog + fileInput.click(); + + // Listen for when the user selects a file + fileInput.onchange = function() { + if (fileInput.files.length === 0) { + return; // No file selected + } + + const file = fileInput.files[0]; + const reader = new FileReader(); + + reader.onload = function(event) { + // Set the content of the Monaco Editor + window.monacoEditor.setValue(event.target.result); + }; + + reader.readAsText(file); + }; +} + +async function clabEditorAddNode(nodeId, nodeName = "Spine-01", kind ='nokia_srlinux', image = 'ghcr.io/nokia/srlinux:latest', group = 'group-01', topoViewerRole = 'dcgw') { + await monacoEditorReady; + + if (!kind || !image || !group || !topoViewerRole) { + console.error("All parameters (kind, image, group, topoViewerRole) must be provided."); + return; + } + + // Get the content of the Monaco Editor + let editorContent = window.monacoEditor.getValue(); + console.log ("editorContent - clabEditorAddNode: ", editorContent); // Debug: log editorContent + nodeId = (`### ${nodeId}`); + + // Updated regex pattern to capture nodeName if it exists under the specified nodeId + const existingNodeRegex = new RegExp(`${nodeId}\\s*\\n\\s+(\\S+):`, 'm'); + + const match = editorContent.match(existingNodeRegex); + const oldNodeName = match ? match[1] : null; + + console.log("oldNodeName: ", oldNodeName); // Debug: log oldNodeName + + // Node definition template with the new nodeName + const nodeDefinition = +`${nodeId} + ${nodeName}: + kind: ${kind} + image: ${image} + group: ${group} + labels: + topoViewer-role: ${topoViewerRole} + +`; + + // Insert or update the node definition in the "nodes" section + const nodesSectionIndex = editorContent.search(/^\s*nodes:/m); + const nodeRegex = new RegExp(`\\s*${nodeId}\\s*\\n(\\s*.*\\n)*?\\s*topoViewer-role: .*\\n`, 'g'); + + if (nodesSectionIndex !== -1) { + const insertionIndex = editorContent.indexOf(" links:", nodesSectionIndex); + const endOfNodesSection = insertionIndex !== -1 ? insertionIndex : editorContent.length; + const nodesSection = editorContent.slice(nodesSectionIndex, endOfNodesSection); + + if (nodesSection.match(nodeRegex)) { + // Replace the existing node + editorContent = editorContent.replace(nodeRegex, + `\n\n${nodeId}\n ${nodeName}:\n kind: ${kind}\n image: ${image}\n group: ${group}\n labels:\n topoViewer-role: ${topoViewerRole}\n`); + } else { + // Insert the new node at the end of the nodes section + editorContent = editorContent.slice(0, endOfNodesSection) + nodeDefinition + editorContent.slice(endOfNodesSection); + } + } else { + // Append if "nodes" section doesn't exist + editorContent += (editorContent.endsWith("\n") ? "" : "\n") + nodeDefinition; + } + + // Update the links section if oldNodeName exists + if (oldNodeName && oldNodeName !== nodeName) { + // Updated regex to match oldNodeName in any position in the endpoints array + const linksRegex = new RegExp(`(endpoints:\\s*\\[\\s*".*?)(\\b${oldNodeName}\\b)(:.*?)\\]`, 'g'); + editorContent = editorContent.replace(linksRegex, `$1${nodeName}$3]`); + } + + // Update the content of the Monaco Editor + window.monacoEditor.setValue(editorContent); + yamlTopoContent = editorContent; +} + +async function clabEditorSaveYamlTopo() { + // Wait until the Monaco Editor is initialized + // await monacoEditorReady; + + // Get the content of the Monaco Editor + const editorContent = window.monacoEditor.getValue(); + clabTopoYamlEditorData = editorContent; + console.log("clabTopoYamlEditorData - yamlTopoContent: ", clabTopoYamlEditorData) + + // Dump clabTopoYamlEditorData to be persisted to clab-topo.yaml + const endpointName = '/clab-save-topo-yaml'; + + try { + // Send the enhanced node data directly without wrapping it in an object + const response = await sendRequestToEndpointPost(endpointName, [clabTopoYamlEditorData]); + console.log('Node data saved successfully', response); + } catch (error) { + console.error('Failed to save yaml topo:', error); + } +} + +function clabEditorAddEdge(sourceCyNode, sourceNodeEndpoint, targetCyNode, targetNodeEndpoint) { + // Get the content of the Monaco Editor + let editorContent = window.monacoEditor.getValue(); + + const sourceNodeName = sourceCyNode.data("name"); + const targetNodeName = targetCyNode.data("name"); + + // Edge definition with dynamic endpoints array + const edgeDefinition = ` + - endpoints: ["${sourceNodeName}:${sourceNodeEndpoint}", "${targetNodeName}:${targetNodeEndpoint}"]`; + + // Locate the 'links' section and insert the edge definition at the end of it + const linksIndex = editorContent.indexOf(" links:"); + if (linksIndex !== -1) { + // Find the end of the links section or where the next section begins + const nextSectionIndex = editorContent.indexOf("\n", linksIndex); + const insertionIndex = nextSectionIndex !== -1 ? nextSectionIndex : editorContent.length; + + // Insert the edge definition at the end of the links section + editorContent = editorContent.slice(0, insertionIndex) + edgeDefinition + editorContent.slice(insertionIndex); + } else { + // If no 'links' section exists, append the edge definition at the end of the content + editorContent += "\n links:" + edgeDefinition; + } + + // Update the content of the Monaco Editor + window.monacoEditor.setValue(editorContent); +} + +async function showPanelNodeEditor(node) { + try { + // Remove all Overlayed Panels + const panelOverlays = document.getElementsByClassName("panel-overlay"); + Array.from(panelOverlays).forEach(panel => { + panel.style.display = "none"; + }); + + console.log("showPanelNodeEditor - node ID:", node.data("id")); + + // Set the node Name in the editor + const nodeNameInput = document.getElementById("panel-node-editor-name"); + if (nodeNameInput) { + nodeNameInput.value = node.data("id"); // defaulted by node id + } + + // Set the node Id in the editor + const nodeIdLabel = document.getElementById("panel-node-editor-id"); + if (nodeIdLabel) { + nodeIdLabel.textContent = node.data("id"); + } + + // Set the node image in the editor + const nodeImageLabel = document.getElementById("panel-node-editor-image"); + if (nodeImageLabel) { + nodeImageLabel.value = 'ghcr.io/nokia/srlinux:latest'; + } + + // Set the node group in the editor + const nodeGroupLabel = document.getElementById("panel-node-editor-group"); + if (nodeGroupLabel) { + nodeGroupLabel.value = 'data-center'; + } + + // Display the node editor panel + const nodeEditorPanel = document.getElementById("panel-node-editor"); + if (nodeEditorPanel) { + nodeEditorPanel.style.display = "block"; + } + + // Fetch JSON schema from the backend + const url = "js/clabJsonSchema-v0.59.0.json"; + try { + const response = await fetch(url); + if (!response.ok) { + throw new Error(`HTTP error! Status: ${response.status}`); + } + const jsonData = await response.json(); + + // Get kind enums from the JSON data + const { kindOptions } = getKindEnums(jsonData); + console.log('Kind Enum:', kindOptions); + + // Populate the dropdown with fetched kindOptions + populateKindDropdown(kindOptions); + + // Populate the dropdown with fetched topoViwerRoleOptions + var topoViwerRoleOptions = ['bridge', 'controller', 'dcgw', 'router', 'leaf', 'pe', 'pon', 'rgw', 'server', 'super-spine', 'spine']; + populateTopoViewerRoleDropdown(topoViwerRoleOptions); + + // List type enums based on kind pattern + const typeOptions = getTypeEnumsByKindPattern(jsonData, '(srl|nokia_srlinux)'); // To be added to the UI + console.log('Type Enum for (srl|nokia_srlinux):', typeOptions); + + } catch (error) { + console.error("Error fetching or processing JSON data:", error.message); + throw error; + } + + } catch (error) { + console.error("Error in showPanelNodeEditor:", error); + // Optionally, display an error message to the user + const errorDiv = document.getElementById('panel-node-editor-error'); + if (errorDiv) { + errorDiv.textContent = "An error occurred while loading the node editor. Please try again."; + errorDiv.style.display = "block"; + } + } +} + +// Function to get kind enums from the JSON schema +function getKindEnums(jsonData) { + let kindOptions = []; + if (jsonData && jsonData.definitions && jsonData.definitions['node-config']) { + kindOptions = jsonData.definitions['node-config'].properties.kind.enum || []; + } else { + throw new Error("Invalid JSON structure or 'kind' enum not found"); + } + return { kindOptions, schemaData: jsonData }; +} + +// Function to get type enums based on a kind pattern +function getTypeEnumsByKindPattern(jsonData, pattern) { + if (jsonData && jsonData.definitions && jsonData.definitions['node-config'] && jsonData.definitions['node-config'].allOf) { + for (const condition of jsonData.definitions['node-config'].allOf) { + if (condition.if && condition.if.properties && condition.if.properties.kind && condition.if.properties.kind.pattern === pattern) { + if (condition.then && condition.then.properties && condition.then.properties.type && condition.then.properties.type.enum) { + return condition.then.properties.type.enum; + } + } + } + } + return []; +} + +let panelNodeEditorKind = "nokia_srlinux"; // Variable to store the selected option for dropdown menu +// Function to populate the kind dropdown +function populateKindDropdown(options) { + // Get the dropdown elements by their IDs + const dropdownTrigger = document.querySelector("#panel-node-kind-dropdown .dropdown-trigger button span"); + const dropdownContent = document.getElementById("panel-node-kind-dropdown-content"); + const dropdownButton = document.querySelector("#panel-node-kind-dropdown .dropdown-trigger button"); + const dropdownContainer = dropdownButton.closest(".dropdown"); + + if (!dropdownTrigger || !dropdownContent || !dropdownButton || !dropdownContainer) { + console.error("Dropdown elements not found in the DOM."); + return; + } + + // Set the initial value on the dropdown button + dropdownTrigger.textContent = panelNodeEditorKind; + + // Clear any existing content + dropdownContent.innerHTML = ""; + + options.forEach(option => { + // Create a new anchor element for each option + const optionElement = document.createElement("a"); + optionElement.classList.add("dropdown-item", "label", "has-text-weight-normal", "is-small", "py-0"); + optionElement.textContent = option; + optionElement.href = "#"; // Optional, can be adjusted as needed + + // Set an event handler for the option + optionElement.addEventListener("click", (event) => { + event.preventDefault(); // Prevent default link behavior + + panelNodeEditorKind = option; // Store the selected option in the variable + console.log(`${panelNodeEditorKind} selected`); // Log the selected option + + dropdownTrigger.textContent = panelNodeEditorKind; + + // Collapse the dropdown menu + dropdownContainer.classList.remove("is-active"); + }); + + // Append the option element to the dropdown content + dropdownContent.appendChild(optionElement); + }); +} + +// Initialize event listeners for the dropdown +function initializeDropdownListeners() { + const dropdownButton = document.querySelector("#panel-node-kind-dropdown .dropdown-trigger button"); + const dropdownContainer = dropdownButton.closest(".dropdown"); + + if (!dropdownButton || !dropdownContainer) { + console.error("Dropdown button or container not found in the DOM."); + return; + } + + // Toggle dropdown menu on button click + dropdownButton.addEventListener("click", (event) => { + event.stopPropagation(); // Prevents the event from bubbling up + dropdownContainer.classList.toggle("is-active"); + }); + + // Collapse the dropdown if clicked outside + document.addEventListener("click", (event) => { + if (dropdownContainer.classList.contains("is-active")) { + dropdownContainer.classList.remove("is-active"); + } + }); +} +// Initialize dropdown listeners once when the DOM is fully loaded +document.addEventListener("DOMContentLoaded", () => { + initializeDropdownListeners(); +}); + +let panelNodeEditorTopoViewerRole = "pe"; // Variable to store the selected option for dropdown menu +// Function to populate the topoViewerRole dropdown +function populateTopoViewerRoleDropdown(options) { + // Get the dropdown elements by their IDs + const dropdownTrigger = document.querySelector("#panel-node-topoviewerrole-dropdown .dropdown-trigger button span"); + const dropdownContent = document.getElementById("panel-node-topoviewerrole-dropdown-content"); + const dropdownButton = document.querySelector("#panel-node-topoviewerrole-dropdown .dropdown-trigger button"); + const dropdownContainer = dropdownButton.closest(".dropdown"); + + if (!dropdownTrigger || !dropdownContent || !dropdownButton || !dropdownContainer) { + console.error("Dropdown elements not found in the DOM."); + return; + } + + // Set the initial value on the dropdown button + dropdownTrigger.textContent = panelNodeEditorTopoViewerRole; + + // Clear any existing content + dropdownContent.innerHTML = ""; + + options.forEach(option => { + // Create a new anchor element for each option + const optionElement = document.createElement("a"); + optionElement.classList.add("dropdown-item", "label", "has-text-weight-normal", "is-small", "py-0"); + optionElement.textContent = option; + optionElement.href = "#"; // Optional, can be adjusted as needed + + // Set an event handler for the option + optionElement.addEventListener("click", (event) => { + event.preventDefault(); // Prevent default link behavior + + panelNodeEditorTopoViewerRole = option; // Store the selected option in the variable + console.log(`${panelNodeEditorTopoViewerRole} selected`); // Log the selected option + + dropdownTrigger.textContent = panelNodeEditorTopoViewerRole; + + // Collapse the dropdown menu + dropdownContainer.classList.remove("is-active"); + }); + + // Append the option element to the dropdown content + dropdownContent.appendChild(optionElement); + }); +} + +// Initialize event listeners for the dropdown +function initializeDropdownTopoViewerRoleListeners() { + const dropdownButton = document.querySelector("#panel-node-topoviewerrole-dropdown .dropdown-trigger button"); + const dropdownContainer = dropdownButton.closest(".dropdown"); + + if (!dropdownButton || !dropdownContainer) { + console.error("Dropdown button or container not found in the DOM."); + return; + } + + // Toggle dropdown menu on button click + dropdownButton.addEventListener("click", (event) => { + event.stopPropagation(); // Prevents the event from bubbling up + dropdownContainer.classList.toggle("is-active"); + }); + + // Collapse the dropdown if clicked outside + document.addEventListener("click", (event) => { + if (dropdownContainer.classList.contains("is-active")) { + dropdownContainer.classList.remove("is-active"); + } + }); +} + +// Initialize dropdown listeners once when the DOM is fully loaded +document.addEventListener("DOMContentLoaded", () => { + initializeDropdownTopoViewerRoleListeners(); +}); + +// Initialize event listener for the close button +document.getElementById("panel-node-editor-close-button").addEventListener("click", () => { + document.getElementById("panel-node-editor").style.display = "none"; +}); + +// Function to save node data from the editor +// Adjusted saveNodeToEditorToFile function +// update node data in the editor, save cyto json to file dataCytoMarshall.json and save to clab topo.yaml +async function saveNodeToEditorToFile() { + const nodeId =document.getElementById("panel-node-editor-id").textContent + var cyNode = cy.$id(nodeId); // Get cytoscpe node object id + + // get value from panel-node-editor + nodeName = document.getElementById("panel-node-editor-name").value + kind = panelNodeEditorKind + image = document.getElementById("panel-node-editor-image").value + group = document.getElementById("panel-node-editor-group").value + topoViewerRole = panelNodeEditorTopoViewerRole + + console.log("panelEditorNodeName", nodeName) + console.log("panelEditorkind", kind) + console.log("panelEditorImage", image) + console.log("panelEditorGroup", group) + console.log("panelEditorTopoViewerRole",topoViewerRole) + + // save node data to cytoscape node object + var extraData = { + "kind": kind, + "image": image, + "longname": "", + "mgmtIpv4Addresss": "" + }; + + cyNode.data(('name'), nodeName) + cyNode.data(('parent'), group) + cyNode.data(('topoViewerRole'), topoViewerRole) + cyNode.data(('extraData'), extraData) + + console.log('cyto node object data: ', cyNode); + + // dump cytoscape node object to nodeData to be persisted to dataCytoMarshall.json + var nodeData = cy.$id(nodeId).json(); // Get JSON data of the node with the specified ID + const endpointName = '/clab-save-topo-cyto-json'; + + try { + // Send the enhanced node data directly without wrapping it in an object + const response = await sendRequestToEndpointPost(endpointName, [nodeData]); + console.log('Node data saved successfully', response); + } catch (error) { + console.error('Failed to save node data:', error); + } + + // add node to clab editor textarea + clabEditorAddNode(nodeId, nodeName, kind, image, group, topoViewerRole) + + // clabEditorSaveYamlTopo() +} + +async function getYamlTopoContent(yamlTopoContent) { + // Wait until the Monaco Editor is initialized + // await monacoEditorReady; + + try { + // Check if yamlTopoContent is already set + console.log('YAML Topo Initial Content:', yamlTopoContent); + + if (!yamlTopoContent) { + // Load the content if yamlTopoContent is empty + yamlTopoContent = await sendRequestToEndpointGetV3("/get-yaml-topo-content"); + } + + console.log('YAML Topo Content:', yamlTopoContent); + + // Set the content of the Monaco Editor + window.monacoEditor.setValue(yamlTopoContent); + } catch (error) { + console.error("Error occurred:", error); + // Handle errors as needed + } +} \ No newline at end of file diff --git a/dist/html-static/js/clabEditor copy.js b/dist/html-static/js/clabEditor copy.js new file mode 100644 index 000000000..cb2d0a65a --- /dev/null +++ b/dist/html-static/js/clabEditor copy.js @@ -0,0 +1,494 @@ +// Declare global variables at the top +var yamlTopoContent; + +// CLAB EDITOR +async function showPanelContainerlabEditor(event) { + // Get the YAML content from backend + getYamlTopoContent(yamlTopoContent) + + // Get all elements with the class "panel-overlay" + var panelOverlays = document.getElementsByClassName("panel-overlay"); + // Loop through each element and set its display to 'none' + for (var i = 0; i < panelOverlays.length; i++) { + panelOverlays[i].style.display = "none"; + } + document.getElementById("panel-clab-editor").style.display = "block"; +} + +// / logMessagesPanel Function to add a click event listener to the close button +document.getElementById("panel-clab-editor-close-button").addEventListener("click", () => { + document.getElementById("panel-clab-editor").style.display = "none"; +}); + +function clabEditorLoadFile() { + const fileInput = document.getElementById('panel-clab-editor-file-input'); + const textarea = document.getElementById('panel-clab-editor-text-area'); + + // Trigger the file input's file browser dialog + fileInput.click(); + + // Listen for when the user selects a file + fileInput.onchange = function() { + if (fileInput.files.length === 0) { + return; // No file selected + } + + const file = fileInput.files[0]; + const reader = new FileReader(); + + reader.onload = function(event) { + textarea.value = event.target.result; + }; + + reader.readAsText(file); + }; +} + + + +function clabEditorAddNode(nodeId, nodeName = "Spine-01", kind ='nokia_srlinux', image = 'ghcr.io/nokia/srlinux:latest', group = 'group-01', topoViewerRole = 'dcgw') { + if (!kind || !image || !group || !topoViewerRole) { + console.error("All parameters (kind, image, group, topoViewerRole) must be provided."); + return; + } + + const textarea = document.getElementById('panel-clab-editor-text-area'); + nodeId = (`### ${nodeId}`); + + // Updated regex pattern to capture nodeName if it exists under the specified nodeId + // const existingNodeRegex = new RegExp(`###\\s*${nodeId}\\s*\\n\\s*(\\S+):`, 'm'); + const existingNodeRegex = new RegExp(`${nodeId}\\s*\\n\\s+(\\S+):`, 'm'); + + const match = textarea.value.match(existingNodeRegex); + const oldNodeName = match ? match[1] : null; + + console.log("oldNodeName: ", oldNodeName); // Debug: log oldNodeName + + // Node definition template with the new nodeName + const nodeDefinition = +`${nodeId} + ${nodeName}: + kind: ${kind} + image: ${image} + group: ${group} + labels: + topoViewer-role: ${topoViewerRole} + +`; + + // Insert or update the node definition in the "nodes" section + const nodesSectionIndex = textarea.value.search(/^\s*nodes:/m); + const nodeRegex = new RegExp(`\\s*${nodeId}\\s*\\n(\\s*.*\\n)*?\\s*topoViewer-role: .*\\n`, 'g'); + + if (nodesSectionIndex !== -1) { + const insertionIndex = textarea.value.indexOf(" links:", nodesSectionIndex); + const endOfNodesSection = insertionIndex !== -1 ? insertionIndex : textarea.value.length; + const nodesSection = textarea.value.slice(nodesSectionIndex, endOfNodesSection); + + if (nodesSection.match(nodeRegex)) { + // Replace the existing node + textarea.value = textarea.value.replace(nodeRegex, + `\n\n${nodeId}\n ${nodeName}:\n kind: ${kind}\n image: ${image}\n group: ${group}\n labels:\n topoViewer-role: ${topoViewerRole}\n`); + } else { + // Insert the new node at the end of the nodes section + textarea.value = textarea.value.slice(0, endOfNodesSection) + nodeDefinition + textarea.value.slice(endOfNodesSection); + } + } else { + // Append if "nodes" section doesn't exist + textarea.value += (textarea.value.endsWith("\n") ? "" : "\n") + nodeDefinition; + } + + // Update the links section if oldNodeName exists + if (oldNodeName && oldNodeName !== nodeName) { + // Updated regex to match oldNodeName in any position in the endpoints array + const linksRegex = new RegExp(`(endpoints:\\s*\\[\\s*".*?)(\\b${oldNodeName}\\b)(:.*?)\\]`, 'g'); + textarea.value = textarea.value.replace(linksRegex, `$1${nodeName}$3]`); + } + + yamlTopoContent = textarea.value; +} + +async function clabEditorSaveYamlTopo() { + const textarea = document.getElementById('panel-clab-editor-text-area'); + clabTopoYamlEditorData = textarea.value; + console.log("clabTopoYamlEditorData - yamlTopoContent: ", clabTopoYamlEditorData) + + // dump clabTopoYamlEditorDatal to be persisted to clab-topo.yaml + const endpointName = '/clab-save-topo-yaml'; + + try { + // Send the enhanced node data directly without wrapping it in an object + const response = await sendRequestToEndpointPost(endpointName, [clabTopoYamlEditorData]); + console.log('Node data saved successfully', response); + } catch (error) { + console.error('Failed to save yaml topo:', error); + } + +} + + + +function clabEditorAddEdge(sourceCyNode, sourceNodeEndpoint, targetCyNode, targetNodeEndpoint) { + const textarea = document.getElementById('panel-clab-editor-text-area'); + + sourceNodeName = sourceCyNode.data("name") + targetNodeName = targetCyNode.data("name") + + + // Edge definition with dynamic endpoints array + const edgeDefinition = ` + - endpoints: ["${sourceNodeName}:${sourceNodeEndpoint}", "${targetNodeName}:${targetNodeEndpoint}"]`; + + // Locate the 'links' section and insert the edge definition at the end of it + const linksIndex = textarea.value.indexOf(" links:"); + if (linksIndex !== -1) { + // Find the end of the links section or where the next section begins + const nextSectionIndex = textarea.value.indexOf("\n", linksIndex); + const insertionIndex = nextSectionIndex !== -1 ? nextSectionIndex : textarea.value.length; + + // Insert the edge definition at the end of the links section + textarea.value = textarea.value.slice(0, insertionIndex) + edgeDefinition + textarea.value.slice(insertionIndex); + } else { + // If no 'links' section exists, append the edge definition at the end of the content + textarea.value += "\n links:" + edgeDefinition; + } +} + +// NODE EDITOR START +// NODE EDITOR START +// NODE EDITOR START + +// var yamlTopoContent + +async function showPanelNodeEditor(node) { + try { + // Remove all Overlayed Panels + const panelOverlays = document.getElementsByClassName("panel-overlay"); + Array.from(panelOverlays).forEach(panel => { + panel.style.display = "none"; + }); + + console.log("showPanelNodeEditor - node ID:", node.data("id")); + + // Set the node Name in the editor + const nodeNameInput = document.getElementById("panel-node-editor-name"); + if (nodeNameInput) { + nodeNameInput.value = node.data("id"); //defaulted by node id + } + + // Set the node Id in the editor + const nodeIdLabel = document.getElementById("panel-node-editor-id"); + if (nodeIdLabel) { + nodeIdLabel.textContent = node.data("id"); + } + + // Set the node image in the editor + const nodeImageLabel = document.getElementById("panel-node-editor-image"); + if (nodeImageLabel) { + nodeImageLabel.value = 'ghcr.io/nokia/srlinux:latest'; + } + + // Set the node image in the editor + const nodeGroupLabel = document.getElementById("panel-node-editor-group"); + if (nodeGroupLabel) { + nodeGroupLabel.value = 'data-center'; + } + + // Display the node editor panel + const nodeEditorPanel = document.getElementById("panel-node-editor"); + if (nodeEditorPanel) { + nodeEditorPanel.style.display = "block"; + } + + + // Fetch JSON schema from the backend + const url = "js/clabJsonSchema-v0.59.0.json"; + try { + const response = await fetch(url); + if (!response.ok) { + throw new Error(`HTTP error! Status: ${response.status}`); + } + const jsonData = await response.json(); + + // Get kind enums from the JSON data + const { kindOptions } = getKindEnums(jsonData); + console.log('Kind Enum:', kindOptions); + + // Populate the dropdown with fetched kindOptions + populateKindDropdown(kindOptions); + + // Populate the dropdown with fetched topoViwerRoleOptions + var topoViwerRoleOptions = ['bridge', 'controller', 'dcgw', 'router', 'leaf', 'pe', 'pon', 'rgw', 'server','super-spine', 'spine']; + populateTopoViewerRoleDropdown(topoViwerRoleOptions) + + // List type enums based on kind pattern + const typeOptions = getTypeEnumsByKindPattern(jsonData, '(srl|nokia_srlinux)'); // aarafat-tag: to be added to the UI + console.log('Type Enum for (srl|nokia_srlinux):', typeOptions); + + } catch (error) { + console.error("Error fetching or processing JSON data:", error.message); + throw error; + } + + } catch (error) { + console.error("Error in showPanelNodeEditor:", error); + // Optionally, display an error message to the user + const errorDiv = document.getElementById('panel-node-editor-error'); + if (errorDiv) { + errorDiv.textContent = "An error occurred while loading the node editor. Please try again."; + errorDiv.style.display = "block"; + } + } +} + +// Function to get kind enums from the JSON schema +function getKindEnums(jsonData) { + let kindOptions = []; + if (jsonData && jsonData.definitions && jsonData.definitions['node-config']) { + kindOptions = jsonData.definitions['node-config'].properties.kind.enum || []; + } else { + throw new Error("Invalid JSON structure or 'kind' enum not found"); + } + return { kindOptions, schemaData: jsonData }; +} + +// Function to get type enums based on a kind pattern +function getTypeEnumsByKindPattern(jsonData, pattern) { + if (jsonData && jsonData.definitions && jsonData.definitions['node-config'] && jsonData.definitions['node-config'].allOf) { + for (const condition of jsonData.definitions['node-config'].allOf) { + if (condition.if && condition.if.properties && condition.if.properties.kind && condition.if.properties.kind.pattern === pattern) { + if (condition.then && condition.then.properties && condition.then.properties.type && condition.then.properties.type.enum) { + return condition.then.properties.type.enum; + } + } + } + } + return []; +} + +let panelNodeEditorKind = "nokia_srlinux"; // Variable to store the selected option for dropdown menu, nokia_srlinux as default +// Function to populate the kind dropdown +function populateKindDropdown(options) { + // Get the dropdown elements by their IDs + const dropdownTrigger = document.querySelector("#panel-node-kind-dropdown .dropdown-trigger button span"); + const dropdownContent = document.getElementById("panel-node-kind-dropdown-content"); + const dropdownButton = document.querySelector("#panel-node-kind-dropdown .dropdown-trigger button"); + const dropdownContainer = dropdownButton.closest(".dropdown"); + + if (!dropdownTrigger || !dropdownContent || !dropdownButton || !dropdownContainer) { + console.error("Dropdown elements not found in the DOM."); + return; + } + + // Set the initial value on the dropdown button + dropdownTrigger.textContent = panelNodeEditorKind; + + // Clear any existing content + dropdownContent.innerHTML = ""; + + options.forEach(option => { + // Create a new anchor element for each option + const optionElement = document.createElement("a"); + optionElement.classList.add("dropdown-item", "label", "has-text-weight-normal", "is-small", "py-0"); + optionElement.textContent = option; + optionElement.href = "#"; // Optional, can be adjusted as needed + + // Set an event handler for the option + optionElement.addEventListener("click", (event) => { + event.preventDefault(); // Prevent default link behavior + + panelNodeEditorKind = option; // Store the selected option in the variable + console.log(`${panelNodeEditorKind} selected`); // Log the selected option + + dropdownTrigger.textContent = panelNodeEditorKind; + + // Collapse the dropdown menu + dropdownContainer.classList.remove("is-active"); + }); + + // Append the option element to the dropdown content + dropdownContent.appendChild(optionElement); + }); +} + +// Initialize event listeners for the dropdown +function initializeDropdownListeners() { + const dropdownButton = document.querySelector("#panel-node-kind-dropdown .dropdown-trigger button"); + const dropdownContainer = dropdownButton.closest(".dropdown"); + + if (!dropdownButton || !dropdownContainer) { + console.error("Dropdown button or container not found in the DOM."); + return; + } + + // Toggle dropdown menu on button click + dropdownButton.addEventListener("click", (event) => { + event.stopPropagation(); // Prevents the event from bubbling up + dropdownContainer.classList.toggle("is-active"); + }); + + // Collapse the dropdown if clicked outside + document.addEventListener("click", (event) => { + if (dropdownContainer.classList.contains("is-active")) { + dropdownContainer.classList.remove("is-active"); + } + }); +} +// Initialize dropdown listeners once when the DOM is fully loaded +document.addEventListener("DOMContentLoaded", () => { + initializeDropdownListeners(); +}); + + +let panelNodeEditorTopoViewerRole = "pe"; // Variable to store the selected option for dropdown menu, nokia_srlinux as default +// Function to populate the topoviewerrole dropdown +function populateTopoViewerRoleDropdown(options) { + // Get the dropdown elements by their IDs + const dropdownTrigger = document.querySelector("#panel-node-topoviewerrole-dropdown .dropdown-trigger button span"); + const dropdownContent = document.getElementById("panel-node-topoviewerrole-dropdown-content"); + const dropdownButton = document.querySelector("#panel-node-topoviewerrole-dropdown .dropdown-trigger button"); + const dropdownContainer = dropdownButton.closest(".dropdown"); + + if (!dropdownTrigger || !dropdownContent || !dropdownButton || !dropdownContainer) { + console.error("Dropdown elements not found in the DOM."); + return; + } + + // Set the initial value on the dropdown button + dropdownTrigger.textContent = panelNodeEditorTopoViewerRole; + + + // Clear any existing content + dropdownContent.innerHTML = ""; + + options.forEach(option => { + // Create a new anchor element for each option + const optionElement = document.createElement("a"); + optionElement.classList.add("dropdown-item", "label", "has-text-weight-normal", "is-small", "py-0"); + optionElement.textContent = option; + optionElement.href = "#"; // Optional, can be adjusted as needed + + // Set an event handler for the option + optionElement.addEventListener("click", (event) => { + event.preventDefault(); // Prevent default link behavior + + panelNodeEditorTopoViewerRole = option; // Store the selected option in the variable + console.log(`${panelNodeEditorTopoViewerRole} selected`); // Log the selected option + + dropdownTrigger.textContent = panelNodeEditorTopoViewerRole; + + // Collapse the dropdown menu + dropdownContainer.classList.remove("is-active"); + }); + + // Append the option element to the dropdown content + dropdownContent.appendChild(optionElement); + }); +} + +// Initialize event listeners for the dropdown +function initializeDropdownTopoViewerRoleListeners() { + const dropdownButton = document.querySelector("#panel-node-topoviewerrole-dropdown .dropdown-trigger button"); + const dropdownContainer = dropdownButton.closest(".dropdown"); + + if (!dropdownButton || !dropdownContainer) { + console.error("Dropdown button or container not found in the DOM."); + return; + } + + // Toggle dropdown menu on button click + dropdownButton.addEventListener("click", (event) => { + event.stopPropagation(); // Prevents the event from bubbling up + dropdownContainer.classList.toggle("is-active"); + }); + + // Collapse the dropdown if clicked outside + document.addEventListener("click", (event) => { + if (dropdownContainer.classList.contains("is-active")) { + dropdownContainer.classList.remove("is-active"); + } + }); +} + +// Initialize dropdown listeners once when the DOM is fully loaded +document.addEventListener("DOMContentLoaded", () => { + initializeDropdownTopoViewerRoleListeners(); +}); + +// Initialize event listener for the close button +document.getElementById("panel-node-editor-close-button").addEventListener("click", () => { + document.getElementById("panel-node-editor").style.display = "none"; +}); + + +// update node data in the editor, save cyto json to file dataCytoMarshall.json and save to clab topo.yaml +async function saveNodeToEditorToFile() { + const nodeId =document.getElementById("panel-node-editor-id").textContent + var cyNode = cy.$id(nodeId); // Get cytoscpe node object id + + // get value from panel-node-editor + nodeName = document.getElementById("panel-node-editor-name").value + kind = panelNodeEditorKind + image = document.getElementById("panel-node-editor-image").value + group = document.getElementById("panel-node-editor-group").value + topoViewerRole = panelNodeEditorTopoViewerRole + + console.log("panelEditorNodeName", nodeName) + console.log("panelEditorkind", kind) + console.log("panelEditorImage", image) + console.log("panelEditorGroup", group) + console.log("panelEditorTopoViewerRole",topoViewerRole) + + // save node data to cytoscape node object + var extraData = { + "kind": kind, + "image": image, + "longname": "", + "mgmtIpv4Addresss": "" + }; + + cyNode.data(('name'), nodeName) + cyNode.data(('parent'), group) + cyNode.data(('topoViewerRole'), topoViewerRole) + cyNode.data(('extraData'), extraData) + + console.log('cyto node object data: ', cyNode); + + // dump cytoscape node object to nodeData to be persisted to dataCytoMarshall.json + var nodeData = cy.$id(nodeId).json(); // Get JSON data of the node with the specified ID + const endpointName = '/clab-save-topo-cyto-json'; + + try { + // Send the enhanced node data directly without wrapping it in an object + const response = await sendRequestToEndpointPost(endpointName, [nodeData]); + console.log('Node data saved successfully', response); + } catch (error) { + console.error('Failed to save node data:', error); + } + + // add node to clab editor textarea + clabEditorAddNode(nodeId, nodeName, kind, image, group, topoViewerRole) + + // clabEditorSaveYamlTopo() +} + +async function getYamlTopoContent(yamlTopoContent) { + + try { + // Check if yamlTopoContent is already set + console.log('YAML Topo Initial Content:', yamlTopoContent); + + if (!yamlTopoContent) { + // Load the content if yamlTopoContent is empty + yamlTopoContent = await sendRequestToEndpointGetV3("/get-yaml-topo-content"); + } + + console.log('YAML Topo Content:', yamlTopoContent); + document.getElementById('panel-clab-editor-text-area').value = yamlTopoContent; + + + } catch (error) { + console.error("Error occurred:", error); + // Handle errors as needed + } +} diff --git a/dist/html-static/js/clabEditor.js b/dist/html-static/js/clabEditor.js new file mode 100644 index 000000000..b0bb63b04 --- /dev/null +++ b/dist/html-static/js/clabEditor.js @@ -0,0 +1,518 @@ +// Declare global variables at the top +var yamlTopoContent; + +// Create a Promise to track when the Monaco Editor is ready +let monacoEditorReady = new Promise((resolve) => { + // Configure Monaco Editor paths + require.config({ paths: { 'vs': ' https://cdn.jsdelivr.net/npm/monaco-editor@0.50.0/min/vs' }}); + + + require(['vs/editor/editor.main'], function() { + // Initialize the Monaco Editor + window.monacoEditor = monaco.editor.create(document.getElementById('panel-clab-editor-text-area'), { + value: '', // Initial content will be set later + language: 'yaml', // Set the language mode + theme: 'vs-dark', // Optional: Set editor theme + automaticLayout: true // Adjust layout automatically + }); + resolve(); // Resolve the Promise when the editor is ready + }); +}); + + +// CLAB EDITOR +function showPanelContainerlabEditor(event) { + // Wait until the Monaco Editor is initialized + monacoEditorReady; + + // Get the YAML content from backend + getYamlTopoContent(yamlTopoContent); + + // Get all elements with the class "panel-overlay" + var panelOverlays = document.getElementsByClassName("panel-overlay"); + // Loop through each element and set its display to 'none' + for (var i = 0; i < panelOverlays.length; i++) { + panelOverlays[i].style.display = "none"; + } + document.getElementById("panel-clab-editor").style.display = "block"; +} + +// Close button event listener +document.getElementById("panel-clab-editor-close-button").addEventListener("click", () => { + document.getElementById("panel-clab-editor").style.display = "none"; +}); + +// Function to load a file into the editor +function clabEditorLoadFile() { + const fileInput = document.getElementById('panel-clab-editor-file-input'); + + // Trigger the file input's file browser dialog + fileInput.click(); + + // Listen for when the user selects a file + fileInput.onchange = function() { + if (fileInput.files.length === 0) { + return; // No file selected + } + + const file = fileInput.files[0]; + const reader = new FileReader(); + + reader.onload = function(event) { + // Set the content of the Monaco Editor + window.monacoEditor.setValue(event.target.result); + }; + + reader.readAsText(file); + }; +} + +async function clabEditorAddNode(nodeId, nodeName = "Spine-01", kind ='nokia_srlinux', image = 'ghcr.io/nokia/srlinux:latest', group = 'group-01', topoViewerRole = 'dcgw') { + await monacoEditorReady; + + if (!kind || !image || !group || !topoViewerRole) { + console.error("All parameters (kind, image, group, topoViewerRole) must be provided."); + return; + } + + // Get the content of the Monaco Editor + let editorContent = window.monacoEditor.getValue(); + console.log ("editorContent - clabEditorAddNode: ", editorContent); // Debug: log editorContent + nodeId = (`### ${nodeId}`); + + // Updated regex pattern to capture nodeName if it exists under the specified nodeId + const existingNodeRegex = new RegExp(`${nodeId}\\s*\\n\\s+(\\S+):`, 'm'); + + const match = editorContent.match(existingNodeRegex); + const oldNodeName = match ? match[1] : null; + + console.log("oldNodeName: ", oldNodeName); // Debug: log oldNodeName + + // Node definition template with the new nodeName + const nodeDefinition = +`${nodeId} + ${nodeName}: + kind: ${kind} + image: ${image} + group: ${group} + labels: + topoViewer-role: ${topoViewerRole} + +`; + + // Insert or update the node definition in the "nodes" section + const nodesSectionIndex = editorContent.search(/^\s*nodes:/m); + const nodeRegex = new RegExp(`\\s*${nodeId}\\s*\\n(\\s*.*\\n)*?\\s*topoViewer-role: .*\\n`, 'g'); + + if (nodesSectionIndex !== -1) { + const insertionIndex = editorContent.indexOf(" links:", nodesSectionIndex); + const endOfNodesSection = insertionIndex !== -1 ? insertionIndex : editorContent.length; + const nodesSection = editorContent.slice(nodesSectionIndex, endOfNodesSection); + + if (nodesSection.match(nodeRegex)) { + // Replace the existing node + editorContent = editorContent.replace(nodeRegex, + `\n\n${nodeId}\n ${nodeName}:\n kind: ${kind}\n image: ${image}\n group: ${group}\n labels:\n topoViewer-role: ${topoViewerRole}\n`); + } else { + // Insert the new node at the end of the nodes section + editorContent = editorContent.slice(0, endOfNodesSection) + nodeDefinition + editorContent.slice(endOfNodesSection); + } + } else { + // Append if "nodes" section doesn't exist + editorContent += (editorContent.endsWith("\n") ? "" : "\n") + nodeDefinition; + } + + // Update the links section if oldNodeName exists + if (oldNodeName && oldNodeName !== nodeName) { + // Updated regex to match oldNodeName in any position in the endpoints array + const linksRegex = new RegExp(`(endpoints:\\s*\\[\\s*".*?)(\\b${oldNodeName}\\b)(:.*?)\\]`, 'g'); + editorContent = editorContent.replace(linksRegex, `$1${nodeName}$3]`); + } + + // Update the content of the Monaco Editor + window.monacoEditor.setValue(editorContent); + yamlTopoContent = editorContent; +} + +async function clabEditorSaveYamlTopo() { + // Wait until the Monaco Editor is initialized + // await monacoEditorReady; + + // Get the content of the Monaco Editor + const editorContent = window.monacoEditor.getValue(); + clabTopoYamlEditorData = editorContent; + console.log("clabTopoYamlEditorData - yamlTopoContent: ", clabTopoYamlEditorData) + + // Dump clabTopoYamlEditorData to be persisted to clab-topo.yaml + const endpointName = '/clab-save-topo-yaml'; + + try { + // Send the enhanced node data directly without wrapping it in an object + const response = await sendRequestToEndpointPost(endpointName, [clabTopoYamlEditorData]); + console.log('Node data saved successfully', response); + } catch (error) { + console.error('Failed to save yaml topo:', error); + } +} + +function clabEditorAddEdge(sourceCyNode, sourceNodeEndpoint, targetCyNode, targetNodeEndpoint) { + // Get the content of the Monaco Editor + let editorContent = window.monacoEditor.getValue(); + + const sourceNodeName = sourceCyNode.data("name"); + const targetNodeName = targetCyNode.data("name"); + + // Edge definition with dynamic endpoints array + const edgeDefinition = ` + - endpoints: ["${sourceNodeName}:${sourceNodeEndpoint}", "${targetNodeName}:${targetNodeEndpoint}"]`; + + // Locate the 'links' section and insert the edge definition at the end of it + const linksIndex = editorContent.indexOf(" links:"); + if (linksIndex !== -1) { + // Find the end of the links section or where the next section begins + const nextSectionIndex = editorContent.indexOf("\n", linksIndex); + const insertionIndex = nextSectionIndex !== -1 ? nextSectionIndex : editorContent.length; + + // Insert the edge definition at the end of the links section + editorContent = editorContent.slice(0, insertionIndex) + edgeDefinition + editorContent.slice(insertionIndex); + } else { + // If no 'links' section exists, append the edge definition at the end of the content + editorContent += "\n links:" + edgeDefinition; + } + + // Update the content of the Monaco Editor + window.monacoEditor.setValue(editorContent); +} + +async function showPanelNodeEditor(node) { + try { + // Remove all Overlayed Panels + const panelOverlays = document.getElementsByClassName("panel-overlay"); + Array.from(panelOverlays).forEach(panel => { + panel.style.display = "none"; + }); + + console.log("showPanelNodeEditor - node ID:", node.data("id")); + + // Set the node Name in the editor + const nodeNameInput = document.getElementById("panel-node-editor-name"); + if (nodeNameInput) { + nodeNameInput.value = node.data("id"); // defaulted by node id + } + + // Set the node Id in the editor + const nodeIdLabel = document.getElementById("panel-node-editor-id"); + if (nodeIdLabel) { + nodeIdLabel.textContent = node.data("id"); + } + + // Set the node image in the editor + const nodeImageLabel = document.getElementById("panel-node-editor-image"); + if (nodeImageLabel) { + nodeImageLabel.value = 'ghcr.io/nokia/srlinux:latest'; + } + + // Set the node group in the editor + const nodeGroupLabel = document.getElementById("panel-node-editor-group"); + if (nodeGroupLabel) { + nodeGroupLabel.value = 'data-center'; + } + + // Display the node editor panel + const nodeEditorPanel = document.getElementById("panel-node-editor"); + if (nodeEditorPanel) { + nodeEditorPanel.style.display = "block"; + } + + // Fetch JSON schema from the backend + const url = "js/clabJsonSchema-v0.59.0.json"; + try { + const response = await fetch(url); + if (!response.ok) { + throw new Error(`HTTP error! Status: ${response.status}`); + } + const jsonData = await response.json(); + + // Get kind enums from the JSON data + const { kindOptions } = getKindEnums(jsonData); + console.log('Kind Enum:', kindOptions); + + // Populate the dropdown with fetched kindOptions + populateKindDropdown(kindOptions); + + // Populate the dropdown with fetched topoViwerRoleOptions + var topoViwerRoleOptions = ['bridge', 'controller', 'dcgw', 'router', 'leaf', 'pe', 'pon', 'rgw', 'server', 'super-spine', 'spine']; + populateTopoViewerRoleDropdown(topoViwerRoleOptions); + + // List type enums based on kind pattern + const typeOptions = getTypeEnumsByKindPattern(jsonData, '(srl|nokia_srlinux)'); // To be added to the UI + console.log('Type Enum for (srl|nokia_srlinux):', typeOptions); + + } catch (error) { + console.error("Error fetching or processing JSON data:", error.message); + throw error; + } + + } catch (error) { + console.error("Error in showPanelNodeEditor:", error); + // Optionally, display an error message to the user + const errorDiv = document.getElementById('panel-node-editor-error'); + if (errorDiv) { + errorDiv.textContent = "An error occurred while loading the node editor. Please try again."; + errorDiv.style.display = "block"; + } + } +} + +// Function to get kind enums from the JSON schema +function getKindEnums(jsonData) { + let kindOptions = []; + if (jsonData && jsonData.definitions && jsonData.definitions['node-config']) { + kindOptions = jsonData.definitions['node-config'].properties.kind.enum || []; + } else { + throw new Error("Invalid JSON structure or 'kind' enum not found"); + } + return { kindOptions, schemaData: jsonData }; +} + +// Function to get type enums based on a kind pattern +function getTypeEnumsByKindPattern(jsonData, pattern) { + if (jsonData && jsonData.definitions && jsonData.definitions['node-config'] && jsonData.definitions['node-config'].allOf) { + for (const condition of jsonData.definitions['node-config'].allOf) { + if (condition.if && condition.if.properties && condition.if.properties.kind && condition.if.properties.kind.pattern === pattern) { + if (condition.then && condition.then.properties && condition.then.properties.type && condition.then.properties.type.enum) { + return condition.then.properties.type.enum; + } + } + } + } + return []; +} + +let panelNodeEditorKind = "nokia_srlinux"; // Variable to store the selected option for dropdown menu +// Function to populate the kind dropdown +function populateKindDropdown(options) { + // Get the dropdown elements by their IDs + const dropdownTrigger = document.querySelector("#panel-node-kind-dropdown .dropdown-trigger button span"); + const dropdownContent = document.getElementById("panel-node-kind-dropdown-content"); + const dropdownButton = document.querySelector("#panel-node-kind-dropdown .dropdown-trigger button"); + const dropdownContainer = dropdownButton.closest(".dropdown"); + + if (!dropdownTrigger || !dropdownContent || !dropdownButton || !dropdownContainer) { + console.error("Dropdown elements not found in the DOM."); + return; + } + + // Set the initial value on the dropdown button + dropdownTrigger.textContent = panelNodeEditorKind; + + // Clear any existing content + dropdownContent.innerHTML = ""; + + options.forEach(option => { + // Create a new anchor element for each option + const optionElement = document.createElement("a"); + optionElement.classList.add("dropdown-item", "label", "has-text-weight-normal", "is-small", "py-0"); + optionElement.textContent = option; + optionElement.href = "#"; // Optional, can be adjusted as needed + + // Set an event handler for the option + optionElement.addEventListener("click", (event) => { + event.preventDefault(); // Prevent default link behavior + + panelNodeEditorKind = option; // Store the selected option in the variable + console.log(`${panelNodeEditorKind} selected`); // Log the selected option + + dropdownTrigger.textContent = panelNodeEditorKind; + + // Collapse the dropdown menu + dropdownContainer.classList.remove("is-active"); + }); + + // Append the option element to the dropdown content + dropdownContent.appendChild(optionElement); + }); +} + +// Initialize event listeners for the dropdown +function initializeDropdownListeners() { + const dropdownButton = document.querySelector("#panel-node-kind-dropdown .dropdown-trigger button"); + const dropdownContainer = dropdownButton.closest(".dropdown"); + + if (!dropdownButton || !dropdownContainer) { + console.error("Dropdown button or container not found in the DOM."); + return; + } + + // Toggle dropdown menu on button click + dropdownButton.addEventListener("click", (event) => { + event.stopPropagation(); // Prevents the event from bubbling up + dropdownContainer.classList.toggle("is-active"); + }); + + // Collapse the dropdown if clicked outside + document.addEventListener("click", (event) => { + if (dropdownContainer.classList.contains("is-active")) { + dropdownContainer.classList.remove("is-active"); + } + }); +} +// Initialize dropdown listeners once when the DOM is fully loaded +document.addEventListener("DOMContentLoaded", () => { + initializeDropdownListeners(); +}); + +let panelNodeEditorTopoViewerRole = "pe"; // Variable to store the selected option for dropdown menu +// Function to populate the topoViewerRole dropdown +function populateTopoViewerRoleDropdown(options) { + // Get the dropdown elements by their IDs + const dropdownTrigger = document.querySelector("#panel-node-topoviewerrole-dropdown .dropdown-trigger button span"); + const dropdownContent = document.getElementById("panel-node-topoviewerrole-dropdown-content"); + const dropdownButton = document.querySelector("#panel-node-topoviewerrole-dropdown .dropdown-trigger button"); + const dropdownContainer = dropdownButton.closest(".dropdown"); + + if (!dropdownTrigger || !dropdownContent || !dropdownButton || !dropdownContainer) { + console.error("Dropdown elements not found in the DOM."); + return; + } + + // Set the initial value on the dropdown button + dropdownTrigger.textContent = panelNodeEditorTopoViewerRole; + + // Clear any existing content + dropdownContent.innerHTML = ""; + + options.forEach(option => { + // Create a new anchor element for each option + const optionElement = document.createElement("a"); + optionElement.classList.add("dropdown-item", "label", "has-text-weight-normal", "is-small", "py-0"); + optionElement.textContent = option; + optionElement.href = "#"; // Optional, can be adjusted as needed + + // Set an event handler for the option + optionElement.addEventListener("click", (event) => { + event.preventDefault(); // Prevent default link behavior + + panelNodeEditorTopoViewerRole = option; // Store the selected option in the variable + console.log(`${panelNodeEditorTopoViewerRole} selected`); // Log the selected option + + dropdownTrigger.textContent = panelNodeEditorTopoViewerRole; + + // Collapse the dropdown menu + dropdownContainer.classList.remove("is-active"); + }); + + // Append the option element to the dropdown content + dropdownContent.appendChild(optionElement); + }); +} + +// Initialize event listeners for the dropdown +function initializeDropdownTopoViewerRoleListeners() { + const dropdownButton = document.querySelector("#panel-node-topoviewerrole-dropdown .dropdown-trigger button"); + const dropdownContainer = dropdownButton.closest(".dropdown"); + + if (!dropdownButton || !dropdownContainer) { + console.error("Dropdown button or container not found in the DOM."); + return; + } + + // Toggle dropdown menu on button click + dropdownButton.addEventListener("click", (event) => { + event.stopPropagation(); // Prevents the event from bubbling up + dropdownContainer.classList.toggle("is-active"); + }); + + // Collapse the dropdown if clicked outside + document.addEventListener("click", (event) => { + if (dropdownContainer.classList.contains("is-active")) { + dropdownContainer.classList.remove("is-active"); + } + }); +} + +// Initialize dropdown listeners once when the DOM is fully loaded +document.addEventListener("DOMContentLoaded", () => { + initializeDropdownTopoViewerRoleListeners(); +}); + +// Initialize event listener for the close button +document.getElementById("panel-node-editor-close-button").addEventListener("click", () => { + document.getElementById("panel-node-editor").style.display = "none"; +}); + +// Function to save node data from the editor +// Adjusted saveNodeToEditorToFile function +// update node data in the editor, save cyto json to file dataCytoMarshall.json and save to clab topo.yaml +async function saveNodeToEditorToFile() { + const nodeId =document.getElementById("panel-node-editor-id").textContent + var cyNode = cy.$id(nodeId); // Get cytoscpe node object id + + // get value from panel-node-editor + nodeName = document.getElementById("panel-node-editor-name").value + kind = panelNodeEditorKind + image = document.getElementById("panel-node-editor-image").value + group = document.getElementById("panel-node-editor-group").value + topoViewerRole = panelNodeEditorTopoViewerRole + + console.log("panelEditorNodeName", nodeName) + console.log("panelEditorkind", kind) + console.log("panelEditorImage", image) + console.log("panelEditorGroup", group) + console.log("panelEditorTopoViewerRole",topoViewerRole) + + // save node data to cytoscape node object + var extraData = { + "kind": kind, + "image": image, + "longname": "", + "mgmtIpv4Addresss": "" + }; + + cyNode.data(('name'), nodeName) + cyNode.data(('parent'), group) + cyNode.data(('topoViewerRole'), topoViewerRole) + cyNode.data(('extraData'), extraData) + + console.log('cyto node object data: ', cyNode); + + // dump cytoscape node object to nodeData to be persisted to dataCytoMarshall.json + var nodeData = cy.$id(nodeId).json(); // Get JSON data of the node with the specified ID + const endpointName = '/clab-save-topo-cyto-json'; + + try { + // Send the enhanced node data directly without wrapping it in an object + const response = await sendRequestToEndpointPost(endpointName, [nodeData]); + console.log('Node data saved successfully', response); + } catch (error) { + console.error('Failed to save node data:', error); + } + + // add node to clab editor textarea + clabEditorAddNode(nodeId, nodeName, kind, image, group, topoViewerRole) + + // clabEditorSaveYamlTopo() +} + +async function getYamlTopoContent(yamlTopoContent) { + // Wait until the Monaco Editor is initialized + // await monacoEditorReady; + + try { + // Check if yamlTopoContent is already set + console.log('YAML Topo Initial Content:', yamlTopoContent); + + if (!yamlTopoContent) { + // Load the content if yamlTopoContent is empty + yamlTopoContent = await sendRequestToEndpointGetV3("/get-yaml-topo-content"); + } + + console.log('YAML Topo Content:', yamlTopoContent); + + // Set the content of the Monaco Editor + window.monacoEditor.setValue(yamlTopoContent); + } catch (error) { + console.error("Error occurred:", error); + // Handle errors as needed + } +} \ No newline at end of file diff --git a/dist/html-static/js/clabJsonSchema-v0.59.0.json b/dist/html-static/js/clabJsonSchema-v0.59.0.json new file mode 100644 index 000000000..50b31559b --- /dev/null +++ b/dist/html-static/js/clabJsonSchema-v0.59.0.json @@ -0,0 +1,1073 @@ +{ + "$id": "https://containerlab.dev/clab.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Containerlab topology definition file", + "definitions": { + "node-config": { + "type": "object", + "description": "topology node configuration container", + "markdownDescription": "topology [node](https://containerlab.dev/manual/nodes/) configuration container", + "properties": { + "image": { + "type": "string", + "description": "container image to use for this node", + "markdownDescription": "container [image](https://containerlab.dev/manual/nodes/#image) to use for this node" + }, + "image-pull-policy": { + "type": "string", + "description": "policy for pulling the referenced container image", + "markdownDescription": "container [image-pull-policy](https://containerlab.dev/manual/nodes/#image-pull-policy) to use for this node", + "enum": [ + "always", + "Always", + "never", + "Never", + "ifnotpresent", + "IfNotPresent" + ] + }, + "restart-policy": { + "type": "string", + "description": "restart policy for the referenced container image", + "markdownDescription": "container [restart-policy](https://containerlab.dev/manual/nodes/#restart-policy) to use for this node", + "enum": [ + "no", + "No", + "on-failure", + "On-failure", + "Always", + "always", + "unless-stopped", + "Unless-stopped" + ] + }, + "kind": { + "type": "string", + "description": "kind of this node", + "markdownDescription": "[kind](https://containerlab.dev/manual/nodes/#kind) of this node", + "enum": [ + "srl", + "nokia_srlinux", + "ceos", + "arista_ceos", + "crpd", + "juniper_crpd", + "sonic-vs", + "sonic-vm", + "vr-sros", + "nokia_sros", + "vr-nokia_sros", + "vr-vmx", + "vr-juniper_vmx", + "juniper_vmx", + "vr-vqfx", + "vr-juniper_vqfx", + "juniper_vqfx", + "vr-vsrx", + "vr-juniper_vsrx", + "juniper_vsrx", + "juniper_vjunosrouter", + "juniper_vjunosswitch", + "juniper_vjunosevolved", + "vr-xrv", + "vr-cisco_xrv", + "cisco_xrv", + "vr-xrv9k", + "vr-cisco_xrv9k", + "cisco_xrv9k", + "vr-veos", + "vr-arista_veos", + "arista_veos", + "vr-csr", + "vr-cisco_csr", + "cisco_csr1000v", + "vr-pan", + "vr-paloalto_panos", + "paloalto_panos", + "vr-ros", + "vr-mikrotik_ros", + "mikrotik_ros", + "vr-n9kv", + "vr-cisco_n9kv", + "cisco_n9kv", + "cisco_ftdv", + "dell_ftosv", + "dell_sonic", + "vr-aoscx", + "vr-aruba_aoscx", + "aruba_aoscx", + "linux", + "bridge", + "ovs-bridge", + "border0", + "host", + "keysight_ixia-c-one", + "ipinfusion_ocnos", + "checkpoint_cloudguard", + "ext-container", + "xrd", + "rare", + "cisco_xrd", + "c8000", + "cisco_c8000", + "cisco_c8000v", + "cisco_cat9kv", + "cisco_iol", + "cvx", + "cumulus_cvx", + "huawei_vrp", + "openbsd", + "freebsd", + "generic_vm", + "fortinet_fortigate", + "k8s-kind" + ] + }, + "license": { + "type": "string", + "description": "path to a license file", + "markdownDescription": "path to a [license](https://containerlab.dev/manual/nodes/#license) file" + }, + "type": { + "type": "string", + "description": "type is a per-node property that can select a special type of a node", + "markdownDescription": "node's [type](https://containerlab.dev/manual/nodes/#type) file" + }, + "group": { + "type": "string", + "description": "grouping parameter of a node. A free form string that is mainly used in sorting elements when graphing", + "markdownDescription": "path to a [license](https://containerlab.dev/manual/nodes/#group) file" + }, + "startup-config": { + "type": "string", + "description": "path to a startup config file (if supported by the kind)", + "markdownDescription": "path to a startup [config file](https://containerlab.dev/manual/nodes/#startup-config) (if supported by the kind)" + }, + "startup-delay": { + "type": "integer", + "description": "Optional startup delay (seconds) to apply", + "markdownDescription": "Optional [startup delay](https://containerlab.dev/manual/nodes/#startup-delay) in seconds" + }, + "enforce-startup-config": { + "type": "boolean", + "description": "Set to `true` to make the node to boot with a startup-config even if the config file is present in the lab directory", + "markdownDescription": "Set to `true` to [make the node to boot with a startup-config](https://containerlab.dev/manual/nodes/#enforce-startup-config) even if the config file is present in the lab directory" + }, + "auto-remove": { + "type": "boolean", + "description": "Set to `true` to remove the node automatically, instead of auto-restarting", + "markdownDescription": "Set to `true` to [remove the node/container automatically](https://containerlab.dev/manual/nodes/#auto-remove), instead of auto-restarting it" + }, + "exec": { + "type": "array", + "description": "list of commands to execute post deploy", + "markdownDescription": "list of [commands to execute](https://containerlab.dev/manual/nodes/#exec) post deploy", + "minItems": 1, + "items": { + "type": "string" + } + }, + "binds": { + "type": "array", + "description": "list of file/directory bindings", + "markdownDescription": "list of file/directory [bindings](https://containerlab.dev/manual/nodes/#binds)", + "minItems": 1, + "items": { + "type": "string" + }, + "uniqueItems": true + }, + "ports": { + "type": "array", + "description": "list of port mappings", + "markdownDescription": "list of [port](https://containerlab.dev/manual/nodes/#ports) mappings", + "minItems": 1, + "items": { + "type": "string", + "pattern": "^(([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])(%[\\p{N}\\p{L}]+)?:([0-9]{1,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5]):([0-9]{1,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$|^(([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])(%[\\p{N}\\p{L}]+)?:([0-9]{1,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5]):([0-9]{1,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])+(\/tcp|\/udp|\/sctp)$|^([0-9]{1,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5]):([0-9]{1,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$|^([0-9]{1,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5]):([0-9]{1,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])+(\/tcp|\/udp|\/sctp)$|^([0-9]{1,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$|^([0-9]{1,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])-([0-9]{1,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5]):([0-9]{1,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])+(\/tcp|\/udp|\/sctp)?$" + }, + "uniqueItems": true + }, + "env": { + "type": "object", + "description": "environment variables", + "markdownDescription": "[environment variables](https://containerlab.dev/manual/nodes/#env)", + "patternProperties": { + ".+": { + "anyOf": [ + { + "type": "string", + "minItems": 1 + }, + { + "type": "number", + "minItems": 1 + } + ] + } + } + }, + "user": { + "description": "user to use within the container", + "markdownDescription": "[user](https://containerlab.dev/manual/nodes/#user) to use within the container", + "anyOf": [ + { + "type": "string", + "minItems": 1 + }, + { + "type": "number", + "minItems": 1 + } + ] + }, + "entrypoint": { + "type": "string", + "description": "container's entrypoint", + "markdownDescription": "container's [entrypoint](https://containerlab.dev/manual/nodes/#entrypoint)" + }, + "cmd": { + "type": "string", + "description": "command to launch container with", + "markdownDescription": "[command](https://containerlab.dev/manual/nodes/#cmd) to launch container with" + }, + "publish": { + "type": "array", + "description": "list of ports to publish", + "markdownDescription": "list of ports to [publish](https://containerlab.dev/manual/nodes/#publish)", + "minItems": 1, + "items": { + "type": "string", + "pattern": "(^http|^https|^tcp|^tls)\/(([0-9]+$)|([0-9]+\/.+$))" + }, + "uniqueItems": true + }, + "labels": { + "type": "object", + "description": "container labels", + "markdownDescription": "container [labels](https://containerlab.dev/manual/nodes/#labels)", + "patternProperties": { + ".+": { + "anyOf": [ + { + "type": "string", + "minItems": 1 + }, + { + "type": "number", + "minItems": 1 + } + ] + } + } + }, + "runtime": { + "type": "string", + "description": "Runtime used to launch the container node", + "markdownDescription": "[Runtime](https://containerlab.dev/manual/nodes/#runtime) for the node", + "enum": [ + "docker", + "ignite" + ] + }, + "mgmt-ipv4": { + "type": "string", + "description": "IPv4 management address of the node (e.g. 172.10.10.11)", + "markdownDescription": "[IPv4 management address](https://containerlab.dev/manual/nodes/#mgmt-ipv4) of the node (e.g. 172.10.10.11)", + "pattern": "^(([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])(%[\\p{N}\\p{L}]+)?$" + }, + "mgmt-ipv6": { + "type": "string", + "description": "IPv6 management address of the node (e.g. 172.10.10.11)", + "markdownDescription": "[IPv6 management address](https://containerlab.dev/manual/nodes/#mgmt-ipv6) of the node (e.g. 172.10.10.11)", + "pattern": "^((:|[0-9a-fA-F]{0,4}):)([0-9a-fA-F]{0,4}:){0,5}((([0-9a-fA-F]{0,4}:)?(:|[0-9a-fA-F]{0,4}))|(((25[0-5]|2[0-4][0-9]|[01]?[0-9]?[0-9])\\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9]?[0-9])))(%[\\p{N}\\p{L}]+)?$" + }, + "network-mode": { + "type": "string", + "description": "node network mode (can only be set host, defaults to bridge)", + "markdownDescription": "node [network mode](https://containerlab.dev/manual/nodes/#network-mode) (can only be set host, defaults to bridge)", + "pattern": "^(host)|(container:\\S+)|(none)$" + }, + "cpu": { + "type": "integer", + "description": "number of vcpu to allocate for this node/container", + "markdownDescription": "Allowed [CPU](https://containerlab.dev/manual/nodes/#cpu) usage by the node/container" + }, + "memory": { + "type": "string", + "description": "memory limit for this node/container", + "markdownDescription": "Allowed [Memory](https://containerlab.dev/manual/nodes/#memory) usage by the node/container" + }, + "cpu-set": { + "type": "string", + "description": "CPU cores to use by this node/container", + "markdownDescription": "[CPU cores](https://containerlab.dev/manual/nodes/#cpu-set) to be used by the node/container" + }, + "sandbox": { + "type": "string", + "description": "ignite's sandbox image name" + }, + "kernel": { + "type": "string", + "description": "ignite's kernel image name" + }, + "extras": { + "type": "object", + "$ref": "#/definitions/extras-config" + }, + "config": { + "$ref": "#/definitions/config-config" + }, + "stages": { + "type": "object", + "$ref": "#/definitions/stages-config" + }, + "dns": { + "type": "object", + "$ref": "#/definitions/dns-config" + }, + "certificate": { + "type": "object", + "$ref": "#/definitions/certificate-config" + }, + "healthcheck": { + "type": "object", + "$ref": "#/definitions/healthcheck-config" + } + }, + "allOf": [ + { + "properties": { + "type": { + "type": "string", + "description": "type of a node", + "markdownDescription": "node [type](https://containerlab.dev/manual/nodes/#type)" + } + } + }, + { + "if": { + "properties": { + "kind": { + "pattern": "(srl|nokia_srlinux)" + } + }, + "required": [ + "kind" + ] + }, + "then": { + "properties": { + "type": { + "type": "string", + "enum": [ + "ixsa1", + "ixrd1", + "ixrd2", + "ixrd3", + "ixrd2l", + "ixrd3l", + "ixrd4", + "ixrd5", + "ixrh2", + "ixrh3", + "ixrh4", + "ixr6", + "ixr6e", + "ixr10", + "ixr10e", + "sxr1x44s", + "sxr1d32d", + "ixrx1b", + "ixrx3b" + ] + } + } + } + }, + { + "if": { + "properties": { + "kind": { + "pattern": "(vr-sros|vr-nokia_sros)" + } + }, + "required": [ + "kind" + ] + }, + "then": { + "properties": { + "type": { + "type": "string", + "anyOf": [ + { + "enum": [ + "sr-1", + "sr-1e", + "sr-1e-sec", + "sr-1s", + "sr-1s-macsec", + "sr-2s", + "sr-7s", + "sr-7s-fp4", + "sr-14s", + "sr-a4", + "ixr-e-small", + "ixr-e-big", + "ixr-e2", + "ixr-ec", + "ixr-r6", + "ixr-s" + ] + }, + { + "pattern": "cp:.+" + } + ] + } + } + } + } + ], + "additionalProperties": false + }, + "link-config": { + "type": "object", + "description": "link configuration container", + "markdownDescription": "link configuration container", + "properties": { + "endpoints": { + "type": "array", + "description": "endpoints list", + "markdownDescription": "[endpoints](http://localhost:8000/manual/topo-def-file/#links) list", + "minItems": 2, + "items": { + "type": "string", + "pattern": "^\\S+:\\S+$" + }, + "uniqueItems": true + }, + "vars": { + "description": "link-scoped variables used by config engine", + "markdownDescription": "link-scoped variables used by config engine", + "type": "object" + } + } + }, + "extras-config": { + "type": "object", + "description": "node's extra configurations", + "properties": { + "srl-agents": { + "type": "array", + "description": "list of SR Linux agent's config files to be copied to the NOS filesystem", + "markdownDescription": "list of [SR Linux agent's config files](https://containerlab.dev/manual/kinds/srl/#user-defined-custom-agents-for-sr-linux-nodes) to be copied to the NOS filesystem", + "minItems": 1, + "items": { + "type": "string" + }, + "uniqueItems": true + }, + "mysocket-proxy": { + "type": "string", + "description": "http/s proxy to be used by mysocketctl" + } + } + }, + "config-config": { + "type": "object", + "description": "containerlab config engine parameters", + "properties": { + "vars": { + "type": "object", + "description": "config variables passed to config engine", + "markdownDescription": "config variables passed to config engine" + } + } + }, + "certificate-config": { + "type": "object", + "description": "Node's Certificate configuration option", + "markdownDescription": "Node's [Certificate configuration options](https://containerlab.dev/manual/nodes/#certificate)", + "properties": { + "issue": { + "description": "Set to `true` to generate a TLS certificate for the node", + "markdownDescription": "Set to `true` to [generate a TLS certificate for the node](https://containerlab.dev/manual/nodes/#certificate)" + }, + "sans": { + "type": "array", + "description": "list of subject alternative names (SAN) to use for this node", + "markdownDescription": "list of [subject alternative names](https://containerlab.dev/manual/nodes/#subject-alternative-names) to use for this node", + "items": { + "type": "string" + }, + "uniqueItems": true + }, + "key-size": { + "type": "integer", + "description": "size of the to be generated key", + "markdownDescription": "size of the to be generated key" + }, + "validity-duration": { + "type": "string", + "description": "Duration for how long the certificate issued by the CA will be valid.", + "markdownDescription": "Duration for how long the certificate issued by the CA will be valid." + } + } + }, + "healthcheck-config": { + "type": "object", + "description": "Node's Healthcheck configuration option", + "markdownDescription": "Node's [Healthcheck configuration options](https://containerlab.dev/manual/nodes/#healthcheck)", + "properties": { + "test": { + "type": "array", + "description": "test command", + "items": { + "type": "string" + } + }, + "interval": { + "type": "integer", + "description": "test execution interval", + "markdownDescription": "test execution interval" + }, + "retries": { + "type": "integer", + "description": "test execution retries", + "markdownDescription": "test execution retries" + }, + "timeout": { + "type": "integer", + "description": "test execution timeout in seconds", + "markdownDescription": "test execution timeout in seconds" + }, + "start-period": { + "type": "integer", + "description": "time in seconds to wait before starting the healthcheck" + } + } + }, + "dns-config": { + "type": "object", + "description": "Node's DNS configuration option", + "markdownDescription": "Node's [DNS configuration options](https://containerlab.dev/manual/nodes/#dns)", + "properties": { + "servers": { + "type": "array", + "description": "DNS server addresses", + "items": { + "type": "string" + }, + "uniqueItems": true + }, + "search": { + "type": "array", + "description": "DNS search domains", + "items": { + "type": "string" + }, + "uniqueItems": true + }, + "options": { + "type": "array", + "description": "DNS options", + "items": { + "type": "string" + }, + "uniqueItems": true + } + } + }, + "certificate-authority-config": { + "type": "object", + "description": "Certificate Authority", + "markdownDescription": "", + "properties": { + "cert": { + "type": "string", + "description": "Path to the CA certificate file. If set, it is expected that the CA certificate already exists by that path" + }, + "key": { + "type": "string", + "description": "Path to the CA key file. If set, it is expected that the CA certificate already exists by that path" + }, + "key-size": { + "type": "integer", + "description": "Key size. Can only be set if the external CA certificate is not provided" + }, + "validity-duration": { + "type": "string", + "description": "CA certificate validity duration. Can only be set if the external CA certificate is not provided" + } + }, + "oneOf": [ + { + "required": [ + "cert", + "key" + ], + "not": { + "anyOf": [ + { + "required": [ + "key-size" + ] + }, + { + "required": [ + "validity-duration" + ] + } + ] + } + }, + { + "anyOf": [ + { + "required": [ + "key-size" + ] + }, + { + "required": [ + "validity-duration" + ] + } + ], + "not": { + "anyOf": [ + { + "required": [ + "cert" + ] + }, + { + "required": [ + "key" + ] + } + ] + } + } + ] + }, + "stages-config": { + "type": "object", + "description": "node's stages configurations", + "markdownDescription": "node's [stages](https://containerlab.dev/manual/nodes/#stages) configurations", + "properties": { + "create": { + "type": "object", + "description": "create stage configuration", + "properties": { + "wait-for": { + "$ref": "#/definitions/wait-for-config" + }, + "exec": { + "$ref": "#/definitions/stage-exec" + } + }, + "additionalProperties": false + }, + "create-links": { + "type": "object", + "description": "create stage configuration", + "properties": { + "wait-for": { + "$ref": "#/definitions/wait-for-config" + }, + "exec": { + "$ref": "#/definitions/stage-exec" + } + }, + "additionalProperties": false + }, + "configure": { + "type": "object", + "description": "create stage configuration", + "properties": { + "wait-for": { + "$ref": "#/definitions/wait-for-config" + }, + "exec": { + "$ref": "#/definitions/stage-exec" + } + }, + "additionalProperties": false + }, + "healthy": { + "type": "object", + "description": "create stage configuration", + "properties": { + "wait-for": { + "$ref": "#/definitions/wait-for-config" + }, + "exec": { + "$ref": "#/definitions/stage-exec" + } + }, + "additionalProperties": false + }, + "exit": { + "type": "object", + "description": "create stage configuration", + "properties": { + "wait-for": { + "$ref": "#/definitions/wait-for-config" + }, + "exec": { + "$ref": "#/definitions/stage-exec" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + "wait-for-config": { + "type": "array", + "items": { + "type": "object", + "properties": { + "node": { + "type": "string", + "description": "node name to wait for" + }, + "stage": { + "type": "string", + "description": "phase to wait for", + "$ref": "#/definitions/stages-enum" + } + }, + "additionalProperties": false + }, + "uniqueItems": true, + "description": "Dependency list for the node", + "markdownDescription": "Dependency list for the node" + }, + "stages-enum": { + "type": "string", + "enum": [ + "create", + "create-links", + "configure", + "healthy", + "exit" + ] + }, + "stage-exec": { + "type": "object", + "description": "per-stage exec configuration", + "properties": { + "on-enter": { + "$ref": "#/definitions/stage-exec-list" + }, + "on-exit": { + "$ref": "#/definitions/stage-exec-list" + } + } + }, + "stage-exec-list": { + "type": "array", + "description": "list of commands to execute", + "markdownDescription": "list of [commands to execute](https://containerlab.dev/manual/nodes/#exec)", + "minItems": 1, + "items": { + "type": "string" + } + } + }, + "type": "object", + "properties": { + "name": { + "description": "topology name", + "type": "string" + }, + "prefix": { + "description": "lab prefix", + "type": "string", + "markdownDescription": "[lab prefix](https://containerlab.dev/manual/topo-def-file/#prefix)" + }, + "mgmt": { + "description": "configuration container for management network", + "markdownDescription": "configuration container for [management network](https://containerlab.dev/manual/network/#management-network)", + "type": "object", + "properties": { + "network": { + "description": "management network name", + "markdownDescription": "[management network name](https://containerlab.dev/manual/network/#network-name)", + "type": "string" + }, + "bridge": { + "description": "Set bridge to use for the management network (instead of the default generated bridge).", + "markdownDescription": "Set [bridge](https://containerlab.dev/manual/network/#bridge-name) to use for the management network (instead of the default generated bridge).", + "type": "string" + }, + "ipv4-subnet": { + "description": "IPv4 subnet to use for the custom management network. e.g. 172.100.100.0/24", + "markdownDescription": "[IPv4 subnet](https://containerlab.dev/manual/network/#user-defined-addresses) to use for the custom management network. e.g. 172.100.100.0/24", + "type": "string", + "pattern": "(^.+\/[0-9]{1,2}$)|(auto)" + }, + "ipv6-subnet": { + "description": "IPv6 subnet to use for the custom management network. e.g. 3fff:172:100:100::/64", + "markdownDescription": "[IPv6 subnet](https://containerlab.dev/manual/network/#user-defined-addresses) to be used for the custom management network. e.g. 3fff:172:100:100::/64", + "type": "string", + "pattern": "(^.+\/[0-9]{1,3}$)|(auto)" + }, + "ipv4-gw": { + "description": "IPv4 gateway address that will be set on a bridge used for the management network. Will be set to the first available IP address by default", + "markdownDescription": "IPv4 gateway address that will be set on a bridge used for the management network. Will be set to the first available IP address by default", + "type": "string", + "pattern": "^(([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])(%[\\p{N}\\p{L}]+)?$" + }, + "ipv6-gw": { + "description": "IPv6 gateway address that will be set on a bridge used for the management network. Will be set to the first available IP address by default", + "markdownDescription": "IPv6 gateway address that will be set on a bridge used for the management network. Will be set to the first available IP address by default", + "type": "string", + "pattern": "^((:|[0-9a-fA-F]{0,4}):)([0-9a-fA-F]{0,4}:){0,5}((([0-9a-fA-F]{0,4}:)?(:|[0-9a-fA-F]{0,4}))|(((25[0-5]|2[0-4][0-9]|[01]?[0-9]?[0-9])\\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9]?[0-9])))(%[\\p{N}\\p{L}]+)?$" + }, + "ipv4-range": { + "description": "IPv4 range out of the ipv4-subnet to use for the custom management network. e.g. 172.100.100.128/25", + "markdownDescription": "[IPv4 range](https://containerlab.dev/manual/network/#ip-range) out of the ipv4-subnet to use for the custom management network. e.g. 172.100.100.128/25", + "type": "string", + "pattern": "^.+\/[0-9]{1,2}$" + }, + "ipv6-range": { + "description": "IPv6 range out of the ipv6-subnet to use for the custom management network. e.g. 3fff:172:100:100:8000::/65", + "markdownDescription": "[IPv6 range](https://containerlab.dev/manual/network/#ip-range) out of the ipv6-subnet to use for the custom management network. e.g. 3fff:172:100:100:8000::/65", + "type": "string", + "pattern": "^((:|[0-9a-fA-F]{0,4}):)([0-9a-fA-F]{0,4}:){0,5}((([0-9a-fA-F]{0,4}:)?(:|[0-9a-fA-F]{0,4}))|(((25[0-5]|2[0-4][0-9]|[01]?[0-9]?[0-9])\\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9]?[0-9])))(%[\\p{N}\\p{L}]+)?$" + }, + "mtu": { + "description": "MTU for the custom network", + "markdownDescription": "[MTU](https://containerlab.dev/manual/network/#mtu) in Bytes for the custom management network", + "type": "number", + "maximum": 65535, + "minimum": 1, + "default": 1500 + } + }, + "minProperties": 1 + }, + "topology": { + "description": "topology configuration container", + "markdownDescription": "[topology](https://containerlab.dev/manual/topo-def-file/) configuration container", + "type": "object", + "properties": { + "nodes": { + "description": "topology nodes configuration container", + "markdownDescription": "topology [nodes](https://containerlab.dev/manual/nodes/) configuration container", + "type": "object", + "patternProperties": { + ".*": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/definitions/node-config" + } + ] + } + } + }, + "kinds": { + "description": "topology kinds configuration container", + "markdownDescription": "topology [kinds](https://containerlab.dev/manual/topo-def-file/#kinds) configuration container", + "type": "object", + "properties": { + "srl": { + "$ref": "#/definitions/node-config" + }, + "nokia_srlinux": { + "$ref": "#/definitions/node-config" + }, + "ceos": { + "$ref": "#/definitions/node-config" + }, + "arista_ceos": { + "$ref": "#/definitions/node-config" + }, + "vr-arista_veos": { + "$ref": "#/definitions/node-config" + }, + "juniper_crpd": { + "$ref": "#/definitions/node-config" + }, + "crpd": { + "$ref": "#/definitions/node-config" + }, + "sonic-vs": { + "$ref": "#/definitions/node-config" + }, + "sonic-vm": { + "$ref": "#/definitions/node-config" + }, + "dell_ftosv": { + "$ref": "#/definitions/node-config" + }, + "dell_sonic": { + "$ref": "#/definitions/node-config" + }, + "vr-nokia_sros": { + "$ref": "#/definitions/node-config" + }, + "vr-sros": { + "$ref": "#/definitions/node-config" + }, + "vr-juniper_vmx": { + "$ref": "#/definitions/node-config" + }, + "vr-vmx": { + "$ref": "#/definitions/node-config" + }, + "vr-juniper_vsrx": { + "$ref": "#/definitions/node-config" + }, + "vr-vsrx": { + "$ref": "#/definitions/node-config" + }, + "juniper_vjunosrouter": { + "$ref": "#/definitions/node-config" + }, + "juniper_vjunosswitch": { + "$ref": "#/definitions/node-config" + }, + "juniper_vjunosevolved": { + "$ref": "#/definitions/node-config" + }, + "vr-aruba_aoscx": { + "$ref": "#/definitions/node-config" + }, + "vr-aoscx": { + "$ref": "#/definitions/node-config" + }, + "vr-cisco_xrv": { + "$ref": "#/definitions/node-config" + }, + "vr-xrv": { + "$ref": "#/definitions/node-config" + }, + "vr-cisco_xrv9k": { + "$ref": "#/definitions/node-config" + }, + "vr-xrv9k": { + "$ref": "#/definitions/node-config" + }, + "vr-cisco_nxos": { + "$ref": "#/definitions/node-config" + }, + "vr-nxos": { + "$ref": "#/definitions/node-config" + }, + "vr-cisco_csr": { + "$ref": "#/definitions/node-config" + }, + "vr-csr": { + "$ref": "#/definitions/node-config" + }, + "cisco_cat9kv": { + "$ref": "#/definitions/node-config" + }, + "cisco_ftdv": { + "$ref": "#/definitions/node-config" + }, + "cisco_iol": { + "$ref": "#/definitions/node-config" + }, + "linux": { + "$ref": "#/definitions/node-config" + }, + "bridge": { + "$ref": "#/definitions/node-config" + }, + "ovs-bridge": { + "$ref": "#/definitions/node-config" + }, + "mysocketio": { + "$ref": "#/definitions/node-config" + }, + "host": { + "$ref": "#/definitions/node-config" + }, + "ipinfusion_ocnos": { + "$ref": "#/definitions/node-config" + }, + "keysight_ixia-c-one": { + "$ref": "#/definitions/node-config" + }, + "checkpoint_cloudguard": { + "$ref": "#/definitions/node-config" + }, + "ext-container": { + "$ref": "#/definitions/node-config" + }, + "xrd": { + "$ref": "#/definitions/node-config" + }, + "rare": { + "$ref": "#/definitions/node-config" + }, + "c8000": { + "$ref": "#/definitions/node-config" + }, + "c8000v": { + "$ref": "#/definitions/node-config" + }, + "cvx": { + "$ref": "#/definitions/node-config" + }, + "cumulus_cvx": { + "$ref": "#/definitions/node-config" + }, + "openbsd": { + "$ref": "#/definitions/node-config" + }, + "freebsd": { + "$ref": "#/definitions/node-config" + }, + "huawei_vrp": { + "$ref": "#/definitions/node-config" + }, + "generic_vm": { + "$ref": "#/definitions/node-config" + } + } + }, + "defaults": { + "$ref": "#/definitions/node-config" + }, + "links": { + "type": "array", + "description": "topology links section", + "markdownDescription": "[topology links](https://containerlab.dev/manual/topo-def-file/#links)", + "minItems": 1, + "items": { + "$ref": "#/definitions/link-config" + } + } + }, + "required": [ + "nodes" + ] + }, + "settings": { + "description": "Global containerlab settings", + "markdownDescription": "Global [containerlab settings]()", + "type": "object", + "properties": { + "certificate-authority": { + "$ref": "#/definitions/certificate-authority-config" + } + } + } + }, + "additionalProperties": false, + "required": [ + "name", + "topology" + ] +} \ No newline at end of file diff --git a/dist/html-static/js/common.js b/dist/html-static/js/common.js index 234288a40..060987d71 100644 --- a/dist/html-static/js/common.js +++ b/dist/html-static/js/common.js @@ -36,7 +36,7 @@ async function getEnvironments(event) { try { - const environments = await sendRequestToEndpointGet("/get-environments"); + const environments = await sendRequestToEndpointGetV2("/get-environments"); // Handle the response data if (environments && typeof environments === 'object' && Object.keys(environments).length > 0) { @@ -52,6 +52,8 @@ } } + + async function postPythonAction(event, commandList) { try { showLoadingSpinnerGlobal() @@ -107,12 +109,12 @@ async function sendRequestToEndpointPost(endpointName, argsList = []) { console.log(`callGoFunction Called with ${endpointName}`); console.log(`Parameters:`, argsList); - + const data = {}; argsList.forEach((arg, index) => { data[`param${index + 1}`] = arg; }); - + try { const response = await fetch(endpointName, { method: "POST", @@ -121,11 +123,11 @@ }, body: JSON.stringify(data), }); - + if (!response.ok) { throw new Error("Network response was not ok"); } - + const responseData = await response.json(); return responseData; } catch (error) { @@ -133,6 +135,7 @@ throw error; } } + async function sendRequestToEndpointGet(endpointName, argsList = []) { console.log(`callGoFunction Called with ${endpointName}`); console.log(`Parameters:`, argsList); @@ -201,6 +204,50 @@ } } + + async function sendRequestToEndpointGetV3(endpointName, argsList = []) { + console.log(`callGoFunction Called with ${endpointName}`); + console.log(`Parameters:`, argsList); + + // Construct the query string from argsList + const params = new URLSearchParams(); + argsList.forEach((arg, index) => { + params.append(`param${index + 1}`, arg); + }); + + const urlWithParams = `${endpointName}?${params.toString()}`; + + try { + const response = await fetch(urlWithParams, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }); + + if (!response.ok) { + throw new Error(`HTTP error! Status: ${response.status}`); + } + + // Check if the response is JSON; otherwise, return as text + const contentType = response.headers.get("Content-Type"); + let responseData; + + if (contentType && contentType.includes("application/json")) { + responseData = await response.json(); + } else { + responseData = await response.text(); // Return as-is for non-JSON content + } + + console.log(responseData); + + return responseData; + } catch (error) { + console.error("Error:", error); + throw error; + } + } + // Function to detect light or dark mode function detectColorScheme() { // Check if the browser supports the prefers-color-scheme media feature diff --git a/dist/html-static/js/dev copy.js b/dist/html-static/js/dev copy.js new file mode 100644 index 000000000..c1e26fe8d --- /dev/null +++ b/dist/html-static/js/dev copy.js @@ -0,0 +1,3060 @@ +// Initialize a state variable to track the element's presence +var isPanel01Cy = false; +var nodeClicked = false; +var edgeClicked = false; + +var cy + +var globalSelectedNode +var globalSelectedEdge + +var linkEndpointVisibility = true; +var nodeContainerStatusVisibility = false; + + +var globalShellUrl = "/js/cloudshell" + +var labName +var deploymentType + +document.addEventListener("DOMContentLoaded", async function() { + + detectColorScheme() + + await changeTitle() + + + // Reusable function to initialize a WebSocket connection + function initializeWebSocket(url, onMessageCallback) { + const protocol = location.protocol === "https:" ? "wss://" : "ws://"; + const socket = new WebSocket(protocol + location.host + url); + + socket.onopen = () => { + console.log(`Successfully connected WebSocket to ${url}`); + if (socket.readyState === WebSocket.OPEN) { + socket.send(`Hi From the WebSocketClient-${url}`); + } + }; + + socket.onclose = (event) => { + console.log(`Socket to ${url} closed: `, event); + socket.send("Client Closed!"); + }; + + socket.onerror = (error) => { + console.log(`Socket to ${url} error: `, error); + }; + + socket.onmessage = onMessageCallback; + + return socket; + } + + + // WebSocket for uptime + // WebSocket for uptime + const socketUptime = initializeWebSocket("/uptime", async (msgUptime) => { + environments = await getEnvironments(); + labName = environments["clab-name"] + deploymentType = environments["deploymentType"] + + console.log("initializeWebSocket - getEnvironments", environments) + console.log("initializeWebSocket - labName", environments["clab-name"]) + + const string01 = "Containerlab Topology: " + labName; + const string02 = " ::: Uptime: " + msgUptime.data; + + const ClabSubtitle = document.getElementById("ClabSubtitle"); + const messageBody = string01 + string02; + + ClabSubtitle.innerText = messageBody; + console.log(ClabSubtitle.innerText); + }); + + // WebSocket for ContainerNodeStatus + const socketContainerNodeStatusInitial = initializeWebSocket( + "/containerNodeStatus", + (msgContainerNodeStatus) => { + try { + const { + Names, + Status, + State + } = JSON.parse(msgContainerNodeStatus.data); + setNodeContainerStatus(Names, Status); + console.log(JSON.parse(msgContainerNodeStatus.data)); + + const IPAddress = JSON.parse(msgContainerNodeStatus.data).Networks.Networks.clab.IPAddress; + const GlobalIPv6Address= JSON.parse(msgContainerNodeStatus.data).Networks.Networks.clab.GlobalIPv6Address + + + setNodeDataWithContainerAttribute(Names, Status, State, IPAddress, GlobalIPv6Address); + + } catch (error) { + console.error("Error parsing JSON:", error); + } + }, + ); + + //- Instantiate Cytoscape.js + cy = cytoscape({ + container: document.getElementById("cy"), + elements: [], + style: [{ + selector: "node", + style: { + "background-color": "#3498db", + label: "data(label)", + }, + }, ], + }); + + + // // Initialize cytoscape-edgehandles plugin + // cytoscape.use(cytoscapeEdgehandles); + + // Initialize edgehandles with configuration + const eh = cy.edgehandles({ + // Enable preview of edge before finalizing + preview: false, + hoverDelay: 50, // time spent hovering over a target node before it is considered selected + snap: false, // when enabled, the edge can be drawn by just moving close to a target node (can be confusing on compound graphs) + snapThreshold: 10, // the target node must be less than or equal to this many pixels away from the cursor/finger + snapFrequency: 150, // the number of times per second (Hz) that snap checks done (lower is less expensive) + noEdgeEventsInDraw: false, // set events:no to edges during draws, prevents mouseouts on compounds + disableBrowserGestures: false, // during an edge drawing gesture, disable browser gestures such as two-finger trackpad swipe and pinch-to-zoom + canConnect: function( sourceNode, targetNode ){ + // whether an edge can be created between source and target + return !sourceNode.same(targetNode) && !sourceNode.isParent() && !targetNode.isParent(); + }, + edgeParams: function( sourceNode, targetNode ){ + // for edges between the specified source and target + // return element object to be passed to cy.add() for edge + return {}; + }, + }); + + // Enable edgehandles functionality + eh.enable(); + + let isEdgeHandlerActive = false; // Flag to track if edge handler is active + + cy.on('ehcomplete', (event, sourceNode, targetNode, addedEdge) => { + console.log(`Edge created from ${sourceNode.id()} to ${targetNode.id()}`); + console.log("Added edge:", addedEdge); + + // Reset the edge handler flag after a short delay + setTimeout(() => { + isEdgeHandlerActive = false; + }, 100); // Adjust delay as needed + + // Get the ID of the added edge + const edgeId = addedEdge.id(); // Extracts the edge ID + + // Helper function to get the next available endpoint with pattern detection + function getNextEndpoint(nodeId, isSource) { + const edges = cy.edges(`[${isSource ? 'source' : 'target'} = "${nodeId}"]`); + const e1Pattern = /^e1-(\d+)$/; + const ethPattern = /^eth(\d+)$/; + let maxEndpoint = 0; + let selectedPattern = e1Pattern; // Default to e1- pattern + + edges.forEach(edge => { + const endpoint = edge.data(isSource ? "sourceEndpoint" : "targetEndpoint"); + let match = endpoint ? endpoint.match(e1Pattern) : null; + if (match) { + // If endpoint matches e1- pattern + const endpointNum = parseInt(match[1], 10); + if (endpointNum > maxEndpoint) { + maxEndpoint = endpointNum; + } + } else { + // If endpoint doesn't match e1-, try eth pattern + match = endpoint ? endpoint.match(ethPattern) : null; + if (match) { + // Switch to eth pattern if detected + selectedPattern = ethPattern; + const endpointNum = parseInt(match[1], 10); + if (endpointNum > maxEndpoint) { + maxEndpoint = endpointNum; + } + } + } + }); + + // Increment max endpoint found and format based on selected pattern + return selectedPattern === e1Pattern + ? `e1-${maxEndpoint + 1}` + : `eth${maxEndpoint + 1}`; + } + + // Calculate next available source and target endpoints + const sourceEndpoint = getNextEndpoint(sourceNode.id(), true); + const targetEndpoint = getNextEndpoint(targetNode.id(), false); + + // Add calculated endpoints to the edge data + addedEdge.data('sourceEndpoint', sourceEndpoint); + addedEdge.data('targetEndpoint', targetEndpoint); + + // Save the edge element to file in the server + saveEdgeToFile(edgeId); + + // Save the edge element to clab editor panel + clabEditorAddEdge(sourceNode, sourceEndpoint, targetNode, targetEndpoint); + }); + + async function saveEdgeToFile(edgeId) { + const edgeData = cy.$id(edgeId).json(); // Get JSON data of the edge with the specified ID + const endpointName = '/clab-save-topo-cyto-json'; + + try { + // Send the enhanced edge data directly without wrapping it in an object + const response = await sendRequestToEndpointPost(endpointName, [edgeData]); + console.log('Edge data saved successfully', response); + } catch (error) { + console.error('Failed to save edge data:', error); + } + } + + + + // cy.on('remove', 'edge', () => { + // saveEdgeToFile(); + // }); + + + + loadCytoStyle(); + + function loadCytoStyle() { + + // detect light or dark mode + const colorScheme = detectColorScheme(); + console.log('The user prefers:', colorScheme); + + //- Load and apply Cytoscape styles from cy-style.json using fetch + if (colorScheme == "light") { + fetch("css/cy-style.json") + .then((response) => response.json()) + .then((styles) => { + cy.style().fromJson(styles).update(); + }) + .catch((error) => { + console.error( + "Oops, we hit a snag! Couldnt load the cyto styles, bro.", + error, + ); + appendMessage( + `Oops, we hit a snag! Couldnt load the cyto styles, bro.: ${error}`, + ); + }); + } else if (colorScheme == "dark") { + fetch("css/cy-style-dark.json") + .then((response) => response.json()) + .then((styles) => { + cy.style().fromJson(styles).update(); + }) + .catch((error) => { + console.error( + "Oops, we hit a snag! Couldnt load the cyto styles, bro.", + error, + ); + appendMessage( + `Oops, we hit a snag! Couldnt load the cyto styles, bro.: ${error}`, + ); + }); + } + } + + // Enable grid guide extension + cy.gridGuide({ + // On/Off Modules + + snapToGridOnRelease: true, + snapToGridDuringDrag: false, + snapToAlignmentLocationOnRelease: true, + snapToAlignmentLocationDuringDrag: false, + distributionGuidelines: false, + geometricGuideline: false, + initPosAlignment: false, + centerToEdgeAlignment: false, + resize: false, + parentPadding: false, + drawGrid: false, + + // General + gridSpacing: 10, + snapToGridCenter: true, + + // Draw Grid + zoomDash: true, + panGrid: true, + gridStackOrder: -1, + gridColor: '#dedede', + lineWidth: 1.0, + + // Guidelines + guidelinesStackOrder: 4, + guidelinesTolerance: 2.00, + guidelinesStyle: { + strokeStyle: "#8b7d6b", + geometricGuidelineRange: 400, + range: 100, + minDistRange: 10, + distGuidelineOffset: 10, + horizontalDistColor: "#ff0000", + verticalDistColor: "#00ff00", + initPosAlignmentColor: "#0000ff", + lineDash: [0, 0], + horizontalDistLine: [0, 0], + verticalDistLine: [0, 0], + initPosAlignmentLine: [0, 0], + }, + + // Parent Padding + parentSpacing: -1 + }); + + // Fetch and load element data from a JSON file + // fetch("dataCytoMarshall-" + labName + ".json") + fetch("dataCytoMarshall.json") + + .then((response) => response.json()) + .then((elements) => { + // Add the elements to the Cytoscape instance + // Add the elements to the Cytoscape instance + cy.add(elements); + //- run layout + //- run layout + const layout = cy.layout({ + name: "cola", + nodeGap: 5, + edgeLength: 100, + animate: true, + randomize: false, + maxSimulationTime: 1500, + }); + layout.run(); + + // remove node topoviewer + topoViewerNode = cy.filter('node[name = "topoviewer"]'); + topoViewerNode.remove(); + }) + .catch((error) => { + console.error("Error loading graph data:", error); + }); + // Instantiate hover text element + const hoverText = document.createElement("box"); + hoverText.classList.add( + "hover-text", + "is-hidden", + "box", + "has-text-weight-normal", + "is-warning", + "is-smallest", + ); + hoverText.textContent = "Launch CloudShell."; + document.body.appendChild(hoverText); + + + + let shiftKeyDown = false; + + // Detect when Shift is pressed or released + document.addEventListener('keydown', (event) => { + if (event.key === 'Shift') { + shiftKeyDown = true; + } + }); + + document.addEventListener('keyup', (event) => { + if (event.key === 'Shift') { + shiftKeyDown = false; + } + }); + + + //- Toggle the Panel(s) when clicking on the cy container + //- Toggle the Panel(s) when clicking on the cy container + document.getElementById("cy").addEventListener("click", function(event) { + + console.log("cy container clicked"); + + console.log("isPanel01Cy: ", isPanel01Cy); + console.log("nodeClicked: ", nodeClicked); + console.log("edgeClicked: ", edgeClicked); + + + //- This code will be executed when you click anywhere in the Cytoscape container + //- You can add logic specific to the container here + //- This code will be executed when you click anywhere in the Cytoscape container + //- You can add logic specific to the container here + + loadCytoStyle(); + + if (!nodeClicked && !edgeClicked) { + + console.log("!nodeClicked -- !edgeClicked"); + + // if (!isPanel01Cy) { + + console.log("!isPanel01Cy: "); + + // Remove all Overlayed Panel + // Get all elements with the class "panel-overlay" + var panelOverlays = document.getElementsByClassName("panel-overlay"); + + console.log("panelOverlays: ", panelOverlays); + + // Loop through each element and set its display to 'none' + for (var i = 0; i < panelOverlays.length; i++) { + console.log + panelOverlays[i].style.display = "none"; + } + + var viewportDrawer = document.getElementsByClassName("viewport-drawer"); + // Loop through each element and set its display to 'none' + for (var i = 0; i < viewportDrawer.length; i++) { + viewportDrawer[i].style.display = "none"; + } + + // display none each ViewPortDrawer Element, the ViewPortDrawer is created during DOM loading and styled as display node initially + var ViewPortDrawerElements = + document.getElementsByClassName("ViewPortDrawer"); + var ViewPortDrawerArray = Array.from(ViewPortDrawerElements); + ViewPortDrawerArray.forEach(function(element) { + element.style.display = "none"; + }); + + // } else { + // removeElementById("Panel-01"); + // appendMessage(`"try to remove panel01-Cy"`); + } + + nodeClicked = false; + edgeClicked = false; + + appendMessage(`"isPanel01Cy-cy: " ${isPanel01Cy}`); + appendMessage(`"nodeClicked: " ${nodeClicked}`); + // } + + }); + + // Listen for tap or click on the Cytoscape canvas + cy.on('click', async (event) => { + if (event.target === cy && shiftKeyDown) { // Ensures Shift + click/tap + + const pos = event.position; + const newNodeId = 'nodeId-' + (cy.nodes().length + 1); + + // Add the new node to the graph + cy.add({ + group: 'nodes', + data: + { + "id": newNodeId, + "editor": "true", + "weight": "30", + "name": newNodeId, + "parent": "", + "topoViewerRole": "pe", + "sourceEndpoint": "", + "targetEndpoint": "", + "containerDockerExtraAttribute": { + "state": "", + "status": "", + }, + "extraData": { + "kind": "container", + "longname": "", + "image": "", + "mgmtIpv4Addresss": "", + }, + }, + position: { x: pos.x, y: pos.y } + }); + + var cyNode = cy.$id(newNodeId); // Get cytoscpe node object id + + showPanelContainerlabEditor(event) + sleep (100) + showPanelNodeEditor(cyNode) + sleep (100) + saveNodeToEditorToFile() + + } + }); + + + + // Click event listener for nodes + // Click event listener for nodes + cy.on("click", "node", function(event) { + console.log("isEdgeHandlerActive after node click: ", isEdgeHandlerActive); + + // Ignore the click event if edge handler is active + if (isEdgeHandlerActive) { + return; + } + + const node = event.target; + nodeClicked = true; + + if (!node.isParent()) { + // if (event.originalEvent.shiftKey && (document.getElementById("panel-clab-editor").style.display != "none")) { // Start edge creation on Shift + Click and the clab editor panel is open + if (event.originalEvent.shiftKey) { // Start edge creation on Shift + + console.log("Shift + Click"); + console.log("edgeHandler Node: ", node.data("extraData").longname); + + // Set the edge handler flag + isEdgeHandlerActive = true; + + // Start the edge handler from the clicked node + eh.start(node); + + + } else { + + if (node.data("editor") === "true") { + console.log("Node is an editor node"); + + showPanelNodeEditor(node) + + + } else { + + // Remove all Overlayed Panel + const panelOverlays = document.getElementsByClassName("panel-overlay"); + for (let i = 0; i < panelOverlays.length; i++) { + panelOverlays[i].style.display = "none"; + } + + + console.log(node); + console.log(node.data("containerDockerExtraAttribute").status); + console.log(node.data("extraData")); + + if (document.getElementById("panel-node").style.display === "none") { + document.getElementById("panel-node").style.display = "block"; + } else { + document.getElementById("panel-node").style.display = "none"; + } + + document.getElementById("panel-node-name").textContent = node.data("extraData").longname; + document.getElementById("panel-node-status").textContent = node.data("containerDockerExtraAttribute").status; + document.getElementById("panel-node-kind").textContent = node.data("extraData").kind; + document.getElementById("panel-node-image").textContent = node.data("extraData").image; + document.getElementById("panel-node-mgmtipv4").textContent = node.data("extraData").mgmtIpv4Addresss; + document.getElementById("panel-node-mgmtipv6").textContent = node.data("extraData").mgmtIpv6Address; + document.getElementById("panel-node-fqdn").textContent = node.data("extraData").fqdn; + document.getElementById("panel-node-group").textContent = node.data("extraData").group; + document.getElementById("panel-node-topoviewerrole").textContent = node.data("topoViewerRole"); + + // Set selected node-long-name to global variable + globalSelectedNode = node.data("extraData").longname; + console.log("internal: ", globalSelectedNode); + + appendMessage(`"isPanel01Cy-cy: " ${isPanel01Cy}`); + appendMessage(`"nodeClicked: " ${nodeClicked}`); + } + } + } + }); + + // Click event listener for edges + // Click event listener for edges + cy.on("click", "edge", async function(event) { + + // Remove all Overlayed Panel + // Get all elements with the class "panel-overlay" + var panelOverlays = document.getElementsByClassName("panel-overlay"); + // Loop through each element and set its display to 'none' + for (var i = 0; i < panelOverlays.length; i++) { + panelOverlays[i].style.display = "none"; + } + + // This code will be executed when you click on a node + // You can add logic specific to nodes here + const clickedEdge = event.target; + const defaultEdgeColor = "#969799"; + edgeClicked = true; + + console.log(defaultEdgeColor); + + // Change the color of the clicked edge (for example, to red) + clickedEdge.style("line-color", "#0043BF"); + + // Revert the color of other edges that were not clicked (e.g., back to their default color) + cy.edges().forEach(function(edge) { + if (edge !== clickedEdge) { + edge.style("line-color", defaultEdgeColor); + } + }); + + document.getElementById("panel-link").style.display = "none"; + + if (document.getElementById("panel-link").style.display === "none") { + document.getElementById("panel-link").style.display = "block"; + } else { + document.getElementById("panel-link").style.display = "none"; + } + + document.getElementById("panel-link-name").textContent = `${clickedEdge.data("source")} --- ${clickedEdge.data("target")}` + + document.getElementById("panel-link-endpoint-a-name").textContent = `${clickedEdge.data("source")}` + // document.getElementById("panel-link-endpoint-a-mac-address").textContent = `${clickedEdge.data("extraData").clabSourceMacAddress}` + document.getElementById("panel-link-endpoint-a-mac-address").textContent = "getting the MAC address" + + document.getElementById("panel-link-endpoint-b-name").textContent = `${clickedEdge.data("target")}` + // document.getElementById("panel-link-endpoint-b-mac-address").textContent = `${clickedEdge.data("extraData").clabTargetMacAddress}` + document.getElementById("panel-link-endpoint-b-mac-address").textContent = "getting the MAC address" + + + // setting clabSourceLinkArgsList + clabLinkMacArgsList = [`${clickedEdge.data("extraData").clabSourceLongName}`,`${clickedEdge.data("extraData").clabTargetLongName}`] + + // setting MAC address endpoint-a values by getting the data from clab via /clab-link-mac GET API + const actualLinkMacPair = await sendRequestToEndpointGetV2("/clab-link-mac", clabLinkMacArgsList) + console.log("actualLinkMacPair-Source: ", actualLinkMacPair[0].sourceIfMac) + console.log("actualLinkMacPair-Target: ", actualLinkMacPair[0].targetIfMac) + + document.getElementById("panel-link-endpoint-a-mac-address").textContent = actualLinkMacPair[0].sourceIfMac + document.getElementById("panel-link-endpoint-b-mac-address").textContent = actualLinkMacPair[0].targetIfMac + + + + // setting default impairment endpoint-a values by getting the data from clab via /clab-link-impairment GET API + clabSourceLinkArgsList = [`${clickedEdge.data("extraData").clabSourceLongName}`,`${clickedEdge.data("extraData").clabSourcePort}`] + clabSourceLinkImpairmentClabData = await sendRequestToEndpointGetV2("/clab-link-impairment", clabSourceLinkArgsList) + + if (clabSourceLinkImpairmentClabData && typeof clabSourceLinkImpairmentClabData === 'object' && Object.keys(clabSourceLinkImpairmentClabData).length > 0) { + hideLoadingSpinnerGlobal(); + console.log("Valid non-empty JSON response received:", clabSourceLinkImpairmentClabData); + console.log("Valid non-empty JSON response received: clabSourceLinkImpairmentClabData returnd data", clabSourceLinkImpairmentClabData["return data"]["delay"]); + + if (clabSourceLinkImpairmentClabData["return data"]["delay"] == "N/A") { + document.getElementById("panel-link-endpoint-a-delay").value = '0' + }else { + document.getElementById("panel-link-endpoint-a-delay").value = clabSourceLinkImpairmentClabData["return data"]["delay"].replace(/ms$/, ''); + } + + if (clabSourceLinkImpairmentClabData["return data"]["jitter"] == "N/A") { + document.getElementById("panel-link-endpoint-a-jitter").value = '0' + }else { + document.getElementById("panel-link-endpoint-a-jitter").value = clabSourceLinkImpairmentClabData["return data"]["jitter"].replace(/ms$/, ''); + } + + if (clabSourceLinkImpairmentClabData["return data"]["rate"] == "N/A") { + document.getElementById("panel-link-endpoint-a-rate").value = '0' + }else { + document.getElementById("panel-link-endpoint-a-rate").value = clabSourceLinkImpairmentClabData["return data"]["rate"] + } + + if (clabSourceLinkImpairmentClabData["return data"]["packet_loss"] == "N/A") { + document.getElementById("panel-link-endpoint-a-loss").value = '0' + }else { + document.getElementById("panel-link-endpoint-a-loss").value = clabSourceLinkImpairmentClabData["return data"]["packet_loss"].replace(/%$/, ''); + } + + + } else { + console.log("Empty or invalid JSON response received"); + } + + + + + // setting default impairment endpoint-b values by getting the data from clab via /clab-link-impairment GET API + clabTargetLinkArgsList = [`${clickedEdge.data("extraData").clabTargetLongName}`,`${clickedEdge.data("extraData").clabTargetPort}`] + clabTargetLinkImpairmentClabData = await sendRequestToEndpointGetV2("/clab-link-impairment", clabTargetLinkArgsList) + + if (clabTargetLinkImpairmentClabData && typeof clabTargetLinkImpairmentClabData === 'object' && Object.keys(clabTargetLinkImpairmentClabData).length > 0) { + hideLoadingSpinnerGlobal(); + console.log("Valid non-empty JSON response received:", clabTargetLinkImpairmentClabData); + console.log("Valid non-empty JSON response received: clabTargetLinkImpairmentClabData returnd data", clabTargetLinkImpairmentClabData["return data"]["delay"]); + + if (clabTargetLinkImpairmentClabData["return data"]["delay"] == "N/A") { + document.getElementById("panel-link-endpoint-b-delay").value = '0' + }else { + document.getElementById("panel-link-endpoint-b-delay").value = clabTargetLinkImpairmentClabData["return data"]["delay"].replace(/ms$/, ''); + } + + if (clabTargetLinkImpairmentClabData["return data"]["jitter"] == "N/A") { + document.getElementById("panel-link-endpoint-b-jitter").value = '0' + }else { + document.getElementById("panel-link-endpoint-b-jitter").value = clabTargetLinkImpairmentClabData["return data"]["jitter"].replace(/ms$/, ''); + } + + if (clabTargetLinkImpairmentClabData["return data"]["rate"] == "N/A") { + document.getElementById("panel-link-endpoint-b-rate").value = '0' + }else { + document.getElementById("panel-link-endpoint-b-rate").value = clabTargetLinkImpairmentClabData["return data"]["rate"] + } + + if (clabTargetLinkImpairmentClabData["return data"]["packet_loss"] == "N/A") { + document.getElementById("panel-link-endpoint-b-loss").value = '0' + }else { + document.getElementById("panel-link-endpoint-b-loss").value = clabTargetLinkImpairmentClabData["return data"]["packet_loss"].replace(/%$/, ''); + } + + + } else { + console.log("Empty or invalid JSON response received"); + } + + + + + // set selected edge-id to global variable + globalSelectedEdge = clickedEdge.data("id") + + appendMessage(`"edgeClicked: " ${edgeClicked}`); + }); + + + + + function generateNodesEvent(event) { + // Your event handling logic here + //- Add a click event listener to the 'Generate' button + //- Get the number of node from the input field + // Your event handling logic here + //- Add a click event listener to the 'Generate' button + //- Get the number of node from the input field + console.log("generateNodesButton clicked"); + const numNodes = document.getElementById("generateNodesInput").value; + console.log(numNodes); + //- Check if the number of node is empty + //- Check if the number of node is empty + if (numNodes === null) { + //- if node number empty do nothing + //- if node number empty do nothing + return; + } + const numNodesToGenerate = parseInt(numNodes, 10); + //- Check if the number of node is positive + //- Check if the number of node is positive + if (isNaN(numNodesToGenerate) || numNodesToGenerate <= 0) { + //- Invalid input + //- Invalid input + appendMessage( + "Error:" + "Bro, you gotta enter a valid positive number, come on!", + ); + return; + } + //- Generate nodes with random positions + //- Generate nodes with random positions + for (let i = 0; i < numNodesToGenerate; i++) { + const nodeName = `node-${i + 1}`; + const newNode = { + group: "nodes", + data: { + id: nodeName, + name: nodeName, + }, + position: { + x: Math.random() * 400, + y: Math.random() * 400, + }, + }; + //-cy.add(newNode); + //-cy.add(newNode); + try { + cy.add(newNode); + //- throw new Error('This is an example exception'); + //- throw new Error('This is an example exception'); + } catch (error) { + //- Log the exception to the console + //- Log the exception to the console + console.error("An exception occurred:", error); + //- Log the exception to notification message to the textarea + //- Log the exception to notification message to the textarea + appendMessage("An exception occurred:" + error); + } + } + //- Generate random edges between nodes + //- Generate random edges between nodes + for (let i = 0; i < numNodesToGenerate; i++) { + const sourceNode = `node-${i + 1}`; + const targetNode = `node-${Math.floor(Math.random() * numNodesToGenerate) + 1}`; + if (sourceNode !== targetNode) { + const newEdge = { + group: "edges", + data: { + id: "from-" + sourceNode + "-to-" + targetNode, + name: "from-" + sourceNode + "-to-" + targetNode, + source: sourceNode, + target: targetNode, + }, + }; + try { + cy.add(newEdge); + //- throw new Error('This is an example exception'); + //- throw new Error('This is an example exception'); + } catch (error) { + //- Log the exception to the console + //- Log the exception to the console + console.error("An exception occurred:", error); + //- Log the exception to notification message to the textarea + //- Log the exception to notification message to the textarea + appendMessage("An exception occurred::" + error); + } + } + } + //- run layout + //- run layout + const layout = cy.layout({ + name: "cola", + nodeGap: 5, + edgeLengthVal: 45, + animate: true, + randomize: false, + maxSimulationTime: 1500, + }); + layout.run(); + //-//- Append a notification message to the textarea + //-//- Append a notification message to the textarea + console.log( + "Info: " + + `Boom! Just generated ${numNodesToGenerate} nodes with some random edges. That's how we roll!`, + ); + appendMessage( + "Info: " + + `Boom! Just generated ${numNodesToGenerate} nodes with some random edges. That's how we roll!`, + ); + } + + function spawnNodeEvent(event) { + //- Add a click event listener to the 'Submit' button in the hidden form + //- Get the node name from the input field + //- Add a click event listener to the 'Submit' button in the hidden form + //- Get the node name from the input field + const nodeName = document.getElementById("nodeName").value; + console.log(nodeName); + //- Check if a node name is empty + //- Check if a node name is empty + if (nodeName == "") { + //- append message in textArea + //- append message in textArea + appendMessage("Error: Enter node name."); + return; + } + //- Check if a node with the same name already exists + //- Check if a node with the same name already exists + if (cy.$(`node[id = "${nodeName}"]`).length > 0) { + //- append message in textArea + //- append message in textArea + appendMessage("Error: Node with this name already exists."); + return; + } + //- Create a new node element + //- Create a new node element + const newNode = { + group: "nodes", + data: { + id: nodeName, + name: nodeName, + label: nodeName, + }, + }; + //- Add the new node to Cytoscape.js + //- Add the new node to Cytoscape.js + cy.add(newNode); + //- Randomize the positions and center the graph + //- Randomize the positions and center the graph + const layout = cy.layout({ + name: "cola", + nodeGap: 5, + edgeLengthVal: 45, + animate: true, + randomize: false, + maxSimulationTime: 1500, + }); + layout.run(); + //- Append a notification message to the textarea + //- Append a notification message to the textarea + console.log("Info: " + `Nice! Node "${nodeName}" added successfully.`); + appendMessage("Info: " + `Nice! Node "${nodeName}" added successfully.`); + } + + function nodeFindEvent(event) { + //- Get a reference to your Cytoscape instance (assuming it's named 'cy') + //- const cy = window.cy; //- Replace 'window.cy' with your actual Cytoscape instance + //- Find the node with the specified name + //- Get a reference to your Cytoscape instance (assuming it's named 'cy') + //- const cy = window.cy; //- Replace 'window.cy' with your actual Cytoscape instance + //- Find the node with the specified name + const nodeName = document.getElementById("nodeFindInput").value; + const node = cy.$(`node[name = "${nodeName}"]`); + //- Check if the node exists + //- Check if the node exists + if (node.length > 0) { + // console + // console + console.log("Info: " + 'Sweet! Node "' + nodeName + '" is in the house.'); + appendMessage("Info: " + 'Sweet! Node "' + nodeName + '" is in the house.'); + //- Apply a highlight style to the node + //- Apply a highlight style to the node + node.style({ + "border-color": "red", + "border-width": "2px", + "background-color": "yellow", + }); + //- Zoom out on the node + //- Zoom out on the node + cy.fit(); + //- Zoom in on the node + //- Zoom in on the node + cy.animate({ + zoom: { + level: 5, + position: { + x: node.position("x"), + y: node.position("y"), + }, + renderedPosition: { + x: node.renderedPosition("x"), + y: node.renderedPosition("y"), + }, + }, + duration: 1500, + }); + } else { + console.error( + `Bro, I couldn't find a node named "${nodeName}". Try another one.`, + ); + appendMessage( + `Bro, I couldn't find a node named "${nodeName}". Try another one.`, + ); + } + } + + function zoomToFitDrawer() { + const initialZoom = cy.zoom(); + appendMessage(`Bro, initial zoom level is "${initialZoom}".`); + //- Fit all nodes possible with padding + //- Fit all nodes possible with padding + cy.fit(); + const currentZoom = cy.zoom(); + appendMessage(`And now the zoom level is "${currentZoom}".`); + } + + function pathFinderDijkstraEvent(event) { + // Usage example: + // highlightShortestPath('node-a', 'node-b'); // Replace with your source and target node IDs + //- Function to get the default node style from cy-style.json + //- weight: (edge) => 1, // You can adjust the weight function if needed + //- weight: (edge) => edge.data('distance') + // Usage example: + // highlightShortestPath('node-a', 'node-b'); // Replace with your source and target node IDs + //- Function to get the default node style from cy-style.json + //- weight: (edge) => 1, // You can adjust the weight function if needed + //- weight: (edge) => edge.data('distance') + + console.log("im triggered"); + + // Remove existing highlight from all edges + // Remove existing highlight from all edges + cy.edges().forEach((edge) => { + edge.removeClass("spf"); + }); + + // Get the node sourceNodeId from pathFinderSourceNodeInput and targetNodeId from pathFinderTargetNodeInput + // Get the node sourceNodeId from pathFinderSourceNodeInput and targetNodeId from pathFinderTargetNodeInput + const sourceNodeId = document.getElementById( + "pathFinderSourceNodeInput", + ).value; + const targetNodeId = document.getElementById( + "pathFinderTargetNodeInput", + ).value; + + // Assuming you have 'cy' as your Cytoscape instance + // Assuming you have 'cy' as your Cytoscape instance + const sourceNode = cy.$(`node[id="${sourceNodeId}"]`); + const targetNode = cy.$(`node[id="${targetNodeId}"]`); + + console.log( + "Info: " + + "Let's find the path from-" + + sourceNodeId + + "-to-" + + targetNodeId + + "!", + ); + appendMessage( + "Info: " + + "Let's find the path from-" + + sourceNodeId + + "-to-" + + targetNodeId + + "!", + ); + + // Check if both nodes exist + // Check if both nodes exist + if (sourceNode.length === 0 || targetNode.length === 0) { + console.error( + `Bro, couldn't find the source or target node you specified. Double-check the node names.`, + ); + appendMessage( + `Bro, couldn't find the source or target node you specified. Double-check the node names.`, + ); + return; + } + + // Get the Dijkstra result with the shortest path + // Get the Dijkstra result with the shortest path + const dijkstraResult = cy.elements().dijkstra({ + root: sourceNode, + weight: (edge) => 1, + // Use the custom weight attribute + // weight: edge => edge.data('customWeight'), + // Use the custom weight attribute + // weight: edge => edge.data('customWeight'), + }); + // Get the shortest path from Dijkstra result + // Get the shortest path from Dijkstra result + const shortestPathEdges = dijkstraResult.pathTo(targetNode); + console.log(shortestPathEdges); + + // Check if there is a valid path (shortestPathEdges is not empty) + // Check if there is a valid path (shortestPathEdges is not empty) + if (shortestPathEdges.length > 1) { + //// Apply a style to highlight the shortest path edges + // shortestPathEdges.style({ + // 'line-color': 'red', + // 'line-style': 'solid', + // }); + //// Apply a style to highlight the shortest path edges + // shortestPathEdges.style({ + // 'line-color': 'red', + // 'line-style': 'solid', + // }); + + // Highlight the shortest path + // Highlight the shortest path + shortestPathEdges.forEach((edge) => { + edge.addClass("spf"); + }); + + //- Zoom out on the node + //- Zoom out on the node + cy.fit(); + + //- Zoom in on the node + //- Zoom in on the node + cy.animate({ + zoom: { + level: 5, + position: { + x: sourceNode.position("x"), + y: sourceNode.position("y"), + }, + renderedPosition: { + x: sourceNode.renderedPosition("x"), + y: sourceNode.renderedPosition("y"), + }, + }, + duration: 1500, + }); + // throw log + // throw log + console.log( + "Info: " + + "Yo, check it out! Shorthest Path from-" + + sourceNodeId + + "-to-" + + targetNodeId + + " has been found.", + ); + appendMessage( + "Info: " + + "Yo, check it out! Shorthest Path from-" + + sourceNodeId + + "-to-" + + targetNodeId + + " has been found, below is the path trace..", + ); + console.log(shortestPathEdges); + + shortestPathEdges.forEach((edge) => { + console.log("Edge ID:", edge.id()); + console.log("Source Node ID:", edge.source().id()); + console.log("Target Node ID:", edge.target().id()); + + edgeId = edge.id(); + sourceNodeId = edge.source().id(); + targetNodeId = edge.target().id(); + // You can access other properties of the edge, e.g., source, target, data, etc. + // You can access other properties of the edge, e.g., source, target, data, etc. + + appendMessage("Info: " + "Edge ID: " + edgeId); + appendMessage("Info: " + "Source Node ID: " + sourceNodeId); + appendMessage("Info: " + "Target Node ID: " + targetNodeId); + }); + } else { + console.error( + `Bro, there is no path from "${sourceNodeId}" to "${targetNodeId}".`, + ); + appendMessage( + `Bro, there is no path from "${sourceNodeId}" to "${targetNodeId}".`, + ); + return; + } + } + + function setNodeContainerStatus(containerNodeName, containerNodeStatus) { + cy.nodes().forEach(function(node) { + var nodeId = node.data("id"); + + // Find the corresponding status nodes based on node ID + // Find the corresponding status nodes based on node ID + var statusGreenNode = cy.$(`node[name="${nodeId}-statusGreen"]`); + var statusOrangeNode = cy.$(`node[name="${nodeId}-statusOrange"]`); + var statusRedNode = cy.$(`node[name="${nodeId}-statusRed"]`); + + if (statusGreenNode.length === 0 || statusRedNode.length === 0) { + // If status nodes are not found, skip this node + return; + } + + // Update positions of status nodes relative to the node + var nodePosition = node.position(); + var offset = { + x: -4, + y: -10 + }; + var statusGreenNodePosition = { + x: nodePosition.x + offset.x, + y: nodePosition.y + offset.y, + }; + var statusRedNodePosition = { + x: nodePosition.x + offset.x, + y: nodePosition.y + offset.y, + }; + + // Check if the nodeContainerStatusVisibility is true + if (nodeContainerStatusVisibility) { + // Check if the containerNodeName includes nodeId and containerNodeStatus includes 'healthy' + if ( + containerNodeName.includes(nodeId) && + (containerNodeStatus.includes("Up") || + containerNodeStatus.includes("healthy")) + ) { + statusGreenNode.show(); + statusRedNode.hide(); + console.log( + "nodeContainerStatusVisibility: " + nodeContainerStatusVisibility, + ); + } else if ( + containerNodeName.includes(nodeId) && + containerNodeStatus.includes("(health: starting)") + ) { + statusGreenNode.hide(); + statusOrangeNode.show(); + } else if ( + containerNodeName.includes(nodeId) && + containerNodeStatus.includes("Exited") + ) { + statusGreenNode.hide(); + statusRedNode.show(); + } + } else { + statusGreenNode.hide(); + statusRedNode.hide(); + } + + statusGreenNode.position(statusGreenNodePosition); + statusRedNode.position(statusRedNodePosition); + }); + } + + function setNodeDataWithContainerAttribute(containerNodeName, status, state, IPAddress, GlobalIPv6Address) { + cy.nodes().forEach(function(node) { + var nodeId = node.data("id"); + if (containerNodeName.includes(nodeId)) { + var containerDockerExtraAttributeData = { + state: state, + status: status, + }; + + node.data( + "containerDockerExtraAttribute", + containerDockerExtraAttributeData, + ); + node.data("extraData").mgmtIpv4Addresss = IPAddress; + node.data("extraData").mgmtIpv6Address = GlobalIPv6Address; + + } + }); + } + + // + // End of JS Functions Event Handling section + // End of JS Functions Event Handling section + // + + // + // Start of JS Generic Functions + // Start of JS Generic Functions + // + // + // Start of JS Generic Functions + // Start of JS Generic Functions + // + + + //- Function to get the default node style from cy-style.json + //- Function to get the default node style from cy-style.json + async function getDefaultNodeStyle(node) { + try { + //- Fetch the cy-style.json file + //- Fetch the cy-style.json file + const response = await fetch("cy-style.json"); + //- Check if the response is successful (status code 200) + //- Check if the response is successful (status code 200) + if (!response.ok) { + throw new Error( + `Failed to fetch cy-style.json (${response.status} ${response.statusText})`, + ); + } + //- Parse the JSON response + //- Parse the JSON response + const styleData = await response.json(); + //- Extract the default node style from the loaded JSON + //- Adjust this based on your JSON structure + //- Extract the default node style from the loaded JSON + //- Adjust this based on your JSON structure + const defaultNodeStyle = styleData[0].style; + return defaultNodeStyle; + } catch (error) { + console.error("Error loading cy-style.json:", error); + appendMessage(`Error loading cy-style.json: ${error}`); + //- Return a default style in case of an error + //- Return a default style in case of an error + return { + "background-color": "blue", + "border-color": "gray", + "border-width": "1px", + }; + } + } + + ///-logMessagesPanel Function to add a click event listener to the copy button + ///-logMessagesPanel Function to add a click event listener to the copy button + const copyButton = document.getElementById("copyToClipboardButton"); + copyButton.className = "button is-smallest-element"; + copyButton.addEventListener("click", copyToClipboard); + + /// logMessagesPanel Function to copy textarea content to clipboard + /// logMessagesPanel Function to copy textarea content to clipboard + function copyToClipboard() { + const textarea = document.getElementById("notificationTextarea"); + textarea.select(); + document.execCommand("copy"); + } + + + + // function closePanelEvent(event, panel) { + // panel.style.display = "block"; + // console.log(panel.style.display); + // panel.style.display = "none"; + // } + + function createModal(modalId, modalContent) { + // Create the modal + // Create the modal + const htmlContent = ` + + `; + + const modalDiv = document.createElement("div"); + modalDiv.innerHTML = htmlContent; + modalDiv.id = "modalDivExportViewport"; + + document.body.appendChild(modalDiv); + const modalBackground = document.getElementById( + `${modalId}-modalBackgroundId`, + ); + + modalBackground.addEventListener("click", function() { + const modal = modalBackground.parentNode; + modal.classList.remove("is-active"); + }); + } + + function showModalCaptureViewport(modalId) { + const modalContentSaveViewport = ` + + `; + + // Instantiate modal + // Instantiate modal + createModal("modalSaveViewport", modalContentSaveViewport); + + // create event listener + // create event listener + const performActionButton = document.getElementById("performActionButton"); + performActionButton.addEventListener("click", function() { + const checkboxName = "checkboxSaveViewPort"; + const checkboxes = document.querySelectorAll( + `input[type="checkbox"][name="${checkboxName}"]`, + ); + const selectedOptions = []; + + checkboxes.forEach(function(checkbox) { + if (checkbox.checked) { + selectedOptions.push(checkbox.value); + } + }); + + if (selectedOptions.length === 0) { + bulmaToast.toast({ + message: `Hey there, please pick at least one option.😊👌`, + type: "is-warning is-size-6 p-3", + duration: 4000, + position: "top-center", + closeOnClick: true, + }); + } else { + // Perform your action based on the selected options + // Perform your action based on the selected options + if (selectedOptions.join(", ") == "option01") { + captureAndSaveViewportAsPng(cy); + modal.classList.remove("is-active"); + } else if (selectedOptions.join(", ") == "option02") { + captureAndSaveViewportAsDrawIo(cy); + modal.classList.remove("is-active"); + } else if (selectedOptions.join(", ") == "option01, option02") { + captureAndSaveViewportAsPng(cy); + sleep(5000); + captureAndSaveViewportAsDrawIo(cy); + modal.classList.remove("is-active"); + } + } + }); + + // show modal + // show modal + modal = document.getElementById(modalId); + modal.classList.add("is-active"); + } + + // + // End of JS Generic Functions section + // End of JS Generic Functions section + // + // + // End of JS Generic Functions section + // End of JS Generic Functions section + // +}); + +// aarafat-tag: +//// REFACTOR START +//// to-do: +//// - re-create about-panel +//// - re-create log-messages +//// - re-create viewport + +async function initEnv() { + environments = await getEnvironments(); + labName = await environments["clab-name"] + deploymentType = await environments["deployment-type"] + + console.log("Lab-Name: ", labName) + console.log("DeploymentType: ", deploymentType) + return environments, labName + } + +async function changeTitle() { + environments = await getEnvironments(); + labName = await environments["clab-name"] + + console.log("changeTitle() - labName: ", labName) + document.title = `TopoViewer::${labName}`; +} + +async function sshWebBased(event) { + console.log("sshWebBased: ", globalSelectedNode) + var routerName = globalSelectedNode + try { + environments = await getEnvironments(event); + console.log("sshWebBased - environments: ", environments) + cytoTopologyJson = environments["EnvCyTopoJsonBytes"] + routerData = findCytoElementByLongname(cytoTopologyJson, routerName) + + console.log("sshWebBased: ", `${globalShellUrl}?RouterID=${routerData["data"]["extraData"]["mgmtIpv4Addresss"]}?RouterName=${routerName}`) + + window.open(`${globalShellUrl}?RouterID=${routerData["data"]["extraData"]["mgmtIpv4Addresss"]}?RouterName=${routerName}`); + + } catch (error) { + console.error('Error executing restore configuration:', error); + } +} + +async function sshCliCommandCopy(event) { + console.log("sshWebBased: ", globalSelectedNode) + var routerName = globalSelectedNode + try { + environments = await getEnvironments(event); + console.log("sshWebBased - environments: ", environments) + + cytoTopologyJson = environments["EnvCyTopoJsonBytes"] + clabServerAddress = environments["clab-server-address"] + routerData = findCytoElementByLongname(cytoTopologyJson, routerName) + clabUser = routerData["data"]["extraData"]["clabServerUsername"] + + sshCopyString = `ssh -t ${clabUser}@${clabServerAddress} "ssh admin@${routerName}"` + + // Check if the clipboard API is available + if (navigator.clipboard && navigator.clipboard.writeText) { + navigator.clipboard.writeText(sshCopyString).then(function() { + alert('Text copied to clipboard'); + }).catch(function(error) { + console.error('Could not copy text: ', error); + }); + } else { + // Fallback method for older browsers + let textArea = document.createElement('textarea'); + textArea.value = sshCopyString; + document.body.appendChild(textArea); + textArea.focus(); + textArea.select(); + try { + document.execCommand('copy'); + alert('Text copied to clipboard'); + } catch (err) { + console.error('Fallback: Oops, unable to copy', err); + } + document.body.removeChild(textArea); + } + + } catch (error) { + console.error('Error executing restore configuration:', error); + } +} + + + +async function linkImpairmentClab(event, impairDirection) { + console.log("linkImpairmentClab - globalSelectedEdge: ", globalSelectedEdge) + var edgeId = globalSelectedEdge + try { + environments = await getEnvironments(event); + console.log("linkImpairment - environments: ", environments) + + var deploymentType = environments["deployment-type"] + var command + + cytoTopologyJson = environments["EnvCyTopoJsonBytes"] + edgeData = findCytoElementById(cytoTopologyJson, edgeId) + + console.log("linkImpairment- edgeData: ", edgeData) + console.log("linkImpairment- edgeSource: ", edgeData["data"]["source"]) + + clabUser = edgeData["data"]["extraData"]["clabServerUsername"] + clabServerAddress = environments["clab-server-address"] + clabSourceLongName = edgeData["data"]["extraData"]["clabSourceLongName"] + clabSourcePort = edgeData["data"]["extraData"]["clabSourcePort"] + + clabTargetLongName = edgeData["data"]["extraData"]["clabTargetLongName"] + clabTargetPort = edgeData["data"]["extraData"]["clabTargetPort"] + + if (impairDirection == "a-to-b") { + console.log("linkImpairment - impairDirection: ", impairDirection) + + delayValue = document.getElementById("panel-link-endpoint-a-delay").value + jitterValue = document.getElementById("panel-link-endpoint-a-jitter").value + rateValue = document.getElementById("panel-link-endpoint-a-rate").value + lossValue = document.getElementById("panel-link-endpoint-a-loss").value + + if (deploymentType == "container") { + command = `ssh ${clabUser}@${clabServerAddress} /usr/bin/containerlab tools netem set -n ${clabSourceLongName} -i ${clabSourcePort} --delay ${delayValue}ms --jitter ${jitterValue}ms --rate ${rateValue} --loss ${lossValue}` + } else if (deploymentType == "colocated") { + command = `/usr/bin/containerlab tools netem set -n ${clabSourceLongName} -i ${clabSourcePort} --delay ${delayValue}ms --jitter ${jitterValue}ms --rate ${rateValue} --loss ${lossValue}` + } + + console.log(`linkImpairment - deployment ${deploymentType}, command: ${command}`) + var postPayload = [] + postPayload[0] = command + await sendRequestToEndpointPost("/clab-link-impairment", postPayload) + + + } else if (impairDirection == "b-to-a") { + console.log("linkImpairment - impairDirection: ", impairDirection) + + delayValue = document.getElementById("panel-link-endpoint-b-delay").value + jitterValue = document.getElementById("panel-link-endpoint-b-jitter").value + rateValue = document.getElementById("panel-link-endpoint-b-rate").value + lossValue = document.getElementById("panel-link-endpoint-b-loss").value + + if (deploymentType == "container") { + command = `ssh ${clabUser}@${clabServerAddress} /usr/bin/containerlab tools netem set -n ${clabTargetLongName} -i ${clabTargetPort} --delay ${delayValue}ms --jitter ${jitterValue}ms --rate ${rateValue} --loss ${lossValue}` + } else if (deploymentType == "colocated") { + command = `/usr/bin/containerlab tools netem set -n ${clabTargetLongName} -i ${clabTargetPort} --delay ${delayValue}ms --jitter ${jitterValue}ms --rate ${rateValue} --loss ${lossValue}` + } + + console.log(`linkImpairment - deployment ${deploymentType}, command: ${command}`) + var postPayload = [] + postPayload[0] = command + await sendRequestToEndpointPost("/clab-link-impairment", postPayload) + } + + } catch (error) { + console.error('Error executing linkImpairment configuration:', error); + } +} + + +async function linkWireshark(event, option, endpoint) { + console.log("linkWireshark - globalSelectedEdge: ", globalSelectedEdge) + var edgeId = globalSelectedEdge + try { + environments = await getEnvironments(event); + console.log("linkWireshark - environments: ", environments) + + var deploymentType = environments["deployment-type"] + + cytoTopologyJson = environments["EnvCyTopoJsonBytes"] + edgeData = findCytoElementById(cytoTopologyJson, edgeId) + + console.log("linkWireshark- edgeData: ", edgeData) + console.log("linkWireshark- edgeSource: ", edgeData["data"]["source"]) + + clabUser = edgeData["data"]["extraData"]["clabServerUsername"] + clabServerAddress = environments["clab-server-address"] + + clabSourceLongName = edgeData["data"]["extraData"]["clabSourceLongName"] + clabSourcePort = edgeData["data"]["extraData"]["clabSourcePort"] + + clabTargetLongName = edgeData["data"]["extraData"]["clabTargetLongName"] + clabTargetPort = edgeData["data"]["extraData"]["clabTargetPort"] + + if (option == "app") { + if (endpoint == "source") { + wiresharkHref = `clab-capture://${clabUser}@${clabServerAddress}?${clabSourceLongName}?${clabSourcePort}` + console.log("linkWireshark- wiresharkHref: ", wiresharkHref) + + } else if (endpoint == "target") { + wiresharkHref = `clab-capture://${clabUser}@${clabServerAddress}?${clabTargetLongName}?${clabTargetPort}` + console.log("linkWireshark- wiresharkHref: ", wiresharkHref) + } + + window.open(wiresharkHref); + + } else if (option == "copy") { + if (endpoint == "source") { + if (deploymentType == "container") { + wiresharkSshCommand = `ssh ${clabUser}@${clabServerAddress} "sudo -S /sbin/ip netns exec ${clabSourceLongName} tcpdump -U -nni ${clabSourcePort} -w -" | wireshark -k -i -` + } else if (deploymentType == "colocated") { + wiresharkSshCommand = `ssh ${clabUser}@${clabServerAddress} "sudo -S /sbin/ip netns exec ${clabSourceLongName} tcpdump -U -nni ${clabSourcePort} -w -" | wireshark -k -i -` + } + } else if (endpoint == "target") { + if (deploymentType == "container") { + wiresharkSshCommand = `ssh ${clabUser}@${clabServerAddress} "sudo -S /sbin/ip netns exec ${clabTargetLongName} tcpdump -U -nni ${clabTargetPort} -w -" | wireshark -k -i -` + } else if (deploymentType == "colocated") { + wiresharkSshCommand = `ssh ${clabUser}@${clabServerAddress} "sudo -S /sbin/ip netns exec ${clabTargetLongName} tcpdump -U -nni ${clabTargetPort} -w -" | wireshark -k -i -` + } + } + + console.log("linkWireshark- wiresharkSShCommand: ", wiresharkSshCommand) + + // Check if the clipboard API is available + if (navigator.clipboard && navigator.clipboard.writeText) { + navigator.clipboard.writeText(wiresharkSshCommand).then(function() { + alert('Text copied to clipboard'); + }).catch(function(error) { + console.error('Could not copy text: ', error); + }); + } else { + // Fallback method for older browsers + let textArea = document.createElement('textarea'); + textArea.value = wiresharkSshCommand; + document.body.appendChild(textArea); + textArea.focus(); + textArea.select(); + try { + document.execCommand('copy'); + alert('Text copied to clipboard'); + } catch (err) { + console.error('Fallback: Oops, unable to copy', err); + } + document.body.removeChild(textArea); + } + + } + + } catch (error) { + console.error('Error executing linkImpairment configuration:', error); + } +} + +async function showPanelLogMessages(event) { + document.getElementById("panel-log-messages").style.display = "block"; +} + +///-logMessagesPanel Function to add a click event listener to the close button +document.getElementById("panel-log-messages-close-button").addEventListener("click", () => { + document.getElementById("panel-log-messages").style.display = "none"; +}); + +// CLAB EDITOR +async function showPanelContainerlabEditor(event) { + // Get the YAML content from backend + getYamlTopoContent(yamlTopoContent) + + // Get all elements with the class "panel-overlay" + var panelOverlays = document.getElementsByClassName("panel-overlay"); + // Loop through each element and set its display to 'none' + for (var i = 0; i < panelOverlays.length; i++) { + panelOverlays[i].style.display = "none"; + } + document.getElementById("panel-clab-editor").style.display = "block"; +} + +///-logMessagesPanel Function to add a click event listener to the close button +document.getElementById("panel-clab-editor-close-button").addEventListener("click", () => { + document.getElementById("panel-clab-editor").style.display = "none"; +}); + +function clabEditorLoadFile() { + const fileInput = document.getElementById('panel-clab-editor-file-input'); + const textarea = document.getElementById('panel-clab-editor-text-area'); + + // Trigger the file input's file browser dialog + fileInput.click(); + + // Listen for when the user selects a file + fileInput.onchange = function() { + if (fileInput.files.length === 0) { + return; // No file selected + } + + const file = fileInput.files[0]; + const reader = new FileReader(); + + reader.onload = function(event) { + textarea.value = event.target.result; + }; + + reader.readAsText(file); + }; +} + + + +function clabEditorAddNode(nodeId, nodeName = "Spine-01", kind ='nokia_srlinux', image = 'ghcr.io/nokia/srlinux:latest', group = 'group-01', topoViewerRole = 'dcgw') { + if (!kind || !image || !group || !topoViewerRole) { + console.error("All parameters (kind, image, group, topoViewerRole) must be provided."); + return; + } + + const textarea = document.getElementById('panel-clab-editor-text-area'); + nodeId = (`### ${nodeId}`); + + // Updated regex pattern to capture nodeName if it exists under the specified nodeId + // const existingNodeRegex = new RegExp(`###\\s*${nodeId}\\s*\\n\\s*(\\S+):`, 'm'); + const existingNodeRegex = new RegExp(`${nodeId}\\s*\\n\\s+(\\S+):`, 'm'); + + const match = textarea.value.match(existingNodeRegex); + const oldNodeName = match ? match[1] : null; + + console.log("oldNodeName: ", oldNodeName); // Debug: log oldNodeName + + // Node definition template with the new nodeName + const nodeDefinition = +`${nodeId} + ${nodeName}: + kind: ${kind} + image: ${image} + group: ${group} + labels: + topoViewer-role: ${topoViewerRole} + +`; + + // Insert or update the node definition in the "nodes" section + const nodesSectionIndex = textarea.value.search(/^\s*nodes:/m); + const nodeRegex = new RegExp(`\\s*${nodeId}\\s*\\n(\\s*.*\\n)*?\\s*topoViewer-role: .*\\n`, 'g'); + + if (nodesSectionIndex !== -1) { + const insertionIndex = textarea.value.indexOf(" links:", nodesSectionIndex); + const endOfNodesSection = insertionIndex !== -1 ? insertionIndex : textarea.value.length; + const nodesSection = textarea.value.slice(nodesSectionIndex, endOfNodesSection); + + if (nodesSection.match(nodeRegex)) { + // Replace the existing node + textarea.value = textarea.value.replace(nodeRegex, + `\n\n${nodeId}\n ${nodeName}:\n kind: ${kind}\n image: ${image}\n group: ${group}\n labels:\n topoViewer-role: ${topoViewerRole}\n`); + } else { + // Insert the new node at the end of the nodes section + textarea.value = textarea.value.slice(0, endOfNodesSection) + nodeDefinition + textarea.value.slice(endOfNodesSection); + } + } else { + // Append if "nodes" section doesn't exist + textarea.value += (textarea.value.endsWith("\n") ? "" : "\n") + nodeDefinition; + } + + // Update the links section if oldNodeName exists + if (oldNodeName && oldNodeName !== nodeName) { + // Updated regex to match oldNodeName in any position in the endpoints array + const linksRegex = new RegExp(`(endpoints:\\s*\\[\\s*".*?)(\\b${oldNodeName}\\b)(:.*?)\\]`, 'g'); + textarea.value = textarea.value.replace(linksRegex, `$1${nodeName}$3]`); + } + + // yamlTopoContent = textarea.value; +} + +async function clabEditorSaveYamlTopo() { + const textarea = document.getElementById('panel-clab-editor-text-area'); + clabTopoYamlEditorData = textarea.value; + console.log("clabTopoYamlEditorData - yamlTopoContent: ", clabTopoYamlEditorData) + + // dump clabTopoYamlEditorDatal to be persisted to clab-topo.yaml + const endpointName = '/clab-save-topo-yaml'; + + try { + // Send the enhanced node data directly without wrapping it in an object + const response = await sendRequestToEndpointPost(endpointName, [clabTopoYamlEditorData]); + console.log('Node data saved successfully', response); + } catch (error) { + console.error('Failed to save yaml topo:', error); + } + +} + + + +function clabEditorAddEdge(sourceCyNode, sourceNodeEndpoint, targetCyNode, targetNodeEndpoint) { + const textarea = document.getElementById('panel-clab-editor-text-area'); + + sourceNodeName = sourceCyNode.data("name") + targetNodeName = targetCyNode.data("name") + + + // Edge definition with dynamic endpoints array + const edgeDefinition = ` + - endpoints: ["${sourceNodeName}:${sourceNodeEndpoint}", "${targetNodeName}:${targetNodeEndpoint}"]`; + + // Locate the 'links' section and insert the edge definition at the end of it + const linksIndex = textarea.value.indexOf(" links:"); + if (linksIndex !== -1) { + // Find the end of the links section or where the next section begins + const nextSectionIndex = textarea.value.indexOf("\n", linksIndex); + const insertionIndex = nextSectionIndex !== -1 ? nextSectionIndex : textarea.value.length; + + // Insert the edge definition at the end of the links section + textarea.value = textarea.value.slice(0, insertionIndex) + edgeDefinition + textarea.value.slice(insertionIndex); + } else { + // If no 'links' section exists, append the edge definition at the end of the content + textarea.value += "\n links:" + edgeDefinition; + } +} + +// NODE EDITOR START +// NODE EDITOR START +// NODE EDITOR START + +var yamlTopoContent + +async function showPanelNodeEditor(node) { + try { + // Remove all Overlayed Panels + const panelOverlays = document.getElementsByClassName("panel-overlay"); + Array.from(panelOverlays).forEach(panel => { + panel.style.display = "none"; + }); + + console.log("showPanelNodeEditor - node ID:", node.data("id")); + + // Set the node Name in the editor + const nodeNameInput = document.getElementById("panel-node-editor-name"); + if (nodeNameInput) { + nodeNameInput.value = node.data("id"); //defaulted by node id + } + + // Set the node Id in the editor + const nodeIdLabel = document.getElementById("panel-node-editor-id"); + if (nodeIdLabel) { + nodeIdLabel.textContent = node.data("id"); + } + + // Set the node image in the editor + const nodeImageLabel = document.getElementById("panel-node-editor-image"); + if (nodeImageLabel) { + nodeImageLabel.value = 'ghcr.io/nokia/srlinux:latest'; + } + + // Set the node image in the editor + const nodeGroupLabel = document.getElementById("panel-node-editor-group"); + if (nodeGroupLabel) { + nodeGroupLabel.value = 'data-center'; + } + + // Display the node editor panel + const nodeEditorPanel = document.getElementById("panel-node-editor"); + if (nodeEditorPanel) { + nodeEditorPanel.style.display = "block"; + } + + + // Fetch JSON schema from the backend + const url = "js/clabJsonSchema-v0.59.0.json"; + try { + const response = await fetch(url); + if (!response.ok) { + throw new Error(`HTTP error! Status: ${response.status}`); + } + const jsonData = await response.json(); + + // Get kind enums from the JSON data + const { kindOptions } = getKindEnums(jsonData); + console.log('Kind Enum:', kindOptions); + + // Populate the dropdown with fetched kindOptions + populateKindDropdown(kindOptions); + + // Populate the dropdown with fetched topoViwerRoleOptions + var topoViwerRoleOptions = ['bridge', 'controller', 'dcgw', 'router', 'leaf', 'pe', 'pon', 'rgw', 'server','super-spine', 'spine']; + populateTopoViewerRoleDropdown(topoViwerRoleOptions) + + // List type enums based on kind pattern + const typeOptions = getTypeEnumsByKindPattern(jsonData, '(srl|nokia_srlinux)'); // aarafat-tag: to be added to the UI + console.log('Type Enum for (srl|nokia_srlinux):', typeOptions); + + } catch (error) { + console.error("Error fetching or processing JSON data:", error.message); + throw error; + } + + } catch (error) { + console.error("Error in showPanelNodeEditor:", error); + // Optionally, display an error message to the user + const errorDiv = document.getElementById('panel-node-editor-error'); + if (errorDiv) { + errorDiv.textContent = "An error occurred while loading the node editor. Please try again."; + errorDiv.style.display = "block"; + } + } +} + +// Function to get kind enums from the JSON schema +function getKindEnums(jsonData) { + let kindOptions = []; + if (jsonData && jsonData.definitions && jsonData.definitions['node-config']) { + kindOptions = jsonData.definitions['node-config'].properties.kind.enum || []; + } else { + throw new Error("Invalid JSON structure or 'kind' enum not found"); + } + return { kindOptions, schemaData: jsonData }; +} + +// Function to get type enums based on a kind pattern +function getTypeEnumsByKindPattern(jsonData, pattern) { + if (jsonData && jsonData.definitions && jsonData.definitions['node-config'] && jsonData.definitions['node-config'].allOf) { + for (const condition of jsonData.definitions['node-config'].allOf) { + if (condition.if && condition.if.properties && condition.if.properties.kind && condition.if.properties.kind.pattern === pattern) { + if (condition.then && condition.then.properties && condition.then.properties.type && condition.then.properties.type.enum) { + return condition.then.properties.type.enum; + } + } + } + } + return []; +} + +let panelNodeEditorKind = "nokia_srlinux"; // Variable to store the selected option for dropdown menu, nokia_srlinux as default +// Function to populate the kind dropdown +function populateKindDropdown(options) { + // Get the dropdown elements by their IDs + const dropdownTrigger = document.querySelector("#panel-node-kind-dropdown .dropdown-trigger button span"); + const dropdownContent = document.getElementById("panel-node-kind-dropdown-content"); + const dropdownButton = document.querySelector("#panel-node-kind-dropdown .dropdown-trigger button"); + const dropdownContainer = dropdownButton.closest(".dropdown"); + + if (!dropdownTrigger || !dropdownContent || !dropdownButton || !dropdownContainer) { + console.error("Dropdown elements not found in the DOM."); + return; + } + + // Set the initial value on the dropdown button + dropdownTrigger.textContent = panelNodeEditorKind; + + // Clear any existing content + dropdownContent.innerHTML = ""; + + options.forEach(option => { + // Create a new anchor element for each option + const optionElement = document.createElement("a"); + optionElement.classList.add("dropdown-item", "label", "has-text-weight-normal", "is-small", "py-0"); + optionElement.textContent = option; + optionElement.href = "#"; // Optional, can be adjusted as needed + + // Set an event handler for the option + optionElement.addEventListener("click", (event) => { + event.preventDefault(); // Prevent default link behavior + + panelNodeEditorKind = option; // Store the selected option in the variable + console.log(`${panelNodeEditorKind} selected`); // Log the selected option + + dropdownTrigger.textContent = panelNodeEditorKind; + + // Collapse the dropdown menu + dropdownContainer.classList.remove("is-active"); + }); + + // Append the option element to the dropdown content + dropdownContent.appendChild(optionElement); + }); +} + +// Initialize event listeners for the dropdown +function initializeDropdownListeners() { + const dropdownButton = document.querySelector("#panel-node-kind-dropdown .dropdown-trigger button"); + const dropdownContainer = dropdownButton.closest(".dropdown"); + + if (!dropdownButton || !dropdownContainer) { + console.error("Dropdown button or container not found in the DOM."); + return; + } + + // Toggle dropdown menu on button click + dropdownButton.addEventListener("click", (event) => { + event.stopPropagation(); // Prevents the event from bubbling up + dropdownContainer.classList.toggle("is-active"); + }); + + // Collapse the dropdown if clicked outside + document.addEventListener("click", (event) => { + if (dropdownContainer.classList.contains("is-active")) { + dropdownContainer.classList.remove("is-active"); + } + }); +} +// Initialize dropdown listeners once when the DOM is fully loaded +document.addEventListener("DOMContentLoaded", () => { + initializeDropdownListeners(); +}); + + +let panelNodeEditorTopoViewerRole = "pe"; // Variable to store the selected option for dropdown menu, nokia_srlinux as default +// Function to populate the topoviewerrole dropdown +function populateTopoViewerRoleDropdown(options) { + // Get the dropdown elements by their IDs + const dropdownTrigger = document.querySelector("#panel-node-topoviewerrole-dropdown .dropdown-trigger button span"); + const dropdownContent = document.getElementById("panel-node-topoviewerrole-dropdown-content"); + const dropdownButton = document.querySelector("#panel-node-topoviewerrole-dropdown .dropdown-trigger button"); + const dropdownContainer = dropdownButton.closest(".dropdown"); + + if (!dropdownTrigger || !dropdownContent || !dropdownButton || !dropdownContainer) { + console.error("Dropdown elements not found in the DOM."); + return; + } + + // Set the initial value on the dropdown button + dropdownTrigger.textContent = panelNodeEditorTopoViewerRole; + + + // Clear any existing content + dropdownContent.innerHTML = ""; + + options.forEach(option => { + // Create a new anchor element for each option + const optionElement = document.createElement("a"); + optionElement.classList.add("dropdown-item", "label", "has-text-weight-normal", "is-small", "py-0"); + optionElement.textContent = option; + optionElement.href = "#"; // Optional, can be adjusted as needed + + // Set an event handler for the option + optionElement.addEventListener("click", (event) => { + event.preventDefault(); // Prevent default link behavior + + panelNodeEditorTopoViewerRole = option; // Store the selected option in the variable + console.log(`${panelNodeEditorTopoViewerRole} selected`); // Log the selected option + + dropdownTrigger.textContent = panelNodeEditorTopoViewerRole; + + // Collapse the dropdown menu + dropdownContainer.classList.remove("is-active"); + }); + + // Append the option element to the dropdown content + dropdownContent.appendChild(optionElement); + }); +} + +// Initialize event listeners for the dropdown +function initializeDropdownTopoViewerRoleListeners() { + const dropdownButton = document.querySelector("#panel-node-topoviewerrole-dropdown .dropdown-trigger button"); + const dropdownContainer = dropdownButton.closest(".dropdown"); + + if (!dropdownButton || !dropdownContainer) { + console.error("Dropdown button or container not found in the DOM."); + return; + } + + // Toggle dropdown menu on button click + dropdownButton.addEventListener("click", (event) => { + event.stopPropagation(); // Prevents the event from bubbling up + dropdownContainer.classList.toggle("is-active"); + }); + + // Collapse the dropdown if clicked outside + document.addEventListener("click", (event) => { + if (dropdownContainer.classList.contains("is-active")) { + dropdownContainer.classList.remove("is-active"); + } + }); +} + +// Initialize dropdown listeners once when the DOM is fully loaded +document.addEventListener("DOMContentLoaded", () => { + initializeDropdownTopoViewerRoleListeners(); +}); + +// Initialize event listener for the close button +document.getElementById("panel-node-editor-close-button").addEventListener("click", () => { + document.getElementById("panel-node-editor").style.display = "none"; +}); + + +// update node data in the editor, save cyto json to file dataCytoMarshall.json and save to clab topo.yaml +async function saveNodeToEditorToFile() { + const nodeId =document.getElementById("panel-node-editor-id").textContent + var cyNode = cy.$id(nodeId); // Get cytoscpe node object id + + // get value from panel-node-editor + nodeName = document.getElementById("panel-node-editor-name").value + kind = panelNodeEditorKind + image = document.getElementById("panel-node-editor-image").value + group = document.getElementById("panel-node-editor-group").value + topoViewerRole = panelNodeEditorTopoViewerRole + + console.log("panelEditorNodeName", nodeName) + console.log("panelEditorkind", kind) + console.log("panelEditorImage", image) + console.log("panelEditorGroup", group) + console.log("panelEditorTopoViewerRole",topoViewerRole) + + // save node data to cytoscape node object + var extraData = { + "kind": kind, + "image": image, + "longname": "", + "mgmtIpv4Addresss": "" + }; + + cyNode.data(('name'), nodeName) + cyNode.data(('parent'), group) + cyNode.data(('topoViewerRole'), topoViewerRole) + cyNode.data(('extraData'), extraData) + + console.log('cyto node object data: ', cyNode); + + // dump cytoscape node object to nodeData to be persisted to dataCytoMarshall.json + var nodeData = cy.$id(nodeId).json(); // Get JSON data of the node with the specified ID + const endpointName = '/clab-save-topo-cyto-json'; + + try { + // Send the enhanced node data directly without wrapping it in an object + const response = await sendRequestToEndpointPost(endpointName, [nodeData]); + console.log('Node data saved successfully', response); + } catch (error) { + console.error('Failed to save node data:', error); + } + + // add node to clab editor textarea + clabEditorAddNode(nodeId, nodeName, kind, image, group, topoViewerRole) + + clabEditorSaveYamlTopo() +} + + +// NODE EDITOR END +// NODE EDITOR END + + +async function showPanelTopoViewerClient(event) { + // Remove all Overlayed Panel + // Get all elements with the class "panel-overlay" + var panelOverlays = document.getElementsByClassName("panel-overlay"); + // Loop through each element and set its display to 'none' + for (var i = 0; i < panelOverlays.length; i++) { + panelOverlays[i].style.display = "none"; + } + + environments = await getEnvironments(event); + console.log("linkImpairment - environments: ", environments) + + clabServerAddress = environments["clab-server-address"] + clabServerPort = environments["clab-server-port"] + + hrefWindows = `http://${clabServerAddress}:${clabServerPort}/clab-client/clab-client-windows/ClabCapture.app.zip` + hrefMac = `http://${clabServerAddress}:${clabServerPort}/clab-client/clab-client-mac/ClabCapture.app.zip` + + document.getElementById("panel-topoviewer-helper").style.display = "block"; + + const htmlContent = ` +
Wireshark Capture
+

+ Please download the following helper app: +

+ +

+ TopoViewer offers a remote capture feature for intercepting ContainerLab node endpoints. + For the best experience, it's recommended to have both TopoViewer and its helper app installed on client-side. + With the TopoViewer helper app, you can effortlessly automate the launch of Wireshark's GUI. +

+

+ Alternatively, if you don't have the helper app, you can simply copy and paste an SSH command to initiate Wireshark manually. + This setup provides flexibility in how you utilize this feature.
+

+ `; + document.getElementById("panel-topoviewer-helper-content").innerHTML = htmlContent; +} + +async function showPanelAbout(event) { + // Remove all Overlayed Panel + // Get all elements with the class "panel-overlay" + var panelOverlays = document.getElementsByClassName("panel-overlay"); + // Loop through each element and set its display to 'none' + for (var i = 0; i < panelOverlays.length; i++) { + panelOverlays[i].style.display = "none"; + } + + environments = await getEnvironments(event); + console.log("linkImpairment - environments: ", environments) + + topoViewerVersion = environments["topoviewer-version"] + + document.getElementById("panel-topoviewer-about").style.display = "block"; + + const htmlContent = ` +
+
Version: ${topoViewerVersion}
+ +

+ Designed and developed by Asad Arafat
+

+

+ Special Thanks: +

+

+ + +
+ `; + document.getElementById("panel-topoviewer-about-content").innerHTML = htmlContent; +} + +async function sidebarButtonFitScreen(event) { + + // --sidebar-button-background-color-default: rgba(54,58, 69, 1); + // --sidebar-button-background-color-active: rgba(76, 82, 97, 1); + + var sidebarButtonFitScreen = document.getElementById("sidebar-button-fit-screen") + const sidebarButtonColorDefault = getComputedStyle(sidebarButtonFitScreen).getPropertyValue('--sidebar-button-background-color-default'); + const sidebarButtonColorActive = getComputedStyle(sidebarButtonFitScreen).getPropertyValue('--sidebar-button-background-color-active'); + + drawer = document.getElementById("drawer") + if (drawer.style.display === 'block') { + drawer.style.display = 'none'; + var sidebarButtonFitScreen = document.getElementById("sidebar-button-fit-screen") + sidebarButtonFitScreen.style.background = sidebarButtonColorDefault.trim(); + sidebarButtonFitScreen.style.border = sidebarButtonColorActive.trim(); + } else { + drawer.style.display = 'block'; + var sidebarButtons = document.getElementsByClassName("is-sidebar"); + // Loop through each element and set its display to 'none' + for (var i = 0; i < sidebarButtons.length; i++) { + sidebarButtons[i].style.background = sidebarButtonColorDefault.trim(); + sidebarButtons[i].style.border = sidebarButtonColorDefault.trim(); + } + sidebarButtonFitScreen.style.background = sidebarButtonColorActive.trim(); + } + +} + +async function getActualNodesEndpoints(event) { + try { + bulmaToast.toast({ + message: `Getting Actual Nodes Endpoint Labels... Hold on..! 🚀💻`, + type: "is-warning is-size-6 p-3", + duration: 4000, + position: "top-center", + closeOnClick: true, + }); + appendMessage( + `Getting Actual Nodes Endpoint Labels... Hold on..! 🚀💻`, + ); + + showLoadingSpinnerGlobal() + const CyTopoJson = await sendRequestToEndpointGetV2("/actual-nodes-endpoints", argsList = []) + location.reload(true); + + // Handle the response data + if (CyTopoJson && typeof CyTopoJson === 'object' && Object.keys(CyTopoJson).length > 0) { + hideLoadingSpinnerGlobal(); + console.log("Valid non-empty JSON response received:", CyTopoJson); + + hideLoadingSpinnerGlobal(); + + return CyTopoJson + + } else { + + hideLoadingSpinnerGlobal(); + + console.log("Empty or invalid JSON response received"); + } + } catch (error) { + hideLoadingSpinnerGlobal(); + console.error("Error occurred:", error); + // Handle errors as needed + } +} + +function viewportButtonsZoomToFit() { + const initialZoom = cy.zoom(); + appendMessage(`Bro, initial zoom level is "${initialZoom}".`); + //- Fit all nodes possible with padding + //- Fit all nodes possible with padding + cy.fit(); + const currentZoom = cy.zoom(); + appendMessage(`And now the zoom level is "${currentZoom}".`); +} + +function viewportButtonsLayoutAlgo() { + var viewportDrawer = document.getElementsByClassName("viewport-drawer"); + // Loop through each element and set its display to 'none' + for (var i = 0; i < viewportDrawer.length; i++) { + viewportDrawer[i].style.display = "none"; + } + + viewportDrawerLayout = document.getElementById("viewport-drawer-layout") + viewportDrawerLayout.style.display = "block" +} + + + + +async function layoutAlgoChange(event) { + + try { + console.log("layoutAlgoChange clicked"); + + var selectElement = document.getElementById("select-layout-algo"); + var selectedOption = selectElement.value; + + if (selectedOption === "Force Directed") { + console.log("Force Directed algo selected"); + + var layoutAlgoPanels = document.getElementsByClassName("layout-algo"); + // Loop through each element and set its display to 'none' + for (var i = 0; i < layoutAlgoPanels.length; i++) { + layoutAlgoPanels[i].style.display = "none"; + } + + viewportDrawerForceDirected = document.getElementById("viewport-drawer-force-directed") + viewportDrawerForceDirected.style.display = "block" + + viewportDrawerForceDirectedResetStart = document.getElementById("viewport-drawer-force-directed-reset-start") + viewportDrawerForceDirectedResetStart.style.display = "block" + + console.log(document.getElementById("viewport-drawer-force-directed")) + console.log(document.getElementById("viewport-drawer-force-directed-reset-start")) + + } else if (selectedOption === "Vertical") { + console.log("Vertical algo selected"); + + var layoutAlgoPanels = document.getElementsByClassName("layout-algo"); + // Loop through each element and set its display to 'none' + for (var i = 0; i < layoutAlgoPanels.length; i++) { + layoutAlgoPanels[i].style.display = "none"; + } + + viewportDrawerForceDirected = document.getElementById("viewport-drawer-dc-vertical") + viewportDrawerForceDirected.style.display = "block" + + viewportDrawerForceDirectedResetStart = document.getElementById("viewport-drawer-dc-vertical-reset-start") + viewportDrawerForceDirectedResetStart.style.display = "block" + + console.log(document.getElementById("viewport-drawer-dc-vertical")) + console.log(document.getElementById("viewport-drawer-dc-vertical-reset-start")) + + } else if (selectedOption === "Horizontal") { + console.log("Horizontal algo selected"); + + var layoutAlgoPanels = document.getElementsByClassName("layout-algo"); + // Loop through each element and set its display to 'none' + for (var i = 0; i < layoutAlgoPanels.length; i++) { + layoutAlgoPanels[i].style.display = "none"; + } + + viewportDrawerForceDirected = document.getElementById("viewport-drawer-dc-horizontal") + viewportDrawerForceDirected.style.display = "block" + + viewportDrawerForceDirectedResetStart = document.getElementById("viewport-drawer-dc-horizontal-reset-start") + viewportDrawerForceDirectedResetStart.style.display = "block" + + console.log(document.getElementById("viewport-drawer-dc-horizontal")) + console.log(document.getElementById("viewport-drawer-dc-horizontal-reset-start")) + } + + }catch (error) { + console.error("Error occurred:", error); + // Handle errors as needed + } +} + + +function viewportButtonsTopologyOverview() { + var viewportDrawer = document.getElementsByClassName("viewport-drawer"); + // Loop through each element and set its display to 'none' + for (var i = 0; i < viewportDrawer.length; i++) { + viewportDrawer[i].style.display = "none"; + } + + console.log("viewportButtonsTopologyOverview clicked") + viewportDrawerLayout = document.getElementById("viewport-drawer-topology-overview") + viewportDrawerLayout.style.display = "block" + + viewportDrawerLayoutContent = document.getElementById("viewport-drawer-topology-overview-content") + viewportDrawerLayoutContent.style.display = "block" +} + +function viewportButtonsTopologyCapture() { + var viewportDrawer = document.getElementsByClassName("viewport-drawer"); + // Loop through each element and set its display to 'none' + for (var i = 0; i < viewportDrawer.length; i++) { + viewportDrawer[i].style.display = "none"; + } + + console.log("viewportButtonsTopologyCapture clicked") + + viewportDrawerCapture = document.getElementById("viewport-drawer-capture-sceenshoot") + viewportDrawerCapture.style.display = "block" + + viewportDrawerCaptureContent = document.getElementById("viewport-drawer-capture-sceenshoot-content") + viewportDrawerCaptureContent.style.display = "block" +} + +function viewportButtonsLabelEndpoint() { + if (linkEndpointVisibility) { + cy.edges().forEach(function(edge) { + // edge.style("source-label", "."); + // edge.style("target-label", "."); + edge.style("text-opacity", 0); + edge.style("text-background-opacity", 0); + + + linkEndpointVisibility = false; + }); + } else { + cy.edges().forEach(function(edge) { + edge.style("text-opacity", 1); + edge.style("text-background-opacity", 0.7); + linkEndpointVisibility = true; + }); + } +} + +function viewportButtonContainerStatusVisibility() { + if (nodeContainerStatusVisibility) { + nodeContainerStatusVisibility = false; + console.log( + "nodeContainerStatusVisibility: " + nodeContainerStatusVisibility, + ); + appendMessage( + "nodeContainerStatusVisibility: " + nodeContainerStatusVisibility, + ); + bulmaToast.toast({ + message: `Alright, mission control, we're standing down. 🛑🔍 Container status probing aborted. Stay chill, folks. 😎👨‍💻`, + type: "is-warning is-size-6 p-3", + duration: 4000, + position: "top-center", + closeOnClick: true, + }); + } else { + nodeContainerStatusVisibility = true; + console.log( + "nodeContainerStatusVisibility: " + nodeContainerStatusVisibility, + ); + appendMessage( + "nodeContainerStatusVisibility: " + nodeContainerStatusVisibility, + ); + bulmaToast.toast({ + message: `🕵️‍♂️ Bro, we're currently on a mission to probe that container status! Stay tuned for the results. 🔍🚀👨‍💻`, + type: "is-warning is-size-6 p-3", + duration: 4000, + position: "top-center", + closeOnClick: true, + }); + } +} + + +function viewportDrawerLayoutForceDirected() { + edgeLengthSlider = document.getElementById("force-directed-slider-link-lenght"); + nodeGapSlider = document.getElementById("force-directed-slider-node-gap"); + + const edgeLengthValue = parseFloat(edgeLengthSlider.value); + const nodeGapValue = parseFloat(nodeGapSlider.value); + + console.log("edgeLengthValue", edgeLengthValue); + console.log("nodeGapValue", nodeGapValue); + + cy.layout( + { + fit: true, + name: "cola", + animate: true, + randomize: false, + maxSimulationTime: 400, + edgeLength: function(e) { + return edgeLengthValue / e.data("weight"); + }, + nodeGap: function(e) { + return nodeGapValue / e.data("weight"); + }, + }) + .run(); +} + +function viewportDrawerLayoutVertical() { + nodevGap = document.getElementById("vertical-layout-slider-node-v-gap"); + groupvGap = document.getElementById("vertical-layout-slider-group-v-gap"); + + const nodevGapValue = parseFloat(nodevGap.value); + const groupvGapValue = parseFloat(groupvGap.value); + + console.log("nodevGapValue", nodevGapValue); + console.log("groupvGapValue", groupvGapValue); + + const xOffset = parseFloat(nodevGapValue); + const yOffset = parseFloat(groupvGapValue); + + console.log("yOffset", yOffset); + console.log("xOffset", xOffset); + + const delay = 100; + + setTimeout(() => { + cy.nodes().forEach(function(node) { + if (node.isParent()) { + // For each parent node + const children = node.children(); + const numRows = 1; + + const cellWidth = node.width() / children.length; + // const xOffset = 5 + + children.forEach(function(child, index) { + // Position children in rows + const xPos = index * (cellWidth + xOffset); + const yPos = 0; + + // Set the position of each child node + child.position({ + x: xPos, + y: yPos + }); + }); + } + }); + + var parentCounts = {}; + var maxWidth = 0; + var centerX = 0; + var centerY = cy.height() / 2; + + // Count children of each parent node + cy.nodes().forEach(function(node) { + if (node.isParent()) { + const childrenCount = node.children().length; + parentCounts[node.id()] = childrenCount; + } + }); + + cy.nodes().forEach(function(node) { + if (node.isParent()) { + const width = node.width(); + if (width > maxWidth) { + maxWidth = width; + console.log("ParentMaxWidth: ", maxWidth); + } + } + }); + + const divisionFactor = maxWidth / 2; + console.log("divisionFactor: ", divisionFactor); + + // Sort parent nodes by child count in ascending order + const sortedParents = Object.keys(parentCounts).sort( + (a, b) => parentCounts[a] - parentCounts[b], + ); + + let yPos = 0; + // const yOffset = 50; + // const yOffset = 50; + + // Position parent nodes vertically and center them horizontally + sortedParents.forEach(function(parentId) { + const parent = cy.getElementById(parentId); + const xPos = centerX - parent.width() / divisionFactor; + // to the left compared to the center of the widest parent node. + parent.position({ + x: xPos, + y: yPos + }); + yPos += yOffset; + }); + cy.fit(); + }, delay); +} + +function viewportDrawerLayoutHorizontal() { + nodehGap = document.getElementById("horizontal-layout-slider-node-h-gap"); + grouphGap = document.getElementById("horizontal-layout-slider-group-h-gap"); + + const horizontalNodeGap = parseFloat(nodehGap.value); + const horizontalGroupGap = parseFloat(grouphGap.value); + + console.log("nodevGapValue", horizontalNodeGap); + console.log("groupvGapValue", horizontalGroupGap); + + const yOffset = parseFloat(horizontalNodeGap); + const xOffset = parseFloat(horizontalGroupGap); + + console.log("yOffset", yOffset); + console.log("xOffset", xOffset); + + const delay = 100; + setTimeout(() => { + cy.nodes().forEach(function(node) { + if (node.isParent()) { + // For each parent node + const children = node.children(); + const numColumns = 1; + const cellHeight = node.height() / children.length; + // const yOffset = 5; + + children.forEach(function(child, index) { + // Position children in columns + const xPos = 0; + const yPos = index * (cellHeight + yOffset); + + // Set the position of each child node + child.position({ + x: xPos, + y: yPos + }); + }); + } + }); + + var parentCounts = {}; + var maxHeight = 0; + var centerX = cy.width() / 2; + var centerY = cy.height() / 2; + + // Count children of each parent node + cy.nodes().forEach(function(node) { + if (node.isParent()) { + const childrenCount = node.children().length; + parentCounts[node.id()] = childrenCount; + } + }); + + cy.nodes().forEach(function(node) { + if (node.isParent()) { + const height = node.height(); + if (height > maxHeight) { + maxHeight = height; + console.log("ParentMaxHeight: ", maxHeight); + } + } + }); + + const divisionFactor = maxHeight / 2; + console.log("divisionFactor: ", divisionFactor); + + // Sort parent nodes by child count in ascending order + const sortedParents = Object.keys(parentCounts).sort( + (a, b) => parentCounts[a] - parentCounts[b], + ); + + let xPos = 0; + // const xOffset = 50; + + // Position parent nodes horizontally and center them vertically + sortedParents.forEach(function(parentId) { + const parent = cy.getElementById(parentId); + const yPos = centerY - parent.height() / divisionFactor; + parent.position({ + x: xPos, + y: yPos + }); + xPos -= xOffset; + }); + + cy.fit(); + }, delay); + +} + + +function viewportDrawerCaptureButton() { + + console.log ("viewportDrawerCaptureButton() - clicked") + + // Get all checkbox inputs within the specific div + const checkboxes = document.querySelectorAll('#viewport-drawer-capture-sceenshoot-content .checkbox-input'); + + // Initialize an array to store the values of checked checkboxes + const selectedOptions = []; + + // Iterate through the NodeList of checkboxes + checkboxes.forEach((checkbox) => { + // If the checkbox is checked, push its value to the array + if (checkbox.checked) { + selectedOptions.push(checkbox.value); + } + }); + + console.log ("viewportDrawerCaptureButton() - ", selectedOptions) + + + if (selectedOptions.length === 0) { + bulmaToast.toast({ + message: `Hey there, please pick at least one option.😊👌`, + type: "is-warning is-size-6 p-3", + duration: 4000, + position: "top-center", + closeOnClick: true, + }); + } else { + // Perform your action based on the selected options + // Perform your action based on the selected options + if (selectedOptions.join(", ") == "option01") { + captureAndSaveViewportAsPng(cy); + modal.classList.remove("is-active"); + } else if (selectedOptions.join(", ") == "option02") { + captureAndSaveViewportAsDrawIo(cy); + modal.classList.remove("is-active"); + } else if (selectedOptions.join(", ") == "option01, option02") { + captureAndSaveViewportAsPng(cy); + sleep(5000); + captureAndSaveViewportAsDrawIo(cy); + modal.classList.remove("is-active"); + } + } + +} + +async function captureAndSaveViewportAsDrawIo(cy) { + // Define base64-encoded SVGs for each role + const svgBase64ByRole = { + dcgw: 'data:image/svg+xml,PHN2ZyB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWw6c3BhY2U9InByZXNlcnZlIiBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCAxMjAgMTIwOyIgdmlld0JveD0iMCAwIDEyMCAxMjAiIHk9IjBweCIgeD0iMHB4IiBpZD0iTGF5ZXJfMSIgdmVyc2lvbj0iMS4xIj4mI3hhOzxzdHlsZSB0eXBlPSJ0ZXh0L2NzcyI+LnN0MCB7IGZpbGw6IHJnYigxLCA5MCwgMjU1KTsgfSAuc3QxIHsgZmlsbDogbm9uZTsgc3Ryb2tlOiByZ2IoMjU1LCAyNTUsIDI1NSk7IHN0cm9rZS13aWR0aDogNDsgc3Ryb2tlLWxpbmVjYXA6IHJvdW5kOyBzdHJva2UtbGluZWpvaW46IHJvdW5kOyBzdHJva2UtbWl0ZXJsaW1pdDogMTA7IH0gLnN0MiB7IGZpbGw6IHJnYigyNTUsIDI1NSwgMjU1KTsgfSAuc3QzIHsgZmlsbDogbm9uZTsgc3Ryb2tlOiByZ2IoMjU1LCAyNTUsIDI1NSk7IHN0cm9rZS13aWR0aDogNDsgc3Ryb2tlLW1pdGVybGltaXQ6IDEwOyB9IC5zdDQgeyBmaWxsOiBub25lOyBzdHJva2U6IHJnYigyNTUsIDI1NSwgMjU1KTsgc3Ryb2tlLXdpZHRoOiA0OyBzdHJva2UtbGluZWNhcDogcm91bmQ7IHN0cm9rZS1saW5lam9pbjogcm91bmQ7IH0gLnN0NSB7IGZpbGw6IHJnYigyNTUsIDI1NSwgMjU1KTsgc3Ryb2tlOiByZ2IoMjU1LCAyNTUsIDI1NSk7IHN0cm9rZS13aWR0aDogNDsgc3Ryb2tlLWxpbmVjYXA6IHJvdW5kOyBzdHJva2UtbGluZWpvaW46IHJvdW5kOyBzdHJva2UtbWl0ZXJsaW1pdDogMTA7IH0gLnN0NiB7IGZpbGw6IG5vbmU7IHN0cm9rZTogcmdiKDI1NSwgMjU1LCAyNTUpOyBzdHJva2Utd2lkdGg6IDQuMjMzMzsgc3Ryb2tlLWxpbmVjYXA6IHJvdW5kOyBzdHJva2UtbGluZWpvaW46IHJvdW5kOyBzdHJva2UtbWl0ZXJsaW1pdDogMTA7IH0gLnN0NyB7IGZpbGw6IG5vbmU7IHN0cm9rZTogcmdiKDI1NSwgMjU1LCAyNTUpOyBzdHJva2Utd2lkdGg6IDQ7IHN0cm9rZS1saW5lY2FwOiByb3VuZDsgc3Ryb2tlLW1pdGVybGltaXQ6IDEwOyB9IC5zdDggeyBmaWxsOiByZ2IoMjU1LCAyNTUsIDI1NSk7IHN0cm9rZTogcmdiKDI1NSwgMjU1LCAyNTUpOyBzdHJva2Utd2lkdGg6IDQ7IHN0cm9rZS1taXRlcmxpbWl0OiAxMDsgfSAuc3Q5IHsgZmlsbDogcmdiKDI1NSwgMjU1LCAyNTUpOyBzdHJva2U6IHJnYigyNTUsIDI1NSwgMjU1KTsgc3Ryb2tlLXdpZHRoOiA0OyB9IC5zdDEwIHsgZmlsbDogbm9uZTsgc3Ryb2tlOiByZ2IoMjU1LCAyNTUsIDI1NSk7IHN0cm9rZS13aWR0aDogNDsgfSAuc3QxMSB7IGZpbGw6IHJnYigzOCwgMzgsIDM4KTsgc3Ryb2tlOiByZ2IoMjU1LCAyNTUsIDI1NSk7IHN0cm9rZS13aWR0aDogNC4yMzMzOyB9IC5zdDEyIHsgZmlsbC1ydWxlOiBldmVub2RkOyBjbGlwLXJ1bGU6IGV2ZW5vZGQ7IGZpbGw6IG5vbmU7IHN0cm9rZTogcmdiKDI1NSwgMjU1LCAyNTUpOyBzdHJva2Utd2lkdGg6IDQ7IHN0cm9rZS1taXRlcmxpbWl0OiAxMDsgfSAuc3QxMyB7IGZpbGwtcnVsZTogZXZlbm9kZDsgY2xpcC1ydWxlOiBldmVub2RkOyBmaWxsOiByZ2IoMjU1LCAyNTUsIDI1NSk7IHN0cm9rZTogcmdiKDI1NSwgMjU1LCAyNTUpOyBzdHJva2Utd2lkdGg6IDQ7IH0gLnN0MTQgeyBmaWxsOiBub25lOyBzdHJva2U6IHJnYigyNTUsIDI1NSwgMjU1KTsgc3Ryb2tlLXdpZHRoOiA0LjIzMzM7IHN0cm9rZS1saW5lY2FwOiByb3VuZDsgc3Ryb2tlLWxpbmVqb2luOiByb3VuZDsgfSAuc3QxNSB7IGZpbGw6IG5vbmU7IHN0cm9rZTogcmdiKDI1NSwgMjU1LCAyNTUpOyBzdHJva2Utd2lkdGg6IDQ7IHN0cm9rZS1saW5lY2FwOiByb3VuZDsgfSAuc3QxNiB7IGZpbGw6IHJnYigyNTUsIDI1NSwgMjU1KTsgc3Ryb2tlOiByZ2IoMjU1LCAyNTUsIDI1NSk7IHN0cm9rZS1taXRlcmxpbWl0OiAxMDsgfSAuc3QxNyB7IGZpbGw6IHJnYigzOCwgMzgsIDM4KTsgc3Ryb2tlOiByZ2IoMjU1LCAyNTUsIDI1NSk7IHN0cm9rZS13aWR0aDogNDsgc3Ryb2tlLW1pdGVybGltaXQ6IDEwOyB9IC5zdDE4IHsgZmlsbDogcmdiKDI1NSwgMjU1LCAyNTUpOyBzdHJva2U6IHJnYigyNTUsIDI1NSwgMjU1KTsgc3Ryb2tlLXdpZHRoOiA0OyBzdHJva2UtbGluZWNhcDogcm91bmQ7IHN0cm9rZS1saW5lam9pbjogcm91bmQ7IH0gPC9zdHlsZT4mI3hhOzxyZWN0IGhlaWdodD0iMTIwIiB3aWR0aD0iMTIwIiBjbGFzcz0ic3QwIi8+JiN4YTs8Zz4mI3hhOwk8Zz4mI3hhOwkJPHBhdGggZD0iTTk4LDMwLjFINjhMNTIsODkuOUgyMiIgY2xhc3M9InN0MSIvPiYjeGE7CQk8cGF0aCBkPSJNMjgsMTAwbC03LTguMWMtMS4zLTEuMy0xLjMtMy4xLDAtNC4zbDctNy42IiBjbGFzcz0ic3QxIi8+JiN4YTsJCTxwYXRoIGQ9Ik05MiwyMGw3LDguMWMxLjMsMS4zLDEuMywzLjEsMCw0LjNMOTIsNDAiIGNsYXNzPSJzdDEiLz4mI3hhOwk8L2c+JiN4YTsJPHBhdGggZD0iTTk4LDg5LjlINjQiIGNsYXNzPSJzdDEiLz4mI3hhOwk8cGF0aCBkPSJNOTIsODBsNyw3LjZjMS4zLDEuMywxLjMsMy4xLDAsNC4zbC03LDguMSIgY2xhc3M9InN0MSIvPiYjeGE7CTxwYXRoIGQ9Ik01NiwzMC4xSDIyIE0yOCw0MGwtNy03LjZjLTEuMy0xLjMtMS4zLTMuMSwwLTQuM2w3LTguMSIgY2xhc3M9InN0MSIvPiYjeGE7CTxsaW5lIHkyPSI0OCIgeDI9Ijc2IiB5MT0iNDgiIHgxPSIxMDAiIGNsYXNzPSJzdDEiLz4mI3hhOwk8bGluZSB5Mj0iNjAiIHgyPSI3MiIgeTE9IjYwIiB4MT0iMTAwIiBjbGFzcz0ic3QxIi8+JiN4YTsJPGxpbmUgeTI9IjcyIiB4Mj0iNjgiIHkxPSI3MiIgeDE9IjEwMCIgY2xhc3M9InN0MSIvPiYjeGE7CTxsaW5lIHkyPSI3MiIgeDI9IjQ0IiB5MT0iNzIiIHgxPSIyMCIgY2xhc3M9InN0MSIvPiYjeGE7CTxsaW5lIHkyPSI2MCIgeDI9IjQ4IiB5MT0iNjAiIHgxPSIyMCIgY2xhc3M9InN0MSIvPiYjeGE7CTxsaW5lIHkyPSI0OCIgeDI9IjUyIiB5MT0iNDgiIHgxPSIyMCIgY2xhc3M9InN0MSIvPiYjeGE7PC9nPiYjeGE7PC9zdmc+', + router: 'data:image/svg+xml,PHN2ZyB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWw6c3BhY2U9InByZXNlcnZlIiBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCAxMjAgMTIwOyIgdmlld0JveD0iMCAwIDEyMCAxMjAiIHk9IjBweCIgeD0iMHB4IiBpZD0iTGF5ZXJfMSIgdmVyc2lvbj0iMS4xIj4mI3hhOzxzdHlsZSB0eXBlPSJ0ZXh0L2NzcyI+LnN0MCB7IGZpbGw6IHJnYigxLCA5MCwgMjU1KTsgfSAuc3QxIHsgZmlsbDogbm9uZTsgc3Ryb2tlOiByZ2IoMjU1LCAyNTUsIDI1NSk7IHN0cm9rZS13aWR0aDogNDsgc3Ryb2tlLWxpbmVjYXA6IHJvdW5kOyBzdHJva2UtbGluZWpvaW46IHJvdW5kOyBzdHJva2UtbWl0ZXJsaW1pdDogMTA7IH0gLnN0MiB7IGZpbGw6IHJnYigyNTUsIDI1NSwgMjU1KTsgfSAuc3QzIHsgZmlsbDogbm9uZTsgc3Ryb2tlOiByZ2IoMjU1LCAyNTUsIDI1NSk7IHN0cm9rZS13aWR0aDogNDsgc3Ryb2tlLW1pdGVybGltaXQ6IDEwOyB9IC5zdDQgeyBmaWxsOiBub25lOyBzdHJva2U6IHJnYigyNTUsIDI1NSwgMjU1KTsgc3Ryb2tlLXdpZHRoOiA0OyBzdHJva2UtbGluZWNhcDogcm91bmQ7IHN0cm9rZS1saW5lam9pbjogcm91bmQ7IH0gLnN0NSB7IGZpbGw6IHJnYigyNTUsIDI1NSwgMjU1KTsgc3Ryb2tlOiByZ2IoMjU1LCAyNTUsIDI1NSk7IHN0cm9rZS13aWR0aDogNDsgc3Ryb2tlLWxpbmVjYXA6IHJvdW5kOyBzdHJva2UtbGluZWpvaW46IHJvdW5kOyBzdHJva2UtbWl0ZXJsaW1pdDogMTA7IH0gLnN0NiB7IGZpbGw6IG5vbmU7IHN0cm9rZTogcmdiKDI1NSwgMjU1LCAyNTUpOyBzdHJva2Utd2lkdGg6IDQuMjMzMzsgc3Ryb2tlLWxpbmVjYXA6IHJvdW5kOyBzdHJva2UtbGluZWpvaW46IHJvdW5kOyBzdHJva2UtbWl0ZXJsaW1pdDogMTA7IH0gLnN0NyB7IGZpbGw6IG5vbmU7IHN0cm9rZTogcmdiKDI1NSwgMjU1LCAyNTUpOyBzdHJva2Utd2lkdGg6IDQ7IHN0cm9rZS1saW5lY2FwOiByb3VuZDsgc3Ryb2tlLW1pdGVybGltaXQ6IDEwOyB9IC5zdDggeyBmaWxsOiByZ2IoMjU1LCAyNTUsIDI1NSk7IHN0cm9rZTogcmdiKDI1NSwgMjU1LCAyNTUpOyBzdHJva2Utd2lkdGg6IDQ7IHN0cm9rZS1taXRlcmxpbWl0OiAxMDsgfSAuc3Q5IHsgZmlsbDogcmdiKDI1NSwgMjU1LCAyNTUpOyBzdHJva2U6IHJnYigyNTUsIDI1NSwgMjU1KTsgc3Ryb2tlLXdpZHRoOiA0OyB9IC5zdDEwIHsgZmlsbDogbm9uZTsgc3Ryb2tlOiByZ2IoMjU1LCAyNTUsIDI1NSk7IHN0cm9rZS13aWR0aDogNDsgfSAuc3QxMSB7IGZpbGw6IHJnYigzOCwgMzgsIDM4KTsgc3Ryb2tlOiByZ2IoMjU1LCAyNTUsIDI1NSk7IHN0cm9rZS13aWR0aDogNC4yMzMzOyB9IC5zdDEyIHsgZmlsbC1ydWxlOiBldmVub2RkOyBjbGlwLXJ1bGU6IGV2ZW5vZGQ7IGZpbGw6IG5vbmU7IHN0cm9rZTogcmdiKDI1NSwgMjU1LCAyNTUpOyBzdHJva2Utd2lkdGg6IDQ7IHN0cm9rZS1taXRlcmxpbWl0OiAxMDsgfSAuc3QxMyB7IGZpbGwtcnVsZTogZXZlbm9kZDsgY2xpcC1ydWxlOiBldmVub2RkOyBmaWxsOiByZ2IoMjU1LCAyNTUsIDI1NSk7IHN0cm9rZTogcmdiKDI1NSwgMjU1LCAyNTUpOyBzdHJva2Utd2lkdGg6IDQ7IH0gLnN0MTQgeyBmaWxsOiBub25lOyBzdHJva2U6IHJnYigyNTUsIDI1NSwgMjU1KTsgc3Ryb2tlLXdpZHRoOiA0LjIzMzM7IHN0cm9rZS1saW5lY2FwOiByb3VuZDsgc3Ryb2tlLWxpbmVqb2luOiByb3VuZDsgfSAuc3QxNSB7IGZpbGw6IG5vbmU7IHN0cm9rZTogcmdiKDI1NSwgMjU1LCAyNTUpOyBzdHJva2Utd2lkdGg6IDQ7IHN0cm9rZS1saW5lY2FwOiByb3VuZDsgfSAuc3QxNiB7IGZpbGw6IHJnYigyNTUsIDI1NSwgMjU1KTsgc3Ryb2tlOiByZ2IoMjU1LCAyNTUsIDI1NSk7IHN0cm9rZS1taXRlcmxpbWl0OiAxMDsgfSAuc3QxNyB7IGZpbGw6IHJnYigzOCwgMzgsIDM4KTsgc3Ryb2tlOiByZ2IoMjU1LCAyNTUsIDI1NSk7IHN0cm9rZS13aWR0aDogNDsgc3Ryb2tlLW1pdGVybGltaXQ6IDEwOyB9IC5zdDE4IHsgZmlsbDogcmdiKDI1NSwgMjU1LCAyNTUpOyBzdHJva2U6IHJnYigyNTUsIDI1NSwgMjU1KTsgc3Ryb2tlLXdpZHRoOiA0OyBzdHJva2UtbGluZWNhcDogcm91bmQ7IHN0cm9rZS1saW5lam9pbjogcm91bmQ7IH0gPC9zdHlsZT4mI3hhOzxyZWN0IGhlaWdodD0iMTIwIiB3aWR0aD0iMTIwIiBjbGFzcz0ic3QwIiB4PSIwIi8+JiN4YTs8Zz4mI3hhOwk8Zz4mI3hhOwkJPHBhdGggZD0iTTQ5LjcsNzBMMjAuMSw5OS44IiBjbGFzcz0ic3QxIi8+JiN4YTsJPC9nPiYjeGE7CTxnPiYjeGE7CQk8cGF0aCBkPSJNOTcuNyw5Ny40TDY4LDY3LjkiIGNsYXNzPSJzdDEiLz4mI3hhOwk8L2c+JiN4YTsJPGc+JiN4YTsJCTxwYXRoIGQ9Ik03MC40LDQ5LjdMOTkuOSwyMCIgY2xhc3M9InN0MSIvPiYjeGE7CTwvZz4mI3hhOwk8cGF0aCBkPSJNMjIuMywyMi4zTDUyLDUxLjkiIGNsYXNzPSJzdDEiLz4mI3hhOwk8cGF0aCBkPSJNMjAuMSwzMy45bDAtMTAuN2MwLTEuOCwxLjMtMywzLjEtMy4xbDEwLjgsMCIgY2xhc3M9InN0MSIvPiYjeGE7CTxwYXRoIGQ9Ik0zOC40LDY4bDEwLjcsMGMxLjgsMCwzLDEuMywzLjEsMy4xbDAsMTAuOCIgY2xhc3M9InN0MSIvPiYjeGE7CTxwYXRoIGQ9Ik05OS44LDg2LjJsMCwxMC43YzAsMS44LTEuMywzLTMuMSwzLjFsLTEwLjgsMCIgY2xhc3M9InN0MSIvPiYjeGE7CTxwYXRoIGQ9Ik04MS44LDUxLjlsLTEwLjcsMGMtMS44LDAtMy0xLjMtMy4xLTMuMUw2OCwzOCIgY2xhc3M9InN0MSIvPiYjeGE7PC9nPiYjeGE7PC9zdmc+', + pe: 'data:image/svg+xml,PHN2ZyB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWw6c3BhY2U9InByZXNlcnZlIiBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCAxMjAgMTIwO2VkaXRhYmxlQ3NzUnVsZXM9Lio7IiB2aWV3Qm94PSIwIDAgMTIwIDEyMCIgeT0iMHB4IiB4PSIwcHgiIGlkPSJMYXllcl8xIiB2ZXJzaW9uPSIxLjEiPiYjeGE7PHN0eWxlIHR5cGU9InRleHQvY3NzIj4uc3QwIHsgZmlsbDogcmdiKDEsIDkwLCAyNTUpOyB9IC5zdDEgeyBmaWxsOiBub25lOyBzdHJva2U6IHJnYigyNTUsIDI1NSwgMjU1KTsgc3Ryb2tlLXdpZHRoOiA0OyBzdHJva2UtbGluZWNhcDogcm91bmQ7IHN0cm9rZS1saW5lam9pbjogcm91bmQ7IHN0cm9rZS1taXRlcmxpbWl0OiAxMDsgfSAuc3QyIHsgZmlsbDogcmdiKDI1NSwgMjU1LCAyNTUpOyB9IC5zdDMgeyBmaWxsOiBub25lOyBzdHJva2U6IHJnYigyNTUsIDI1NSwgMjU1KTsgc3Ryb2tlLXdpZHRoOiA0OyBzdHJva2UtbWl0ZXJsaW1pdDogMTA7IH0gLnN0NCB7IGZpbGw6IG5vbmU7IHN0cm9rZTogcmdiKDI1NSwgMjU1LCAyNTUpOyBzdHJva2Utd2lkdGg6IDQ7IHN0cm9rZS1saW5lY2FwOiByb3VuZDsgc3Ryb2tlLWxpbmVqb2luOiByb3VuZDsgfSAuc3Q1IHsgZmlsbDogcmdiKDI1NSwgMjU1LCAyNTUpOyBzdHJva2U6IHJnYigyNTUsIDI1NSwgMjU1KTsgc3Ryb2tlLXdpZHRoOiA0OyBzdHJva2UtbGluZWNhcDogcm91bmQ7IHN0cm9rZS1saW5lam9pbjogcm91bmQ7IHN0cm9rZS1taXRlcmxpbWl0OiAxMDsgfSAuc3Q2IHsgZmlsbDogbm9uZTsgc3Ryb2tlOiByZ2IoMjU1LCAyNTUsIDI1NSk7IHN0cm9rZS13aWR0aDogNC4yMzMzOyBzdHJva2UtbGluZWNhcDogcm91bmQ7IHN0cm9rZS1saW5lam9pbjogcm91bmQ7IHN0cm9rZS1taXRlcmxpbWl0OiAxMDsgfSAuc3Q3IHsgZmlsbDogbm9uZTsgc3Ryb2tlOiByZ2IoMjU1LCAyNTUsIDI1NSk7IHN0cm9rZS13aWR0aDogNDsgc3Ryb2tlLWxpbmVjYXA6IHJvdW5kOyBzdHJva2UtbWl0ZXJsaW1pdDogMTA7IH0gLnN0OCB7IGZpbGw6IHJnYigyNTUsIDI1NSwgMjU1KTsgc3Ryb2tlOiByZ2IoMjU1LCAyNTUsIDI1NSk7IHN0cm9rZS13aWR0aDogNDsgc3Ryb2tlLW1pdGVybGltaXQ6IDEwOyB9IC5zdDkgeyBmaWxsOiByZ2IoMjU1LCAyNTUsIDI1NSk7IHN0cm9rZTogcmdiKDI1NSwgMjU1LCAyNTUpOyBzdHJva2Utd2lkdGg6IDQ7IH0gLnN0MTAgeyBmaWxsOiBub25lOyBzdHJva2U6IHJnYigyNTUsIDI1NSwgMjU1KTsgc3Ryb2tlLXdpZHRoOiA0OyB9IC5zdDExIHsgZmlsbDogcmdiKDM4LCAzOCwgMzgpOyBzdHJva2U6IHJnYigyNTUsIDI1NSwgMjU1KTsgc3Ryb2tlLXdpZHRoOiA0LjIzMzM7IH0gLnN0MTIgeyBmaWxsLXJ1bGU6IGV2ZW5vZGQ7IGNsaXAtcnVsZTogZXZlbm9kZDsgZmlsbDogbm9uZTsgc3Ryb2tlOiByZ2IoMjU1LCAyNTUsIDI1NSk7IHN0cm9rZS13aWR0aDogNDsgc3Ryb2tlLW1pdGVybGltaXQ6IDEwOyB9IC5zdDEzIHsgZmlsbC1ydWxlOiBldmVub2RkOyBjbGlwLXJ1bGU6IGV2ZW5vZGQ7IGZpbGw6IHJnYigyNTUsIDI1NSwgMjU1KTsgc3Ryb2tlOiByZ2IoMjU1LCAyNTUsIDI1NSk7IHN0cm9rZS13aWR0aDogNDsgfSAuc3QxNCB7IGZpbGw6IG5vbmU7IHN0cm9rZTogcmdiKDI1NSwgMjU1LCAyNTUpOyBzdHJva2Utd2lkdGg6IDQuMjMzMzsgc3Ryb2tlLWxpbmVjYXA6IHJvdW5kOyBzdHJva2UtbGluZWpvaW46IHJvdW5kOyB9IC5zdDE1IHsgZmlsbDogbm9uZTsgc3Ryb2tlOiByZ2IoMjU1LCAyNTUsIDI1NSk7IHN0cm9rZS13aWR0aDogNDsgc3Ryb2tlLWxpbmVjYXA6IHJvdW5kOyB9IC5zdDE2IHsgZmlsbDogcmdiKDI1NSwgMjU1LCAyNTUpOyBzdHJva2U6IHJnYigyNTUsIDI1NSwgMjU1KTsgc3Ryb2tlLW1pdGVybGltaXQ6IDEwOyB9IC5zdDE3IHsgZmlsbDogcmdiKDM4LCAzOCwgMzgpOyBzdHJva2U6IHJnYigyNTUsIDI1NSwgMjU1KTsgc3Ryb2tlLXdpZHRoOiA0OyBzdHJva2UtbWl0ZXJsaW1pdDogMTA7IH0gLnN0MTggeyBmaWxsOiByZ2IoMjU1LCAyNTUsIDI1NSk7IHN0cm9rZTogcmdiKDI1NSwgMjU1LCAyNTUpOyBzdHJva2Utd2lkdGg6IDQ7IHN0cm9rZS1saW5lY2FwOiByb3VuZDsgc3Ryb2tlLWxpbmVqb2luOiByb3VuZDsgfSA8L3N0eWxlPiYjeGE7PHJlY3QgaGVpZ2h0PSIxMjAiIHdpZHRoPSIxMjAiIGNsYXNzPSJzdDAiLz4mI3hhOzxnPiYjeGE7CTxnPiYjeGE7CQk8cGF0aCBkPSJNNzEuNywxOS43VjQ4aDI4IiBjbGFzcz0ic3QxIi8+JiN4YTsJCTxwYXRoIGQ9Ik05MS4yLDM4LjVsNy41LDcuNmMxLjMsMS4zLDEuMywzLjEsMCw0LjNMOTEuMSw1OCIgY2xhc3M9InN0MSIvPiYjeGE7CTwvZz4mI3hhOwk8Zz4mI3hhOwkJPHBhdGggZD0iTTIwLDQ3LjhoMjguNHYtMjgiIGNsYXNzPSJzdDEiLz4mI3hhOwkJPHBhdGggZD0iTTM4LjgsMjguM2w3LjYtNy41YzEuMy0xLjMsMy4xLTEuMyw0LjMsMGw3LjcsNy42IiBjbGFzcz0ic3QxIi8+JiN4YTsJPC9nPiYjeGE7CTxnPiYjeGE7CQk8cGF0aCBkPSJNNDgsMTAwLjNWNzJIMjAiIGNsYXNzPSJzdDEiLz4mI3hhOwkJPHBhdGggZD0iTTI4LjUsODEuNUwyMSw3My45Yy0xLjMtMS4zLTEuMy0zLjEsMC00LjNsNy42LTcuNyIgY2xhc3M9InN0MSIvPiYjeGE7CTwvZz4mI3hhOwk8Zz4mI3hhOwkJPHBhdGggZD0iTTEwMCw3MS45SDcxLjZ2MjgiIGNsYXNzPSJzdDEiLz4mI3hhOwkJPHBhdGggZD0iTTgxLjIsOTEuNGwtNy42LDcuNWMtMS4zLDEuMy0zLjEsMS4zLTQuMywwbC03LjctNy42IiBjbGFzcz0ic3QxIi8+JiN4YTsJPC9nPiYjeGE7PC9nPiYjeGE7PC9zdmc+', + controller: 'data:image/svg+xml,PHN2ZyB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiBvdmVyZmxvdz0iaGlkZGVuIiB4bWw6c3BhY2U9InByZXNlcnZlIiBoZWlnaHQ9IjU4IiB3aWR0aD0iNTkiIHZpZXdCb3g9IjAgMCA1OSA1OCI+PGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTQxNyAtMTg0KSI+PGc+PGc+PGc+PGc+PHBhdGggZmlsbC1vcGFjaXR5PSIxIiBmaWxsLXJ1bGU9Im5vbnplcm8iIGZpbGw9IiMwMDVBRkYiIGQ9Ik00MTggMTg1IDQ3NSAxODUgNDc1IDI0MiA0MTggMjQyWiIvPjxwYXRoIGZpbGwtb3BhY2l0eT0iMSIgZmlsbC1ydWxlPSJub256ZXJvIiBmaWxsPSIjMDA1QUZGIiBzdHJva2Utb3BhY2l0eT0iMSIgc3Ryb2tlLW1pdGVybGltaXQ9IjEwIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS13aWR0aD0iMS45IiBzdHJva2U9IiNGRkZGRkYiIGQ9Ik00NTYgMjAwLjEwNUM0NTEuMDYgMTk2LjU5IDQ0NC4zNjIgMTk1Ljk3MyA0MzguNzEgMTk5LjA2IDQzMy41MzMgMjAxLjg2MyA0MzAuNDQ1IDIwNy4wNCA0MzAuMTYgMjEyLjU1Ii8+PHBhdGggZmlsbC1vcGFjaXR5PSIxIiBmaWxsLXJ1bGU9Im5vbnplcm8iIGZpbGw9IiMwMDVBRkYiIHN0cm9rZS1vcGFjaXR5PSIxIiBzdHJva2UtbWl0ZXJsaW1pdD0iMTAiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLXdpZHRoPSIxLjkiIHN0cm9rZT0iI0ZGRkZGRiIgZD0iTTQzNyAyMjYuODQ4QzQ0MS45NCAyMzAuMzE1IDQ0OC41OSAyMzAuOTggNDU0LjI5IDIyNy44OTMgNDU5LjQ2NyAyMjUuMDkgNDYyLjU1NSAyMTkuODY1IDQ2Mi44NCAyMTQuNDAyIi8+PHBhdGggZmlsbC1vcGFjaXR5PSIxIiBmaWxsLXJ1bGU9Im5vbnplcm8iIGZpbGw9IiMwMDVBRkYiIHN0cm9rZS1vcGFjaXR5PSIxIiBzdHJva2UtbWl0ZXJsaW1pdD0iMTAiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLXdpZHRoPSIxLjkiIHN0cm9rZT0iI0ZGRkZGRiIgZD0iTTQ1MC45NjUgMjAyLjU3NSA0NTUuMzM1IDIwMC44MThDNDU2LjA5NSAyMDAuNTMzIDQ1Ni40MjcgMTk5LjgyIDQ1Ni4xOSAxOTkuMDEyTDQ1NC44NiAxOTQuMzEiLz48cGF0aCBmaWxsLW9wYWNpdHk9IjEiIGZpbGwtcnVsZT0ibm9uemVybyIgZmlsbD0iIzAwNUFGRiIgc3Ryb2tlLW9wYWNpdHk9IjEiIHN0cm9rZS1taXRlcmxpbWl0PSIxMCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2Utd2lkdGg9IjEuOSIgc3Ryb2tlPSIjRkZGRkZGIiBkPSJNNDQxLjk4NyAyMjQuNDI1IDQzNy42MTcgMjI2LjE4MkM0MzYuODU4IDIyNi40NjcgNDM2LjUyNSAyMjcuMTggNDM2Ljc2MyAyMjcuOTg4TDQzOC4wOTIgMjMyLjY5Ii8+PHBhdGggZmlsbC1vcGFjaXR5PSIxIiBmaWxsLXJ1bGU9Im5vbnplcm8iIGZpbGw9IiMwMDVBRkYiIHN0cm9rZS1vcGFjaXR5PSIxIiBzdHJva2UtbWl0ZXJsaW1pdD0iMTAiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLXdpZHRoPSIxLjkiIHN0cm9rZT0iI0ZGRkZGRiIgZD0iTTQzNC4zODggMjIwLjQzNUM0MzQuMzg4IDIyMS45MyA0MzMuMTc1IDIyMy4xNDMgNDMxLjY4IDIyMy4xNDMgNDMwLjE4NSAyMjMuMTQzIDQyOC45NzMgMjIxLjkzIDQyOC45NzMgMjIwLjQzNSA0MjguOTczIDIxOC45NCA0MzAuMTg1IDIxNy43MjcgNDMxLjY4IDIxNy43MjcgNDMzLjE3NSAyMTcuNzI3IDQzNC4zODggMjE4Ljk0IDQzNC4zODggMjIwLjQzNVoiLz48cGF0aCBmaWxsLW9wYWNpdHk9IjEiIGZpbGwtcnVsZT0ibm9uemVybyIgZmlsbD0iIzAwNUFGRiIgc3Ryb2tlLW9wYWNpdHk9IjEiIHN0cm9rZS1taXRlcmxpbWl0PSIxMCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2Utd2lkdGg9IjEuOSIgc3Ryb2tlPSIjRkZGRkZGIiBkPSJNNDY0LjAyNyAyMDYuNDIzQzQ2NC4wMjcgMjA3LjkxOCA0NjIuODE1IDIwOS4xMyA0NjEuMzIgMjA5LjEzIDQ1OS44MjUgMjA5LjEzIDQ1OC42MTMgMjA3LjkxOCA0NTguNjEzIDIwNi40MjMgNDU4LjYxMyAyMDQuOTI3IDQ1OS44MjUgMjAzLjcxNSA0NjEuMzIgMjAzLjcxNSA0NjIuODE1IDIwMy43MTUgNDY0LjAyNyAyMDQuOTI3IDQ2NC4wMjcgMjA2LjQyM1oiLz48L2c+PC9nPjwvZz48L2c+PC9nPjwvc3ZnPg==', + pon: 'data:image/svg+xml,PHN2ZyB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiBvdmVyZmxvdz0iaGlkZGVuIiB4bWw6c3BhY2U9InByZXNlcnZlIiBoZWlnaHQ9IjQ4MCIgd2lkdGg9IjQ4MiIgdmlld0JveD0iMCAwIDQ4MiA0ODAiPjxnIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMTQgLTQpIj48Zz48Zz48Zz48Zz48cGF0aCBmaWxsLW9wYWNpdHk9IjEiIGZpbGwtcnVsZT0ibm9uemVybyIgZmlsbD0iIzAwNUFGRiIgZD0iTTIxNSA0IDY5NSA0IDY5NSA0ODQgMjE1IDQ4NFoiLz48cGF0aCBmaWxsLW9wYWNpdHk9IjEiIGZpbGwtcnVsZT0ibm9uemVybyIgZmlsbD0iIzAwNUFGRiIgc3Ryb2tlLW9wYWNpdHk9IjEiIHN0cm9rZS1taXRlcmxpbWl0PSIxMCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2Utd2lkdGg9IjE2IiBzdHJva2U9IiNGRkZGRkYiIGQ9Ik0yOTguNiA4NCA2MDMgMjQ0IDI5OC42IDQwNCIvPjxwYXRoIGZpbGwtcnVsZT0ibm9uemVybyIgZmlsbD0ibm9uZSIgc3Ryb2tlLW9wYWNpdHk9IjEiIHN0cm9rZS1taXRlcmxpbWl0PSIxMCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2Utd2lkdGg9IjE2IiBzdHJva2U9IiNGRkZGRkYiIGQ9Ik0yOTguNiAyNDQgNTEwLjIgMjQ0Ii8+PHBhdGggZmlsbC1vcGFjaXR5PSIxIiBmaWxsLXJ1bGU9Im5vbnplcm8iIGZpbGw9IiMwMDVBRkYiIHN0cm9rZS1vcGFjaXR5PSIxIiBzdHJva2UtbWl0ZXJsaW1pdD0iMTAiIHN0cm9rZS1saW5lam9pbj0ibWl0ZXIiIHN0cm9rZS1saW5lY2FwPSJidXR0IiBzdHJva2Utd2lkdGg9IjE2IiBzdHJva2U9IiNGRkZGRkYiIGQ9Ik02MDcuNCAyNDRDNjA3LjQgMjUwLjYyNyA2MDIuMDI3IDI1NiA1OTUuNCAyNTYgNTg4Ljc3MyAyNTYgNTgzLjQgMjUwLjYyNyA1ODMuNCAyNDQgNTgzLjQgMjM3LjM3MyA1ODguNzczIDIzMiA1OTUuNCAyMzIgNjAyLjAyNyAyMzIgNjA3LjQgMjM3LjM3MyA2MDcuNCAyNDRaIi8+PC9nPjwvZz48L2c+PC9nPjwvZz48L3N2Zz4=', + leaf: 'data:image/svg+xml,PHN2ZyB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWw6c3BhY2U9InByZXNlcnZlIiBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCAxMjAgMTIwOyIgdmlld0JveD0iMCAwIDEyMCAxMjAiIHk9IjBweCIgeD0iMHB4IiBpZD0iTGF5ZXJfMSIgdmVyc2lvbj0iMS4xIj4mI3hhOzxzdHlsZSB0eXBlPSJ0ZXh0L2NzcyI+LnN0MCB7IGZpbGw6IHJnYigwLCA5MCwgMjU1KTsgfSAuc3QxIHsgZmlsbDogbm9uZTsgc3Ryb2tlOiByZ2IoMjU1LCAyNTUsIDI1NSk7IHN0cm9rZS13aWR0aDogNDsgc3Ryb2tlLWxpbmVjYXA6IHJvdW5kOyBzdHJva2UtbGluZWpvaW46IHJvdW5kOyBzdHJva2UtbWl0ZXJsaW1pdDogMTA7IH0gLnN0MiB7IGZpbGw6IHJnYigyNTUsIDI1NSwgMjU1KTsgfSAuc3QzIHsgZmlsbDogbm9uZTsgc3Ryb2tlOiByZ2IoMjU1LCAyNTUsIDI1NSk7IHN0cm9rZS13aWR0aDogNDsgc3Ryb2tlLW1pdGVybGltaXQ6IDEwOyB9IC5zdDQgeyBmaWxsOiBub25lOyBzdHJva2U6IHJnYigyNTUsIDI1NSwgMjU1KTsgc3Ryb2tlLXdpZHRoOiA0OyBzdHJva2UtbGluZWNhcDogcm91bmQ7IHN0cm9rZS1saW5lam9pbjogcm91bmQ7IH0gLnN0NSB7IGZpbGw6IHJnYigyNTUsIDI1NSwgMjU1KTsgc3Ryb2tlOiByZ2IoMjU1LCAyNTUsIDI1NSk7IHN0cm9rZS13aWR0aDogNDsgc3Ryb2tlLWxpbmVjYXA6IHJvdW5kOyBzdHJva2UtbGluZWpvaW46IHJvdW5kOyBzdHJva2UtbWl0ZXJsaW1pdDogMTA7IH0gLnN0NiB7IGZpbGw6IG5vbmU7IHN0cm9rZTogcmdiKDI1NSwgMjU1LCAyNTUpOyBzdHJva2Utd2lkdGg6IDQuMjMzMzsgc3Ryb2tlLWxpbmVjYXA6IHJvdW5kOyBzdHJva2UtbGluZWpvaW46IHJvdW5kOyBzdHJva2UtbWl0ZXJsaW1pdDogMTA7IH0gLnN0NyB7IGZpbGw6IG5vbmU7IHN0cm9rZTogcmdiKDI1NSwgMjU1LCAyNTUpOyBzdHJva2Utd2lkdGg6IDQ7IHN0cm9rZS1saW5lY2FwOiByb3VuZDsgc3Ryb2tlLW1pdGVybGltaXQ6IDEwOyB9IC5zdDggeyBmaWxsOiByZ2IoMjU1LCAyNTUsIDI1NSk7IHN0cm9rZTogcmdiKDI1NSwgMjU1LCAyNTUpOyBzdHJva2Utd2lkdGg6IDQ7IHN0cm9rZS1taXRlcmxpbWl0OiAxMDsgfSAuc3Q5IHsgZmlsbDogcmdiKDI1NSwgMjU1LCAyNTUpOyBzdHJva2U6IHJnYigyNTUsIDI1NSwgMjU1KTsgc3Ryb2tlLXdpZHRoOiA0OyB9IC5zdDEwIHsgZmlsbDogbm9uZTsgc3Ryb2tlOiByZ2IoMjU1LCAyNTUsIDI1NSk7IHN0cm9rZS13aWR0aDogNDsgfSAuc3QxMSB7IGZpbGw6IHJnYigzOCwgMzgsIDM4KTsgc3Ryb2tlOiByZ2IoMjU1LCAyNTUsIDI1NSk7IHN0cm9rZS13aWR0aDogNC4yMzMzOyB9IC5zdDEyIHsgZmlsbC1ydWxlOiBldmVub2RkOyBjbGlwLXJ1bGU6IGV2ZW5vZGQ7IGZpbGw6IG5vbmU7IHN0cm9rZTogcmdiKDI1NSwgMjU1LCAyNTUpOyBzdHJva2Utd2lkdGg6IDQ7IHN0cm9rZS1taXRlcmxpbWl0OiAxMDsgfSAuc3QxMyB7IGZpbGwtcnVsZTogZXZlbm9kZDsgY2xpcC1ydWxlOiBldmVub2RkOyBmaWxsOiByZ2IoMjU1LCAyNTUsIDI1NSk7IHN0cm9rZTogcmdiKDI1NSwgMjU1LCAyNTUpOyBzdHJva2Utd2lkdGg6IDQ7IH0gLnN0MTQgeyBmaWxsOiBub25lOyBzdHJva2U6IHJnYigyNTUsIDI1NSwgMjU1KTsgc3Ryb2tlLXdpZHRoOiA0LjIzMzM7IHN0cm9rZS1saW5lY2FwOiByb3VuZDsgc3Ryb2tlLWxpbmVqb2luOiByb3VuZDsgfSAuc3QxNSB7IGZpbGw6IG5vbmU7IHN0cm9rZTogcmdiKDI1NSwgMjU1LCAyNTUpOyBzdHJva2Utd2lkdGg6IDQ7IHN0cm9rZS1saW5lY2FwOiByb3VuZDsgfSAuc3QxNiB7IGZpbGw6IHJnYigyNTUsIDI1NSwgMjU1KTsgc3Ryb2tlOiByZ2IoMjU1LCAyNTUsIDI1NSk7IHN0cm9rZS1taXRlcmxpbWl0OiAxMDsgfSAuc3QxNyB7IGZpbGw6IHJnYigzOCwgMzgsIDM4KTsgc3Ryb2tlOiByZ2IoMjU1LCAyNTUsIDI1NSk7IHN0cm9rZS13aWR0aDogNDsgc3Ryb2tlLW1pdGVybGltaXQ6IDEwOyB9IC5zdDE4IHsgZmlsbDogcmdiKDI1NSwgMjU1LCAyNTUpOyBzdHJva2U6IHJnYigyNTUsIDI1NSwgMjU1KTsgc3Ryb2tlLXdpZHRoOiA0OyBzdHJva2UtbGluZWNhcDogcm91bmQ7IHN0cm9rZS1saW5lam9pbjogcm91bmQ7IH0gPC9zdHlsZT4mI3hhOzxyZWN0IGhlaWdodD0iMTIwIiB3aWR0aD0iMTIwIiBjbGFzcz0ic3QwIi8+JiN4YTs8Zz4mI3hhOwk8cGF0aCBkPSJNOTEuNSwyNy4zbDcuNiw3LjZjMS4zLDEuMywxLjMsMy4xLDAsNC4zbC03LjYsNy43IiBjbGFzcz0ic3QxIi8+JiN4YTsJPHBhdGggZD0iTTI4LjUsNDYuOWwtNy42LTcuNmMtMS4zLTEuMy0xLjMtMy4xLDAtNC4zbDcuNi03LjciIGNsYXNzPSJzdDEiLz4mI3hhOwk8cGF0aCBkPSJNOTEuNSw3My4xbDcuNiw3LjZjMS4zLDEuMywxLjMsMy4xLDAsNC4zbC03LjYsNy43IiBjbGFzcz0ic3QxIi8+JiN4YTsJPHBhdGggZD0iTTI4LjUsOTIuN2wtNy42LTcuNmMtMS4zLTEuMy0xLjMtMy4xLDAtNC4zbDcuNi03LjciIGNsYXNzPSJzdDEiLz4mI3hhOwk8Zz4mI3hhOwkJPHBhdGggZD0iTTk2LjYsMzYuOEg2Ny45bC0xNiw0NS45SDIzLjIiIGNsYXNzPSJzdDEiLz4mI3hhOwkJPHBhdGggZD0iTTk2LjYsODIuN0g2Ny45bC0xNi00NS45SDIzLjIiIGNsYXNzPSJzdDEiLz4mI3hhOwk8L2c+JiN4YTs8L2c+JiN4YTs8L3N2Zz4=', + spine: 'data:image/svg+xml,PHN2ZyB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWw6c3BhY2U9InByZXNlcnZlIiBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCAxMjAgMTIwOyIgdmlld0JveD0iMCAwIDEyMCAxMjAiIHk9IjBweCIgeD0iMHB4IiBpZD0iTGF5ZXJfMSIgdmVyc2lvbj0iMS4xIj4mI3hhOzxzdHlsZSB0eXBlPSJ0ZXh0L2NzcyI+LnN0MCB7IGZpbGw6IHJnYigwLCA5MCwgMjU1KTsgfSAuc3QxIHsgZmlsbDogbm9uZTsgc3Ryb2tlOiByZ2IoMjU1LCAyNTUsIDI1NSk7IHN0cm9rZS13aWR0aDogNDsgc3Ryb2tlLWxpbmVjYXA6IHJvdW5kOyBzdHJva2UtbGluZWpvaW46IHJvdW5kOyBzdHJva2UtbWl0ZXJsaW1pdDogMTA7IH0gLnN0MiB7IGZpbGw6IHJnYigyNTUsIDI1NSwgMjU1KTsgfSAuc3QzIHsgZmlsbDogbm9uZTsgc3Ryb2tlOiByZ2IoMjU1LCAyNTUsIDI1NSk7IHN0cm9rZS13aWR0aDogNDsgc3Ryb2tlLW1pdGVybGltaXQ6IDEwOyB9IC5zdDQgeyBmaWxsOiBub25lOyBzdHJva2U6IHJnYigyNTUsIDI1NSwgMjU1KTsgc3Ryb2tlLXdpZHRoOiA0OyBzdHJva2UtbGluZWNhcDogcm91bmQ7IHN0cm9rZS1saW5lam9pbjogcm91bmQ7IH0gLnN0NSB7IGZpbGw6IHJnYigyNTUsIDI1NSwgMjU1KTsgc3Ryb2tlOiByZ2IoMjU1LCAyNTUsIDI1NSk7IHN0cm9rZS13aWR0aDogNDsgc3Ryb2tlLWxpbmVjYXA6IHJvdW5kOyBzdHJva2UtbGluZWpvaW46IHJvdW5kOyBzdHJva2UtbWl0ZXJsaW1pdDogMTA7IH0gLnN0NiB7IGZpbGw6IG5vbmU7IHN0cm9rZTogcmdiKDI1NSwgMjU1LCAyNTUpOyBzdHJva2Utd2lkdGg6IDQuMjMzMzsgc3Ryb2tlLWxpbmVjYXA6IHJvdW5kOyBzdHJva2UtbGluZWpvaW46IHJvdW5kOyBzdHJva2UtbWl0ZXJsaW1pdDogMTA7IH0gLnN0NyB7IGZpbGw6IG5vbmU7IHN0cm9rZTogcmdiKDI1NSwgMjU1LCAyNTUpOyBzdHJva2Utd2lkdGg6IDQ7IHN0cm9rZS1saW5lY2FwOiByb3VuZDsgc3Ryb2tlLW1pdGVybGltaXQ6IDEwOyB9IC5zdDggeyBmaWxsOiByZ2IoMjU1LCAyNTUsIDI1NSk7IHN0cm9rZTogcmdiKDI1NSwgMjU1LCAyNTUpOyBzdHJva2Utd2lkdGg6IDQ7IHN0cm9rZS1taXRlcmxpbWl0OiAxMDsgfSAuc3Q5IHsgZmlsbDogcmdiKDI1NSwgMjU1LCAyNTUpOyBzdHJva2U6IHJnYigyNTUsIDI1NSwgMjU1KTsgc3Ryb2tlLXdpZHRoOiA0OyB9IC5zdDEwIHsgZmlsbDogbm9uZTsgc3Ryb2tlOiByZ2IoMjU1LCAyNTUsIDI1NSk7IHN0cm9rZS13aWR0aDogNDsgfSAuc3QxMSB7IGZpbGw6IHJnYigzOCwgMzgsIDM4KTsgc3Ryb2tlOiByZ2IoMjU1LCAyNTUsIDI1NSk7IHN0cm9rZS13aWR0aDogNC4yMzMzOyB9IC5zdDEyIHsgZmlsbC1ydWxlOiBldmVub2RkOyBjbGlwLXJ1bGU6IGV2ZW5vZGQ7IGZpbGw6IG5vbmU7IHN0cm9rZTogcmdiKDI1NSwgMjU1LCAyNTUpOyBzdHJva2Utd2lkdGg6IDQ7IHN0cm9rZS1taXRlcmxpbWl0OiAxMDsgfSAuc3QxMyB7IGZpbGwtcnVsZTogZXZlbm9kZDsgY2xpcC1ydWxlOiBldmVub2RkOyBmaWxsOiByZ2IoMjU1LCAyNTUsIDI1NSk7IHN0cm9rZTogcmdiKDI1NSwgMjU1LCAyNTUpOyBzdHJva2Utd2lkdGg6IDQ7IH0gLnN0MTQgeyBmaWxsOiBub25lOyBzdHJva2U6IHJnYigyNTUsIDI1NSwgMjU1KTsgc3Ryb2tlLXdpZHRoOiA0LjIzMzM7IHN0cm9rZS1saW5lY2FwOiByb3VuZDsgc3Ryb2tlLWxpbmVqb2luOiByb3VuZDsgfSAuc3QxNSB7IGZpbGw6IG5vbmU7IHN0cm9rZTogcmdiKDI1NSwgMjU1LCAyNTUpOyBzdHJva2Utd2lkdGg6IDQ7IHN0cm9rZS1saW5lY2FwOiByb3VuZDsgfSAuc3QxNiB7IGZpbGw6IHJnYigyNTUsIDI1NSwgMjU1KTsgc3Ryb2tlOiByZ2IoMjU1LCAyNTUsIDI1NSk7IHN0cm9rZS1taXRlcmxpbWl0OiAxMDsgfSAuc3QxNyB7IGZpbGw6IHJnYigzOCwgMzgsIDM4KTsgc3Ryb2tlOiByZ2IoMjU1LCAyNTUsIDI1NSk7IHN0cm9rZS13aWR0aDogNDsgc3Ryb2tlLW1pdGVybGltaXQ6IDEwOyB9IC5zdDE4IHsgZmlsbDogcmdiKDI1NSwgMjU1LCAyNTUpOyBzdHJva2U6IHJnYigyNTUsIDI1NSwgMjU1KTsgc3Ryb2tlLXdpZHRoOiA0OyBzdHJva2UtbGluZWNhcDogcm91bmQ7IHN0cm9rZS1saW5lam9pbjogcm91bmQ7IH0gLnN0MTkgeyBmaWxsOiByZ2IoMCwgMTcsIDUzKTsgc3Ryb2tlOiByZ2IoMjU1LCAyNTUsIDI1NSk7IHN0cm9rZS13aWR0aDogNDsgc3Ryb2tlLWxpbmVjYXA6IHJvdW5kOyBzdHJva2UtbGluZWpvaW46IHJvdW5kOyBzdHJva2UtbWl0ZXJsaW1pdDogMTA7IH0gPC9zdHlsZT4mI3hhOzxyZWN0IGhlaWdodD0iMTIwIiB3aWR0aD0iMTIwIiBjbGFzcz0ic3QwIiB5PSIwIi8+JiN4YTs8cmVjdCBoZWlnaHQ9IjEyMCIgd2lkdGg9IjEyMCIgY2xhc3M9InN0MCIvPiYjeGE7PGc+JiN4YTsJPGc+JiN4YTsJCTxwYXRoIGQ9Ik05OCwzMC4xSDY4TDUyLDg5LjlIMjIiIGNsYXNzPSJzdDEiLz4mI3hhOwkJPHBhdGggZD0iTTI4LDEwMGwtNy04LjFjLTEuMy0xLjMtMS4zLTMuMSwwLTQuM2w3LTcuNiIgY2xhc3M9InN0MSIvPiYjeGE7CQk8cGF0aCBkPSJNOTIsMjBsNyw4LjFjMS4zLDEuMywxLjMsMy4xLDAsNC4zTDkyLDQwIiBjbGFzcz0ic3QxIi8+JiN4YTsJPC9nPiYjeGE7CTxwYXRoIGQ9Ik05OCw4OS45SDY0IiBjbGFzcz0ic3QxIi8+JiN4YTsJPHBhdGggZD0iTTkyLDgwbDcsNy42YzEuMywxLjMsMS4zLDMuMSwwLDQuM2wtNyw4LjEiIGNsYXNzPSJzdDEiLz4mI3hhOwk8cGF0aCBkPSJNNTYsMzAuMUgyMiBNMjgsNDBsLTctNy42Yy0xLjMtMS4zLTEuMy0zLjEsMC00LjNsNy04LjEiIGNsYXNzPSJzdDEiLz4mI3hhOwk8bGluZSB5Mj0iNjAiIHgyPSI3MiIgeTE9IjYwIiB4MT0iMTAwIiBjbGFzcz0ic3QxIi8+JiN4YTsJPGxpbmUgeTI9IjYwIiB4Mj0iNDgiIHkxPSI2MCIgeDE9IjIwIiBjbGFzcz0ic3QxIi8+JiN4YTs8L2c+JiN4YTs8L3N2Zz4=', + 'super-spine': 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMDAiIGhlaWdodD0iMTAwIj48cGF0aCBkPSJNMTAsMTAgTDkwLDkwIiBzdHlsZT0iZmlsbDojZmYwMGYwOyIgLz48L3N2Zz4=', + }; + + const canvasElement = document.querySelector('#cy canvas[data-id="layer2-node"]'); + const drawIoWidth = canvasElement.width / 10; + const drawIoHeight = canvasElement.height / 10; + + const mxGraphHeader = ` + + + `; + + const mxGraphFooter = ` + `; + + const mxCells = []; + + function createMxCellForNode(node, imageURL) { + if (node.isParent()) { + console.log("createMxCellForNode - node.isParent()",node.isParent() ); + // Use a tiny transparent SVG as a placeholder for the image + return ` + + + `; + } else if (!node.data("id").includes("statusGreen") && !node.data("id").includes("statusRed")) { + return ` + + + `; + } + } + + cy.nodes().forEach(function(node) { + const svgBase64 = svgBase64ByRole[node.data("topoViewerRole")] || (node.isParent() ? 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMSIgaGVpZ2h0PSIxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciLz4=' : null); + + if (svgBase64) { + // Add parent nodes at the start of the array for bottom-layer rendering + if (node.isParent()) { + mxCells.unshift(createMxCellForNode(node, svgBase64)); + } else { + // Add non-parent nodes at the end of the array + mxCells.push(createMxCellForNode(node, svgBase64)); + } + } + }); + + + cy.edges().forEach(function(edge) { + mxCells.push(` + + + + + + + + + + + + + `); + }); + + // Combine all parts and create XML + const mxGraphXML = mxGraphHeader + mxCells.join("") + mxGraphFooter; + + // Create a Blob from the XML + const blob = new Blob([mxGraphXML], { type: "application/xml" }); + + // Create a URL for the Blob + const url = window.URL.createObjectURL(blob); + + // Create a download link and trigger a click event + const a = document.createElement("a"); + a.style.display = "none"; + a.href = url; + a.download = "filename.drawio"; + document.body.appendChild(a); + + bulmaToast.toast({ + message: `Brace yourselves for a quick snapshot, folks! 📸 Capturing the viewport in 3... 2... 1... 🚀💥`, + type: "is-warning is-size-6 p-3", + duration: 2000, + position: "top-center", + closeOnClick: true, + }); + await sleep(2000); + + // Simulate a click to trigger the download + a.click(); + + // Clean up by revoking the URL and removing the download link + window.URL.revokeObjectURL(url); + document.body.removeChild(a); +} + +async function getYamlTopoContent(yamlTopoContent) { + + try { + // Check if yamlTopoContent is already set + console.log('YAML Topo Initial Content:', yamlTopoContent); + + if (!yamlTopoContent) { + // Load the content if yamlTopoContent is empty + yamlTopoContent = await sendRequestToEndpointGetV3("/get-yaml-topo-content"); + } + + console.log('YAML Topo Content:', yamlTopoContent); + document.getElementById('panel-clab-editor-text-area').value = yamlTopoContent; + + + } catch (error) { + console.error("Error occurred:", error); + // Handle errors as needed + } +} + + + +// aarafat-tag: +//// REFACTOR END + +// logMessagesPanel manager +///-logMessagesPanel Function to append message function +function appendMessage(message) { + // const textarea = document.getElementById('notificationTextarea'); + const textarea = document.getElementById("notificationTextarea"); + + // Get the current date and time + const timestamp = new Date().toLocaleString(); + + textarea.value += `[${timestamp}] ${message}\n`; + textarea.scrollTop = textarea.scrollHeight; +} + +function nodeFindDrawer(cy) { + //- Get a reference to your Cytoscape instance (assuming it's named 'cy') + //- const cy = window.cy; //- Replace 'window.cy' with your actual Cytoscape instance + //- Find the node with the specified name + //- Get a reference to your Cytoscape instance (assuming it's named 'cy') + //- const cy = window.cy; //- Replace 'window.cy' with your actual Cytoscape instance + //- Find the node with the specified name + const nodeName = document.getElementById( + "panelBlock-viewportButtons-buttonfindNode-divPanelBlock-columnContainerlabelFindNodeNodeName-panelContentlabelFindNodeNodeName-columnsPanelContentlabelFindNodeNodeName-labelColumnlabelFindNodeNodeName-inputColumnlabelFindNodeNodeName-labellabelFindNodeNodeName", + ).value; + + const node = cy.$(`node[name = "${nodeName}"]`); + //- Check if the node exists + //- Check if the node exists + if (node.length > 0) { + // console + // console + console.log("Info: " + 'Sweet! Node "' + nodeName + '" is in the house.'); + appendMessage("Info: " + 'Sweet! Node "' + nodeName + '" is in the house.'); + //- Apply a highlight style to the node + //- Apply a highlight style to the node + node.style({ + "border-color": "red", + "border-width": "2px", + "background-color": "yellow", + }); + //- Zoom out on the node + //- Zoom out on the node + cy.fit(); + //- Zoom in on the node + //- Zoom in on the node + cy.animate({ + zoom: { + level: 5, + position: { + x: node.position("x"), + y: node.position("y"), + }, + renderedPosition: { + x: node.renderedPosition("x"), + y: node.renderedPosition("y"), + }, + }, + duration: 1500, + }); + } else { + console.error( + `Bro, I couldn't find a node named "${nodeName}". Try another one.`, + ); + appendMessage( + `Bro, I couldn't find a node named "${nodeName}". Try another one.`, + ); + } +} + +function pathFinderDijkstraDrawer(cy) { + // Usage example: + // highlightShortestPath('node-a', 'node-b'); // Replace with your source and target node IDs + //- Function to get the default node style from cy-style.json + //- weight: (edge) => 1, // You can adjust the weight function if needed + //- weight: (edge) => edge.data('distance') + // Usage example: + // highlightShortestPath('node-a', 'node-b'); // Replace with your source and target node IDs + //- Function to get the default node style from cy-style.json + //- weight: (edge) => 1, // You can adjust the weight function if needed + //- weight: (edge) => edge.data('distance') + + console.log("im triggered"); + + // Remove existing highlight from all edges + // Remove existing highlight from all edges + cy.edges().forEach((edge) => { + edge.removeClass("spf"); + }); + + // Get the node sourceNodeId from pathFinderSourceNodeInput and targetNodeId from pathFinderTargetNodeInput + // Get the node sourceNodeId from pathFinderSourceNodeInput and targetNodeId from pathFinderTargetNodeInput + const sourceNodeId = document.getElementById( + "panelBlock-viewportButtons-buttonfindRoute-divPanelBlock-columnContainerlabelFindRouteSource-panelContentlabelFindRouteSource-columnsPanelContentlabelFindRouteSource-labelColumnlabelFindRouteSource-inputColumnlabelFindRouteSource-labellabelFindRouteSource", + ).value; + const targetNodeId = document.getElementById( + "panelBlock-viewportButtons-buttonfindRoute-divPanelBlock-columnContainerlabelFindRouteTarget-panelContentlabelFindRouteTarget-columnsPanelContentlabelFindRouteTarget-labelColumnlabelFindRouteTarget-inputColumnlabelFindRouteTarget-labellabelFindRouteTarget", + ).value; + + // Assuming you have 'cy' as your Cytoscape instance + // Assuming you have 'cy' as your Cytoscape instance + const sourceNode = cy.$(`node[id="${sourceNodeId}"]`); + const targetNode = cy.$(`node[id="${targetNodeId}"]`); + + console.log( + "Info: " + + "Let's find the path from-" + + sourceNodeId + + "-to-" + + targetNodeId + + "!", + ); + appendMessage( + "Info: " + + "Let's find the path from-" + + sourceNodeId + + "-to-" + + targetNodeId + + "!", + ); + + // Check if both nodes exist + // Check if both nodes exist + if (sourceNode.length === 0 || targetNode.length === 0) { + console.error( + `Bro, couldn't find the source or target node you specified. Double-check the node names.`, + ); + appendMessage( + `Bro, couldn't find the source or target node you specified. Double-check the node names.`, + ); + return; + } + + // Get the Dijkstra result with the shortest path + // Get the Dijkstra result with the shortest path + const dijkstraResult = cy.elements().dijkstra({ + root: sourceNode, + weight: (edge) => 1, + // Use the custom weight attribute + // weight: edge => edge.data('customWeight'), + // Use the custom weight attribute + // weight: edge => edge.data('customWeight'), + }); + // Get the shortest path from Dijkstra result + // Get the shortest path from Dijkstra result + const shortestPathEdges = dijkstraResult.pathTo(targetNode); + console.log(shortestPathEdges); + + // Check if there is a valid path (shortestPathEdges is not empty) + // Check if there is a valid path (shortestPathEdges is not empty) + if (shortestPathEdges.length > 1) { + // Highlight the shortest path + // Highlight the shortest path + shortestPathEdges.forEach((edge) => { + edge.addClass("spf"); + }); + + //- Zoom out on the node + //- Zoom out on the node + cy.fit(); + + //- Zoom in on the node + //- Zoom in on the node + cy.animate({ + zoom: { + level: 5, + position: { + x: sourceNode.position("x"), + y: sourceNode.position("y"), + }, + renderedPosition: { + x: sourceNode.renderedPosition("x"), + y: sourceNode.renderedPosition("y"), + }, + }, + duration: 1500, + }); + // throw log + // throw log + console.log( + "Info: " + + "Yo, check it out! Shorthest Path from-" + + sourceNodeId + + "-to-" + + targetNodeId + + " has been found.", + ); + appendMessage( + "Info: " + + "Yo, check it out! Shorthest Path from-" + + sourceNodeId + + "-to-" + + targetNodeId + + " has been found, below is the path trace..", + ); + console.log(shortestPathEdges); + + shortestPathEdges.forEach((edge) => { + console.log("Edge ID:", edge.id()); + console.log("Source Node ID:", edge.source().id()); + console.log("Target Node ID:", edge.target().id()); + + edgeId = edge.id(); + sourceNodeId = edge.source().id(); + targetNodeId = edge.target().id(); + // You can access other properties of the edge, e.g., source, target, data, etc. + // You can access other properties of the edge, e.g., source, target, data, etc. + + appendMessage("Info: " + "Edge ID: " + edgeId); + appendMessage("Info: " + "Source Node ID: " + sourceNodeId); + appendMessage("Info: " + "Target Node ID: " + targetNodeId); + }); + } else { + console.error( + `Bro, there is no path from "${sourceNodeId}" to "${targetNodeId}".`, + ); + appendMessage( + `Bro, there is no path from "${sourceNodeId}" to "${targetNodeId}".`, + ); + return; + } +} + +// sleep funtion +function sleep(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} \ No newline at end of file diff --git a/dist/html-static/js/dev.js b/dist/html-static/js/dev.js index 83fc33af3..2b49d5cc0 100644 --- a/dist/html-static/js/dev.js +++ b/dist/html-static/js/dev.js @@ -200,12 +200,12 @@ document.addEventListener("DOMContentLoaded", async function() { saveEdgeToFile(edgeId); // Save the edge element to clab editor panel - clabEditorAddEdge(sourceNode.id(), sourceEndpoint, targetNode.id(), targetEndpoint); + clabEditorAddEdge(sourceNode, sourceEndpoint, targetNode, targetEndpoint); }); async function saveEdgeToFile(edgeId) { const edgeData = cy.$id(edgeId).json(); // Get JSON data of the edge with the specified ID - const endpointName = '/clab-save-topo'; + const endpointName = '/clab-save-topo-cyto-json'; try { // Send the enhanced edge data directly without wrapping it in an object @@ -385,7 +385,6 @@ document.addEventListener("DOMContentLoaded", async function() { console.log("edgeClicked: ", edgeClicked); - //- This code will be executed when you click anywhere in the Cytoscape container //- You can add logic specific to the container here //- This code will be executed when you click anywhere in the Cytoscape container @@ -401,7 +400,6 @@ document.addEventListener("DOMContentLoaded", async function() { console.log("!isPanel01Cy: "); - // Remove all Overlayed Panel // Get all elements with the class "panel-overlay" var panelOverlays = document.getElementsByClassName("panel-overlay"); @@ -420,7 +418,6 @@ document.addEventListener("DOMContentLoaded", async function() { viewportDrawer[i].style.display = "none"; } - // display none each ViewPortDrawer Element, the ViewPortDrawer is created during DOM loading and styled as display node initially // display none each ViewPortDrawer Element, the ViewPortDrawer is created during DOM loading and styled as display node initially var ViewPortDrawerElements = document.getElementsByClassName("ViewPortDrawer"); @@ -445,61 +442,117 @@ document.addEventListener("DOMContentLoaded", async function() { // Listen for tap or click on the Cytoscape canvas cy.on('click', async (event) => { - if (event.target === cy && shiftKeyDown) { // Ensures Shift + click/tap + if (event.target === cy && shiftKeyDown) { // Ensures Shift + click/tap + const pos = event.position; - const newNodeId = 'node' + (cy.nodes().length + 1); + const newNodeId = 'nodeId-' + (cy.nodes().length + 1); // Add the new node to the graph cy.add({ group: 'nodes', - data: { id: newNodeId }, + data: + { + "id": newNodeId, + "editor": "true", + "weight": "30", + "name": newNodeId, + "parent": "", + "topoViewerRole": "pe", + "sourceEndpoint": "", + "targetEndpoint": "", + "containerDockerExtraAttribute": { + "state": "", + "status": "", + }, + "extraData": { + "kind": "container", + "longname": "", + "image": "", + "mgmtIpv4Addresss": "", + }, + }, position: { x: pos.x, y: pos.y } }); - // Save the node element to file in the server - await saveNodeToFile(newNodeId); - // Save the node element to clab editor panel - clabEditorAddNode(newNodeId) + var cyNode = cy.$id(newNodeId); // Get cytoscpe node object id + + // showPanelContainerlabEditor(event) + // sleep (100) + // showPanelNodeEditor(cyNode) + // sleep (100) + // saveNodeToEditorToFile() + + showPanelContainerlabEditor(event) + sleep (1000) + await showPanelNodeEditor(cyNode) + sleep (100) + + await saveNodeToEditorToFile() } }); - async function saveNodeToFile(nodeId) { - const nodeData = cy.$id(nodeId).json(); // Get JSON data of the node with the specified ID - - // Helper function to get the next available label or custom pattern if needed - function getNextLabel(nodeId) { - const nodes = cy.nodes(`[id = "${nodeId}"]`); - let maxLabelNumber = 0; - - nodes.forEach(node => { - const label = node.data("label"); - const match = label ? label.match(/^node-(\d+)$/) : null; - if (match) { - const labelNum = parseInt(match[1], 10); - if (labelNum > maxLabelNumber) { - maxLabelNumber = labelNum; - } - } - }); - - // Increment max label number found - return `node-${maxLabelNumber + 1}`; - } - - // Assign a new label if needed (e.g., for consistency or uniqueness) - nodeData.data.label = getNextLabel(nodeId); - - const endpointName = '/clab-save-topo'; - - try { - // Send the enhanced node data directly without wrapping it in an object - const response = await sendRequestToEndpointPost(endpointName, [nodeData]); - console.log('Node data saved successfully', response); - } catch (error) { - console.error('Failed to save node data:', error); - } - } +// // Click event handler +// cy.on('click', async (event) => { +// if (event.target === cy && shiftKeyDown) { + +// const pos = event.position; +// const newNodeId = 'nodeId-' + (cy.nodes().length + 1); + +// // Add the new node to Cytoscape +// cy.add({ +// group: 'nodes', +// data: { +// "id": newNodeId, +// "editor": "true", +// "weight": "30", +// "name": newNodeId, +// "parent": "", +// "topoViewerRole": "pe", +// "sourceEndpoint": "", +// "targetEndpoint": "", +// "containerDockerExtraAttribute": { +// "state": "", +// "status": "", +// }, +// "extraData": { +// "kind": "container", +// "longname": "", +// "image": "", +// "mgmtIpv4Addresss": "", +// }, +// }, +// position: { x: pos.x, y: pos.y } +// }); + +// var cyNode = cy.$id(newNodeId); + +// // Prepare data for saving +// const nodeName = newNodeId; +// const kind = 'nokia_srlinux'; +// const image = 'ghcr.io/nokia/srlinux:latest'; +// const group = 'data-center'; +// const topoViewerRole = 'pe'; + +// // Update Cytoscape node data +// cyNode.data('name', nodeName); +// cyNode.data('parent', group); +// cyNode.data('topoViewerRole', topoViewerRole); +// cyNode.data('extraData', { +// "kind": kind, +// "image": image, +// "longname": "", +// "mgmtIpv4Addresss": "" +// }); + +// // await showPanelContainerlabEditor(event) +// // sleep (100) +// // await showPanelNodeEditor(cyNode) +// // sleep (100) +// // Save the node +// await saveNodeToEditorToFile(newNodeId, nodeName, kind, image, group, topoViewerRole); +// } +// }); // Click event listener for nodes // Click event listener for nodes @@ -515,7 +568,9 @@ document.addEventListener("DOMContentLoaded", async function() { nodeClicked = true; if (!node.isParent()) { - if (event.originalEvent.shiftKey) { // Start edge creation on Shift + Click + // if (event.originalEvent.shiftKey && (document.getElementById("panel-clab-editor").style.display != "none")) { // Start edge creation on Shift + Click and the clab editor panel is open + if (event.originalEvent.shiftKey) { // Start edge creation on Shift + console.log("Shift + Click"); console.log("edgeHandler Node: ", node.data("extraData").longname); @@ -524,41 +579,52 @@ document.addEventListener("DOMContentLoaded", async function() { // Start the edge handler from the clicked node eh.start(node); - - console.log("isEdgeHandlerActive - Set the edge handler flag: ", isEdgeHandlerActive); + + } else { - // Remove all Overlayed Panel - const panelOverlays = document.getElementsByClassName("panel-overlay"); - for (let i = 0; i < panelOverlays.length; i++) { - panelOverlays[i].style.display = "none"; - } - - console.log(node); - console.log(node.data("containerDockerExtraAttribute").status); - console.log(node.data("extraData")); - - if (document.getElementById("panel-node").style.display === "none") { - document.getElementById("panel-node").style.display = "block"; + + if (node.data("editor") === "true") { + console.log("Node is an editor node"); + + showPanelNodeEditor(node) + + } else { - document.getElementById("panel-node").style.display = "none"; + + // Remove all Overlayed Panel + const panelOverlays = document.getElementsByClassName("panel-overlay"); + for (let i = 0; i < panelOverlays.length; i++) { + panelOverlays[i].style.display = "none"; + } + + + console.log(node); + console.log(node.data("containerDockerExtraAttribute").status); + console.log(node.data("extraData")); + + if (document.getElementById("panel-node").style.display === "none") { + document.getElementById("panel-node").style.display = "block"; + } else { + document.getElementById("panel-node").style.display = "none"; + } + + document.getElementById("panel-node-name").textContent = node.data("extraData").longname; + document.getElementById("panel-node-status").textContent = node.data("containerDockerExtraAttribute").status; + document.getElementById("panel-node-kind").textContent = node.data("extraData").kind; + document.getElementById("panel-node-image").textContent = node.data("extraData").image; + document.getElementById("panel-node-mgmtipv4").textContent = node.data("extraData").mgmtIpv4Addresss; + document.getElementById("panel-node-mgmtipv6").textContent = node.data("extraData").mgmtIpv6Address; + document.getElementById("panel-node-fqdn").textContent = node.data("extraData").fqdn; + document.getElementById("panel-node-group").textContent = node.data("extraData").group; + document.getElementById("panel-node-topoviewerrole").textContent = node.data("topoViewerRole"); + + // Set selected node-long-name to global variable + globalSelectedNode = node.data("extraData").longname; + console.log("internal: ", globalSelectedNode); + + appendMessage(`"isPanel01Cy-cy: " ${isPanel01Cy}`); + appendMessage(`"nodeClicked: " ${nodeClicked}`); } - - document.getElementById("panel-node-name").textContent = node.data("extraData").longname; - document.getElementById("panel-node-status").textContent = node.data("containerDockerExtraAttribute").status; - document.getElementById("panel-node-kind").textContent = node.data("extraData").kind; - document.getElementById("panel-node-image").textContent = node.data("extraData").image; - document.getElementById("panel-node-mgmtipv4").textContent = node.data("extraData").mgmtIpv4Addresss; - document.getElementById("panel-node-mgmtipv6").textContent = node.data("extraData").mgmtIpv6Address; - document.getElementById("panel-node-fqdn").textContent = node.data("extraData").fqdn; - document.getElementById("panel-node-group").textContent = node.data("extraData").group; - document.getElementById("panel-node-topoviewerrole").textContent = node.data("topoViewerRole"); - - // Set selected node-long-name to global variable - globalSelectedNode = node.data("extraData").longname; - console.log("internal: ", globalSelectedNode); - - appendMessage(`"isPanel01Cy-cy: " ${isPanel01Cy}`); - appendMessage(`"nodeClicked: " ${nodeClicked}`); } } }); @@ -1638,99 +1704,480 @@ document.getElementById("panel-log-messages-close-button").addEventListener("cli document.getElementById("panel-log-messages").style.display = "none"; }); -// CLAB EDITOR -async function showPanelContainerlabEditor(event) { - // Remove all Overlayed Panel - // Get all elements with the class "panel-overlay" - var panelOverlays = document.getElementsByClassName("panel-overlay"); - // Loop through each element and set its display to 'none' - for (var i = 0; i < panelOverlays.length; i++) { - panelOverlays[i].style.display = "none"; - } - document.getElementById("panel-clab-editor").style.display = "block"; -} +// // CLAB EDITOR +// async function showPanelContainerlabEditor(event) { +// // Get the YAML content from backend +// getYamlTopoContent(yamlTopoContent) -///-logMessagesPanel Function to add a click event listener to the close button -document.getElementById("panel-clab-editor-close-button").addEventListener("click", () => { - document.getElementById("panel-clab-editor").style.display = "none"; -}); +// // Get all elements with the class "panel-overlay" +// var panelOverlays = document.getElementsByClassName("panel-overlay"); +// // Loop through each element and set its display to 'none' +// for (var i = 0; i < panelOverlays.length; i++) { +// panelOverlays[i].style.display = "none"; +// } +// document.getElementById("panel-clab-editor").style.display = "block"; +// } -function clabEditorLoadFile() { - const fileInput = document.getElementById('panel-clab-editor-file-input'); - const textarea = document.getElementById('panel-clab-editor-text-area'); +// // / logMessagesPanel Function to add a click event listener to the close button +// document.getElementById("panel-clab-editor-close-button").addEventListener("click", () => { +// document.getElementById("panel-clab-editor").style.display = "none"; +// }); - // Trigger the file input's file browser dialog - fileInput.click(); +// function clabEditorLoadFile() { +// const fileInput = document.getElementById('panel-clab-editor-file-input'); +// const textarea = document.getElementById('panel-clab-editor-text-area'); - // Listen for when the user selects a file - fileInput.onchange = function() { - if (fileInput.files.length === 0) { - return; // No file selected - } +// // Trigger the file input's file browser dialog +// fileInput.click(); - const file = fileInput.files[0]; - const reader = new FileReader(); +// // Listen for when the user selects a file +// fileInput.onchange = function() { +// if (fileInput.files.length === 0) { +// return; // No file selected +// } - reader.onload = function(event) { - textarea.value = event.target.result; - }; +// const file = fileInput.files[0]; +// const reader = new FileReader(); - reader.readAsText(file); - }; -} +// reader.onload = function(event) { +// textarea.value = event.target.result; +// }; +// reader.readAsText(file); +// }; +// } -function clabEditorAddNode(nodeName = "Spine-01") { - const textarea = document.getElementById('panel-clab-editor-text-area'); - const nodeDefinition = ` - ${nodeName}: - kind: srl - image: ghcr.io/nokia/srlinux - group: "Data Center Spine" - labels: - topoViewer-role: spine - `; - // Append the new node definition at the end of the 'nodes' section - const nodesIndex = textarea.value.indexOf(" nodes:"); - if (nodesIndex !== -1) { - const insertionIndex = textarea.value.indexOf(" links:", nodesIndex); - if (insertionIndex !== -1) { - textarea.value = textarea.value.slice(0, insertionIndex) + nodeDefinition + textarea.value.slice(insertionIndex); - } else { - // If 'links' section isn't found, append at the end of the content - textarea.value += nodeDefinition; - } - } else { - // If no 'nodes' section, append the node definition at the end - textarea.value += nodeDefinition; - } -} -function clabEditorAddEdge(sourceNodeName, sourceNodeEndpoint, targetNodeName, targetNodeEndpoint) { - const textarea = document.getElementById('panel-clab-editor-text-area'); +// function clabEditorAddNode(nodeId, nodeName = "Spine-01", kind ='nokia_srlinux', image = 'ghcr.io/nokia/srlinux:latest', group = 'group-01', topoViewerRole = 'dcgw') { +// if (!kind || !image || !group || !topoViewerRole) { +// console.error("All parameters (kind, image, group, topoViewerRole) must be provided."); +// return; +// } + +// const textarea = document.getElementById('panel-clab-editor-text-area'); +// nodeId = (`### ${nodeId}`); - // Edge definition with dynamic endpoints array - const edgeDefinition = ` - - endpoints: ["${sourceNodeName}:${sourceNodeEndpoint}", "${targetNodeName}:${targetNodeEndpoint}"]`; - - // Locate the 'links' section and insert the edge definition at the end of it - const linksIndex = textarea.value.indexOf(" links:"); - if (linksIndex !== -1) { - // Find the end of the links section or where the next section begins - const nextSectionIndex = textarea.value.indexOf("\n", linksIndex); - const insertionIndex = nextSectionIndex !== -1 ? nextSectionIndex : textarea.value.length; - - // Insert the edge definition at the end of the links section - textarea.value = textarea.value.slice(0, insertionIndex) + edgeDefinition + textarea.value.slice(insertionIndex); - } else { - // If no 'links' section exists, append the edge definition at the end of the content - textarea.value += "\n links:" + edgeDefinition; - } -} +// // Updated regex pattern to capture nodeName if it exists under the specified nodeId +// // const existingNodeRegex = new RegExp(`###\\s*${nodeId}\\s*\\n\\s*(\\S+):`, 'm'); +// const existingNodeRegex = new RegExp(`${nodeId}\\s*\\n\\s+(\\S+):`, 'm'); + +// const match = textarea.value.match(existingNodeRegex); +// const oldNodeName = match ? match[1] : null; + +// console.log("oldNodeName: ", oldNodeName); // Debug: log oldNodeName + +// // Node definition template with the new nodeName +// const nodeDefinition = +// `${nodeId} +// ${nodeName}: +// kind: ${kind} +// image: ${image} +// group: ${group} +// labels: +// topoViewer-role: ${topoViewerRole} + +// `; + +// // Insert or update the node definition in the "nodes" section +// const nodesSectionIndex = textarea.value.search(/^\s*nodes:/m); +// const nodeRegex = new RegExp(`\\s*${nodeId}\\s*\\n(\\s*.*\\n)*?\\s*topoViewer-role: .*\\n`, 'g'); + +// if (nodesSectionIndex !== -1) { +// const insertionIndex = textarea.value.indexOf(" links:", nodesSectionIndex); +// const endOfNodesSection = insertionIndex !== -1 ? insertionIndex : textarea.value.length; +// const nodesSection = textarea.value.slice(nodesSectionIndex, endOfNodesSection); + +// if (nodesSection.match(nodeRegex)) { +// // Replace the existing node +// textarea.value = textarea.value.replace(nodeRegex, +// `\n\n${nodeId}\n ${nodeName}:\n kind: ${kind}\n image: ${image}\n group: ${group}\n labels:\n topoViewer-role: ${topoViewerRole}\n`); +// } else { +// // Insert the new node at the end of the nodes section +// textarea.value = textarea.value.slice(0, endOfNodesSection) + nodeDefinition + textarea.value.slice(endOfNodesSection); +// } +// } else { +// // Append if "nodes" section doesn't exist +// textarea.value += (textarea.value.endsWith("\n") ? "" : "\n") + nodeDefinition; +// } + +// // Update the links section if oldNodeName exists +// if (oldNodeName && oldNodeName !== nodeName) { +// // Updated regex to match oldNodeName in any position in the endpoints array +// const linksRegex = new RegExp(`(endpoints:\\s*\\[\\s*".*?)(\\b${oldNodeName}\\b)(:.*?)\\]`, 'g'); +// textarea.value = textarea.value.replace(linksRegex, `$1${nodeName}$3]`); +// } + +// yamlTopoContent = textarea.value; +// } + +// async function clabEditorSaveYamlTopo() { +// const textarea = document.getElementById('panel-clab-editor-text-area'); +// clabTopoYamlEditorData = textarea.value; +// console.log("clabTopoYamlEditorData - yamlTopoContent: ", clabTopoYamlEditorData) + +// // dump clabTopoYamlEditorDatal to be persisted to clab-topo.yaml +// const endpointName = '/clab-save-topo-yaml'; + +// try { +// // Send the enhanced node data directly without wrapping it in an object +// const response = await sendRequestToEndpointPost(endpointName, [clabTopoYamlEditorData]); +// console.log('Node data saved successfully', response); +// } catch (error) { +// console.error('Failed to save yaml topo:', error); +// } + +// } + + + +// function clabEditorAddEdge(sourceCyNode, sourceNodeEndpoint, targetCyNode, targetNodeEndpoint) { +// const textarea = document.getElementById('panel-clab-editor-text-area'); + +// sourceNodeName = sourceCyNode.data("name") +// targetNodeName = targetCyNode.data("name") + + +// // Edge definition with dynamic endpoints array +// const edgeDefinition = ` +// - endpoints: ["${sourceNodeName}:${sourceNodeEndpoint}", "${targetNodeName}:${targetNodeEndpoint}"]`; + +// // Locate the 'links' section and insert the edge definition at the end of it +// const linksIndex = textarea.value.indexOf(" links:"); +// if (linksIndex !== -1) { +// // Find the end of the links section or where the next section begins +// const nextSectionIndex = textarea.value.indexOf("\n", linksIndex); +// const insertionIndex = nextSectionIndex !== -1 ? nextSectionIndex : textarea.value.length; + +// // Insert the edge definition at the end of the links section +// textarea.value = textarea.value.slice(0, insertionIndex) + edgeDefinition + textarea.value.slice(insertionIndex); +// } else { +// // If no 'links' section exists, append the edge definition at the end of the content +// textarea.value += "\n links:" + edgeDefinition; +// } +// } + +// // NODE EDITOR START +// // NODE EDITOR START +// // NODE EDITOR START + +// var yamlTopoContent + +// async function showPanelNodeEditor(node) { +// try { +// // Remove all Overlayed Panels +// const panelOverlays = document.getElementsByClassName("panel-overlay"); +// Array.from(panelOverlays).forEach(panel => { +// panel.style.display = "none"; +// }); + +// console.log("showPanelNodeEditor - node ID:", node.data("id")); + +// // Set the node Name in the editor +// const nodeNameInput = document.getElementById("panel-node-editor-name"); +// if (nodeNameInput) { +// nodeNameInput.value = node.data("id"); //defaulted by node id +// } +// // Set the node Id in the editor +// const nodeIdLabel = document.getElementById("panel-node-editor-id"); +// if (nodeIdLabel) { +// nodeIdLabel.textContent = node.data("id"); +// } + +// // Set the node image in the editor +// const nodeImageLabel = document.getElementById("panel-node-editor-image"); +// if (nodeImageLabel) { +// nodeImageLabel.value = 'ghcr.io/nokia/srlinux:latest'; +// } + +// // Set the node image in the editor +// const nodeGroupLabel = document.getElementById("panel-node-editor-group"); +// if (nodeGroupLabel) { +// nodeGroupLabel.value = 'data-center'; +// } + +// // Display the node editor panel +// const nodeEditorPanel = document.getElementById("panel-node-editor"); +// if (nodeEditorPanel) { +// nodeEditorPanel.style.display = "block"; +// } + + +// // Fetch JSON schema from the backend +// const url = "js/clabJsonSchema-v0.59.0.json"; +// try { +// const response = await fetch(url); +// if (!response.ok) { +// throw new Error(`HTTP error! Status: ${response.status}`); +// } +// const jsonData = await response.json(); + +// // Get kind enums from the JSON data +// const { kindOptions } = getKindEnums(jsonData); +// console.log('Kind Enum:', kindOptions); + +// // Populate the dropdown with fetched kindOptions +// populateKindDropdown(kindOptions); + +// // Populate the dropdown with fetched topoViwerRoleOptions +// var topoViwerRoleOptions = ['bridge', 'controller', 'dcgw', 'router', 'leaf', 'pe', 'pon', 'rgw', 'server','super-spine', 'spine']; +// populateTopoViewerRoleDropdown(topoViwerRoleOptions) + +// // List type enums based on kind pattern +// const typeOptions = getTypeEnumsByKindPattern(jsonData, '(srl|nokia_srlinux)'); // aarafat-tag: to be added to the UI +// console.log('Type Enum for (srl|nokia_srlinux):', typeOptions); + +// } catch (error) { +// console.error("Error fetching or processing JSON data:", error.message); +// throw error; +// } + +// } catch (error) { +// console.error("Error in showPanelNodeEditor:", error); +// // Optionally, display an error message to the user +// const errorDiv = document.getElementById('panel-node-editor-error'); +// if (errorDiv) { +// errorDiv.textContent = "An error occurred while loading the node editor. Please try again."; +// errorDiv.style.display = "block"; +// } +// } +// } + +// // Function to get kind enums from the JSON schema +// function getKindEnums(jsonData) { +// let kindOptions = []; +// if (jsonData && jsonData.definitions && jsonData.definitions['node-config']) { +// kindOptions = jsonData.definitions['node-config'].properties.kind.enum || []; +// } else { +// throw new Error("Invalid JSON structure or 'kind' enum not found"); +// } +// return { kindOptions, schemaData: jsonData }; +// } + +// // Function to get type enums based on a kind pattern +// function getTypeEnumsByKindPattern(jsonData, pattern) { +// if (jsonData && jsonData.definitions && jsonData.definitions['node-config'] && jsonData.definitions['node-config'].allOf) { +// for (const condition of jsonData.definitions['node-config'].allOf) { +// if (condition.if && condition.if.properties && condition.if.properties.kind && condition.if.properties.kind.pattern === pattern) { +// if (condition.then && condition.then.properties && condition.then.properties.type && condition.then.properties.type.enum) { +// return condition.then.properties.type.enum; +// } +// } +// } +// } +// return []; +// } + +// let panelNodeEditorKind = "nokia_srlinux"; // Variable to store the selected option for dropdown menu, nokia_srlinux as default +// // Function to populate the kind dropdown +// function populateKindDropdown(options) { +// // Get the dropdown elements by their IDs +// const dropdownTrigger = document.querySelector("#panel-node-kind-dropdown .dropdown-trigger button span"); +// const dropdownContent = document.getElementById("panel-node-kind-dropdown-content"); +// const dropdownButton = document.querySelector("#panel-node-kind-dropdown .dropdown-trigger button"); +// const dropdownContainer = dropdownButton.closest(".dropdown"); + +// if (!dropdownTrigger || !dropdownContent || !dropdownButton || !dropdownContainer) { +// console.error("Dropdown elements not found in the DOM."); +// return; +// } + +// // Set the initial value on the dropdown button +// dropdownTrigger.textContent = panelNodeEditorKind; + +// // Clear any existing content +// dropdownContent.innerHTML = ""; + +// options.forEach(option => { +// // Create a new anchor element for each option +// const optionElement = document.createElement("a"); +// optionElement.classList.add("dropdown-item", "label", "has-text-weight-normal", "is-small", "py-0"); +// optionElement.textContent = option; +// optionElement.href = "#"; // Optional, can be adjusted as needed + +// // Set an event handler for the option +// optionElement.addEventListener("click", (event) => { +// event.preventDefault(); // Prevent default link behavior + +// panelNodeEditorKind = option; // Store the selected option in the variable +// console.log(`${panelNodeEditorKind} selected`); // Log the selected option + +// dropdownTrigger.textContent = panelNodeEditorKind; + +// // Collapse the dropdown menu +// dropdownContainer.classList.remove("is-active"); +// }); + +// // Append the option element to the dropdown content +// dropdownContent.appendChild(optionElement); +// }); +// } + +// // Initialize event listeners for the dropdown +// function initializeDropdownListeners() { +// const dropdownButton = document.querySelector("#panel-node-kind-dropdown .dropdown-trigger button"); +// const dropdownContainer = dropdownButton.closest(".dropdown"); + +// if (!dropdownButton || !dropdownContainer) { +// console.error("Dropdown button or container not found in the DOM."); +// return; +// } + +// // Toggle dropdown menu on button click +// dropdownButton.addEventListener("click", (event) => { +// event.stopPropagation(); // Prevents the event from bubbling up +// dropdownContainer.classList.toggle("is-active"); +// }); + +// // Collapse the dropdown if clicked outside +// document.addEventListener("click", (event) => { +// if (dropdownContainer.classList.contains("is-active")) { +// dropdownContainer.classList.remove("is-active"); +// } +// }); +// } +// // Initialize dropdown listeners once when the DOM is fully loaded +// document.addEventListener("DOMContentLoaded", () => { +// initializeDropdownListeners(); +// }); + + +// let panelNodeEditorTopoViewerRole = "pe"; // Variable to store the selected option for dropdown menu, nokia_srlinux as default +// // Function to populate the topoviewerrole dropdown +// function populateTopoViewerRoleDropdown(options) { +// // Get the dropdown elements by their IDs +// const dropdownTrigger = document.querySelector("#panel-node-topoviewerrole-dropdown .dropdown-trigger button span"); +// const dropdownContent = document.getElementById("panel-node-topoviewerrole-dropdown-content"); +// const dropdownButton = document.querySelector("#panel-node-topoviewerrole-dropdown .dropdown-trigger button"); +// const dropdownContainer = dropdownButton.closest(".dropdown"); + +// if (!dropdownTrigger || !dropdownContent || !dropdownButton || !dropdownContainer) { +// console.error("Dropdown elements not found in the DOM."); +// return; +// } + +// // Set the initial value on the dropdown button +// dropdownTrigger.textContent = panelNodeEditorTopoViewerRole; + + +// // Clear any existing content +// dropdownContent.innerHTML = ""; + +// options.forEach(option => { +// // Create a new anchor element for each option +// const optionElement = document.createElement("a"); +// optionElement.classList.add("dropdown-item", "label", "has-text-weight-normal", "is-small", "py-0"); +// optionElement.textContent = option; +// optionElement.href = "#"; // Optional, can be adjusted as needed -// CLAB EDITOR +// // Set an event handler for the option +// optionElement.addEventListener("click", (event) => { +// event.preventDefault(); // Prevent default link behavior + +// panelNodeEditorTopoViewerRole = option; // Store the selected option in the variable +// console.log(`${panelNodeEditorTopoViewerRole} selected`); // Log the selected option + +// dropdownTrigger.textContent = panelNodeEditorTopoViewerRole; + +// // Collapse the dropdown menu +// dropdownContainer.classList.remove("is-active"); +// }); + +// // Append the option element to the dropdown content +// dropdownContent.appendChild(optionElement); +// }); +// } + +// // Initialize event listeners for the dropdown +// function initializeDropdownTopoViewerRoleListeners() { +// const dropdownButton = document.querySelector("#panel-node-topoviewerrole-dropdown .dropdown-trigger button"); +// const dropdownContainer = dropdownButton.closest(".dropdown"); + +// if (!dropdownButton || !dropdownContainer) { +// console.error("Dropdown button or container not found in the DOM."); +// return; +// } + +// // Toggle dropdown menu on button click +// dropdownButton.addEventListener("click", (event) => { +// event.stopPropagation(); // Prevents the event from bubbling up +// dropdownContainer.classList.toggle("is-active"); +// }); + +// // Collapse the dropdown if clicked outside +// document.addEventListener("click", (event) => { +// if (dropdownContainer.classList.contains("is-active")) { +// dropdownContainer.classList.remove("is-active"); +// } +// }); +// } + +// // Initialize dropdown listeners once when the DOM is fully loaded +// document.addEventListener("DOMContentLoaded", () => { +// initializeDropdownTopoViewerRoleListeners(); +// }); + +// // Initialize event listener for the close button +// document.getElementById("panel-node-editor-close-button").addEventListener("click", () => { +// document.getElementById("panel-node-editor").style.display = "none"; +// }); + + +// // update node data in the editor, save cyto json to file dataCytoMarshall.json and save to clab topo.yaml +// async function saveNodeToEditorToFile() { +// const nodeId =document.getElementById("panel-node-editor-id").textContent +// var cyNode = cy.$id(nodeId); // Get cytoscpe node object id + +// // get value from panel-node-editor +// nodeName = document.getElementById("panel-node-editor-name").value +// kind = panelNodeEditorKind +// image = document.getElementById("panel-node-editor-image").value +// group = document.getElementById("panel-node-editor-group").value +// topoViewerRole = panelNodeEditorTopoViewerRole + +// console.log("panelEditorNodeName", nodeName) +// console.log("panelEditorkind", kind) +// console.log("panelEditorImage", image) +// console.log("panelEditorGroup", group) +// console.log("panelEditorTopoViewerRole",topoViewerRole) + +// // save node data to cytoscape node object +// var extraData = { +// "kind": kind, +// "image": image, +// "longname": "", +// "mgmtIpv4Addresss": "" +// }; + +// cyNode.data(('name'), nodeName) +// cyNode.data(('parent'), group) +// cyNode.data(('topoViewerRole'), topoViewerRole) +// cyNode.data(('extraData'), extraData) + +// console.log('cyto node object data: ', cyNode); + +// // dump cytoscape node object to nodeData to be persisted to dataCytoMarshall.json +// var nodeData = cy.$id(nodeId).json(); // Get JSON data of the node with the specified ID +// const endpointName = '/clab-save-topo-cyto-json'; + +// try { +// // Send the enhanced node data directly without wrapping it in an object +// const response = await sendRequestToEndpointPost(endpointName, [nodeData]); +// console.log('Node data saved successfully', response); +// } catch (error) { +// console.error('Failed to save node data:', error); +// } + +// // add node to clab editor textarea +// clabEditorAddNode(nodeId, nodeName, kind, image, group, topoViewerRole) + +// // clabEditorSaveYamlTopo() +// } + + +// NODE EDITOR END +// NODE EDITOR END async function showPanelTopoViewerClient(event) { @@ -1856,7 +2303,7 @@ async function getActualNodesEndpoints(event) { ); showLoadingSpinnerGlobal() - const CyTopoJson = await sendRequestToEndpointGet("/actual-nodes-endpoints", argsList = []) + const CyTopoJson = await sendRequestToEndpointGetV2("/actual-nodes-endpoints", argsList = []) location.reload(true); // Handle the response data @@ -2275,9 +2722,6 @@ function viewportDrawerCaptureButton() { console.log ("viewportDrawerCaptureButton() - clicked") - - - // Get all checkbox inputs within the specific div const checkboxes = document.querySelectorAll('#viewport-drawer-capture-sceenshoot-content .checkbox-input'); @@ -2322,131 +2766,6 @@ function viewportDrawerCaptureButton() { } - - - -// async function captureAndSaveViewportAsDrawIo(cy) { -// // Find the canvas element for layer2-node -// // Find the canvas element for layer2-node -// const canvasElement = document.querySelector( -// '#cy canvas[data-id="layer2-node"]', -// ); -// const drawIoWidht = canvasElement.width / 10; -// const drawIoHeight = canvasElement.height / 10; -// const drawIoaAspectRatio = drawIoWidht / drawIoHeight; - -// const mxGraphHeader = ` -// -// -// `; - -// const mxGraphFooter = ` -// `; - -// const mxCells = []; - -// // Iterate through nodes and edges -// // Function to create mxCell XML for nodes -// // Iterate through nodes and edges -// // Function to create mxCell XML for nodes -// function createMxCellForNode(node, imageURL) { -// if (node.isParent()) { -// return ` -// -// -// `; -// } else if ( -// !node.data("id").includes("statusGreen") && -// !node.data("id").includes("statusRed") -// ) { -// return ` -// -// -// `; -// } -// } - -// cy.nodes().forEach(function(node) { -// let imageURL; -// switch (node.data("topoViewerRole")) { -// case "pe": -// imageURL = `http://${location.host}/images/clab-pe-light-blue.png`; -// break; -// case "controller": -// imageURL = -// `http://${location.host}/images/clab-controller-light-blue.png`; -// break; -// case "pon": -// imageURL = `http://${location.host}/images/clab-pon-dark-blue.png`; -// break; -// case "dcgw": -// imageURL = `http://${location.host}/images/clab-dcgw-dark-blue.png`; -// break; -// case "leaf": -// imageURL = `http://${location.host}/images/clab-leaf-light-blue.png`; -// break; -// case "spine": -// imageURL = `http://${location.host}/images/clab-spine-dark-blue.png`; -// break; -// case "super-spine": -// imageURL = `http://${location.host}/images/clab-spine-light-blue.png`; -// break; -// } -// mxCells.push(createMxCellForNode(node, imageURL)); -// }); - -// cy.edges().forEach(function(edge) { -// mxCells.push(` -// -// -// -// -// -// -// -// -// -// -// -// -// -// `); -// }); - -// // Combine all parts and create XML -// const mxGraphXML = mxGraphHeader + mxCells.join("") + mxGraphFooter; - -// // Create a Blob from the XML -// const blob = new Blob([mxGraphXML], { -// type: "application/xml", -// }); - -// // Create a URL for the Blob -// const url = window.URL.createObjectURL(blob); - -// // Create a download link and trigger a click event -// const a = document.createElement("a"); -// a.style.display = "none"; -// a.href = url; -// a.download = "filename.drawio"; -// document.body.appendChild(a); - -// bulmaToast.toast({ -// message: `Brace yourselves for a quick snapshot, folks! 📸 Capturing the viewport in 3... 2... 1... 🚀💥`, -// type: "is-warning is-size-6 p-3", -// duration: 2000, -// position: "top-center", -// closeOnClick: true, -// }); -// await sleep(2000); -// // Simulate a click to trigger the download -// a.click(); - -// // Clean up by revoking the URL and removing the download link -// window.URL.revokeObjectURL(url); -// document.body.removeChild(a); -// } - async function captureAndSaveViewportAsDrawIo(cy) { // Define base64-encoded SVGs for each role const svgBase64ByRole = { @@ -2555,6 +2874,26 @@ async function captureAndSaveViewportAsDrawIo(cy) { document.body.removeChild(a); } +// async function getYamlTopoContent(yamlTopoContent) { + +// try { +// // Check if yamlTopoContent is already set +// console.log('YAML Topo Initial Content:', yamlTopoContent); + +// if (!yamlTopoContent) { +// // Load the content if yamlTopoContent is empty +// yamlTopoContent = await sendRequestToEndpointGetV3("/get-yaml-topo-content"); +// } + +// console.log('YAML Topo Content:', yamlTopoContent); +// document.getElementById('panel-clab-editor-text-area').value = yamlTopoContent; + + +// } catch (error) { +// console.error("Error occurred:", error); +// // Handle errors as needed +// } +// } diff --git a/dist/html-static/template/clab/button.html.tmpl b/dist/html-static/template/clab/button.html.tmpl index d36a21041..c5940ba4b 100644 --- a/dist/html-static/template/clab/button.html.tmpl +++ b/dist/html-static/template/clab/button.html.tmpl @@ -9,6 +9,10 @@ + + + +