aboutsummaryrefslogtreecommitdiffstats
path: root/src/static
diff options
context:
space:
mode:
Diffstat (limited to 'src/static')
-rw-r--r--src/static/css/base.css124
-rw-r--r--src/static/js/dashboard.js1664
-rw-r--r--src/static/js/workflows/book-a-pod.js708
-rw-r--r--src/static/js/workflows/common-models.js189
-rw-r--r--src/static/js/workflows/design-a-pod.js1186
-rw-r--r--src/static/js/workflows/workflow.js246
6 files changed, 2452 insertions, 1665 deletions
diff --git a/src/static/css/base.css b/src/static/css/base.css
index 12364bd..b6ab104 100644
--- a/src/static/css/base.css
+++ b/src/static/css/base.css
@@ -69,17 +69,39 @@ a[aria-expanded="true"] > i.rotate {
/* Booking Node Styles */
.selected_node {
border-color: #40c640;
+ border-width: 2px;
box-shadow: 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(109, 243, 76, 0.6);
transition: border-color ease-in-out .1s,box-shadow ease-in-out .1s;
}
+.invalid_field {
+ border-color: #c65040;
+ box-shadow: 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(243, 76, 76, 0.6);
+ transition: border-color ease-in-out .1s,box-shadow ease-in-out .1s;
+}
+
/* Cursor effects */
.not-allowed {
cursor: not-allowed;
}
+.z-n1 {
+ z-index: -1 !important;
+}
+
+.z-0 {
+ z-index: 0 !important;
+}
+
+.z-1 {
+z-index: 1 !important;
+}
+
.z-2 {
- z-index: 2;
+ z-index: 2 !important;
+}
+.z-3 {
+ z-index: 3 !important;
}
.mh-30vh {
@@ -91,3 +113,103 @@ a[aria-expanded="true"] > i.rotate {
white-space: nowrap;
overflow: hidden;
}
+
+/* Design a pod Styles */
+.scroll-container {
+ position: absolute !important; /* Needed for proper functionality*/
+ overflow: auto;
+ scroll-snap-type: y proximity;
+}
+
+.scroll-area {
+ scroll-snap-align: start;
+ scroll-snap-stop: always;
+ min-height: 100vh;
+}
+
+.add-button {
+ font-size: 3em;
+ font-weight: bolder;
+ text-align: center;
+ height: 2em;
+ width: 2em;
+ margin-left: auto;
+ margin-right: auto;
+}
+
+.cancel-book-button {
+ font-size: 1em;
+ font-weight: bolder;
+ text-align: center;
+ height: 2em;
+ width: 10em;
+}
+
+.input-search:focus {
+ border-color: none !important;
+ box-shadow: none !important;
+}
+
+.arrow {
+ box-sizing: border-box;
+ height: 2vw;
+ width: 2vw;
+ border-style: solid;
+ border-color: black;
+ border-width: 0px 3px 3px 0px;
+ transition: border-width 150ms ease-in-out;
+ box-shadow: 0, 0, 100px, 100px, black;
+}
+
+.arrow-down {
+ transform: rotate(45deg);
+}
+
+.arrow-up {
+ transform: rotate(225deg);
+}
+
+#next {
+ position: fixed !important;
+ bottom: 0;
+ left: 0;
+ background-color: white;
+ align-items: center;
+ justify-content: center;
+ z-index: 2 !important;
+}
+
+#prev {
+ position: fixed !important;
+ left: 0;
+ background-color: white;
+ align-items: center;
+ justify-content: center;
+ z-index: 2 !important;
+}
+
+#next:hover,#next:active {
+ background-color: #d4d4d4;
+}
+
+#prev:hover,#prev:active {
+ background-color: #d4d4d4;
+}
+
+.btn-workflow-nav {
+ box-shadow: none !important;
+}
+
+.interface-btn {
+ color: inherit;
+}
+
+.card-body-scroll {
+ height: 25vh;
+ overflow-y: auto;
+}
+
+.overflow-control {
+ overflow-y: auto;
+ overflow-x: hidden;
+} \ No newline at end of file
diff --git a/src/static/js/dashboard.js b/src/static/js/dashboard.js
deleted file mode 100644
index a63c71b..0000000
--- a/src/static/js/dashboard.js
+++ /dev/null
@@ -1,1664 +0,0 @@
-///////////////////
-// 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("workflow-nav-back");
- 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("workflow-nav-next");
- 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(`<span class="d-flex align-items-center justify-content-center text-capitalize w-100">${step_json['title']}</span>`)
-
- const code = step_json.valid;
-
- let stat = "";
- let msg = "";
- if (code < 100) {
- $(step_dom).children().first().append("<i class='ml-2 far fa-square'></i>")
- stat = "";
- msg = "";
- } else if (code < 200) {
- $(step_dom).children().first().append("<i class='ml-2 fas fa-minus-square'></i>")
- stat = "invalid";
- msg = step_json.message;
- } else if (code < 300) {
- $(step_dom).children().first().append("<i class='ml-2 far fa-check-square'></i>")
- 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) {
- let color_code;
- if (stepstatus == 'valid') {
- color_code = 'text-success';
- } else if (stepstatus == 'invalid') {
- color_code = 'text-danger';
- } else {
- color_code = 'none';
- }
- document.getElementById("view_message").innerText = message;
- document.getElementById("view_message").className = "step_message";
- document.getElementById("view_message").classList.add("message_" + stepstatus);
- document.getElementById("view_message").classList.add(color_code);
-}
-
-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 = '<br> 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 <rr_index> on the <within> 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: {
- * <django network id>: <mxnetwork id>
- * }
- *
- * 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";
- tagged.checked = "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<num_ports; i++){
- let port = this.graph.insertVertex(
- net,
- null,
- '',
- 0,
- (1/num_ports) * i,
- 10,
- height / num_ports,
- 'fillColor=black;opacity=0',
- true
- );
- }
-
- const ret_val = { color: color, element_id: "network_" + this.netCount };
-
- this.networks.add(net_name);
- this.netCount++;
- return ret_val;
- }
-
- // expects:
- //
- // {
- // id: int,
- // name: str,
- // public: bool,
- // }
- //
- // returns:
- // mxgraph id of network
- populateNetwork(network) {
- let mxNet = this.makeMxNetwork(network.name, network.public);
- this.makeSidebarNetwork(network.name, mxNet.color, mxNet.element_id);
-
- if( network.public ) {
- this.has_public_net = true;
- }
-
- return mxNet.element_id;
- }
-
- addPublicNetwork() {
- const net = this.makeMxNetwork("public", true);
- this.makeSidebarNetwork("public", net['color'], net['element_id']);
- this.has_public_net = true;
- }
-
- addNetwork(net_name) {
- const ret = this.makeMxNetwork(net_name);
- this.makeSidebarNetwork(net_name, ret.color, ret.element_id);
- }
-
- updateHosts(removed) {
- const cells = []
- for(const hostID of removed) {
- cells.push(this.graph.getModel().getCell("host_" + hostID));
- }
- this.graph.removeCells(cells);
-
- const hosts = this.graph.getChildVertices(this.graph.getDefaultParent());
- let topdist = 100;
- for(const i in hosts) {
- const host = hosts[i];
- if(host.id.startsWith("host_")){
- const geometry = host.getGeometry();
- geometry.y = topdist + 50;
- topdist = geometry.y + geometry.height;
- host.setGeometry(geometry);
- }
- }
- }
-
- makeSidebarNetwork(net_name, color, net_id){
- const colorBlob = document.createElement("div");
- colorBlob.className = "square-20 rounded-circle";
- colorBlob.style['background'] = color;
-
- const textContainer = document.createElement("span");
- textContainer.className = "ml-2";
- textContainer.appendChild(document.createTextNode(net_name));
-
- const timesIcon = document.createElement("i");
- timesIcon.classList.add("fas", "fa-times");
-
- const deletebutton = document.createElement("button");
- deletebutton.className = "btn btn-danger ml-auto square-20 p-0 d-flex justify-content-center";
- deletebutton.appendChild(timesIcon);
- deletebutton.addEventListener("click", function() { this.createDeleteDialog(net_id); }.bind(this), false);
-
- const newNet = document.createElement("li");
- newNet.classList.add("list-group-item", "d-flex", "bg-light");
- newNet.id = net_id;
- newNet.appendChild(colorBlob);
- newNet.appendChild(textContainer);
-
- if( net_name != "public" ) {
- newNet.appendChild(deletebutton);
- }
- document.getElementById("network_list").appendChild(newNet);
- }
-
- /**
- * Expects format:
- * {
- * 'id': int,
- * 'value': {
- * 'description': string,
- * },
- * 'interfaces': [
- * {
- * id: int,
- * name: str,
- * description: str,
- * connections: [
- * {
- * network: int, <django network id>,
- * tagged: bool
- * }
- * ]
- * }
- * ]
- * }
- *
- * network_mappings: {
- * <django network id>: <mxnetwork id>
- * }
- */
- 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<interfaces.length; i++) {
- const port = this.graph.insertVertex(
- host,
- null,
- JSON.stringify(interfaces[i]),
- 90,
- (i * 25) + 12,
- 20,
- 20,
- 'fillColor=blue;editable=0',
- false
- );
- port.getGeometry().offset = new mxPoint(-4*interfaces[i].name.length -2,0);
- const iface = interfaces[i];
- for( const connection of iface.connections ) {
- const network = this
- .graph
- .getModel()
- .getCell(network_mappings[connection.network]);
-
- this.connectNetwork(port, network, connection.tagged);
- }
- this.graph.refresh(port);
- }
- this.graph.refresh(host);
- }
-
- prepareForm() {
- const input_elem = document.getElementById("hidden_xml_input");
- input_elem.value = this.encodeGraph(this.graph);
- }
-}
-
-class SearchableSelectMultipleWidget {
- constructor(format_vars, field_dataset, field_initial) {
- this.format_vars = format_vars;
- this.items = field_dataset;
- this.initial = field_initial;
-
- this.expanded_name_trie = {"isComplete": false};
- this.small_name_trie = {"isComplete": false};
- this.string_trie = {"isComplete": false};
-
- this.added_items = new Set();
-
- for( let e of ["show_from_noentry", "show_x_results", "results_scrollable", "selectable_limit", "placeholder"] )
- {
- this[e] = format_vars[e];
- }
-
- this.search_field_init();
-
- if( this.show_from_noentry )
- {
- this.search("");
- }
- }
-
- disable() {
- const textfield = document.getElementById("user_field");
- const drop = document.getElementById("drop_results");
-
- textfield.disabled = "True";
- drop.style.display = "none";
-
- const btns = document.getElementsByClassName("btn-remove");
- for( const btn of btns )
- {
- btn.classList.add("disabled");
- btn.onclick = "";
- }
- }
-
- search_field_init() {
- this.build_all_tries(this.items);
-
- for( const elem of this.initial )
- {
- this.select_item(elem);
- }
- if(this.initial.length == 1)
- {
- this.search(this.items[this.initial[0]]["small_name"]);
- document.getElementById("user_field").value = this.items[this.initial[0]]["small_name"];
- }
- }
-
- build_all_tries(dict)
- {
- for( const key in dict )
- {
- this.add_item(dict[key]);
- }
- }
-
- add_item(item)
- {
- const id = item['id'];
- this.add_to_tree(item['expanded_name'], id, this.expanded_name_trie);
- this.add_to_tree(item['small_name'], id, this.small_name_trie);
- this.add_to_tree(item['string'], id, this.string_trie);
- }
-
- add_to_tree(str, id, trie)
- {
- let inner_trie = trie;
- while( str )
- {
- if( !inner_trie[str.charAt(0)] )
- {
- var new_trie = {};
- inner_trie[str.charAt(0)] = new_trie;
- }
- else
- {
- var new_trie = inner_trie[str.charAt(0)];
- }
-
- if( str.length == 1 )
- {
- new_trie.isComplete = true;
- if( !new_trie.ids )
- {
- new_trie.ids = [];
- }
- new_trie.ids.push(id);
- }
- inner_trie = new_trie;
- str = str.substring(1);
- }
- }
-
- search(input)
- {
- if( input.length == 0 && !this.show_from_noentry){
- this.dropdown([]);
- return;
- }
- else if( input.length == 0 && this.show_from_noentry)
- {
- this.dropdown(this.items); //show all items
- }
- else
- {
- const trees = []
- const tr1 = this.getSubtree(input, this.expanded_name_trie);
- trees.push(tr1);
- const tr2 = this.getSubtree(input, this.small_name_trie);
- trees.push(tr2);
- const tr3 = this.getSubtree(input, this.string_trie);
- trees.push(tr3);
- const results = this.collate(trees);
- this.dropdown(results);
- }
- }
-
- getSubtree(input, given_trie)
- {
- /*
- recursive function to return the trie accessed at input
- */
-
- if( input.length == 0 ){
- return given_trie;
- }
-
- else{
- const substr = input.substring(0, input.length - 1);
- const last_char = input.charAt(input.length-1);
- const subtrie = this.getSubtree(substr, given_trie);
-
- if( !subtrie ) //substr not in the trie
- {
- return {};
- }
-
- const indexed_trie = subtrie[last_char];
- return indexed_trie;
- }
- }
-
- serialize(trie)
- {
- /*
- takes in a trie and returns a list of its item id's
- */
- let itemIDs = [];
- if ( !trie )
- {
- return itemIDs; //empty, base case
- }
- for( const key in trie )
- {
- if(key.length > 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 = "<i class='fas fa-times'></i>"
- // 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;
- }
-}
diff --git a/src/static/js/workflows/book-a-pod.js b/src/static/js/workflows/book-a-pod.js
new file mode 100644
index 0000000..d573342
--- /dev/null
+++ b/src/static/js/workflows/book-a-pod.js
@@ -0,0 +1,708 @@
+/**
+ * book-a-pod.js
+ */
+
+const steps = {
+ SELECT_TEMPLATE: 0,
+ CLOUD_INIT: 1,
+ BOOKING_DETAILS: 2,
+ ADD_COLLABS: 3,
+ BOOKING_SUMMARY: 4
+ }
+
+ class BookingWorkflow extends Workflow {
+ constructor(savedBookingBlob) {
+ super(["select_template", "cloud_init", "booking_details" ,"add_collabs", "booking_summary"])
+
+ // if (savedBookingBlob) {
+ // this.resume_workflow()
+ // }
+
+ this.bookingBlob = new BookingBlob({});
+ this.userTemplates = null;
+ }
+
+ async startWorkflow() {
+ this.userTemplates = await LibLaaSAPI.getTemplatesForUser() // List<TemplateBlob>
+ GUI.displayTemplates(this.userTemplates);
+ GUI.modifyCollabWidget();
+ this.setEventListeners();
+ document.getElementById(this.sections[0]).scrollIntoView({behavior: 'smooth'});
+ }
+
+ setEventListeners() {
+ const ci_textarea = document.getElementById('ci-textarea');
+ ci_textarea.value = ""
+ ci_textarea.addEventListener('focusin', this.onFocusInCIFile);
+ ci_textarea.addEventListener('focusout', this.onFocusOutCIFile);
+
+ const input_purpose = document.getElementById('input_purpose');
+ input_purpose.value = ""
+ input_purpose.addEventListener('focusin', this.onFocusInPurpose);
+ input_purpose.addEventListener('focusout', this.onFocusOutPurpose)
+
+ const input_project = document.getElementById('input_project');
+ input_project.value = ""
+ input_project.addEventListener('focusin', this.onFocusInProject);
+ input_project.addEventListener('focusout', this.onFocusOutProject);
+
+ const input_length = document.getElementById('input_length');
+ input_length.value = 1;
+ }
+
+ getTemplateBlobFromId(templateId) {
+ for (const t of this.userTemplates) {
+ if (t.id == templateId) return t
+ }
+
+ return null
+ }
+
+ onclickSelectTemplate(templateCard, templateId) {
+ this.step = steps.SELECT_TEMPLATE
+ const oldHighlight = document.querySelector("#default_templates_list .selected_node")
+ if (oldHighlight) {
+ GUI.unhighlightCard(oldHighlight)
+ }
+
+ GUI.highlightCard(templateCard);
+ this.bookingBlob.template_id = templateId;
+ GUI.refreshSummaryHosts(this.getTemplateBlobFromId(templateId));
+ }
+
+ isValidCIFile(ci_file) {
+ // todo
+ return true;
+ }
+
+ isValidProject(project) {
+ let passed = true
+ let message = "success"
+
+ if (project == "") {
+ passed = false;
+ message = "Project field cannot be empty."
+ return[passed, message]
+ }
+
+ if (!(project.match(/^[a-z0-9~@#$^*()_+=[\]{}|,.?': -]+$/i))) {
+ passed = false;
+ message = "Project field contains invalid characters"
+ return[passed, message]
+ }
+
+ return [passed, message]
+ }
+
+ isValidPurpose(purpose) {
+ let passed = true
+ let message = "success"
+
+ if (purpose == "") {
+ passed = false;
+ message = "Purpose field cannot be empty."
+ return[passed, message]
+ }
+
+ if (!(purpose.match(/^[a-z0-9~@#$^*()_+=[\]{}|,.?': -]+$/i))) {
+ passed = false;
+ message = "Purpose field contains invalid characters"
+ return[passed, message]
+ }
+
+ return [passed, message]
+ }
+
+ // Ci FIle
+ onFocusOutCIFile() {
+ const ci_textarea = document.getElementById('ci-textarea');
+ if (workflow.isValidCIFile(ci_textarea.value)) {
+ workflow.bookingBlob.global_cifile = ci_textarea.value;
+ } else {
+ GUI.highlightError(ci_textarea);
+ }
+ }
+
+ onFocusInCIFile() {
+ this.step = steps.CLOUD_INIT
+ const ci_textarea = document.getElementById('ci-textarea')
+ GUI.unhighlightError(ci_textarea)
+ }
+
+ // Purpose
+ onFocusOutPurpose() {
+ const input = document.getElementById('input_purpose');
+ const valid = workflow.isValidPurpose(input.value);
+ if (valid[0]) {
+ workflow.bookingBlob.metadata.purpose = input.value;
+ GUI.refreshSummaryDetails(workflow.bookingBlob.metadata)
+ } else {
+ GUI.showDetailsError(valid[1])
+ GUI.highlightError(input);
+ }
+ }
+
+ onFocusInPurpose() {
+ this.step = steps.BOOKING_DETAILS
+ const input = document.getElementById('input_purpose');
+ GUI.hideDetailsError()
+ GUI.unhighlightError(input)
+ }
+
+ // Project
+ onFocusOutProject() {
+ const input = document.getElementById('input_project');
+ const valid = workflow.isValidProject(input.value);
+ if (valid[0]) {
+ workflow.bookingBlob.metadata.project = input.value;
+ GUI.refreshSummaryDetails(workflow.bookingBlob.metadata)
+ } else {
+ GUI.showDetailsError(valid[1])
+ GUI.highlightError(input);
+ }
+ }
+
+ onFocusInProject() {
+ this.step = steps.BOOKING_DETAILS
+ const input = document.getElementById('input_project');
+ GUI.hideDetailsError()
+ GUI.unhighlightError(input)
+ }
+
+ onchangeDays() {
+ this.step = steps.BOOKING_DETAILS
+ const counter = document.getElementById("booking_details_day_counter")
+ const input = document.getElementById('input_length')
+ workflow.bookingBlob.metadata.length = input.value
+ GUI.refreshSummaryDetails(workflow.bookingBlob.metadata)
+ counter.innerText = "Days: " + input.value
+ }
+
+ add_collaborator(username) {
+ this.step = steps.ADD_COLLABS;
+
+ for (const c of this.bookingBlob.allowed_users) {
+ if (c == username) {
+ return;
+ }
+ }
+
+ this.bookingBlob.allowed_users.push(username)
+ GUI.refreshSummaryCollabs(this.bookingBlob.allowed_users)
+ }
+
+ remove_collaborator(username) {
+ // Removes collab from collaborators list and updates summary
+ this.step = steps.ADD_COLLABS
+
+ const temp = [];
+
+ for (const c of this.bookingBlob.allowed_users) {
+ if (c != username) {
+ temp.push(c);
+ }
+ }
+
+ this.bookingBlob.allowed_users = temp;
+ GUI.refreshSummaryCollabs(this.bookingBlob.allowed_users)
+ }
+
+ isCompleteBookingInfo() {
+ let passed = true
+ let message = "success"
+ let section = steps.BOOKING_SUMMARY
+
+ const blob = this.bookingBlob;
+ const meta = blob.metadata;
+
+ if (blob.template_id == null) {
+ passed = false;
+ message = "Please select a template."
+ section = steps.SELECT_TEMPLATE
+ return [passed, message, section]
+ }
+
+ if (meta.purpose == null || meta.project == null || meta.length == 0) {
+ passed = false
+ message = "Please finish adding booking details."
+ section = steps.BOOKING_DETAILS
+ return [passed, message, section]
+ }
+
+ return[passed, message, section];
+ }
+
+ onclickCancel() {
+ if (confirm("Are you sure you wish to discard this booking?")) {
+ location.reload();
+ }
+ }
+
+ /** Async / await is more infectious than I thought, so all functions that rely on an API call will need to be async */
+ async onclickConfirm() {
+ const complete = this.isCompleteBookingInfo();
+ if (!complete[0]) {
+ alert(complete[1]);
+ this.step = complete[2]
+ document.getElementById(this.sections[complete[2]]).scrollIntoView({behavior: 'smooth'});
+ return
+ }
+ if (confirm("Are you sure you would like to create this booking?")) {
+ const response = await LibLaaSAPI.makeBooking(this.bookingBlob);
+ if (response.bookingId) {
+ alert("The booking has been successfully created.")
+ window.location.href = "../../";
+ } else {
+ alert("The booking could not be created at this time.")
+ }
+ }
+ }
+
+
+
+ }
+
+
+/** View class that displays cards and generates HTML
+ * Functions as a namespace, does not hold state
+*/
+class GUI {
+
+ static highlightCard(card) {
+ card.classList.add('selected_node');
+ }
+
+ static unhighlightCard(card) {
+ card.classList.remove('selected_node');
+ }
+
+ static highlightError(element) {
+ element.classList.add('invalid_field');
+ }
+
+ static unhighlightError(element) {
+ element.classList.remove("invalid_field");
+ }
+
+ /** Takes a list of templateBlobs and creates a selectable card for each of them */
+ static displayTemplates(templates) {
+ const templates_list = document.getElementById("default_templates_list");
+
+ for (const t of templates) {
+ const newCard = this.makeTemplateCard(t);
+ templates_list.appendChild(newCard);
+ }
+ }
+
+ static makeTemplateCard(templateBlob) {
+ const col = document.createElement('div');
+ col.classList.add('col-3', 'my-1');
+ col.innerHTML= `
+ <div class="card">
+ <div class="card-header">
+ <p class="h5 font-weight-bold mt-2">` + templateBlob.pod_name + `</p>
+ </div>
+ <div class="card-body">
+ <p class="grid-item-description">` + templateBlob.pod_desc +`</p>
+ </div>
+ <div class="card-footer">
+ <button type="button" class="btn btn-success grid-item-select-btn w-100 stretched-link"
+ onclick="workflow.onclickSelectTemplate(this.parentNode.parentNode, '` + templateBlob.id +`')">Select</button>
+ </div>
+ </div>
+ `
+ return col;
+ }
+
+ /** Removes default styling applied by django */
+ static modifyCollabWidget() {
+ document.getElementsByTagName('label')[0].setAttribute('hidden', '');
+ document.getElementById('addable_limit').setAttribute('hidden', '');
+ document.getElementById('added_number').setAttribute('hidden', '');
+ const user_field = document.getElementById('user_field');
+ user_field.classList.add('border-top-0');
+ document.querySelector('.form-group').classList.add('mb-0');
+
+ const added_list = document.getElementById('added_list');
+ added_list.remove();
+ document.getElementById('search_select_outer').appendChild(added_list);
+ }
+
+ static showDetailsError(message) {
+ document.getElementById("booking_details_error").innerText = message;
+ }
+
+ static hideDetailsError() {
+ document.getElementById("booking_details_error").innerText = '';
+ }
+
+ static refreshSummaryDetails(bookingMetaData) {
+ const ul = document.getElementById("booking_summary_booking_details")
+ ul.innerHTML = '';
+
+ if (bookingMetaData.project) {
+ const project_li = document.createElement('li');
+ project_li.innerText = 'Project: ' + bookingMetaData.project;
+ ul.appendChild(project_li);
+ }
+
+ if (bookingMetaData.purpose) {
+ const project_li = document.createElement('li');
+ project_li.innerText = 'Purpose: ' + bookingMetaData.purpose;
+ ul.appendChild(project_li);
+ }
+
+ if (bookingMetaData.length) {
+ const project_li = document.createElement('li');
+ project_li.innerText = 'Length: ' + bookingMetaData.length + ' days';
+ ul.appendChild(project_li);
+ }
+ }
+
+ static refreshSummaryCollabs(collaborators) {
+ const collabs_ul = document.getElementById('booking_summary_collaborators');
+ collabs_ul.innerHTML = '';
+ for (const u of collaborators) {
+ const collabs_li = document.createElement('li');
+ collabs_li.innerText = u
+ collabs_ul.appendChild(collabs_li);
+ }
+ }
+
+ static refreshSummaryHosts(templateBlob) {
+ const hosts_ul = document.getElementById('booking_summary_hosts');
+ hosts_ul.innerHTML = '';
+ for (const h of templateBlob.host_list) {
+ const hosts_li = document.createElement('li');
+ hosts_li.innerText = h.hostname;
+ hosts_ul.appendChild(hosts_li);
+ }
+ }
+}
+
+
+ // Search widget for django forms (taken from dashboard.js and slightly modified)
+ class SearchableSelectMultipleWidget {
+ constructor(format_vars, field_dataset, field_initial) {
+ this.format_vars = format_vars;
+ this.items = field_dataset;
+ this.initial = field_initial;
+
+ this.expanded_name_trie = {"isComplete": false};
+ this.small_name_trie = {"isComplete": false};
+ this.string_trie = {"isComplete": false};
+
+ this.added_items = new Set();
+
+ for( let e of ["show_from_noentry", "show_x_results", "results_scrollable", "selectable_limit", "placeholder"] )
+ {
+ this[e] = format_vars[e];
+ }
+
+ this.search_field_init();
+
+ if( this.show_from_noentry )
+ {
+ this.search("");
+ }
+ }
+
+ disable() {
+ const textfield = document.getElementById("user_field");
+ const drop = document.getElementById("drop_results");
+
+ textfield.disabled = "True";
+ drop.style.display = "none";
+
+ const btns = document.getElementsByClassName("btn-remove");
+ for( const btn of btns )
+ {
+ btn.classList.add("disabled");
+ btn.onclick = "";
+ }
+ }
+
+ search_field_init() {
+ this.build_all_tries(this.items);
+
+ for( const elem of this.initial )
+ {
+ this.select_item(elem);
+ }
+ if(this.initial.length == 1)
+ {
+ this.search(this.items[this.initial[0]]["small_name"]);
+ document.getElementById("user_field").value = this.items[this.initial[0]]["small_name"];
+ }
+ }
+
+ build_all_tries(dict)
+ {
+ for( const key in dict )
+ {
+ this.add_item(dict[key]);
+ }
+ }
+
+ add_item(item)
+ {
+ const id = item['id'];
+ this.add_to_tree(item['expanded_name'], id, this.expanded_name_trie);
+ this.add_to_tree(item['small_name'], id, this.small_name_trie);
+ this.add_to_tree(item['string'], id, this.string_trie);
+ }
+
+ add_to_tree(str, id, trie)
+ {
+ let inner_trie = trie;
+ while( str )
+ {
+ if( !inner_trie[str.charAt(0)] )
+ {
+ var new_trie = {};
+ inner_trie[str.charAt(0)] = new_trie;
+ }
+ else
+ {
+ var new_trie = inner_trie[str.charAt(0)];
+ }
+
+ if( str.length == 1 )
+ {
+ new_trie.isComplete = true;
+ if( !new_trie.ids )
+ {
+ new_trie.ids = [];
+ }
+ new_trie.ids.push(id);
+ }
+ inner_trie = new_trie;
+ str = str.substring(1);
+ }
+ }
+
+ search(input)
+ {
+ if( input.length == 0 && !this.show_from_noentry){
+ this.dropdown([]);
+ return;
+ }
+ else if( input.length == 0 && this.show_from_noentry)
+ {
+ this.dropdown(this.items); //show all items
+ }
+ else
+ {
+ const trees = []
+ const tr1 = this.getSubtree(input, this.expanded_name_trie);
+ trees.push(tr1);
+ const tr2 = this.getSubtree(input, this.small_name_trie);
+ trees.push(tr2);
+ const tr3 = this.getSubtree(input, this.string_trie);
+ trees.push(tr3);
+ const results = this.collate(trees);
+ this.dropdown(results);
+ }
+ }
+
+ getSubtree(input, given_trie)
+ {
+ /*
+ recursive function to return the trie accessed at input
+ */
+
+ if( input.length == 0 ){
+ return given_trie;
+ }
+
+ else{
+ const substr = input.substring(0, input.length - 1);
+ const last_char = input.charAt(input.length-1);
+ const subtrie = this.getSubtree(substr, given_trie);
+
+ if( !subtrie ) //substr not in the trie
+ {
+ return {};
+ }
+
+ const indexed_trie = subtrie[last_char];
+ return indexed_trie;
+ }
+ }
+
+ serialize(trie)
+ {
+ /*
+ takes in a trie and returns a list of its item id's
+ */
+ let itemIDs = [];
+ if ( !trie )
+ {
+ return itemIDs; //empty, base case
+ }
+ for( const key in trie )
+ {
+ if(key.length > 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("");
+
+ const item = this.items[item_id];
+ const element_entry_text = this.generate_element_text(item);
+ const username = item.small_name;
+ workflow.add_collaborator(username, element_entry_text);
+ }
+
+ remove_item(item_id)
+ {
+ this.added_items.delete(item_id); // delete from set
+
+ const item = this.items[item_id];
+ workflow.remove_collaborator(item.small_name);
+
+ 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 = "<i class='fas fa-times'></i>"
+ 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;
+ textColumn.id = `coldel-${item_id}`; // Needed for book a pod
+
+ const itemRow = document.createElement("div");
+ itemRow.classList.add("list-group-item", "d-flex", "p-0", "align-items-center", "my-2", "border");
+ itemRow.append(textColumn, deleteColumn);
+
+ list_html.append(itemRow);
+ }
+ added_list.innerHTML = list_html.innerHTML;
+ }
+} \ No newline at end of file
diff --git a/src/static/js/workflows/common-models.js b/src/static/js/workflows/common-models.js
new file mode 100644
index 0000000..65fedb1
--- /dev/null
+++ b/src/static/js/workflows/common-models.js
@@ -0,0 +1,189 @@
+/*
+common-models.js
+Defines classes used by the workflows
+Functions as the "model" part of MVC
+*/
+
+// Provided by the LibLaaS API
+// TemplateBlob classes
+class TemplateBlob {
+ constructor(incomingBlob) {
+ this.id = incomingBlob.id; // UUID (String)
+ this.owner = incomingBlob.owner; // String
+ this.lab_name = incomingBlob.lab_name; // String
+ this.pod_name = incomingBlob.pod_name; // String
+ this.pod_desc = incomingBlob.pod_desc; // String
+ this["public"] = incomingBlob["public"]; // bool
+ this.host_list = []; // List<HostConfigBlob>
+ this.networks = []; // List<NetworkBlob>
+
+ if (incomingBlob.host_list) {
+ this.host_list = incomingBlob.host_list;
+ }
+
+ if (incomingBlob.networks) {
+ this.networks = incomingBlob.networks;
+ }
+ }
+
+ /**
+ * Takes a network name (string) and returns the network stored in the template, or null if it does not exist
+ * @param {String} network_name
+ */
+ findNetwork(network_name) {
+ for (const network of this.networks) {
+ if (network.name == network_name) {
+ return network;
+ }
+ }
+
+ // Did not find it
+ return null;
+ }
+
+
+ /**
+ * Takes a hostname (string) and returns the host stored in the template, or null if it does not exist
+ * @param {String} hostname
+ */
+ findHost(hostname) {
+ for (const host of this.host_list) {
+ if (host.hostname == hostname) {
+ return host;
+ }
+ }
+
+ // Did not find it
+ return null;
+ }
+}
+
+class HostConfigBlob {
+ constructor(incomingBlob) {
+ this.hostname = incomingBlob.hostname; // String
+ this.flavor = incomingBlob.flavor; // UUID (String)
+ this.image = incomingBlob.image; // UUID (String)
+ this.cifile = []; // List<String>
+ this.bondgroups = []; // List<BondgroupBlob>
+
+ if (incomingBlob.cifile) {
+ this.cifile = incomingBlob.cifile;
+ }
+
+ if (incomingBlob.bondgroups) {
+ this.bondgroups = incomingBlob.bondgroups;
+ }
+ }
+}
+
+class NetworkBlob {
+ constructor(incomingBlob) {
+ this.name = incomingBlob.name;
+ this['public'] = incomingBlob['public'];
+
+ }
+}
+
+/** One bondgroup per interface at this time. */
+class BondgroupBlob {
+ constructor(incomingBlob) {
+ this.connections = []; //List<ConnectionBlob>
+ this.ifaces = []; // List<IfaceBlob> (will only contain the one iface for now)
+
+ if (incomingBlob.connections) {
+ this.connections = incomingBlob.connections;
+ }
+
+ if (incomingBlob.ifaces) {
+ this.ifaces = incomingBlob.ifaces;
+ }
+ }
+
+}
+
+class ConnectionBlob {
+ constructor(incomingBlob) {
+ this.tagged = incomingBlob.tagged; // bool,
+ this.connects_to = incomingBlob.connects_to; // String
+ }
+}
+
+class InterfaceBlob {
+ constructor(incomingBlob) {
+ this.name = incomingBlob.name; // String,
+ this.speed = incomingBlob.speed;
+ this.cardtype = incomingBlob.cardtype;
+ }
+}
+
+// BookingClasses
+class BookingBlob {
+ // constructor({template_id, allowed_users, global_cifile}) {
+ constructor(incomingBlob) {
+
+ this.template_id = incomingBlob.template_id; // UUID (String)
+ this.allowed_users = []; // List<String>
+ this.global_cifile = ""; // String
+ this.metadata = new BookingMetaDataBlob({});
+
+ if (incomingBlob.allowed_users) {
+ this.allowed_users = incomingBlob.allowed_users;
+ }
+
+ if (incomingBlob.global_cifile) {
+ this.global_cifile = incomingBlob.global_cifile;
+ }
+
+ if (incomingBlob.metadata) {
+ this.metadata = incomingBlob.metadata;
+ }
+ }
+}
+
+
+class BookingMetaDataBlob {
+ constructor(incomingBlob) {
+ this.booking_id = incomingBlob.booking_id; // String
+ this.owner = incomingBlob.owner; // String
+ this.lab = incomingBlob.lab; // String
+ this.purpose = incomingBlob.purpose; // String
+ this.project = incomingBlob.project; // String
+ this.length = 1 // Number
+
+ if (incomingBlob.length) {
+ this.length = incomingBlob.length;
+ }
+ }
+}
+
+// Utility Classes
+class ImageBlob {
+ constructor(incomingBlob) {
+ this.image_id = incomingBlob.image_id; // UUID (String)
+ this.name = incomingBlob.name; // String,
+ }
+}
+
+class FlavorBlob {
+ constructor(incomingBlob) {
+ this.flavor_id = incomingBlob.flavor_id; // UUID (String)
+ this.name = incomingBlob.name; // String
+ this.interfaces = []; // List<String>
+ // images are added after
+
+ if (incomingBlob.interfaces) {
+ this.interfaces = incomingBlob.interfaces;
+ }
+ }
+
+}
+
+class LabBlob {
+ constructor(incomingBlob) {
+ this.name = incomingBlob.name; // String
+ this.description = incomingBlob.description; // String
+ this.location = incomingBlob.location; //String
+ this.status = incomingBlob.status; // Number
+
+ }
+} \ No newline at end of file
diff --git a/src/static/js/workflows/design-a-pod.js b/src/static/js/workflows/design-a-pod.js
new file mode 100644
index 0000000..7632537
--- /dev/null
+++ b/src/static/js/workflows/design-a-pod.js
@@ -0,0 +1,1186 @@
+/*
+design-a-pod.js
+
+Functions as the "controller" part of MVC
+*/
+
+
+const steps = {
+ SELECT_LAB: 0,
+ ADD_RESOURCES: 1,
+ ADD_NETWORKS: 2,
+ CONFIGURE_CONNECTIONS: 3,
+ POD_DETAILS: 4,
+ POD_SUMMARY: 5
+}
+
+/** Concrete controller class that handles button inputs from the user.
+ * Holds the in-progress TemplateBlob.
+ * Click methods are prefaced with 'onclick'.
+ * Step initialization methods are prefaced with 'step'.
+ */
+class DesignWorkflow extends Workflow {
+ constructor(savedTemplateBlob) {
+ super(["select_lab", "add_resources", "add_networks", "configure_connections", "pod_details", "pod_summary"])
+
+ // if(savedTemplateBlob) {
+ // this.resume_workflow();
+ // }
+
+ this.templateBlob = new TemplateBlob({});
+ this.labFlavors; // Map<UUID, FlavorBlob>
+ this.userTemplates; // List<TemplateBlob>
+ this.resourceBuilder; // ResourceBuilder
+
+ this.templateBlob.public = false;
+ }
+
+ /** Finds the templateBlob object in the userTemplates list based on a given uuid */
+ getTemplateById(template_id) {
+ for (let template of this.userTemplates) {
+ if (template.id == template_id) {
+ return template;
+ }
+ }
+ return null;
+ }
+
+ resumeWorkflow() {
+ todo()
+ }
+
+ /** Initializes the select_lab step */
+ async startWorkflow() {
+ this.setPodDetailEventListeners();
+ const labs = await LibLaaSAPI.getLabs();
+ GUI.display_labs(labs);
+ document.getElementById(this.sections[0]).scrollIntoView({behavior: 'smooth'});
+ }
+
+ /** Adds the public network on start */
+ addDefaultNetwork() {
+ const new_network = new NetworkBlob({});
+ new_network.name = "public";
+ new_network.public = true;
+ this.addNetworkToPod(new_network);
+ GUI.refreshNetworkStep(this.templateBlob.networks);
+ }
+
+ /** Takes an HTML element */
+ async onclickSelectLab(lab_card) {
+ this.step = steps.SELECT_LAB;
+
+ if (this.templateBlob.lab_name == null) { // Lab has not been selected yet
+ this.templateBlob.lab_name = lab_card.id;
+ lab_card.classList.add("selected_node");
+ await this.setLabDetails(this.templateBlob.lab_name);
+ this.addDefaultNetwork();
+ } else { // Lab has been selected
+ if(confirm('Unselecting a lab will reset all selected resources, are you sure?')) {
+ location.reload();
+ }
+ }
+ }
+
+ /** Calls the API to fetch flavors and images for a lab */
+ async setLabDetails(lab_name) {
+ const flavorsList = await LibLaaSAPI.getLabFlavors(lab_name);
+ this.labFlavors = new Map(); // Map<UUID, FlavorBlob>
+ this.labImages = new Map(); // Map<UUID, ImageBlob>
+
+ for (const fblob of flavorsList) {
+ fblob.images = await LibLaaSAPI.getImagesForFlavor(fblob.flavor_id);
+ for (const iblob of fblob.images) {
+ this.labImages.set(iblob.image_id, iblob)
+ }
+ this.labFlavors.set(fblob.flavor_id, fblob);
+ }
+
+ this.userTemplates = await LibLaaSAPI.getTemplatesForUser();
+ }
+
+ /** Prepopulates fields and launches the modal */
+ onclickAddResource() {
+ // Set step
+ // Check prerequisites
+ // Reset resourceBuilder
+ // Generate template cards
+ // Show modal
+
+ this.step = steps.ADD_RESOURCES;
+
+ if (this.templateBlob.lab_name == null) {
+ alert("Please select a lab before adding resources.");
+ this.goTo(steps.SELECT_LAB);
+ return;
+ }
+
+ if (this.templateBlob.host_list.length >= 8) {
+ alert("You may not add more than 8 hosts to a single pod.")
+ return;
+ }
+
+ this.resourceBuilder = null;
+ GUI.refreshAddHostModal(this.userTemplates);
+ $("#resource_modal").modal('toggle');
+
+ }
+
+ onclickSelectTemplate(template_id, card) {
+
+ // Do nothing on reselect
+ if (this.resourceBuilder && this.resourceBuilder.template_id == template_id) {
+ return;
+ }
+
+ if (this.resourceBuilder) {
+ GUI.unhighlightCard(document.querySelector('#template-cards .selected_node'));
+ }
+
+ this.resourceBuilder = new ResourceBuilder(this.getTemplateById(template_id));
+ GUI.highlightCard(card);
+ GUI.refreshConfigSection(this.resourceBuilder, this.labFlavors);
+ GUI.refreshInputSection(this.resourceBuilder, this.labFlavors);
+ }
+
+ onclickSelectNode(index) {
+ this.resourceBuilder.tab = index;
+ GUI.refreshInputSection(this.resourceBuilder, this.labFlavors);
+ }
+
+ onclickSelectImage(image_id, card) {
+ const old_selection = document.querySelector("#image-cards .selected_node");
+ if (old_selection) {
+ GUI.unhighlightCard(old_selection);
+ }
+ this.resourceBuilder.user_configs[this.resourceBuilder.tab].image = image_id;
+ GUI.highlightCard(card.childNodes[1]);
+ }
+
+ /** Takes a string and returns a tuple containing the result and the error message (bool, string)*/
+ isValidHostname(hostname) {
+ let result = true;
+ let message = "success";
+
+ if (hostname == null || hostname == '') {
+ result = false;
+ message = 'Please enter a hostname';
+
+ } else if (hostname.length > 25) {
+ result = false;
+ message = 'Hostnames cannot exceed 25 characters';
+
+ } else if (!(hostname.match(/^[0-9a-z-]+$/i))) {
+ result = false;
+ message = 'Hostnames must only contain alphanumeric characters and dashes';
+
+ } else if ((hostname.charAt(0).match(/^[0-9-]+$/)) || (hostname.charAt(hostname.length - 1) == '-')) {
+ result = false;
+ message = 'Hostnames must start with a letter and end with a letter or digit.';
+ }
+
+ return [result, message];
+ }
+
+ /** Takes a hostname and a list of existing hosts and checks for duplicates in the existing hostlist*/
+ isUniqueHostname(hostname, existing_hosts) {
+ for (const existing_host of existing_hosts) {
+ if (hostname == existing_host.hostname) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ onclickSubmitHostConfig() {
+ // Validate form fields
+ // Create host config blobs
+ // Apply networking
+ // Create cards (refresh hostcard view)
+ // Refresh networks view
+ // Refresh connections view
+
+ // Validate Configs
+ for (const [index, host] of this.resourceBuilder.user_configs.entries()) {
+ let result = this.isValidHostname(host.hostname);
+ if (!result[0]) {
+ this.resourceBuilder.tab = index;
+ GUI.refreshConfigSection(this.resourceBuilder, this.labFlavors);
+ GUI.refreshInputSection(this.resourceBuilder, this.labFlavors);
+ GUI.showHostConfigErrorMessage(result[1]);
+ return;
+ }
+
+ let uniqueHost = this.isUniqueHostname(host.hostname, this.templateBlob.host_list);
+ if (!uniqueHost) {
+ this.resourceBuilder.tab = index;
+ GUI.refreshConfigSection(this.resourceBuilder, this.labFlavors);
+ GUI.refreshInputSection(this.resourceBuilder, this.labFlavors);
+ GUI.showHostConfigErrorMessage("Hostname '"+ host.hostname + "' already exists in Pod.");
+ return;
+ }
+
+ if (index < this.resourceBuilder.user_configs.length - 1) {
+ let uniqueConfigName = true;
+ for (let i = index + 1; i < this.resourceBuilder.user_configs.length; i++) {
+ if (host.hostname == this.resourceBuilder.user_configs[i].hostname) {
+ uniqueConfigName = false;
+ break;
+ }
+ }
+
+ if (!uniqueConfigName) {
+ this.resourceBuilder.tab = index;
+ GUI.refreshConfigSection(this.resourceBuilder, this.labFlavors);
+ GUI.refreshInputSection(this.resourceBuilder, this.labFlavors);
+ GUI.showHostConfigErrorMessage("Hostname '"+ host.hostname + "' is a duplicate hostname.");
+ return;
+ }
+ }
+
+ // todo
+ // let result2 = isValidCIFile(host.cifile[0]);
+ }
+
+
+ // Add host configs to TemplateBlob
+ for (const [index, host] of this.resourceBuilder.user_configs.entries()) {
+ const new_host = new HostConfigBlob(host);
+ this.templateBlob.host_list.push(new_host);
+ }
+
+ // Add networks
+ for (const n of this.resourceBuilder.networks) {
+ if (!this.templateBlob.findNetwork(n.name)) {
+ this.templateBlob.networks.push(n);
+ }
+ }
+
+ // We are done
+ GUI.refreshHostStep(this.templateBlob.host_list, this.labFlavors, this.labImages);
+ GUI.refreshNetworkStep(this.templateBlob.networks);
+ GUI.refreshConnectionStep(this.templateBlob.host_list);
+ GUI.refreshPodSummaryHosts(this.templateBlob.host_list, this.labFlavors, this.labImages)
+ $('#resource_modal').modal('hide')
+ }
+
+ /**
+ * Takes a hostname, looks for the matching HostConfigBlob in the TemplateBlob, removes it from the list, and refreshes the appropriate views
+ * @param {String} hostname
+ */
+ onclickDeleteHost(hostname) {
+ this.step = steps.ADD_RESOURCES;
+ for (let existing_host of this.templateBlob.host_list) {
+ if (hostname == existing_host.hostname) {
+ this.removeHostFromTemplateBlob(existing_host);
+ GUI.refreshHostStep(this.templateBlob.host_list, this.labFlavors, this.labImages);
+ GUI.refreshNetworkStep(this.templateBlob.networks);
+ GUI.refreshConnectionStep(this.templateBlob.host_list);
+ GUI.refreshPodSummaryHosts(this.templateBlob.host_list, this.labFlavors, this.labImages);
+ return;
+ }
+ }
+
+ alert("didnt remove");
+ }
+
+
+
+
+ /** onclick handler for the add_network_plus_card */
+ onclickAddNetwork() {
+ // Set step
+ // Prerequisite step checks
+ // GUI stuff
+
+ this.step = steps.ADD_NETWORKS;
+
+ if (this.templateBlob.lab_name == null) {
+ alert("Please select a lab before adding networks.");
+ this.goTo(steps.SELECT_LAB);
+ return;
+ }
+
+ if (document.querySelector('#new_network_card') != null) {
+ alert("Please finish adding the current network before adding a new one.");
+ return;
+ }
+
+ GUI.display_network_input();
+ }
+
+ /** onclick handler for the adding_network_confirm button */
+ onclickConfirmNetwork() {
+ this.step = steps.ADD_NETWORKS;
+
+ // Add the network
+ // call the GUI to make the card (refresh the whole view to make it easier)
+
+ const new_network = new NetworkBlob({});
+ new_network.name = document.getElementById('network_input').value;
+ new_network.public = document.getElementById('network-public-input').checked;
+ const error_message = this.addNetworkToPod(new_network);
+
+ if (error_message == null) {
+ GUI.refreshNetworkStep(this.templateBlob.networks);
+ GUI.refreshConnectionStep(this.templateBlob.host_list);
+ } else {
+ GUI.display_add_network_error(error_message);
+ }
+ }
+
+ /** Takes a NetworkBlob and tries to add to the TemplateBlob.
+ * Fails if input validation fails.
+ * Returns error message or null.
+ */
+ addNetworkToPod(networkBlob) {
+ if (networkBlob.name == '' || networkBlob.name == null) {
+ return "Network name cannot be empty.";
+ }
+
+ if (networkBlob.name.length > 25) {
+ return 'Network names cannot exceed 25 characters';
+ }
+
+ if (!(networkBlob.name.match(/^[0-9a-z-]+$/i))) {
+ return 'Network names must only contain alphanumeric characters and dashes';
+ }
+
+ if ((networkBlob.name.charAt(0).match(/^[0-9-]+$/)) || (networkBlob.name.charAt(networkBlob.name.length - 1) == '-')) {
+ return 'Network names must start with a letter and end with a letter or digit.';
+ }
+
+ for (let existing_network of this.templateBlob.networks) {
+ if (networkBlob.name == existing_network.name) {
+ return 'Networks must have unique names';
+ }
+ }
+
+ this.templateBlob.networks.push(networkBlob);
+ return null;
+ }
+
+ /** Iterates through the templateBlob looking for the correct network to delete
+ * Takes a network name as a parameter.
+ */
+ onclickDeleteNetwork(network_name) {
+ this.step = steps.ADD_NETWORKS;
+
+ for (let existing_network of this.templateBlob.networks) {
+ if (network_name == existing_network.name) {
+ this.removeNetworkFromTemplateBlob(existing_network);
+ this.removeConnectionsOnNetwork(existing_network.name)
+ GUI.refreshNetworkStep(this.templateBlob.networks);
+ GUI.refreshConnectionStep(this.templateBlob.host_list);
+ return;
+ }
+ }
+
+ alert("didnt remove");
+ }
+
+ /** Rebuilds the list without the chosen template */
+ removeNetworkFromTemplateBlob(network_to_remove) {
+ this.templateBlob.networks = this.templateBlob.networks.filter(network => network !== network_to_remove);
+ }
+
+ removeConnectionsOnNetwork(network_name) {
+ for (const host of this.templateBlob.host_list) {
+ for (const bg of host.bondgroups) {
+ bg.connections = bg.connections.filter((connection) => connection.connects_to != network_name)
+ }
+ }
+ }
+
+ /**
+ * Rebuilds the hostlist without the chosen host
+ * Also removes all connections from this host's interfaces
+ * @param {HostConfigBlob} hostBlob
+ */
+ removeHostFromTemplateBlob(hostBlob) {
+ this.templateBlob.host_list = this.templateBlob.host_list.filter(host => host !== hostBlob);
+ }
+
+ onclickConfigureConnection(hostname) {
+ this.step = steps.CONFIGURE_CONNECTIONS;
+
+ const host = this.templateBlob.findHost(hostname);
+ if (!host) {
+ alert("host not found error");
+ }
+
+ this.connectionTemp = new ConnectionTemp(host, this.templateBlob.networks, this.labFlavors.get(host.flavor).interfaces);
+ GUI.refreshConnectionModal(this.connectionTemp);
+ $("#connection_modal").modal('toggle');
+ }
+
+ onclickSelectIfaceTab(tab_index) {
+ this.connectionTemp.selected_index = tab_index;
+ GUI.refreshConnectionModal(this.connectionTemp);
+ }
+
+ onclickSelectVlan(network_name, tagged, iface_name) {
+ const x = this.connectionTemp.config.get(iface_name);
+ if (x.get(network_name) === tagged) {
+ x.set(network_name, null);
+ } else {
+ x.set(network_name, tagged);
+ }
+
+ GUI.refreshConnectionTable(this.connectionTemp);
+ }
+
+ onclickSubmitConnectionConfig() {
+ this.connectionTemp.applyConfigs();
+ GUI.refreshConnectionStep(this.templateBlob.host_list);
+ }
+
+ /** Sets input validation event listeners and clears the value in case of caching*/
+ setPodDetailEventListeners() {
+ const pod_name_input = document.getElementById("pod-name-input");
+ const pod_desc_input = document.getElementById("pod-desc-input");
+ const pod_public_input = document.getElementById("pod-public-input");
+
+ pod_name_input.value = "";
+ pod_desc_input.value = "";
+ pod_public_input.checked = false;
+
+ pod_name_input.addEventListener('focusout', (event)=> {
+ workflow.onFocusOutPodNameInput(pod_name_input);
+ });
+
+ pod_name_input.addEventListener('focusin', (event)=> {
+ this.step = steps.POD_DETAILS;
+ GUI.unhighlightError(pod_name_input);
+ GUI.hidePodDetailsError();
+ });
+
+ pod_desc_input.addEventListener('focusout', (event)=> {
+ workflow.onFocusOutPodDescInput(pod_desc_input);
+ });
+
+ pod_desc_input.addEventListener('focusin', (event)=> {
+ this.step = steps.POD_DETAILS;
+ GUI.unhighlightError(pod_desc_input);
+ GUI.hidePodDetailsError();
+ });
+
+ pod_public_input.addEventListener('focusout', (event)=> {
+ this.step = steps.POD_DETAILS;
+ workflow.onFocusOutPodPublicInput(pod_public_input);
+ });
+ }
+
+ onFocusOutPodNameInput(element) {
+ const pod_name = element.value;
+ const validator = this.validatePodInput(pod_name, 53, "Pod name");
+
+ if (validator[0]) {
+ this.templateBlob.pod_name = pod_name;
+ GUI.refreshPodSummaryDetails(this.templateBlob.pod_name, this.templateBlob.pod_desc, this.templateBlob.public)
+ } else {
+ GUI.highlightError(element);
+ GUI.showPodDetailsError(validator[1]);
+ }
+ }
+
+ onFocusOutPodDescInput(element) {
+ const pod_desc = element.value;
+ const validator = this.validatePodInput(pod_desc, 255, "Pod description");
+
+ if (validator[0]) {
+ this.templateBlob.pod_desc = pod_desc;
+ GUI.refreshPodSummaryDetails(this.templateBlob.pod_name, this.templateBlob.pod_desc, this.templateBlob.public)
+ } else {
+ GUI.highlightError(element);
+ GUI.showPodDetailsError(validator[1]);
+ }
+
+ }
+
+ onFocusOutPodPublicInput(element) {
+ this.templateBlob.public = element.checked;
+ GUI.refreshPodSummaryDetails(this.templateBlob.pod_name, this.templateBlob.pod_desc, this.templateBlob.public)
+ }
+
+ /** Returns a tuple containing result and message (bool, String) */
+ validatePodInput(input, maxCharCount, form_name) {
+ let result = true;
+ let message = "valid"
+
+ if (input === '') {
+ message = form_name + ' cannot be empty.';
+ result = false;
+ }
+ else if (input.length > maxCharCount) {
+ message = form_name + ' cannot exceed ' + maxCharCount + ' characters.';
+ result = false;
+ } else if (!(input.match(/^[a-z0-9~@#$^*()_+=[\]{}|,.?': -!]+$/i))) {
+ message = form_name + ' contains invalid characters.';
+ result = false;
+ }
+
+ return [result, message]
+ }
+
+ async onclickDiscardTemplate() {
+ this.step = steps.POD_SUMMARY;
+ if(confirm('Are you sure you wish to delete this Pod?')) {
+ await LibLaaSAPI.deleteTemplate(this.templateBlob);
+ location.reload();
+ }
+ }
+
+ simpleStepValidation() {
+ let passed = true;
+ let message = "done";
+ let step = steps.POD_SUMMARY;
+
+ if (this.templateBlob.lab_name == null) {
+ passed = false;
+ message = "Please select a lab";
+ step = steps.SELECT_LAB;
+ } else if (this.templateBlob.host_list.length < 1 || this.templateBlob.host_list.length > 8) {
+ passed = false;
+ message = "Pods must contain 1 to 8 hosts";
+ step = steps.ADD_RESOURCES;
+ } else if (this.templateBlob.networks.length < 1) {
+ passed = false;
+ message = "Pods must contain at least one network.";
+ step = steps.ADD_NETWORKS;
+ } else if (this.templateBlob.pod_name == null || this.templateBlob.pod_desc == null) {
+ passed = false;
+ message = "Please add a valid pod name and description.";
+ step = steps.POD_DETAILS;
+ }
+ return [passed, message, step];
+ }
+
+ async onclickSubmitTemplate() {
+ this.step = steps.POD_SUMMARY;
+ const simpleValidation = this.simpleStepValidation();
+ if (!simpleValidation[0]) {
+ alert(simpleValidation[1])
+ this.goTo(simpleValidation[2]);
+ return;
+ }
+
+ // todo - make sure each host has at least one connection on any network.
+
+ if (confirm("Are you sure you wish to create this pod?")) {
+ let success = await LibLaaSAPI.makeTemplate(this.templateBlob);
+ if (success) {
+ window.location.href = "../../accounts/my/resources/";
+ } else {
+ alert("Could not create template.")
+ }
+ }
+ }
+}
+
+/** View class that displays cards and generates HTML
+ * Functions as a namespace, does not hold state
+*/
+class GUI {
+ /** Takes a list of LabBlobs and creates a card for each of them on the screen */
+ static display_labs(labs) {
+ const card_deck = document.getElementById('lab_cards');
+ for (let lab of labs) {
+ const new_col = document.createElement('div');
+ new_col.classList.add('col-xl-3','col-md-6','col-11');
+ let status;
+ if (lab.status == 0) {
+ status = "Up";
+ } else if (lab.status == 100) {
+ status = "Down for Maintenance";
+ } else if (lab.status == 200) {
+ status = "Down";
+ } else {
+ status = "Unknown";
+ }
+
+ new_col.innerHTML = `
+ <div class="card" id= ` + lab.name + `>
+ <div class="card-header">
+ <h3 class="mt-2">` + lab.name + `</h3>
+ </div>
+ <ul class="list-group list-group-flush h-100">
+ <li class="list-group-item">Name: ` + lab.name + `</li>
+ <li class="list-group-item">Description: ` + lab.description + `</li>
+ <li class="list-group-item">Location: ` + lab.location + `</li>
+ <li class="list-group-item">Status: `+ status + `</li>
+ </ul>
+ <div class="card-footer">
+ <btn class="btn btn-success w-100 stretched-link" href="#" onclick="workflow.onclickSelectLab(this.parentNode.parentNode)">Select</btn>
+ </div>
+ </div>
+ `
+ card_deck.appendChild(new_col);
+ }
+ }
+
+ static highlightCard(card) {
+ card.classList.add('selected_node');
+ }
+
+ static unhighlightCard(card) {
+ card.classList.remove('selected_node');
+ }
+
+ /** Resets the host modal inner html
+ * Takes a list of templateBlobs
+ */
+ static refreshAddHostModal(template_list) {
+ document.getElementById('add_resource_modal_body').innerHTML = `
+ <h2>Resource</h2>
+ <div id="template-cards" class="row align-items-center justify-content-start">
+ </div>
+
+ <div id="template-config-section">
+ <ul class="nav nav-tabs" role="tablist" id="add_resource_tablist">
+ <!-- add a tab per host in template -->
+ </ul>
+ <!-- tabs -->
+ <div id="resource_config_section" hidden="true">
+ <h2>Image</h2>
+ <div id="image-cards" class="row justify-content-start align-items-center">
+ </div>
+ <div class="form-group">
+ <h2>Hostname</h2>
+ <input type="text" class="form-control" id="hostname-input" placeholder="Enter Hostname">
+ <h2>Cloud Init</h2>
+ <div class="d-flex justify-content-center align-items-center">
+ <textarea name="ci-textarea" id="ci-textarea" rows="5" class="w-100"></textarea>
+ </div>
+ </div>
+ </div>
+ </div>
+ <p id="add-host-error-msg" class="text-danger"></p>
+ `
+
+ const template_cards = document.getElementById('template-cards');
+
+ for (let template of template_list) {
+ template_cards.appendChild(this.makeTemplateCard(template));
+ }
+ }
+
+
+ /** Makes a card to be displayed in the add resource modal for a given templateBlob */
+ static makeTemplateCard(templateBlob) {
+ const col = document.createElement('div');
+ col.classList.add('col-12', 'col-md-6', 'col-xl-3', 'my-3');
+ col.innerHTML= `
+ <div class="card" id="card-" ` + templateBlob.id + `>
+ <div class="card-header">
+ <p class="h5 font-weight-bold mt-2">` + templateBlob.pod_name + `</p>
+ </div>
+ <div class="card-body">
+ <p class="grid-item-description">` + templateBlob.pod_desc +`</p>
+ </div>
+ <div class="card-footer">
+ <button type="button" class="btn btn-success grid-item-select-btn w-100 stretched-link"
+ onclick="workflow.onclickSelectTemplate('` + templateBlob.id + `', this.parentNode.parentNode)">Select</button>
+ </div>
+ </div>
+ `
+ return col;
+ }
+
+ /** Takes a ResourceBuilder and generates form fields */
+ static refreshConfigSection(resourceBuilder, flavors) {
+ // Create a tab for head host in the selected template
+ const tablist = document.getElementById('add_resource_tablist'); // ul
+ tablist.innerHTML = "";
+ for (const [index, host] of resourceBuilder.user_configs.entries()) {
+ const li_interface = document.createElement('li');
+ li_interface.classList.add('nav-item');
+ const btn_interface = document.createElement('a');
+ btn_interface.classList.add('nav-link', 'interface-btn');
+ btn_interface.id = "select-node-" + index;
+ btn_interface.setAttribute("onclick", "workflow.onclickSelectNode("+ index + ")");
+ btn_interface.setAttribute('href', "#");
+ btn_interface.setAttribute('role', 'tab');
+ btn_interface.setAttribute('data-toggle', 'tab');
+ btn_interface.innerText = flavors.get(host.flavor).name;
+
+ if (index == resourceBuilder.tab) {
+ btn_interface.classList.add('active');
+ }
+ li_interface.appendChild(btn_interface);
+ tablist.appendChild(li_interface);
+ }
+ }
+
+ static refreshInputSection(resourceBuilder, flavor_map) {
+ // config stuff
+ const image_cards = document.getElementById('image-cards');
+ const hostname_input = document.getElementById('hostname-input');
+ const ci_textarea = document.getElementById('ci-textarea');
+
+ const tab_flavor_id = resourceBuilder.original_configs[resourceBuilder.tab].flavor;
+ const tab_flavor = flavor_map.get(tab_flavor_id);
+ const image_list = tab_flavor.images;
+ image_cards.innerHTML = "";
+ for (let imageBlob of image_list) {
+ const new_image_card = this.makeImageCard(imageBlob);
+ new_image_card.setAttribute("onclick", "workflow.onclickSelectImage('" + imageBlob.image_id + "', this)");
+ if (resourceBuilder.user_configs[resourceBuilder.tab].image == imageBlob.image_id) {
+ GUI.highlightCard(new_image_card.childNodes[1]);
+ }
+ image_cards.appendChild(new_image_card);
+ }
+
+ // Hostname input
+ hostname_input.value = resourceBuilder.user_configs[resourceBuilder.tab].hostname;
+ hostname_input.addEventListener('focusout', (event)=> {
+ resourceBuilder.user_configs[resourceBuilder.tab].hostname = hostname_input.value;
+ });
+
+ hostname_input.addEventListener('focusin', (event)=> {
+ this.removeHostConfigErrorMessage();
+ });
+
+ // CI input
+ let ci_value = resourceBuilder.user_configs[resourceBuilder.tab].cifile[0];
+ if (!ci_value) {
+ ci_value = "";
+ }
+ ci_textarea.value = ci_value;
+ ci_textarea.addEventListener('focusout', (event)=> {
+ resourceBuilder.user_configs[resourceBuilder.tab].cifile[0] = ci_textarea.value;
+ })
+ this.removeHostConfigErrorMessage();
+ document.getElementById('resource_config_section').removeAttribute('hidden');
+ }
+
+ static showHostConfigErrorMessage(message) {
+ document.getElementById("hostname-input").classList.add("invalid_field");
+ document.getElementById('add-host-error-msg').innerText = message;
+ }
+
+ static removeHostConfigErrorMessage() {
+ document.getElementById("hostname-input").classList.remove("invalid_field");
+ document.getElementById('add-host-error-msg').innerText = "";
+ }
+
+ static makeImageCard(imageBlob) {
+ const col = document.createElement('div');
+ col.classList.add('col-12', 'col-md-6', 'col-xl-3', 'my-3');
+ col.innerHTML = `
+ <div class="btn border w-100">` + imageBlob.name +`</div>
+ `
+
+ return col;
+ }
+
+ static highlightError(element) {
+ element.classList.add('invalid_field');
+ }
+
+ static unhighlightError(element) {
+ element.classList.remove("invalid_field");
+ }
+
+ static showPodDetailsError(message) {
+ document.getElementById('pod_details_error').innerText = message;
+ }
+
+ static hidePodDetailsError() {
+ document.getElementById('pod_details_error').innerText = ""
+ }
+
+ /**
+ * Refreshes the step and creates a card for each host in the hostlist
+ * @param {List<HostConfigBlob>} hostlist
+ */
+ static refreshHostStep(hostlist, flavors, images) {
+ const host_cards = document.getElementById('host_cards');
+ host_cards.innerHTML = "";
+ for (const host of hostlist) {
+ host_cards.appendChild(this.makeHostCard(host, flavors, images));
+ }
+
+ let span_class = ''
+ if (hostlist.length == 8) {
+ span_class = 'text-primary'
+ } else if (hostlist.length > 8) {
+ span_class = 'text-danger'
+ }
+ const plus_card = document.createElement("div");
+ plus_card.classList.add("col-xl-3", "col-md-6", "col-12");
+ plus_card.id = "add_resource_plus_card";
+ plus_card.innerHTML = `
+ <div class="card align-items-center border-0">
+ <span class="` + span_class + `" id="resource-count">` + hostlist.length + `/ 8</span>
+ <button class="btn btn-success add-button p-0" onclick="workflow.onclickAddResource()">+</button>
+ </div>
+ `
+
+ host_cards.appendChild(plus_card);
+ }
+
+ /**
+ * Makes a host card element for a given host and returns a reference to the card
+ * @param {HostConfigBlob} host
+ */
+ static makeHostCard(host, flavors, images) {
+ const new_card = document.createElement("div");
+ new_card.classList.add("col-xl-3", "col-md-6","col-12", "my-3");
+ new_card.innerHTML = `
+ <div class="card">
+ <div class="card-header">
+ <h3 class="mt-2">` + flavors.get(host.flavor).name + `</h3>
+ </div>
+ <ul class="list-group list-group-flush h-100">
+ <li class="list-group-item">Hostname: ` + host.hostname + `</li>
+ <li class="list-group-item">Image: ` + images.get(host.image).name + `</li>
+ </ul>
+ <div class="card-footer border-top-0">
+ <button class="btn btn-danger w-100" id="delete-host-` + host.hostname + `" onclick="workflow.onclickDeleteHost('` + host.hostname +`')">Delete</button>
+ </div>
+ </div>
+ `;
+
+ return new_card;
+ }
+
+
+ /** Shows the input card for adding a new network */
+ // Don't forget to redisable
+ static display_network_input() {
+ // New empty card
+ const network_plus_card = document.getElementById('add_network_plus_card');
+ const new_card = document.createElement('div');
+ new_card.classList.add("col-xl-3", "col-md-6","col-12");
+ new_card.innerHTML =
+ `<div class="card pb-0" id="new_network_card">
+ <div class="card-body pb-0">
+ <div class="justify-content-center my-5 mx-2">
+ <input type="text" class="form-control col-12 mb-2 text-center" id="network_input" style="font-size: 1.75rem;" placeholder="Enter Network Name">
+ <div class="custom-control custom-switch">
+ <input type="checkbox" class="custom-control-input" id="network-public-input">
+ <label class="custom-control-label" for="network-public-input">public?</label>
+ </div>
+ </br>
+ <p class="text-danger mt-n2" id="adding_network_error"></p>
+ </div>
+ <div class="row mb-3">
+ <div class="col-6"><button class="btn btn-danger w-100" onclick="GUI.hide_network_input()">Delete</button></div>
+ <div class="col-6"><button class="btn btn-success w-100" id="adding_network_confirm" onclick="workflow.onclickConfirmNetwork()">Confirm</button></div>
+ </div>
+ </div>
+ </div>`;
+ network_plus_card.parentNode.insertBefore(new_card, network_plus_card);
+
+ document.getElementById('network_input').addEventListener('focusin', e => {
+ document.getElementById('adding_network_error').innerText = '';
+ })
+ }
+
+ static hide_network_input() {
+ document.getElementById('new_network_card').parentElement.remove();
+ document.getElementById('add_network_plus_card').hidden = false;
+ }
+
+ /** Redraws all the cards on the network step.
+ * Takes a list of networks to display
+ */
+ static refreshNetworkStep(network_list) {
+
+ document.getElementById('network_card_deck').innerHTML = `
+ <div class="col-xl-3 col-md-6 col-12" id="add_network_plus_card">
+ <div class="card align-items-center border-0">
+ <button class="btn btn-success add-button p-0" onclick="workflow.onclickAddNetwork()">+</button>
+ </div>
+ </div>
+ `
+
+ const network_plus_card = document.getElementById('add_network_plus_card');
+ for (let network of network_list) { // NetworkBlobs
+
+ let pub_str = ' (private)';
+ if (network.public) {
+ pub_str = ' (public)'
+ }
+ const new_card = document.createElement('div');
+ new_card.classList.add("col-xl-3", "col-md-6","col-12", "my-3");
+ new_card.innerHTML = `
+ <div class="card">
+ <div class="text-center">
+ <h3 class="py-5 my-4">` + network.name + pub_str +`</h3>
+ </div>
+ <div class="row mb-3 mx-3">
+ <button class="btn btn-danger w-100" id="delete_network_` + network.name + `" onclick="workflow.onclickDeleteNetwork('`+ network.name +`')">Delete</button>
+ </div>
+ </div>`;
+ network_plus_card.parentNode.insertBefore(new_card, network_plus_card);
+ }
+ }
+
+ /** Displays an error message on the add network card */
+ static display_add_network_error(error_message) {
+ document.getElementById("adding_network_error").innerHTML = error_message;
+ }
+
+ static refreshConnectionStep(host_list) {
+ const connection_cards = document.getElementById('connection_cards');
+ connection_cards.innerHTML = "";
+
+ for (const host of host_list) {
+ connection_cards.appendChild(this.makeConnectionCard(host));
+ }
+
+ }
+
+
+ /** Makes a blank connection card that does not contain interface details */
+ static makeConnectionCard(host) {
+ const new_card = document.createElement('div');
+ new_card.classList.add("col-xl-3", "col-md-6","col-11", "my-3");
+
+ const card_div = document.createElement('div');
+ card_div.classList.add('card');
+ new_card.appendChild(card_div);
+
+ const card_header = document.createElement('div');
+ card_header.classList.add('card-header', 'text-center', 'p-0');
+ card_header.innerHTML = `<h3 class="mt-2">` + host.hostname + `</h3>`
+
+ const card_body = document.createElement('div');
+ card_body.classList.add('card-body', 'card-body-scroll', 'p-0');
+ const bondgroup_list = document.createElement('ul');
+ bondgroup_list.classList.add('list-group', 'list-group-flush', 'h-100')
+ card_body.appendChild(bondgroup_list)
+
+ const card_footer = document.createElement('div');
+ card_footer.classList.add('card-footer');
+ card_footer.innerHTML = `<button class="btn btn-info w-100" onclick="workflow.onclickConfigureConnection('` + host.hostname + `')">Configure</button>`
+
+ card_div.appendChild(card_header);
+ card_div.appendChild(card_body);
+ card_div.appendChild(card_footer);
+
+ for (const bg of host.bondgroups) {
+ const outer_block = document.createElement('li');
+ outer_block.classList.add('list-group-item');
+ outer_block.innerHTML = `
+ <h5>` + bg.ifaces[0].name + `</h5>`
+
+ const inner_block = document.createElement('ul');
+ inner_block.classList.add('connection-holder');
+ outer_block.appendChild(inner_block)
+ for (const c of bg.connections) {
+ const connection_li = document.createElement('li');
+ connection_li.innerText = c.connects_to + `: ` + c.tagged;
+ inner_block.appendChild(connection_li);
+ }
+ bondgroup_list.appendChild(outer_block)
+ }
+
+ return new_card;
+ }
+
+ /** */
+ static refreshConnectionModal(connectionTemp) {
+ this.refreshConnectionTabs(connectionTemp.iface_list, connectionTemp.selected_index);
+ this.refreshConnectionTable(connectionTemp);
+ }
+
+ /** Displays a tab in the connections modal for each interface
+ * Returns the name of the currently selected interface for use in the connections table
+ */
+ static refreshConnectionTabs (iface_list, iface_index) {
+ const tablist_ul = document.getElementById('configure-connections-tablist');
+ tablist_ul.innerHTML = '';
+ for (const [index, iface_name] of iface_list.entries()) {
+ const li_interface = document.createElement('li');
+ li_interface.classList.add('nav-item');
+ const btn_interface = document.createElement('a');
+ btn_interface.classList.add('nav-link', 'interface-btn');
+ btn_interface.setAttribute("onclick", "workflow.onclickSelectIfaceTab(" + index +")");
+ btn_interface.setAttribute('href', "#");
+ btn_interface.setAttribute('role', 'tab');
+ btn_interface.setAttribute('data-toggle', 'tab');
+ btn_interface.innerText = iface_name.name;
+ li_interface.appendChild(btn_interface);
+ tablist_ul.appendChild(li_interface);
+ if (index == iface_index) {
+ btn_interface.classList.add('active');
+ }
+ }
+ }
+
+ static refreshConnectionTable(connectionTemp) {
+ const connections_table = document.getElementById('connections_widget');
+ connections_table.innerHTML =`
+ <tr>
+ <th>Network</th>
+ <th colspan='2'>Vlan</th>
+ </tr>
+ `;
+
+ const selected_iface_name = connectionTemp.iface_list[connectionTemp.selected_index].name;
+ const iface_config = connectionTemp.config.get(selected_iface_name);
+ for (const network of connectionTemp.networks) {
+ const tagged = iface_config.get(network.name);
+ const new_row = document.createElement('tr');
+ const td_network = document.createElement('td');
+ td_network.innerText = network.name;
+ new_row.appendChild(td_network);
+ new_row.appendChild(this.makeTagTd(true, network.name, tagged === true, selected_iface_name));
+ new_row.appendChild(this.makeTagTd(false, network.name, tagged === false, selected_iface_name));
+ connections_table.appendChild(new_row);
+ }
+
+ // If an untagged is selected, disable all buttons that are not the selected button
+ if (document.querySelector(".vlan-radio.untagged.btn-success")) {
+ const other_buttons = document.querySelectorAll(".vlan-radio.untagged:not(.btn-success");
+ for (const btn of other_buttons) {
+ btn.setAttribute("disabled", "true")
+ }
+ }
+ }
+
+ static makeTagTd(tagged, network_name, isSelected, selected_iface_name) {
+ let tagged_as_str = "untagged"
+ if (tagged) {
+ tagged_as_str = "tagged"
+ }
+
+ const td = document.createElement('td');
+ const btn = document.createElement('button');
+ btn.classList.add("btn", "w-100", "h-100", "vlan-radio", "border", tagged_as_str);
+ btn.setAttribute("onclick" ,"workflow.onclickSelectVlan('"+ network_name + "'," + tagged + ", '" + selected_iface_name +"')");
+ if (isSelected) {
+ btn.classList.add('btn-success');
+ }
+ btn.innerText = tagged_as_str;
+ td.appendChild(btn);
+ return td;
+ }
+
+ static refreshPodSummaryDetails(pod_name, pod_desc, isPublic) {
+ const list = document.getElementById('pod_summary_pod_details');
+ list.innerHTML = '';
+ const name_li = document.createElement('li');
+ name_li.innerText = 'Pod name: ' + pod_name;
+ list.appendChild(name_li);
+
+ const desc_li = document.createElement('li')
+ desc_li.innerText = 'Description: ' + pod_desc;
+ list.appendChild(desc_li);
+
+ const public_li = document.createElement('li');
+ public_li.innerText = 'Public: ' + isPublic;
+ list.appendChild(public_li);
+ }
+
+ static refreshPodSummaryHosts(host_list, flavors, images) {
+ const list = document.getElementById('pod_summary_hosts');
+ list.innerHTML = '';
+
+ for (const host of host_list) {
+ const new_li = document.createElement('li');
+ // new_li.innerText = hosts[i].hostname + ': ' + this.lab_flavor_from_uuid(hosts[i].flavor).name + ' (' + hosts[i].image + ')';
+ const details = `${host.hostname}: ${flavors.get(host.flavor).name}, ${images.get(host.image).name}`
+ new_li.innerText = details;
+ list.appendChild(new_li);
+ }
+ }
+
+ static update_pod_summary() {
+ // Takes a section (string) and updates the appropriate element's innertext
+
+ if (section == 'pod_details') {
+ const list = document.getElementById('pod_summary_pod_details');
+ list.innerHTML = '';
+ const name_li = document.createElement('li');
+ name_li.innerText = 'Pod name: ' + this.pod.pod_name;
+ list.appendChild(name_li);
+
+ const desc_li = document.createElement('li')
+ desc_li.innerText = 'Description: ' + this.pod.pod_desc;
+ list.appendChild(desc_li);
+
+ const public_li = document.createElement('li');
+ public_li.innerText = 'Public: ' + this.pod.is_public;
+ list.appendChild(public_li);
+ } else if (section == 'hosts') {
+ const list = document.getElementById('pod_summary_hosts');
+ list.innerHTML = '';
+ const hosts = this.pod.host_list;
+ for (let i = 0; i < this.pod.host_list.length; i++) {
+ const new_li = document.createElement('li');
+ new_li.innerText = hosts[i].hostname + ': ' + this.lab_flavor_from_uuid(hosts[i].flavor).name + ' (' + hosts[i].image + ')';
+ list.appendChild(new_li);
+ }
+ } else {
+ console.log(section + ' is not a valid section.');
+ }
+ }
+}
+
+/** Holds in-memory configurations for the add resource step */
+class ResourceBuilder {
+ constructor(templateBlob) {
+ this.template_id = templateBlob.id; // UUID (String)
+ this.networks = templateBlob.networks;
+ this.original_configs = templateBlob.host_list; // List<HostConfigBlob>
+ this.user_configs = []; // List<HostConfigBlob>
+ this.tab = 0; // Currently selected tab index where configs will be saved to
+
+ // Create deep copies of the hosts
+ for (let host of this.original_configs) {
+ const copied_host = new HostConfigBlob(host);
+ this.user_configs.push(copied_host);
+ }
+ }
+}
+
+class ConnectionTemp {
+ // keep track of user inputs, commits to host bondgroups after user clicks submit
+ constructor(host, networks, iface_list) {
+ this.host = host; // reference
+ this.config = new Map(); // Map<iface_name, Map<network_name, tagged>}>
+ this.iface_list = iface_list; // List<IfaceBlob>
+ for (const i of iface_list) {
+ this.config.set(i.name, new Map())
+ }
+
+ // set initial mappings
+ for (const ebg of host.bondgroups) {
+ // if (ebg.ifaces[0].name)
+ const iface_config = this.config.get(ebg.ifaces[0].name);
+ for (const c of ebg.connections) {
+ iface_config.set(c.connects_to, c.tagged)
+ }
+ }
+ this.networks = networks; // List<NetworkBlob>
+ this.selected_index = 0;
+ }
+
+
+ /** Replaces the old configs in the hostconfigblob with the ones set in this.config */
+ applyConfigs() {
+ this.host.bondgroups = [];
+ for (const [key, value] of this.config) {
+ if (value.size > 0) {
+ const full_iface = workflow.labFlavors.get(this.host.flavor).interfaces.filter((iface) => iface.name == key)[0];
+ const new_bg = new BondgroupBlob({});
+ this.host.bondgroups.push(new_bg)
+ new_bg.ifaces.push(full_iface);
+ for (const [network, tagged] of value) {
+ if (tagged != null) {
+ new_bg.connections.push(new ConnectionBlob({"tagged": tagged, "connects_to": network}))
+ }
+ }
+ }
+ }
+ }
+}
+
+function todo() {
+ alert('todo');
+} \ No newline at end of file
diff --git a/src/static/js/workflows/workflow.js b/src/static/js/workflows/workflow.js
new file mode 100644
index 0000000..745a706
--- /dev/null
+++ b/src/static/js/workflows/workflow.js
@@ -0,0 +1,246 @@
+/*
+Defines a common interface for creating workflows
+Functions as the "view" part of MVC, or the "controller" part. Not really sure tbh
+*/
+
+
+const HTTP = {
+ GET: "GET",
+ POST: "POST",
+ DELETE: "DELETE",
+ PUT: "PUT"
+}
+
+const endpoint = {
+ LABS: "todo", // Not implemented
+ FLAVORS: "flavor/",
+ IMAGES: "images/",
+ TEMPLATES: "template/list/[username]",
+ SAVE_DESIGN_WORKFLOW: "todo", // Post MVP
+ SAVE_BOOKING_WORKFLOW: "todo", // Post MVP
+ MAKE_TEMPLATE: "template/create",
+ DELETE_TEMPLATE: "template",
+ MAKE_BOOKING: "booking/create",
+}
+
+/** Functions as a namespace for static methods that post to the dashboard, then send an HttpRequest to LibLaas, then receive the response */
+class LibLaaSAPI {
+
+ /** POSTs to dashboard, which then auths and logs the requests, makes the request to LibLaaS, and passes the result back to here.
+ Treat this as a private function. Only use the async functions when outside of this class */
+ static makeRequest(method, endpoint, workflow_data) {
+ console.log("Making request: %s, %s, %s", method, endpoint, workflow_data.toString())
+ const token = document.getElementsByName('csrfmiddlewaretoken')[0].value
+ return new Promise((resolve, reject) => {// -> HttpResponse
+ $.ajax(
+ {
+ crossDomain: true, // might need to change this back to true
+ method: "POST",
+ contentType: "application/json; charset=utf-8",
+ dataType : 'json',
+ headers: {
+ 'X-CSRFToken': token
+ },
+ data: JSON.stringify(
+ {
+ "method": method,
+ "endpoint": endpoint,
+ "workflow_data": workflow_data
+ }
+ ),
+ timeout: 10000,
+ success: (response) => {
+ resolve(response);
+ },
+ error: (response) => {
+ reject(response);
+ }
+ }
+ )
+ })
+ }
+
+ static async getLabs() { // -> List<LabBlob>
+ // return this.makeRequest(HTTP.GET, endpoint.LABS, {});
+ let jsonObject = JSON.parse('{"name": "UNH_IOL","description": "University of New Hampshire InterOperability Lab","location": "NH","status": 0}');
+ return [new LabBlob(jsonObject)];
+ }
+
+ static async getLabFlavors(lab_name) { // -> List<FlavorBlob>
+ const data = await this.handleResponse(this.makeRequest(HTTP.GET, endpoint.FLAVORS, {"lab_name": lab_name}));
+ let flavors = [];
+ if (data) {
+ for (const d of data) {
+ flavors.push(new FlavorBlob(d))
+ }
+ } else {
+ apiError("flavors")
+ }
+ return flavors;
+ // let jsonObject = JSON.parse('{"flavor_id": "aaa-bbb-ccc", "name": "HPE Gen 9", "description": "placeholder", "interfaces": ["ens1", "ens2", "ens3"]}')
+ // return [new FlavorBlob(jsonObject)];
+ }
+
+ static async getImagesForFlavor(flavor_id) {
+ let full_endpoint = endpoint.FLAVORS + flavor_id + '/[username]/' + endpoint.IMAGES;
+ const data = await this.handleResponse(this.makeRequest(HTTP.GET, full_endpoint, {}));
+ let images = []
+
+ if (data) {
+ for (const d of data) {
+ images.push(new ImageBlob(d));
+ }
+ } else {
+ apiError("images")
+ }
+
+ return images;
+ // let jsonObject = JSON.parse('{"image_id": "111-222-333", "name": "Arch Linux"}')
+ // let jsonObject2 = JSON.parse('{"image_id": "444-555-666", "name": "Oracle Linux"}')
+ // return [new ImageBlob(jsonObject), new ImageBlob(jsonObject2)];
+ }
+
+ /** Doesn't need to be passed a username because django will pull this from the request */
+ static async getTemplatesForUser() { // -> List<TemplateBlob>
+ const data = await this.handleResponse(this.makeRequest(HTTP.GET, endpoint.TEMPLATES, {}))
+ let templates = []
+
+ if (data)
+ for (const d of data) {
+ templates.push(new TemplateBlob(d))
+ } else {
+ apiError("templates")
+ }
+ return templates;
+ // let jsonObject = JSON.parse('{"id": "12345", "owner":"jchoquette", "lab_name":"UNH_IOL","pod_name":"test pod","pod_desc":"for e2e testing","public":false,"host_list":[{"hostname":"test-node","flavor":"1ca6169c-a857-43c6-80b7-09b608c0daec","image":"3fc3833e-7b8b-4748-ab44-eacec8d14f8b","cifile":[],"bondgroups":[{"connections":[{"tagged":true,"connects_to":"public"}],"ifaces":[{"name":"eno49","speed":{"value":10000,"unit":"BitsPerSecond"},"cardtype":"Unknown"}]}]}],"networks":[{"name":"public","public":true}]}')
+ // let jsonObject2 = JSON.parse('{"id":6789,"owner":"jchoquette","lab_name":"UNH_IOL","pod_name":"Other Host","pod_desc":"Default Template","public":false,"host_list":[{"cifile":["some ci data goes here"],"hostname":"node","flavor":"aaa-bbb-ccc","image":"111-222-333", "bondgroups":[{"connections": [{"tagged": false, "connects_to": "private"}], "ifaces": [{"name": "ens2"}]}]}],"networks":[{"name": "private", "public": false}]}');
+
+ return [new TemplateBlob(jsonObject)];
+ }
+
+ static async saveDesignWorkflow(templateBlob) { // -> bool
+ templateBlob.owner = user;
+ return await this.handleResponse(this.makeRequest(HTTP.PUT, endpoint.SAVE_DESIGN_WORKFLOW))
+ }
+
+ static async saveBookingWorkflow(bookingBlob) { // -> bool
+ bookingBlob.owner = user;
+ return await this.handleResponse(this.makeRequest(HTTP.PUT, endpoint.SAVE_BOOKING_WORKFLOW, bookingBlob));
+ }
+
+ static async makeTemplate(templateBlob) { // -> UUID or null
+ templateBlob.owner = user;
+ console.log(JSON.stringify(templateBlob))
+ return await this.handleResponse(this.makeRequest(HTTP.POST, endpoint.MAKE_TEMPLATE, templateBlob));
+ }
+
+ static async deleteTemplate(template_id) { // -> UUID or null
+ return await this.handleResponse(this.makeRequest(HTTP.DELETE, endpoint.DELETE_TEMPLATE + "/" + template_id, {}));
+ }
+
+ /** PUT to the dashboard with the bookingBlob. Dashboard will fill in lab and owner, make the django model, then hit liblaas, then come back and fill in the agg_id */
+ static async makeBooking(bookingBlob) {
+ return await this.handleResponse(this.createDashboardBooking(bookingBlob));
+ }
+
+ /** Wraps a call in a try / catch, processes the result, and returns the response or null if it failed */
+ static async handleResponse(promise) {
+ try {
+ let x = await promise;
+ return x;
+ } catch(e) {
+ console.log(e)
+ return null;
+ }
+ }
+
+ /** Uses PUT instead of POST to tell the dashboard that we want to create a dashboard booking instead of a liblaas request */
+ static createDashboardBooking(bookingBlob) {
+ const token = document.getElementsByName('csrfmiddlewaretoken')[0].value
+ return new Promise((resolve, reject) => { // -> HttpResponse
+ $.ajax(
+ {
+ crossDomain: false,
+ method: "PUT",
+ contentType: "application/json; charset=utf-8",
+ dataType : 'json',
+ headers: {
+ 'X-CSRFToken': token
+ },
+ data: JSON.stringify(
+ bookingBlob),
+ timeout: 10000,
+ success: (response) => {
+ resolve(response);
+ },
+ error: (response) => {
+ reject(response);
+ }
+ }
+ )
+ })
+ }
+}
+
+
+/** Controller class that handles button inputs to navigate through the workflow and generate HTML dynamically
+ * Treat this as an abstract class and extend it in the appropriate workflow module.
+*/
+class Workflow {
+ constructor(sections_list) {
+ this.sections = []; // List of strings
+ this.step = 0; // Current step of the workflow
+ this.sections = sections_list;
+ }
+
+ /** Advances the workflow by one step and scrolls to that section
+ * Disables the previous button if the step becomes 0 after executing
+ * Enables the next button if the step is less than sections.length after executing
+ */
+ goPrev() {
+
+ if (workflow.step <= 0) {
+ return;
+ }
+
+ this.step--;
+
+ document.getElementById(this.sections[this.step]).scrollIntoView({behavior: 'smooth'});
+
+ if (this.step == 0) {
+ document.getElementById('prev').setAttribute('disabled', '');
+ } else if (this.step == this.sections.length - 2) {
+ document.getElementById('next').removeAttribute('disabled');
+ }
+ }
+
+ goNext() {
+ if (this.step >= this.sections.length - 1 ) {
+ return;
+ }
+
+ this.step++;
+ document.getElementById(this.sections[this.step]).scrollIntoView({behavior: 'smooth'});
+
+ if (this.step == this.sections.length - 1) {
+ document.getElementById('next').setAttribute('disabled', '');
+ } else if (this.step == 1) {
+ document.getElementById('prev').removeAttribute('disabled');
+ }
+ }
+
+ goTo(step_number) {
+ while (step_number > this.step) {
+ this.goNext();
+ }
+
+ while (step_number < this.step) {
+ this.goPrev();
+ }
+ }
+
+}
+
+function apiError(info) {
+ alert("Unable to fetch " + info +". Please try again later or contact support.")
+ }