a03dc989d3412d3829c70f7f94254db3a10747b6
[laas.git] / src / static / js / dashboard.js
1 ///////////////////
2 // Global Variables
3 ///////////////////
4
5 form_submission_callbacks = [];  //all runnables will be executed before form submission
6
7
8 ///////////////////
9 // Global Functions
10 ///////////////////
11
12 function updatePage(data){
13     updateBreadcrumbs(data['meta']);
14     $("formContainer").html(data['content']);
15 }
16
17 function submitStepForm(next_step = "current"){
18     run_form_callbacks();
19     const step_form_data = $("#step_form").serialize();
20     const form_data = $.param({
21         "step": next_step,
22         "step_form": step_form_data,
23         "csrfmiddlewaretoken": $("[name=csrfmiddlewaretoken]").val()
24     });
25     console.log(form_data);
26     $.post(
27         '/workflow/manager/',
28         form_data,
29         (data) => updatePage(data),
30         'json'
31     ).fail(() => alert("failure"));
32 }
33
34 function run_form_callbacks(){
35     for(f of form_submission_callbacks)
36         f();
37     form_submission_callbacks = [];
38 }
39
40 ///////////////////
41 //Class Definitions
42 ///////////////////
43
44 class MultipleSelectFilterWidget {
45
46     constructor(neighbors, items, initial) {
47         this.inputs = [];
48         this.graph_neighbors = neighbors;
49         this.filter_items = items;
50         this.result = {};
51         this.dropdown_count = 0;
52
53         for(let nodeId in this.filter_items) {
54             const node = this.filter_items[nodeId];
55             this.result[node.class] = {}
56         }
57
58         this.make_selection(initial);
59     }
60
61     make_selection( initial_data ){
62         if(!initial_data || jQuery.isEmptyObject(initial_data))
63             return;
64         for(let item_class in initial_data) {
65             const selected_items = initial_data[item_class];
66             for( let node_id in selected_items ){
67                 const node = this.filter_items[node_id];
68                 const selection_data = selected_items[node_id]
69                 if( selection_data.selected ) {
70                     this.select(node);
71                     this.markAndSweep(node);
72                     this.updateResult(node);
73                 }
74                 if(node['multiple']){
75                     this.make_multiple_selection(node, selection_data);
76                 }
77             }
78         }
79     }
80
81     make_multiple_selection(node, selection_data){
82         const prepop_data = selection_data.values;
83         for(let k in prepop_data){
84             const div = this.add_item_prepopulate(node, prepop_data[k]);
85             this.updateObjectResult(node, div.id, prepop_data[k]);
86         }
87     }
88
89     markAndSweep(root){
90         for(let i in this.filter_items) {
91             const node = this.filter_items[i];
92             node['marked'] = true; //mark all nodes
93         }
94
95         const toCheck = [root];
96         while(toCheck.length > 0){
97             const node = toCheck.pop();
98             if(!node['marked']) {
99                 continue; //already visited, just continue
100             }
101             node['marked'] = false; //mark as visited
102             if(node['follow'] || node == root){ //add neighbors if we want to follow this node
103                 const neighbors = this.graph_neighbors[node.id];
104                 for(let neighId of neighbors) {
105                     const neighbor = this.filter_items[neighId];
106                     toCheck.push(neighbor);
107                 }
108             }
109         }
110
111         //now remove all nodes still marked
112         for(let i in this.filter_items){
113             const node = this.filter_items[i];
114             if(node['marked']){
115                 this.disable_node(node);
116             }
117         }
118     }
119
120     process(node) {
121         if(node['selected']) {
122             this.markAndSweep(node);
123         }
124         else {  //TODO: make this not dumb
125             const selected = []
126             //remember the currently selected, then reset everything and reselect one at a time
127             for(let nodeId in this.filter_items) {
128                 node = this.filter_items[nodeId];
129                 if(node['selected']) {
130                     selected.push(node);
131                 }
132                 this.clear(node);
133             }
134             for(let node of selected) {
135                 this.select(node);
136                 this.markAndSweep(node);
137             }
138         }
139     }
140
141     select(node) {
142         const elem = document.getElementById(node['id']);
143         node['selected'] = true;
144         elem.classList.remove('bg-white', 'not-allowed', 'bg-light');
145         elem.classList.add('selected_node');
146     }
147
148     clear(node) {
149         const elem = document.getElementById(node['id']);
150         node['selected'] = false;
151         node['selectable'] = true;
152         elem.classList.add('bg-white')
153         elem.classList.remove('not-allowed', 'bg-light', 'selected_node');
154     }
155
156     disable_node(node) {
157         const elem = document.getElementById(node['id']);
158         node['selected'] = false;
159         node['selectable'] = false;
160         elem.classList.remove('bg-white', 'selected_node');
161         elem.classList.add('not-allowed', 'bg-light');
162     }
163
164     processClick(id){
165         const node = this.filter_items[id];
166         if(!node['selectable'])
167             return;
168
169         if(node['multiple']){
170             return this.processClickMultiple(node);
171         } else {
172             return this.processClickSingle(node);
173         }
174     }
175
176     processClickSingle(node){
177         node['selected'] = !node['selected']; //toggle on click
178         if(node['selected']) {
179             this.select(node);
180         } else {
181             this.clear(node);
182         }
183         this.process(node);
184         this.updateResult(node);
185     }
186
187     processClickMultiple(node){
188         this.select(node);
189         const div = this.add_item_prepopulate(node, false);
190         this.process(node);
191         this.updateObjectResult(node, div.id, "");
192     }
193
194     restrictchars(input){
195         if( input.validity.patternMismatch ){
196             input.setCustomValidity("Only alphanumeric characters (a-z, A-Z, 0-9), underscore(_), and hyphen (-) are allowed");
197             input.reportValidity();
198         }
199         input.value = input.value.replace(/([^A-Za-z0-9-_.])+/g, "");
200         this.checkunique(input);
201     }
202
203     checkunique(tocheck){ //TODO: use set
204         const val = tocheck.value;
205         for( let input of this.inputs ){
206             if( input.value == val && input != tocheck){
207                 tocheck.setCustomValidity("All hostnames must be unique");
208                 tocheck.reportValidity();
209                 return;
210             }
211         }
212         tocheck.setCustomValidity("");
213     }
214
215     make_remove_button(div, node){
216         const button = document.createElement("BUTTON");
217         button.type = "button";
218         button.appendChild(document.createTextNode("Remove"));
219         button.classList.add("btn", "btn-danger", "d-inline-block");
220         const that = this;
221         button.onclick = function(){ that.remove_dropdown(div.id, node.id); }
222         return button;
223     }
224
225     make_input(div, node, prepopulate){
226         const input = document.createElement("INPUT");
227         input.type = node.form.type;
228         input.name = node.id + node.form.name
229         input.classList.add("form-control", "w-auto", "d-inline-block");
230         input.pattern = "(?=^.{1,253}$)(^([A-Za-z0-9-_]{1,62}\.)*[A-Za-z0-9-_]{1,63})";
231         input.title = "Only alphanumeric characters (a-z, A-Z, 0-9), underscore(_), and hyphen (-) are allowed"
232         input.placeholder = node.form.placeholder;
233         this.inputs.push(input);
234         const that = this;
235         input.onchange = function() { that.updateObjectResult(node, div.id, input.value); that.restrictchars(this); };
236         input.oninput = function() { that.restrictchars(this); };
237         if(prepopulate)
238             input.value = prepopulate;
239         return input;
240     }
241
242     add_item_prepopulate(node, prepopulate){
243         const div = document.createElement("DIV");
244         div.id = "dropdown_" + this.dropdown_count;
245         div.classList.add("list-group-item");
246         this.dropdown_count++;
247         const label = document.createElement("H5")
248         label.appendChild(document.createTextNode(node['name']))
249         div.appendChild(label);
250         div.appendChild(this.make_input(div, node, prepopulate));
251         div.appendChild(this.make_remove_button(div, node));
252         document.getElementById("dropdown_wrapper").appendChild(div);
253         return div;
254     }
255
256     remove_dropdown(div_id, node_id){
257         const div = document.getElementById(div_id);
258         const node = this.filter_items[node_id]
259         const parent = div.parentNode;
260         div.parentNode.removeChild(div);
261         delete this.result[node.class][node.id]['values'][div.id];
262
263         //checks if we have removed last item in class
264         if(jQuery.isEmptyObject(this.result[node.class][node.id]['values'])){
265             delete this.result[node.class][node.id];
266             this.clear(node);
267         }
268     }
269
270     updateResult(node){
271         if(!node['multiple']){
272             this.result[node.class][node.id] = {selected: node.selected, id: node.model_id}
273             if(!node.selected)
274                 delete this.result[node.class][node.id];
275         }
276     }
277
278     updateObjectResult(node, childKey, childValue){
279         if(!this.result[node.class][node.id])
280             this.result[node.class][node.id] = {selected: true, id: node.model_id, values: {}}
281
282         this.result[node.class][node.id]['values'][childKey] = childValue;
283     }
284
285     finish(){
286         document.getElementById("filter_field").value = JSON.stringify(this.result);
287     }
288 }
289
290 class NetworkStep {
291     constructor(debug, xml, hosts, added_hosts, removed_host_ids, graphContainer, overviewContainer, toolbarContainer){
292         if(!this.check_support())
293             return;
294
295         this.currentWindow = null;
296         this.netCount = 0;
297         this.netColors = ['red', 'blue', 'purple', 'green', 'orange', '#8CCDF5', '#1E9BAC'];
298         this.hostCount = 0;
299         this.lastHostBottom = 100;
300         this.networks = new Set();
301         this.has_public_net = false;
302         this.debug = debug;
303         this.editor = new mxEditor();
304         this.graph = this.editor.graph;
305
306         this.editor.setGraphContainer(graphContainer);
307         this.doGlobalConfig();
308         this.prefill(xml, hosts, added_hosts, removed_host_ids);
309         this.addToolbarButton(this.editor, toolbarContainer, 'zoomIn', '', "/static/img/mxgraph/zoom_in.png", true);
310         this.addToolbarButton(this.editor, toolbarContainer, 'zoomOut', '', "/static/img/mxgraph/zoom_out.png", true);
311
312         if(this.debug){
313             this.editor.addAction('printXML', function(editor, cell) {
314                 mxLog.write(this.encodeGraph());
315                 mxLog.show();
316             }.bind(this));
317             this.addToolbarButton(this.editor, toolbarContainer, 'printXML', '', '/static/img/mxgraph/fit_to_size.png', true);
318         }
319
320         new mxOutline(this.graph, overviewContainer);
321         //sets the edge color to be the same as the network
322         this.graph.addListener(mxEvent.CELL_CONNECTED, function(sender, event) {this.cellConnectionHandler(sender, event)}.bind(this));
323         //hooks up double click functionality
324         this.graph.dblClick = function(evt, cell) {this.doubleClickHandler(evt, cell);}.bind(this);
325
326         if(!this.has_public_net){
327             this.addPublicNetwork();
328         }
329     }
330
331     check_support(){
332         if (!mxClient.isBrowserSupported()) {
333             mxUtils.error('Browser is not supported', 200, false);
334             return false;
335         }
336         return true;
337     }
338
339     prefill(xml, hosts, added_hosts, removed_host_ids){
340         //populate existing data
341         if(xml){
342             this.restoreFromXml(xml, this.editor);
343         } else if(hosts){
344             for(const host of hosts)
345                 this.makeHost(host);
346         }
347
348         //apply any changes
349         if(added_hosts){
350             for(const host of added_hosts)
351                 this.makeHost(host);
352             this.updateHosts([]); //TODO: why?
353         }
354         this.updateHosts(removed_host_ids);
355     }
356
357     cellConnectionHandler(sender, event){
358         const edge = event.getProperty('edge');
359         const terminal = event.getProperty('terminal')
360         const source = event.getProperty('source');
361         if(this.checkAllowed(edge, terminal, source)) {
362             this.colorEdge(edge, terminal, source);
363             this.alertVlan(edge, terminal, source);
364         }
365     }
366
367     doubleClickHandler(evt, cell) {
368         if( cell != null ){
369             if( cell.getParent() != null && cell.getParent().getId().indexOf("network") > -1) {
370                 cell = cell.getParent();
371             }
372             if( cell.isEdge() || cell.getId().indexOf("network") > -1 ) {
373                 this.createDeleteDialog(cell.getId());
374             }
375             else {
376                 this.showDetailWindow(cell);
377            }
378         }
379     }
380
381     alertVlan(edge, terminal, source) {
382         if( terminal == null || edge.getTerminal(!source) == null) {
383             return;
384         }
385         const form = document.createElement("form");
386         const tagged = document.createElement("input");
387         tagged.type = "radio";
388         tagged.name = "tagged";
389         tagged.value = "True";
390         form.appendChild(tagged);
391         form.appendChild(document.createTextNode(" Tagged"));
392         form.appendChild(document.createElement("br"));
393
394         const untagged = document.createElement("input");
395         untagged.type = "radio";
396         untagged.name = "tagged";
397         untagged.value = "False";
398         form.appendChild(untagged);
399         form.appendChild(document.createTextNode(" Untagged"));
400         form.appendChild(document.createElement("br"));
401
402         const yes_button = document.createElement("button");
403         yes_button.onclick = function() {this.parseVlanWindow(edge.getId());}.bind(this);
404         yes_button.appendChild(document.createTextNode("Okay"));
405
406         const cancel_button = document.createElement("button");
407         cancel_button.onclick = function() {this.deleteVlanWindow(edge.getId());}.bind(this);
408         cancel_button.appendChild(document.createTextNode("Cancel"));
409
410         const error_div = document.createElement("div");
411         error_div.id = "current_window_errors";
412         form.appendChild(error_div);
413
414         const content = document.createElement('div');
415         content.appendChild(form);
416         content.appendChild(yes_button);
417         content.appendChild(cancel_button);
418         this.showWindow("Vlan Selection", content, 200, 200);
419     }
420
421     createDeleteDialog(id) {
422         const content = document.createElement('div');
423         const remove_button = document.createElement("button");
424         remove_button.style.width = '46%';
425         remove_button.onclick = function() { this.deleteCell(id);}.bind(this);
426         remove_button.appendChild(document.createTextNode("Remove"));
427         const cancel_button = document.createElement("button");
428         cancel_button.style.width = '46%';
429         cancel_button.onclick = function() { this.closeWindow();}.bind(this);
430         cancel_button.appendChild(document.createTextNode("Cancel"));
431
432         content.appendChild(remove_button);
433         content.appendChild(cancel_button);
434         this.showWindow('Do you want to delete this network?', content, 200, 62);
435     }
436
437     checkAllowed(edge, terminal, source) {
438         //check if other terminal is null, and that they are different
439         const otherTerminal = edge.getTerminal(!source);
440         if(terminal != null && otherTerminal != null) {
441             if( terminal.getParent().getId().split('_')[0] == //'host' or 'network'
442                 otherTerminal.getParent().getId().split('_')[0] ) {
443                 //not allowed
444                 this.graph.removeCells([edge]);
445                 return false;
446             }
447         }
448         return true;
449     }
450
451     colorEdge(edge, terminal, source) {
452         if(terminal.getParent().getId().indexOf('network') >= 0) {
453             const styles = terminal.getParent().getStyle().split(';');
454             let color = 'black';
455             for(let style of styles){
456                 const kvp = style.split('=');
457                 if(kvp[0] == "fillColor"){
458                     color = kvp[1];
459                 }
460             }
461             edge.setStyle('strokeColor=' + color);
462         }
463     }
464
465     showDetailWindow(cell) {
466         const info = JSON.parse(cell.getValue());
467         const content = document.createElement("div");
468         const pre_tag = document.createElement("pre");
469         pre_tag.appendChild(document.createTextNode("Name: " + info.name + "\nDescription:\n" + info.description));
470         const ok_button = document.createElement("button");
471         ok_button.onclick = function() { this.closeWindow();};
472         content.appendChild(pre_tag);
473         content.appendChild(ok_button);
474         this.showWindow('Details', content, 400, 400);
475     }
476
477     restoreFromXml(xml, editor) {
478         const doc = mxUtils.parseXml(xml);
479         const node = doc.documentElement;
480         editor.readGraphModel(node);
481
482         //Iterate over all children, and parse the networks to add them to the sidebar
483         for( const cell of this.graph.getModel().getChildren(this.graph.getDefaultParent())) {
484             if(cell.getId().indexOf("network") > -1) {
485                 const info = JSON.parse(cell.getValue());
486                 const name = info['name'];
487                 this.networks.add(name);
488                 const styles = cell.getStyle().split(";");
489                 let color = null;
490                 for(const style of styles){
491                     const kvp = style.split('=');
492                     if(kvp[0] == "fillColor") {
493                         color = kvp[1];
494                         break;
495                     }
496                 }
497                 if(info.public){
498                     this.has_public_net = true;
499                 }
500                 this.netCount++;
501                 this.makeSidebarNetwork(name, color, cell.getId());
502             }
503         }
504     }
505
506     deleteCell(cellId) {
507         var cell = this.graph.getModel().getCell(cellId);
508         if( cellId.indexOf("network") > -1 ) {
509             let elem = document.getElementById(cellId);
510             elem.parentElement.removeChild(elem);
511         }
512         this.graph.removeCells([cell]);
513         this.currentWindow.destroy();
514     }
515
516     newNetworkWindow() {
517         const input = document.createElement("input");
518         input.type = "text";
519         input.name = "net_name";
520         input.maxlength = 100;
521         input.id = "net_name_input";
522         input.style.margin = "5px";
523
524         const yes_button = document.createElement("button");
525         yes_button.onclick = function() {this.parseNetworkWindow();}.bind(this);
526         yes_button.appendChild(document.createTextNode("Okay"));
527
528         const cancel_button = document.createElement("button");
529         cancel_button.onclick = function() {this.closeWindow();}.bind(this);
530         cancel_button.appendChild(document.createTextNode("Cancel"));
531
532         const error_div = document.createElement("div");
533         error_div.id = "current_window_errors";
534
535         const content = document.createElement("div");
536         content.appendChild(document.createTextNode("Name: "));
537         content.appendChild(input);
538         content.appendChild(document.createElement("br"));
539         content.appendChild(yes_button);
540         content.appendChild(cancel_button);
541         content.appendChild(document.createElement("br"));
542         content.appendChild(error_div);
543
544         this.showWindow("Network Creation", content, 300, 300);
545     }
546
547     parseNetworkWindow() {
548         const net_name = document.getElementById("net_name_input").value
549         const error_div = document.getElementById("current_window_errors");
550         if( this.networks.has(net_name) ){
551             error_div.innerHTML = "All network names must be unique";
552             return;
553         }
554         this.addNetwork(net_name);
555         this.currentWindow.destroy();
556     }
557
558     addToolbarButton(editor, toolbar, action, label, image, isTransparent) {
559         const button = document.createElement('button');
560         button.style.fontSize = '10';
561         if (image != null) {
562             const img = document.createElement('img');
563             img.setAttribute('src', image);
564             img.style.width = '16px';
565             img.style.height = '16px';
566             img.style.verticalAlign = 'middle';
567             img.style.marginRight = '2px';
568             button.appendChild(img);
569         }
570         if (isTransparent) {
571             button.style.background = 'transparent';
572             button.style.color = '#FFFFFF';
573             button.style.border = 'none';
574         }
575         mxEvent.addListener(button, 'click', function(evt) {
576             editor.execute(action);
577         });
578         mxUtils.write(button, label);
579         toolbar.appendChild(button);
580     };
581
582     encodeGraph() {
583         const encoder = new mxCodec();
584         const xml = encoder.encode(this.graph.getModel());
585         return mxUtils.getXml(xml);
586     }
587
588     doGlobalConfig() {
589         //general graph stuff
590         this.graph.setMultigraph(false);
591         this.graph.setCellsSelectable(false);
592         this.graph.setCellsMovable(false);
593
594         //testing
595         this.graph.vertexLabelIsMovable = true;
596
597         //edge behavior
598         this.graph.setConnectable(true);
599         this.graph.setAllowDanglingEdges(false);
600         mxEdgeHandler.prototype.snapToTerminals = true;
601         mxConstants.MIN_HOTSPOT_SIZE = 16;
602         mxConstants.DEFAULT_HOTSPOT = 1;
603         //edge 'style' (still affects behavior greatly)
604         const style = this.graph.getStylesheet().getDefaultEdgeStyle();
605         style[mxConstants.STYLE_EDGE] = mxConstants.EDGESTYLE_ELBOW;
606         style[mxConstants.STYLE_ENDARROW] = mxConstants.NONE;
607         style[mxConstants.STYLE_ROUNDED] = true;
608         style[mxConstants.STYLE_FONTCOLOR] = 'black';
609         style[mxConstants.STYLE_STROKECOLOR] = 'red';
610         style[mxConstants.STYLE_LABEL_BACKGROUNDCOLOR] = '#FFFFFF';
611         style[mxConstants.STYLE_STROKEWIDTH] = '3';
612         style[mxConstants.STYLE_ROUNDED] = true;
613         style[mxConstants.STYLE_EDGE] = mxEdgeStyle.EntityRelation;
614
615         const hostStyle = this.graph.getStylesheet().getDefaultVertexStyle();
616         hostStyle[mxConstants.STYLE_ROUNDED] = 1;
617
618         this.graph.convertValueToString = function(cell) {
619             try{
620                 //changes value for edges with xml value
621                 if(cell.isEdge()) {
622                     if(JSON.parse(cell.getValue())["tagged"]) {
623                         return "tagged";
624                     }
625                     return "untagged";
626                 }
627                 else{
628                     return JSON.parse(cell.getValue())['name'];
629                 }
630             }
631             catch(e){
632                 return cell.getValue();
633             }
634         };
635     }
636
637     showWindow(title, content, width, height) {
638         //create transparent black background
639         const background = document.createElement('div');
640         background.style.position = 'absolute';
641         background.style.left = '0px';
642         background.style.top = '0px';
643         background.style.right = '0px';
644         background.style.bottom = '0px';
645         background.style.background = 'black';
646         mxUtils.setOpacity(background, 50);
647         document.body.appendChild(background);
648
649         const x = Math.max(0, document.body.scrollWidth/2-width/2);
650         const y = Math.max(10, (document.body.scrollHeight ||
651                     document.documentElement.scrollHeight)/2-height*2/3);
652
653         const wnd = new mxWindow(title, content, x, y, width, height, false, true);
654         wnd.setClosable(false);
655
656         wnd.addListener(mxEvent.DESTROY, function(evt) {
657             this.graph.setEnabled(true);
658             mxEffects.fadeOut(background, 50, true, 10, 30, true);
659         }.bind(this));
660         this.currentWindow = wnd;
661
662         this.graph.setEnabled(false);
663         this.currentWindow.setVisible(true);
664     };
665
666     closeWindow() {
667         //allows the current window to be destroyed
668         this.currentWindow.destroy();
669     };
670
671     othersUntagged(edgeID) {
672         const edge = this.graph.getModel().getCell(edgeID);
673         const end1 = edge.getTerminal(true);
674         const end2 = edge.getTerminal(false);
675
676         if( end1.getParent().getId().split('_')[0] == 'host' ){
677             var netint = end1;
678         } else {
679             var netint = end2;
680         }
681
682         var edges = netint.edges;
683         for( let edge of edges) {
684             if( edge.getValue() ) {
685                 var tagged = JSON.parse(edge.getValue()).tagged;
686             } else {
687                 var tagged = true;
688             }
689             if( !tagged ) {
690                 return true;
691             }
692         }
693         return false;
694     };
695
696
697     deleteVlanWindow(edgeID) {
698         const cell = this.graph.getModel().getCell(edgeID);
699         this.graph.removeCells([cell]);
700         this.currentWindow.destroy();
701     }
702
703     parseVlanWindow(edgeID) {
704         //do parsing and data manipulation
705         const radios = document.getElementsByName("tagged");
706         const edge = this.graph.getModel().getCell(edgeID);
707
708         for(let radio of radios){
709             if(radio.checked) {
710                 //set edge to be tagged or untagged
711                 if( radio.value == "False") {
712                     if( this.othersUntagged(edgeID) ) {
713                         document.getElementById("current_window_errors").innerHTML = "Only one untagged vlan per interface is allowed.";
714                         return;
715                     }
716                 }
717                 const edgeVal = {tagged: radio.value == "True"};
718                 edge.setValue(JSON.stringify(edgeVal));
719                 break;
720             }
721         }
722         this.graph.refresh(edge);
723         this.closeWindow();
724     }
725
726     makeMxNetwork(net_name, is_public = false) {
727         const model = this.graph.getModel();
728         const width = 10;
729         const height = 1700;
730         const xoff = 400 + (30 * this.netCount);
731         const yoff = -10;
732         let color = this.netColors[this.netCount];
733         if( this.netCount > (this.netColors.length - 1)) {
734             color = Math.floor(Math.random() * 16777215); //int in possible color space
735             color = '#' + color.toString(16).toUpperCase(); //convert to hex
736         }
737         const net_val = { name: net_name, public: is_public};
738         const net = this.graph.insertVertex(
739             this.graph.getDefaultParent(),
740             'network_' + this.netCount,
741             JSON.stringify(net_val),
742             xoff,
743             yoff,
744             width,
745             height,
746             'fillColor=' + color,
747             false
748         );
749         const num_ports = 45;
750         for(var i=0; i<num_ports; i++){
751             let port = this.graph.insertVertex(
752                 net,
753                 null,
754                 '',
755                 0,
756                 (1/num_ports) * i,
757                 10,
758                 height / num_ports,
759                 'fillColor=black;opacity=0',
760                 true
761             );
762         }
763
764         const ret_val = { color: color, element_id: "network_" + this.netCount };
765
766         this.networks.add(net_name);
767         this.netCount++;
768         return ret_val;
769     }
770
771     addPublicNetwork() {
772         const net = this.makeMxNetwork("public", true);
773         this.makeSidebarNetwork("public", net['color'], net['element_id']);
774         this.has_public_net = true;
775     }
776
777     addNetwork(net_name) {
778         const ret = this.makeMxNetwork(net_name);
779         this.makeSidebarNetwork(net_name, ret.color, ret.element_id);
780     }
781
782     updateHosts(removed) {
783         const cells = []
784         for(const hostID of removed) {
785             cells.push(this.graph.getModel().getCell("host_" + hostID));
786         }
787         this.graph.removeCells(cells);
788
789         const hosts = this.graph.getChildVertices(this.graph.getDefaultParent());
790         let topdist = 100;
791         for(const i in hosts) {
792             const host = hosts[i];
793             if(host.id.startsWith("host_")){
794                 const geometry = host.getGeometry();
795                 geometry.y = topdist + 50;
796                 topdist = geometry.y + geometry.height;
797                 host.setGeometry(geometry);
798             }
799         }
800     }
801
802     makeSidebarNetwork(net_name, color, net_id){
803         const colorBlob = document.createElement("div");
804         colorBlob.className = "square-20 rounded-circle";
805         colorBlob.style['background'] = color;
806
807         const textContainer = document.createElement("span");
808         textContainer.className = "ml-2";
809         textContainer.appendChild(document.createTextNode(net_name));
810
811         const timesIcon = document.createElement("i");
812         timesIcon.classList.add("fas", "fa-times");
813
814         const deletebutton = document.createElement("button");
815         deletebutton.className = "btn btn-danger ml-auto square-20 p-0 d-flex justify-content-center";
816         deletebutton.appendChild(timesIcon);
817         deletebutton.addEventListener("click", function() { this.createDeleteDialog(net_id); }.bind(this), false);
818
819         const newNet = document.createElement("li");
820         newNet.classList.add("list-group-item", "d-flex", "bg-light");
821         newNet.id = net_id;
822         newNet.appendChild(colorBlob);
823         newNet.appendChild(textContainer);
824
825         if( net_name != "public" ) {
826             newNet.appendChild(deletebutton);
827         }
828         document.getElementById("network_list").appendChild(newNet);
829     }
830
831     makeHost(hostInfo) {
832         const value = JSON.stringify(hostInfo['value']);
833         const interfaces = hostInfo['interfaces'];
834         const width = 100;
835         const height = (25 * interfaces.length) + 25;
836         const xoff = 75;
837         const yoff = this.lastHostBottom + 50;
838         this.lastHostBottom = yoff + height;
839         const host = this.graph.insertVertex(
840             this.graph.getDefaultParent(),
841             'host_' + hostInfo['id'],
842             value,
843             xoff,
844             yoff,
845             width,
846             height,
847             'editable=0',
848             false
849         );
850         host.getGeometry().offset = new mxPoint(-50,0);
851         host.setConnectable(false);
852         this.hostCount++;
853
854         for(var i=0; i<interfaces.length; i++) {
855             const port = this.graph.insertVertex(
856                 host,
857                 null,
858                 JSON.stringify(interfaces[i]),
859                 90,
860                 (i * 25) + 12,
861                 20,
862                 20,
863                 'fillColor=blue;editable=0',
864                 false
865             );
866             port.getGeometry().offset = new mxPoint(-4*interfaces[i].name.length -2,0);
867             this.graph.refresh(port);
868         }
869         this.graph.refresh(host);
870     }
871
872     prepareForm() {
873         const input_elem = document.getElementById("hidden_xml_input");
874         input_elem.value = this.encodeGraph(this.graph);
875     }
876 }
877
878 class SearchableSelectMultipleWidget {
879     constructor(format_vars, field_dataset, field_initial) {
880         this.format_vars = format_vars;
881         this.items = field_dataset;
882         this.initial = field_initial;
883
884         this.expanded_name_trie = {"isComplete": false};
885         this.small_name_trie = {"isComplete": false};
886         this.string_trie = {"isComplete": false};
887
888         this.added_items = new Set();
889
890         for( let e of ["show_from_noentry", "show_x_results", "results_scrollable", "selectable_limit", "placeholder"] )
891         {
892             this[e] = format_vars[e];
893         }
894
895         this.search_field_init();
896
897         if( this.show_from_noentry )
898         {
899             this.search("");
900         }
901     }
902
903     disable() {
904         const textfield = document.getElementById("user_field");
905         const drop = document.getElementById("drop_results");
906
907         textfield.disabled = "True";
908         drop.style.display = "none";
909
910         const btns = document.getElementsByClassName("btn-remove");
911         for( const btn of btns )
912         {
913             btn.classList.add("disabled");
914             btn.onclick = "";
915         }
916     }
917
918     search_field_init() {
919         this.build_all_tries(this.items);
920
921         for( const elem of this.initial )
922         {
923             this.select_item(elem);
924         }
925         if(this.initial.length == 1)
926         {
927             this.search(this.items[this.initial[0]]["small_name"]);
928             document.getElementById("user_field").value = this.items[this.initial[0]]["small_name"];
929         }
930     }
931
932     build_all_tries(dict)
933     {
934         for( const key in dict )
935         {
936             this.add_item(dict[key]);
937         }
938     }
939
940     add_item(item)
941     {
942         const id = item['id'];
943         this.add_to_tree(item['expanded_name'], id, this.expanded_name_trie);
944         this.add_to_tree(item['small_name'], id, this.small_name_trie);
945         this.add_to_tree(item['string'], id, this.string_trie);
946     }
947
948     add_to_tree(str, id, trie)
949     {
950         let inner_trie = trie;
951         while( str )
952         {
953             if( !inner_trie[str.charAt(0)] )
954             {
955                 var new_trie = {};
956                 inner_trie[str.charAt(0)] = new_trie;
957             }
958             else
959             {
960                 var new_trie = inner_trie[str.charAt(0)];
961             }
962
963             if( str.length == 1 )
964             {
965                 new_trie.isComplete = true;
966                 if( !new_trie.ids )
967                 {
968                     new_trie.ids = [];
969                 }
970                 new_trie.ids.push(id);
971             }
972             inner_trie = new_trie;
973             str = str.substring(1);
974         }
975     }
976
977     search(input)
978     {
979         if( input.length == 0 && !this.show_from_noentry){
980             this.dropdown([]);
981             return;
982         }
983         else if( input.length == 0 && this.show_from_noentry)
984         {
985             this.dropdown(this.items); //show all items
986         }
987         else
988         {
989             const trees = []
990             const tr1 = this.getSubtree(input, this.expanded_name_trie);
991             trees.push(tr1);
992             const tr2 = this.getSubtree(input, this.small_name_trie);
993             trees.push(tr2);
994             const tr3 = this.getSubtree(input, this.string_trie);
995             trees.push(tr3);
996             const results = this.collate(trees);
997             this.dropdown(results);
998         }
999     }
1000
1001     getSubtree(input, given_trie)
1002     {
1003         /*
1004         recursive function to return the trie accessed at input
1005         */
1006
1007         if( input.length == 0 ){
1008             return given_trie;
1009         }
1010
1011         else{
1012             const substr = input.substring(0, input.length - 1);
1013             const last_char = input.charAt(input.length-1);
1014             const subtrie = this.getSubtree(substr, given_trie);
1015
1016             if( !subtrie ) //substr not in the trie
1017             {
1018                 return {};
1019             }
1020
1021             const indexed_trie = subtrie[last_char];
1022             return indexed_trie;
1023         }
1024     }
1025
1026     serialize(trie)
1027     {
1028         /*
1029         takes in a trie and returns a list of its item id's
1030         */
1031         let itemIDs = [];
1032         if ( !trie )
1033         {
1034             return itemIDs; //empty, base case
1035         }
1036         for( const key in trie )
1037         {
1038             if(key.length > 1)
1039             {
1040                 continue;
1041             }
1042             itemIDs = itemIDs.concat(this.serialize(trie[key]));
1043         }
1044         if ( trie.isComplete )
1045         {
1046             itemIDs.push(...trie.ids);
1047         }
1048
1049         return itemIDs;
1050     }
1051
1052     collate(trees)
1053     {
1054         /*
1055         takes a list of tries
1056         returns a list of ids of objects that are available
1057         */
1058         const results = [];
1059         for( const tree of trees )
1060         {
1061             const available_IDs = this.serialize(tree);
1062
1063             for( const itemID of available_IDs ) {
1064                 results[itemID] = this.items[itemID];
1065             }
1066         }
1067         return results;
1068     }
1069
1070     generate_element_text(obj)
1071     {
1072         const content_strings = [obj.expanded_name, obj.small_name, obj.string].filter(x => Boolean(x));
1073         const result = content_strings.shift();
1074         if( result == null || content_strings.length < 1) {
1075             return result;
1076         } else {
1077             return result + " (" + content_strings.join(", ") + ")";
1078         }
1079     }
1080
1081     dropdown(ids)
1082     {
1083         /*
1084         takes in a mapping of ids to objects in  items
1085         and displays them in the dropdown
1086         */
1087         const drop = document.getElementById("drop_results");
1088         while(drop.firstChild)
1089         {
1090             drop.removeChild(drop.firstChild);
1091         }
1092
1093         for( const id in ids )
1094         {
1095             const obj = this.items[id];
1096             const result_text = this.generate_element_text(obj);
1097             const result_entry = document.createElement("a");
1098             result_entry.href = "#";
1099             result_entry.innerText = result_text;
1100             result_entry.title = result_text;
1101             result_entry.classList.add("list-group-item", "list-group-item-action", "overflow-ellipsis", "flex-shrink-0");
1102             result_entry.onclick = function() { searchable_select_multiple_widget.select_item(obj.id); };
1103             const tooltip = document.createElement("span");
1104             const tooltiptext = document.createTextNode(result_text);
1105             tooltip.appendChild(tooltiptext);
1106             tooltip.classList.add("d-none");
1107             result_entry.appendChild(tooltip);
1108             drop.appendChild(result_entry);
1109         }
1110
1111         const scroll_restrictor = document.getElementById("scroll_restrictor");
1112
1113         if( !drop.firstChild )
1114         {
1115             scroll_restrictor.style.visibility = 'hidden';
1116         }
1117         else
1118         {
1119             scroll_restrictor.style.visibility = 'inherit';
1120         }
1121     }
1122
1123     select_item(item_id)
1124     {
1125         if( (this.selectable_limit > -1 && this.added_items.size < this.selectable_limit) || this.selectable_limit < 0 )
1126         {
1127             this.added_items.add(item_id);
1128         }
1129         this.update_selected_list();
1130         // clear search bar contents
1131         document.getElementById("user_field").value = "";
1132         document.getElementById("user_field").focus();
1133         this.search("");
1134     }
1135
1136     remove_item(item_id)
1137     {
1138         this.added_items.delete(item_id);
1139
1140         this.update_selected_list()
1141         document.getElementById("user_field").focus();
1142     }
1143
1144     update_selected_list()
1145     {
1146         document.getElementById("added_number").innerText = this.added_items.size;
1147         const selector = document.getElementById('selector');
1148         selector.value = JSON.stringify([...this.added_items]);
1149         const added_list = document.getElementById('added_list');
1150
1151         while(selector.firstChild)
1152         {
1153             selector.removeChild(selector.firstChild);
1154         }
1155         while(added_list.firstChild)
1156         {
1157             added_list.removeChild(added_list.firstChild);
1158         }
1159
1160         const list_html = document.createElement("div");
1161         list_html.classList.add("list-group");
1162
1163         for( const item_id of this.added_items )
1164         {
1165             const times = document.createElement("li");
1166             times.classList.add("fas", "fa-times");
1167
1168             const deleteButton = document.createElement("a");
1169             deleteButton.href = "#";
1170             deleteButton.innerHTML = "<i class='fas fa-times'></i>"
1171             // Setting .onclick/.addEventListener does not work,
1172             // which is why I took the setAttribute approach
1173             // If anyone knows why, please let me know :]
1174             deleteButton.setAttribute("onclick", `searchable_select_multiple_widget.remove_item(${item_id});`);
1175             deleteButton.classList.add("btn");
1176             const deleteColumn = document.createElement("div");
1177             deleteColumn.classList.add("col-auto");
1178             deleteColumn.append(deleteButton);
1179
1180             const item = this.items[item_id];
1181             const element_entry_text = this.generate_element_text(item);
1182             const textColumn = document.createElement("div");
1183             textColumn.classList.add("col", "overflow-ellipsis");
1184             textColumn.innerText = element_entry_text;
1185             textColumn.title = element_entry_text;
1186
1187             const itemRow = document.createElement("div");
1188             itemRow.classList.add("list-group-item", "d-flex", "p-0", "align-items-center");
1189             itemRow.append(textColumn, deleteColumn);
1190
1191             list_html.append(itemRow);
1192         }
1193         added_list.innerHTML = list_html.innerHTML;
1194     }
1195 }