summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--dashboard/requirements.txt2
-rw-r--r--dashboard/src/api/tests/test_models_unittest.py62
-rw-r--r--dashboard/src/api/urls.py4
-rw-r--r--dashboard/src/booking/forms.py3
-rw-r--r--dashboard/src/booking/quick_deployer.py35
-rw-r--r--dashboard/src/booking/stats.py42
-rw-r--r--dashboard/src/booking/views.py12
-rw-r--r--dashboard/src/dashboard/testing_utils.py9
-rw-r--r--dashboard/src/pharos_dashboard/settings.py2
-rw-r--r--dashboard/src/static/bower.json12
-rw-r--r--dashboard/src/static/css/base.css8
-rw-r--r--dashboard/src/static/js/dashboard.js1134
-rw-r--r--dashboard/src/templates/account/booking_list.html55
-rw-r--r--dashboard/src/templates/account/configuration_list.html11
-rw-r--r--dashboard/src/templates/account/image_list.html17
-rw-r--r--dashboard/src/templates/account/resource_list.html10
-rw-r--r--dashboard/src/templates/account/userprofile_update_form.html17
-rw-r--r--dashboard/src/templates/base.html302
-rw-r--r--dashboard/src/templates/booking/booking_calendar.html2
-rw-r--r--dashboard/src/templates/booking/booking_delete.html2
-rw-r--r--dashboard/src/templates/booking/booking_detail.html42
-rw-r--r--dashboard/src/templates/booking/booking_list.html36
-rw-r--r--dashboard/src/templates/booking/quick_deploy.html121
-rw-r--r--dashboard/src/templates/booking/stats.html31
-rw-r--r--dashboard/src/templates/booking/steps/booking_confirm.html2
-rw-r--r--dashboard/src/templates/booking/steps/booking_meta.html15
-rw-r--r--dashboard/src/templates/booking/steps/resource_select.html2
-rw-r--r--dashboard/src/templates/booking/steps/swconfig_select.html2
-rw-r--r--dashboard/src/templates/config_bundle/steps/assign_host_roles.html2
-rw-r--r--dashboard/src/templates/config_bundle/steps/assign_network_roles.html2
-rw-r--r--dashboard/src/templates/config_bundle/steps/config_software.html2
-rw-r--r--dashboard/src/templates/config_bundle/steps/define_software.html2
-rw-r--r--dashboard/src/templates/config_bundle/steps/pick_installer.html2
-rw-r--r--dashboard/src/templates/config_bundle/steps/table_formset.html11
-rw-r--r--dashboard/src/templates/dashboard/genericselect.html2
-rw-r--r--dashboard/src/templates/dashboard/lab_detail.html71
-rw-r--r--dashboard/src/templates/dashboard/lab_list.html44
-rw-r--r--dashboard/src/templates/dashboard/landing.html159
-rw-r--r--dashboard/src/templates/dashboard/multiple_select_filter_widget.html420
-rw-r--r--dashboard/src/templates/dashboard/resource.html10
-rw-r--r--dashboard/src/templates/dashboard/resource_all.html10
-rw-r--r--dashboard/src/templates/dashboard/searchable_select_multiple.html366
-rw-r--r--dashboard/src/templates/dashboard/table.html9
-rw-r--r--dashboard/src/templates/layout.html11
-rw-r--r--dashboard/src/templates/notifier/inbox.html197
-rw-r--r--dashboard/src/templates/resource/hostprofile_detail.html112
-rw-r--r--dashboard/src/templates/resource/steps/define_hardware.html17
-rw-r--r--dashboard/src/templates/resource/steps/host_info.html2
-rw-r--r--dashboard/src/templates/resource/steps/meta_info.html2
-rw-r--r--dashboard/src/templates/resource/steps/pod_definition.html617
-rw-r--r--dashboard/src/templates/snapshot_workflow/steps/meta.html2
-rw-r--r--dashboard/src/templates/snapshot_workflow/steps/select_host.html2
-rw-r--r--dashboard/src/templates/workflow/confirm.html2
-rw-r--r--dashboard/src/templates/workflow/resource_select.html2
-rw-r--r--dashboard/src/templates/workflow/viewport-base.html443
-rw-r--r--dashboard/src/templates/workflow/viewport-element.html2
-rw-r--r--dashboard/src/workflow/forms.py169
-rw-r--r--dashboard/src/workflow/resource_bundle_workflow.py170
58 files changed, 2469 insertions, 2385 deletions
diff --git a/dashboard/requirements.txt b/dashboard/requirements.txt
index 9ea10a4..55e5fc9 100644
--- a/dashboard/requirements.txt
+++ b/dashboard/requirements.txt
@@ -1,7 +1,7 @@
celery==3.1.23
cryptography==2.3.1
Django==2.1
-django-bootstrap3==10.0.1
+django-bootstrap4==0.0.8
django-crispy-forms==1.7.2
django-filter==2.0.0
django-registration==2.1.2
diff --git a/dashboard/src/api/tests/test_models_unittest.py b/dashboard/src/api/tests/test_models_unittest.py
index cabffc9..e6f97a6 100644
--- a/dashboard/src/api/tests/test_models_unittest.py
+++ b/dashboard/src/api/tests/test_models_unittest.py
@@ -15,6 +15,7 @@ from api.models import (
HostHardwareRelation,
SoftwareRelation,
AccessConfig,
+ SnapshotRelation
)
from resource_inventory.models import (
@@ -103,8 +104,6 @@ class ValidBookingCreatesValidJob(TestCase):
if not booking.resource:
raise Exception("Booking does not have a resource when trying to pass to makeCompleteJob")
- JobFactory.makeCompleteJob(booking)
-
return booking, compute_hostnames, "jump"
def make_networks(self, hostprofile, nets):
@@ -122,9 +121,12 @@ class ValidBookingCreatesValidJob(TestCase):
return network_struct
- # begin tests
+ #################################################################
+ # Complete Job Tests
+ #################################################################
- def test_valid_access_configs(self):
+ def test_complete_job_makes_access_configs(self):
+ JobFactory.makeCompleteJob(self.booking)
job = Job.objects.get(booking=self.booking)
self.assertIsNotNone(job)
@@ -142,7 +144,8 @@ class ValidBookingCreatesValidJob(TestCase):
self.assertTrue(vpn_configs.filter(user=user).exists())
self.assertTrue(ssh_configs.filter(user=user).exists())
- def test_valid_network_configs(self):
+ def test_complete_job_makes_network_configs(self):
+ JobFactory.makeCompleteJob(self.booking)
job = Job.objects.get(booking=self.booking)
self.assertIsNotNone(job)
@@ -180,7 +183,8 @@ class ValidBookingCreatesValidJob(TestCase):
for host in netrelation_hosts:
self.assertTrue(host in booking_hosts)
- def test_valid_hardware_configs(self):
+ def test_complete_job_makes_hardware_configs(self):
+ JobFactory.makeCompleteJob(self.booking)
job = Job.objects.get(booking=self.booking)
self.assertIsNotNone(job)
@@ -199,7 +203,8 @@ class ValidBookingCreatesValidJob(TestCase):
host = relation.host
self.assertEqual(config.hostname, host.template.resource.name)
- def test_valid_software_configs(self):
+ def test_complete_job_makes_software_configs(self):
+ JobFactory.makeCompleteJob(self.booking)
job = Job.objects.get(booking=self.booking)
self.assertIsNotNone(job)
@@ -218,10 +223,47 @@ class ValidBookingCreatesValidJob(TestCase):
self.assertEqual(oconfig.scenario, self.booking.config_bundle.opnfv_config.first().scenario.name)
for host in oconfig.roles.all():
- role_name = host.config.opnfvRole.name
- if str(role_name) == "Jumphost":
+ role_name = host.config.host_opnfv_config.first().role.name
+ if str(role_name).lower() == "jumphost":
self.assertEqual(host.template.resource.name, self.jump_hostname)
- elif str(role_name) == "Compute":
+ elif str(role_name).lower() == "compute":
self.assertTrue(host.template.resource.name in self.compute_hostnames)
else:
self.fail(msg="Host with non-configured role name related to job: " + str(role_name))
+
+ def test_make_snapshot_task(self):
+ host = self.booking.resource.hosts.first()
+ image = make_image(self.lab, -1, None, None, host.profile)
+
+ Job.objects.create(booking=self.booking)
+
+ JobFactory.makeSnapshotTask(image, self.booking, host)
+
+ snap_relation = SnapshotRelation.objects.get(job=self.booking.job)
+ config = snap_relation.config
+ self.assertEqual(host.id, config.host.id)
+ self.assertEqual(config.dashboard_id, image.id)
+ self.assertEqual(snap_relation.snapshot.id, image.id)
+
+ def test_make_hardware_configs(self):
+ hosts = self.booking.resource.hosts.all()
+ job = Job.objects.create(booking=self.booking)
+ JobFactory.makeHardwareConfigs(hosts=hosts, job=job)
+
+ hardware_relations = HostHardwareRelation.objects.filter(job=job)
+
+ self.assertEqual(hardware_relations.count(), hosts.count())
+
+ host_set = set([h.id for h in hosts])
+
+ for relation in hardware_relations:
+ try:
+ host_set.remove(relation.host.id)
+ except KeyError:
+ self.fail("Hardware Relation/Config not created for host " + str(relation.host))
+
+ self.assertEqual(relation.config.power, "on")
+ self.assertTrue(relation.config.ipmi_create)
+ # TODO: the rest of hwconf attrs
+
+ self.assertEqual(len(host_set), 0)
diff --git a/dashboard/src/api/urls.py b/dashboard/src/api/urls.py
index d1f772a..7a48425 100644
--- a/dashboard/src/api/urls.py
+++ b/dashboard/src/api/urls.py
@@ -57,8 +57,8 @@ urlpatterns = [
path('labs/<slug:lab_name>/inventory', lab_inventory),
path('labs/<slug:lab_name>/hosts/<slug:host_id>', lab_host),
path('labs/<slug:lab_name>/hosts/<slug:host_id>/bmc', update_host_bmc),
- path('labs/<slug:lab_name>/booking/<slug:booking_id>/pdf', get_pdf, name="get-pdf"),
- path('labs/<slug:lab_name>/booking/<slug:booking_id>/idf', get_idf, name="get-idf"),
+ path('labs/<slug:lab_name>/booking/<int:booking_id>/pdf', get_pdf, name="get-pdf"),
+ path('labs/<slug:lab_name>/booking/<int:booking_id>/idf', get_idf, name="get-idf"),
path('labs/<slug:lab_name>/jobs/<int:job_id>', specific_job),
path('labs/<slug:lab_name>/jobs/<int:job_id>/<slug:task_id>', specific_task),
path('labs/<slug:lab_name>/jobs/new', new_jobs),
diff --git a/dashboard/src/booking/forms.py b/dashboard/src/booking/forms.py
index e48b293..df88cc6 100644
--- a/dashboard/src/booking/forms.py
+++ b/dashboard/src/booking/forms.py
@@ -48,8 +48,7 @@ class QuickBookingForm(forms.Form):
)
attrs = FormUtils.getLabData(0)
- attrs['selection_data'] = 'false'
- self.fields['filter_field'] = MultipleSelectFilterField(widget=MultipleSelectFilterWidget(attrs=attrs))
+ self.fields['filter_field'] = MultipleSelectFilterField(widget=MultipleSelectFilterWidget(**attrs))
self.fields['length'] = forms.IntegerField(
widget=NumberInput(
attrs={
diff --git a/dashboard/src/booking/quick_deployer.py b/dashboard/src/booking/quick_deployer.py
index ac69c8c..0e0cc5a 100644
--- a/dashboard/src/booking/quick_deployer.py
+++ b/dashboard/src/booking/quick_deployer.py
@@ -96,25 +96,22 @@ class BookingPermissionException(Exception):
pass
-def parse_host_field(host_field_contents):
- host_json = json.loads(host_field_contents)
- lab_dict = host_json['labs'][0]
- lab_id = list(lab_dict.keys())[0]
- lab_user_id = int(lab_id.split("_")[-1])
- lab = Lab.objects.get(lab_user__id=lab_user_id)
-
- host_dict = host_json['hosts'][0]
- profile_id = list(host_dict.keys())[0]
- profile_id = int(profile_id.split("_")[-1])
- profile = HostProfile.objects.get(id=profile_id)
-
- # check validity of field data before trying to apply to models
- if len(host_json['labs']) != 1:
+def parse_host_field(host_json):
+ lab, profile = (None, None)
+ lab_dict = host_json['lab']
+ for lab_info in lab_dict.values():
+ if lab_info['selected']:
+ lab = Lab.objects.get(lab_user__id=lab_info['id'])
+
+ host_dict = host_json['host']
+ for host_info in host_dict.values():
+ if host_info['selected']:
+ profile = HostProfile.objects.get(pk=host_info['id'])
+
+ if lab is None:
raise NoLabSelectedError("No lab was selected")
- if not lab:
- raise LabDNE("Lab with provided ID does not exist")
- if not profile:
- raise HostProfileDNE("Host type with provided ID does not exist")
+ if profile is None:
+ raise HostProfileDNE("No Host was selected")
return lab, profile
@@ -329,6 +326,8 @@ def create_from_form(form, request):
JobFactory.makeCompleteJob(booking)
NotificationHandler.notify_new_booking(booking)
+ return booking
+
def drop_filter(user):
installer_filter = {}
diff --git a/dashboard/src/booking/stats.py b/dashboard/src/booking/stats.py
index b706577..383723a 100644
--- a/dashboard/src/booking/stats.py
+++ b/dashboard/src/booking/stats.py
@@ -25,34 +25,34 @@ class StatisticsManager(object):
some point in the given date span is the number of days to plot.
The last x value will always be the current time
"""
- x_set = set()
+ data = []
x = []
y = []
users = []
now = datetime.datetime.now(pytz.utc)
delta = datetime.timedelta(days=span)
end = now - delta
- bookings = Booking.objects.filter(start__lte=now, end__gte=end)
- for booking in bookings:
- x_set.add(booking.start)
- if booking.end < now:
- x_set.add(booking.end)
+ bookings = Booking.objects.filter(start__lte=now, end__gte=end).prefetch_related("collaborators")
+ for booking in bookings: # collect data from each booking
+ user_list = [u.pk for u in booking.collaborators.all()]
+ user_list.append(booking.owner.pk)
+ data.append((booking.start, 1, user_list))
+ data.append((booking.end, -1, user_list))
- x_set.add(now)
- x_set.add(end)
+ # sort based on time
+ data.sort(key=lambda i: i[0])
- x_list = list(x_set)
- x_list.sort(reverse=True)
- for time in x_list:
- x.append(str(time))
- active = Booking.objects.filter(start__lte=time, end__gt=time)
- booking_count = len(active)
- users_set = set()
- for booking in active:
- users_set.add(booking.owner)
- for user in booking.collaborators.all():
- users_set.add(user)
- y.append(booking_count)
- users.append(len(users_set))
+ # collect data
+ count = 0
+ active_users = {}
+ for datum in data:
+ x.append(str(datum[0])) # time
+ count += datum[1] # booking count
+ y.append(count)
+ for pk in datum[2]: # maintain count of each user's active bookings
+ active_users[pk] = active_users.setdefault(pk, 0) + datum[1]
+ if active_users[pk] == 0:
+ del active_users[pk]
+ users.append(len([x for x in active_users.values() if x > 0]))
return {"booking": [x, y], "user": [x, users]}
diff --git a/dashboard/src/booking/views.py b/dashboard/src/booking/views.py
index 13e9d01..bad7dc9 100644
--- a/dashboard/src/booking/views.py
+++ b/dashboard/src/booking/views.py
@@ -16,6 +16,7 @@ from django.views import View
from django.views.generic import TemplateView
from django.shortcuts import redirect, render
from django.db.models import Q
+from django.urls import reverse
from resource_inventory.models import ResourceBundle, HostProfile, Image, Host
from resource_inventory.resource_manager import ResourceManager
@@ -60,14 +61,13 @@ def quick_create(request):
if form.is_valid():
try:
- create_from_form(form, request)
+ booking = create_from_form(form, request)
+ messages.success(request, "We've processed your request. "
+ "Check Account->My Bookings for the status of your new booking")
+ return redirect(reverse('booking:booking_detail', kwargs={'booking_id': booking.id}))
except Exception as e:
messages.error(request, "Whoops, an error occurred: " + str(e))
- return render(request, 'workflow/exit_redirect.html', context)
-
- messages.success(request, "We've processed your request. "
- "Check Account->My Bookings for the status of your new booking")
- return render(request, 'workflow/exit_redirect.html', context)
+ return render(request, 'booking/quick_deploy.html', context)
else:
messages.error(request, "Looks like the form didn't validate. Check that you entered everything correctly")
return render(request, 'booking/quick_deploy.html', context)
diff --git a/dashboard/src/dashboard/testing_utils.py b/dashboard/src/dashboard/testing_utils.py
index 1cca3e6..a96b6d0 100644
--- a/dashboard/src/dashboard/testing_utils.py
+++ b/dashboard/src/dashboard/testing_utils.py
@@ -80,7 +80,7 @@ def make_booking(owner=None, start=timezone.now(),
topology={}, installer=None, scenario=None):
grb, host_set = make_grb(topology, owner, lab)
- config_bundle = make_config_bundle(grb, owner, topology, host_set, installer, scenario)
+ config_bundle, opnfv_bundle = make_config_bundle(grb, owner, topology, host_set, installer, scenario)
resource = ResourceManager.getInstance().convertResourceBundle(grb, config=config_bundle)
if not resource:
raise Exception("Resource not created")
@@ -93,7 +93,8 @@ def make_booking(owner=None, start=timezone.now(),
owner=owner,
purpose=purpose,
project=project,
- lab=lab
+ lab=lab,
+ opnfv_config=opnfv_bundle
)
@@ -124,7 +125,7 @@ def make_config_bundle(grb, owner, topology={}, host_set={},
host_config=host_config,
opnfv_config=opnfv_config
)
- return cb
+ return cb, opnfv_config
def make_network(name, lab, grb, public):
@@ -230,7 +231,7 @@ def make_vlan_manager(vlans=None, block_size=20, allow_overlapping=False, reserv
)
-def make_lab(user=None, name="Test Lab Instance",
+def make_lab(user=None, name="Test_Lab_Instance",
status=LabStatus.UP, vlan_manager=None,
pub_net_count=5):
if not vlan_manager:
diff --git a/dashboard/src/pharos_dashboard/settings.py b/dashboard/src/pharos_dashboard/settings.py
index 793eec7..86de78c 100644
--- a/dashboard/src/pharos_dashboard/settings.py
+++ b/dashboard/src/pharos_dashboard/settings.py
@@ -35,7 +35,7 @@ INSTALLED_APPS = [
'django.contrib.messages',
'django.contrib.staticfiles',
'django.contrib.humanize',
- 'bootstrap3',
+ 'bootstrap4',
'crispy_forms',
'rest_framework',
'rest_framework.authtoken',
diff --git a/dashboard/src/static/bower.json b/dashboard/src/static/bower.json
index 9ae744a..dda786d 100644
--- a/dashboard/src/static/bower.json
+++ b/dashboard/src/static/bower.json
@@ -16,12 +16,14 @@
"tests"
],
"dependencies": {
- "eonasdan-bootstrap-datetimepicker": "^4.17.37",
"fullcalendar": "^2.9.0",
"jquery-migrate": "^3.0.0",
- "startbootstrap-sb-admin-2-blackrockdigital": "^3.3.7"
- },
- "resolutions": {
- "font-awesome": "~4.6.3"
+ "bootstrap": "4.3.1",
+ "popper.js": "1.14.3",
+ "Font-Awesome": "5.9.0",
+ "datatables.net": "1.10.19",
+ "datatables.net-bs4": "1.10.19",
+ "datatables.net-responsive": "2.1.1",
+ "datatables.net-responsive-bs4": "2.2.3"
}
}
diff --git a/dashboard/src/static/css/base.css b/dashboard/src/static/css/base.css
new file mode 100644
index 0000000..c51728c
--- /dev/null
+++ b/dashboard/src/static/css/base.css
@@ -0,0 +1,8 @@
+/* Rotating arrows when dropdown happens */
+i.fas.rotate {
+ transition: transform 0.3s ease-in-out;
+}
+
+a[aria-expanded="true"] > i.rotate {
+ transform: rotate(180deg);
+}
diff --git a/dashboard/src/static/js/dashboard.js b/dashboard/src/static/js/dashboard.js
new file mode 100644
index 0000000..84c3703
--- /dev/null
+++ b/dashboard/src/static/js/dashboard.js
@@ -0,0 +1,1134 @@
+class MultipleSelectFilterWidget {
+
+ constructor(neighbors, items, initial) {
+ this.inputs = [];
+ this.graph_neighbors = neighbors;
+ this.filter_items = items;
+ 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;
+ for(let item_class in initial_data) {
+ const selected_items = initial_data[item_class];
+ for( let node_id in selected_items ){
+ const node = this.filter_items[node_id];
+ const selection_data = selected_items[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);
+ }
+ }
+ }
+ }
+
+ 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('disabled_node', 'cleared_node');
+ elem.classList.add('selected_node');
+ }
+
+ clear(node) {
+ const elem = document.getElementById(node['id']);
+ node['selected'] = false;
+ node['selectable'] = true;
+ elem.classList.add('cleared_node')
+ elem.classList.remove('disabled_node', 'selected_node');
+ }
+
+ disable_node(node) {
+ const elem = document.getElementById(node['id']);
+ node['selected'] = false;
+ node['selectable'] = false;
+ elem.classList.remove('cleared_node', 'selected_node');
+ elem.classList.add('disabled_node');
+ }
+
+ processClick(id){
+ const node = this.filter_items[id];
+ if(!node['selectable'])
+ 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.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");
+ 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.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("dropdown_item");
+ this.dropdown_count++;
+ const label = document.createElement("H5")
+ label.appendChild(document.createTextNode(node['name']))
+ div.appendChild(label);
+ div.appendChild(this.make_input(div, node, prepopulate));
+ div.appendChild(this.make_remove_button(div, node));
+ 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);
+ delete this.result[node.class][node.id]['values'][div.id];
+
+ //checks if we have removed last item in class
+ if(jQuery.isEmptyObject(this.result[node.class][node.id]['values'])){
+ 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, values: {}}
+
+ this.result[node.class][node.id]['values'][childKey] = childValue;
+ }
+
+ finish(){
+ document.getElementById("filter_field").value = JSON.stringify(this.result);
+ }
+}
+
+class NetworkStep {
+ constructor(debug, xml, hosts, added_hosts, removed_host_ids, graphContainer, overviewContainer, toolbarContainer){
+ if(!this.check_support())
+ 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;
+
+ this.editor.setGraphContainer(graphContainer);
+ this.doGlobalConfig();
+ this.prefill(xml, hosts, added_hosts, removed_host_ids);
+ 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);
+
+ if(this.debug){
+ this.editor.addAction('printXML', function(editor, cell) {
+ mxLog.write(this.encodeGraph());
+ mxLog.show();
+ }.bind(this));
+ this.addToolbarButton(this.editor, toolbarContainer, 'printXML', '', '/static/img/mxgraph/fit_to_size.png', true);
+ }
+
+ 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);
+
+ if(!this.has_public_net){
+ this.addPublicNetwork();
+ }
+ }
+
+ check_support(){
+ if (!mxClient.isBrowserSupported()) {
+ mxUtils.error('Browser is not supported', 200, false);
+ return false;
+ }
+ return true;
+ }
+
+ prefill(xml, hosts, added_hosts, removed_host_ids){
+ //populate existing data
+ if(xml){
+ this.restoreFromXml(xml, this.editor);
+ } else if(hosts){
+ for(const host of hosts)
+ this.makeHost(host);
+ }
+
+ //apply any changes
+ if(added_hosts){
+ for(const host of added_hosts)
+ this.makeHost(host);
+ this.updateHosts([]); //TODO: why?
+ }
+ this.updateHosts(removed_host_ids);
+ }
+
+ cellConnectionHandler(sender, event){
+ const edge = event.getProperty('edge');
+ const terminal = event.getProperty('terminal')
+ const source = event.getProperty('source');
+ if(this.checkAllowed(edge, terminal, source)) {
+ this.colorEdge(edge, terminal, source);
+ this.alertVlan(edge, terminal, source);
+ }
+ }
+
+ doubleClickHandler(evt, cell) {
+ if( cell != null ){
+ if( cell.getParent() != null && cell.getParent().getId().indexOf("network") > -1) {
+ cell = cell.getParent();
+ }
+ if( cell.isEdge() || cell.getId().indexOf("network") > -1 ) {
+ this.createDeleteDialog(cell.getId());
+ }
+ else {
+ this.showDetailWindow(cell);
+ }
+ }
+ }
+
+ alertVlan(edge, terminal, source) {
+ if( terminal == null || edge.getTerminal(!source) == null) {
+ return;
+ }
+ const form = document.createElement("form");
+ const tagged = document.createElement("input");
+ tagged.type = "radio";
+ tagged.name = "tagged";
+ tagged.value = "True";
+ form.appendChild(tagged);
+ form.appendChild(document.createTextNode(" Tagged"));
+ form.appendChild(document.createElement("br"));
+
+ const untagged = document.createElement("input");
+ untagged.type = "radio";
+ untagged.name = "tagged";
+ untagged.value = "False";
+ form.appendChild(untagged);
+ form.appendChild(document.createTextNode(" Untagged"));
+ form.appendChild(document.createElement("br"));
+
+ const yes_button = document.createElement("button");
+ yes_button.onclick = function() {this.parseVlanWindow(edge.getId());}.bind(this);
+ yes_button.appendChild(document.createTextNode("Okay"));
+
+ const cancel_button = document.createElement("button");
+ cancel_button.onclick = function() {this.deleteVlanWindow(edge.getId());}.bind(this);
+ cancel_button.appendChild(document.createTextNode("Cancel"));
+
+ const error_div = document.createElement("div");
+ error_div.id = "current_window_errors";
+ form.appendChild(error_div);
+
+ const content = document.createElement('div');
+ content.appendChild(form);
+ content.appendChild(yes_button);
+ content.appendChild(cancel_button);
+ this.showWindow("Vlan Selection", content, 200, 200);
+ }
+
+ createDeleteDialog(id) {
+ const content = document.createElement('div');
+ const remove_button = document.createElement("button");
+ remove_button.style.width = '46%';
+ remove_button.onclick = function() { this.deleteCell(id);}.bind(this);
+ remove_button.appendChild(document.createTextNode("Remove"));
+ const cancel_button = document.createElement("button");
+ cancel_button.style.width = '46%';
+ cancel_button.onclick = function() { this.closeWindow();}.bind(this);
+ cancel_button.appendChild(document.createTextNode("Cancel"));
+
+ content.appendChild(remove_button);
+ content.appendChild(cancel_button);
+ this.showWindow('Do you want to delete this network?', content, 200, 62);
+ }
+
+ checkAllowed(edge, terminal, source) {
+ //check if other terminal is null, and that they are different
+ const otherTerminal = edge.getTerminal(!source);
+ if(terminal != null && otherTerminal != null) {
+ if( terminal.getParent().getId().split('_')[0] == //'host' or 'network'
+ otherTerminal.getParent().getId().split('_')[0] ) {
+ //not allowed
+ this.graph.removeCells([edge]);
+ return false;
+ }
+ }
+ return true;
+ }
+
+ colorEdge(edge, terminal, source) {
+ if(terminal.getParent().getId().indexOf('network') >= 0) {
+ const styles = terminal.getParent().getStyle().split(';');
+ let color = 'black';
+ for(let style of styles){
+ const kvp = style.split('=');
+ if(kvp[0] == "fillColor"){
+ color = kvp[1];
+ }
+ }
+ edge.setStyle('strokeColor=' + color);
+ }
+ }
+
+ 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, label, image, isTransparent) {
+ const button = document.createElement('button');
+ button.style.fontSize = '10';
+ if (image != null) {
+ const img = document.createElement('img');
+ img.setAttribute('src', image);
+ img.style.width = '16px';
+ img.style.height = '16px';
+ img.style.verticalAlign = 'middle';
+ img.style.marginRight = '2px';
+ button.appendChild(img);
+ }
+ if (isTransparent) {
+ button.style.background = 'transparent';
+ button.style.color = '#FFFFFF';
+ button.style.border = 'none';
+ }
+ mxEvent.addListener(button, 'click', function(evt) {
+ editor.execute(action);
+ });
+ mxUtils.write(button, label);
+ 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;
+ }
+
+ 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 newNet = document.createElement("li");
+ const colorBlob = document.createElement("div");
+ colorBlob.className = "colorblob";
+ const textContainer = document.createElement("p");
+ textContainer.className = "network_innertext";
+ newNet.id = net_id;
+ const deletebutton = document.createElement("button");
+ deletebutton.className = "btn btn-danger";
+ deletebutton.style = "float: right; height: 20px; line-height: 8px; vertical-align: middle; width: 20px; padding-left: 5px;";
+ deletebutton.appendChild(document.createTextNode("X"));
+ deletebutton.addEventListener("click", function() { this.createDeleteDialog(net_id); }.bind(this), false);
+ textContainer.appendChild(document.createTextNode(net_name));
+ colorBlob.style['background'] = color;
+ newNet.appendChild(colorBlob);
+ newNet.appendChild(textContainer);
+ if( net_name != "public" ) {
+ newNet.appendChild(deletebutton);
+ }
+ document.getElementById("network_list").appendChild(newNet);
+ }
+
+ makeHost(hostInfo) {
+ 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);
+ this.graph.refresh(port);
+ }
+ this.graph.refresh(host);
+ }
+
+ submitForm() {
+ const form = document.getElementById("xml_form");
+ const input_elem = document.getElementById("hidden_xml_input");
+ input_elem.value = this.encodeGraph(this.graph);
+ const req = new XMLHttpRequest();
+ req.open("POST", "/wf/workflow/", false);
+ req.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
+ req.onerror = function() { alert("problem with form submission"); }
+ const formData = $("#xml_form").serialize();
+ req.send(formData);
+ }
+}
+
+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 result_entry = document.createElement("li");
+ const result_button = document.createElement("a");
+ const obj = this.items[id];
+ const result_text = this.generate_element_text(obj);
+ result_button.appendChild(document.createTextNode(result_text));
+ result_button.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.setAttribute('class', 'entry_tooltip');
+ result_button.appendChild(tooltip);
+ result_entry.appendChild(result_button);
+ 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);
+ }
+
+ let list_html = "";
+
+ for( const item_id of this.added_items )
+ {
+ const item = this.items[item_id];
+
+ const element_entry_text = this.generate_element_text(item);
+
+ list_html += '<div class="list_entry">'
+ + '<p class="added_entry_text">'
+ + element_entry_text
+ + '</p>'
+ + '<button onclick="searchable_select_multiple_widget.remove_item('
+ + item_id
+ + ')" class="btn-remove btn">remove</button>';
+ list_html += '</div>';
+ }
+ added_list.innerHTML = list_html;
+ }
+}
diff --git a/dashboard/src/templates/account/booking_list.html b/dashboard/src/templates/account/booking_list.html
index ed59b81..98ab5c8 100644
--- a/dashboard/src/templates/account/booking_list.html
+++ b/dashboard/src/templates/account/booking_list.html
@@ -3,9 +3,11 @@
<h2>Bookings I Own</h2>
<div class="card_container">
{% for booking in bookings %}
- <div class="detail_card">
- <div>
+ <div class="card">
+ <div class="card-header">
<h3>Booking {{booking.id}}</h3>
+ </div>
+ <div class="card-body">
<ul class="list-group">
<li class="list-group-item">id: {{booking.id}}</li>
<li class="list-group-item">lab: {{booking.lab}}</li>
@@ -15,8 +17,8 @@
<li class="list-group-item">purpose: {{booking.purpose}}</li>
</ul>
</div>
- <div class="detail_button_container">
- <a class="btn btn-primary" href="/booking/detail/{{booking.id}}/">Details</a>
+ <div class="card-footer d-flex">
+ <a class="btn btn-primary ml-auto mr-2" href="/booking/detail/{{booking.id}}/">Details</a>
<button
class="btn btn-danger"
onclick='cancel_booking({{booking.id}});'
@@ -29,32 +31,37 @@
</div>
<h2>Bookings I Collaborate On</h2>
<div class="card_container">
- {% for booking in collab_bookings %}
- <div class="detail_card">
- <div>
- <h3>Booking {{booking.id}}</h3>
- <ul class="list-group">
- <li class="list-group-item">id: {{booking.id}}</li>
- <li class="list-group-item">lab: {{booking.lab}}</li>
- <li class="list-group-item">resource: {{booking.resource.template.name}}</li>
- <li class="list-group-item">start: {{booking.start}}</li>
- <li class="list-group-item">end: {{booking.end}}</li>
- <li class="list-group-item">purpose: {{booking.purpose}}</li>
- </ul>
+ {% for booking in collab_bookings %}
+ <div class="card">
+ <div class="card-header">
+ <h3>Booking {{booking.id}}</h3>
+ </div>
+ <div class="card-body">
+ <ul class="list-group">
+ <li class="list-group-item">id: {{booking.id}}</li>
+ <li class="list-group-item">lab: {{booking.lab}}</li>
+ <li class="list-group-item">resource: {{booking.resource.template.name}}</li>
+ <li class="list-group-item">start: {{booking.start}}</li>
+ <li class="list-group-item">end: {{booking.end}}</li>
+ <li class="list-group-item">purpose: {{booking.purpose}}</li>
+ </ul>
+ </div>
+ <div class="card-footer d-flex">
+ <a class="btn btn-primary ml-auto" href="/booking/detail/{{booking.id}}/">Details</a>
+ </div>
</div>
- <a class="btn btn-primary" href="/booking/detail/{{booking.id}}/">Details</a>
- </div>
- {% endfor %}
+ {% endfor %}
</div>
-
<h2>Expired Bookings
<i class="fa fa-fw fa-caret-down" onclick='toggle_display("expired_bookings");'></i>
</h2>
<div id="expired_bookings" class="card_container" style="display:none;">
{% for booking in expired_bookings %}
- <div class="detail_card">
- <div>
+ <div class="card">
+ <div class="card-header">
<h3>Booking {{booking.id}}</h3>
+ </div>
+ <div class="card-body">
<ul class="list-group">
<li class="list-group-item">id: {{booking.id}}</li>
<li class="list-group-item">lab: {{booking.lab}}</li>
@@ -65,7 +72,9 @@
<li class="list-group-item">owner: {{booking.owner.userprofile.email_addr}}</li>
</ul>
</div>
- <a class="btn btn-primary" href="/booking/detail/{{booking.id}}/">Details</a>
+ <div class="card-footer d-flex">
+ <a class="btn btn-primary ml-auto" href="/booking/detail/{{booking.id}}/">Details</a>
+ </div>
</div>
{% endfor %}
</div>
diff --git a/dashboard/src/templates/account/configuration_list.html b/dashboard/src/templates/account/configuration_list.html
index b920ba6..6f7844a 100644
--- a/dashboard/src/templates/account/configuration_list.html
+++ b/dashboard/src/templates/account/configuration_list.html
@@ -2,9 +2,11 @@
{% block content %}
<div class="card_container">
{% for config in configurations %}
- <div class="detail_card">
- <div>
+ <div class="card">
+ <div class="card-header">
<h3>Configuration {{config.id}}</h3>
+ </div>
+ <div class="card-body">
<ul class="list-group">
<li class="list-group-item">id: {{config.id}}</li>
<li class="list-group-item">name: {{config.name}}</li>
@@ -12,10 +14,9 @@
<li class="list-group-item">resource: {{config.bundle}}</li>
</ul>
</div>
- <div class="detail_button_container">
+ <div class="card-footer">
<button
- class="btn btn-danger"
- style="width:49%;float:right;"
+ class="btn btn-danger w-100"
onclick='delete_config({{config.id}});'
data-toggle="modal"
data-target="#configModal"
diff --git a/dashboard/src/templates/account/image_list.html b/dashboard/src/templates/account/image_list.html
index cd83dcf..068e096 100644
--- a/dashboard/src/templates/account/image_list.html
+++ b/dashboard/src/templates/account/image_list.html
@@ -3,9 +3,11 @@
<h2>Images I Own</h2>
<div class="card_container">
{% for image in images %}
- <div class="detail_card">
- <div>
+ <div class="card">
+ <div class="card-header">
<h3>Image {{image.id}}</h3>
+ </div>
+ <div class="card-body">
<ul class="list-group">
<li class="list-group-item">id: {{image.id}}</li>
<li class="list-group-item">lab: {{image.from_lab.name}}</li>
@@ -14,10 +16,9 @@
<li class="list-group-item">host profile: {{image.host_type.name}}</li>
</ul>
</div>
- <div class="detail_button_container">
+ <div class="card-footer">
<button
- class="btn btn-danger"
- style="width:49%;float:right;"
+ class="btn btn-danger w-100"
onclick='delete_image({{image.id}});'
data-toggle="modal"
data-target="#imageModal"
@@ -29,9 +30,11 @@
<h2>Public Images</h2>
<div class="card_container">
{% for image in public_images %}
- <div class="detail_card">
- <div>
+ <div class="card">
+ <div class="card-header">
<h3>Image {{image.id}}</h3>
+ </div>
+ <div class="card-body">
<ul class="list-group">
<li class="list-group-item">id: {{image.id}}</li>
<li class="list-group-item">lab: {{image.from_lab.name}}</li>
diff --git a/dashboard/src/templates/account/resource_list.html b/dashboard/src/templates/account/resource_list.html
index 1391e8e..f92f78e 100644
--- a/dashboard/src/templates/account/resource_list.html
+++ b/dashboard/src/templates/account/resource_list.html
@@ -2,18 +2,20 @@
{% block content %}
<div class="card_container">
{% for resource in resources %}
- <div class="detail_card">
- <div>
+ <div class="card">
+ <div class="card-header">
<h3>Resource {{resource.id}}</h3>
+ </div>
+ <div class="card-body p-4">
<ul class="list-group">
<li class="list-group-item">id: {{resource.id}}</li>
<li class="list-group-item">name: {{resource.name}}</li>
<li class="list-group-item">description: {{resource.description}}</li>
</ul>
</div>
- <div class="detail_button_container">
+ <div class="card-footer">
<button
- class="btn btn-danger"
+ class="btn btn-danger w-100"
onclick='delete_resource({{resource.id}});'
data-toggle="modal"
data-target="#resModal"
diff --git a/dashboard/src/templates/account/userprofile_update_form.html b/dashboard/src/templates/account/userprofile_update_form.html
index f1d2852..6ab8242 100644
--- a/dashboard/src/templates/account/userprofile_update_form.html
+++ b/dashboard/src/templates/account/userprofile_update_form.html
@@ -1,17 +1,12 @@
-{% extends "layout.html" %}
-{% load bootstrap3 %}
+{% extends "base.html" %}
+{% load bootstrap4 %}
-{% block basecontent %}
- <div class="container">
+{% block content %}
+ <div class="container-fluid">
<div class="row">
- <div class="col-md-4 col-md-offset-4">
+ <div class="col-12 col-xl-6">
{% bootstrap_messages %}
<div class="login-panel panel panel-default">
- <div class="panel-heading">
- <h3 class="panel-title">
- {{ title }}
- </h3>
- </div>
<div class="panel-body">
<form enctype="multipart/form-data" method="post">
{% csrf_token %}
@@ -35,4 +30,4 @@
</div>
</div>
</div>
-{% endblock basecontent %}
+{% endblock content %}
diff --git a/dashboard/src/templates/base.html b/dashboard/src/templates/base.html
index 1c83a0e..62a9ed5 100644
--- a/dashboard/src/templates/base.html
+++ b/dashboard/src/templates/base.html
@@ -1,52 +1,51 @@
{% extends "layout.html" %}
-{% load bootstrap3 %}
+{% load bootstrap4 %}
{% load staticfiles %}
{% block extrahead %}
- <!-- Custom CSS -->
- <link href="{% static "bower_components/startbootstrap-sb-admin-2-blackrockdigital/dist/css/sb-admin-2.css" %}"
- rel="stylesheet">
- <link href="{% static "css/theme.css" %}" rel="stylesheet">
- <link href="{% static "css/detail_view.css" %}" rel="stylesheet">
-
+<!-- Custom CSS -->
+<link href="{% static "css/detail_view.css" %}" rel="stylesheet">
+<link href="{% static "css/base.css" %}" rel="stylesheet">
<script type="text/javascript">
- function cwf(type)
- {
+ function cwf(type) {
$.ajax({
type: "POST",
url: "/",
- data: {"create":type},
- beforeSend: function(request) {
+ data: {
+ "create": type
+ },
+ beforeSend: function (request) {
request.setRequestHeader("X-CSRFToken",
- $('input[name="csrfmiddlewaretoken"]').val()
+ $('input[name="csrfmiddlewaretoken"]').val()
);
}
}).done(function (data) {
window.location.replace("/wf/");
- }).fail(function(jqxHR, textstatus) {
- alert("Something went wrong...");});
+ }).fail(function (jqxHR, textstatus) {
+ alert("Something went wrong...");
+ });
}
- function continue_wf()
- {
+
+ function continue_wf() {
window.location.replace("/wf/");
}
- function toggle_create_drop()
- {
+ function toggle_create_drop() {
drop_div = document.getElementById("create_drop");
- if (drop_div.style.display === "none")
- {
+ if (drop_div.style.display === "none") {
drop_div.style.display = "inherit";
- }
- else
- {
+ } else {
drop_div.style.display = "none";
}
}
</script>
<style>
+ .navbar {
+ min-width: 200px;
+ }
+
.create_drop {
display: none;
width: 100%;
@@ -66,137 +65,172 @@
border-top: 1px solid #E7E7E7;
border-bottom: 1px solid #E7E7E7;
}
+
+ #wrapper {
+ height: 100vh;
+ }
</style>
{% endblock %}
{% block basecontent %}
- <div id="wrapper">
- <!-- Navigation -->
- <nav class="navbar navbar-default navbar-static-top" role="navigation"
- style="margin-bottom: 0">
- <div class="navbar-header">
- <button type="button" class="navbar-toggle" data-toggle="collapse"
- data-target=".navbar-collapse">
- <span class="sr-only">Toggle navigation</span>
- <span class="icon-bar"></span>
- <span class="icon-bar"></span>
- <span class="icon-bar"></span>
- </button>
- <a href="https://www.opnfv.org/" class="navbar-left"><img
- src="{% static "img/opnfv-logo.png" %}"></a>
- <a class="navbar-brand" href={% url 'dashboard:index' %}>Pharos Dashboard</a>
+<div id="wrapper" class="d-flex flex-column">
+ <!-- Navigation -->
+ <nav class="navbar navbar-light bg-light navbar-fixed-top border-bottom py-0" role="navigation" style="margin-bottom: 0">
+ <div class="container-fluid">
+ <div class="col order-2 order-lg-1 text-center text-lg-left">
+ <a href="https://www.opnfv.org/" class="navbar-brand">
+ <img src="{% static "img/opnfv-logo.png" %}">
+ </a>
+ <a class="navbar-brand" href={% url 'dashboard:index' %}>
+ Pharos Dashboard
+ </a>
</div>
<!-- /.navbar-header -->
+ <div class="col-2 order-1 order-lg-3 d-lg-none">
+ <button class="btn border" type="button" data-toggle="collapse" data-target="#sidebar"
+ aria-expanded="false" aria-controls="sidebar">
+ <i class="fas fa-bars"></i>
+ </button>
+ </div>
+ <div class="col-2 order-3">
+ <ul class="nav ml-auto">
+ <li class="dropdown ml-auto">
+ <a class="nav-link p-0 text-dark p-2" data-toggle="dropdown" href="#">
+ {% if request.user.username %}
+ {{request.user.username}}
+ {% else %}
+ <i class="fas fa-user"></i>
+ {% endif %}
+ <i class="fas fa-caret-down rotate"></i>
+ </a>
+ <div class="dropdown-menu dropdown-menu-right">
+ {% if user.is_authenticated %}
+ <a href="{% url 'account:settings' %}" class="text-dark dropdown-item">
+ <i class="fas fa-cog"></i>
+ Settings
+ </a>
+ <a href="{% url 'account:logout' %}?next={{ request.path }}" class="text-dark dropdown-item">
+ <i class="fas fa-sign-out-alt"></i>
+ Logout
+ </a>
+ {% else %}
+ <a href="{% url 'account:login' %}" class="text-dark dropdown-item">
+ <i class="fas fa-sign-in-alt"></i>
+ Login with Jira
+ </a>
+ {% endif %}
+ </div> <!-- End dropdown-menu -->
+ </li> <!-- End dropdown -->
+ </ul>
+ </div> <!-- End top right account menu -->
+ </div>
+ </nav>
+ <!-- /.navbar-top-links -->
- <ul class="nav navbar-top-links navbar-right">
- <li class="dropdown">
- <a class="dropdown-toggle" data-toggle="dropdown" href="#">
- <i class="fa fa-user fa-fw"></i> <i class="fa fa-caret-down"></i>
- </a>
- <ul class="dropdown-menu dropdown-user">
- {% if user.is_authenticated %}
- <li><a href="{% url 'account:settings' %}"><i
- class="fa fa-gear fa-fw"></i>
- Settings</a>
- </li>
- <li class="divider"></li>
- <li><a href="{% url 'account:logout' %}?next={{ request.path }}"><i
- class="fa fa-sign-out fa-fw"></i>
- Logout</a>
- </li>
- {% else %}
- <li><a href="{% url 'account:login' %}"><i
- class="fa fa-sign-in fa-fw"></i>
- Login with Jira</a>
- <li>
- {% endif %}
- </ul>
- <!-- /.dropdown-user -->
- </li>
- <!-- /.dropdown -->
- </ul>
- <!-- /.navbar-top-links -->
-
- <div class="navbar-default sidebar" role="navigation">
- <div class="sidebar-nav navbar-collapse">
- <ul class="nav" id="side-menu">
- <li>
- <a href="/"><i class="fa fa-fw"></i>Home</a>
- </li>
- <li style="width: 100%;">
- <a href="javascript:toggle_create_drop();"><i class="fa fa-fw"></i>Create<i
- class="fa fa-fw fa-caret-down"></i>
+ <!-- Page Content -->
+ <div class="container-fluid d-lg-flex flex-lg-grow-1 px-0">
+ <div class="row h-100 w-100 mx-0">
+ <div class="col-12 col-lg-auto px-0 border-right border-left bg-light" role="navigation">
+ <nav class="navbar navbar-expand-lg border-bottom p-0 w-100">
+ <div class="collapse navbar-collapse" id="sidebar">
+ <div class="list-group list-group-flush w-100 bg-light">
+ <a href="/" class="list-group-item list-group-item-action bg-light">
+ Home
+ </a>
+ {% csrf_token %}
+ <a class="list-group-item list-group-item-action bg-light" data-toggle="collapse"
+ href="#createList" role="button">
+ Create <i class="fas fa-angle-down rotate"></i>
+ </a>
+ <div class="collapse" id="createList">
+ <a href="/booking/quick/" class="list-group-item list-group-item-action list-group-item-secondary">
+ Express Booking
+ </a>
+ <a href="#" onclick="cwf(0)" class="list-group-item list-group-item-action list-group-item-secondary">
+ Book a Pod
+ </a>
+ <a href="#" onclick="cwf(1)" class="list-group-item list-group-item-action list-group-item-secondary">
+ Design a Pod
+ </a>
+ <a href="#" onclick="cwf(2)" class="list-group-item list-group-item-action list-group-item-secondary">
+ Configure a Pod
+ </a>
+ <a href="#" onclick="cwf(3)" class="list-group-item list-group-item-action list-group-item-secondary">
+ Create a Snapshot
</a>
- {% csrf_token %}
- <div id="create_drop" class="create_drop" style="display:none">
- <button class="btn drop_btn" onclick="location.href='/booking/quick/'">Express Booking</a>
- <button class="btn drop_btn" onclick="cwf(0)">Book a Pod</button>
- <button class="btn drop_btn" onclick="cwf(1)">Design a Pod</button>
- <button class="btn drop_btn" onclick="cwf(2)">Configure a Pod</button>
- <button class="btn drop_btn" onclick="cwf(3)">Create a Snapshot</button>
- <button class="btn drop_btn" onclick="cwf(4)">Configure OPNFV</button>
- </div>
- </li>
- <li>
- <a href="{% url 'resource:hosts' %}"><i
- class="fa fa-fw"></i>Hosts
+ <a href="#" onclick="cwf(4)" class="list-group-item list-group-item-action list-group-item-secondary">
+ Configure OPNFV
</a>
- </li>
- {% if user.is_authenticated %}
- <li>
- <a href="{% url 'account:users' %}"><i
- class="fa fa-fw"></i>User List
+ </div>
+ <a href="{% url 'resource:hosts' %}" class="list-group-item list-group-item-action bg-light">
+ Hosts
</a>
- </li>
- {% endif %}
- <li>
- <a href="{% url 'booking:list' %}"><i
- class="fa fa-fw"></i>Booking List
+ {% if user.is_authenticated %}
+ <a href="{% url 'account:users' %}" class="list-group-item list-group-item-action bg-light">
+ User List
+ </a>
+ {% endif %}
+ <a href="{% url 'booking:list' %}" class="list-group-item list-group-item-action bg-light">
+ Booking List
</a>
- </li>
- <li>
- <a href="{% url 'booking:stats' %}"><i
- class="fa fa-fw"></i>Booking Statistics</a>
- </li>
- <li>
- <a href="{% url 'account:my-account' %}"><i
- class="fa fa-fw"></i>Account
+ <a href="{% url 'booking:stats' %}" class="list-group-item list-group-item-action bg-light">
+ Booking Statistics
</a>
- </li>
- <li>
- <a href="{% url 'dashboard:all_labs' %}"><i
- class="fa fa-fw"></i>Lab Info
+ <!-- <a href="{% url 'account:my-account' %}" class="list-group-item list-group-item-action bg-light">
+ Account
+ </a> -->
+ <a class="list-group-item list-group-item-action bg-light" data-toggle="collapse"
+ href="#accountList" role="button">
+ Account <i class="fas fa-angle-down rotate"></i>
</a>
- </li>
- <li>
- <a href="{% url 'notifier:messages' %}"><i
- class="fa fa-fw"></i>Inbox
+ <div class="collapse" id="accountList">
+ <a href="{% url 'account:my-resources' %}" class="list-group-item list-group-item-action list-group-item-secondary">
+ My Resources
+ </a>
+ <a href="{% url 'account:my-bookings' %}" class="list-group-item list-group-item-action list-group-item-secondary">
+ My Bookings
+ </a>
+ <a href="{% url 'account:my-configurations' %}" class="list-group-item list-group-item-action list-group-item-secondary">
+ My Configurations
+ </a>
+ <a href="{% url 'account:my-images' %}" class="list-group-item list-group-item-action list-group-item-secondary">
+ My Snapshots
+ </a>
+ </div>
+ <a href="{% url 'dashboard:all_labs' %}" class="list-group-item list-group-item-action bg-light">
+ Lab Info
</a>
- </li>
- </ul>
- </div>
- <!-- /.sidebar-collapse -->
+ <a href="{% url 'notifier:messages' %}" class="list-group-item list-group-item-action bg-light">
+ Inbox
+ </a>
+ </div>
+ </div>
+ </nav>
+ <!--/.well -->
</div>
- <!-- /.navbar-static-side -->
- </nav>
+ <!--/span-->
- <!-- Page Content -->
- <div id="page-wrapper">
- {% if title %}
- <div class="row">
- <div class="col-lg-12">
- <h1 class="page-header">{{ title }}</h1>
+ <div class="col flex-grow-1 d-flex flex-column">
+ {% if title %}
+ <div class="row flex-shrink-1">
+ <div class="col-lg-12">
+ <h1 class="page-header">{{ title }}</h1>
+ </div>
+ <!-- /.col-lg-12 -->
</div>
- <!-- /.col-lg-12 -->
+ {% endif %}
+ <div id="bsm">{% bootstrap_messages %}</div>
+ <!-- Content block placed here -->
+ {% block content %}
+ {% endblock content %}
</div>
- {% endif %}
- <div id="bsm">{% bootstrap_messages %}</div>
+ <!--/span-->
- {% block content %}
- {% endblock content %}
</div>
- <!-- /#page-wrapper -->
+ <!--/row-->
</div>
- <!-- /#wrapper -->
+ <!-- /#page-wrapper -->
+</div>
+<!-- /#wrapper -->
{% endblock basecontent %}
diff --git a/dashboard/src/templates/booking/booking_calendar.html b/dashboard/src/templates/booking/booking_calendar.html
index 1b29dc2..ddcb45d 100644
--- a/dashboard/src/templates/booking/booking_calendar.html
+++ b/dashboard/src/templates/booking/booking_calendar.html
@@ -1,7 +1,7 @@
{% extends "base.html" %}
{% load staticfiles %}
-{% load bootstrap3 %}
+{% load bootstrap4 %}
{% block extrahead %}
{{ block.super }}
diff --git a/dashboard/src/templates/booking/booking_delete.html b/dashboard/src/templates/booking/booking_delete.html
index 76a5634..b89eb15 100644
--- a/dashboard/src/templates/booking/booking_delete.html
+++ b/dashboard/src/templates/booking/booking_delete.html
@@ -1,5 +1,5 @@
{% load jira_filters %}
-{% load bootstrap3 %}
+{% load bootstrap4 %}
<p>
Really delete Booking from {{ booking.start}} to {{ booking.end }}?
diff --git a/dashboard/src/templates/booking/booking_detail.html b/dashboard/src/templates/booking/booking_detail.html
index ac00e22..918f5af 100644
--- a/dashboard/src/templates/booking/booking_detail.html
+++ b/dashboard/src/templates/booking/booking_detail.html
@@ -1,6 +1,6 @@
{% extends "base.html" %}
{% load staticfiles %}
-{% load bootstrap3 %}
+{% load bootstrap4 %}
{% block extrahead %}
{{block.super}}
@@ -19,13 +19,13 @@
<div class="container-fluid">
<div class="row">
- <div class="col-lg-4">
- <div class="panel panel-default">
- <div class="panel-heading clearfix">
+ <div class="col-12 col-lg-5">
+ <div class="card mb-4">
+ <div class="card-header d-flex">
<h4 style="display: inline;">Overview</h4>
- <a data-toggle="collapse" data-target="#panel_overview" class="btn pull-right" style="line-height: 1;">Expand</a>
+ <button data-toggle="collapse" data-target="#panel_overview" class="btn btn-outline-secondary ml-auto">Expand</button>
</div>
- <div class="panel-body collapse in" id="panel_overview">
+ <div class="card-body collapse show" id="panel_overview">
<table class="table">
<tr>
<td>Purpose</td>
@@ -60,12 +60,12 @@
</div>
<div class="row">
<div class="col-lg-12">
- <div class="panel panel-default">
- <div class="panel-heading clearfix">
+ <div class="card">
+ <div class="card-header d-flex">
<h4 style="display: inline;">Pod</h4>
- <a data-toggle="collapse" data-target="#pod_panel" class="btn pull-right" style="line-height: 1;" >Expand</a>
+ <button data-toggle="collapse" data-target="#pod_panel" class="btn btn-outline-secondary ml-auto">Expand</button>
</div>
- <div class="panel-body pod_panel collapse in" id="pod_panel">
+ <div class="card-body collapse show" id="pod_panel">
<table class="table">
{% for host in booking.resource.hosts.all %}
<tr>
@@ -177,14 +177,14 @@
</div>
</div>
</div>
- <div class="col-lg-8">
- <div class="panel panel-default">
- <div class="panel-heading clearfix">
+ <div class="col">
+ <div class="card mb-4">
+ <div class="card-header d-flex">
<h4 style="display: inline;">Deployment Progress</h4>
<p style="display: inline; margin-left: 10px;"> These are the different tasks that have to be completed before your deployment is ready</p>
- <a data-toggle="collapse" data-target="#panel_tasks" class="btn pull-right" style="line-height: 1;" >Expand</a>
+ <button data-toggle="collapse" data-target="#panel_tasks" class="btn btn-outline-secondary ml-auto">Expand</button>
</div>
- <div class="panel-body collapse in" id="panel_tasks">
+ <div class="card-body collapse show" id="panel_tasks">
<table class="table">
<style>
.progress {
@@ -269,15 +269,15 @@
</div>
</div>
<div class="row">
- <div class="col-lg-8">
- <div class="panel panel-default">
- <div class="panel-heading clearfix">
+ <div class="col">
+ <div class="card">
+ <div class="card-header d-flex">
<h4 style="display: inline;">PDF</h4>
- <a data-toggle="collapse" data-target="#pdf_panel" class="btn pull-right" style="line-height: 1;" >Expand</a>
+ <button data-toggle="collapse" data-target="#pdf_panel" class="btn btn-outline-secondary ml-auto">Expand</button>
</div>
- <div class="panel-body collapse in" id="pdf_panel" style="padding: 0px;">
+ <div class="card-body collapse show" id="pdf_panel" style="padding: 0px;">
<pre class="prettyprint lang-yaml" style="margin: 0px; padding: 15px; border: none;">
-{{pdf}}
+ {{pdf}}
</pre>
</div>
</div>
diff --git a/dashboard/src/templates/booking/booking_list.html b/dashboard/src/templates/booking/booking_list.html
index a245450..591ecc9 100644
--- a/dashboard/src/templates/booking/booking_list.html
+++ b/dashboard/src/templates/booking/booking_list.html
@@ -1,44 +1,38 @@
{% extends "base.html" %}
{% load staticfiles %}
-{% load bootstrap3 %}
+{% load bootstrap4 %}
{% block extrahead %}
{{ block.super }}
<!-- DataTables CSS -->
- <link href="{% static "bower_components/datatables-plugins/integration/bootstrap/3/dataTables.bootstrap.css" %}"
+ <link href="{% static "bower_components/datatables.net-bs4/css/dataTables.bootstrap4.min.css" %}"
rel="stylesheet">
<!-- DataTables Responsive CSS -->
- <link href="{% static "bower_components/datatables-responsive/css/dataTables.responsive.css" %}"
+ <link href="{% static "bower_components/datatables.net-responsive-bs4/css/responsive.bootstrap4.min.css" %}"
rel="stylesheet">
{% endblock extrahead %}
{% block content %}
<div class="row">
- <div class="panel-body">
- <div class="dataTables_wrapper">
- <table class="table table-striped table-bordered table-hover" id="table"
- cellspacing="0"
- width="100%">
- {% include "booking/booking_table.html" %}
- </table>
- </div>
- <!-- /.table-responsive -->
- <!-- /.panel-body -->
- <!-- /.panel -->
+ <div class="col">
+ <div class="panel-body">
+ <div class="dataTables_wrapper">
+ <table class="table table-striped table-bordered table-hover" id="table"
+ cellspacing="0"
+ width="100%">
+ {% include "booking/booking_table.html" %}
+ </table>
+ </div>
+ </div>
</div>
- <!-- /.col-lg-12 -->
</div>
{% endblock content %}
{% block extrajs %}
<!-- DataTables JavaScript -->
- <link href="{% static "bower_components/datatables-plugins/integration/bootstrap/3/dataTables.bootstrap.css" %}"
- rel="stylesheet">
-
-
- <script src={% static "bower_components/datatables/media/js/jquery.dataTables.min.js" %}></script>
- <script src={% static "bower_components/datatables-plugins/integration/bootstrap/3/dataTables.bootstrap.min.js" %}></script>
+ <script src={% static "bower_components/datatables.net/js/jquery.dataTables.min.js" %}></script>
+ <script src={% static "bower_components/datatables.net-bs4/js/dataTables.bootstrap4.min.js" %}></script>
<script type="text/javascript">
$(document).ready(function () {
diff --git a/dashboard/src/templates/booking/quick_deploy.html b/dashboard/src/templates/booking/quick_deploy.html
index 2fbd035..07f3d89 100644
--- a/dashboard/src/templates/booking/quick_deploy.html
+++ b/dashboard/src/templates/booking/quick_deploy.html
@@ -1,6 +1,6 @@
{% extends "base.html" %}
{% load staticfiles %}
-{% load bootstrap3 %}
+{% load bootstrap4 %}
{% block content %}
<style>
.grid_container {
@@ -79,77 +79,55 @@
</div>
</div>
<script type="text/javascript">
- var normalize = function(data)
- {
- //converts the top level keys in data to map to lists
- var normalized = {}
- for( var key in data ){
- normalized[key] = [];
- for( var subkey in data[key] ){
- normalized[key].push(data[key][subkey]);
- }
- }
- return normalized;
- }
- var update_page_contents = function(response)
- {
- document.open();
- document.write(response);
- document.close();
- }
- //form hamdler code
- submit_form = function()
+ function submit_form()
{
- //altered from initial prototype: form submits automatically,
- //but needs formatting for multiple select field
- var data = normalize(result);
- data = JSON.stringify(data);
- document.getElementById("filter_field").value = data;
+ //formats data for form submission
+ multi_filter_widget.finish();
}
- var sup_image_dict = {{ image_filter|safe }};
- var sup_installer_dict = {{ installer_filter|safe }};
- var sup_scenario_dict = {{ scenario_filter|safe }};
-
- function imageHider() {
- var data = normalize(result);
- var drop = document.getElementById("id_image");
+ function hide_dropdown(drop_id) {
+ var drop = document.getElementById(drop_id);
+ //select 'blank' option
for( var i=0; i < drop.length; i++ )
{
if ( drop.options[i].text == '---------' )
- {
drop.selectedIndex = i;
- }
}
+ //cross browser hide children
$('#id_image').children().hide();
-
for( var i = 0; i < drop.childNodes.length; i++ )
{
drop.childNodes[i].disabled = true; // closest we can get on safari to hiding it outright
}
+ }
+
+ function get_selected_value(key){
+ for( var attr in multi_filter_widget.result[key] ){
+ if(!(attr in {}) )
+ return attr;
+ }
+ return null;
+ }
+
+ var sup_image_dict = {{ image_filter|safe }};
+ var sup_installer_dict = {{ installer_filter|safe }};
+ var sup_scenario_dict = {{ scenario_filter|safe }};
+
+ function imageHider() {
+ var drop = document.getElementById("id_image");
+ hide_dropdown("id_image");
- var empty_map = {}
+ var lab_pk = get_selected_value("lab");
+ var host_pk = get_selected_value("host");
for ( var i=0; i < drop.childNodes.length; i++ )
{
var image_object = sup_image_dict[drop.childNodes[i].value];
if( image_object ) //weed out empty option
{
- var lab_pk = ""
- for( var j in data["labs"][0] )
- {
- if( j in {} ) { continue; }
- else { lab_pk = j; break; }
- }
- var host_pk = "";
- for( var j in data["hosts"][0] )
- {
- if( j in {} ) { continue; }
- else { host_pk = j; break; }
- }
if( image_object.host_profile == host_pk && image_object.lab == lab_pk )
{
drop.childNodes[i].style.display = "inherit";
@@ -180,28 +158,15 @@
document.getElementById('id_installer').addEventListener('change', scenarioHider);
function dropFilter(target, target_filter, master) {
- ob = document.getElementById(target);
+ var dropdown = document.getElementById(target);
- for(var i=0; i<ob.options.length; i++) {
- if ( ob.options[i].text == '---------' ) {
- ob.selectedIndex = i;
- }
- }
+ hide_dropdown(target);
- targ_id = "#" + target;
-
- $(targ_id).children().hide();
-
- for (var i = 0; i < document.getElementById(target).childNodes.length; i++)
- {
- document.getElementById(target).childNodes[i].disabled = true;
- }
var drop = document.getElementById(master);
var opts = target_filter[drop.options[drop.selectedIndex].value];
if (!opts) {
opts = {};
}
- var emptyMap = {}
var map = Object.create(null);
for (var i = 0; i < opts.length; i++) {
@@ -209,34 +174,14 @@
map[j] = true;
}
- for (var i = 0; i < document.getElementById(target).childNodes.length; i++) {
- if (document.getElementById(target).childNodes[i].value in opts && !(document.getElementById(target).childNodes[i].value in emptyMap) ) {
- document.getElementById(target).childNodes[i].style.display = "inherit";
- document.getElementById(target).childNodes[i].disabled = false;
+ for (var i = 0; i < dropdown.childNodes.length; i++) {
+ if (dropdown.childNodes[i].value in opts && !(dropdown.childNodes[i].value in {}) ) {
+ dropdown.childNodes[i].style.display = "inherit";
+ dropdown.childNodes[i].disabled = false;
}
}
}
</script>
<button id="quick_booking_confirm" onclick="submit_form();" class="btn btn-success">Confirm</button>
</form>
-<script>
- //context vars
- var prefill_host_selection = "{{host_select_field_prefill_data|default:""|safe}}";
- var prefill_purpose = "{{prefill_purpose|default:""|safe}}";
- var prefill_project = "{{prefill_project|default:""|safe}}";
- var prefill_hostname = "{{prefill_hostname|default:""|safe}}";
-
- //to handle prefill
- function prefill_host_select_field(data)
- {
- //
- if(data)
- {
- make_selection(data);
- }
- }
-
- //call init functions
- prefill_host_select_field(prefill_host_selection);
-</script>
{% endblock %}
diff --git a/dashboard/src/templates/booking/stats.html b/dashboard/src/templates/booking/stats.html
index 42eebdd..8bc68cd 100644
--- a/dashboard/src/templates/booking/stats.html
+++ b/dashboard/src/templates/booking/stats.html
@@ -41,17 +41,28 @@ function getData(){
{% endblock %}
{% block content %}
- <p>Number of days to plot: </p>
- <div class="form-group">
- <input id="number_days" type="number" class="form-control" min="1" step="1" style="display:inline;width:200px"/>
- <button class="btn btn-primary" onclick="getData();" style="display:inline;">Submit</button>
- </div>
- <div id="all_graph_container">
- <div id="booking_graph_wrapper">
- <div id="booking_graph_container"/>
+ <div class="container-fluid">
+ <div class="row">
+ <div class="col">
+ <p>Number of days to plot: </p>
+ <div class="form-group">
+ <input id="number_days" type="number" class="form-control" min="1" step="1" style="display:inline;width:200px"/>
+ <button class="btn btn-primary" onclick="getData();" style="display:inline;">Submit</button>
+ </div>
+ </div>
</div>
- <div id="user_graph_wrapper" >
- <div id="user_graph_container"/>
+ <div class="row">
+ <div class="col-12 col-md-10">
+ <!-- These graphs do NOT get redrawn when the browser size is changed -->
+ <div id="all_graph_container border" class="mw-100">
+ <div id="booking_graph_wrapper">
+ <div id="booking_graph_container"/>
+ </div>
+ <div id="user_graph_wrapper">
+ <div id="user_graph_container"/>
+ </div>
+ </div>
+ </div>
</div>
</div>
<script>
diff --git a/dashboard/src/templates/booking/steps/booking_confirm.html b/dashboard/src/templates/booking/steps/booking_confirm.html
index 9c7e951..40c30a9 100644
--- a/dashboard/src/templates/booking/steps/booking_confirm.html
+++ b/dashboard/src/templates/booking/steps/booking_confirm.html
@@ -1,7 +1,7 @@
{% extends "workflow/viewport-element.html" %}
{% load staticfiles %}
-{% load bootstrap3 %}
+{% load bootstrap4 %}
{% block content %}
diff --git a/dashboard/src/templates/booking/steps/booking_meta.html b/dashboard/src/templates/booking/steps/booking_meta.html
index cdd7834..710d4ee 100644
--- a/dashboard/src/templates/booking/steps/booking_meta.html
+++ b/dashboard/src/templates/booking/steps/booking_meta.html
@@ -1,7 +1,7 @@
{% extends "workflow/viewport-element.html" %}
{% load staticfiles %}
-{% load bootstrap3 %}
+{% load bootstrap4 %}
{% block content %}
@@ -10,6 +10,17 @@
padding: 5%;
}
+ .bkcontrib_panel {
+ display: flex;
+ flex-direction: column;
+ }
+
+ .bkcontrib_panel > .form-group {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ }
+
.panel{
padding: 5%;
/*border: solid 1px black;*/
@@ -52,7 +63,7 @@
</div>
<div class="panel panel_center">
</div>
- <div class="panel">
+ <div class="panel bkcontrib_panel">
<p>You may add collaborators on your booking to share resources with coworkers.</p>
{% bootstrap_field form.users label="Collaborators" %}
</div>
diff --git a/dashboard/src/templates/booking/steps/resource_select.html b/dashboard/src/templates/booking/steps/resource_select.html
index 7ccceb3..382316f 100644
--- a/dashboard/src/templates/booking/steps/resource_select.html
+++ b/dashboard/src/templates/booking/steps/resource_select.html
@@ -1,7 +1,7 @@
{% extends "workflow/viewport-element.html" %}
{% load staticfiles %}
-{% load bootstrap3 %}
+{% load bootstrap4 %}
{% block content %}
diff --git a/dashboard/src/templates/booking/steps/swconfig_select.html b/dashboard/src/templates/booking/steps/swconfig_select.html
index 15c79d8..60a0df7 100644
--- a/dashboard/src/templates/booking/steps/swconfig_select.html
+++ b/dashboard/src/templates/booking/steps/swconfig_select.html
@@ -1,7 +1,7 @@
{% extends "workflow/viewport-element.html" %}
{% load staticfiles %}
-{% load bootstrap3 %}
+{% load bootstrap4 %}
{% block content %}
diff --git a/dashboard/src/templates/config_bundle/steps/assign_host_roles.html b/dashboard/src/templates/config_bundle/steps/assign_host_roles.html
index 3ba7665..b87a17f 100644
--- a/dashboard/src/templates/config_bundle/steps/assign_host_roles.html
+++ b/dashboard/src/templates/config_bundle/steps/assign_host_roles.html
@@ -1,6 +1,6 @@
{% extends "config_bundle/steps/table_formset.html" %}
-{% load bootstrap3 %}
+{% load bootstrap4 %}
{% block table %}
<thead>
diff --git a/dashboard/src/templates/config_bundle/steps/assign_network_roles.html b/dashboard/src/templates/config_bundle/steps/assign_network_roles.html
index 0e887d6..aa1df44 100644
--- a/dashboard/src/templates/config_bundle/steps/assign_network_roles.html
+++ b/dashboard/src/templates/config_bundle/steps/assign_network_roles.html
@@ -1,6 +1,6 @@
{% extends "config_bundle/steps/table_formset.html" %}
-{% load bootstrap3 %}
+{% load bootstrap4 %}
{% block table %}
<thead>
diff --git a/dashboard/src/templates/config_bundle/steps/config_software.html b/dashboard/src/templates/config_bundle/steps/config_software.html
index b181c7e..68417bc 100644
--- a/dashboard/src/templates/config_bundle/steps/config_software.html
+++ b/dashboard/src/templates/config_bundle/steps/config_software.html
@@ -1,7 +1,7 @@
{% extends "workflow/viewport-element.html" %}
{% load staticfiles %}
-{% load bootstrap3 %}
+{% load bootstrap4 %}
{% block content %}
diff --git a/dashboard/src/templates/config_bundle/steps/define_software.html b/dashboard/src/templates/config_bundle/steps/define_software.html
index ba1ff34..87e5997 100644
--- a/dashboard/src/templates/config_bundle/steps/define_software.html
+++ b/dashboard/src/templates/config_bundle/steps/define_software.html
@@ -1,6 +1,6 @@
{% extends "config_bundle/steps/table_formset.html" %}
-{% load bootstrap3 %}
+{% load bootstrap4 %}
{% block table %}
<thead>
diff --git a/dashboard/src/templates/config_bundle/steps/pick_installer.html b/dashboard/src/templates/config_bundle/steps/pick_installer.html
index 3b170d9..31a06de 100644
--- a/dashboard/src/templates/config_bundle/steps/pick_installer.html
+++ b/dashboard/src/templates/config_bundle/steps/pick_installer.html
@@ -1,7 +1,7 @@
{% extends "workflow/viewport-element.html" %}
{% load staticfiles %}
-{% load bootstrap3 %}
+{% load bootstrap4 %}
{% block content %}
diff --git a/dashboard/src/templates/config_bundle/steps/table_formset.html b/dashboard/src/templates/config_bundle/steps/table_formset.html
index ad2c5a3..18edc72 100644
--- a/dashboard/src/templates/config_bundle/steps/table_formset.html
+++ b/dashboard/src/templates/config_bundle/steps/table_formset.html
@@ -1,15 +1,16 @@
{% extends "workflow/viewport-element.html" %}
{% load staticfiles %}
-{% load bootstrap3 %}
+{% load bootstrap4 %}
{% block extrahead %}
<!-- DataTables CSS -->
- <link href="{% static "bower_components/datatables-plugins/integration/bootstrap/3/dataTables.bootstrap.css" %}"
+ <link href="{% static "bower_components/datatables.net-bs4/css/dataTables.bootstrap4.min.css" %}"
rel="stylesheet">
<!-- DataTables Responsive CSS -->
- <link href="{% static "bower_components/datatables-responsive/css/dataTables.responsive.css" %}" rel="stylesheet">
+ <link href="{% static "bower_components/datatables.net-responsive-bs4/css/responsive.bootstrap4.min.css" %}"
+ rel="stylesheet">
{% endblock extrahead %}
{% block content %}
@@ -42,8 +43,8 @@
{{ block.super }}
<!-- DataTables JavaScript -->
- <script src={% static "bower_components/datatables/media/js/jquery.dataTables.min.js" %}></script>
- <script src={% static "bower_components/datatables-plugins/integration/bootstrap/3/dataTables.bootstrap.min.js" %}></script>
+ <script src={% static "bower_components/datatables.net/js/jquery.dataTables.min.js" %}></script>
+ <script src={% static "bower_components/datatables.net-bs4/js/dataTables.bootstrap4.min.js" %}></script>
<script src={% static "js/dataTables-sort.js" %}></script>
diff --git a/dashboard/src/templates/dashboard/genericselect.html b/dashboard/src/templates/dashboard/genericselect.html
index 1e9a29a..441d8dc 100644
--- a/dashboard/src/templates/dashboard/genericselect.html
+++ b/dashboard/src/templates/dashboard/genericselect.html
@@ -1,7 +1,7 @@
{% extends "workflow/viewport-element.html" %}
{% load staticfiles %}
-{% load bootstrap3 %}
+{% load bootstrap4 %}
{% block content %}
diff --git a/dashboard/src/templates/dashboard/lab_detail.html b/dashboard/src/templates/dashboard/lab_detail.html
index 7938e86..336b32e 100644
--- a/dashboard/src/templates/dashboard/lab_detail.html
+++ b/dashboard/src/templates/dashboard/lab_detail.html
@@ -9,12 +9,12 @@
{% block content %}
<div class="row">
<div class="col-lg-4">
- <div class="panel panel-default">
- <div class="panel-heading clearfix">
- <h4 style="display: inline;">Lab Profile</h4>
- <a data-toggle="collapse" data-target="#panel_overview" class="btn pull-right" style="line-height: 1;" >Expand</a>
+ <div class="card my-2">
+ <div class="card-header d-flex">
+ <h4>Lab Profile</h4>
+ <button class="btn btn-outline-secondary ml-auto" data-toggle="collapse" data-target="#panel_overview">Expand</button>
</div>
- <div class="panel-body" id="panel_overview">
+ <div id="panel_overview" class="card-body collapse show">
<table class="table">
<tr>
<td>Lab Name: </td><td>{{lab.name}}</td>
@@ -50,19 +50,18 @@
</table>
</div>
</div>
- <div class="panel panel-default">
- <div class="panel-heading clearfix">
- <h4 style="display: inline;">Host Profiles</h4>
-
- <a data-toggle="collapse" data-target="#profile_panel" class="btn pull-right" style="line-height: 1;" >Expand</a>
+ <div class="card my-2">
+ <div class="card-header d-flex">
+ <h4 class="d-inline-block">Host Profiles</h4>
+ <button data-toggle="collapse" data-target="#profile_panel" class="btn btn-outline-secondary ml-auto" style="line-height: 1;" >Expand</button>
</div>
- <div class="panel-body pod_panel" id="profile_panel">
+ <div id="profile_panel" class="card-body collapse show">
<table class="table">
{% for profile in hostprofiles %}
<tr>
<td>{{profile.name}}</td>
<td>{{profile.description}}</td>
- <td><a href="/resource/profiles/{{ profile.id }}" class="btn btn-primary">Profile</a></td>
+ <td><a href="/resource/profiles/{{ profile.id }}" class="btn btn-info">Profile</a></td>
</tr>
{% endfor %}
</table>
@@ -70,31 +69,30 @@
</div>
- <div class="panel panel-default">
- <div class="panel-heading clearfix">
+ <div class="card my-2">
+ <div class="card-header d-flex">
<h4 style="display: inline;">Networking Capabilities</h4>
- <a data-toggle="collapse" data-target="#network_panel" class="btn pull-right" style="line-height: 1;" >Expand</a>
+ <button data-toggle="collapse" data-target="#network_panel" class="btn btn-outline-secondary ml-auto" style="line-height: 1;" >Expand</button>
</div>
- <div class="panel-body" id="network_panel">
-
- <table class="table">
- <tr>
- <td>Block Size: (number of VLANs allowed per deployment)</td><td>{{lab.vlan_manager.block_size}}</td>
- </tr>
- <tr>
- <td>Overlapping Vlans Allowed (user can pick which VLANs they wish to use): </td>
- <td>{{lab.vlan_manager.allow_overlapping}}</td>
- </tr>
- </table>
+ <div class="card-body collapse show" id="network_panel">
+ <table class="table">
+ <tr>
+ <td>Block Size: (number of VLANs allowed per deployment)</td><td>{{lab.vlan_manager.block_size}}</td>
+ </tr>
+ <tr>
+ <td>Overlapping Vlans Allowed (user can pick which VLANs they wish to use): </td>
+ <td>{{lab.vlan_manager.allow_overlapping}}</td>
+ </tr>
+ </table>
</div>
</div>
- <div class="panel panel-default">
- <div class="panel-heading clearfix">
- <h4 style="display: inline;">Images</h4>
- <a data-toggle="collapse" data-target="#image_panel" class="btn pull-right" style="line-height: 1;" >Expand</a>
+ <div class="card my-2">
+ <div class="card-header d-flex">
+ <h4>Images</h4>
+ <button data-toggle="collapse" data-target="#image_panel" class="btn btn-outline-secondary ml-auto">Expand</button>
</div>
- <div class="panel-body" id="image_panel">
+ <div class="card-body collapse show" id="image_panel">
<table class="table">
<tr>
<th>Name</th>
@@ -116,14 +114,13 @@
</div>
<div class="col-lg-8">
- <div class="panel panel-default">
- <div class="panel-heading clearfix">
- <h4 style="display: inline;">Lab Hosts</h4>
- <p style="display: inline; margin-left: 10px;"></p>
- <a data-toggle="collapse" data-target="#lab_hosts_panel" class="btn pull-right" style="line-height: 1;" >Expand</a>
+ <div class="card my-2">
+ <div class="card-header d-flex">
+ <h4>Lab Hosts</h4>
+ <button data-toggle="collapse" data-target="#lab_hosts_panel" class="btn btn-outline-secondary ml-auto">Expand</button>
</div>
- <div class="panel-body" id="lab_hosts_panel">
+ <div class="card-body collapse show" id="lab_hosts_panel">
<table class="table">
<tr>
<th>Name</th>
diff --git a/dashboard/src/templates/dashboard/lab_list.html b/dashboard/src/templates/dashboard/lab_list.html
index c459dd9..9cde80c 100644
--- a/dashboard/src/templates/dashboard/lab_list.html
+++ b/dashboard/src/templates/dashboard/lab_list.html
@@ -1,26 +1,28 @@
{% extends "base.html" %}
{% block content %}
- <h2>Labs</h2>
- <div class="card_container">
+<h2>Labs</h2>
+<div class="card_container">
{% for lab in labs %}
- <div class="detail_card">
- <div>
- <h3>{{lab.name}}</h3>
- <ul class="list-group">
- <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>
- {% if lab.status == 0 %}
- <li class="list-group-item">status: Up</li>
- {% elif lab.status == 100 %}
- <li class="list-group-item">status: Down for Maintenance</li>
- {% elif lab.status == 200 %}
- <li class="list-group-item">status: Down</li>
- {% endif %}
- </ul>
- </div>
- <a class="btn btn-primary" href="/lab/{{lab.name}}/">Details</a>
+ <div class="card">
+ <div class="card-header">
+ <h3 class="mt-2">{{lab.name}}</h3>
+ </div>
+ <div class="p-4">
+ <ul class="list-group">
+ <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>
+ {% if lab.status == 0 %}
+ <li class="list-group-item">status: Up</li>
+ {% elif lab.status == 100 %}
+ <li class="list-group-item">status: Down for Maintenance</li>
+ {% elif lab.status == 200 %}
+ <li class="list-group-item">status: Down</li>
+ {% endif %}
+ </ul>
+ <a class="btn btn-primary mt-4 w-100" href="/lab/{{lab.name}}/">Details</a>
</div>
- {% endfor %}
</div>
-{% endblock %}
+ {% endfor %}
+</div>
+{% endblock %} \ No newline at end of file
diff --git a/dashboard/src/templates/dashboard/landing.html b/dashboard/src/templates/dashboard/landing.html
index fb75d5f..e6a235f 100644
--- a/dashboard/src/templates/dashboard/landing.html
+++ b/dashboard/src/templates/dashboard/landing.html
@@ -2,27 +2,30 @@
{% load staticfiles %}
{% block content %}
- <div class="" style="text-align: center;">
- {% if not request.user.is_anonymous %}
- {% if not request.user.userprofile.ssh_public_key %}
- <h4 style="display: inline; text-align: center; border: 3px solid red; padding: 10px; border-radius: 10000px; height: 40px;">
- Warning: you need to upload an ssh key under <a href="/accounts/settings">account settings</a> if you wish to log into the servers you book
- </h4>
- {% endif %}
- {% else %}
- {% endif %}
+<div class="" style="text-align: center;">
+ {% if not request.user.is_anonymous %}
+ {% if not request.user.userprofile.ssh_public_key %}
+ <div class="alert alert-danger" role="alert">
+ Warning: you need to upload an ssh key under <a href="/accounts/settings">account settings</a> if you wish to
+ log into the servers you book
</div>
+ {% endif %}
+ {% else %}
+ {% endif %}
+</div>
{% csrf_token %}
<style>
- .wf_create{
+ .wf_create {
display: inline-block;
text-align: center;
}
- .wf_create_div{
+
+ .wf_create_div {
text-align: center;
}
- .hidden_form{
+
+ .hidden_form {
display: none;
}
@@ -39,81 +42,117 @@
display: grid;
grid-template-columns: 1fr 30px 1fr;
}
+
.grid_panel {
padding: 30px;
}
+
.btn-primary {
margin: 10px;
}
+
h2 {
border-bottom: 1px solid #cccccc;
}
- h1 {
- }
-</style>
-<div class="landing_container">
- <div class="info_panel grid_panel">
- <h2>About Us:</h2>
- <p>The Lab as a Service (LaaS) project aims to help in the development and testing of LFN projects such as OPNFV by hosting hardware and providing access to the community. Currently, the only participating lab is the University of New Hampshire Interoperability Lab (UNH-IOL).</p>
- <p>To get started, you can request access to a server at the right. PTL's have the ability to design and book a whole block of servers with customized layer2 networks (e.g. a Pharos Pod). Read more here: <a href="https://wiki.opnfv.org/display/INF/Lab+as+a+Service+2.0">LaaS Wiki</a></p>
+ h1 {}
+</style>
+<div class="container-fluid">
+ <div class="row">
+ <!-- About us -->
+ <div class="col-12 col-lg-6 mb-4">
+ <h2>About Us:</h2>
+ <p>The Lab as a Service (LaaS) project aims to help in the development and testing of LFN projects such as
+ OPNFV
+ by hosting hardware and providing access to the community. Currently, the only participating lab is the
+ University of New Hampshire Interoperability Lab (UNH-IOL).</p>
+ <p>To get started, you can request access to a server at the right. PTL's have the ability to design and
+ book a
+ whole block of servers with customized layer2 networks (e.g. a Pharos Pod). Read more here: <a
+ href="https://wiki.opnfv.org/display/INF/Lab+as+a+Service+2.0">LaaS Wiki</a></p>
+ </div>
+ <!-- Get started -->
+ <div class="col-12 col-lg-6 mb-4">
+ <h2>Get Started:</h2>
+ {% if request.user.is_anonymous %}
+ <h4 style="text-align:center;">To get started, please log in with your <a href="/accounts/login">Linux
+ Foundation Jira account</a></h4>
+ {% else %}
+ <p>To get started, book a server below:</p>
+ <a class="wf_create btn btn-primary"
+ style="display: flex; flex-direction: column; justify-content: center; margin: 20px; height: 100pt; vertical-align: middle; text-align: center; color: #FFF;"
+ href="/booking/quick/">
+ <p style="font-size: xx-large">Book a Server</p>
+ </a>
+ <p>PTLs can use our advanced options to book multi-node pods. If you are a PTL, you may use the options
+ below:
+ </p>
+ <div class='container'>
+ <div class="row">
+ <div class="col-12 col-xl-4">
+ <button class="wf_create btn btn-primary w-100" onclick="cwf(0)">Book a Pod</button>
+ </div>
+ <div class="col-12 col-xl-4">
+ <button class="wf_create btn btn-primary w-100" onclick="cwf(1)">Design a Pod</button>
+ </div>
+ <div class="col-12 col-xl-4">
+ <button class="wf_create btn btn-primary w-100" onclick="cwf(2)">Configure a Pod</button>
+ </div>
+ </div>
+ {% endif %}
+ </div>
+ </div>
+ <!-- Returning users -->
{% if not request.user.is_anonymous %}
- <h2 style="margin-top: 50px;">Returning Users:</h2>
- <p>If you're a returning user, some of the following options may be of interest:</p>
- <button class="wf_create btn btn-primary" onclick="cwf(3)">Snapshot a Host</button>
- <a class="wf_create btn btn-primary" href="{% url 'account:my-bookings' %}">My Bookings</a>
- {% if manager == True %}
- <button class="wf_continue btn btn-primary" onclick="continue_wf()">Continue Unfinished Workflow</button>
- {% endif %}
- {% endif %}
- </div>
- <div class="">
- </div>
- <div class="actions_panel grid_panel">
- <h2>Get Started:</h2>
- {% if request.user.is_anonymous %}
- <h4 style="text-align:center;">To get started, please log in with your <a href="/accounts/login">Linux Foundation Jira account</a></h4>
- {% else %}
- <p>To get started, book a server below:</p>
- <a class="wf_create btn btn-primary" style="display: flex; flex-direction: column; justify-content: center; margin: 20px; height: 100pt; vertical-align: middle; text-align: center; color: #FFF;" href="/booking/quick/"><p style="font-size: xx-large">Book a Server</p></a>
- <p>PTLs can use our advanced options to book multi-node pods. If you are a PTL, you may use the options below:</p>
- <div class='wf_create_div'>
-
- <button class="wf_create btn btn-primary" onclick="cwf(0)">Book a Pod</button>
- <button class="wf_create btn btn-primary" onclick="cwf(1)">Design a Pod</button>
- <button class="wf_create btn btn-primary" onclick="cwf(2)">Configure a Pod</button>
- {% endif %}
+ <div class="col-12 col-lg-6 offset-lg-6 mb-4 mt-lg-4">
+ <h2 class="ht-4">Returning Users:</h2>
+ <p>If you're a returning user, some of the following options may be of interest:</p>
+ <div class="container">
+ <div class="row">
+ <div class="col-12 col-xl-4">
+ <button class="wf_create btn btn-primary w-100" onclick="cwf(3)">Snapshot a Host</button>
+ </div>
+ <div class="col-12 col-xl-4">
+ <a class="wf_create btn btn-primary w-100" href="{% url 'account:my-bookings' %}">My
+ Bookings</a>
+ </div>
+ {% if manager == True %}
+ <div class="col-12 col-xl-4">
+ <button class="wf_continue btn btn-primary w-100" onclick="continue_wf()">Resume
+ Workflow</button>
+ </div>
+ {% endif %}
+ </div>
+ </div>
</div>
+ {% endif %}
</div>
</div>
-
-
-
-
<script type="text/javascript">
- function cwf(type)
- {
+ function cwf(type) {
$.ajax({
type: "POST",
url: "/",
- data: {"create":type},
- beforeSend: function(request) {
+ data: {
+ "create": type
+ },
+ beforeSend: function (request) {
request.setRequestHeader("X-CSRFToken",
- $('input[name="csrfmiddlewaretoken"]').val()
+ $('input[name="csrfmiddlewaretoken"]').val()
);
}
}).done(function (data) {
window.location.replace("/wf/");
- }).fail(function(jqxHR, textstatus) {
- alert("Something went wrong...");});
+ }).fail(function (jqxHR, textstatus) {
+ alert("Something went wrong...");
+ });
}
- function continue_wf()
- {
+
+ function continue_wf() {
window.location.replace("/wf/");
}
-
</script>
<div class="hidden_form" id="form_div">
@@ -130,4 +169,4 @@
{% block vport_comm %}
{% endblock %}
-{% endblock content %}
+{% endblock content %} \ No newline at end of file
diff --git a/dashboard/src/templates/dashboard/multiple_select_filter_widget.html b/dashboard/src/templates/dashboard/multiple_select_filter_widget.html
index 536fdcc..4302543 100644
--- a/dashboard/src/templates/dashboard/multiple_select_filter_widget.html
+++ b/dashboard/src/templates/dashboard/multiple_select_filter_widget.html
@@ -1,9 +1,13 @@
+<script src="/static/js/dashboard.js">
+</script>
+
<style>
.object_class_wrapper {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
border: 0px;
}
+
.class_grid_wrapper {
border: 0px;
text-align: center;
@@ -20,112 +24,109 @@
display: grid;
grid-template-columns: 1fr 1fr;
}
+
.grid-item {
cursor: pointer;
- border:1px solid #cccccc;
+ border: 1px solid #cccccc;
border-radius: 5px;
- margin:20px;
+ margin: 20px;
height: 200px;
padding: 7px;
- transition-property: box-shadow, background-color;
- transition-duration: .2s;
+ transition: border-color ease-in-out .1s,box-shadow ease-in-out .1s;
+ box-shadow: 0 1px 1px rgba(0,0,0,.075);
+
+ display: flex;
+ flex-direction: column;
}
-.grid-item:hover {
- box-shadow: 0px 0px 7px 0px rgba(0,0,0,0.45);
- transition-property: box-shadow;
- transition-duration: .2s;
+.grid-item > .btn:active, .grid-item > .btn:focus {
+ outline: none; !important;
+ box-shadow: none;
+}
+.grid-item-description {
+ flex: 1;
}
.selected_node {
- box-shadow: 0px 0px 4px 0px rgba(0,0,0,0.45);
- background-color: #fff;
- transition-property: background-color;
- transition-duration: .2s;
+ border-color: #40c640;
+ 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;
+}
+
+.grid-item:hover:not(.selected_node):not(.disabled_node) {
+ box-shadow: 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(100, 100, 100, 0.3);
+ transition: border-color ease-in-out .1s,box-shadow ease-in-out .1s;
}
.disabled_node {
cursor: not-allowed;
background-color: #EFEFEF;
- transition-property: box-shadow;
- transition-duration: .2s;
- border: 1px solid #ccc;
}
-.disabled_node:hover {
-}
+.disabled_node:hover {}
.cleared_node {
background-color: #FFFFFF;
}
-.grid-item-header
-{
+.grid-item-header {
font-weight: bold;
font-size: 20px;
margin-top: 10px;
}
-#dropdown_wrapper > div > h5 {
- margin: 12px;
- display: inline-block;
- vertical-align: middle;
+.dropdown_item {
+ border: 1px;
+ border-style: solid;
+ border-color: lightgray;
+ border-radius: 5px;
+ margin: 20px;
+ padding: 2px;
+ grid-column: 1;
+ display: grid;
+ grid-template-columns: 1fr 3fr 1fr;
+ justify-items: center;
}
-#dropdown_wrapper > div > button {
- padding: 7px;
+.dropdown_item > button {
margin: 2px;
- float: right;
- width: 80px;
+ justify-self: end;
}
-#dropdown_wrapper > div > input {
- padding: 7px;
- margin: 2px;
- float: right;
- width: 300px;
- width: calc(100% - 240px);
+
+.dropdown_item > h5 {
+ margin: auto;
}
-#dropdown_wrapper > div {
- border:2px;
- border-style:none;
- border-color:black;
- border-radius: 5px;
- margin:20px;
- padding: 2px;
- box-shadow: 0px 0px 7px 0px rgba(0,0,0,0.75);
- transition-property: box-shadow, background-color;
- transition-duration: .2s;
- display: inline-block;
- vertical-align: middle;
+.dropdown_item > input {
+ padding: 7px;
+ margin: 2px;
+ width: 90%;
}
#dropdown_wrapper {
display: grid;
- grid-template-columns: 3fr 5fr;
+ grid-template-columns: 4fr 5fr;
}
-
</style>
+
<input name="filter_field" id="filter_field" type="hidden"/>
<div id="grid_wrapper" class="grid_wrapper">
-{% for object_class, object_list in filter_objects %}
+{% for object_class, object_list in display_objects %}
<div class="class_grid_wrapper">
<div style="display:inline-block;margin:auto">
<h4>{{object_class}}</h4>
</div>
<div id="{{object_class}}" class="object_class_wrapper">
{% for obj in object_list %}
- <div id="{{ obj.id|default:'not_provided' }}" class="grid-item">
+ <div id="{{ obj.id|default:'not_provided' }}" class="grid-item" onclick="multi_filter_widget.processClick(
+ '{{obj.id}}');">
<p class="grid-item-header">{{obj.name}}</p>
<p class="grid-item-description">{{obj.description}}</p>
- <button type="button" class="btn btn-success grid-item-select-btn" onclick="processClick(
- '{{obj.id}}',
- {% if obj.multiple %}true
- {% else %}false
- {% endif %});">{% if obj.multiple %}Add{% else %}Select{% endif %}</button>
+ <button type="button" class="btn btn-success grid-item-select-btn">
+ {% if obj.multiple %}Add{% else %}Select{% endif %}
+ </button>
</div>
- <input type="hidden" name="{{obj.id}}_selected" value="false"/>
{% endfor %}
</div>
</div>
@@ -134,310 +135,15 @@
<div id="dropdown_wrapper">
</div>
-
<script>
-var initialized = false;
-var mapping = {{ mapping|safe }};
-var filter_items = {{ filter_items|safe }};
-var result = {};
-var selection = {{selection_data|default_if_none:"null"|safe}};
-var dropdown_count = 0;
-
-{% if selection_data %}
-make_selection({{selection_data|safe}});
-{% endif %}
-
-function make_selection( selection_data ){
- if(!initialized) {
- filter_field_init();
- }
- for(var k in selection_data) {
- selected_items = selection_data[k];
- for( var selected_item in selected_items ){
- var node = filter_items[selected_item];
- if(!node['multiple']){
- var input_value = selected_items[selected_item];
- if( input_value != 'false' ) {
- select(node);
- markAndSweep(node);
- }
- var div = document.getElementById(selected_item)
- var inputs = div.parentNode.getElementsByTagName("input")
- var input = div.parentNode.getElementsByTagName("input")[0]
- input.value = input_value;
- updateResult(selected_item);
- } else {
- make_multiple_selection(selected_items, selected_item);
- }
- }
- }
-}
-
-function make_multiple_selection(data, item_class){
- var node = filter_items[item_class];
- select(node);
- markAndSweep(node);
- prepop_data = data[item_class];
- for(var i=0; i<prepop_data.length; i++){
- var div = add_item_prepopulate(node, prepop_data[i]);
- updateObjectResult(div);
- }
-}
-
-function markAndSweep(root){
- for(var nodeId in filter_items) {
- node = filter_items[nodeId];
- node['marked'] = true; //mark all nodes
- //clears grey background of everything
- }
-
- toCheck = [root];
-
- while(toCheck.length > 0){
- node = toCheck.pop();
- if(!node['marked']) {
- //already visited, just continue
- continue;
- }
- node['marked'] = false; //mark as visited
- if(node['follow'] || node == root){ //add neighbors if we want to follow this node (labs)
- var mappingId = node.id
- var neighbors = mapping[mappingId];
- for(var neighId in neighbors) {
- neighId = neighbors[neighId];
- var neighbor = filter_items[neighId];
- toCheck.push(neighbor);
- }
- }
- }
-
- //now remove all nodes still marked
- for(var nodeId in filter_items){
- node = filter_items[nodeId];
- if(node['marked']){
- disable_node(node);
- }
- }
-}
-
-function process(node) {
- if(node['selected']) {
- markAndSweep(node);
- }
- else {
- var selected = []
- //remember the currently selected, then reset everything and reselect one at a time
- for(var nodeId in filter_items) {
- node = filter_items[nodeId];
- if(node['selected']) {
- selected.push(node);
- }
- clear(node);
-
- }
- for(var i=0; i<selected.length; i++) {
- node = selected[i];
- select(node);
- markAndSweep(selected[i]);
- }
- }
-}
-
-function select(node) {
- elem = document.getElementById(node['id']);
- node['selected'] = true;
- elem.classList.remove('cleared_node');
- elem.classList.remove('disabled_node');
- elem.classList.add('selected_node');
- var input = elem.parentNode.getElementsByTagName("input")[0];
- input.disabled = false;
- input.value = true;
-}
-
-function clear(node) {
- elem = document.getElementById(node['id']);
- node['selected'] = false;
- node['selectable'] = true;
- elem.classList.add('cleared_node')
- elem.classList.remove('disabled_node');
- elem.classList.remove('selected_node');
- elem.parentNode.getElementsByTagName("input")[0].disabled = true;
-}
-
-function disable_node(node) {
- elem = document.getElementById(node['id']);
- node['selected'] = false;
- node['selectable'] = false;
- elem.classList.remove('cleared_node');
- elem.classList.add('disabled_node');
- elem.classList.remove('selected_node');
- elem.parentNode.getElementsByTagName("input")[0].disabled = true;
-}
-
-function processClick(id, multiple){
- if(!initialized){
- filter_field_init();
- }
- var element = document.getElementById(id);
- var node = filter_items[id];
- if(!node['selectable']){
- return;
- }
- if(multiple){
- return processClickMultipleObject(node);
- }
- node['selected'] = !node['selected']; //toggle on click
-
- if(node['selected']) {
- select(node);
- } else {
- clear(node);
- }
- process(node);
- updateResult(id);
-}
-
-function processClickMultipleObject(node){
- select(node);
- add_node(node);
- process(node);
-}
-
-function add_node(node){
- return add_item_prepopulate(node, {});
-}
-
-inputs = []
-
-function 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, "");
- checkunique(input);
-}
-
-function checkunique(tocheck)
-{
- val = tocheck.value;
- for( var i = 0; i < inputs.length; i++ )
- {
- if( inputs[i].value == val && inputs[i] != tocheck)
- {
- tocheck.setCustomValidity("All hostnames must be unique");
- tocheck.reportValidity();
- return;
- }
- }
- tocheck.setCustomValidity("");
-}
-
-function add_item_prepopulate(node, prepopulate){
- inputs = [];
- var div = document.createElement("DIV");
- div.class = node['id'];
- div.id = "dropdown_" + dropdown_count;
- dropdown_count++;
- var label = document.createElement("H5");
- label.appendChild(document.createTextNode(node['name']));
- div.appendChild(label);
- button = document.createElement("BUTTON");
- button.type = "button";
- button.appendChild(document.createTextNode("Remove"));
- button.classList.add("btn-danger");
- button.classList.add("btn");
- div.appendChild(button);
- for(var i=0; i<node['forms'].length; i++){
- form = node['forms'][i];
- var input = document.createElement("INPUT");
- input.type = form['type'];
- input.name = form['name'];
- 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 = form['placeholder'];
- inputs.push(input);
- input.onchange = function() { updateObjectResult(div); restrictchars(this); };
- input.oninput = function() { restrictchars(this); };
- if(form['name'] in prepopulate){
- input.value = prepopulate[form['name']];
- }
- div.appendChild(input);
- }
- //add class id to dropdown object
- var hiddenInput = document.createElement("INPUT");
- hiddenInput.type = "hidden";
- hiddenInput.name = "class";
- hiddenInput.value = node['id'];
- div.appendChild(hiddenInput);
- button.onclick = function(){
- remove_dropdown(div.id);
- }
- document.getElementById("dropdown_wrapper").appendChild(div);
- var linebreak = document.createElement("BR");
- document.getElementById("dropdown_wrapper").appendChild(linebreak);
- updateObjectResult(div);
- return div;
-}
-
-function remove_dropdown(id){
- var div = document.getElementById(id);
- var parent = div.parentNode;
- div.parentNode.removeChild(div);
- //checks if we have removed last item in class
- var deselect_class = true;
- var div_inputs = div.getElementsByTagName("input");
- var div_class = div_inputs[div_inputs.length-1].value;
- var result_class = document.getElementById(div_class).parentNode.parentNode.id;
- delete result[result_class][div.id];
- for(var i=0; i<parent.children.length; i++){
- var inputs = parent.children[i].getElementsByTagName("input");
- var object_class = "";
- for(var k=0; k<inputs.length; k++){
- if(inputs[k].name == "class"){
- object_class = inputs[k].value;
- }
- }
- if(object_class == div_class){
- deselect_class = false;
- }
- }
- if(deselect_class){
- clear(filter_items[div_class]);
- }
-}
-
-function updateResult(nodeId){
- if(!initialized){
- filter_field_init();
- }
- if(!filter_items[nodeId]['multiple']){
- var node = document.getElementById(nodeId);
- var value = {}
- value[nodeId] = node.parentNode.getElementsByTagName("input")[0].value;
- result[node.parentNode.id] = {};
- result[node.parentNode.id][nodeId] = value;
- }
-}
-
-function updateObjectResult(parentElem){
- node_type = document.getElementById(parentElem.class).parentNode.id;
- input = {};
- inputs = parentElem.getElementsByTagName("input");
- for(var i in inputs){
- var e = inputs[i];
- input[e.name] = e.value;
- }
- result[node_type][parentElem.id] = input;
-}
+function multipleSelectFilterWidgetEntry() {
+ const graph_neighbors = {{ neighbors|safe }};
+ const filter_items = {{ filter_items|safe }};
+ const initial_value = {{ initial_value|default_if_none:"{}"|safe }};
-function filter_field_init() {
- for(nodeId in filter_items) {
- element = document.getElementById(nodeId);
- node = filter_items[nodeId];
- result[element.parentNode.id] = {}
- }
- initialized = true;
+ //global variable
+ multi_filter_widget = new MultipleSelectFilterWidget(graph_neighbors, filter_items, initial_value);
}
+multipleSelectFilterWidgetEntry();
</script>
diff --git a/dashboard/src/templates/dashboard/resource.html b/dashboard/src/templates/dashboard/resource.html
index 28e7998..f36ee7b 100644
--- a/dashboard/src/templates/dashboard/resource.html
+++ b/dashboard/src/templates/dashboard/resource.html
@@ -7,11 +7,11 @@
<link href="{% static "bower_components/morrisjs/morris.css" %}" rel="stylesheet">
<!-- DataTables CSS -->
- <link href="{% static "bower_components/datatables-plugins/integration/bootstrap/3/dataTables.bootstrap.css" %}"
+ <link href="{% static "bower_components/datatables.net-bs4/css/dataTables.bootstrap4.min.css" %}"
rel="stylesheet">
<!-- DataTables Responsive CSS -->
- <link href="{% static "bower_components/datatables-responsive/css/dataTables.responsive.css" %}"
+ <link href="{% static "bower_components/datatables.net-responsive-bs4/css/responsive.bootstrap4.min.css" %}"
rel="stylesheet">
{% endblock extrahead %}
@@ -23,11 +23,11 @@
{% block extrajs %}
<!-- DataTables JavaScript -->
- <link href="{% static "bower_components/datatables-plugins/integration/bootstrap/3/dataTables.bootstrap.css" %}"
+ <link href="{% static "bower_components/datatables/media/css/dataTables.bootstrap.css" %}"
rel="stylesheet">
- <script src={% static "bower_components/datatables/media/js/jquery.dataTables.min.js" %}></script>
- <script src={% static "bower_components/datatables-plugins/integration/bootstrap/3/dataTables.bootstrap.min.js" %}></script>
+ <script src={% static "bower_components/datatables.net/js/jquery.dataTables.min.js" %}></script>
+ <script src={% static "bower_components/datatables.net-bs4/js/dataTables.bootstrap4.min.js" %}></script>
diff --git a/dashboard/src/templates/dashboard/resource_all.html b/dashboard/src/templates/dashboard/resource_all.html
index 0b0d0d4..fb8cc7e 100644
--- a/dashboard/src/templates/dashboard/resource_all.html
+++ b/dashboard/src/templates/dashboard/resource_all.html
@@ -7,11 +7,11 @@
<link href="{% static "bower_components/morrisjs/morris.css" %}" rel="stylesheet">
<!-- DataTables CSS -->
- <link href="{% static "bower_components/datatables-plugins/integration/bootstrap/3/dataTables.bootstrap.css" %}"
+ <link href="{% static "bower_components/datatables.net-bs4/css/dataTables.bootstrap4.min.css" %}"
rel="stylesheet">
<!-- DataTables Responsive CSS -->
- <link href="{% static "bower_components/datatables-responsive/css/dataTables.responsive.css" %}"
+ <link href="{% static "bower_components/datatables.net-responsive-bs4/css/responsive.bootstrap4.min.css" %}"
rel="stylesheet">
{% endblock extrahead %}
@@ -36,11 +36,11 @@
{% block extrajs %}
<!-- DataTables JavaScript -->
- <link href="{% static "bower_components/datatables-plugins/integration/bootstrap/3/dataTables.bootstrap.css" %}"
+ <link href="{% static "bower_components/datatables/media/css/dataTables.bootstrap.css" %}"
rel="stylesheet">
- <script src={% static "bower_components/datatables/media/js/jquery.dataTables.min.js" %}></script>
- <script src={% static "bower_components/datatables-plugins/integration/bootstrap/3/dataTables.bootstrap.min.js" %}></script>
+ <script src={% static "bower_components/datatables.net/js/jquery.dataTables.min.js" %}></script>
+ <script src={% static "bower_components/datatables.net-bs4/js/dataTables.bootstrap4.min.js" %}></script>
diff --git a/dashboard/src/templates/dashboard/searchable_select_multiple.html b/dashboard/src/templates/dashboard/searchable_select_multiple.html
index 5fa8993..8bcf890 100644
--- a/dashboard/src/templates/dashboard/searchable_select_multiple.html
+++ b/dashboard/src/templates/dashboard/searchable_select_multiple.html
@@ -1,22 +1,23 @@
<script src="https://code.jquery.com/jquery-2.2.4.min.js"></script>
+<script src="/static/js/dashboard.js"></script>
-<div class="autocomplete">
+<div id="search_select_outer" class="autocomplete">
<div id="warning_pane" style="background: #FFFFFF; color: #CC0000;">
{% if incompatible == "true" %}
<h3>Warning: Incompatible Configuration</h3>
<p>Please make a different selection, as the current config conflicts with the selected pod</p>
{% endif %}
</div>
-
- <div id="added_list">
-
- </div>
<div id="added_counter">
<p id="added_number">0</p>
<p id="addable_limit">/ {% if selectable_limit > -1 %} {{ selectable_limit }} {% else %} &infin; {% endif %}added</p>
</div>
- <input id="user_field" name="ignore_this" class="form-control" autocomplete="off" type="text" placeholder="{{placeholder}}" value="" oninput="search(this.value)"
+ <div id="added_list">
+
+ </div>
+
+ <input id="user_field" name="ignore_this" class="form-control" autocomplete="off" type="text" placeholder="{{placeholder}}" value="" oninput="searchable_select_multiple_widget.search(this.value)"
{% if disabled %} disabled {% endif %}
>
</input>
@@ -36,6 +37,11 @@
overflow-y: auto;
padding-bottom: 10px;
}
+
+ #added_list {
+ margin-bottom: 5px;
+ }
+
.autocomplete {
display: flex;
flex: 1;
@@ -44,6 +50,9 @@
#user_field {
font-size: 14pt;
padding: 5px;
+ height: 40px;
+ border: 1px solid #ccc;
+ border-radius: 5px;
}
@@ -55,7 +64,7 @@
border: solid 1px #ddd;
border-top: none;
border-bottom: none;
- visibility: hidden;
+ visibility: inherit;
flex: 1;
position: absolute;
@@ -153,325 +162,48 @@
</div>
<script type="text/javascript">
- //flags
- var show_from_noentry = {{show_from_noentry|yesno:"true,false"}}; // whether to show any results before user starts typing
- var show_x_results = {{show_x_results|default:-1}}; // how many results to show at a time, -1 shows all results
- var results_scrollable = {{results_scrollable|yesno:"true,false"}}; // whether list should be scrollable
- var selectable_limit = {{selectable_limit|default:-1}}; // how many selections can be made, -1 allows infinitely many
- var placeholder = "{{placeholder|default:"begin typing"}}"; // placeholder that goes in text box
-
- //needed info
- var items = {{items|safe}} // items to add to trie. Type is a dictionary of dictionaries with structure:
- /*
- {
- id# : {
- "id": any, identifiable on backend
- "small_name": string, displayed first (before separator), searchable (use for e.g. username)
- "expanded_name": string, displayed second (after separator), searchable (use for e.g. email address)
- "string": string, not displayed, still searchable
- }
- }
- */
-
- /* used later:
- {{ selectable_limit }}: changes what number displays for field
- {{ name }}: form identifiable name, relevant for backend
- // when submitted, form will contain field data in post with name as the key
- {{ placeholder }}: "greyed out" contents put into search field initially to guide user as to what they're searching for
- {{ initial }}: in search_field_init(), marked safe, an array of id's each referring to an id from items
- */
-
- //tries
- var expanded_name_trie = {}
- expanded_name_trie.isComplete = false;
- var small_name_trie = {}
- small_name_trie.isComplete = false;
- var string_trie = {}
- string_trie.isComplete = false;
-
- var added_items = [];
-
- search_field_init();
-
- if( show_from_noentry )
- {
- search("");
- }
+ function searchableSelectMultipleWidgetEntry() {
+ let format_vars = {
+ "show_from_noentry": {{show_from_noentry|yesno:"true,false"}},
+ "show_x_results": {{show_x_results|default:-1}},
+ "results_scrollable": {{results_scrollable|yesno:"true,false"}},
+ "selectable_limit": {{selectable_limit|default:-1}},
+ "placeholder": "{{placeholder|default:"begin typing"}}"
+ };
- function disable() {
- var textfield = document.getElementById("user_field");
- var drop = document.getElementById("drop_results");
+ let field_dataset = {{items|safe}};
- textfield.disabled = "True";
- drop.style.display = "none";
+ let field_initial = {{ initial|safe }};
- var btns = document.getElementsByClassName("btn-remove");
- for( var i = 0; i < btns.length; i++ )
- {
- btns[i].classList.add("disabled");
- }
- }
-
- function search_field_init() {
- build_all_tries(items);
-
- var initial = {{ initial|safe }};
-
- for( var i = 0; i < initial.length; i++)
- {
- select_item(String(initial[i]));
- }
- if(initial.length == 1)
- {
- search(items[initial[0]]["small_name"]);
- document.getElementById("user_field").value = items[initial[0]]["small_name"];
- }
- }
-
- function build_all_tries(dict)
- {
- for( var i in dict )
- {
- add_item(dict[i]);
- }
- }
-
- function add_item(item)
- {
- var id = item['id'];
- add_to_tree(item['expanded_name'], id, expanded_name_trie);
- add_to_tree(item['small_name'], id, small_name_trie);
- add_to_tree(item['string'], id, string_trie);
+ //global
+ searchable_select_multiple_widget = new SearchableSelectMultipleWidget(format_vars, field_dataset, field_initial);
}
- function add_to_tree(str, id, trie)
- {
- inner_trie = trie;
- while( str )
- {
- if( !inner_trie[str.charAt(0)] )
- {
- new_trie = {};
- inner_trie[str.charAt(0)] = new_trie;
- }
- else
- {
- new_trie = inner_trie[str.charAt(0)];
- }
+ searchableSelectMultipleWidgetEntry();
- 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);
- }
- }
+ /*
+ var show_from_noentry = context(show_from_noentry|yesno:"true,false") // whether to show any results before user starts typing
+ var show_x_results = context(show_x_results|default:-1) // how many results to show at a time, -1 shows all results
+ var results_scrollable = {{results_scrollable|yesno:"true,false") // whether list should be scrollable
+ var selectable_limit = {{selectable_limit|default:-1) // how many selections can be made, -1 allows infinitely many
+ var placeholder = "context(placeholder|default:"begin typing")" // placeholder that goes in text box
- function search(input)
- {
- if( input.length == 0 && !show_from_noentry){
- dropdown([]);
- return;
- }
- else if( input.length == 0 && show_from_noentry)
+ needed info
+ var items = context(items|safe) // items to add to trie. Type is a dictionary of dictionaries with structure:
{
- dropdown(items); //show all items
- }
- else
- {
- var trees = []
- var tr1 = getSubtree(input, expanded_name_trie);
- trees.push(tr1);
- var tr2 = getSubtree(input, small_name_trie);
- trees.push(tr2);
- var tr3 = getSubtree(input, string_trie);
- trees.push(tr3);
- var results = collate(trees);
- dropdown(results);
- }
- }
-
- function getSubtree(input, given_trie)
- {
- /*
- recursive function to return the trie accessed at input
- */
-
- if( input.length == 0 ){
- return given_trie;
- }
-
- else{
- var substr = input.substring(0, input.length - 1);
- var last_char = input.charAt(input.length-1);
- var subtrie = getSubtree(substr, given_trie);
- if( !subtrie ) //substr not in the trie
- {
- return {};
- }
- var indexed_trie = subtrie[last_char];
- return indexed_trie;
- }
- }
-
- function serialize(trie)
- {
- /*
- takes in a trie and returns a list of its item id's
- */
- var itemIDs = [];
- if ( !trie )
- {
- return itemIDs; //empty, base case
- }
- for( var key in trie )
- {
- if(key.length > 1)
- {
- continue;
- }
- itemIDs = itemIDs.concat(serialize(trie[key]));
- }
- if ( trie.isComplete )
- {
- itemIDs.push(...trie.ids);
- }
-
- return itemIDs;
- }
-
- function collate(trees)
- {
- /*
- takes a list of tries
- returns a list of ids of objects that are available
- */
- results = [];
- for( var i in trees )
- {
- var available_IDs = serialize(trees[i]);
- for( var j=0; j<available_IDs.length; j++){
- var itemID = available_IDs[j];
- results[itemID] = items[itemID];
- }
- }
- return results;
- }
-
- function generate_element_text(obj)
- {
- var content_strings = [obj['expanded_name'], obj['small_name'], obj['string']].filter(x => Boolean(x));
- var result = content_strings.shift();
- if( result == null || content_strings.length < 1) return result;
- return result + " (" + content_strings.join(", ") + ")";
- }
-
- function dropdown(ids)
- {
- /*
- takes in a mapping of ids to objects in items
- and displays them in the dropdown
- */
- var drop = document.getElementById("drop_results");
- while(drop.firstChild)
- {
- drop.removeChild(drop.firstChild);
- }
-
- for( var id in ids )
- {
- var result_entry = document.createElement("li");
- var result_button = document.createElement("a");
- var obj = items[id];
- var result_text = generate_element_text(obj);
- result_button.appendChild(document.createTextNode(result_text));
- result_button.setAttribute('onclick', 'select_item("' + obj['id'] + '")');
- var tooltip = document.createElement("span");
- var tooltiptext = document.createTextNode(result_text);
- tooltip.appendChild(tooltiptext);
- tooltip.setAttribute('class', 'entry_tooltip');
- result_button.appendChild(tooltip);
- result_entry.appendChild(result_button);
- drop.appendChild(result_entry);
- }
-
- if( !drop.firstChild )
- {
- drop.style.visibility = 'hidden';
- }
- else
- {
- drop.style.visibility = 'inherit';
- }
- }
-
- function select_item(item_id)
- {
- //TODO make faster
- var item = items[item_id]['id'];
- if( (selectable_limit > -1 && added_items.length < selectable_limit) || selectable_limit < 0 )
- {
- if( added_items.indexOf(item) == -1 )
- {
- added_items.push(item);
+ id# : {
+ "id": any, identifiable on backend
+ "small_name": string, displayed first (before separator), searchable (use for e.g. username)
+ "expanded_name": string, displayed second (after separator), searchable (use for e.g. email address)
+ "string": string, not displayed, still searchable
}
}
- update_selected_list();
- document.getElementById("user_field").focus();
- }
-
- function remove_item(item_ref)
- {
- item = Object.values(items)[item_ref];
- var index = added_items.indexOf(item);
- added_items.splice(index, 1);
-
- update_selected_list()
- document.getElementById("user_field").focus();
- }
-
- function update_selected_list()
- {
- document.getElementById("added_number").innerText = added_items.length;
- selector = document.getElementById('selector');
- selector.value = JSON.stringify(added_items);
- added_list = document.getElementById('added_list');
-
- while(selector.firstChild)
- {
- selector.removeChild(selector.firstChild);
- }
- while(added_list.firstChild)
- {
- added_list.removeChild(added_list.firstChild);
- }
-
- list_html = "";
-
- for( var key in added_items )
- {
- item_id = added_items[key];
- item = items[item_id];
-
- var element_entry_text = generate_element_text(item);
-
- list_html += '<div class="list_entry">'
- + '<p class="added_entry_text">'
- + element_entry_text
- + '</p>'
- + '<button onclick="remove_item('
- + Object.values(items).indexOf(item)
- + ')" class="btn-remove btn">remove</button>';
- list_html += '</div>';
- }
-
- added_list.innerHTML = list_html;
- }
+ used later:
+ context(selectable_limit): changes what number displays for field
+ context(name): form identifiable name, relevant for backend
+ // when submitted, form will contain field data in post with name as the key
+ context(placeholder): "greyed out" contents put into search field initially to guide user as to what they're searching for
+ context(initial): in search_field_init(), marked safe, an array of id's each referring to an id from items
+ */
</script>
diff --git a/dashboard/src/templates/dashboard/table.html b/dashboard/src/templates/dashboard/table.html
index b3f4b5f..0a37ded 100644
--- a/dashboard/src/templates/dashboard/table.html
+++ b/dashboard/src/templates/dashboard/table.html
@@ -4,11 +4,12 @@
{% block extrahead %}
{{ block.super }}
<!-- DataTables CSS -->
- <link href="{% static "bower_components/datatables-plugins/integration/bootstrap/3/dataTables.bootstrap.css" %}"
+ <link href="{% static "bower_components/datatables.net-bs4/css/dataTables.bootstrap4.min.css" %}"
rel="stylesheet">
<!-- DataTables Responsive CSS -->
- <link href="{% static "bower_components/datatables-responsive/css/dataTables.responsive.css" %}" rel="stylesheet">
+ <link href="{% static "bower_components/datatables.net-responsive-bs4/css/responsive.bootstrap4.min.css" %}"
+ rel="stylesheet">
{% endblock extrahead %}
{% block content %}
@@ -34,8 +35,8 @@
{% block extrajs %}
<!-- DataTables JavaScript -->
- <script src={% static "bower_components/datatables/media/js/jquery.dataTables.min.js" %}></script>
- <script src={% static "bower_components/datatables-plugins/integration/bootstrap/3/dataTables.bootstrap.min.js" %}></script>
+ <script src={% static "bower_components/datatables.net/js/jquery.dataTables.min.js" %}></script>
+ <script src={% static "bower_components/datatables.net-bs4/js/dataTables.bootstrap4.min.js" %}></script>
<script src={% static "js/dataTables-sort.js" %}></script>
diff --git a/dashboard/src/templates/layout.html b/dashboard/src/templates/layout.html
index 378cc63..d37d4f5 100644
--- a/dashboard/src/templates/layout.html
+++ b/dashboard/src/templates/layout.html
@@ -20,7 +20,7 @@
<link href="{% static "bower_components/metisMenu/dist/metisMenu.min.css" %}" rel="stylesheet">
<!-- Custom Fonts -->
- <link href="{% static "bower_components/font-awesome/css/font-awesome.min.css" %}"
+ <link href="{% static "bower_components/Font-Awesome/css/all.min.css" %}"
rel="stylesheet" type="text/css">
<!-- Favicon -->
@@ -50,17 +50,12 @@
{#<!-- jQuery -->#}
{#<script src="{% static "bower_components/jquery/dist/jquery.min.js" %}"></script>#}
{#<script src="{% static "bower_components/jquery-migrate/jquery-migrate.min.js" %}"></script>#}
-
+<!-- Popper.js -->
+<script src="{% static "bower_components/popper.js/dist/umd/popper.min.js" %}"></script>
{#<script src="https://code.jquery.com/jquery-2.2.0.min.js"></script>#}
<!-- Bootstrap Core JavaScript -->
<script src="{% static "bower_components/bootstrap/dist/js/bootstrap.min.js" %}"></script>
-<!-- Metis Menu Plugin JavaScript -->
-<script src="{% static "bower_components/metisMenu/dist/metisMenu.min.js" %}"></script>
-
-<!-- Custom Theme JavaScript -->
-<script src="{% static "bower_components/startbootstrap-sb-admin-2-blackrockdigital/dist/js/sb-admin-2.js" %}"></script>
-
{% block extrajs %}
{% endblock extrajs %}
</body>
diff --git a/dashboard/src/templates/notifier/inbox.html b/dashboard/src/templates/notifier/inbox.html
index 4184d1d..72207ed 100644
--- a/dashboard/src/templates/notifier/inbox.html
+++ b/dashboard/src/templates/notifier/inbox.html
@@ -6,97 +6,124 @@
{% block content %}
<style media="screen">
-
- .inbox-panel {
- display: grid;
- grid-template-columns: 30% 5% 65%;
- }
-
- .section-panel {
- padding: 10px;
- }
-
- .iframe-panel {
- padding: 0px;
- margin-top: 0px;
- }
-
- .card-container {
- border: 1px solid #cccccc;
- border-bottom: 0px;
- }
- .card {
- height: 50px;
- position: relative;
- border-bottom: 1px solid #cccccc;
- padding: 10px;
- width: 100%;
- background-color: #ffffff;
- z-index: 5;
- }
- .selected-card {
- background-color: #f3f3f3;
- }
-
- .card:hover {
- box-shadow: 0px 0 5px 2px #cccccc;
- z-index: 6;
- }
-
- #inbox-iframe {
- height: calc(100vh - 57px);
- }
-
- .half_width {
- width: 50%;
- }
- .card-wrapper {
- }
-
- #page-wrapper{
- padding: 0px;
- }
-
- .read_notification{
- background-color: #efefef;
- }
+ .inbox-panel {
+ display: grid;
+ grid-template-columns: 30% 5% 65%;
+ }
+
+ .section-panel {
+ padding: 10px;
+ }
+
+ .iframe-panel {
+ padding: 0px;
+ margin-top: 0px;
+ }
+
+ .card-container {
+ border: 1px solid #cccccc;
+ border-bottom: 0px;
+ }
+
+ .card {
+ height: 50px;
+ position: relative;
+ border-bottom: 1px solid #cccccc;
+ padding: 10px;
+ width: 100%;
+ background-color: #ffffff;
+ z-index: 5;
+ }
+
+ .selected-card {
+ background-color: #f3f3f3;
+ }
+
+ .card:hover {
+ box-shadow: 0px 0 5px 2px #cccccc;
+ z-index: 6;
+ }
+
+ .half_width {
+ width: 50%;
+ }
+
+ #page-wrapper {
+ padding: 0px;
+ }
+
+ .read_notification {
+ background-color: #efefef;
+ }
+
+ .scrollable {
+ overflow-y: auto;
+ }
</style>
-
-<div class="inbox-panel">
- <div class="section-panel">
- <h4>New:</h4>
- <div class="card-container">
- {% for notification in unread_notifications %}
- <div class="inbox-entry card" onclick="showmessage({{notification.id}}); setactive(this);">
- {{ notification }}
+<div class="container-fluid d-flex flex-grow-1 flex-column">
+ <div class="row mt-3 mb-2">
+ <div class="col-2 px-0">
+ <div class="btn-group w-100" id="filterGroup">
+ <button class="btn btn-secondary active" data-read="-1">All</button>
+ <button class="btn btn-secondary" data-read="0">Unread</button>
+ <button class="btn btn-secondary" data-read="1">Read</button>
+ </div>
</div>
- {% endfor %}
</div>
- <h4>Read:</h4>
- <div class="card-container">
- {% for notification in read_notifications %}
- <div class="inbox-entry card read_notification" onclick="showmessage({{notification.id}}); setactive(this);">
- {{ notification }}
+ <div class="row flex-grow-1" id="fixHeight">
+ <!-- Notification list && Controls -->
+ <div class="mb-2 mb-lg-0 col-lg-2 px-0 mh-100">
+ <div class="list-group rounded-0 rounded-left scrollable mh-100 notifications" id="unreadNotifications" data-read="0">
+ {% for notification in unread_notifications %}
+ <a
+ href="#"
+ onclick="showmessage({{notification.id}}); setactive(this);"
+ class="list-group-item list-group-item-action notification">
+ {{ notification }}
+ </a>
+ {% endfor %}
+ </div>
+ <div class="list-group rounded-0 rounded-left scrollable mh-100 notifications" id="readNotifications" data-read="1">
+ {% for notification in read_notifications %}
+ <a
+ href="#"
+ onclick="showmessage({{notification.id}}); setactive(this);"
+ class="list-group-item list-group-item-action list-group-item-secondary notification">
+ {{ notification }}
+ </a>
+ {% endfor %}
+ </div>
+ </div>
+ <!-- Content -->
+ <div class="col ml-lg-2 border mh-100 p-4">
+ <iframe class="w-100 h-100" id="inbox-iframe" frameBorder="0" scrolling="yes">Please select a notification</iframe>
</div>
- {% endfor %}
</div>
- </div>
- <div>
- </div>
- <div class="iframe-panel inbox-expanded-view">
- <div class="inbox-iframe-div">
- <iframe id="inbox-iframe" frameBorder="0" width="100%" height="100vh" scrolling="yes">Please select a notification</iframe>
- </div>
- </div>
</div>
<script type="text/javascript">
-
- function showmessage(msg_id)
- {
- iframe = document.getElementById("inbox-iframe");
- iframe.src = "notification/" + msg_id;
- }
-
+ function showmessage(msg_id) {
+ iframe = document.getElementById("inbox-iframe");
+ iframe.src = "notification/" + msg_id;
+ }
+
+ function setactive(obj) {
+ $(".notification").removeClass("active");
+ $(obj).addClass("active");
+ }
+
+ $(document).ready(function(){
+ // For all / unread / read
+ $("#filterGroup button").click(function(){
+ let read = $(this).attr("data-read");
+ $(this).siblings().removeClass("active");
+ $(".notifications").addClass("d-none");
+ $(this).addClass("active");
+ if (read === "-1") {
+ return $(".notifications").removeClass("d-none");
+ }
+ $(`.notifications[data-read="${read}"]`).removeClass("d-none");
+ });
+ });
</script>
-{% endblock %}
+{% endblock %} \ No newline at end of file
diff --git a/dashboard/src/templates/resource/hostprofile_detail.html b/dashboard/src/templates/resource/hostprofile_detail.html
index 0776b9e..dc20600 100644
--- a/dashboard/src/templates/resource/hostprofile_detail.html
+++ b/dashboard/src/templates/resource/hostprofile_detail.html
@@ -4,45 +4,35 @@
{% block content %}
<div class="row">
<div class="col-lg-6">
- <div class="panel panel-default">
- <div class="panel-heading clearfix">
+ <div class="card mb-4">
+ <div class="card-header d-flex">
<h4 style="display: inline;">Available at</h4>
- <a data-toggle="collapse" data-target="#panel_overview" class="btn pull-right" style="line-height: 1;" >Expand</a>
+ <button data-toggle="collapse" data-target="#avilableAt" class="btn ml-auto btn-outline-secondary">Expand</button>
</div>
- <div class="panel-body" id="panel_overview">
- <table class="table">
- <tr>
- <td>
- <ul>
- {% for lab in hostprofile.labs.all %}
- <li>{{lab.name}}</li>
- {% endfor %}
- </ul>
- </td>
- </tr>
- </table>
+ <div class="card-body collapse show" id="avilableAt">
+ <ul class="list-group">
+ {% for lab in hostprofile.labs.all %}
+ <li class="list-group-item">{{lab.name}}</li>
+ {% endfor %}
+ </ul>
</div>
</div>
- <div class="panel panel-default">
- <div class="panel-heading clearfix">
+ <div class="card mb-4">
+ <div class="card-header d-flex">
<h4 style="display: inline;">RAM</h4>
- <a data-toggle="collapse" data-target="#panel_overview" class="btn pull-right" style="line-height: 1;" >Expand</a>
+ <button data-toggle="collapse" data-target="#ramPanel" class="btn ml-auto btn-outline-secondary">Expand</button>
</div>
- <div class="panel-body" id="panel_overview">
- <table class="table">
- <tr>
- <td>{{hostprofile.ramprofile.first.amount}}G,
- {{hostprofile.ramprofile.first.channels}} channels</td>
- </tr>
- </table>
+ <div class="card-body collapse show" id="ramPanel">
+ {{hostprofile.ramprofile.first.amount}}G,
+ {{hostprofile.ramprofile.first.channels}} channels
</div>
</div>
- <div class="panel panel-default">
- <div class="panel-heading clearfix">
+ <div class="card mb-4">
+ <div class="card-header d-flex">
<h4 style="display: inline;">CPU</h4>
- <a data-toggle="collapse" data-target="#panel_overview" class="btn pull-right" style="line-height: 1;" >Expand</a>
+ <button data-toggle="collapse" data-target="#cpuPanel" class="btn ml-auto btn-outline-secondary">Expand</button>
</div>
- <div class="panel-body" id="panel_overview">
+ <div class="card-body collapse show" id="cpuPanel">
<table class="table">
<tr>
<td>Arch:</td>
@@ -59,42 +49,12 @@
</table>
</div>
</div>
- </div>
- <div class="col-lg-6">
- <div class="panel panel-default">
- <div class="panel-heading clearfix">
- <h4 style="display: inline;">Interfaces</h4>
- <a data-toggle="collapse" data-target="#panel_overview" class="btn pull-right" style="line-height: 1;" >Expand</a>
- </div>
- <div class="panel-body" id="panel_overview">
- <table class="table">
- {% for intprof in hostprofile.interfaceprofile.all %}
- <tr>
- <td>
- <table class="table borderless">
- <tr>
- <td>Name:</td>
- <td>{{intprof.name}}</td>
- </tr>
- <tr>
- <td>Speed:</td>
- <td>{{intprof.speed}}</td>
- </tr>
- </table>
- </td>
- </tr>
- {% endfor %}
- </table>
- </div>
- </div>
- </div>
- <div class="col-lg-6">
- <div class="panel panel-default">
- <div class="panel-heading clearfix">
+ <div class="card mb-4">
+ <div class="card-header d-flex">
<h4 style="display: inline;">Disk</h4>
- <a data-toggle="collapse" data-target="#panel_overview" class="btn pull-right" style="line-height: 1;" >Expand</a>
+ <button data-toggle="collapse" data-target="#diskPanel" class="btn ml-auto btn-outline-secondary">Expand</button>
</div>
- <div class="panel-body" id="panel_overview">
+ <div class="card-body collapse show" id="diskPanel">
<table class="table">
<tr>
<td>Size:</td>
@@ -112,5 +72,31 @@
</div>
</div>
</div>
+ <div class="col-lg-6">
+ <div class="card">
+ <div class="card-header d-flex">
+ <h4 style="display: inline;">Interfaces</h4>
+ <button data-toggle="collapse" data-target="#interfacePanel" class="btn ml-auto btn-outline-secondary">Expand</button>
+ </div>
+ <div class="card-body collapse show" id="interfacePanel">
+ <table class="table">
+ <thead>
+ <tr>
+ <th>Name</th>
+ <th>Speed</th>
+ </tr>
+ </thead>
+ <tbody>
+ {% for intprof in hostprofile.interfaceprofile.all %}
+ <tr>
+ <td>{{intprof.name}}</td>
+ <td>{{intprof.speed}}</td>
+ </tr>
+ {% endfor %}
+ </tbody>
+ </table>
+ </div>
+ </div>
+ </div>
</div>
{% endblock content %}
diff --git a/dashboard/src/templates/resource/steps/define_hardware.html b/dashboard/src/templates/resource/steps/define_hardware.html
index 933b4ab..57078e9 100644
--- a/dashboard/src/templates/resource/steps/define_hardware.html
+++ b/dashboard/src/templates/resource/steps/define_hardware.html
@@ -1,7 +1,7 @@
{% extends "workflow/viewport-element.html" %}
{% load staticfiles %}
-{% load bootstrap3 %}
+{% load bootstrap4 %}
{% block content %}
<p>Note that not all labs host every kind of machine.
@@ -15,20 +15,7 @@ with your current configuration will become unavailable.</p>
</form>
{% endblock content %}
{% block onleave %}
-var normalize = function(data){
- //converts the top level keys in data to map to lists
- var normalized = {}
- for( var key in data ){
- normalized[key] = [];
- for( var subkey in data[key] ){
- normalized[key].push(data[key][subkey]);
- }
- }
- return normalized;
-}
-var data = normalize(result);
-data = JSON.stringify(data);
-document.getElementById("filter_field").value = data;
+multi_filter_widget.finish();
var formData = $("#define_hardware_form").serialize();
req = new XMLHttpRequest();
req.open('POST', '/wf/workflow/', false);
diff --git a/dashboard/src/templates/resource/steps/host_info.html b/dashboard/src/templates/resource/steps/host_info.html
index 0275727..bbbafdc 100644
--- a/dashboard/src/templates/resource/steps/host_info.html
+++ b/dashboard/src/templates/resource/steps/host_info.html
@@ -1,7 +1,7 @@
{% extends "workflow/viewport-element.html" %}
{% load staticfiles %}
-{% load bootstrap3 %}
+{% load bootstrap4 %}
{% block content %}
diff --git a/dashboard/src/templates/resource/steps/meta_info.html b/dashboard/src/templates/resource/steps/meta_info.html
index da98267..cebd343 100644
--- a/dashboard/src/templates/resource/steps/meta_info.html
+++ b/dashboard/src/templates/resource/steps/meta_info.html
@@ -1,7 +1,7 @@
{% extends "workflow/viewport-element.html" %}
{% load staticfiles %}
-{% load bootstrap3 %}
+{% load bootstrap4 %}
{% block content %}
diff --git a/dashboard/src/templates/resource/steps/pod_definition.html b/dashboard/src/templates/resource/steps/pod_definition.html
index f8aaa74..5826ccb 100644
--- a/dashboard/src/templates/resource/steps/pod_definition.html
+++ b/dashboard/src/templates/resource/steps/pod_definition.html
@@ -8,580 +8,7 @@
var mxLoadStylesheets = false;
</script>
<script type="text/javascript" src="/static/js/mxClient.min.js" ></script>
-<style>
-p {
- word-break: normal;
- white-space: normal;
-}
-</style>
-<script type="text/javascript">
-var currentWindow;
-var currentGraph;
-var netCount = 0;
-var netColors = ['red', 'blue', 'purple', 'green', 'orange', '#8CCDF5', '#1E9BAC'];
-var hostCount = 0;
-var lastHostBottom = 100;
-var networks = new Set([]);
-var has_public_net = false;
-
-function main(graphContainer, overviewContainer, toolbarContainer) {
- //check if the browser is supported
- if (!mxClient.isBrowserSupported()) {
- mxUtils.error('Browser is not supported', 200, false);
- return null;
- }
-
- // Workaround for Internet Explorer ignoring certain styles
- if (mxClient.IS_QUIRKS) {
- document.body.style.overflow = 'hidden';
- new mxDivResizer(graphContainer);
- }
- var editor = new mxEditor();
- var graph = editor.graph;
- var model = graph.getModel();
- editor.setGraphContainer(graphContainer);
-
- doGlobalConfig(graph);
- currentGraph = graph;
-
- {% if xml %}
- restoreFromXml('{{xml|safe}}', editor);
- {% elif hosts %}
- {% for host in hosts %}
- var host = {{host|safe}};
- makeHost(host);
- {% endfor %}
- {% endif %}
- {% if added_hosts %}
- {% for host in added_hosts %}
- var host = {{host|safe}}
- makeHost(host);
- {% endfor %}
- updateHosts([]);
- {% endif %}
-
- addToolbarButton(editor, toolbarContainer, 'zoomIn', '', "/static/img/mxgraph/zoom_in.png", true);
- addToolbarButton(editor, toolbarContainer, 'zoomOut', '', "/static/img/mxgraph/zoom_out.png", true);
-
- {% if debug %}
- editor.addAction('printXML', function(editor, cell) {
- mxLog.write(encodeGraph(graph));
- mxLog.show();
- });
- addToolbarButton(editor, toolbarContainer, 'printXML', '', '/static/img/mxgraph/fit_to_size.png', true);
- {% endif %}
-
- var outline = new mxOutline(graph, overviewContainer);
-
- var checkAllowed = function(edge, terminal, source) {
- //check if other terminal is null, and that they are different
- 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
- graph.removeCells([edge]);
- return false;
- }
- }
- return true;
- };
-
- var colorEdge = function(edge, terminal, source) {
- if(terminal.getParent().getId().indexOf('network') >= 0) {
- styles = terminal.getParent().getStyle().split(';');
- color = 'black';
- for(var i=0; i<styles.length; i++){
- kvp = styles[i].split('=');
- if(kvp[0] == "fillColor"){
- color = kvp[1];
- }
- }
- edge.setStyle('strokeColor=' + color);
- }
- };
-
- var alertVlan = function(edge, terminal, source) {
- if( terminal == null || edge.getTerminal(!source) == null) {
- return;
- }
- var vlanHTML = '<form> <input type="radio" name="tagged" value="True" checked> Tagged<br>'
- vlanHTML += '<input type="radio" name="tagged" value="False"> Untagged </form>'
- vlanHTML += '<button onclick=parseVlanWindow(' + edge.getId() + ');>Okay</button>'
- vlanHTML += '<button onclick=deleteVlanWindow(' + edge.getId() + ');>Cancel</button>'
- content = document.createElement('div');
- content.innerHTML = vlanHTML;
- showWindow(graph, "Vlan Selection", content, 200, 200);
- }
-
- //sets the edge color to be the same as the network
- graph.addListener(mxEvent.CELL_CONNECTED, function(sender, event){
- edge = event.getProperty('edge');
- terminal = event.getProperty('terminal')
- source = event.getProperty('source');
- if(checkAllowed(edge, terminal, source)) {
- colorEdge(edge, terminal, source);
- alertVlan(edge, terminal, source);
- }
- });
-
- createDeleteDialog = function(id) {
- var content = document.createElement('div');
- var innerHTML = "<button style='width: 46%;' onclick=deleteCell('" + id + "');>Remove</button>"
- innerHTML += "<button style='width: 46%;' onclick='currentWindow.destroy();'>Cancel</button>"
- content.innerHTML = innerHTML;
- showWindow(currentGraph, 'Do you want to delete this network?', content, 200, 62);
- }
-
- graph.dblClick = function(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 ) {
- createDeleteDialog(cell.getId());
- }
- else {
- showDetailWindow(cell);
- }
- }
- };
-
- updateHosts({{ removed_hosts|default:"[]"|safe }});
- if(!has_public_net){
- addPublicNetwork();
- }
-}
-
-function showDetailWindow(cell) {
- var info = JSON.parse(cell.getValue());
- var content = document.createElement("div");
- var inner = "<pre>Name: " + info.name + "\n";
- inner += "Description:\n" + info.description + "</pre>";
- inner += '<button onclick="currentWindow.destroy();">Okay</button>'
- content.innerHTML = inner
- showWindow(currentGraph, 'Details', content, 400, 400);
-}
-
-function restoreFromXml(xml, editor) {
- var doc = mxUtils.parseXml(xml);
- var node = doc.documentElement;
- editor.readGraphModel(node);
-
- //Iterate over all children, and parse the networks to add them to the sidebar
- var root = currentGraph.getDefaultParent();
- for( var i=0; i<root.getChildCount(); i++) {
- var cell = root.getChildAt(i);
- if(cell.getId().indexOf("network") > -1) {
- var info = JSON.parse(cell.getValue());
- var name = info['name'];
- networks.add(name);
- var styles = cell.getStyle().split(";");
- var color = null;
- for(var j=0; j< styles.length; j++){
- var kvp = styles[j].split('=');
- if(kvp[0] == "fillColor") {
- color = kvp[1];
- break;
- }
- }
- if(info.public){
- has_public_net = true;
- }
- netCount++;
- makeSidebarNetwork(name, color, cell.getId());
- }
- }
-}
-
-function deleteCell(cellId) {
- var cell = currentGraph.getModel().getCell(cellId);
- if( cellId.indexOf("network") > -1 ) {
- elem = document.getElementById(cellId);
- elem.parentElement.removeChild(elem);
- }
- currentGraph.removeCells([cell]);
- currentWindow.destroy();
-}
-
-function newNetworkWindow() {
- var innerHtml = 'Name: <input type="text" name="net_name" maxlength="100" id="net_name_input" style="margin:5px;"><br>';
- innerHtml += '<button style="width: 46%;" onclick="parseNetworkWindow()">Okay</button>';
- innerHtml += '<button style="width: 46%;" onclick="currentWindow.destroy();">Cancel</button><br>';
- innerHtml += '<div id="current_window_errors"/>';
- var content = document.createElement("div");
- content.innerHTML = innerHtml;
-
- showWindow(currentGraph, "Network Creation", content, 300, 300);
-}
-
-function parseNetworkWindow() {
- var net_name = document.getElementById("net_name_input").value
- var error_div = document.getElementById("current_window_errors");
- if( networks.has(net_name) ){
- error_div.innerHTML = "All network names must be unique";
- return;
- }
- addNetwork(net_name);
- currentWindow.destroy();
-}
-
-function addToolbarButton(editor, toolbar, action, label, image, isTransparent)
-{
- var button = document.createElement('button');
- button.style.fontSize = '10';
- if (image != null)
- {
- var img = document.createElement('img');
- img.setAttribute('src', image);
- img.style.width = '16px';
- img.style.height = '16px';
- img.style.verticalAlign = 'middle';
- img.style.marginRight = '2px';
- button.appendChild(img);
- }
- if (isTransparent)
- {
- button.style.background = 'transparent';
- button.style.color = '#FFFFFF';
- button.style.border = 'none';
- }
- mxEvent.addListener(button, 'click', function(evt)
- {
- editor.execute(action);
- });
- mxUtils.write(button, label);
- toolbar.appendChild(button);
-};
-
-function encodeGraph(graph) {
- var encoder = new mxCodec();
- var xml = encoder.encode(graph.getModel());
- return mxUtils.getXml(xml);
-}
-
-function doGlobalConfig(graph) {
- //general graph stuff
- graph.setMultigraph(false);
- graph.setCellsSelectable(false);
- graph.setCellsMovable(false);
-
- //testing
- graph.vertexLabelIsMovable = true;
-
-
- //edge behavior
- graph.setConnectable(true);
- graph.setAllowDanglingEdges(false);
- mxEdgeHandler.prototype.snapToTerminals = true;
- mxConstants.MIN_HOTSPOT_SIZE = 16;
- mxConstants.DEFAULT_HOTSPOT = 1;
- //edge 'style' (still affects behavior greatly)
- style = 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;
-
- hostStyle = graph.getStylesheet().getDefaultVertexStyle();
- hostStyle[mxConstants.STYLE_ROUNDED] = 1;
-
- // TODO: Proper override
- 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();
- }
- };
-}
-
-function showWindow(graph, title, content, width, height) {
- //create transparent black background
- var 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);
-
- //deal with IE quirk
- if (mxClient.IS_IE) {
- new mxDivResizer(background);
- }
-
- var x = Math.max(0, document.body.scrollWidth/2-width/2);
- var y = Math.max(10, (document.body.scrollHeight ||
- document.documentElement.scrollHeight)/2-height*2/3);
-
- var wnd = new mxWindow(title, content, x, y, width, height, false, true);
- wnd.setClosable(false);
-
- wnd.addListener(mxEvent.DESTROY, function(evt) {
- graph.setEnabled(true);
- mxEffects.fadeOut(background, 50, true, 10, 30, true);
- });
- currentWindow = wnd;
-
- graph.setEnabled(false);
- wnd.setVisible(true);
-};
-
-function closeWindow() {
- //allows the current window to be destroyed
- currentWindow.destroy();
-};
-
-function othersUntagged(edgeID) {
- var edge = currentGraph.getModel().getCell(edgeID);
- var end1 = edge.getTerminal(true);
- var end2 = edge.getTerminal(false);
-
- if( end1.getParent().getId().split('_')[0] == 'host' ){
- var netint = end1;
- } else {
- var netint = end2;
- }
-
- var edges = netint.edges;
-
- for( var i=0; i < edges.length; i++ ) {
- if( edges[i].getValue() ) {
- var tagged = JSON.parse(edges[i].getValue()).tagged;
- } else {
- var tagged = true;
- }
- if( !tagged ) {
- return true;
- }
- }
- return false;
-};
-
-
-function deleteVlanWindow(edgeID) {
- var cell = currentGraph.getModel().getCell(edgeID);
- currentGraph.removeCells([cell]);
- currentWindow.destroy();
-}
-
-function parseVlanWindow(edgeID) {
- //do parsing and data manipulation
- var radios = document.getElementsByName("tagged");
- edge = currentGraph.getModel().getCell(edgeID);
-
- for(var i=0; i<radios.length; i++) {
- if(radios[i].checked) {
- //set edge to be tagged or untagged
- //cellValue.setAttribute("tagged", radios[i].value);
- if( radios[i].value == "False")
- {
- if( othersUntagged(edgeID) )
- {
- alert("Only one untagged VLAN is allowed per interface");
- return;
- }
- }
- edgeVal = Object();
- edgeVal['tagged'] = radios[i].value == "True";
- edge.setValue(JSON.stringify(edgeVal));
- break;
- }
- }
- currentGraph.refresh(edge);
- closeWindow();
-}
-
-function makeMxNetwork(net_name, public = false) {
- model = currentGraph.getModel();
- width = 10;
- height = 1700;
- xoff = 400 + (30 * netCount);
- yoff = -10;
- var color = netColors[netCount];
- if( netCount > (netColors.length - 1)) {
- color = Math.floor(Math.random() * 16777215); //int in possible color space
- color = '#' + color.toString(16).toUpperCase(); //convert to hex
- //alert(color);
- }
- var net_val = Object();
- net_val['name'] = net_name;
- net_val['public'] = public;
- net = currentGraph.insertVertex(
- currentGraph.getDefaultParent(),
- 'network_' + netCount,
- JSON.stringify(net_val),
- xoff,
- yoff,
- width,
- height,
- 'fillColor=' + color,
- false
- );
- var num_ports = 45;
- for(var i=0; i<num_ports; i++){
- port = currentGraph.insertVertex(
- net,
- null,
- '',
- 0,
- (1/num_ports) * i,
- 10,
- height / num_ports,
- 'fillColor=black;opacity=0',
- true
- );
- }
-
- var retVal = Object();
- retVal['color'] = color;
- retVal['element_id'] = "network_" + netCount;
-
- networks.add(net_name);
-
- netCount++;
- return retVal;
-}
-
-function addPublicNetwork() {
- var net = makeMxNetwork("public", true);
- makeSidebarNetwork("public", net['color'], net['element_id']);
- has_public_net = true;
-}
-
-function addNetwork(net_name) {
- var ret = makeMxNetwork(net_name);
- var color = ret['color'];
- var net_id = ret['element_id'];
- makeSidebarNetwork(net_name, color, net_id);
-}
-
-function updateHosts(removed) {
- for(var i=0; i < removed.length; i++)
- {
- var hoststring = removed[i];
- var hostid = "host_" + hoststring.split("*")[0];
- var cell = currentGraph.getModel().getCell(hostid);
- currentGraph.removeCells([cell]);
- }
-
- var hosts = currentGraph.getChildVertices(currentGraph.getDefaultParent());
- var topdist = 100;
- for(var i=0; i<hosts.length; i++) {
- var host = hosts[i];
- if(!host.id.startsWith("host_"))
- {
- continue;
- }
- var geometry = host.getGeometry();
- geometry.y = topdist + 50;
- topdist = geometry.y + geometry.height;
- host.setGeometry(geometry);
- }
-}
-
-function makeSidebarNetwork(net_name, color, net_id){
- var newNet = document.createElement("li");
- var colorBlob = document.createElement("div");
- colorBlob.className = "colorblob";
- var textContainer = document.createElement("p");
- textContainer.className = "network_innertext";
- newNet.id = net_id;
- var deletebutton = document.createElement("button");
- deletebutton.className = "btn btn-danger";
- deletebutton.style = "float: right; height: 20px; line-height: 8px; vertical-align: middle; width: 20px; padding-left: 5px;";
- deleteButtonText = document.createTextNode("X");
- deletebutton.appendChild(deleteButtonText);
- deletebutton.addEventListener("click", function() {
- createDeleteDialog(net_id);
- }, false);
- var text = net_name;
- var newNetValue = document.createTextNode(text);
- textContainer.appendChild(newNetValue);
- colorBlob.style['background'] = color;
- newNet.appendChild(colorBlob);
- newNet.appendChild(textContainer);
- if( net_name != "public" )
- {
- newNet.appendChild(deletebutton);
- }
- document.getElementById("network_list").appendChild(newNet);
-}
-
-function makeHost(hostInfo) {
- value = JSON.stringify(hostInfo['value']);
- interfaces = hostInfo['interfaces'];
- graph = currentGraph;
- width = 100;
- height = (25 * interfaces.length) + 25;
- xoff = 75;
- yoff = lastHostBottom + 50;
- lastHostBottom = yoff + height;
- host = graph.insertVertex(
- graph.getDefaultParent(),
- 'host_' + hostInfo['id'],
- value,
- xoff,
- yoff,
- width,
- height,
- 'editable=0',
- false
- );
- host.getGeometry().offset = new mxPoint(-50,0);
- host.setConnectable(false);
- hostCount++;
-
- for(var i=0; i<interfaces.length; i++) {
- port = 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);
- currentGraph.refresh(port);
- }
- currentGraph.refresh(host);
-}
-
-function submitForm() {
- var form = document.getElementById("xml_form");
- var input_elem = document.getElementById("hidden_xml_input");
- var s = encodeGraph(currentGraph);
- input_elem.value = s;
- req = new XMLHttpRequest();
- req.open("POST", "/wf/workflow/", false);
- req.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
- req.onerror = function() { alert("problem with form submission"); }
- var formData = $("#xml_form").serialize();
- req.send(formData);
-}
-</script>
+<script type="text/javascript" src="/static/js/dashboard.js" ></script>
{% endblock extrahead %}
<!-- Calls the main function after the page has loaded. Container is dynamically created. -->
@@ -605,6 +32,10 @@ function submitForm() {
</div>
<style>
+ p {
+ word-break: normal;
+ white-space: normal;
+ }
#network_select {
background: inherit;
padding: 0px;
@@ -656,11 +87,11 @@ function submitForm() {
<div id="network_select" style="position:absolute;top:0px;bottom:0px;width:25%;right:0px;left:auto;">
<div id="toolbar_extension">
- <button id="btn_add_network" type="button" class="btn btn-primary" onclick="newNetworkWindow();">Add Network</button>
+ <button id="btn_add_network" type="button" class="btn btn-primary" onclick="network_step.newNetworkWindow();">Add Network</button>
</div>
<ul id="network_list">
</ul>
- <button type="button" style="display: none" onclick="submitForm();">Submit</button>
+ <button type="button" style="display: none" onclick="network_step.submitForm();">Submit</button>
</div>
<form id="xml_form" method="post" action="/wf/workflow/">
{% csrf_token %}
@@ -668,14 +99,42 @@ function submitForm() {
</form>
<script>
- main(
+ //gather context data
+ let debug = false;
+ {% if debug %}
+ debug = true;
+ {% endif %}
+
+ let xml = '';
+ {% if xml %}
+ xml = '{{xml|safe}}';
+ {% endif %}
+
+ let hosts = [];
+ {% for host in hosts %}
+ hosts.push({{host|safe}});
+ {% endfor %}
+
+ let added_hosts = [];
+ {% for host in added_hosts %}
+ added_hosts.push({{host|safe}});
+ {% endfor %}
+
+ let removed_host_ids = {{removed_hosts|safe}};
+
+ network_step = new NetworkStep(
+ debug,
+ xml,
+ hosts,
+ added_hosts,
+ removed_host_ids,
document.getElementById('graphContainer'),
document.getElementById('outlineContainer'),
document.getElementById('toolbarContainer'),
document.getElementById('sidebarContainer')
- )
+ );
</script>
{% endblock content %}
{% block onleave %}
-submitForm();
+network_step.submitForm();
{% endblock %}
diff --git a/dashboard/src/templates/snapshot_workflow/steps/meta.html b/dashboard/src/templates/snapshot_workflow/steps/meta.html
index cc49691..bea475d 100644
--- a/dashboard/src/templates/snapshot_workflow/steps/meta.html
+++ b/dashboard/src/templates/snapshot_workflow/steps/meta.html
@@ -1,7 +1,7 @@
{% extends "workflow/viewport-element.html" %}
{% load staticfiles %}
-{% load bootstrap3 %}
+{% load bootstrap4 %}
{% block content %}
<style>
diff --git a/dashboard/src/templates/snapshot_workflow/steps/select_host.html b/dashboard/src/templates/snapshot_workflow/steps/select_host.html
index 27a9238..f438bac 100644
--- a/dashboard/src/templates/snapshot_workflow/steps/select_host.html
+++ b/dashboard/src/templates/snapshot_workflow/steps/select_host.html
@@ -1,7 +1,7 @@
{% extends "workflow/viewport-element.html" %}
{% load staticfiles %}
-{% load bootstrap3 %}
+{% load bootstrap4 %}
{% block content %}
diff --git a/dashboard/src/templates/workflow/confirm.html b/dashboard/src/templates/workflow/confirm.html
index a234a71..c1f3440 100644
--- a/dashboard/src/templates/workflow/confirm.html
+++ b/dashboard/src/templates/workflow/confirm.html
@@ -1,7 +1,7 @@
{% extends "workflow/viewport-element.html" %}
{% load staticfiles %}
-{% load bootstrap3 %}
+{% load bootstrap4 %}
{% block content %}
diff --git a/dashboard/src/templates/workflow/resource_select.html b/dashboard/src/templates/workflow/resource_select.html
index c319ff5..cd04137 100644
--- a/dashboard/src/templates/workflow/resource_select.html
+++ b/dashboard/src/templates/workflow/resource_select.html
@@ -1,7 +1,7 @@
{% extends "workflow/viewport-element.html" %}
{% load staticfiles %}
-{% load bootstrap3 %}
+{% load bootstrap4 %}
{% block content %}
diff --git a/dashboard/src/templates/workflow/viewport-base.html b/dashboard/src/templates/workflow/viewport-base.html
index 4608ef9..aa01d7e 100644
--- a/dashboard/src/templates/workflow/viewport-base.html
+++ b/dashboard/src/templates/workflow/viewport-base.html
@@ -1,12 +1,12 @@
{% extends "base.html" %}
{% load staticfiles %}
-{% load bootstrap3 %}
+{% load bootstrap4 %}
{% block content %}
<style>
- .go_btn{
+ .go_btn {
position: absolute;
width: 100px;
@@ -14,43 +14,36 @@
height: calc(100% - 170px);
}
- .go_btn_disabled{
- background-color: #ffffff;
+
+ .go_btn_disabled {
+ background-color: #ffffff;
}
- .go_forward{
+
+ .go_forward {
right: 0px;
border-left: none;
}
- .go_back{
+ .go_back {
left: 251px;
border-right: none;
}
- .btn_wrapper{
+ .btn_wrapper {
text-align: center;
margin-bottom: 5px;
}
{% if DEBUG %}
-
- .add_btn_wrapper{
- right: 130px;
- top: 10px;
- position: absolute;
- }
+ .add_btn_wrapper {
+ right: 130px;
+ top: 10px;
+ position: absolute;
+ }
{% endif %}
-
-
- .options{
- position: absolute;
- top: 60px;
- right: 20px;
- }
-
#breadcrumbs {
margin-bottom: 0;
}
@@ -59,23 +52,25 @@
margin: 0;
}
- .step{
+ .step {
display: inline;
padding: 7px;
margin: 1px;
font-size: 14pt;
cursor: default;
}
+
.step:active {
-webkit-box-shadow: inherit;
box-shadow: inherit;
}
+
.step_active:active {
-webkit-box-shadow: inherit;
box-shadow: inherit;
}
- .step_active{
+ .step_active {
display: inline;
padding: 7px;
margin: 1px;
@@ -85,26 +80,22 @@
border-bottom: 4px solid #41ba78 !important;
}
- .step_hidden
- {
+ .step_hidden {
background: #EFEFEF;
color: #999999;
}
- .step_invalid::after
- {
+ .step_invalid::after {
content: " \2612";
color: #CC3300;
}
- .step_valid::after
- {
+ .step_valid::after {
content: " \2611";
color: #41ba78;
}
- .step_untouched::after
- {
+ .step_untouched::after {
content: " \2610";
}
@@ -126,55 +117,153 @@
background-color: inherit;
}
- #breadcrumbs.breadcrumb > li {
+ #breadcrumbs.breadcrumb>li {
border: 1px solid #cccccc;
border-left: none;
}
- #breadcrumbs.breadcrumb > li:first-child {
+
+ #breadcrumbs.breadcrumb>li:first-child {
border-left: 1px solid #cccccc;
}
- #breadcrumbs.breadcrumb > li + li:before {
+
+ #breadcrumbs.breadcrumb>li+li:before {
content: "";
width: 0;
margin: 0;
padding: 0;
}
-</style>
-<button id="gof" onclick="go('next')" class="btn go_btn go_forward">Go Forward</button>
-<button id="gob" onclick="go('prev')" class="btn go_btn go_back">Go Back</button>
+ #topPagination .topcrumb {
+ flex: 1 1 0;
+ display: flex;
+ align-content: center;
+ justify-content: center;
+ border: 1px solid #dee2e6;
+ border-left: none;
+ }
+
+ .topcrumb > span {
+ color: #343a40;
+ cursor: default;
+ }
-<div class="options">
- <button id="cancel_btn" class="btn btn-primary" onclick="cancel_wf()">Cancel</button>
+ .topcrumb.active > span {
+ background: #007bff;
+ color: white;
+ }
+
+ .topcrumb.disabled > span {
+ color: #6c757d;
+ background: #f8f9fa;
+ }
+</style>
+<!-- Pagination -->
+<div class="row mt-3">
+ <div class="col">
+ <nav>
+ <ul class="pagination d-flex flex-row" id="topPagination">
+ <li class="page-item flex-shrink-1 page-control">
+ <a class="page-link" href="#" id="gob" onclick="go('prev')">
+ <i class="fas fa-backward"></i> Back
+ </a>
+ </li>
+ <li class="page-item flex-grow-1 active">
+ <a class="page-link disabled" href="#">
+ Select <i class="far fa-check-square"></i>
+ </a>
+ </li>
+ <li class="page-item flex-grow-1">
+ <a class="page-link disabled" href="#">
+ Configure <i class="far fa-square"></i>
+ </a>
+ </li>
+ <li class="page-item flex-grow-1">
+ <a class="page-link disabled" href="#">
+ Information <i class="far fa-square"></i>
+ </a>
+ </li>
+ <li class="page-item flex-grow-1">
+ <a class="page-link disabled" href="#">
+ OPNFV <i class="far fa-square"></i>
+ </a>
+ </li>
+ <li class="page-item flex-grow-1">
+ <a class="page-link disabled" href="#">
+ Confirm <i class="far fa-square"></i>
+ </a>
+ </li>
+ <li class="page-item flex-shrink-1 page-control">
+ <a class="page-link text-right" href="#" id="gof" onclick="go('next')">
+ Next <i class="fas fa-forward"></i>
+ </a>
+ </li>
+ </ul>
+ </nav>
+ </div>
+</div>
+<!-- Top header -->
+<div class="row px-4">
+ <div class="col">
+ <div id="iframe_header" class="row view-header">
+ <div class="col-lg-12 step_header">
+ <h1 class="step_title d-inline-block" id="view_title"></h1>
+ <span class="description text-muted" id="view_desc"></span>
+ <p class="step_message" id="view_message"></p>
+ </div>
+ <script>
+ function update_description(title, desc) {
+ document.getElementById("view_title").innerText = title;
+ document.getElementById("view_desc").innerText = desc;
+ }
+
+ function update_message(message, stepstatus) {
+ document.getElementById("view_message").innerText = message;
+ document.getElementById("view_message").className = "step_message";
+ document.getElementById("view_message").classList.add("message_" + stepstatus);
+ }
+ </script>
+ <!-- /.col-lg-12 -->
+ </div>
+ </div>
+ <div class="col-auto align-self-center d-flex">
+ <button id="cancel_btn" class="btn btn-danger ml-auto" onclick="cancel_wf()">Cancel</button>
+ </div>
+</div>
+<!-- Content here -->
+<div class="row d-flex flex-column flex-grow-1">
+ <div class="container-fluid d-flex flex-column h-100">
+ <div class="row d-flex flex-grow-1 p-4">
+ <!-- iframe workflow -->
+ <div class="col-12 d-flex border flex-grow-1">
+ <!-- This was where the iframe went -->
+ <iframe src="/wf/workflow" class="w-100 h-100" scrolling="yes" id="viewport-iframe"
+ frameBorder="0"></iframe>
+ </div>
+ </div>
+ </div>
</div>
<div class="btn_wrapper">
-<ol id="breadcrumbs" class="btn-group breadcrumb">
-</ol>
</div>
{% csrf_token %}
<script type="text/javascript">
-
-
update_context();
var step = 0;
var page_count = 0;
var context_data = false;
- function go(to)
- {
+ function go(to) {
step_on_leave();
request_leave(to);
}
- function request_leave(to)
- {
+ function request_leave(to) {
$.ajax({
type: "GET",
url: "/wf/manager/",
- beforeSend: function(request) {
+ beforeSend: function (request) {
request.setRequestHeader("X-CSRFToken",
- $('input[name="csrfmiddlewaretoken"]').val());
+ $('input[name="csrfmiddlewaretoken"]').val());
},
success: function (data) {
confirm_permission(to, data);
@@ -183,48 +272,44 @@
});
}
- function confirm_permission(to, data)
- {
- if( errors_exist(data) )
- {
- if( to != "prev" )
- {
+ function confirm_permission(to, data) {
+ if (errors_exist(data)) {
+ if (to != "prev") {
return;
}
}
- var problem = function() {
+ var problem = function () {
alert("There was a problem");
}
//makes an asynch request
req = new XMLHttpRequest();
url = "/wf/workflow/?step=" + to;
req.open("GET", url, true);
- req.onload = function(e) {
- if(req.readyState === 4){
- if(req.status < 300){
+ req.onload = function (e) {
+ if (req.readyState === 4) {
+ if (req.status < 300) {
document.getElementById("viewport-iframe").srcdoc = this.responseText;
- } else { problem(); }
- } else { problem(); }
+ } else {
+ problem();
+ }
+ } else {
+ problem();
+ }
}
req.onerror = problem;
req.send();
}
- function step_on_leave()
- {
+ function step_on_leave() {
document.getElementById("viewport-iframe").contentWindow.step_on_leave();
}
- function errors_exist(data)
- {
+ function errors_exist(data) {
var stat = data['steps'][data['active']]['valid'];
- if( stat >= 100 && stat < 200 )
- {
+ if (stat >= 100 && stat < 200) {
return true;
- }
- else
- {
+ } else {
return false;
}
}
@@ -233,9 +318,9 @@
$.ajax({
type: "GET",
url: "/wf/manager/",
- beforeSend: function(request) {
+ beforeSend: function (request) {
request.setRequestHeader("X-CSRFToken",
- $('input[name="csrfmiddlewaretoken"]').val());
+ $('input[name="csrfmiddlewaretoken"]').val());
},
success: function (data) {
update_page(data);
@@ -243,120 +328,100 @@
});
}
- function update_page(data)
- {
+ function update_page(data) {
context_data = data;
update_breadcrumbs(data);
- if(data["workflow_count"] == 1)
- {
- document.getElementById("cancel_btn").innerText = "Exit Workflow";
- }
- else
- {
- document.getElementById("cancel_btn").innerText = "Return to Parent";
+ if (data["workflow_count"] == 1) {
+ document.getElementById("cancel_btn").innerText = "Exit Workflow";
+ } else {
+ document.getElementById("cancel_btn").innerText = "Return to Parent";
}
}
function update_breadcrumbs(meta_json) {
step = meta_json['active'];
page_count = meta_json['steps'].length;
- if( step == 0 )
- {
- var btn = document.getElementById("gob");
- btn.classList.add("go_btn_disabled");
- btn.disabled = true;
- }
- else
- {
- var btn = document.getElementById("gob");
- btn.classList.remove("go_btn_disabled");
- btn.disabled = false;
+ if (step == 0) {
+ var btn = document.getElementById("gob");
+ btn.classList.add("invisible");
+ btn.disabled = true;
+ } else {
+ var btn = document.getElementById("gob");
+ btn.classList.remove("invisible");
+ btn.disabled = false;
}
- if( step == page_count - 1 )
- {
- var btn = document.getElementById("gof");
- btn.classList.add("go_btn_disabled");
- btn.disabled = true;
- }
- else
- {
- var btn = document.getElementById("gof");
- btn.classList.remove("go_btn_disabled");
- btn.disabled = false;
+ if (step == page_count - 1) {
+ var btn = document.getElementById("gof");
+ btn.classList.add("invisible");
+ btn.disabled = true;
+ } else {
+ var btn = document.getElementById("gof");
+ btn.classList.remove("invisible");
+ btn.disabled = false;
}
//remove all children of breadcrumbs so we can redraw
- var container = document.getElementById("breadcrumbs");
- while(container.firstChild){
- container.removeChild(container.firstChild);
- }
-
+ $("#topPagination").children().not(".page-control").remove();
draw_steps(meta_json);
}
- function draw_steps(meta_json){
- for( var i = 0; i < meta_json["steps"].length; i++ )
- {
+ function draw_steps(meta_json) {
+ for (var i = 0; i < meta_json["steps"].length; i++) {
meta_json["steps"][i]["index"] = i;
var step_btn = create_step(meta_json["steps"][i], i == meta_json["active"]);
- document.getElementById("breadcrumbs").appendChild(step_btn);
+ $("#topPagination li:last-child").before(step_btn);
}
}
- function create_step(step_json, active){
+ function create_step(step_json, active) {
var step_dom = document.createElement("li");
- if(active){
- step_dom.className = "step_active";
-
- } else{
- step_dom.className = "step";
+ // First create the dom object depending on active or not
+ if (active) {
+ step_dom.className = "topcrumb active";
+ } else {
+ step_dom.className = "topcrumb";
}
- step_dom.appendChild(document.createTextNode(step_json['title']));
+ $(step_dom).html(`<span class="d-flex align-items-center justify-content-center text-capitalize w-100">${step_json['title']}</span>`)
var code = step_json['valid'];
stat = "";
msg = "";
- if( code < 100 )
- {
- step_dom.classList.add("step_untouched");
-
+ if (code < 100) {
+ $(step_dom).children().first().append("<i class='ml-2 far fa-square'></i>")
stat = "";
msg = "";
- }
- else if( code < 200 )
- {
- step_dom.classList.add("step_invalid");
+ } 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.classList.add("step_valid");
+ } 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("step_hidden");
+ if (step_json['enabled'] == false) {
+ step_dom.classList.add("disabled");
}
- if(active)
- {
+ if (active) {
update_message(msg, stat);
}
- step_dom.classList.add("btn");
var step_number = step_json['index'];
return step_dom;
}
- function cancel_wf(){
+ function cancel_wf() {
var form = $("#workflow_pop_form");
var formData = form.serialize();
var req = new XMLHttpRequest();
req.open("POST", "/wf/workflow/finish/", false);
req.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
- req.onerror = function() { alert("problem occurred while trying to cancel current workflow"); }
- req.onreadystatechange = function() { if(req.readyState === 4){
- refresh_iframe();
- }};
+ req.onerror = function () {
+ alert("problem occurred while trying to cancel current workflow");
+ }
+ req.onreadystatechange = function () {
+ if (req.readyState === 4) {
+ refresh_iframe();
+ }
+ };
req.send(formData);
}
@@ -364,43 +429,45 @@
req = new XMLHttpRequest();
url = "/wf/workflow/";
req.open("GET", url, true);
- req.onload = function(e) {
+ req.onload = function (e) {
var doc = document.getElementById("viewport-iframe").contentWindow.document;
- doc.open(); doc.write(this.responseText); doc.close();
+ doc.open();
+ doc.write(this.responseText);
+ doc.close();
}
req.send();
}
- function write_iframe(contents)
- {
- document.getElementById("viewport-iframe").contentWindow.document.innerHTML= contents;
+ function write_iframe(contents) {
+ document.getElementById("viewport-iframe").contentWindow.document.innerHTML = contents;
}
- function redirect_root()
- {
+ function redirect_root() {
window.location.replace('/wf/');
}
- function add_wf(type){
+ function add_wf(type) {
add_wf_internal(type, false);
}
- function add_edit_wf(type, target){
+ function add_edit_wf(type, target) {
add_wf_internal(type, target);
}
- function add_wf_internal(type, itemid){
- data = {"add": type};
- if(itemid){
+ function add_wf_internal(type, itemid) {
+ data = {
+ "add": type
+ };
+ if (itemid) {
data['target'] = itemid;
}
$.ajax({
type: "POST",
url: "/wf/manager/",
data: data,
- beforeSend: function(request) {
+ beforeSend: function (request) {
request.setRequestHeader("X-CSRFToken",
- $('input[name="csrfmiddlewaretoken"]').val()
+ $('input[name="csrfmiddlewaretoken"]').val()
);
},
success: refresh_wf_iframe()
@@ -408,66 +475,12 @@
}
function refresh_wf_iframe() {
- window.location=window.location;
+ window.location = window.location;
}
</script>
-<div id="iframe_header" class="row view-header">
- <div class="col-lg-12 step_header">
- <h1 class="step_title" id="view_title"></h1>
- <p class="description" id="view_desc"></p>
- <p class="step_message" id="view_message"></p>
- </div>
- <style>
- #view_desc{
- margin-bottom: 15px;
- margin-top: 5px;
- margin-left: 30px;
- display: inline;
- }
- #view_title{
- margin-top: 5px;
- margin-bottom: 0px;
- display: inline;
- }
- #view_message{
- margin-top: 10px;
- margin-bottom: 5px;
- float: right;
- }
- .message_invalid{
- color: #ff4400;
- }
- .message_valid{
- color: #44cc00;
- }
- .step_header{
- border-bottom: 1px solid #eee;
- border-top: 1px solid #eee;
- left: 101px;
- width: calc(100% - 202px);
- }
- </style>
- <script>
- function update_description(title, desc){
- document.getElementById("view_title").innerText = title;
- document.getElementById("view_desc").innerText = desc;
- }
- function update_message(message, stepstatus){
- document.getElementById("view_message").innerText = message;
- document.getElementById("view_message").className = "step_message";
- document.getElementById("view_message").classList.add("message_" + stepstatus);
- }
-
- </script>
- <!-- /.col-lg-12 -->
-</div>
<div style="display: none;" id="workflow_pop_form_div">
-<form id="workflow_pop_form" action="/wf/workflow/finish/" method="post">
- {% csrf_token %}
-</form>
-</div>
-
-<div class="iframe_div">
- <iframe src="/wf/workflow" class="iframe_elem" scrolling="yes" id="viewport-iframe"></iframe>
+ <form id="workflow_pop_form" action="/wf/workflow/finish/" method="post">
+ {% csrf_token %}
+ </form>
</div>
-{% endblock content %}
+{% endblock content %} \ No newline at end of file
diff --git a/dashboard/src/templates/workflow/viewport-element.html b/dashboard/src/templates/workflow/viewport-element.html
index f25e644..7a7165a 100644
--- a/dashboard/src/templates/workflow/viewport-element.html
+++ b/dashboard/src/templates/workflow/viewport-element.html
@@ -1,5 +1,5 @@
{% extends "layout.html" %}
-{% load bootstrap3 %}
+{% load bootstrap4 %}
{% load staticfiles %}
{% block basecontent %}
diff --git a/dashboard/src/workflow/forms.py b/dashboard/src/workflow/forms.py
index 0fb45d6..ee44ecd 100644
--- a/dashboard/src/workflow/forms.py
+++ b/dashboard/src/workflow/forms.py
@@ -242,124 +242,101 @@ class BookingMetaForm(forms.Form):
class MultipleSelectFilterWidget(forms.Widget):
- def __init__(self, attrs=None):
- super(MultipleSelectFilterWidget, self).__init__(attrs)
- self.attrs = attrs
+ def __init__(self, *args, display_objects=None, filter_items=None, neighbors=None, **kwargs):
+ super(MultipleSelectFilterWidget, self).__init__(*args, **kwargs)
+ self.display_objects = display_objects
+ self.filter_items = filter_items
+ self.neighbors = neighbors
self.template_name = "dashboard/multiple_select_filter_widget.html"
def render(self, name, value, attrs=None, renderer=None):
- attrs = self.attrs
- self.context = self.get_context(name, value, attrs)
- html = render_to_string(self.template_name, context=self.context)
+ context = self.get_context(name, value, attrs)
+ html = render_to_string(self.template_name, context=context)
return mark_safe(html)
def get_context(self, name, value, attrs):
- return attrs
+ return {
+ 'display_objects': self.display_objects,
+ 'neighbors': self.neighbors,
+ 'filter_items': self.filter_items,
+ 'initial_value': value
+ }
class MultipleSelectFilterField(forms.Field):
- def __init__(self, required=True, widget=None, label=None, initial=None,
- help_text='', error_messages=None, show_hidden_initial=False,
- validators=(), localize=False, disabled=False, label_suffix=None):
- """from the documentation:
- # required -- Boolean that specifies whether the field is required.
- # True by default.
- # widget -- A Widget class, or instance of a Widget class, that should
- # be used for this Field when displaying it. Each Field has a
- # default Widget that it'll use if you don't specify this. In
- # most cases, the default widget is TextInput.
- # label -- A verbose name for this field, for use in displaying this
- # field in a form. By default, Django will use a "pretty"
- # version of the form field name, if the Field is part of a
- # Form.
- # initial -- A value to use in this Field's initial display. This value
- # is *not* used as a fallback if data isn't given.
- # help_text -- An optional string to use as "help; text" for this Field.
- # error_messages -- An optional dictionary to override the default
- # messages that the field will raise.
- # show_hidden_initial -- Boolean that specifies if it is needed to render a
- # hidden widget with initial value after widget.
- # validators -- List of additional validators to use
- # localize -- Boolean that specifies if the field should be localized.
- # disabled -- Boolean that specifies whether the field is disabled, that
- # is its widget is shown in the form but not editable.
- # label_suffix -- Suffix to be added to the label. Overrides
- # form's label_suffix.
- """
- # this is bad, but django forms are annoying
- self.widget = widget
- if self.widget is None:
- self.widget = MultipleSelectFilterWidget()
- super(MultipleSelectFilterField, self).__init__(
- required=required,
- widget=self.widget,
- label=label,
- initial=None,
- help_text=help_text,
- error_messages=error_messages,
- show_hidden_initial=show_hidden_initial,
- validators=validators,
- localize=localize,
- disabled=disabled,
- label_suffix=label_suffix
- )
+ def __init__(self, **kwargs):
+ self.initial = kwargs.get("initial")
+ super().__init__(**kwargs)
- def clean(data):
- """
- This method will raise a django.forms.ValidationError or return clean data
- """
- return data
+ def to_python(self, value):
+ return json.loads(value)
class FormUtils:
@staticmethod
- def getLabData(multiple_selectable_hosts):
+ def getLabData(multiple_hosts=False):
"""
Gets all labs and thier host profiles and returns a serialized version the form can understand.
Should be rewritten with a related query to make it faster
- Should be moved outside of global scope
"""
+ # javascript truthy variables
+ true = 1
+ false = 0
+ if multiple_hosts:
+ multiple_hosts = true
+ else:
+ multiple_hosts = false
labs = {}
hosts = {}
items = {}
- mapping = {}
+ neighbors = {}
for lab in Lab.objects.all():
- slab = {}
- slab['id'] = "lab_" + str(lab.lab_user.id)
- slab['name'] = lab.name
- slab['description'] = lab.description
- slab['selected'] = 0
- slab['selectable'] = 1
- slab['follow'] = 1
- if not multiple_selectable_hosts:
- slab['follow'] = 0
- slab['multiple'] = 0
- items[slab['id']] = slab
- mapping[slab['id']] = []
- labs[slab['id']] = slab
+ lab_node = {
+ 'id': "lab_" + str(lab.lab_user.id),
+ 'model_id': lab.lab_user.id,
+ 'name': lab.name,
+ 'description': lab.description,
+ 'selected': false,
+ 'selectable': true,
+ 'follow': false,
+ 'multiple': false,
+ 'class': 'lab'
+ }
+ if multiple_hosts:
+ # "follow" this lab node to discover more hosts if allowed
+ lab_node['follow'] = true
+ items[lab_node['id']] = lab_node
+ neighbors[lab_node['id']] = []
+ labs[lab_node['id']] = lab_node
+
for host in lab.hostprofiles.all():
- shost = {}
- shost['forms'] = [{"name": "host_name", "type": "text", "placeholder": "hostname"}]
- shost['id'] = "host_" + str(host.id)
- shost['name'] = host.name
- shost['description'] = host.description
- shost['selected'] = 0
- shost['selectable'] = 1
- shost['follow'] = 0
- shost['multiple'] = multiple_selectable_hosts
- items[shost['id']] = shost
- mapping[slab['id']].append(shost['id'])
- if shost['id'] not in mapping:
- mapping[shost['id']] = []
- mapping[shost['id']].append(slab['id'])
- hosts[shost['id']] = shost
-
- filter_objects = [("labs", labs.values()), ("hosts", hosts.values())]
+ host_node = {
+ 'form': {"name": "host_name", "type": "text", "placeholder": "hostname"},
+ 'id': "host_" + str(host.id),
+ 'model_id': host.id,
+ 'name': host.name,
+ 'description': host.description,
+ 'selected': false,
+ 'selectable': true,
+ 'follow': false,
+ 'multiple': multiple_hosts,
+ 'class': 'host'
+ }
+ if multiple_hosts:
+ host_node['values'] = [] # place to store multiple values
+ items[host_node['id']] = host_node
+ neighbors[lab_node['id']].append(host_node['id'])
+ if host_node['id'] not in neighbors:
+ neighbors[host_node['id']] = []
+ neighbors[host_node['id']].append(lab_node['id'])
+ hosts[host_node['id']] = host_node
+
+ display_objects = [("lab", labs.values()), ("host", hosts.values())]
context = {
- 'filter_objects': filter_objects,
- 'mapping': mapping,
+ 'display_objects': display_objects,
+ 'neighbors': neighbors,
'filter_items': items
}
return context
@@ -368,14 +345,10 @@ class FormUtils:
class HardwareDefinitionForm(forms.Form):
def __init__(self, *args, **kwargs):
- selection_data = kwargs.pop("selection_data", False)
super(HardwareDefinitionForm, self).__init__(*args, **kwargs)
- attrs = FormUtils.getLabData(1)
- attrs['selection_data'] = selection_data
+ attrs = FormUtils.getLabData(multiple_hosts=True)
self.fields['filter_field'] = MultipleSelectFilterField(
- widget=MultipleSelectFilterWidget(
- attrs=attrs
- )
+ widget=MultipleSelectFilterWidget(**attrs)
)
diff --git a/dashboard/src/workflow/resource_bundle_workflow.py b/dashboard/src/workflow/resource_bundle_workflow.py
index ced355f..06737d2 100644
--- a/dashboard/src/workflow/resource_bundle_workflow.py
+++ b/dashboard/src/workflow/resource_bundle_workflow.py
@@ -52,65 +52,47 @@ class Define_Hardware(WorkflowStep):
description = "Choose the type and amount of machines you want"
short_title = "hosts"
+ def __init__(self, *args, **kwargs):
+ self.form = None
+ super().__init__(*args, **kwargs)
+
def get_context(self):
context = super(Define_Hardware, self).get_context()
- selection_data = {"hosts": {}, "labs": {}}
- models = self.repo_get(self.repo.GRESOURCE_BUNDLE_MODELS, {})
- hosts = models.get("hosts", [])
- for host in hosts:
- profile_id = "host_" + str(host.profile.id)
- if profile_id not in selection_data['hosts']:
- selection_data['hosts'][profile_id] = []
- selection_data['hosts'][profile_id].append({"host_name": host.resource.name, "class": profile_id})
-
- if models.get("bundle", GenericResourceBundle()).lab:
- selection_data['labs'] = {"lab_" + str(models.get("bundle").lab.lab_user.id): "true"}
-
- form = HardwareDefinitionForm(
- selection_data=selection_data
- )
- context['form'] = form
+ context['form'] = self.form or HardwareDefinitionForm()
return context
- def render(self, request):
- self.context = self.get_context()
- return render(request, self.template, self.context)
-
def update_models(self, data):
- data = json.loads(data['filter_field'])
+ data = data['filter_field']
models = self.repo_get(self.repo.GRESOURCE_BUNDLE_MODELS, {})
models['hosts'] = [] # This will always clear existing data when this step changes
models['interfaces'] = {}
if "bundle" not in models:
models['bundle'] = GenericResourceBundle(owner=self.repo_get(self.repo.SESSION_USER))
- host_data = data['hosts']
+ host_data = data['host']
names = {}
- for host_dict in host_data:
- id = host_dict['class']
- # bit of formatting
- id = int(id.split("_")[-1])
+ for host_profile_dict in host_data.values():
+ id = host_profile_dict['id']
profile = HostProfile.objects.get(id=id)
# instantiate genericHost and store in repo
- name = host_dict['host_name']
- if not re.match(r"(?=^.{1,253}$)(^([A-Za-z0-9-_]{1,62}\.)*[A-Za-z0-9-_]{1,63})", name):
- raise InvalidHostnameException("Hostname must comply to RFC 952 and all extensions to it until this point")
- if name in names:
- raise NonUniqueHostnameException("All hosts must have unique names")
- names[name] = True
- genericResource = GenericResource(bundle=models['bundle'], name=name)
- genericHost = GenericHost(profile=profile, resource=genericResource)
- models['hosts'].append(genericHost)
- for interface_profile in profile.interfaceprofile.all():
- genericInterface = GenericInterface(profile=interface_profile, host=genericHost)
- if genericHost.resource.name not in models['interfaces']:
- models['interfaces'][genericHost.resource.name] = []
- models['interfaces'][genericHost.resource.name].append(genericInterface)
+ for name in host_profile_dict['values'].values():
+ if not re.match(r"(?=^.{1,253}$)(^([A-Za-z0-9-_]{1,62}\.)*[A-Za-z0-9-_]{1,63})", name):
+ raise InvalidHostnameException("Invalid hostname: '" + name + "'")
+ if name in names:
+ raise NonUniqueHostnameException("All hosts must have unique names")
+ names[name] = True
+ genericResource = GenericResource(bundle=models['bundle'], name=name)
+ genericHost = GenericHost(profile=profile, resource=genericResource)
+ models['hosts'].append(genericHost)
+ for interface_profile in profile.interfaceprofile.all():
+ genericInterface = GenericInterface(profile=interface_profile, host=genericHost)
+ if genericHost.resource.name not in models['interfaces']:
+ models['interfaces'][genericHost.resource.name] = []
+ models['interfaces'][genericHost.resource.name].append(genericInterface)
# add selected lab to models
- for lab_dict in data['labs']:
- if list(lab_dict.values())[0]: # True for lab the user selected
- lab_user_id = int(list(lab_dict.keys())[0].split("_")[-1])
- models['bundle'].lab = Lab.objects.get(lab_user__id=lab_user_id)
+ for lab_dict in data['lab'].values():
+ if lab_dict['selected']:
+ models['bundle'].lab = Lab.objects.get(lab_user__id=lab_dict['id'])
break # if somehow we get two 'true' labs, we only use one
# return to repo
@@ -133,15 +115,11 @@ class Define_Hardware(WorkflowStep):
try:
self.form = HardwareDefinitionForm(request.POST)
if self.form.is_valid():
- if len(json.loads(self.form.cleaned_data['filter_field'])['labs']) != 1:
- self.set_invalid("Please select one lab")
- else:
- self.update_models(self.form.cleaned_data)
- self.update_confirmation()
- self.set_valid("Step Completed")
+ self.update_models(self.form.cleaned_data)
+ self.update_confirmation()
+ self.set_valid("Step Completed")
else:
self.set_invalid("Please complete the fields highlighted in red to continue")
- pass
except Exception as e:
self.set_invalid(str(e))
self.context = self.get_context()
@@ -173,54 +151,55 @@ class Define_Nets(WorkflowStep):
except Exception:
return None
+ def make_mx_host_dict(self, generic_host):
+ host = {
+ 'id': generic_host.resource.name,
+ 'interfaces': [],
+ 'value': {
+ "name": generic_host.resource.name,
+ "description": generic_host.profile.description
+ }
+ }
+ for iface in generic_host.profile.interfaceprofile.all():
+ host['interfaces'].append({
+ "name": iface.name,
+ "description": "speed: " + str(iface.speed) + "M\ntype: " + iface.nic_type
+ })
+ return host
+
def get_context(self):
- # TODO: render *primarily* on hosts in repo models
context = super(Define_Nets, self).get_context()
- context['form'] = NetworkDefinitionForm()
+ context.update({
+ 'form': NetworkDefinitionForm(),
+ 'debug': settings.DEBUG,
+ 'hosts': [],
+ 'added_hosts': [],
+ 'removed_hosts': []
+ })
+ vlans = self.get_vlans()
+ if vlans:
+ context['vlans'] = vlans
try:
- context['hosts'] = []
models = self.repo_get(self.repo.GRESOURCE_BUNDLE_MODELS, {})
- vlans = self.get_vlans()
- if vlans:
- context['vlans'] = vlans
hosts = models.get("hosts", [])
- hostlist = self.repo_get(self.repo.GRB_LAST_HOSTLIST, None)
- added_list = []
- added_dict = {}
- context['debug'] = settings.DEBUG
- context['added_hosts'] = []
- if hostlist is not None:
- new_hostlist = []
- for host in models['hosts']:
- intcount = host.profile.interfaceprofile.count()
- new_hostlist.append(str(host.resource.name) + "*" + str(host.profile) + "&" + str(intcount))
- context['removed_hosts'] = list(set(hostlist) - set(new_hostlist))
- added_list = list(set(new_hostlist) - set(hostlist))
- for hoststr in added_list:
- key = hoststr.split("*")[0]
- added_dict[key] = hoststr
+ # calculate if the selected hosts have changed
+ added_hosts = set()
+ host_set = set(self.repo_get(self.repo.GRB_LAST_HOSTLIST, []))
+ if len(host_set):
+ new_host_set = set([h.resource.name + "*" + h.profile.name for h in models['hosts']])
+ context['removed_hosts'] = [h.split("*")[0] for h in (host_set - new_host_set)]
+ added_hosts.update([h.split("*")[0] for h in (new_host_set - host_set)])
+
+ # add all host info to context
for generic_host in hosts:
- host_profile = generic_host.profile
- host = {}
- host['id'] = generic_host.resource.name
- host['interfaces'] = []
- for iface in host_profile.interfaceprofile.all():
- host['interfaces'].append(
- {
- "name": iface.name,
- "description": "speed: " + str(iface.speed) + "M\ntype: " + iface.nic_type
- }
- )
- host['value'] = {"name": generic_host.resource.name}
- host['value']['description'] = generic_host.profile.description
- context['hosts'].append(json.dumps(host))
- if host['id'] in added_dict:
- context['added_hosts'].append(json.dumps(host))
+ host = self.make_mx_host_dict(generic_host)
+ host_serialized = json.dumps(host)
+ context['hosts'].append(host_serialized)
+ if host['id'] in added_hosts:
+ context['added_hosts'].append(host_serialized)
bundle = models.get("bundle", False)
- if bundle and bundle.xml:
- context['xml'] = bundle.xml
- else:
- context['xml'] = False
+ if bundle:
+ context['xml'] = bundle.xml or False
except Exception:
pass
@@ -230,11 +209,8 @@ class Define_Nets(WorkflowStep):
def post_render(self, request):
models = self.repo_get(self.repo.GRESOURCE_BUNDLE_MODELS, {})
if 'hosts' in models:
- hostlist = []
- for host in models['hosts']:
- intcount = host.profile.interfaceprofile.count()
- hostlist.append(str(host.resource.name) + "*" + str(host.profile) + "&" + str(intcount))
- self.repo_put(self.repo.GRB_LAST_HOSTLIST, hostlist)
+ host_set = set([h.resource.name + "*" + h.profile.name for h in models['hosts']])
+ self.repo_put(self.repo.GRB_LAST_HOSTLIST, host_set)
try:
xmlData = request.POST.get("xml")
self.updateModels(xmlData)