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 = `
+
+
+
+
+
+
+
+
Select file type
+
Choose one or multiple types you want to export
+
+
+
+
+
+
+ `;
+
+ // 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:
+
+ Siva Sivakumar - For pioneering the integration of Bulma CSS, significantly enhancing TopoViewer design and usability.
+ Gatot Susilo - For seamlessly incorporating TopoViewer into the Komodo2 tool, bridging functionality with innovation.
+ Gusman Dharma Putra - For his invaluable contribution in integrating TopoViewer into Komodo2, enriching its capabilities.
+ Sven Wisotzky - For offering insightful feedback that led to significant full stack optimizations.
+
+
+
+
+
+ `;
+ 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 @@
+
+
+
+
@@ -36,6 +40,8 @@
Action - Log Messages
TopoViewer Helper App
+ Containerlab Editor
+
@@ -311,6 +317,39 @@
+
+
Containerlab YAML Editor
+
+
+
+
+
+
+
+
+
+ Load YAML
+ Add Node
+ Close
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -430,6 +469,128 @@
+
+
Node Properties Editor
+
+
+
+
+
+
+
+
+
+ Kind
+
+
+
+
+
+ Select Kind
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ TopoViewer Role
+
+
+
+
+
+ Select TopoViewerRole
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Link Properties
@@ -655,14 +816,7 @@
-
+
@@ -773,12 +927,16 @@
+
-
+
+
+
+
diff --git a/dist/html-static/template/clab/clabJsonSchema-v0.59.0.json b/dist/html-static/template/clab/clabJsonSchema-v0.59.0.json
new file mode 100644
index 000000000..50b31559b
--- /dev/null
+++ b/dist/html-static/template/clab/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/template/clab/cy-style-dark.tmpl b/dist/html-static/template/clab/cy-style-dark.tmpl
index f4928a583..368263b89 100644
--- a/dist/html-static/template/clab/cy-style-dark.tmpl
+++ b/dist/html-static/template/clab/cy-style-dark.tmpl
@@ -19,12 +19,14 @@
"text-valign": "bottom",
"text-halign": "center",
"background-color": "#8F96AC",
-
"min-zoomed-font-size": "7px",
"color": "#F5F5F5",
"text-outline-color": "#3C3E41",
-
"text-outline-width": "0.3px",
+ "text-background-color": "#FFFF",
+ "text-background-opacity": 0.7,
+ "text-background-shape": "roundrectangle",
+ "text-background-padding": "1px",
"overlay-padding": "0.3px",
"z-index": "10"
}
@@ -149,6 +151,13 @@
"background-fit": "cover"
}
},
+ {
+ "selector": "node[topoViewerRole=\"leaf-svg\"]",
+ "style": {
+ "background-image": "data:image/svg+xml;base64,PHN2ZyB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWw6c3BhY2U9InByZXNlcnZlIiBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCAxMjAgMTIwOyIgdmlld0JveD0iMCAwIDEyMCAxMjAiIHk9IjBweCIgeD0iMHB4IiBpZD0iTGF5ZXJfMSIgdmVyc2lvbj0iMS4xIj4mI3hhOzxzdHlsZSB0eXBlPSJ0ZXh0L2NzcyI+LnN0MCB7IGZpbGw6IHJnYigwLCA5MCwgMjU1KTsgfSAuc3QxIHsgZmlsbDogbm9uZTsgc3Ryb2tlOiByZ2IoMjU1LCAyNTUsIDI1NSk7IHN0cm9rZS13aWR0aDogNDsgc3Ryb2tlLWxpbmVjYXA6IHJvdW5kOyBzdHJva2UtbGluZWpvaW46IHJvdW5kOyBzdHJva2UtbWl0ZXJsaW1pdDogMTA7IH0gLnN0MiB7IGZpbGw6IHJnYigyNTUsIDI1NSwgMjU1KTsgfSAuc3QzIHsgZmlsbDogbm9uZTsgc3Ryb2tlOiByZ2IoMjU1LCAyNTUsIDI1NSk7IHN0cm9rZS13aWR0aDogNDsgc3Ryb2tlLW1pdGVybGltaXQ6IDEwOyB9IC5zdDQgeyBmaWxsOiBub25lOyBzdHJva2U6IHJnYigyNTUsIDI1NSwgMjU1KTsgc3Ryb2tlLXdpZHRoOiA0OyBzdHJva2UtbGluZWNhcDogcm91bmQ7IHN0cm9rZS1saW5lam9pbjogcm91bmQ7IH0gLnN0NSB7IGZpbGw6IHJnYigyNTUsIDI1NSwgMjU1KTsgc3Ryb2tlOiByZ2IoMjU1LCAyNTUsIDI1NSk7IHN0cm9rZS13aWR0aDogNDsgc3Ryb2tlLWxpbmVjYXA6IHJvdW5kOyBzdHJva2UtbGluZWpvaW46IHJvdW5kOyBzdHJva2UtbWl0ZXJsaW1pdDogMTA7IH0gLnN0NiB7IGZpbGw6IG5vbmU7IHN0cm9rZTogcmdiKDI1NSwgMjU1LCAyNTUpOyBzdHJva2Utd2lkdGg6IDQuMjMzMzsgc3Ryb2tlLWxpbmVjYXA6IHJvdW5kOyBzdHJva2UtbGluZWpvaW46IHJvdW5kOyBzdHJva2UtbWl0ZXJsaW1pdDogMTA7IH0gLnN0NyB7IGZpbGw6IG5vbmU7IHN0cm9rZTogcmdiKDI1NSwgMjU1LCAyNTUpOyBzdHJva2Utd2lkdGg6IDQ7IHN0cm9rZS1saW5lY2FwOiByb3VuZDsgc3Ryb2tlLW1pdGVybGltaXQ6IDEwOyB9IC5zdDggeyBmaWxsOiByZ2IoMjU1LCAyNTUsIDI1NSk7IHN0cm9rZTogcmdiKDI1NSwgMjU1LCAyNTUpOyBzdHJva2Utd2lkdGg6IDQ7IHN0cm9rZS1taXRlcmxpbWl0OiAxMDsgfSAuc3Q5IHsgZmlsbDogcmdiKDI1NSwgMjU1LCAyNTUpOyBzdHJva2U6IHJnYigyNTUsIDI1NSwgMjU1KTsgc3Ryb2tlLXdpZHRoOiA0OyB9IC5zdDEwIHsgZmlsbDogbm9uZTsgc3Ryb2tlOiByZ2IoMjU1LCAyNTUsIDI1NSk7IHN0cm9rZS13aWR0aDogNDsgfSAuc3QxMSB7IGZpbGw6IHJnYigzOCwgMzgsIDM4KTsgc3Ryb2tlOiByZ2IoMjU1LCAyNTUsIDI1NSk7IHN0cm9rZS13aWR0aDogNC4yMzMzOyB9IC5zdDEyIHsgZmlsbC1ydWxlOiBldmVub2RkOyBjbGlwLXJ1bGU6IGV2ZW5vZGQ7IGZpbGw6IG5vbmU7IHN0cm9rZTogcmdiKDI1NSwgMjU1LCAyNTUpOyBzdHJva2Utd2lkdGg6IDQ7IHN0cm9rZS1taXRlcmxpbWl0OiAxMDsgfSAuc3QxMyB7IGZpbGwtcnVsZTogZXZlbm9kZDsgY2xpcC1ydWxlOiBldmVub2RkOyBmaWxsOiByZ2IoMjU1LCAyNTUsIDI1NSk7IHN0cm9rZTogcmdiKDI1NSwgMjU1LCAyNTUpOyBzdHJva2Utd2lkdGg6IDQ7IH0gLnN0MTQgeyBmaWxsOiBub25lOyBzdHJva2U6IHJnYigyNTUsIDI1NSwgMjU1KTsgc3Ryb2tlLXdpZHRoOiA0LjIzMzM7IHN0cm9rZS1saW5lY2FwOiByb3VuZDsgc3Ryb2tlLWxpbmVqb2luOiByb3VuZDsgfSAuc3QxNSB7IGZpbGw6IG5vbmU7IHN0cm9rZTogcmdiKDI1NSwgMjU1LCAyNTUpOyBzdHJva2Utd2lkdGg6IDQ7IHN0cm9rZS1saW5lY2FwOiByb3VuZDsgfSAuc3QxNiB7IGZpbGw6IHJnYigyNTUsIDI1NSwgMjU1KTsgc3Ryb2tlOiByZ2IoMjU1LCAyNTUsIDI1NSk7IHN0cm9rZS1taXRlcmxpbWl0OiAxMDsgfSAuc3QxNyB7IGZpbGw6IHJnYigzOCwgMzgsIDM4KTsgc3Ryb2tlOiByZ2IoMjU1LCAyNTUsIDI1NSk7IHN0cm9rZS13aWR0aDogNDsgc3Ryb2tlLW1pdGVybGltaXQ6IDEwOyB9IC5zdDE4IHsgZmlsbDogcmdiKDI1NSwgMjU1LCAyNTUpOyBzdHJva2U6IHJnYigyNTUsIDI1NSwgMjU1KTsgc3Ryb2tlLXdpZHRoOiA0OyBzdHJva2UtbGluZWNhcDogcm91bmQ7IHN0cm9rZS1saW5lam9pbjogcm91bmQ7IH0gPC9zdHlsZT4mI3hhOzxyZWN0IGhlaWdodD0iMTIwIiB3aWR0aD0iMTIwIiBjbGFzcz0ic3QwIi8+JiN4YTs8Zz4mI3hhOwk8cGF0aCBkPSJNOTEuNSwyNy4zbDcuNiw3LjZjMS4zLDEuMywxLjMsMy4xLDAsNC4zbC03LjYsNy43IiBjbGFzcz0ic3QxIi8+JiN4YTsJPHBhdGggZD0iTTI4LjUsNDYuOWwtNy42LTcuNmMtMS4zLTEuMy0xLjMtMy4xLDAtNC4zbDcuNi03LjciIGNsYXNzPSJzdDEiLz4mI3hhOwk8cGF0aCBkPSJNOTEuNSw3My4xbDcuNiw3LjZjMS4zLDEuMywxLjMsMy4xLDAsNC4zbC03LjYsNy43IiBjbGFzcz0ic3QxIi8+JiN4YTsJPHBhdGggZD0iTTI4LjUsOTIuN2wtNy42LTcuNmMtMS4zLTEuMy0xLjMtMy4xLDAtNC4zbDcuNi03LjciIGNsYXNzPSJzdDEiLz4mI3hhOwk8Zz4mI3hhOwkJPHBhdGggZD0iTTk2LjYsMzYuOEg2Ny45bC0xNiw0NS45SDIzLjIiIGNsYXNzPSJzdDEiLz4mI3hhOwkJPHBhdGggZD0iTTk2LjYsODIuN0g2Ny45bC0xNi00NS45SDIzLjIiIGNsYXNzPSJzdDEiLz4mI3hhOwk8L2c+JiN4YTs8L2c+JiN4YTs8L3N2Zz4=",
+ "background-fit": "cover"
+ }
+ },
{
"selector": "node[topoViewerRole=\"rgw\"]",
"style": {
@@ -202,12 +211,13 @@
"source-label": "data(sourceEndpoint)",
"target-label": "data(targetEndpoint)",
"arrow-scale": "0.5",
- "color": "#CACBCC",
+ "color": "#FFFF",
"text-outline-width": "0.3px",
-
- "text-outline-color": "#3C3E41",
-
-
+ "text-outline-color": "#FFFFFF",
+ "text-background-color": "#CACBCC",
+ "text-background-opacity": 0.7,
+ "text-background-shape": "roundrectangle",
+ "text-background-padding": "1px",
"curve-style": "bezier",
"opacity": "1",
"line-color": "#969799",
@@ -244,6 +254,59 @@
}
},
+ {
+ "selector": ".eh-handle",
+ "style": {
+ "background-color": "red",
+ "width": 2,
+ "height": 2,
+ "shape": "ellipse",
+ "overlay-opacity": 0,
+ "border-width": 2,
+ "border-opacity": 0
+ }
+ },
+
+ {
+ "selector": ".eh-hover",
+ "style": {
+ "background-color": "red"
+ }
+ },
+
+ {
+ "selector": ".eh-source",
+ "style": {
+ "border-width": 2,
+ "border-color": "red"
+ }
+ },
+
+ {
+ "selector": ".eh-target",
+ "style": {
+ "border-width": 2,
+ "border-color": "red"
+ }
+ },
+
+ {
+ "selector": ".eh-preview, .eh-ghost-edge",
+ "style": {
+ "background-color": "red",
+ "line-color": "red",
+ "target-arrow-color": "red",
+ "source-arrow-color": "red"
+ }
+ },
+ {
+ "selector": ".eh-ghost-edge.eh-preview-active",
+ "style": {
+ "opacity": 0
+ }
+ },
+
+
{"selector": "edge[group=\"coexp\"]", "style": {"line-color": "#d0b7d5"}},
{"selector": "edge[group=\"coloc\"]", "style": {"line-color": "#a0b3dc"}},
diff --git a/dist/html-static/template/clab/dev.html.tmpl b/dist/html-static/template/clab/dev.html.tmpl
new file mode 100644
index 000000000..c5940ba4b
--- /dev/null
+++ b/dist/html-static/template/clab/dev.html.tmpl
@@ -0,0 +1,943 @@
+
+
+
+
+
+ TopoViewer
+
+
+
+
+
+
+
+
+
+
+
+
+
+
TopoViewer
+
Topology name: nokia-MAGc-lab ::: Uptime: 10m10s
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Layout
+
+
+
+
+ This panel allows computing new coordinates to nodes of the graph.
+
+
+
+
+
+ Select Layout Algorithm
+ Force Directed
+ Vertical
+ Horizontal
+
+
+
+
+
+
+
+
+
+
+ Link Lenght
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Group v-Gap
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Group h-Gap
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Topology Overview
+
+
+
+
+ This panel allows to filter the topology.
+
+
+
+
+
+
+
+
+
+
+
+
+ Capture Screenshot
+
+
+
+
+ This panel allows to capture the topology screenshoot.
+
+
+
+
+
+
+
+
+
+
+
+ Draw.IO
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Containerlab YAML Editor
+
+
+
+
+
+
+
+
+
+ Load YAML
+ Add Node
+ Close
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Node Properties
+
+
+
+
+
+
+
Node Name
+
+ node-name-placeholder
+
+
+
+
+
+
+
+
+
+
+
+
+ Actions
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Status
+
node-status-placeholder
+
+
+
+
+
+
+
Kind
+
node-kind-placeholder
+
+
+
+
+
+
+
Image
+
node-image-placeholder
+
+
+
+
+
+
+
Management IPv4
+
node-mgmtipv4-placeholder
+
+
+
+
+
+
+
Management IPv6
+
node-mgmtipv6-placeholder
+
+
+
+
+
+
+
FQDN
+
node-fqdn-placeholder
+
+
+
+
+
+
+
Group
+
node-group-placeholder
+
+
+
+
+
+
+
TopoViewer Role
+
node-topoviewerrole-placeholder
+
+
+
+
+
+
+
+
+
+
Node Properties Editor
+
+
+
+
+
+
+
+
+
+ Kind
+
+
+
+
+
+ Select Kind
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ TopoViewer Role
+
+
+
+
+
+ Select TopoViewerRole
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Link Properties
+
+
+
+
+
+
+
Link Name
+
+ link-name-placeholder
+
+
+
+
+
+
+
+
+
+
+
+
+ Actions
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Endpoint-A
+
link-endpoint-a-name-placeholder
+
+
+
+
+
+
+
MAC address
+
link-mac-address-placeholder
+
+
+
+
+
+
+
+
+
+
+
Endpoint-B
+
link-endpoint-a-name-placeholder
+
+
+
+
+
+
+
MAC address
+
link-mac-address-placeholder
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Diff
+
+
+
+
+
+
+
Saved Config
+
+
+ Restore
+
+
+
+
+
+
+
+
Running Config
+
+
+ Backup
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Introduction
+
+
+
+
+
+
Welcome to TopoViewer!
+
+ TopoViewer is a powerful network topology visualization tool designed to help you easily manage and monitor your network infrastructure. Whether you're a network administrator, engineer, or simply curious about your network, TopoViewer has you covered.
+
+
+ Designed and developed by Asad Arafat
+
+
Key Features:
+
+ Visualize your network topology with ease.
+ View detailed attributes of nodes and links by clicking on them.
+ Analyze network traffic using Wireshark integration.
+ Apply network impairments to simulate real-world conditions.
+
+
+ Getting Started:
+
+
+ Click on nodes and links to explore your network.
+ Use the settings menu to show/hide link endpoint labels.
+ Analyze network traffic using Wireshark integration.
+ For advanced network analysis, download our client package.
+ Visit our GitHub repository for more details https/github.com/asadarafat/topoViewer .
+
+
+ I hope you find TopoViewer a valuable tool for your network management needs. If you have any questions or feedback, please don't hesitate to reach out to me.
+
+
+
+
+
+
+
+
+
+
Log Messages
+
+
+
+
+
+ Copy to Clipboard
+ Close
+
+
+
+
+
+
TopoViewer Helper App
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dist/topoviewer b/dist/topoviewer
index 782c60559..99a93c22c 100755
Binary files a/dist/topoviewer and b/dist/topoviewer differ
diff --git a/go_cloudshellwrapper/constants.go b/go_cloudshellwrapper/constants.go
index a2b7f6f03..395a36c31 100755
--- a/go_cloudshellwrapper/constants.go
+++ b/go_cloudshellwrapper/constants.go
@@ -1,6 +1,6 @@
package cloudshellwrapper
-var VersionInfo string = "nightly-24.11.07"
+var VersionInfo string = "nightly-24.11.18"
// create html-public files
var HtmlPublicPrefixPath string = "./html-public/"
diff --git a/go_cloudshellwrapper/constants.go.bak b/go_cloudshellwrapper/constants.go.bak
index 14c2d03b3..a2b7f6f03 100755
--- a/go_cloudshellwrapper/constants.go.bak
+++ b/go_cloudshellwrapper/constants.go.bak
@@ -1,6 +1,6 @@
package cloudshellwrapper
-var VersionInfo string = "nightly-24.11.06"
+var VersionInfo string = "nightly-24.11.07"
// create html-public files
var HtmlPublicPrefixPath string = "./html-public/"
diff --git a/html-static/template/clab/button.html.tmpl b/html-static/template/clab/button.html.tmpl
index d36a21041..c5940ba4b 100644
--- a/html-static/template/clab/button.html.tmpl
+++ b/html-static/template/clab/button.html.tmpl
@@ -9,6 +9,10 @@
+
+
+
+
@@ -36,6 +40,8 @@
Action - Log Messages
TopoViewer Helper App
+ Containerlab Editor
+
@@ -311,6 +317,39 @@
+
+
Containerlab YAML Editor
+
+
+
+
+
+
+
+
+
+ Load YAML
+ Add Node
+ Close
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -430,6 +469,128 @@
+
+
Node Properties Editor
+
+
+
+
+
+
+
+
+
+ Kind
+
+
+
+
+
+ Select Kind
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ TopoViewer Role
+
+
+
+
+
+ Select TopoViewerRole
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Link Properties
@@ -655,14 +816,7 @@
-
+
@@ -773,12 +927,16 @@
+
-
+
+
+
+
diff --git a/html-static/template/clab/dev.html.tmpl b/html-static/template/clab/dev.html.tmpl
new file mode 100644
index 000000000..c5940ba4b
--- /dev/null
+++ b/html-static/template/clab/dev.html.tmpl
@@ -0,0 +1,943 @@
+
+
+
+
+
+ TopoViewer
+
+
+
+
+
+
+
+
+
+
+
+
+
+
TopoViewer
+
Topology name: nokia-MAGc-lab ::: Uptime: 10m10s
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Layout
+
+
+
+
+ This panel allows computing new coordinates to nodes of the graph.
+
+
+
+
+
+ Select Layout Algorithm
+ Force Directed
+ Vertical
+ Horizontal
+
+
+
+
+
+
+
+
+
+
+ Link Lenght
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Group v-Gap
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Group h-Gap
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Topology Overview
+
+
+
+
+ This panel allows to filter the topology.
+
+
+
+
+
+
+
+
+
+
+
+
+ Capture Screenshot
+
+
+
+
+ This panel allows to capture the topology screenshoot.
+
+
+
+
+
+
+
+
+
+
+
+ Draw.IO
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Containerlab YAML Editor
+
+
+
+
+
+
+
+
+
+ Load YAML
+ Add Node
+ Close
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Node Properties
+
+
+
+
+
+
+
Node Name
+
+ node-name-placeholder
+
+
+
+
+
+
+
+
+
+
+
+
+ Actions
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Status
+
node-status-placeholder
+
+
+
+
+
+
+
Kind
+
node-kind-placeholder
+
+
+
+
+
+
+
Image
+
node-image-placeholder
+
+
+
+
+
+
+
Management IPv4
+
node-mgmtipv4-placeholder
+
+
+
+
+
+
+
Management IPv6
+
node-mgmtipv6-placeholder
+
+
+
+
+
+
+
FQDN
+
node-fqdn-placeholder
+
+
+
+
+
+
+
Group
+
node-group-placeholder
+
+
+
+
+
+
+
TopoViewer Role
+
node-topoviewerrole-placeholder
+
+
+
+
+
+
+
+
+
+
Node Properties Editor
+
+
+
+
+
+
+
+
+
+ Kind
+
+
+
+
+
+ Select Kind
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ TopoViewer Role
+
+
+
+
+
+ Select TopoViewerRole
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Link Properties
+
+
+
+
+
+
+
Link Name
+
+ link-name-placeholder
+
+
+
+
+
+
+
+
+
+
+
+
+ Actions
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Endpoint-A
+
link-endpoint-a-name-placeholder
+
+
+
+
+
+
+
MAC address
+
link-mac-address-placeholder
+
+
+
+
+
+
+
+
+
+
+
Endpoint-B
+
link-endpoint-a-name-placeholder
+
+
+
+
+
+
+
MAC address
+
link-mac-address-placeholder
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Diff
+
+
+
+
+
+
+
Saved Config
+
+
+ Restore
+
+
+
+
+
+
+
+
Running Config
+
+
+ Backup
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Introduction
+
+
+
+
+
+
Welcome to TopoViewer!
+
+ TopoViewer is a powerful network topology visualization tool designed to help you easily manage and monitor your network infrastructure. Whether you're a network administrator, engineer, or simply curious about your network, TopoViewer has you covered.
+
+
+ Designed and developed by Asad Arafat
+
+
Key Features:
+
+ Visualize your network topology with ease.
+ View detailed attributes of nodes and links by clicking on them.
+ Analyze network traffic using Wireshark integration.
+ Apply network impairments to simulate real-world conditions.
+
+
+ Getting Started:
+
+
+ Click on nodes and links to explore your network.
+ Use the settings menu to show/hide link endpoint labels.
+ Analyze network traffic using Wireshark integration.
+ For advanced network analysis, download our client package.
+ Visit our GitHub repository for more details https/github.com/asadarafat/topoViewer .
+
+
+ I hope you find TopoViewer a valuable tool for your network management needs. If you have any questions or feedback, please don't hesitate to reach out to me.
+
+
+
+
+
+
+
+
+
+
Log Messages
+
+
+
+
+
+ Copy to Clipboard
+ Close
+
+
+
+
+
+
TopoViewer Helper App
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tools/getGithubApiV2.sh b/tools/getGithubApiV2.sh
index e7bf5926d..bc0eddb5d 100644
--- a/tools/getGithubApiV2.sh
+++ b/tools/getGithubApiV2.sh
@@ -1,22 +1,17 @@
#!/bin/bash
# Default values
-GITHUB_TOKEN=""
SPECIFIC_VERSION=""
# Function to display usage information
usage() {
- echo "Usage: $0 [--github-token
] [--version ]"
- exit 1
+ echo "Usage: $0 [--version ] [--help] [--list]"
}
-# Function to check for jq installation
-check_jq() {
- if ! command -v jq &>/dev/null; then
- echo "jq is not installed."
- echo "Please download and install jq from https://stedolan.github.io/jq/download/"
- exit 1
- fi
+# Function to list versions
+versions() {
+ echo "Available versions on GitHub:"
+ git ls-remote --tags https://github.com/asadarafat/topoViewer.git | grep -v komodo | sed -n 's|.*refs/tags/\(nightly.*\)|\1|p' | sort
}
# Function to log messages with timestamps
@@ -24,23 +19,6 @@ log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"
}
-# Function to fetch tags from GitHub API
-fetch_tags() {
- local url="$1"
- local token="$2"
- local response
- if [ -n "$token" ]; then
- response=$(curl -H "Authorization: token $token" "$url")
- else
- response=$(curl "$url")
- if [[ "$response" == *'"message":"API rate limit exceeded'* ]]; then
- log "API rate limit exceeded. Please consider using a GitHub token for authentication."
- exit 1
- fi
- fi
- echo "$response"
-}
-
# Function to pull the Docker image
pull_docker_image() {
local version="$1"
@@ -81,17 +59,22 @@ copy_assets_from_container() {
# Parse command-line arguments
while [ "$#" -gt 0 ]; do
case "$1" in
- --github-token)
- GITHUB_TOKEN="$2"
- shift 2
- ;;
--version)
SPECIFIC_VERSION="$2"
shift 2
;;
+ --help)
+ usage
+ exit 0
+ ;;
+ --list)
+ versions
+ exit 0
+ ;;
*)
echo "Invalid argument: $1"
usage
+ exit 1
;;
esac
done
@@ -100,44 +83,38 @@ done
USER="asadarafat"
REPO="topoViewer"
-# Ensure jq is installed
-check_jq
-
# GitHub API URL for tags
API_URL="https://api.github.com/repos/$USER/$REPO/tags"
log "The API_URL is: $API_URL"
log "Fetching available versions...."
-tags_response=$(fetch_tags "$API_URL" "$GITHUB_TOKEN")
-tags=$(echo "$tags_response" | jq -r '.[] | select(.name | test("komodo") | not) | .name')
-
-log "All available versions:"
-echo "$tags"
+tags=$(git ls-remote --tags https://github.com/asadarafat/topoViewer.git | grep -v komodo | sed -n 's|.*refs/tags/\(nightly.*\)|\1|p' | sort -r)
# Convert the tags into an array
tags_array=($tags)
# Determine the version to download
if [ -z "$SPECIFIC_VERSION" ]; then
- LATEST_VERSION="${tags_array[0]}"
+ # Get latest version
+ VERSION_TO_INSTALL="${tags_array[0]}"
else
- LATEST_VERSION="$SPECIFIC_VERSION"
+ VERSION_TO_INSTALL="$SPECIFIC_VERSION"
# Check if the specified version exists
- if ! echo "${tags_array[@]}" | grep -qw "$LATEST_VERSION"; then
- log "Specified version $LATEST_VERSION not found. Available versions are:"
+ if ! echo "${tags_array[@]}" | grep -qw "$VERSION_TO_INSTALL"; then
+ log "Specified version $VERSION_TO_INSTALL not found. Available versions are:"
echo "$tags"
exit 1
fi
fi
-log "The version to install is $LATEST_VERSION"
+log "The version to install is $VERSION_TO_INSTALL"
# Pull the Docker image for the specified or latest version
-pull_docker_image "$SPECIFIC_VERSION"
+pull_docker_image "$VERSION_TO_INSTALL"
# Copy assets from the container to the host
-copy_assets_from_container "$SPECIFIC_VERSION"
+copy_assets_from_container "$VERSION_TO_INSTALL"
-log "Installation complete. topoViewer version $SPECIFIC_VERSION is now available in /opt/topoviewer."
+log "Installation complete. topoViewer version $VERSION_TO_INSTALL is now available in /opt/topoviewer."
exit 0