Laas Dashboard Front End Improvements 27/73427/20
authorJustin Choquette <jchoquette@iol.unh.edu>
Tue, 7 Jun 2022 20:07:54 +0000 (16:07 -0400)
committerJustin Choquette <jchoquette@iol.unh.edu>
Thu, 29 Sep 2022 17:34:30 +0000 (13:34 -0400)
Change-Id: Ib9aa21747bd57faef94db7795cd89119ad4b0a9d
Signed-off-by: Justin Choquette <jchoquette@iol.unh.edu>
13 files changed:
src/api/tests/test_models_unittest.py
src/api/views.py
src/booking/forms.py
src/resource_inventory/resource_manager.py
src/static/js/dashboard.js
src/static/package-lock.json
src/templates/base/resource/steps/pod_definition.html
src/templates/base/workflow/confirm.html
src/templates/base/workflow/viewport-base.html
src/workflow/forms.py
src/workflow/models.py
src/workflow/resource_bundle_workflow.py
src/workflow/workflow_manager.py

index 2a6fa0b..2dee29b 100644 (file)
@@ -116,7 +116,7 @@ class ValidBookingCreatesValidJob(TestCase):
         count = hostprofile.interfaceprofile.all().count()
         for i in range(count):
             network_struct.append([])
-        while(nets):
+        while (nets):
             index = len(nets) % count
             network_struct[index].append(nets.pop())
 
index 1516374..ffa9b3f 100644 (file)
@@ -430,7 +430,11 @@ def auth_and_log(request, endpoint):
         token = Token.objects.get(key=user_token)
     except Token.DoesNotExist:
         token = None
-        response = HttpResponse('Unauthorized', status=401)
+        # Added logic to detect malformed token
+        if len(str(user_token)) != 40:
+            response = HttpResponse('Malformed Token', status=401)
+        else:
+            response = HttpResponse('Unauthorized', status=401)
 
     x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
     if x_forwarded_for:
index ff829b2..9c9b053 100644 (file)
@@ -19,6 +19,7 @@ from booking.lib import get_user_items, get_user_field_opts
 
 
 class QuickBookingForm(forms.Form):
+    # Django Form class for Express Booking
     purpose = forms.CharField(max_length=1000)
     project = forms.CharField(max_length=400)
     hostname = forms.CharField(required=False, max_length=400)
index 52af824..16c106e 100644 (file)
@@ -176,7 +176,7 @@ class ResourceManager:
         vlan_manager = template.lab.vlan_manager
         for net_name, vlan_id in vlans.items():
             net = Network.objects.get(name=net_name, bundle=template)
-            if(net.is_public):
+            if (net.is_public):
                 vlan_manager.release_public_vlan(vlan_id)
             else:
                 vlan_manager.release_vlans(vlan_id)
index e3978e3..a63c71b 100644 (file)
@@ -41,7 +41,7 @@ function update_side_buttons(meta) {
     const step = meta.active;
     const page_count = meta.steps.length;
 
-    const back_button = document.getElementById("gob");
+    const back_button = document.getElementById("workflow-nav-back");
     if (step == 0) {
         back_button.classList.add("disabled");
         back_button.disabled = true;
@@ -50,7 +50,7 @@ function update_side_buttons(meta) {
         back_button.disabled = false;
     }
 
-    const forward_btn = document.getElementById("gof");
+    const forward_btn = document.getElementById("workflow-nav-next");
     if (step == page_count - 1) {
         forward_btn.classList.add("disabled");
         forward_btn.disabled = true;
@@ -120,9 +120,18 @@ function update_description(title, 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"){
@@ -795,6 +804,7 @@ class NetworkStep {
         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"));
index f8eabe4..89a26db 100644 (file)
@@ -1,8 +1,97 @@
 {
   "name": "laas",
   "version": "1.0.0",
-  "lockfileVersion": 1,
+  "lockfileVersion": 2,
   "requires": true,
+  "packages": {
+    "": {
+      "name": "laas",
+      "version": "1.0.0",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@fortawesome/fontawesome-free": "^5.12.0",
+        "bootstrap": "^4.4.1",
+        "datatables.net-bs4": "^1.10.20",
+        "datatables.net-responsive-bs4": "^2.2.3",
+        "jquery": "^3.4.1",
+        "mxgraph": "^4.0.6",
+        "plotly.js-dist": "^1.51.3",
+        "popper.js": "^1.16.0"
+      }
+    },
+    "node_modules/@fortawesome/fontawesome-free": {
+      "version": "5.12.0",
+      "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-5.12.0.tgz",
+      "integrity": "sha512-vKDJUuE2GAdBERaQWmmtsciAMzjwNrROXA5KTGSZvayAsmuTGjam5z6QNqNPCwDfVljLWuov1nEC3mEQf/n6fQ==",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/bootstrap": {
+      "version": "4.4.1",
+      "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.4.1.tgz",
+      "integrity": "sha512-tbx5cHubwE6e2ZG7nqM3g/FZ5PQEDMWmMGNrCUBVRPHXTJaH7CBDdsLeu3eCh3B1tzAxTnAbtmrzvWEvT2NNEA==",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/datatables.net": {
+      "version": "1.10.20",
+      "resolved": "https://registry.npmjs.org/datatables.net/-/datatables.net-1.10.20.tgz",
+      "integrity": "sha512-4E4S7tTU607N3h0fZPkGmAtr9mwy462u+VJ6gxYZ8MxcRIjZqHy3Dv1GNry7i3zQCktTdWbULVKBbkAJkuHEnQ==",
+      "dependencies": {
+        "jquery": "3.4.1"
+      }
+    },
+    "node_modules/datatables.net-bs4": {
+      "version": "1.10.20",
+      "resolved": "https://registry.npmjs.org/datatables.net-bs4/-/datatables.net-bs4-1.10.20.tgz",
+      "integrity": "sha512-kQmMUMsHMOlAW96ztdoFqjSbLnlGZQ63iIM82kHbmldsfYdzuyhbb4hTx6YNBi481WCO3iPSvI6YodNec46ZAw==",
+      "dependencies": {
+        "datatables.net": "1.10.20",
+        "jquery": "3.4.1"
+      }
+    },
+    "node_modules/datatables.net-responsive": {
+      "version": "2.2.3",
+      "resolved": "https://registry.npmjs.org/datatables.net-responsive/-/datatables.net-responsive-2.2.3.tgz",
+      "integrity": "sha512-8D6VtZcyuH3FG0Hn5A4LPZQEOX3+HrRFM7HjpmsQc/nQDBbdeBLkJX4Sh/o1nzFTSneuT1Wh/lYZHVPpjcN+Sw==",
+      "dependencies": {
+        "datatables.net": "1.10.20",
+        "jquery": "3.4.1"
+      }
+    },
+    "node_modules/datatables.net-responsive-bs4": {
+      "version": "2.2.3",
+      "resolved": "https://registry.npmjs.org/datatables.net-responsive-bs4/-/datatables.net-responsive-bs4-2.2.3.tgz",
+      "integrity": "sha512-SQaWI0uLuPcaiBBin9zX+MuQfTSIkK1bYxbXqUV6NLkHCVa6PMQK7Rvftj0ywG4R7uOtjbzY8nSVqxEKvQI0Vg==",
+      "dependencies": {
+        "datatables.net-bs4": "1.10.20",
+        "datatables.net-responsive": "2.2.3",
+        "jquery": "3.4.1"
+      }
+    },
+    "node_modules/jquery": {
+      "version": "3.4.1",
+      "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.4.1.tgz",
+      "integrity": "sha512-36+AdBzCL+y6qjw5Tx7HgzeGCzC81MDDgaUP8ld2zhx58HdqXGoBd+tHdrBMiyjGQs0Hxs/MLZTu/eHNJJuWPw=="
+    },
+    "node_modules/mxgraph": {
+      "version": "4.0.6",
+      "resolved": "https://registry.npmjs.org/mxgraph/-/mxgraph-4.0.6.tgz",
+      "integrity": "sha512-5XZXeAkA4k6n4BS05Fxd2cNhMw+3dnlRqAaLtsuXdT0g8BvvEa1VT4jjuGtUW4QTt38Q+I2Dr/3EWiAaGRfAXw=="
+    },
+    "node_modules/plotly.js-dist": {
+      "version": "1.51.3",
+      "resolved": "https://registry.npmjs.org/plotly.js-dist/-/plotly.js-dist-1.51.3.tgz",
+      "integrity": "sha512-Bxz0XBg963gpnbt7FVPEhYvT33JsaKa0hEozXBnQZkiKtsiM2M1lZN6tkEHmq6o1N2K6qJXFtdzCXbZ/hLGV0Q=="
+    },
+    "node_modules/popper.js": {
+      "version": "1.16.0",
+      "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.0.tgz",
+      "integrity": "sha512-+G+EkOPoE5S/zChTpmBSSDYmhXJ5PsW8eMhH8cP/CQHMFPBG/kC9Y5IIw6qNYgdJ+/COf0ddY2li28iHaZRSjw=="
+    }
+  },
   "dependencies": {
     "@fortawesome/fontawesome-free": {
       "version": "5.12.0",
index 83c4fcb..233d995 100644 (file)
 </form>
 <script>
     //gather context data
-    let debug = false;
-    {% if debug %}
-    debug = true;
-    {% endif %}
+    try {
+        let debug = false;
+        {% if debug %}
+        debug = true;
+        {% endif %}
 
-    const False = false;
-    const True = true;
+        const False = false;
+        const True = true;
 
-    let resources = {{resources|safe}};
-    let networks = {{networks|safe}};
+        let resources = {{resources|safe}};
+        let networks = {{networks|safe}};
+
+        network_step = new NetworkStep(
+            debug,
+            resources,
+            networks,
+            document.getElementById('graphContainer'),
+            document.getElementById('outlineContainer'),
+            document.getElementById('toolbarContainer'),
+            document.getElementById('sidebarContainer')
+        );
+        form_submission_callbacks.push(() => network_step.prepareForm());
+    } catch (e) {
+        console.log(e)
+    }
 
-    network_step = new NetworkStep(
-        debug,
-        resources,
-        networks,
-        document.getElementById('graphContainer'),
-        document.getElementById('outlineContainer'),
-        document.getElementById('toolbarContainer'),
-        document.getElementById('sidebarContainer')
-    );
-    form_submission_callbacks.push(() => network_step.prepareForm());
 </script>
 {% endblock content %}
 {% block onleave %}
index 2f99a41..bc8e4e3 100644 (file)
     {
         select.value = "True";
         submitStepForm();
+        pop_workflow();
     }
     function formcancel()
-    {
-        select.value = "False";
-        submitStepForm();
-    }
-
-    var confirmed = {{confirm_succeeded|default:"false"}};
-    if( confirmed )
     {
         pop_workflow();
     }
index d9648c2..88229ca 100644 (file)
@@ -10,7 +10,7 @@
     <div class="col">
         <nav>
             <ul class="pagination d-flex flex-row" id="topPagination">
-                <li class="page-item flex-shrink-1 page-control">
+                <li class="page-item flex-shrink-1 page-control" id="workflow-nav-back">
                     <a class="page-link" href="#" id="gob" onclick="submit_and_go('prev')">
                         <i class="fas fa-backward"></i> Back
                     </a>
@@ -20,7 +20,7 @@
                         <i class="far"></i>
                     </a>
                 </li>
-                <li class="page-item flex-shrink-1 page-control">
+                <li class="page-item flex-shrink-1 page-control" id="workflow-nav-next">
                     <a class="page-link text-right" href="#" id="gof" onclick="submit_and_go('next')">
                         Next <i class="fas fa-forward"></i>
                     </a>
index 9b56f93..62abad6 100644 (file)
@@ -222,7 +222,7 @@ class ResourceSelectorForm(SearchableSelectAbstractForm):
 
 
 class BookingMetaForm(forms.Form):
-
+    # Django Form class for Book a Pod
     length = forms.IntegerField(
         widget=NumberInput(
             attrs={
@@ -380,7 +380,7 @@ class PodDefinitionForm(forms.Form):
 class ResourceMetaForm(forms.Form):
 
     bundle_name = forms.CharField(label="POD Name")
-    bundle_description = forms.CharField(label="POD Description", widget=forms.Textarea)
+    bundle_description = forms.CharField(label="POD Description", widget=forms.Textarea, max_length=1000)
 
 
 class GenericHostMetaForm(forms.Form):
@@ -400,8 +400,12 @@ class NetworkConfigurationForm(forms.Form):
 
 
 class HostSoftwareDefinitionForm(forms.Form):
-
-    host_name = forms.CharField(max_length=200, disabled=False, required=True)
+    # Django Form class for Design a Pod
+    host_name = forms.CharField(
+        max_length=200,
+        disabled=False,
+        required=True
+    )
     headnode = forms.BooleanField(required=False, widget=forms.HiddenInput)
 
     def __init__(self, *args, **kwargs):
@@ -441,8 +445,8 @@ class ConfirmationForm(forms.Form):
 
     confirm = forms.ChoiceField(
         choices=(
-            (True, "Confirm"),
-            (False, "Cancel")
+            (False, "Cancel"),
+            (True, "Confirm")
         )
     )
 
index 91a216c..e065202 100644 (file)
@@ -326,11 +326,15 @@ class Confirmation_Step(WorkflowStep):
     def get_context(self):
         context = super(Confirmation_Step, self).get_context()
         context['form'] = ConfirmationForm()
-        context['confirmation_info'] = yaml.dump(
-            self.repo_get(self.repo.CONFIRMATION),
-            default_flow_style=False
-        ).strip()
-
+        # Summary of submitted form data shown on the 'confirm' step of the workflow
+        confirm_details = "\nPod:\n  Name: '{name}'\n  Description: '{desc}'\nLab: '{lab}'".format(
+            name=self.repo_get(self.repo.CONFIRMATION)['resource']['name'],
+            desc=self.repo_get(self.repo.CONFIRMATION)['resource']['description'],
+            lab=self.repo_get(self.repo.CONFIRMATION)['template']['lab'])
+        confirm_details += "\nResources:"
+        for i, device in enumerate(self.repo_get(self.repo.RESOURCE_TEMPLATE_MODELS)['resources']):
+            confirm_details += "\n  " + str(device) + ": " + str(self.repo_get(self.repo.CONFIRMATION)['template']['resources'][i]['profile'])
+        context['confirmation_info'] = confirm_details
         if self.valid == WorkflowStepStatus.VALID:
             context["confirm_succeeded"] = "true"
 
index a461e9a..4e288b5 100644 (file)
@@ -14,6 +14,7 @@ from django.core.exceptions import ValidationError
 
 from typing import List
 
+import re
 import json
 from xml.dom import minidom
 import traceback
@@ -172,7 +173,8 @@ class Define_Hardware(WorkflowStep):
         except Exception as e:
             print("Caught exception: " + str(e))
             traceback.print_exc()
-            self.set_invalid(str(e))
+            self.form = None
+            self.set_invalid("Please select a lab.")
 
 
 class Define_Software(WorkflowStep):
@@ -208,12 +210,15 @@ class Define_Software(WorkflowStep):
         hosts_initial = []
         configs = self.repo_get(self.repo.RESOURCE_TEMPLATE_MODELS, {}).get("resources")
         if configs:
-            for config in configs:
+            for i in range(len(configs)):
+                default_name = 'laas-node'
+                if i > 0:
+                    default_name = default_name + "-" + str(i + 1)
                 hosts_initial.append({
-                    'host_id': config.id,
-                    'host_name': config.name,
-                    'headnode': config.is_head_node,
-                    'image': config.image
+                    'host_id': configs[i].id,
+                    'host_name': default_name,
+                    'headnode': False,
+                    'image': configs[i].image
                 })
         else:
             for host in hostlist:
@@ -248,9 +253,6 @@ class Define_Software(WorkflowStep):
 
     def post(self, post_data, user):
         hosts = self.get_host_list()
-
-        # TODO: fix headnode in form, currently doesn't return a selected one
-        # models['headnode_index'] = post_data.get("headnode", 1)
         formset = self.create_hostformset(hosts, data=post_data)
         has_headnode = False
         if formset.is_valid():
@@ -264,6 +266,17 @@ class Define_Software(WorkflowStep):
                 host.is_head_node = headnode
                 host.name = hostname
                 host.image = image
+                # RFC921: They must start with a letter, end with a letter or digit and have only letters or digits or hyphen as interior characters
+                if bool(re.match("^[A-Za-z0-9-]*$", hostname)) is False:
+                    self.set_invalid("Device names must only contain alphanumeric characters and dashes.")
+                    return
+                if not hostname[0].isalpha() or not hostname[-1].isalnum():
+                    self.set_invalid("Device names must start with a letter and end with a letter or digit.")
+                    return
+                for j in range(i):
+                    if j != i and hostname == hosts[j].name:
+                        self.set_invalid("Devices must have unique names. Please try again.")
+                        return
                 host.save()
 
             if not has_headnode and len(hosts) > 0:
@@ -272,7 +285,7 @@ class Define_Software(WorkflowStep):
 
             self.set_valid("Completed")
         else:
-            self.set_invalid("Please complete all fields")
+            self.set_invalid("Please complete all fields.")
 
 
 class Define_Nets(WorkflowStep):
@@ -598,4 +611,4 @@ class Resource_Meta_Info(WorkflowStep):
             self.repo_put(self.repo.CONFIRMATION, confirm)
             self.set_valid("Step Completed")
         else:
-            self.set_invalid("Please correct the fields highlighted in red to continue")
+            self.set_invalid("Please complete all fields.")
index a48efe5..40be9d6 100644 (file)
@@ -48,7 +48,7 @@ class SessionManager():
 
     def add_workflow(self, workflow_type=None, **kwargs):
         repo = Repository()
-        if(len(self.workflows) >= 1):
+        if (len(self.workflows) >= 1):
             defaults = self.workflows[-1].repository.get_child_defaults()
             repo.set_defaults(defaults)
             repo.el[repo.HAS_RESULT] = False