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