Move All JS of Networking Step to External File
[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(selected[i]);
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(...ret);
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 }