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