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