///////////////////
// Global Variables
///////////////////
form_submission_callbacks = []; //all runnables will be executed before form submission
///////////////////
// Global Functions
///////////////////
// Taken from https://docs.djangoproject.com/en/3.0/ref/csrf/
function getCookie(name) {
var cookieValue = null;
if (document.cookie && document.cookie !== '') {
var cookies = document.cookie.split(';');
for (var i = 0; i < cookies.length; i++) {
var cookie = cookies[i].trim();
// Does this cookie string begin with the name we want?
if (cookie.substring(0, name.length + 1) === (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}
function update_page(response) {
if( response.redirect )
{
window.location.replace(response.redirect);
return;
}
draw_breadcrumbs(response.meta);
update_exit_button(response.meta);
update_side_buttons(response.meta);
$("#formContainer").html(response.content);
}
function update_side_buttons(meta) {
const step = meta.active;
const page_count = meta.steps.length;
const back_button = document.getElementById("gob");
if (step == 0) {
back_button.classList.add("disabled");
back_button.disabled = true;
} else {
back_button.classList.remove("disabled");
back_button.disabled = false;
}
const forward_btn = document.getElementById("gof");
if (step == page_count - 1) {
forward_btn.classList.add("disabled");
forward_btn.disabled = true;
} else {
forward_btn.classList.remove("disabled");
forward_btn.disabled = false;
}
}
function update_exit_button(meta) {
if (meta.workflow_count == 1) {
document.getElementById("cancel_btn").innerText = "Exit Workflow";
} else {
document.getElementById("cancel_btn").innerText = "Return to Parent";
}
}
function draw_breadcrumbs(meta) {
$("#topPagination").children().not(".page-control").remove();
for (const i in meta.steps) {
const step_btn = create_step(meta.steps[i], i == meta["active"]);
$("#topPagination li:last-child").before(step_btn);
}
}
function create_step(step_json, active) {
const step_dom = document.createElement("li");
// First create the dom object depending on active or not
step_dom.className = "topcrumb";
if (active) {
step_dom.classList.add("active");
}
$(step_dom).html(`${step_json['title']}`)
const code = step_json.valid;
let stat = "";
let msg = "";
if (code < 100) {
$(step_dom).children().first().append("")
stat = "";
msg = "";
} else if (code < 200) {
$(step_dom).children().first().append("")
stat = "invalid";
msg = step_json.message;
} else if (code < 300) {
$(step_dom).children().first().append("")
stat = "valid";
msg = step_json.message;
}
if (step_json.enabled == false) {
step_dom.classList.add("disabled");
}
if (active) {
update_message(msg, stat);
}
return step_dom;
}
function update_description(title, desc) {
document.getElementById("view_title").innerText = title;
document.getElementById("view_desc").innerText = desc;
}
function update_message(message, stepstatus) {
document.getElementById("view_message").innerText = message;
document.getElementById("view_message").className = "step_message";
document.getElementById("view_message").classList.add("message_" + stepstatus);
}
function submitStepForm(next_step = "current"){
run_form_callbacks();
const step_form_data = $("#step_form").serialize();
const form_data = $.param({
"step": next_step,
"step_form": step_form_data,
"csrfmiddlewaretoken": $("[name=csrfmiddlewaretoken]").val()
});
$.post(
'/workflow/manager/',
form_data,
(data) => update_page(data),
'json'
).fail(() => alert("failure"));
}
function run_form_callbacks(){
for(f of form_submission_callbacks)
f();
form_submission_callbacks = [];
}
function create_workflow(type) {
$.ajax({
type: "POST",
url: "/workflow/create/",
data: {
"workflow_type": type
},
headers: {
"X-CSRFToken": getCookie('csrftoken')
}
}).done(function (data, textStatus, jqXHR) {
window.location = "/workflow/";
}).fail(function (jqxHR, textstatus) {
alert("Something went wrong...");
});
}
function add_workflow(type) {
data = $.ajax({
type: "POST",
url: "/workflow/add/",
data: {
"workflow_type": type
},
headers: {
"X-CSRFToken": getCookie('csrftoken')
}
}).done(function (data, textStatus, jqXHR) {
update_page(data);
}).fail(function (jqxHR, textstatus) {
alert("Something went wrong...");
});
}
function pop_workflow() {
data = $.ajax({
type: "POST",
url: "/workflow/pop/",
headers: {
"X-CSRFToken": getCookie('csrftoken')
}
}).done(function (data, textStatus, jqXHR) {
update_page(data);
}).fail(function (jqxHR, textstatus) {
alert("Something went wrong...");
});
}
function continue_workflow() {
window.location.replace("/workflow/");
}
///////////////////
//Class Definitions
///////////////////
class MultipleSelectFilterWidget {
constructor(neighbors, items, initial) {
this.inputs = [];
this.graph_neighbors = neighbors;
this.filter_items = items;
this.currentLab = null;
this.available_resources = {};
this.result = {};
this.dropdown_count = 0;
for(let nodeId in this.filter_items) {
const node = this.filter_items[nodeId];
this.result[node.class] = {}
}
this.make_selection(initial);
}
make_selection(initial_data){
if(!initial_data || jQuery.isEmptyObject(initial_data))
return;
// Need to sort through labs first
let initial_lab = initial_data['lab'];
let initial_resources = initial_data['resource'];
for( let node_id in initial_lab) { // This should only be length one
const node = this.filter_items[node_id];
const selection_data = initial_lab[node_id];
if( selection_data.selected ) {
this.select(node);
this.markAndSweep(node);
this.updateResult(node);
}
if(node['multiple']){
this.make_multiple_selection(node, selection_data);
}
this.currentLab = node;
this.available_resources = JSON.parse(node['available_resources']);
}
for( let node_id in initial_resources){
const node = this.filter_items[node_id];
const selection_data = initial_resources[node_id];
if( selection_data.selected ) {
this.select(node);
this.markAndSweep(node);
this.updateResult(node);
}
if(node['multiple']){
this.make_multiple_selection(node, selection_data);
}
}
this.updateAvailibility();
}
make_multiple_selection(node, selection_data){
const prepop_data = selection_data.values;
for(let k in prepop_data){
const div = this.add_item_prepopulate(node, prepop_data[k]);
this.updateObjectResult(node, div.id, prepop_data[k]);
}
}
markAndSweep(root){
for(let i in this.filter_items) {
const node = this.filter_items[i];
node['marked'] = true; //mark all nodes
}
const toCheck = [root];
while(toCheck.length > 0){
const node = toCheck.pop();
if(!node['marked']) {
continue; //already visited, just continue
}
node['marked'] = false; //mark as visited
if(node['follow'] || node == root){ //add neighbors if we want to follow this node
const neighbors = this.graph_neighbors[node.id];
for(let neighId of neighbors) {
const neighbor = this.filter_items[neighId];
toCheck.push(neighbor);
}
}
}
//now remove all nodes still marked
for(let i in this.filter_items){
const node = this.filter_items[i];
if(node['marked']){
this.disable_node(node);
}
}
}
process(node) {
if(node['selected']) {
this.markAndSweep(node);
}
else { //TODO: make this not dumb
const selected = []
//remember the currently selected, then reset everything and reselect one at a time
for(let nodeId in this.filter_items) {
node = this.filter_items[nodeId];
if(node['selected']) {
selected.push(node);
}
this.clear(node);
}
for(let node of selected) {
this.select(node);
this.markAndSweep(node);
}
}
}
select(node) {
const elem = document.getElementById(node['id']);
node['selected'] = true;
elem.classList.remove('bg-white', 'not-allowed', 'bg-light');
elem.classList.add('selected_node');
if(node['class'] == 'resource')
this.reserveResource(node);
}
clear(node) {
const elem = document.getElementById(node['id']);
node['selected'] = false;
node['selectable'] = true;
elem.classList.add('bg-white')
elem.classList.remove('not-allowed', 'bg-light', 'selected_node');
}
disable_node(node) {
const elem = document.getElementById(node['id']);
node['selected'] = false;
node['selectable'] = false;
elem.classList.remove('bg-white', 'selected_node');
elem.classList.add('not-allowed', 'bg-light');
}
labCheck(node){
// if lab is not already selected update available resources
if(!node['selected']) {
this.currentLab = node;
this.available_resources = JSON.parse(node['available_resources']);
this.updateAvailibility();
} else {
// a lab is already selected, clear already selected resources
if(confirm('Unselecting a lab will reset all selected resources, are you sure?')) {
location.reload();
return false;
}
}
return true;
}
updateAvailibility() {
const lab_resources = this.graph_neighbors[this.currentLab.id];
// need to loop through and update all quantities
for(let i in lab_resources) {
const resource_node = this.filter_items[lab_resources[i]];
const required_resources = JSON.parse(resource_node['required_resources']);
let elem = document.getElementById(resource_node.id).getElementsByClassName("grid-item-description")[0];
let leastAvailable = 100;
let currCount;
let quantityDescription;
let quantityNode;
for(let resource in required_resources) {
currCount = Math.floor(this.available_resources[resource] / required_resources[resource]);
if(currCount < leastAvailable)
leastAvailable = currCount;
if(!currCount || currCount < 0) {
leastAvailable = 0
break;
}
}
if (elem.children[0]){
elem.removeChild(elem.children[0]);
}
quantityDescription = '
Quantity Currently Available: ' + leastAvailable;
quantityNode = document.createElement('P');
if (leastAvailable > 0) {
quantityDescription = quantityDescription.fontcolor('green');
} else {
quantityDescription = quantityDescription.fontcolor('red');
}
quantityNode.innerHTML = quantityDescription;
elem.appendChild(quantityNode)
}
}
reserveResource(node){
const required_resources = JSON.parse(node['required_resources']);
let hostname = document.getElementById('id_hostname');
let image = document.getElementById('id_image');
let cnt = 0
for(let resource in required_resources){
this.available_resources[resource] -= required_resources[resource];
cnt += required_resources[resource];
}
if (cnt > 1 && hostname) {
hostname.readOnly = true;
// we only disable hostname modification because there is no sane case where you want all hosts to have the same hostname
// image is still allowed to be set across all hosts, but is filtered to the set of images that are commonly applicable still
// if no images exist that would apply to all hosts in a pod, then the user is restricted to not setting an image
// and the default image for each host is used
}
this.updateAvailibility();
}
releaseResource(node){
const required_resources = JSON.parse(node['required_resources']);
let hostname = document.getElementById('id_hostname');
let image = document.getElementById('id_image');
for(let resource in required_resources){
this.available_resources[resource] += required_resources[resource];
}
if (hostname && image) {
hostname.readOnly = false;
image.disabled = false;
}
this.updateAvailibility();
}
processClick(id){
let lab_check;
const node = this.filter_items[id];
if(!node['selectable'])
return;
// If they are selecting a lab, update accordingly
if (node['class'] == 'lab') {
lab_check = this.labCheck(node);
if (!lab_check)
return;
}
// Can only select a resource if a lab is selected
if (!this.currentLab) {
alert('You must select a lab before selecting a resource');
return;
}
if(node['multiple']){
return this.processClickMultiple(node);
} else {
return this.processClickSingle(node);
}
}
processClickSingle(node){
node['selected'] = !node['selected']; //toggle on click
if(node['selected']) {
this.select(node);
} else {
this.clear(node);
this.releaseResource(node); // can't do this in clear since clear removes border
}
this.process(node);
this.updateResult(node);
}
processClickMultiple(node){
this.select(node);
const div = this.add_item_prepopulate(node, false);
this.process(node);
this.updateObjectResult(node, div.id, "");
}
restrictchars(input){
if( input.validity.patternMismatch ){
input.setCustomValidity("Only alphanumeric characters (a-z, A-Z, 0-9), underscore(_), and hyphen (-) are allowed");
input.reportValidity();
}
input.value = input.value.replace(/([^A-Za-z0-9-_.])+/g, "");
this.checkunique(input);
}
checkunique(tocheck){ //TODO: use set
const val = tocheck.value;
for( let input of this.inputs ){
if( input.value == val && input != tocheck){
tocheck.setCustomValidity("All hostnames must be unique");
tocheck.reportValidity();
return;
}
}
tocheck.setCustomValidity("");
}
make_remove_button(div, node){
const button = document.createElement("BUTTON");
button.type = "button";
button.appendChild(document.createTextNode("Remove"));
button.classList.add("btn", "btn-danger", "d-inline-block");
const that = this;
button.onclick = function(){ that.remove_dropdown(div.id, node.id); }
return button;
}
make_input(div, node, prepopulate){
const input = document.createElement("INPUT");
input.type = node.form.type;
input.name = node.id + node.form.name
input.classList.add("form-control", "w-auto", "d-inline-block");
input.pattern = "(?=^.{1,253}$)(^([A-Za-z0-9-_]{1,62}\.)*[A-Za-z0-9-_]{1,63})";
input.title = "Only alphanumeric characters (a-z, A-Z, 0-9), underscore(_), and hyphen (-) are allowed"
input.placeholder = node.form.placeholder;
this.inputs.push(input);
const that = this;
input.onchange = function() { that.updateObjectResult(node, div.id, input.value); that.restrictchars(this); };
input.oninput = function() { that.restrictchars(this); };
if(prepopulate)
input.value = prepopulate;
return input;
}
add_item_prepopulate(node, prepopulate){
const div = document.createElement("DIV");
div.id = "dropdown_" + this.dropdown_count;
div.classList.add("card", "flex-row", "d-flex", "mb-2");
this.dropdown_count++;
const label = document.createElement("H5")
label.appendChild(document.createTextNode(node['name']))
label.classList.add("p-1", "m-1", "flex-grow-1");
div.appendChild(label);
let remove_btn = this.make_remove_button(div, node);
remove_btn.classList.add("p-1", "m-1");
div.appendChild(remove_btn);
document.getElementById("dropdown_wrapper").appendChild(div);
return div;
}
remove_dropdown(div_id, node_id){
const div = document.getElementById(div_id);
const node = this.filter_items[node_id]
const parent = div.parentNode;
div.parentNode.removeChild(div);
this.result[node.class][node.id]['count']--;
this.releaseResource(node); // This can't be done on clear b/c clear removes border
//checks if we have removed last item in class
if(this.result[node.class][node.id]['count'] == 0){
delete this.result[node.class][node.id];
this.clear(node);
}
}
updateResult(node){
if(!node['multiple']){
this.result[node.class][node.id] = {selected: node.selected, id: node.model_id}
if(!node.selected)
delete this.result[node.class][node.id];
}
}
updateObjectResult(node, childKey, childValue){
if(!this.result[node.class][node.id])
this.result[node.class][node.id] = {selected: true, id: node.model_id, count: 0}
this.result[node.class][node.id]['count']++;
}
finish(){
document.getElementById("filter_field").value = JSON.stringify(this.result);
}
}
class NetworkStep {
// expects:
//
// debug: bool
// resources: {
// id: {
// id: int,
// value: {
// description: string,
// },
// interfaces: [
// id: int,
// name: str,
// description: str,
// connections: [
// {
// network: int, [networks.id]
// tagged: bool
// }
// ],
// ],
// }
// }
// networks: {
// id: {
// id: int,
// name: str,
// public: bool,
// }
// }
//
constructor(debug, resources, networks, graphContainer, overviewContainer, toolbarContainer){
if(!this.check_support()) {
console.log("Aborting, browser is not supported");
return;
}
this.currentWindow = null;
this.netCount = 0;
this.netColors = ['red', 'blue', 'purple', 'green', 'orange', '#8CCDF5', '#1E9BAC'];
this.hostCount = 0;
this.lastHostBottom = 100;
this.networks = new Set();
this.has_public_net = false;
this.debug = debug;
this.editor = new mxEditor();
this.graph = this.editor.graph;
window.global_graph = this.graph;
window.network_rr_index = 5;
this.editor.setGraphContainer(graphContainer);
this.doGlobalConfig();
let mx_networks = {}
for(const network_id in networks) {
let network = networks[network_id];
mx_networks[network_id] = this.populateNetwork(network);
}
this.prefillHosts(resources, mx_networks);
//this.addToolbarButton(this.editor, toolbarContainer, 'zoomIn', '', "/static/img/mxgraph/zoom_in.png", true);
//this.addToolbarButton(this.editor, toolbarContainer, 'zoomOut', '', "/static/img/mxgraph/zoom_out.png", true);
this.addToolbarButton(this.editor, toolbarContainer, 'zoomIn', 'fa-search-plus');
this.addToolbarButton(this.editor, toolbarContainer, 'zoomOut', 'fa-search-minus');
if(this.debug){
this.editor.addAction('printXML', function(editor, cell) {
mxLog.write(this.encodeGraph());
mxLog.show();
}.bind(this));
this.addToolbarButton(this.editor, toolbarContainer, 'printXML', 'fa-file-code');
}
new mxOutline(this.graph, overviewContainer);
//sets the edge color to be the same as the network
this.graph.addListener(mxEvent.CELL_CONNECTED, function(sender, event) {this.cellConnectionHandler(sender, event)}.bind(this));
//hooks up double click functionality
this.graph.dblClick = function(evt, cell) {this.doubleClickHandler(evt, cell);}.bind(this);
}
check_support(){
if (!mxClient.isBrowserSupported()) {
mxUtils.error('Browser is not supported', 200, false);
return false;
}
return true;
}
/**
* Expects
* mx_interface: mxCell for the interface itself
* network: mxCell for the outer network
* tagged: bool
*/
connectNetwork(mx_interface, network, tagged) {
var cell = new mxCell(
"connection from " + network + " to " + mx_interface,
new mxGeometry(0, 0, 50, 50));
cell.edge = true;
cell.geometry.relative = true;
cell.setValue(JSON.stringify({tagged: tagged}));
let terminal = this.getClosestNetworkCell(mx_interface.geometry.y, network);
let edge = this.graph.addEdge(cell, null, mx_interface, terminal);
this.colorEdge(edge, terminal, true);
this.graph.refresh(edge);
}
/**
* Expects:
*
* to: desired y axis position of the matching cell
* within: graph cell for a full network, with all child cells
*
* Returns:
* an mx cell, the one vertically closest to the desired value
*
* Side effect:
* modifies the on the parameter
*/
getClosestNetworkCell(to, within) {
if(window.network_rr_index === undefined) {
window.network_rr_index = 5;
}
let child_keys = within.children.keys();
let children = Array.from(within.children);
let index = (window.network_rr_index++) % children.length;
let child = within.children[child_keys[index]];
return children[index];
}
/** Expects
*
* hosts: {
* id: {
* id: int,
* value: {
* description: string,
* },
* interfaces: [
* id: int,
* name: str,
* description: str,
* connections: [
* {
* network: int, [networks.id]
* tagged: bool
* }
* ],
* ],
* }
* }
*
* network_mappings: {
* :
* }
*
* draws given hosts into the mxgraph
*/
prefillHosts(hosts, network_mappings){
for(const host_id in hosts) {
this.makeHost(hosts[host_id], network_mappings);
}
}
cellConnectionHandler(sender, event){
const edge = event.getProperty('edge');
const terminal = event.getProperty('terminal')
const source = event.getProperty('source');
if(this.checkAllowed(edge, terminal, source)) {
this.colorEdge(edge, terminal, source);
this.alertVlan(edge, terminal, source);
}
}
doubleClickHandler(evt, cell) {
if( cell != null ){
if( cell.getParent() != null && cell.getParent().getId().indexOf("network") > -1) {
cell = cell.getParent();
}
if( cell.isEdge() || cell.getId().indexOf("network") > -1 ) {
this.createDeleteDialog(cell.getId());
}
else {
this.showDetailWindow(cell);
}
}
}
alertVlan(edge, terminal, source) {
if( terminal == null || edge.getTerminal(!source) == null) {
return;
}
const form = document.createElement("form");
const tagged = document.createElement("input");
tagged.type = "radio";
tagged.name = "tagged";
tagged.value = "True";
form.appendChild(tagged);
form.appendChild(document.createTextNode(" Tagged"));
form.appendChild(document.createElement("br"));
const untagged = document.createElement("input");
untagged.type = "radio";
untagged.name = "tagged";
untagged.value = "False";
form.appendChild(untagged);
form.appendChild(document.createTextNode(" Untagged"));
form.appendChild(document.createElement("br"));
const yes_button = document.createElement("button");
yes_button.onclick = function() {this.parseVlanWindow(edge.getId());}.bind(this);
yes_button.appendChild(document.createTextNode("Okay"));
const cancel_button = document.createElement("button");
cancel_button.onclick = function() {this.deleteVlanWindow(edge.getId());}.bind(this);
cancel_button.appendChild(document.createTextNode("Cancel"));
const error_div = document.createElement("div");
error_div.id = "current_window_errors";
form.appendChild(error_div);
const content = document.createElement('div');
content.appendChild(form);
content.appendChild(yes_button);
content.appendChild(cancel_button);
this.showWindow("Vlan Selection", content, 200, 200);
}
createDeleteDialog(id) {
const content = document.createElement('div');
const remove_button = document.createElement("button");
remove_button.style.width = '46%';
remove_button.onclick = function() { this.deleteCell(id);}.bind(this);
remove_button.appendChild(document.createTextNode("Remove"));
const cancel_button = document.createElement("button");
cancel_button.style.width = '46%';
cancel_button.onclick = function() { this.closeWindow();}.bind(this);
cancel_button.appendChild(document.createTextNode("Cancel"));
content.appendChild(remove_button);
content.appendChild(cancel_button);
this.showWindow('Do you want to delete this network?', content, 200, 62);
}
checkAllowed(edge, terminal, source) {
//check if other terminal is null, and that they are different
const otherTerminal = edge.getTerminal(!source);
if(terminal != null && otherTerminal != null) {
if( terminal.getParent().getId().split('_')[0] == //'host' or 'network'
otherTerminal.getParent().getId().split('_')[0] ) {
//not allowed
this.graph.removeCells([edge]);
return false;
}
}
return true;
}
colorEdge(edge, terminal, source) {
if(terminal.getParent().getId().indexOf('network') >= 0) {
const styles = terminal.getParent().getStyle().split(';');
let color = 'black';
for(let style of styles){
const kvp = style.split('=');
if(kvp[0] == "fillColor"){
color = kvp[1];
}
}
edge.setStyle('strokeColor=' + color);
} else {
console.log("Failed to color " + edge + ", " + terminal + ", " + source);
}
}
showDetailWindow(cell) {
const info = JSON.parse(cell.getValue());
const content = document.createElement("div");
const pre_tag = document.createElement("pre");
pre_tag.appendChild(document.createTextNode("Name: " + info.name + "\nDescription:\n" + info.description));
const ok_button = document.createElement("button");
ok_button.onclick = function() { this.closeWindow();};
content.appendChild(pre_tag);
content.appendChild(ok_button);
this.showWindow('Details', content, 400, 400);
}
restoreFromXml(xml, editor) {
const doc = mxUtils.parseXml(xml);
const node = doc.documentElement;
editor.readGraphModel(node);
//Iterate over all children, and parse the networks to add them to the sidebar
for( const cell of this.graph.getModel().getChildren(this.graph.getDefaultParent())) {
if(cell.getId().indexOf("network") > -1) {
const info = JSON.parse(cell.getValue());
const name = info['name'];
this.networks.add(name);
const styles = cell.getStyle().split(";");
let color = null;
for(const style of styles){
const kvp = style.split('=');
if(kvp[0] == "fillColor") {
color = kvp[1];
break;
}
}
if(info.public){
this.has_public_net = true;
}
this.netCount++;
this.makeSidebarNetwork(name, color, cell.getId());
}
}
}
deleteCell(cellId) {
var cell = this.graph.getModel().getCell(cellId);
if( cellId.indexOf("network") > -1 ) {
let elem = document.getElementById(cellId);
elem.parentElement.removeChild(elem);
}
this.graph.removeCells([cell]);
this.currentWindow.destroy();
}
newNetworkWindow() {
const input = document.createElement("input");
input.type = "text";
input.name = "net_name";
input.maxlength = 100;
input.id = "net_name_input";
input.style.margin = "5px";
const yes_button = document.createElement("button");
yes_button.onclick = function() {this.parseNetworkWindow();}.bind(this);
yes_button.appendChild(document.createTextNode("Okay"));
const cancel_button = document.createElement("button");
cancel_button.onclick = function() {this.closeWindow();}.bind(this);
cancel_button.appendChild(document.createTextNode("Cancel"));
const error_div = document.createElement("div");
error_div.id = "current_window_errors";
const content = document.createElement("div");
content.appendChild(document.createTextNode("Name: "));
content.appendChild(input);
content.appendChild(document.createElement("br"));
content.appendChild(yes_button);
content.appendChild(cancel_button);
content.appendChild(document.createElement("br"));
content.appendChild(error_div);
this.showWindow("Network Creation", content, 300, 300);
}
parseNetworkWindow() {
const net_name = document.getElementById("net_name_input").value
const error_div = document.getElementById("current_window_errors");
if( this.networks.has(net_name) ){
error_div.innerHTML = "All network names must be unique";
return;
}
this.addNetwork(net_name);
this.currentWindow.destroy();
}
addToolbarButton(editor, toolbar, action, image) {
const button = document.createElement('button');
button.setAttribute('class', 'btn btn-sm m-1');
if (image != null) {
const icon = document.createElement('i');
icon.setAttribute('class', 'fas ' + image);
button.appendChild(icon);
}
mxEvent.addListener(button, 'click', function(evt) {
editor.execute(action);
});
mxUtils.write(button, '');
toolbar.appendChild(button);
};
encodeGraph() {
const encoder = new mxCodec();
const xml = encoder.encode(this.graph.getModel());
return mxUtils.getXml(xml);
}
doGlobalConfig() {
//general graph stuff
this.graph.setMultigraph(false);
this.graph.setCellsSelectable(false);
this.graph.setCellsMovable(false);
//testing
this.graph.vertexLabelIsMovable = true;
//edge behavior
this.graph.setConnectable(true);
this.graph.setAllowDanglingEdges(false);
mxEdgeHandler.prototype.snapToTerminals = true;
mxConstants.MIN_HOTSPOT_SIZE = 16;
mxConstants.DEFAULT_HOTSPOT = 1;
//edge 'style' (still affects behavior greatly)
const style = this.graph.getStylesheet().getDefaultEdgeStyle();
style[mxConstants.STYLE_EDGE] = mxConstants.EDGESTYLE_ELBOW;
style[mxConstants.STYLE_ENDARROW] = mxConstants.NONE;
style[mxConstants.STYLE_ROUNDED] = true;
style[mxConstants.STYLE_FONTCOLOR] = 'black';
style[mxConstants.STYLE_STROKECOLOR] = 'red';
style[mxConstants.STYLE_LABEL_BACKGROUNDCOLOR] = '#FFFFFF';
style[mxConstants.STYLE_STROKEWIDTH] = '3';
style[mxConstants.STYLE_ROUNDED] = true;
style[mxConstants.STYLE_EDGE] = mxEdgeStyle.EntityRelation;
const hostStyle = this.graph.getStylesheet().getDefaultVertexStyle();
hostStyle[mxConstants.STYLE_ROUNDED] = 1;
this.graph.convertValueToString = function(cell) {
try{
//changes value for edges with xml value
if(cell.isEdge()) {
if(JSON.parse(cell.getValue())["tagged"]) {
return "tagged";
}
return "untagged";
}
else{
return JSON.parse(cell.getValue())['name'];
}
}
catch(e){
return cell.getValue();
}
};
}
showWindow(title, content, width, height) {
//create transparent black background
const background = document.createElement('div');
background.style.position = 'absolute';
background.style.left = '0px';
background.style.top = '0px';
background.style.right = '0px';
background.style.bottom = '0px';
background.style.background = 'black';
mxUtils.setOpacity(background, 50);
document.body.appendChild(background);
const x = Math.max(0, document.body.scrollWidth/2-width/2);
const y = Math.max(10, (document.body.scrollHeight ||
document.documentElement.scrollHeight)/2-height*2/3);
const wnd = new mxWindow(title, content, x, y, width, height, false, true);
wnd.setClosable(false);
wnd.addListener(mxEvent.DESTROY, function(evt) {
this.graph.setEnabled(true);
mxEffects.fadeOut(background, 50, true, 10, 30, true);
}.bind(this));
this.currentWindow = wnd;
this.graph.setEnabled(false);
this.currentWindow.setVisible(true);
};
closeWindow() {
//allows the current window to be destroyed
this.currentWindow.destroy();
};
othersUntagged(edgeID) {
const edge = this.graph.getModel().getCell(edgeID);
const end1 = edge.getTerminal(true);
const end2 = edge.getTerminal(false);
if( end1.getParent().getId().split('_')[0] == 'host' ){
var netint = end1;
} else {
var netint = end2;
}
var edges = netint.edges;
for( let edge of edges) {
if( edge.getValue() ) {
var tagged = JSON.parse(edge.getValue()).tagged;
} else {
var tagged = true;
}
if( !tagged ) {
return true;
}
}
return false;
};
deleteVlanWindow(edgeID) {
const cell = this.graph.getModel().getCell(edgeID);
this.graph.removeCells([cell]);
this.currentWindow.destroy();
}
parseVlanWindow(edgeID) {
//do parsing and data manipulation
const radios = document.getElementsByName("tagged");
const edge = this.graph.getModel().getCell(edgeID);
for(let radio of radios){
if(radio.checked) {
//set edge to be tagged or untagged
if( radio.value == "False") {
if( this.othersUntagged(edgeID) ) {
document.getElementById("current_window_errors").innerHTML = "Only one untagged vlan per interface is allowed.";
return;
}
}
const edgeVal = {tagged: radio.value == "True"};
edge.setValue(JSON.stringify(edgeVal));
break;
}
}
this.graph.refresh(edge);
this.closeWindow();
}
makeMxNetwork(net_name, is_public = false) {
const model = this.graph.getModel();
const width = 10;
const height = 1700;
const xoff = 400 + (30 * this.netCount);
const yoff = -10;
let color = this.netColors[this.netCount];
if( this.netCount > (this.netColors.length - 1)) {
color = Math.floor(Math.random() * 16777215); //int in possible color space
color = '#' + color.toString(16).toUpperCase(); //convert to hex
}
const net_val = { name: net_name, public: is_public};
const net = this.graph.insertVertex(
this.graph.getDefaultParent(),
'network_' + this.netCount,
JSON.stringify(net_val),
xoff,
yoff,
width,
height,
'fillColor=' + color,
false
);
const num_ports = 45;
for(var i=0; i,
* tagged: bool
* }
* ]
* }
* ]
* }
*
* network_mappings: {
* :
* }
*/
makeHost(hostInfo, network_mappings) {
const value = JSON.stringify(hostInfo['value']);
const interfaces = hostInfo['interfaces'];
const width = 100;
const height = (25 * interfaces.length) + 25;
const xoff = 75;
const yoff = this.lastHostBottom + 50;
this.lastHostBottom = yoff + height;
const host = this.graph.insertVertex(
this.graph.getDefaultParent(),
'host_' + hostInfo['id'],
value,
xoff,
yoff,
width,
height,
'editable=0',
false
);
host.getGeometry().offset = new mxPoint(-50,0);
host.setConnectable(false);
this.hostCount++;
for(var i=0; i 1)
{
continue;
}
itemIDs = itemIDs.concat(this.serialize(trie[key]));
}
if ( trie.isComplete )
{
itemIDs.push(...trie.ids);
}
return itemIDs;
}
collate(trees)
{
/*
takes a list of tries
returns a list of ids of objects that are available
*/
const results = [];
for( const tree of trees )
{
const available_IDs = this.serialize(tree);
for( const itemID of available_IDs ) {
results[itemID] = this.items[itemID];
}
}
return results;
}
generate_element_text(obj)
{
const content_strings = [obj.expanded_name, obj.small_name, obj.string].filter(x => Boolean(x));
const result = content_strings.shift();
if( result == null || content_strings.length < 1) {
return result;
} else {
return result + " (" + content_strings.join(", ") + ")";
}
}
dropdown(ids)
{
/*
takes in a mapping of ids to objects in items
and displays them in the dropdown
*/
const drop = document.getElementById("drop_results");
while(drop.firstChild)
{
drop.removeChild(drop.firstChild);
}
for( const id in ids )
{
const obj = this.items[id];
const result_text = this.generate_element_text(obj);
const result_entry = document.createElement("a");
result_entry.href = "#";
result_entry.innerText = result_text;
result_entry.title = result_text;
result_entry.classList.add("list-group-item", "list-group-item-action", "overflow-ellipsis", "flex-shrink-0");
result_entry.onclick = function() { searchable_select_multiple_widget.select_item(obj.id); };
const tooltip = document.createElement("span");
const tooltiptext = document.createTextNode(result_text);
tooltip.appendChild(tooltiptext);
tooltip.classList.add("d-none");
result_entry.appendChild(tooltip);
drop.appendChild(result_entry);
}
const scroll_restrictor = document.getElementById("scroll_restrictor");
if( !drop.firstChild )
{
scroll_restrictor.style.visibility = 'hidden';
}
else
{
scroll_restrictor.style.visibility = 'inherit';
}
}
select_item(item_id)
{
if( (this.selectable_limit > -1 && this.added_items.size < this.selectable_limit) || this.selectable_limit < 0 )
{
this.added_items.add(item_id);
}
this.update_selected_list();
// clear search bar contents
document.getElementById("user_field").value = "";
document.getElementById("user_field").focus();
this.search("");
}
remove_item(item_id)
{
this.added_items.delete(item_id);
this.update_selected_list()
document.getElementById("user_field").focus();
}
update_selected_list()
{
document.getElementById("added_number").innerText = this.added_items.size;
const selector = document.getElementById('selector');
selector.value = JSON.stringify([...this.added_items]);
const added_list = document.getElementById('added_list');
while(selector.firstChild)
{
selector.removeChild(selector.firstChild);
}
while(added_list.firstChild)
{
added_list.removeChild(added_list.firstChild);
}
const list_html = document.createElement("div");
list_html.classList.add("list-group");
for( const item_id of this.added_items )
{
const times = document.createElement("li");
times.classList.add("fas", "fa-times");
const deleteButton = document.createElement("a");
deleteButton.href = "#";
deleteButton.innerHTML = ""
// Setting .onclick/.addEventListener does not work,
// which is why I took the setAttribute approach
// If anyone knows why, please let me know :]
deleteButton.setAttribute("onclick", `searchable_select_multiple_widget.remove_item(${item_id});`);
deleteButton.classList.add("btn");
const deleteColumn = document.createElement("div");
deleteColumn.classList.add("col-auto");
deleteColumn.append(deleteButton);
const item = this.items[item_id];
const element_entry_text = this.generate_element_text(item);
const textColumn = document.createElement("div");
textColumn.classList.add("col", "overflow-ellipsis");
textColumn.innerText = element_entry_text;
textColumn.title = element_entry_text;
const itemRow = document.createElement("div");
itemRow.classList.add("list-group-item", "d-flex", "p-0", "align-items-center");
itemRow.append(textColumn, deleteColumn);
list_html.append(itemRow);
}
added_list.innerHTML = list_html.innerHTML;
}
}