5 form_submission_callbacks = []; //all runnables will be executed before form submission
10 function update_page(response) {
11 if( response.redirect )
13 window.location.replace(response.redirect);
16 draw_breadcrumbs(response.meta);
17 update_exit_button(response.meta);
18 update_side_buttons(response.meta);
19 $("#formContainer").html(response.content);
22 function update_side_buttons(meta) {
23 const step = meta.active;
24 const page_count = meta.steps.length;
26 const back_button = document.getElementById("gob");
28 back_button.classList.add("disabled");
29 back_button.disabled = true;
31 back_button.classList.remove("disabled");
32 back_button.disabled = false;
35 const forward_btn = document.getElementById("gof");
36 if (step == page_count - 1) {
37 forward_btn.classList.add("disabled");
38 forward_btn.disabled = true;
40 forward_btn.classList.remove("disabled");
41 forward_btn.disabled = false;
45 function update_exit_button(meta) {
46 if (meta.workflow_count == 1) {
47 document.getElementById("cancel_btn").innerText = "Exit Workflow";
49 document.getElementById("cancel_btn").innerText = "Return to Parent";
53 function draw_breadcrumbs(meta) {
54 $("#topPagination").children().not(".page-control").remove();
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);
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";
67 step_dom.classList.add("active");
69 $(step_dom).html(`<span class="d-flex align-items-center justify-content-center text-capitalize w-100">${step_json['title']}</span>`)
71 const code = step_json.valid;
76 $(step_dom).children().first().append("<i class='ml-2 far fa-square'></i>")
79 } else if (code < 200) {
80 $(step_dom).children().first().append("<i class='ml-2 fas fa-minus-square'></i>")
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>")
86 msg = step_json.message;
89 if (step_json.enabled == false) {
90 step_dom.classList.add("disabled");
93 update_message(msg, stat);
99 function update_description(title, desc) {
100 document.getElementById("view_title").innerText = title;
101 document.getElementById("view_desc").innerText = desc;
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);
110 function submitStepForm(next_step = "current"){
111 run_form_callbacks();
112 const step_form_data = $("#step_form").serialize();
113 const form_data = $.param({
115 "step_form": step_form_data,
116 "csrfmiddlewaretoken": $("[name=csrfmiddlewaretoken]").val()
119 '/workflow/manager/',
121 (data) => update_page(data),
123 ).fail(() => alert("failure"));
126 function run_form_callbacks(){
127 for(f of form_submission_callbacks)
129 form_submission_callbacks = [];
132 function create_workflow(type) {
135 url: "/workflow/create/",
137 "workflow_type": type
140 "X-CSRFToken": $('input[name="csrfmiddlewaretoken"]').val()
142 }).done(function (data, textStatus, jqXHR) {
143 window.location = "/workflow/";
144 }).fail(function (jqxHR, textstatus) {
145 alert("Something went wrong...");
149 function add_workflow(type) {
152 url: "/workflow/add/",
154 "workflow_type": type
157 "X-CSRFToken": $('input[name="csrfmiddlewaretoken"]').val()
159 }).done(function (data, textStatus, jqXHR) {
161 }).fail(function (jqxHR, textstatus) {
162 alert("Something went wrong...");
166 function pop_workflow() {
169 url: "/workflow/pop/",
171 "X-CSRFToken": $('input[name="csrfmiddlewaretoken"]').val()
173 }).done(function (data, textStatus, jqXHR) {
175 }).fail(function (jqxHR, textstatus) {
176 alert("Something went wrong...");
180 function continue_workflow() {
181 window.location.replace("/workflow/");
188 class MultipleSelectFilterWidget {
190 constructor(neighbors, items, initial) {
192 this.graph_neighbors = neighbors;
193 this.filter_items = items;
195 this.dropdown_count = 0;
197 for(let nodeId in this.filter_items) {
198 const node = this.filter_items[nodeId];
199 this.result[node.class] = {}
202 this.make_selection(initial);
205 make_selection( initial_data ){
206 if(!initial_data || jQuery.isEmptyObject(initial_data))
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 ) {
215 this.markAndSweep(node);
216 this.updateResult(node);
218 if(node['multiple']){
219 this.make_multiple_selection(node, selection_data);
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]);
234 for(let i in this.filter_items) {
235 const node = this.filter_items[i];
236 node['marked'] = true; //mark all nodes
239 const toCheck = [root];
240 while(toCheck.length > 0){
241 const node = toCheck.pop();
242 if(!node['marked']) {
243 continue; //already visited, just continue
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);
255 //now remove all nodes still marked
256 for(let i in this.filter_items){
257 const node = this.filter_items[i];
259 this.disable_node(node);
265 if(node['selected']) {
266 this.markAndSweep(node);
268 else { //TODO: make this not dumb
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']) {
278 for(let node of selected) {
280 this.markAndSweep(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');
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');
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');
309 const node = this.filter_items[id];
310 if(!node['selectable'])
313 if(node['multiple']){
314 return this.processClickMultiple(node);
316 return this.processClickSingle(node);
320 processClickSingle(node){
321 node['selected'] = !node['selected']; //toggle on click
322 if(node['selected']) {
328 this.updateResult(node);
331 processClickMultiple(node){
333 const div = this.add_item_prepopulate(node, false);
335 this.updateObjectResult(node, div.id, "");
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();
343 input.value = input.value.replace(/([^A-Za-z0-9-_.])+/g, "");
344 this.checkunique(input);
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();
356 tocheck.setCustomValidity("");
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");
365 button.onclick = function(){ that.remove_dropdown(div.id, node.id); }
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);
379 input.onchange = function() { that.updateObjectResult(node, div.id, input.value); that.restrictchars(this); };
380 input.oninput = function() { that.restrictchars(this); };
382 input.value = prepopulate;
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);
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];
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];
415 if(!node['multiple']){
416 this.result[node.class][node.id] = {selected: node.selected, id: node.model_id}
418 delete this.result[node.class][node.id];
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: {}}
426 this.result[node.class][node.id]['values'][childKey] = childValue;
430 document.getElementById("filter_field").value = JSON.stringify(this.result);
435 constructor(debug, xml, hosts, added_hosts, removed_host_ids, graphContainer, overviewContainer, toolbarContainer){
436 if(!this.check_support())
439 this.currentWindow = null;
441 this.netColors = ['red', 'blue', 'purple', 'green', 'orange', '#8CCDF5', '#1E9BAC'];
443 this.lastHostBottom = 100;
444 this.networks = new Set();
445 this.has_public_net = false;
447 this.editor = new mxEditor();
448 this.graph = this.editor.graph;
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);
457 this.editor.addAction('printXML', function(editor, cell) {
458 mxLog.write(this.encodeGraph());
461 this.addToolbarButton(this.editor, toolbarContainer, 'printXML', '', '/static/img/mxgraph/fit_to_size.png', true);
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);
470 if(!this.has_public_net){
471 this.addPublicNetwork();
476 if (!mxClient.isBrowserSupported()) {
477 mxUtils.error('Browser is not supported', 200, false);
483 prefill(xml, hosts, added_hosts, removed_host_ids){
484 //populate existing data
486 this.restoreFromXml(xml, this.editor);
488 for(const host of hosts)
494 for(const host of added_hosts)
496 this.updateHosts([]); //TODO: why?
498 this.updateHosts(removed_host_ids);
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);
511 doubleClickHandler(evt, cell) {
513 if( cell.getParent() != null && cell.getParent().getId().indexOf("network") > -1) {
514 cell = cell.getParent();
516 if( cell.isEdge() || cell.getId().indexOf("network") > -1 ) {
517 this.createDeleteDialog(cell.getId());
520 this.showDetailWindow(cell);
525 alertVlan(edge, terminal, source) {
526 if( terminal == null || edge.getTerminal(!source) == null) {
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"));
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"));
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"));
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"));
554 const error_div = document.createElement("div");
555 error_div.id = "current_window_errors";
556 form.appendChild(error_div);
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);
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"));
576 content.appendChild(remove_button);
577 content.appendChild(cancel_button);
578 this.showWindow('Do you want to delete this network?', content, 200, 62);
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] ) {
588 this.graph.removeCells([edge]);
595 colorEdge(edge, terminal, source) {
596 if(terminal.getParent().getId().indexOf('network') >= 0) {
597 const styles = terminal.getParent().getStyle().split(';');
599 for(let style of styles){
600 const kvp = style.split('=');
601 if(kvp[0] == "fillColor"){
605 edge.setStyle('strokeColor=' + color);
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);
621 restoreFromXml(xml, editor) {
622 const doc = mxUtils.parseXml(xml);
623 const node = doc.documentElement;
624 editor.readGraphModel(node);
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(";");
634 for(const style of styles){
635 const kvp = style.split('=');
636 if(kvp[0] == "fillColor") {
642 this.has_public_net = true;
645 this.makeSidebarNetwork(name, color, cell.getId());
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);
656 this.graph.removeCells([cell]);
657 this.currentWindow.destroy();
661 const input = document.createElement("input");
663 input.name = "net_name";
664 input.maxlength = 100;
665 input.id = "net_name_input";
666 input.style.margin = "5px";
668 const yes_button = document.createElement("button");
669 yes_button.onclick = function() {this.parseNetworkWindow();}.bind(this);
670 yes_button.appendChild(document.createTextNode("Okay"));
672 const cancel_button = document.createElement("button");
673 cancel_button.onclick = function() {this.closeWindow();}.bind(this);
674 cancel_button.appendChild(document.createTextNode("Cancel"));
676 const error_div = document.createElement("div");
677 error_div.id = "current_window_errors";
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);
688 this.showWindow("Network Creation", content, 300, 300);
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";
698 this.addNetwork(net_name);
699 this.currentWindow.destroy();
702 addToolbarButton(editor, toolbar, action, label, image, isTransparent) {
703 const button = document.createElement('button');
704 button.style.fontSize = '10';
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);
715 button.style.background = 'transparent';
716 button.style.color = '#FFFFFF';
717 button.style.border = 'none';
719 mxEvent.addListener(button, 'click', function(evt) {
720 editor.execute(action);
722 mxUtils.write(button, label);
723 toolbar.appendChild(button);
727 const encoder = new mxCodec();
728 const xml = encoder.encode(this.graph.getModel());
729 return mxUtils.getXml(xml);
733 //general graph stuff
734 this.graph.setMultigraph(false);
735 this.graph.setCellsSelectable(false);
736 this.graph.setCellsMovable(false);
739 this.graph.vertexLabelIsMovable = true;
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;
759 const hostStyle = this.graph.getStylesheet().getDefaultVertexStyle();
760 hostStyle[mxConstants.STYLE_ROUNDED] = 1;
762 this.graph.convertValueToString = function(cell) {
764 //changes value for edges with xml value
766 if(JSON.parse(cell.getValue())["tagged"]) {
772 return JSON.parse(cell.getValue())['name'];
776 return cell.getValue();
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);
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);
797 const wnd = new mxWindow(title, content, x, y, width, height, false, true);
798 wnd.setClosable(false);
800 wnd.addListener(mxEvent.DESTROY, function(evt) {
801 this.graph.setEnabled(true);
802 mxEffects.fadeOut(background, 50, true, 10, 30, true);
804 this.currentWindow = wnd;
806 this.graph.setEnabled(false);
807 this.currentWindow.setVisible(true);
811 //allows the current window to be destroyed
812 this.currentWindow.destroy();
815 othersUntagged(edgeID) {
816 const edge = this.graph.getModel().getCell(edgeID);
817 const end1 = edge.getTerminal(true);
818 const end2 = edge.getTerminal(false);
820 if( end1.getParent().getId().split('_')[0] == 'host' ){
826 var edges = netint.edges;
827 for( let edge of edges) {
828 if( edge.getValue() ) {
829 var tagged = JSON.parse(edge.getValue()).tagged;
841 deleteVlanWindow(edgeID) {
842 const cell = this.graph.getModel().getCell(edgeID);
843 this.graph.removeCells([cell]);
844 this.currentWindow.destroy();
847 parseVlanWindow(edgeID) {
848 //do parsing and data manipulation
849 const radios = document.getElementsByName("tagged");
850 const edge = this.graph.getModel().getCell(edgeID);
852 for(let radio of radios){
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.";
861 const edgeVal = {tagged: radio.value == "True"};
862 edge.setValue(JSON.stringify(edgeVal));
866 this.graph.refresh(edge);
870 makeMxNetwork(net_name, is_public = false) {
871 const model = this.graph.getModel();
874 const xoff = 400 + (30 * this.netCount);
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
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),
890 'fillColor=' + color,
893 const num_ports = 45;
894 for(var i=0; i<num_ports; i++){
895 let port = this.graph.insertVertex(
903 'fillColor=black;opacity=0',
908 const ret_val = { color: color, element_id: "network_" + this.netCount };
910 this.networks.add(net_name);
916 const net = this.makeMxNetwork("public", true);
917 this.makeSidebarNetwork("public", net['color'], net['element_id']);
918 this.has_public_net = true;
921 addNetwork(net_name) {
922 const ret = this.makeMxNetwork(net_name);
923 this.makeSidebarNetwork(net_name, ret.color, ret.element_id);
926 updateHosts(removed) {
928 for(const hostID of removed) {
929 cells.push(this.graph.getModel().getCell("host_" + hostID));
931 this.graph.removeCells(cells);
933 const hosts = this.graph.getChildVertices(this.graph.getDefaultParent());
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);
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;
951 const textContainer = document.createElement("span");
952 textContainer.className = "ml-2";
953 textContainer.appendChild(document.createTextNode(net_name));
955 const timesIcon = document.createElement("i");
956 timesIcon.classList.add("fas", "fa-times");
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);
963 const newNet = document.createElement("li");
964 newNet.classList.add("list-group-item", "d-flex", "bg-light");
966 newNet.appendChild(colorBlob);
967 newNet.appendChild(textContainer);
969 if( net_name != "public" ) {
970 newNet.appendChild(deletebutton);
972 document.getElementById("network_list").appendChild(newNet);
976 const value = JSON.stringify(hostInfo['value']);
977 const interfaces = hostInfo['interfaces'];
979 const height = (25 * interfaces.length) + 25;
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'],
994 host.getGeometry().offset = new mxPoint(-50,0);
995 host.setConnectable(false);
998 for(var i=0; i<interfaces.length; i++) {
999 const port = this.graph.insertVertex(
1002 JSON.stringify(interfaces[i]),
1007 'fillColor=blue;editable=0',
1010 port.getGeometry().offset = new mxPoint(-4*interfaces[i].name.length -2,0);
1011 this.graph.refresh(port);
1013 this.graph.refresh(host);
1017 const input_elem = document.getElementById("hidden_xml_input");
1018 input_elem.value = this.encodeGraph(this.graph);
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;
1028 this.expanded_name_trie = {"isComplete": false};
1029 this.small_name_trie = {"isComplete": false};
1030 this.string_trie = {"isComplete": false};
1032 this.added_items = new Set();
1034 for( let e of ["show_from_noentry", "show_x_results", "results_scrollable", "selectable_limit", "placeholder"] )
1036 this[e] = format_vars[e];
1039 this.search_field_init();
1041 if( this.show_from_noentry )
1048 const textfield = document.getElementById("user_field");
1049 const drop = document.getElementById("drop_results");
1051 textfield.disabled = "True";
1052 drop.style.display = "none";
1054 const btns = document.getElementsByClassName("btn-remove");
1055 for( const btn of btns )
1057 btn.classList.add("disabled");
1062 search_field_init() {
1063 this.build_all_tries(this.items);
1065 for( const elem of this.initial )
1067 this.select_item(elem);
1069 if(this.initial.length == 1)
1071 this.search(this.items[this.initial[0]]["small_name"]);
1072 document.getElementById("user_field").value = this.items[this.initial[0]]["small_name"];
1076 build_all_tries(dict)
1078 for( const key in dict )
1080 this.add_item(dict[key]);
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);
1092 add_to_tree(str, id, trie)
1094 let inner_trie = trie;
1097 if( !inner_trie[str.charAt(0)] )
1100 inner_trie[str.charAt(0)] = new_trie;
1104 var new_trie = inner_trie[str.charAt(0)];
1107 if( str.length == 1 )
1109 new_trie.isComplete = true;
1114 new_trie.ids.push(id);
1116 inner_trie = new_trie;
1117 str = str.substring(1);
1123 if( input.length == 0 && !this.show_from_noentry){
1127 else if( input.length == 0 && this.show_from_noentry)
1129 this.dropdown(this.items); //show all items
1134 const tr1 = this.getSubtree(input, this.expanded_name_trie);
1136 const tr2 = this.getSubtree(input, this.small_name_trie);
1138 const tr3 = this.getSubtree(input, this.string_trie);
1140 const results = this.collate(trees);
1141 this.dropdown(results);
1145 getSubtree(input, given_trie)
1148 recursive function to return the trie accessed at input
1151 if( input.length == 0 ){
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);
1160 if( !subtrie ) //substr not in the trie
1165 const indexed_trie = subtrie[last_char];
1166 return indexed_trie;
1173 takes in a trie and returns a list of its item id's
1178 return itemIDs; //empty, base case
1180 for( const key in trie )
1186 itemIDs = itemIDs.concat(this.serialize(trie[key]));
1188 if ( trie.isComplete )
1190 itemIDs.push(...trie.ids);
1199 takes a list of tries
1200 returns a list of ids of objects that are available
1203 for( const tree of trees )
1205 const available_IDs = this.serialize(tree);
1207 for( const itemID of available_IDs ) {
1208 results[itemID] = this.items[itemID];
1214 generate_element_text(obj)
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) {
1221 return result + " (" + content_strings.join(", ") + ")";
1228 takes in a mapping of ids to objects in items
1229 and displays them in the dropdown
1231 const drop = document.getElementById("drop_results");
1232 while(drop.firstChild)
1234 drop.removeChild(drop.firstChild);
1237 for( const id in ids )
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);
1255 const scroll_restrictor = document.getElementById("scroll_restrictor");
1257 if( !drop.firstChild )
1259 scroll_restrictor.style.visibility = 'hidden';
1263 scroll_restrictor.style.visibility = 'inherit';
1267 select_item(item_id)
1269 if( (this.selectable_limit > -1 && this.added_items.size < this.selectable_limit) || this.selectable_limit < 0 )
1271 this.added_items.add(item_id);
1273 this.update_selected_list();
1274 // clear search bar contents
1275 document.getElementById("user_field").value = "";
1276 document.getElementById("user_field").focus();
1280 remove_item(item_id)
1282 this.added_items.delete(item_id);
1284 this.update_selected_list()
1285 document.getElementById("user_field").focus();
1288 update_selected_list()
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');
1295 while(selector.firstChild)
1297 selector.removeChild(selector.firstChild);
1299 while(added_list.firstChild)
1301 added_list.removeChild(added_list.firstChild);
1304 const list_html = document.createElement("div");
1305 list_html.classList.add("list-group");
1307 for( const item_id of this.added_items )
1309 const times = document.createElement("li");
1310 times.classList.add("fas", "fa-times");
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);
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;
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);
1335 list_html.append(itemRow);
1337 added_list.innerHTML = list_html.innerHTML;