Skip to content

Commit 137fdfc

Browse files
authored
Topoeditor node type field (#178)
* initial code of type field addition in node panel of topoeditor * Provide default node attributes for Nokia SR Linux when a node is created in the TopoEditor. * revert to default-type * use clab.schema.json for defining srl and sros node * retrive existing type from clab.yaml to nodePanel and tidy up icon dropdown contents * bumped the version
1 parent ea12f05 commit 137fdfc

File tree

10 files changed

+333
-181
lines changed

10 files changed

+333
-181
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"icon": "resources/containerlab.png",
66
"description": "Manages containerlab topologies in VS Code",
77
"author": "SRL Labs",
8-
"version": "0.13.5",
8+
"version": "0.13.6",
99
"homepage": "https://containerlab.dev/manual/vsc-extension/",
1010
"engines": {
1111
"vscode": "^1.100.0"

src/topoViewer/backend/topoViewerAdaptorClab.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,7 @@ export class TopoViewerAdaptorClab {
329329
image: nodeObj.image ?? '',
330330
index: nodeIndex.toString(),
331331
kind: nodeObj.kind ?? '',
332+
type: nodeObj.type ?? '',
332333
labdir: `clab-${clabName}/`,
333334
labels: nodeObj.labels ?? {},
334335
longname: `clab-${clabName}-${nodeName}`,

src/topoViewer/backend/types/topoViewerType.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
export interface ClabNode {
88
kind?: string;
99
image?: string;
10+
type?: string;
1011
group?: string;
1112
labels?: Record<string, any>;
1213
}

src/topoViewer/webview-ui/html-static/css/style.css

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -414,8 +414,15 @@ body {
414414
/* Enables vertical scrolling */
415415
}
416416

417+
#panel-node-type-dropdown-content {
418+
max-height: 80px;
419+
/* Adjust height to show about 4 items */
420+
overflow-y: auto;
421+
/* Enables vertical scrolling */
422+
}
423+
417424
#panel-node-topoviewerrole-dropdown-content {
418-
max-height: 100px;
425+
max-height: 60px;
419426
/* Adjust height to show about 4 items */
420427
overflow-y: auto;
421428
/* Enables vertical scrolling */

src/topoViewerEditor/backend/topoViewerEditorWebUiFacade.ts

Lines changed: 159 additions & 147 deletions
Large diffs are not rendered by default.

src/topoViewerEditor/webview-ui/managerCytoscapeStyle.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -492,20 +492,23 @@ export default async function loadCytoStyle(
492492
/**
493493
* Extracts node types from an array of Cytoscape style definitions.
494494
*
495-
* This function looks for selectors of the form:
495+
* This function looks for selectors matching the pattern:
496496
* node[topoViewerRole="someType"]
497497
* and returns an array of node types (e.g., "router", "default", "pe", etc.).
498498
*
499-
* @returns An array of extracted node types.
499+
* Node types with topoViewerRole set to "dummyChild" or "group" are excluded.
500+
*
501+
* @returns An array of extracted node types, excluding "dummyChild" and "group".
500502
*/
501503
export function extractNodeIcons(): string[] {
502504
const nodeTypes: string[] = [];
503505
const regex = /node\[topoViewerRole="([^"]+)"\]/;
506+
const skipList = ['dummyChild', 'group'];
504507

505508
for (const styleDef of cytoscapeStylesBase) {
506509
if (typeof styleDef.selector === 'string') {
507510
const match = styleDef.selector.match(regex);
508-
if (match && match[1]) {
511+
if (match && match[1] && !skipList.includes(match[1])) {
509512
nodeTypes.push(match[1]);
510513
}
511514
}
@@ -515,6 +518,7 @@ export function extractNodeIcons(): string[] {
515518
}
516519

517520

521+
518522
/**
519523
* Generates an encoded SVG string to be used as a background image in Cytoscape.
520524
*

src/topoViewerEditor/webview-ui/managerViewportButtons.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -292,7 +292,7 @@ export class ManagerViewportButtons {
292292
sourceEndpoint: "",
293293
targetEndpoint: "",
294294
containerDockerExtraAttribute: { state: "", status: "" },
295-
extraData: { kind: "nokia_srlinux", longname: "", image: "", mgmtIpv4Address: "" },
295+
extraData: { kind: "nokia_srlinux", longname: "", image: "", type: "", mgmtIpv4Address: "" },
296296
};
297297

298298
// Get the current viewport bounds

src/topoViewerEditor/webview-ui/managerViewportPanels.ts

Lines changed: 107 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,10 @@ export class ManagerViewportPanels {
1919
public edgeClicked: boolean = false;
2020
// Variables to store the current selection for dropdowns.
2121
private panelNodeEditorKind: string = "nokia_srlinux";
22+
private panelNodeEditorType: string = "";
23+
private panelNodeEditorUseDropdownForType: boolean = false;
2224
private panelNodeEditorTopoViewerRole: string = "pe";
23-
25+
private nodeSchemaData: any = null;
2426
/**
2527
* Creates an instance of ManagerViewportPanels.
2628
* @param viewportButtons - The ManagerViewportButtons instance.
@@ -125,6 +127,12 @@ export class ManagerViewportPanels {
125127
panelNodeEditorImageLabel.value = 'ghcr.io/nokia/srlinux:latest';
126128
}
127129

130+
// Set the node type in the editor.
131+
const extraData = node.data('extraData') || {};
132+
this.panelNodeEditorKind = extraData.kind || this.panelNodeEditorKind;
133+
this.panelNodeEditorType = extraData.type || '';
134+
this.panelNodeEditorUseDropdownForType = false;
135+
128136
// Set the node group in the editor.
129137
const panelNodeEditorGroupLabel = document.getElementById("panel-node-editor-group") as HTMLInputElement;
130138
if (panelNodeEditorGroupLabel) {
@@ -149,21 +157,24 @@ export class ManagerViewportPanels {
149157
}
150158
const jsonData = await response.json();
151159

160+
this.nodeSchemaData = jsonData;
161+
152162
// Get kind enums from the JSON schema.
153163
const { kindOptions } = this.panelNodeEditorGetKindEnums(jsonData);
154164
console.log('Kind Enum:', kindOptions);
155165
// Populate the kind dropdown.
156166
this.panelNodeEditorPopulateKindDropdown(kindOptions);
157167

168+
const typeOptions = this.panelNodeEditorGetTypeEnumsByKindPattern(jsonData, `(${this.panelNodeEditorKind})`);
169+
this.panelNodeEditorSetupTypeField(typeOptions);
170+
158171
// Then call the function:
159172
const nodeIcons = extractNodeIcons();
160173
console.log("Extracted node icons:", nodeIcons);
161174

162175
this.panelNodeEditorPopulateTopoViewerRoleDropdown(nodeIcons);
163176

164-
// List type enums based on a kind pattern.
165-
const typeOptions = this.panelNodeEditorGetTypeEnumsByKindPattern(jsonData, '(srl|nokia_srlinux)');
166-
console.log('Type Enum for (srl|nokia_srlinux):', typeOptions);
177+
167178

168179
// Register the close button event.
169180
const panelNodeEditorCloseButton = document.getElementById("panel-node-editor-close-button");
@@ -214,7 +225,7 @@ export class ManagerViewportPanels {
214225
const panelLinkEditorIdLabelSaveBtn = document.getElementById("panel-link-editor-save-button");
215226

216227
// if (!panelLinkEditorIdLabel || !panelLinkEditor || !panelLinkEditorIdLabelSrcInput || !panelLinkEditorIdLabelTgtInput || !panelLinkEditorIdLabelCloseBtn || !panelLinkEditorIdLabelSaveBtn) {
217-
if (!panelLinkEditorIdLabel || !panelLinkEditor || !panelLinkEditorIdLabelSrcInput || !panelLinkEditorIdLabelTgtInput || !panelLinkEditorIdLabelSaveBtn) {
228+
if (!panelLinkEditorIdLabel || !panelLinkEditor || !panelLinkEditorIdLabelSrcInput || !panelLinkEditorIdLabelTgtInput || !panelLinkEditorIdLabelSaveBtn) {
218229

219230
console.error("panelEdgeEditor: missing required DOM elements");
220231
return;
@@ -314,6 +325,8 @@ export class ManagerViewportPanels {
314325
// Get the input values.
315326
const nodeNameInput = document.getElementById("panel-node-editor-name") as HTMLInputElement;
316327
const nodeImageInput = document.getElementById("panel-node-editor-image") as HTMLInputElement;
328+
const typeDropdownTrigger = document.querySelector("#panel-node-type-dropdown .dropdown-trigger button span");
329+
const typeInput = document.getElementById("panel-node-editor-type-input") as HTMLInputElement;
317330

318331
// Retrieve dropdown selections.
319332
const kindDropdownTrigger = document.querySelector("#panel-node-kind-dropdown .dropdown-trigger button span");
@@ -325,13 +338,23 @@ export class ManagerViewportPanels {
325338
const newName = nodeNameInput.value; // the new name
326339

327340
// Build updated extraData, preserving other fields.
341+
const typeValue = this.panelNodeEditorUseDropdownForType
342+
? (typeDropdownTrigger ? (typeDropdownTrigger as HTMLElement).textContent || '' : '')
343+
: (typeInput ? typeInput.value : '');
344+
328345
const updatedExtraData = {
329346
...currentData.extraData,
330347
name: nodeNameInput.value,
331348
image: nodeImageInput.value,
332349
kind: kindDropdownTrigger ? kindDropdownTrigger.textContent : 'nokia_srlinux',
333350
};
334351

352+
if (this.panelNodeEditorUseDropdownForType || typeValue.trim() !== '') {
353+
updatedExtraData.type = typeValue;
354+
} else if ('type' in updatedExtraData) {
355+
delete updatedExtraData.type;
356+
}
357+
335358
// Build the updated data object.
336359
const updatedData = {
337360
...currentData,
@@ -428,6 +451,73 @@ export class ManagerViewportPanels {
428451
console.log(`${this.panelNodeEditorKind} selected`);
429452
dropdownTrigger.textContent = this.panelNodeEditorKind;
430453
dropdownContainer.classList.remove("is-active");
454+
const typeOptions = this.panelNodeEditorGetTypeEnumsByKindPattern(this.nodeSchemaData, `(${option})`);
455+
// Reset the stored type when kind changes
456+
this.panelNodeEditorType = "";
457+
this.panelNodeEditorSetupTypeField(typeOptions);
458+
});
459+
460+
dropdownContent.appendChild(optionElement);
461+
});
462+
}
463+
464+
private panelNodeEditorSetupTypeField(options: string[]): void {
465+
const dropdown = document.getElementById("panel-node-type-dropdown");
466+
const input = document.getElementById("panel-node-editor-type-input") as HTMLInputElement;
467+
468+
if (!dropdown || !input) {
469+
console.error("Type input elements not found in the DOM.");
470+
return;
471+
}
472+
473+
if (options.length > 0) {
474+
dropdown.style.display = "";
475+
input.style.display = "none";
476+
this.panelNodeEditorUseDropdownForType = true;
477+
// Ensure type matches available options
478+
if (!options.includes(this.panelNodeEditorType)) {
479+
this.panelNodeEditorType = options[0];
480+
}
481+
this.panelNodeEditorPopulateTypeDropdown(options);
482+
} else {
483+
dropdown.style.display = "none";
484+
input.style.display = "";
485+
this.panelNodeEditorUseDropdownForType = false;
486+
input.value = this.panelNodeEditorType || "";
487+
input.oninput = () => {
488+
this.panelNodeEditorType = input.value;
489+
};
490+
}
491+
}
492+
493+
private panelNodeEditorPopulateTypeDropdown(options: string[]): void {
494+
const dropdownTrigger = document.querySelector("#panel-node-type-dropdown .dropdown-trigger button span");
495+
const dropdownContent = document.getElementById("panel-node-type-dropdown-content");
496+
const dropdownButton = document.querySelector("#panel-node-type-dropdown .dropdown-trigger button");
497+
const dropdownContainer = dropdownButton ? dropdownButton.closest(".dropdown") : null;
498+
499+
if (!dropdownTrigger || !dropdownContent || !dropdownButton || !dropdownContainer) {
500+
console.error("Dropdown elements not found in the DOM.");
501+
return;
502+
}
503+
504+
if (!options.includes(this.panelNodeEditorType)) {
505+
this.panelNodeEditorType = options.length > 0 ? options[0] : "";
506+
}
507+
dropdownTrigger.textContent = this.panelNodeEditorType || "";
508+
dropdownContent.innerHTML = "";
509+
510+
options.forEach(option => {
511+
const optionElement = document.createElement("a");
512+
optionElement.classList.add("dropdown-item", "label", "has-text-weight-normal", "is-small", "py-0");
513+
optionElement.textContent = option;
514+
optionElement.href = "#";
515+
516+
optionElement.addEventListener("click", (event) => {
517+
event.preventDefault();
518+
this.panelNodeEditorType = option;
519+
dropdownTrigger.textContent = this.panelNodeEditorType;
520+
dropdownContainer.classList.remove("is-active");
431521
});
432522

433523
dropdownContent.appendChild(optionElement);
@@ -525,13 +615,18 @@ export class ManagerViewportPanels {
525615
condition.if.properties.kind &&
526616
condition.if.properties.kind.pattern === pattern
527617
) {
528-
if (
529-
condition.then &&
530-
condition.then.properties &&
531-
condition.then.properties.type &&
532-
condition.then.properties.type.enum
533-
) {
534-
return condition.then.properties.type.enum;
618+
if (condition.then && condition.then.properties && condition.then.properties.type) {
619+
const typeProp = condition.then.properties.type;
620+
if (typeProp.enum) {
621+
return typeProp.enum;
622+
}
623+
if (Array.isArray(typeProp.anyOf)) {
624+
for (const sub of typeProp.anyOf) {
625+
if (sub.enum) {
626+
return sub.enum;
627+
}
628+
}
629+
}
535630
}
536631
}
537632
}

src/topoViewerEditor/webview-ui/template/vscodeHtmlTemplate.ts

Lines changed: 46 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1122,50 +1122,81 @@ export function getHTMLTemplate(
11221122
</div>
11231123
</div>
11241124
1125-
<!-- TopoViewer Role or Icons Dropdown -->
1125+
<!-- Image -->
11261126
<div class="column my-auto is-11">
11271127
<div class="panel-content">
11281128
<div class="columns is-mobile is-multiline py-auto">
11291129
<div class="column is-full-mobile is-half-tablet is-4 p-1">
1130-
<label for="panel-node-topoviewerrole-dropdown"
1131-
class="label is-size-7 has-text-right has-text-weight-medium">
1132-
Icons
1130+
<label for="panel-node-editor-image" class="label is-size-7 has-text-right has-text-weight-medium">
1131+
Image
11331132
</label>
11341133
</div>
11351134
<div class="column is-8 p-1 pl-3">
1136-
<div class="dropdown is-hoverable" id="panel-node-topoviewerrole-dropdown">
1135+
<input type="text" id="panel-node-editor-image"
1136+
class="input is-size-7 has-text-left link-impairment-widht has-text-weight-normal" />
1137+
</div>
1138+
</div>
1139+
</div>
1140+
</div>
1141+
1142+
<!-- Type Dropdown -->
1143+
<div class="column my-auto is-11">
1144+
<div class="panel-content">
1145+
<div class="columns is-mobile is-multiline py-auto">
1146+
<div class="column is-full-mobile is-half-tablet is-4 p-1">
1147+
<label for="panel-node-type-dropdown" class="label is-size-7 has-text-right has-text-weight-medium">
1148+
Type
1149+
</label>
1150+
</div>
1151+
<div class="column is-8 p-1 pl-3">
1152+
<div class="dropdown is-hoverable" id="panel-node-type-dropdown">
11371153
<div class="dropdown-trigger">
1138-
<button class="button is-size-7" type="button" aria-haspopup="true"
1139-
aria-controls="dropdown-menu-topoviewerrole">
1140-
<span>Select Icons</span>
1154+
<button class="button is-size-7" type="button" aria-haspopup="true" aria-controls="dropdown-menu">
1155+
<span>Select Type</span>
11411156
<span class="icon is-small">
11421157
<i class="fas fa-angle-down" aria-hidden="true"></i>
11431158
</span>
11441159
</button>
11451160
</div>
1146-
<div class="dropdown-menu" id="dropdown-menu-topoviewerrole" role="menu">
1147-
<div class="dropdown-content" id="panel-node-topoviewerrole-dropdown-content">
1161+
<div class="dropdown-menu" id="dropdown-menu" role="menu">
1162+
<div class="dropdown-content" id="panel-node-type-dropdown-content">
11481163
<!-- Dropdown items go here -->
11491164
</div>
11501165
</div>
11511166
</div>
1167+
<input type="text" id="panel-node-editor-type-input" class="input is-size-7 has-text-left link-impairment-widht has-text-weight-normal" style="display:none;" />
11521168
</div>
11531169
</div>
11541170
</div>
11551171
</div>
11561172
1157-
<!-- Image -->
1173+
<!-- TopoViewer Role or Icons Dropdown -->
11581174
<div class="column my-auto is-11">
11591175
<div class="panel-content">
11601176
<div class="columns is-mobile is-multiline py-auto">
11611177
<div class="column is-full-mobile is-half-tablet is-4 p-1">
1162-
<label for="panel-node-editor-image" class="label is-size-7 has-text-right has-text-weight-medium">
1163-
Image
1178+
<label for="panel-node-topoviewerrole-dropdown"
1179+
class="label is-size-7 has-text-right has-text-weight-medium">
1180+
Icons
11641181
</label>
11651182
</div>
11661183
<div class="column is-8 p-1 pl-3">
1167-
<input type="text" id="panel-node-editor-image"
1168-
class="input is-size-7 has-text-left link-impairment-widht has-text-weight-normal" />
1184+
<div class="dropdown is-hoverable" id="panel-node-topoviewerrole-dropdown">
1185+
<div class="dropdown-trigger">
1186+
<button class="button is-size-7" type="button" aria-haspopup="true"
1187+
aria-controls="dropdown-menu-topoviewerrole">
1188+
<span>Select Icons</span>
1189+
<span class="icon is-small">
1190+
<i class="fas fa-angle-down" aria-hidden="true"></i>
1191+
</span>
1192+
</button>
1193+
</div>
1194+
<div class="dropdown-menu" id="dropdown-menu-topoviewerrole" role="menu">
1195+
<div class="dropdown-content" id="panel-node-topoviewerrole-dropdown-content">
1196+
<!-- Dropdown items go here -->
1197+
</div>
1198+
</div>
1199+
</div>
11691200
</div>
11701201
</div>
11711202
</div>

src/topoViewerEditor/webview-ui/topoViewerEditorEngine.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,9 @@ export interface NodeData {
4646
};
4747
extraData?: {
4848
kind?: string;
49-
longname?: string;
5049
image?: string;
50+
type?: string
51+
longname?: string;
5152
mgmtIpv4Address?: string;
5253
};
5354
}

0 commit comments

Comments
 (0)