Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 16 additions & 11 deletions src/topoViewer/backend/topoViewerAdaptorClab.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,10 @@ export class TopoViewerAdaptorClab {


var clabName = parsed.name

var clabPrefix = parsed.prefix;
// if (clabPrefix == "") {
// clabPrefix = ""
// }


// Define the EnvironmentJson object
Expand All @@ -104,6 +107,7 @@ export class TopoViewerAdaptorClab {

const environmentJson: EnvironmentJson = {
workingDirectory: ".",
clabPrefix: `${clabPrefix}`,
clabName: `${clabName}`,
clabServerAddress: "",
clabAllowedHostname: hostname,
Expand All @@ -112,8 +116,7 @@ export class TopoViewerAdaptorClab {
deploymentType: "vs-code",
topoviewerVersion: `${topoViewerVersion}`,
topviewerPresetLayout: `${this.currentIsPresetLayout.toString()}`,
envCyTopoJsonBytes: cytoTopology,
envCyTopoJsonBytesAddon: cytoTopology
envCyTopoJsonBytes: cytoTopology
};

// Serialize EnvironmentJson with hyphenated keys
Expand Down Expand Up @@ -175,9 +178,9 @@ export class TopoViewerAdaptorClab {
* @returns An array of Cytoscape elements (`CyElement[]`) representing nodes and edges.
*/
public clabYamlToCytoscapeElements(yamlContent: string, clabTreeDataToTopoviewer: Record<string, ClabLabTreeNode> | undefined): CyElement[] {
const parsed = yaml.load(yamlContent) as ClabTopology;
return this.buildCytoscapeElements(parsed, { includeContainerData: true, clabTreeData: clabTreeDataToTopoviewer });
}
const parsed = yaml.load(yamlContent) as ClabTopology;
return this.buildCytoscapeElements(parsed, { includeContainerData: true, clabTreeData: clabTreeDataToTopoviewer });
}


/**
Expand All @@ -193,9 +196,9 @@ export class TopoViewerAdaptorClab {
* @returns An array of Cytoscape elements (`CyElement[]`) representing nodes and edges.
*/
public clabYamlToCytoscapeElementsEditor(yamlContent: string): CyElement[] {
const parsed = yaml.load(yamlContent) as ClabTopology;
return this.buildCytoscapeElements(parsed, { includeContainerData: false });
}
const parsed = yaml.load(yamlContent) as ClabTopology;
return this.buildCytoscapeElements(parsed, { includeContainerData: false });
}


/**
Expand Down Expand Up @@ -248,6 +251,7 @@ export class TopoViewerAdaptorClab {
private mapEnvironmentJsonToHyphenated(envJson: EnvironmentJson): string {
const hyphenatedJson = {
"working-directory": envJson.workingDirectory,
"clab-prefix": envJson.clabPrefix,
"clab-name": envJson.clabName,
"clab-server-address": envJson.clabServerAddress,
"clab-allowed-hostname": envJson.clabAllowedHostname,
Expand All @@ -256,8 +260,7 @@ export class TopoViewerAdaptorClab {
"deployment-type": envJson.deploymentType,
"topoviewer-version": envJson.topoviewerVersion,
"topoviewer-layout-preset": envJson.topviewerPresetLayout,
"EnvCyTopoJsonBytes": envJson.envCyTopoJsonBytes,
"EnvCyTopoJsonBytesAddon": envJson.envCyTopoJsonBytesAddon
"EnvCyTopoJsonBytes": envJson.envCyTopoJsonBytes
};

return JSON.stringify(hyphenatedJson, null, 2);
Expand All @@ -284,6 +287,8 @@ export class TopoViewerAdaptorClab {
log.info(`######### status preset layout: ${this.currentIsPresetLayout}`);

const clabName = parsed.name;


const parentMap = new Map<string, string | undefined>();
let nodeIndex = 0;

Expand Down
29 changes: 29 additions & 0 deletions src/topoViewer/backend/topoViewerWebUiFacade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -642,6 +642,35 @@ export class TopoViewer {
}
break;
}

case 'save-environment-json-to-disk': {
try {
const environmentData = JSON.parse(payload as string);

if (!this.lastFolderName) {
throw new Error('No folderName available (this.lastFolderName is undefined).');
}

// Construct full path under `topoViewerData/<folder>/environment.json`
const environmentJsonPath = vscode.Uri.joinPath(
this.context.extensionUri,
'topoViewerData',
this.lastFolderName,
'environment.json'
);

// Write to disk
await fs.promises.writeFile(environmentJsonPath.fsPath, JSON.stringify(environmentData, null, 2), 'utf8');

result = `Environment JSON successfully saved to disk at ${environmentJsonPath.fsPath}`;
log.info(result);
} catch (innerError) {
result = `Error saving environment JSON to disk.`;
log.error(`Error in 'save-environment-json-to-disk': ${JSON.stringify(innerError, null, 2)}`);
}
break;
}

default: {
error = `Unknown endpoint "${endpointName}".`;
log.error(error);
Expand Down
2 changes: 1 addition & 1 deletion src/topoViewer/backend/types/topoViewerType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export type CytoTopology = CyElement[];
*/
export interface EnvironmentJson {
workingDirectory: string;
clabPrefix: string;
clabName: string;
clabServerAddress: string;
clabAllowedHostname: string;
Expand All @@ -65,7 +66,6 @@ export interface EnvironmentJson {
topoviewerVersion: string;
topviewerPresetLayout: string
envCyTopoJsonBytes: CytoTopology | '';
envCyTopoJsonBytesAddon: CytoTopology | '';
}


Expand Down
51 changes: 42 additions & 9 deletions src/topoViewer/webview-ui/html-static/js/common.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ var globalNodeContainerStatusVisibility = false;
var globalShellUrl = "/js/cloudshell";
let deploymentType;
var globalLabName;
var globalPrefixName
var multiLayerViewPortState = false;

// Cytoscape-Leaflet variables
Expand Down Expand Up @@ -69,6 +70,7 @@ async function initEnv() {

// Assign to global variables
globalLabName = environments["clab-name"];
globalPrefixName = environments["clab-prefix"];
deploymentType = environments["deployment-type"];
globalIsPresetLayout = environments["topoviewer-layout-preset"] === "true";
globalAllowedhostname = environments["clab-allowed-hostname"];
Expand All @@ -90,36 +92,67 @@ async function initEnv() {
// -----------------------------------------------------------

/**
* Fetches environment configurations.
* Fetches environment configuration JSON and updates its topology data
* with the current live Cytoscape graph. Persists the updated JSON to disk
* using VS Code's postMessage API.
*
* @returns {Promise<object|null>} Updated environment object or null on failure.
*/
async function getEnvironments() {
try {
let environments;

if (isVscodeDeployment) {
// Using a JSON file in the VS Code deployment scenario
const response = await fetch(window.jsonFileUrlDataEnvironment);
if (!response.ok) {
throw new Error(`Network response not ok: ${response.status}`);
throw new Error(`Failed to fetch environment JSON: ${response.statusText}`);
}

environments = await response.json();

if (typeof cy !== 'undefined' && typeof cy.elements === 'function') {
const liveCyData = cy.elements().jsons();
environments.EnvCyTopoJsonBytes = liveCyData;

console.log("Updated environments with live Cytoscape data:", environments)
console.debug("Replaced EnvCyTopoJsonBytes with live Cytoscape data.");

if (typeof vsCode !== 'undefined' && typeof vsCode.postMessage === 'function') {
vsCode.postMessage({
type: "POST",
requestId: `save-${Date.now()}`,
endpointName: "save-environment-json-to-disk",
payload: JSON.stringify(environments)
});
console.log("[VS Code] saveEnvJsonToDisk message sent to extension host.");
} else {
console.warn("VS Code postMessage API not available — skipping save.");
}
} else {
console.warn("Cytoscape instance is not available. EnvCyTopoJsonBytes not updated.");
}
} else {
// Using a dedicated GET endpoint in other scenarios
environments = await sendRequestToEndpointGetV2("/get-environments");
}

if (environments && typeof environments === 'object' && Object.keys(environments).length > 0) {
console.log("Fetched Environments:", environments);
if (
environments &&
typeof environments === "object" &&
Object.keys(environments).length > 0
) {
console.debug("Final environments object:", environments);
return environments;
} else {
console.log("Empty or invalid JSON response for environments");
console.warn("Fetched environment object is empty or invalid.");
return null;
}
} catch (error) {
console.error("Error fetching environments:", error);
console.error("Error while fetching environments:", error);
return null;
}
}


/**
* Helper function to send a GET request to an endpoint.
*/
Expand Down Expand Up @@ -357,7 +390,7 @@ function updateSocketBinding() {
function updateMessageStreamBinding() {
// Create the event handler once and store it as a property if it doesn't exist.
if (!updateMessageStreamBinding.handler) {
updateMessageStreamBinding.handler = function(event) {
updateMessageStreamBinding.handler = function (event) {
try {
const message = event.data;
if (message && message.type === 'clab-tree-provider-data-native-vscode-message-stream') {
Expand Down
8 changes: 5 additions & 3 deletions src/topoViewer/webview-ui/html-static/js/dev.js
Original file line number Diff line number Diff line change
Expand Up @@ -2374,8 +2374,9 @@ async function linkWireshark(event, option, endpoint, referenceElementAfterId) {
netNsResponse = await sendRequestToEndpointGetV3("/clab-node-network-namespace", [clabSourceLongName]);
netNsId = extractNamespaceId(netNsResponse.namespace_id);
console.info("linkWireshark - netNsSource: ", netNsId);

urlParams = `container={"netns":${netNsId},"network-interfaces":["${clabSourcePort}"],"name":"${clabSourceLongName.toLowerCase()}","type":"docker","prefix":""}&nif=${clabSourcePort}`;
const edgeSharkHref = baseUrl + urlParams;
console.info("linkWireshark - edgeSharkHref: ", edgeSharkHref);
}
} else if (endpoint === "target") {
if (isVscodeDeployment) {
Expand All @@ -2393,10 +2394,11 @@ async function linkWireshark(event, option, endpoint, referenceElementAfterId) {
netNsId = extractNamespaceId(netNsResponse.namespace_id);
console.info("linkWireshark - netNsTarget: ", netNsId);
urlParams = `container={"netns":${netNsId},"network-interfaces":["${clabTargetPort}"],"name":"${clabTargetLongName.toLowerCase()}","type":"docker","prefix":""}&nif=${clabTargetPort}`;
const edgeSharkHref = baseUrl + urlParams;
console.info("linkWireshark - edgeSharkHref: ", edgeSharkHref);
}
}
const edgeSharkHref = baseUrl + urlParams;
console.info("linkWireshark - edgeSharkHref: ", edgeSharkHref);


// window.open(edgeSharkHref);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,35 +48,77 @@
function socketDataEncrichmentLink(labData) {
const linkMap = new Map();

// Iterate through all lab entries and gather interface data from matching lab
console.log(`debug labData:`, labData);
console.log(`debug labData.name`, labData.name);
console.log(`debug globalLabName`, globalLabName);

// Build interface key mapping for the current lab
Object.values(labData).forEach(lab => {
if (lab.name !== globalLabName || !Array.isArray(lab.containers)) return;

lab.containers.forEach(container => {
if (typeof container.label !== 'string' || !Array.isArray(container.interfaces)) return;

// Derive short node name by stripping lab prefix from the container label
const nodeName = container.label.split(lab.name)[1]?.replace(/^-/, '') || container.label;

// Build key per interface and store MAC/MTU/type

container.interfaces.forEach(iface => {
const key = `${lab.name}::${nodeName}::${iface.label}`;
linkMap.set(key, { mac: iface.mac, mtu: iface.mtu, type: iface.type });
});

console.log(`Enriched link data for node: ${nodeName} with interfaces:`, container.interfaces);
console.log(`Enriched link map data:`, linkMap);
});
});

// Match the key parts with Cytoscape edge's source/target endpoint data
// Compute prefix safely
let assignedPrefixLabName;

switch (true) {
case typeof globalPrefixName === "string" && globalPrefixName.trim() === "undefined":
assignedPrefixLabName = `clab-${globalLabName}-`;
break;

case typeof globalPrefixName === "string" && globalPrefixName.trim() !== "":
assignedPrefixLabName = `${globalPrefixName.trim()}-${globalLabName}-`;
break;

default:
assignedPrefixLabName = null;
break;
}

// Enrich edges
linkMap.forEach((iface, key) => {
const [, nodeName, endpoint] = key.split('::');
cy.edges().forEach(edge => {
const data = edge.data();

// Safely build clabSourceLongName and clabTargetLongName
const clabSourceLongName = assignedPrefixLabName
? `${assignedPrefixLabName}${data.source}`
: data.source;

const clabTargetLongName = assignedPrefixLabName
? `${assignedPrefixLabName}${data.target}`
: data.target;

const updatedExtraData = {
...edge.data('extraData'),
clabSourceLongName,
clabTargetLongName
};
edge.data('extraData', updatedExtraData);

// Enrich with interface details if matched
if (data.source === nodeName && data.sourceEndpoint === endpoint) {
edge.data({ sourceMac: iface.mac, sourceMtu: iface.mtu, sourceType: iface.type });
}
if (data.target === nodeName && data.targetEndpoint === endpoint) {
edge.data({ targetMac: iface.mac, targetMtu: iface.mtu, targetType: iface.type });
}

console.log(`Edge data after enrichment:`, edge.data());
});
});
}
Expand All @@ -101,7 +143,7 @@ function socketDataEncrichmentLink(labData) {
*/
function socketDataEncrichmentNode(labData) {
const nodeMap = new Map();

// Build node mapping from container longname -> metadata
Object.values(labData).forEach(lab => {
if (lab.name !== globalLabName || !Array.isArray(lab.containers)) return;
Expand Down