diff options
Diffstat (limited to 'src')
65 files changed, 2016 insertions, 3518 deletions
diff --git a/src/api/urls.py b/src/api/urls.py index 7a48425..778f6eb 100644 --- a/src/api/urls.py +++ b/src/api/urls.py @@ -24,13 +24,10 @@ Including another URLconf 1. Import the include() function: from django.conf.urls import url, include 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) """ -from django.conf.urls import url, include +from django.conf.urls import url from django.urls import path -from rest_framework import routers from api.views import ( - BookingViewSet, - UserViewSet, lab_profile, lab_status, lab_inventory, @@ -46,12 +43,7 @@ from api.views import ( GenerateTokenView ) -router = routers.DefaultRouter() -router.register(r'bookings', BookingViewSet) -router.register(r'user', UserViewSet) - urlpatterns = [ - url(r'^', include(router.urls)), path('labs/<slug:lab_name>/profile', lab_profile), path('labs/<slug:lab_name>/status', lab_status), path('labs/<slug:lab_name>/inventory', lab_inventory), diff --git a/src/booking/tests/test_quick_booking.py b/src/booking/tests/test_quick_booking.py index e445860..5ba1744 100644 --- a/src/booking/tests/test_quick_booking.py +++ b/src/booking/tests/test_quick_booking.py @@ -8,6 +8,7 @@ ############################################################################## import datetime +import json from django.test import TestCase, Client @@ -30,7 +31,9 @@ from dashboard.testing_utils import ( class QuickBookingValidFormTestCase(TestCase): @classmethod def setUpTestData(cls): - cls.user = make_user(False, username="newtestuser", password="testpassword") + cls.user = make_user(False, username="newtestuser") + cls.user.set_password("testpassword") + cls.user.save() make_user_profile(cls.user, True) lab_user = make_user(True) @@ -51,7 +54,20 @@ class QuickBookingValidFormTestCase(TestCase): @classmethod def build_post_data(cls): return { - 'filter_field': '{"hosts":[{"host_' + str(cls.host_profile.id) + '":"true"}], "labs": [{"lab_' + str(cls.lab.lab_user.id) + '":"true"}]}', + 'filter_field': json.dumps({ + "host": { + "host_" + str(cls.host_profile.id): { + "selected": True, + "id": cls.host_profile.id + } + }, + "lab": { + "lab_" + str(cls.lab.lab_user.id): { + "selected": True, + "id": cls.lab.lab_user.id + } + } + }), 'purpose': 'my_purpose', 'project': 'my_project', 'length': '3', @@ -70,7 +86,7 @@ class QuickBookingValidFormTestCase(TestCase): return response def setUp(self): - self.client.login(username=self.user.username, password="testpassword") + self.client.login(username="newtestuser", password="testpassword") def assertValidBooking(self, booking): self.assertEqual(booking.owner, self.user) @@ -116,13 +132,40 @@ class QuickBookingValidFormTestCase(TestCase): self.assertIsNone(Booking.objects.first()) def test_with_invalid_host_id(self): - response = self.post({'filter_field': '{"hosts":[{"host_' + str(self.host_profile.id + 100) + '":"true"}], "labs": [{"lab_' + str(self.lab.lab_user.id) + '":"true"}]}'}) + response = self.post({'filter_field': json.dumps({ + "host": { + "host_" + str(self.host_profile.id + 100): { + "selected": True, + "id": self.host_profile.id + 100 + } + }, + "lab": { + "lab_" + str(self.lab.lab_user.id): { + "selected": True, + "id": self.lab.lab_user.id + } + } + })}) self.assertEqual(response.status_code, 200) self.assertIsNone(Booking.objects.first()) def test_with_invalid_lab_id(self): response = self.post({'filter_field': '{"hosts":[{"host_' + str(self.host_profile.id) + '":"true"}], "labs": [{"lab_' + str(self.lab.lab_user.id + 100) + '":"true"}]}'}) + response = self.post({'filter_field': json.dumps({ + "host": { + "host_" + str(self.host_profile.id): { + "selected": True, + "id": self.host_profile.id + } + }, + "lab": { + "lab_" + str(self.lab.lab_user.id + 100): { + "selected": True, + "id": self.lab.lab_user.id + 100 + } + } + })}) self.assertEqual(response.status_code, 200) self.assertIsNone(Booking.objects.first()) @@ -134,17 +177,16 @@ class QuickBookingValidFormTestCase(TestCase): self.assertIsNone(Booking.objects.first()) def test_with_garbage_users_field(self): # expected behavior: treat as though field is empty if it has garbage data - response = self.post({'users': 'X�]QP�槰DP�+m���h�U�_�yJA:.rDi��QN|.��C��n�P��F!��D�����5ȅj�9�LV��'}) # output from /dev/urandom + response = self.post({'users': ['X�]QP�槰DP�+m���h�U�_�yJA:.rDi��QN|.��C��n�P��F!��D�����5ȅj�9�LV��']}) # output from /dev/urandom self.assertEqual(response.status_code, 200) booking = Booking.objects.first() - self.assertIsNotNone(booking) - self.assertValidBooking(booking) + self.assertIsNone(booking) def test_with_valid_form(self): response = self.post() - self.assertEqual(response.status_code, 200) + self.assertEqual(response.status_code, 302) # success should redirect booking = Booking.objects.first() self.assertIsNotNone(booking) self.assertValidBooking(booking) diff --git a/src/dashboard/testing_utils.py b/src/dashboard/testing_utils.py index a96b6d0..0f52daa 100644 --- a/src/dashboard/testing_utils.py +++ b/src/dashboard/testing_utils.py @@ -103,12 +103,16 @@ def make_config_bundle(grb, owner, topology={}, host_set={}, cb = ConfigBundle.objects.create( owner=owner, name="config bundle " + str(ConfigBundle.objects.count()), - description="cb generated by make_config_bundle() method" + description="cb generated by make_config_bundle() method", + bundle=grb ) + scen = scenario or Scenario.objects.first() or make_scenario() + inst = installer or Installer.objects.first() or make_installer([scen]) + opnfv_config = OPNFVConfig.objects.create( - installer=installer, - scenario=scenario, + installer=inst, + scenario=scen, bundle=cb ) @@ -194,7 +198,8 @@ def make_generic_host(grb, host_profile, hostname): def make_user(is_superuser=False, username="testuser", password="testpassword", email="default_email@user.com"): - user = User.objects.create_user(username=username, email=email, password=password) + user = User.objects.get_or_create(username=username, email=email, password=password)[0] + user.is_superuser = is_superuser user.save() @@ -204,14 +209,14 @@ def make_user(is_superuser=False, username="testuser", def make_user_profile(user=None, email_addr="email@email.com", company="company", full_name="John Doe", booking_privledge=True, ssh_file=None): - user = user or User.objects.first() or make_user() - profile = UserProfile.objects.create( + user = user or make_user() + profile = UserProfile.objects.get_or_create( email_addr=email_addr, company=company, full_name=full_name, booking_privledge=booking_privledge, user=user - ) + )[0] profile.ssh_public_key.save("user_ssh_key", ssh_file if ssh_file else ContentFile("public key content string")) return profile @@ -238,7 +243,7 @@ def make_lab(user=None, name="Test_Lab_Instance", vlan_manager = make_vlan_manager() if not user: - user = make_user() + user = make_user(username=name + " user") lab = Lab.objects.create( lab_user=user, @@ -354,7 +359,9 @@ def make_installer(scenarios, name="test installer"): return installer -def make_os(installers, name="test OS"): +def make_os(installers=None, name="test OS"): + if not installers: + installers = [make_installer([make_scenario()])] os = Opsys.objects.create(name=name) for installer in installers: os.sup_installers.add(installer) diff --git a/src/dashboard/views.py b/src/dashboard/views.py index aaad7ab..c387251 100644 --- a/src/dashboard/views.py +++ b/src/dashboard/views.py @@ -12,12 +12,10 @@ from django.shortcuts import get_object_or_404 from django.views.generic import TemplateView from django.shortcuts import render -from django.http import HttpResponseRedirect from account.models import Lab from resource_inventory.models import Image, HostProfile -from workflow.views import create_session from workflow.workflow_manager import ManagerTracker @@ -63,36 +61,15 @@ def host_profile_detail_view(request): def landing_view(request): - manager = None - manager_detected = False - if 'manager_session' in request.session: - - try: - manager = ManagerTracker.managers[request.session['manager_session']] - - except KeyError: - pass - - if manager is not None: - # no manager detected, don't display continue button - manager_detected = True - - if request.method == 'GET': - return render(request, 'dashboard/landing.html', {'manager': manager_detected, 'title': "Welcome to the Lab as a Service Dashboard"}) - - if request.method == 'POST': - try: - create = request.POST['create'] - - if manager is not None: - del manager - - mgr_uuid = create_session(create, request=request,) - request.session['manager_session'] = mgr_uuid - return HttpResponseRedirect('/wf/') - - except KeyError: - pass + manager = ManagerTracker.managers.get(request.session.get('manager_session')) + return render( + request, + 'dashboard/landing.html', + { + 'manager': manager is not None, + 'title': "Welcome to the Lab as a Service Dashboard" + } + ) class LandingView(TemplateView): diff --git a/src/pharos_dashboard/settings.py b/src/pharos_dashboard/settings.py index 86de78c..b44fed8 100644 --- a/src/pharos_dashboard/settings.py +++ b/src/pharos_dashboard/settings.py @@ -36,9 +36,8 @@ INSTALLED_APPS = [ 'django.contrib.staticfiles', 'django.contrib.humanize', 'bootstrap4', - 'crispy_forms', 'rest_framework', - 'rest_framework.authtoken', + 'rest_framework.authtoken' ] MIDDLEWARE = [ diff --git a/src/pharos_dashboard/urls.py b/src/pharos_dashboard/urls.py index 8535bed..fd791c3 100644 --- a/src/pharos_dashboard/urls.py +++ b/src/pharos_dashboard/urls.py @@ -32,7 +32,7 @@ from django.contrib import admin urlpatterns = [ - url(r'^wf/', include('workflow.urls', namespace='workflow')), + url(r'^workflow/', include('workflow.urls', namespace='workflow')), url(r'^', include('dashboard.urls', namespace='dashboard')), url(r'^booking/', include('booking.urls', namespace='booking')), url(r'^accounts/', include('account.urls', namespace='account')), diff --git a/src/static/css/base.css b/src/static/css/base.css index c51728c..9fec97e 100644 --- a/src/static/css/base.css +++ b/src/static/css/base.css @@ -1,3 +1,30 @@ +/* Sizing */ +#wrapper { + height: 100vh; +} + +/* Used for turning divs into square */ +.square-20 { + height: 20px; + width: 20px; +} + +/* Make links stay the same color with no underline */ +.discrete-a { + text-decoration: none; + color: inherit; +} + +.discrete-a:hover { + text-decoration: none; + color: inherit; +} + +/* Allow for sidebar to be small, but also resize on small screens */ +.sidebar { + min-width: 200px; +} + /* Rotating arrows when dropdown happens */ i.fas.rotate { transition: transform 0.3s ease-in-out; @@ -6,3 +33,235 @@ i.fas.rotate { a[aria-expanded="true"] > i.rotate { transform: rotate(180deg); } +/* End rotating arrows */ + +/* Start breadcrumbs for workflow */ +#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; +} + +.topcrumb.active > span { + background: #007bff; + color: white; +} + +.topcrumb.disabled > span { + color: #6c757d; + background: #f8f9fa; +} + +/* Booking Node Styles */ +.selected_node { + 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; +} + +/* Cursor effects */ +.not-allowed { + cursor: not-allowed; +} + +/* Used with position-absolute class to make a full height object */ +.topToBottom { + bottom: 0; + top: 0; +} + +.z-2 { + z-index: 2; +} + +.mh-30vh { + max-height: 30vh; +} + +.overflow-ellipsis { + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; +} + +/* Graphing for networks */ +div.mxRubberband { + position: absolute; + overflow: hidden; + border-style: solid; + border-width: 1px; + border-color: #0000FF; + background: #0077FF; +} +.mxCellEditor { + background: url(data:image/gif;base64,R0lGODlhMAAwAIAAAP///wAAACH5BAEAAAAALAAAAAAwADAAAAIxhI+py+0Po5y02ouz3rz7D4biSJbmiabqyrbuC8fyTNf2jef6zvf+DwwKh8Si8egpAAA7); + _background: url('/static/img/mxgraph/transparent.gif'); + border-color: transparent; + border-style: solid; + display: inline-block; + position: absolute; + overflow: visible; + word-wrap: normal; + border-width: 0; + min-width: 1px; + resize: none; + padding: 0px; + margin: 0px; +} +.mxPlainTextEditor * { + padding: 0px; + margin: 0px; +} +div.mxWindow { + background: url('../img/mxgraph/window.gif'); + border:1px solid #c3c3c3; + position: absolute; + overflow: hidden; + z-index: 3; +} +table.mxWindow { + border-collapse: collapse; + table-layout: fixed; + font-family: Arial; + font-size: 8pt; +} +td.mxWindowTitle { + background: url('/static/img/mxgraph/window-title.gif') repeat-x; + text-overflow: ellipsis; + white-space: nowrap; + text-align: center; + font-weight: bold; + overflow: hidden; + height: 13px; + padding: 2px; + padding-top: 4px; + padding-bottom: 6px; + color: black; +} +td.mxWindowPane { + vertical-align: top; + padding: 0px; +} +div.mxWindowPane { + overflow: hidden; + position: relative; +} +td.mxWindowPane td { + font-family: Arial; + font-size: 8pt; +} +td.mxWindowPane input, td.mxWindowPane select, td.mxWindowPane textarea, td.mxWindowPane radio { + font-family: Arial; + font-size: 8pt; + padding: 1px; +} +td.mxWindowPane button { + color: #fff; + background-color: #337ab7; + border-color: #2e6da4; + display: inline-block; + margin: 2%; + font-size: 14px; + font-weight: 400; + line-height: 1.42857143; + text-align: center; + white-space: nowrap; + vertical-align: middle; + -ms-touch-action: manipulation; + touch-action: manipulation; + cursor: pointer; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + background-image: none; + border: 1px solid transparent; + border-radius: 4px; +} +img.mxToolbarItem { + margin-right: 6px; + margin-bottom: 6px; + border-width: 1px; +} +select.mxToolbarCombo { + vertical-align: top; + border-style: inset; + border-width: 2px; +} +div.mxToolbarComboContainer { + padding: 2px; +} +img.mxToolbarMode { + margin: 2px; + margin-right: 4px; + margin-bottom: 4px; + border-width: 0px; +} +img.mxToolbarModeSelected { + margin: 0px; + margin-right: 2px; + margin-bottom: 2px; + border-width: 2px; + border-style: inset; +} +div.mxTooltip { + -webkit-box-shadow: 3px 3px 12px #C0C0C0; + -moz-box-shadow: 3px 3px 12px #C0C0C0; + box-shadow: 3px 3px 12px #C0C0C0; + background: #FFFFCC; + border-style: solid; + border-width: 1px; + border-color: black; + font-family: Arial; + font-size: 8pt; + position: absolute; + cursor: default; + padding: 4px; + color: black; +} +div.mxPopupMenu { + -webkit-box-shadow: 3px 3px 12px #C0C0C0; + -moz-box-shadow: 3px 3px 12px #C0C0C0; + box-shadow: 3px 3px 12px #C0C0C0; + background: url('/static/img/mxgraph/window.gif'); + position: absolute; + border-style: solid; + border-width: 1px; + border-color: black; +} +table.mxPopupMenu { + border-collapse: collapse; + margin-top: 1px; + margin-bottom: 1px; +} +tr.mxPopupMenuItem { + color: black; + cursor: pointer; +} +tr.mxPopupMenuItemHover { + background-color: #000066; + color: #FFFFFF; + cursor: pointer; +} +td.mxPopupMenuItem { + padding: 2px 30px 2px 10px; + white-space: nowrap; + font-family: Arial; + font-size: 8pt; +} +td.mxPopupMenuIcon { + background-color: #D0D0D0; + padding: 2px 4px 2px 4px; +} +.mxDisabled { + opacity: 0.2 !important; + cursor:default !important; +} diff --git a/src/static/css/detail_view.css b/src/static/css/detail_view.css deleted file mode 100644 index c3d0a4d..0000000 --- a/src/static/css/detail_view.css +++ /dev/null @@ -1,39 +0,0 @@ -.card_container { - display: grid; - grid-template-columns: 1fr 1fr 1fr 1fr; - grid-gap: 25px 25px; - justify-items: stretch; -} - -.card_container ul > li { - padding: 7px !important; - font-size: 16px; -} - -.detail_button_container .btn { - width: 49%; -} - -.detail_button_container .btn-danger { - float: right; -} - -#modal_warning { - transition: max-height 0.5s ease-out; - overflow: hidden; -} - -.detail_card { - border-radius: 5px; - border-top: 1px solid #ccc; - border-left: 1px solid #ccc; - border-right: 1px solid #ccc; - border-bottom: 1px solid #ccc; - margin: 5px; - padding-left: 25px; - padding-right: 25px; - padding-bottom: 15px; - display: flex; - flex-direction: column; - justify-content: space-between; -} diff --git a/src/static/css/graph_common.css b/src/static/css/graph_common.css deleted file mode 100644 index cff1516..0000000 --- a/src/static/css/graph_common.css +++ /dev/null @@ -1,172 +0,0 @@ -div.mxRubberband { - position: absolute; - overflow: hidden; - border-style: solid; - border-width: 1px; - border-color: #0000FF; - background: #0077FF; -} -.mxCellEditor { - background: url(data:image/gif;base64,R0lGODlhMAAwAIAAAP///wAAACH5BAEAAAAALAAAAAAwADAAAAIxhI+py+0Po5y02ouz3rz7D4biSJbmiabqyrbuC8fyTNf2jef6zvf+DwwKh8Si8egpAAA7); - _background: url('/static/img/mxgraph/transparent.gif'); - border-color: transparent; - border-style: solid; - display: inline-block; - position: absolute; - overflow: visible; - word-wrap: normal; - border-width: 0; - min-width: 1px; - resize: none; - padding: 0px; - margin: 0px; -} -.mxPlainTextEditor * { - padding: 0px; - margin: 0px; -} -div.mxWindow { - background: url('../img/mxgraph/window.gif'); - border:1px solid #c3c3c3; - position: absolute; - overflow: hidden; - z-index: 3; -} -table.mxWindow { - border-collapse: collapse; - table-layout: fixed; - font-family: Arial; - font-size: 8pt; -} -td.mxWindowTitle { - background: url('/static/img/mxgraph/window-title.gif') repeat-x; - text-overflow: ellipsis; - white-space: nowrap; - text-align: center; - font-weight: bold; - overflow: hidden; - height: 13px; - padding: 2px; - padding-top: 4px; - padding-bottom: 6px; - color: black; -} -td.mxWindowPane { - vertical-align: top; - padding: 0px; -} -div.mxWindowPane { - overflow: hidden; - position: relative; -} -td.mxWindowPane td { - font-family: Arial; - font-size: 8pt; -} -td.mxWindowPane input, td.mxWindowPane select, td.mxWindowPane textarea, td.mxWindowPane radio { - font-family: Arial; - font-size: 8pt; - padding: 1px; -} -td.mxWindowPane button { - color: #fff; - background-color: #337ab7; - border-color: #2e6da4; - display: inline-block; - margin: 2%; - font-size: 14px; - font-weight: 400; - line-height: 1.42857143; - text-align: center; - white-space: nowrap; - vertical-align: middle; - -ms-touch-action: manipulation; - touch-action: manipulation; - cursor: pointer; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; - background-image: none; - border: 1px solid transparent; - border-radius: 4px; -} -img.mxToolbarItem { - margin-right: 6px; - margin-bottom: 6px; - border-width: 1px; -} -select.mxToolbarCombo { - vertical-align: top; - border-style: inset; - border-width: 2px; -} -div.mxToolbarComboContainer { - padding: 2px; -} -img.mxToolbarMode { - margin: 2px; - margin-right: 4px; - margin-bottom: 4px; - border-width: 0px; -} -img.mxToolbarModeSelected { - margin: 0px; - margin-right: 2px; - margin-bottom: 2px; - border-width: 2px; - border-style: inset; -} -div.mxTooltip { - -webkit-box-shadow: 3px 3px 12px #C0C0C0; - -moz-box-shadow: 3px 3px 12px #C0C0C0; - box-shadow: 3px 3px 12px #C0C0C0; - background: #FFFFCC; - border-style: solid; - border-width: 1px; - border-color: black; - font-family: Arial; - font-size: 8pt; - position: absolute; - cursor: default; - padding: 4px; - color: black; -} -div.mxPopupMenu { - -webkit-box-shadow: 3px 3px 12px #C0C0C0; - -moz-box-shadow: 3px 3px 12px #C0C0C0; - box-shadow: 3px 3px 12px #C0C0C0; - background: url('/static/img/mxgraph/window.gif'); - position: absolute; - border-style: solid; - border-width: 1px; - border-color: black; -} -table.mxPopupMenu { - border-collapse: collapse; - margin-top: 1px; - margin-bottom: 1px; -} -tr.mxPopupMenuItem { - color: black; - cursor: pointer; -} -tr.mxPopupMenuItemHover { - background-color: #000066; - color: #FFFFFF; - cursor: pointer; -} -td.mxPopupMenuItem { - padding: 2px 30px 2px 10px; - white-space: nowrap; - font-family: Arial; - font-size: 8pt; -} -td.mxPopupMenuIcon { - background-color: #D0D0D0; - padding: 2px 4px 2px 4px; -} -.mxDisabled { - opacity: 0.2 !important; - cursor:default !important; -} diff --git a/src/static/js/dashboard.js b/src/static/js/dashboard.js index 84c3703..8e1250a 100644 --- a/src/static/js/dashboard.js +++ b/src/static/js/dashboard.js @@ -1,3 +1,190 @@ +/////////////////// +// Global Variables +/////////////////// + +form_submission_callbacks = []; //all runnables will be executed before form submission + +/////////////////// +// Global Functions +/////////////////// +function update_page(response) { + if( response.redirect ) + { + window.location.replace(response.redirect); + return; + } + draw_breadcrumbs(response.meta); + update_exit_button(response.meta); + update_side_buttons(response.meta); + $("#formContainer").html(response.content); +} + +function update_side_buttons(meta) { + const step = meta.active; + const page_count = meta.steps.length; + + const back_button = document.getElementById("gob"); + if (step == 0) { + back_button.classList.add("disabled"); + back_button.disabled = true; + } else { + back_button.classList.remove("disabled"); + back_button.disabled = false; + } + + const forward_btn = document.getElementById("gof"); + if (step == page_count - 1) { + forward_btn.classList.add("disabled"); + forward_btn.disabled = true; + } else { + forward_btn.classList.remove("disabled"); + forward_btn.disabled = false; + } +} + +function update_exit_button(meta) { + if (meta.workflow_count == 1) { + document.getElementById("cancel_btn").innerText = "Exit Workflow"; + } else { + document.getElementById("cancel_btn").innerText = "Return to Parent"; + } +} + +function draw_breadcrumbs(meta) { + $("#topPagination").children().not(".page-control").remove(); + + for (const i in meta.steps) { + const step_btn = create_step(meta.steps[i], i == meta["active"]); + $("#topPagination li:last-child").before(step_btn); + } +} + +function create_step(step_json, active) { + const step_dom = document.createElement("li"); + // First create the dom object depending on active or not + step_dom.className = "topcrumb"; + if (active) { + step_dom.classList.add("active"); + } + $(step_dom).html(`<span class="d-flex align-items-center justify-content-center text-capitalize w-100">${step_json['title']}</span>`) + + const code = step_json.valid; + + let stat = ""; + let msg = ""; + if (code < 100) { + $(step_dom).children().first().append("<i class='ml-2 far fa-square'></i>") + stat = ""; + msg = ""; + } else if (code < 200) { + $(step_dom).children().first().append("<i class='ml-2 fas fa-minus-square'></i>") + stat = "invalid"; + msg = step_json.message; + } else if (code < 300) { + $(step_dom).children().first().append("<i class='ml-2 far fa-check-square'></i>") + stat = "valid"; + msg = step_json.message; + } + + if (step_json.enabled == false) { + step_dom.classList.add("disabled"); + } + if (active) { + update_message(msg, stat); + } + + return step_dom; +} + +function update_description(title, desc) { + document.getElementById("view_title").innerText = title; + document.getElementById("view_desc").innerText = desc; +} + +function update_message(message, stepstatus) { + document.getElementById("view_message").innerText = message; + document.getElementById("view_message").className = "step_message"; + document.getElementById("view_message").classList.add("message_" + stepstatus); +} + +function submitStepForm(next_step = "current"){ + run_form_callbacks(); + const step_form_data = $("#step_form").serialize(); + const form_data = $.param({ + "step": next_step, + "step_form": step_form_data, + "csrfmiddlewaretoken": $("[name=csrfmiddlewaretoken]").val() + }); + $.post( + '/workflow/manager/', + form_data, + (data) => update_page(data), + 'json' + ).fail(() => alert("failure")); +} + +function run_form_callbacks(){ + for(f of form_submission_callbacks) + f(); + form_submission_callbacks = []; +} + +function create_workflow(type) { + $.ajax({ + type: "POST", + url: "/workflow/create/", + data: { + "workflow_type": type + }, + headers: { + "X-CSRFToken": $('input[name="csrfmiddlewaretoken"]').val() + } + }).done(function (data, textStatus, jqXHR) { + window.location = "/workflow/"; + }).fail(function (jqxHR, textstatus) { + alert("Something went wrong..."); + }); +} + +function add_workflow(type) { + data = $.ajax({ + type: "POST", + url: "/workflow/add/", + data: { + "workflow_type": type + }, + headers: { + "X-CSRFToken": $('input[name="csrfmiddlewaretoken"]').val() + } + }).done(function (data, textStatus, jqXHR) { + update_page(data); + }).fail(function (jqxHR, textstatus) { + alert("Something went wrong..."); + }); +} + +function pop_workflow() { + data = $.ajax({ + type: "POST", + url: "/workflow/pop/", + headers: { + "X-CSRFToken": $('input[name="csrfmiddlewaretoken"]').val() + } + }).done(function (data, textStatus, jqXHR) { + update_page(data); + }).fail(function (jqxHR, textstatus) { + alert("Something went wrong..."); + }); +} + +function continue_workflow() { + window.location.replace("/workflow/"); +} + +/////////////////// +//Class Definitions +/////////////////// + class MultipleSelectFilterWidget { constructor(neighbors, items, initial) { @@ -98,7 +285,7 @@ class MultipleSelectFilterWidget { select(node) { const elem = document.getElementById(node['id']); node['selected'] = true; - elem.classList.remove('disabled_node', 'cleared_node'); + elem.classList.remove('bg-white', 'not-allowed', 'bg-light'); elem.classList.add('selected_node'); } @@ -106,16 +293,16 @@ class MultipleSelectFilterWidget { const elem = document.getElementById(node['id']); node['selected'] = false; node['selectable'] = true; - elem.classList.add('cleared_node') - elem.classList.remove('disabled_node', 'selected_node'); + elem.classList.add('bg-white') + elem.classList.remove('not-allowed', 'bg-light', 'selected_node'); } disable_node(node) { const elem = document.getElementById(node['id']); node['selected'] = false; node['selectable'] = false; - elem.classList.remove('cleared_node', 'selected_node'); - elem.classList.add('disabled_node'); + elem.classList.remove('bg-white', 'selected_node'); + elem.classList.add('not-allowed', 'bg-light'); } processClick(id){ @@ -173,7 +360,7 @@ class MultipleSelectFilterWidget { const button = document.createElement("BUTTON"); button.type = "button"; button.appendChild(document.createTextNode("Remove")); - button.classList.add("btn", "btn-danger"); + button.classList.add("btn", "btn-danger", "d-inline-block"); const that = this; button.onclick = function(){ that.remove_dropdown(div.id, node.id); } return button; @@ -183,6 +370,7 @@ class MultipleSelectFilterWidget { const input = document.createElement("INPUT"); input.type = node.form.type; input.name = node.id + node.form.name + input.classList.add("form-control", "w-auto", "d-inline-block"); input.pattern = "(?=^.{1,253}$)(^([A-Za-z0-9-_]{1,62}\.)*[A-Za-z0-9-_]{1,63})"; input.title = "Only alphanumeric characters (a-z, A-Z, 0-9), underscore(_), and hyphen (-) are allowed" input.placeholder = node.form.placeholder; @@ -198,13 +386,18 @@ class MultipleSelectFilterWidget { add_item_prepopulate(node, prepopulate){ const div = document.createElement("DIV"); div.id = "dropdown_" + this.dropdown_count; - div.classList.add("dropdown_item"); + div.classList.add("card", "flex-row", "d-flex", "mb-2"); this.dropdown_count++; const label = document.createElement("H5") label.appendChild(document.createTextNode(node['name'])) + label.classList.add("p-1", "m-1"); div.appendChild(label); - div.appendChild(this.make_input(div, node, prepopulate)); - div.appendChild(this.make_remove_button(div, node)); + let input = this.make_input(div, node, prepopulate); + input.classList.add("flex-grow-1", "p-1", "m-1"); + div.appendChild(input); + let remove_btn = this.make_remove_button(div, node); + remove_btn.classList.add("p-1", "m-1"); + div.appendChild(remove_btn); document.getElementById("dropdown_wrapper").appendChild(div); return div; } @@ -756,21 +949,28 @@ class NetworkStep { } 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; + colorBlob.className = "square-20 rounded-circle"; + colorBlob.style['background'] = color; + + const textContainer = document.createElement("span"); + textContainer.className = "ml-2"; + textContainer.appendChild(document.createTextNode(net_name)); + + const timesIcon = document.createElement("i"); + timesIcon.classList.add("fas", "fa-times"); + const deletebutton = document.createElement("button"); - deletebutton.className = "btn btn-danger"; - deletebutton.style = "float: right; height: 20px; line-height: 8px; vertical-align: middle; width: 20px; padding-left: 5px;"; - deletebutton.appendChild(document.createTextNode("X")); + deletebutton.className = "btn btn-danger ml-auto square-20 p-0 d-flex justify-content-center"; + deletebutton.appendChild(timesIcon); deletebutton.addEventListener("click", function() { this.createDeleteDialog(net_id); }.bind(this), false); - textContainer.appendChild(document.createTextNode(net_name)); - colorBlob.style['background'] = color; + + const newNet = document.createElement("li"); + newNet.classList.add("list-group-item", "d-flex", "bg-light"); + newNet.id = net_id; newNet.appendChild(colorBlob); newNet.appendChild(textContainer); + if( net_name != "public" ) { newNet.appendChild(deletebutton); } @@ -818,16 +1018,9 @@ class NetworkStep { this.graph.refresh(host); } - submitForm() { - const form = document.getElementById("xml_form"); + prepareForm() { 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); } } @@ -1048,18 +1241,19 @@ class SearchableSelectMultipleWidget { 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 result_entry = document.createElement("a"); + result_entry.href = "#"; + result_entry.innerText = result_text; + result_entry.title = result_text; + result_entry.classList.add("list-group-item", "list-group-item-action", "overflow-ellipsis", "flex-shrink-0"); + result_entry.onclick = function() { searchable_select_multiple_widget.select_item(obj.id); }; const tooltip = document.createElement("span"); const tooltiptext = document.createTextNode(result_text); tooltip.appendChild(tooltiptext); - tooltip.setAttribute('class', 'entry_tooltip'); - result_button.appendChild(tooltip); - result_entry.appendChild(result_button); + tooltip.classList.add("d-none"); + result_entry.appendChild(tooltip); drop.appendChild(result_entry); } @@ -1112,23 +1306,39 @@ class SearchableSelectMultipleWidget { added_list.removeChild(added_list.firstChild); } - let list_html = ""; + const list_html = document.createElement("div"); + list_html.classList.add("list-group"); for( const item_id of this.added_items ) { - const item = this.items[item_id]; + const times = document.createElement("li"); + times.classList.add("fas", "fa-times"); + + const deleteButton = document.createElement("a"); + deleteButton.href = "#"; + deleteButton.innerHTML = "<i class='fas fa-times'></i>" + // Setting .onclick/.addEventListener does not work, + // which is why I took the setAttribute approach + // If anyone knows why, please let me know :] + deleteButton.setAttribute("onclick", `searchable_select_multiple_widget.remove_item(${item_id});`); + deleteButton.classList.add("btn"); + const deleteColumn = document.createElement("div"); + deleteColumn.classList.add("col-auto"); + deleteColumn.append(deleteButton); + const item = this.items[item_id]; const element_entry_text = this.generate_element_text(item); + const textColumn = document.createElement("div"); + textColumn.classList.add("col", "overflow-ellipsis"); + textColumn.innerText = element_entry_text; + textColumn.title = element_entry_text; + + const itemRow = document.createElement("div"); + itemRow.classList.add("list-group-item", "d-flex", "p-0", "align-items-center"); + itemRow.append(textColumn, deleteColumn); - list_html += '<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>'; + list_html.append(itemRow); } - added_list.innerHTML = list_html; + added_list.innerHTML = list_html.innerHTML; } } diff --git a/src/templates/account/booking_list.html b/src/templates/account/booking_list.html index 98ab5c8..55c6c0d 100644 --- a/src/templates/account/booking_list.html +++ b/src/templates/account/booking_list.html @@ -1,88 +1,94 @@ {% extends "base.html" %} {% block content %} <h2>Bookings I Own</h2> - <div class="card_container"> - {% for booking in 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 mr-2" href="/booking/detail/{{booking.id}}/">Details</a> - <button - class="btn btn-danger" - onclick='cancel_booking({{booking.id}});' - data-toggle="modal" - data-target="#resModal" - >Cancel</button> + <div class="row"> + {% for booking in bookings %} + <div class="col-12 col-md-6 col-lg-4 col-xl-3 p-2"> + <div class="card h-100"> + <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 mr-2" href="/booking/detail/{{booking.id}}/">Details</a> + <button + class="btn btn-danger" + onclick='cancel_booking({{booking.id}});' + data-toggle="modal" + data-target="#resModal" + >Cancel</button> + </div> + </div> </div> - </div> - {% endfor %} + {% endfor %} </div> <h2>Bookings I Collaborate On</h2> - <div class="card_container"> + <div class="row"> {% 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 class="col-12 col-md-6 col-lg-4 col-xl-3 p-2"> + <div class="card h-100"> + <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> </div> {% 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="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> - <li class="list-group-item">owner: {{booking.owner.userprofile.email_addr}}</li> - </ul> - </div> - <div class="card-footer d-flex"> - <a class="btn btn-primary ml-auto" href="/booking/detail/{{booking.id}}/">Details</a> + <a href="#expired_bookings" data-toggle="collapse" class="h2 discrete-a"> + Expired Bookings + <i class="fas fa-angle-down rotate"></i> + </a> + <div id="expired_bookings" class="row collapse"> + {% for booking in expired_bookings %} + <div class="col-12 col-md-6 col-lg-4 col-xl-3 p-2"> + <div class="card h-100"> + <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> + <li class="list-group-item">owner: {{booking.owner.userprofile.email_addr}}</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> </div> - </div> - {% endfor %} + {% endfor %} </div> <script> var current_booking_id = -1; function cancel_booking(booking_id) { current_booking_id = booking_id; - document.getElementById('modal_warning').style['max-height'] = '0px'; } function submit_cancel_form() { @@ -95,22 +101,12 @@ req.onerror = function() { alert("problem submitting form"); } req.send(formData); } - - function toggle_display(elem_id){ - var e = document.getElementById(elem_id); - if (e.style.display === "none"){ - e.style.display = "grid"; - } else { - e.style.display = "none"; - } - } </script> -<div class="modal fade" id="resModal" tabindex="-1" role="dialog" aria-labelledby="my_modal" aria-hidden="true"> - <div class="modal-dialog" style="width: 450px;" role="document"> +<div class="modal fade" id="resModal" tabindex="-1" role="dialog" aria-hidden="true"> + <div class="modal-dialog" role="document"> <div class="modal-content"> <div class="modal-header"> - <h4 class="modal-title" id="my_modal" style="display: inline; float: left;">Cancel Booking?</h4> - <p>Everthing on your machine(s) will be lost</p> + <h4 class="modal-title d-inline float-left">Cancel Booking?</h4> <button type="button" class="close" data-dismiss="modal" aria-label="Close"> <span aria-hidden="true">×</span> </button> @@ -118,16 +114,19 @@ <form id="booking_cancel_form"> {% csrf_token %} </form> - <div class="modal-footer"> - <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button> - <button type="button" class="btn btn-primary" onclick="document.getElementById('modal_warning').style['max-height'] = '500px';">Cancel Booking</button> - </div> - <div id="modal_warning" class="modal-footer" style="max-height:0px;" > - <div style="text-align:center; margin: 5px"> - <h3>Are You Sure?</h3> - <p>This cannot be undone</p> - <button class="btn" onclick="document.getElementById('modal_warning').style['max-height'] = '0px';">Nevermind</button> - <button class="btn btn-danger" id="confirm_cancel_button" data-dismiss="modal" onclick="submit_cancel_form();">I'm Sure</button> + <div class="modal-footer d-flex flex-column"> + <p>Everything on your machine(s) will be lost</p> + <div class="mb-2"> + <button type="button" class="btn btn-outline-secondary" data-dismiss="modal">Close</button> + <button type="button" class="btn btn-danger" data-toggle="collapse" data-target="#warning">Cancel Booking</button> + </div> + <div class="collapse w-100 text-center border-top" id="warning"> + <div class="p-4"> + <h3>Are You Sure?</h3> + <p>This cannot be undone</p> + <button class="btn btn-outline-secondary" data-dismiss="modal">Nevermind</button> + <button class="btn btn-danger" id="confirm_cancel_button" data-dismiss="modal" onclick="submit_cancel_form();">I'm Sure</button> + </div> </div> </div> </div> diff --git a/src/templates/account/configuration_list.html b/src/templates/account/configuration_list.html index 6f7844a..94a1120 100644 --- a/src/templates/account/configuration_list.html +++ b/src/templates/account/configuration_list.html @@ -1,26 +1,28 @@ {% extends "base.html" %} {% block content %} -<div class="card_container"> +<div class="row"> {% for config in configurations %} - <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> - <li class="list-group-item">description: {{config.description}}</li> - <li class="list-group-item">resource: {{config.bundle}}</li> - </ul> - </div> - <div class="card-footer"> - <button - class="btn btn-danger w-100" - onclick='delete_config({{config.id}});' - data-toggle="modal" - data-target="#configModal" - >Delete</button> + <div class="p-2 col-12 col-md-6 col-lg-4 col-xl-3"> + <div class="card h-100"> + <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> + <li class="list-group-item">description: {{config.description}}</li> + <li class="list-group-item">resource: {{config.bundle}}</li> + </ul> + </div> + <div class="card-footer"> + <button + class="btn btn-danger w-100" + onclick='delete_config({{config.id}});' + data-toggle="modal" + data-target="#configModal" + >Delete</button> + </div> </div> </div> {% endfor %} @@ -29,7 +31,6 @@ var current_config_id = -1; function delete_config(config_id) { current_config_id = config_id; - document.getElementById('modal_warning').style['max-height'] = '0px'; } function submit_delete_form() { @@ -43,11 +44,11 @@ req.send(formData); } </script> -<div class="modal fade" id="configModal" tabindex="-1" role="dialog" aria-labelledby="my_modal" aria-hidden="true"> - <div class="modal-dialog" style="width: 450px;" role="document"> +<div class="modal fade" id="configModal" tabindex="-1" role="dialog" aria-hidden="true"> + <div class="modal-dialog" role="document"> <div class="modal-content"> <div class="modal-header"> - <h4 class="modal-title" id="my_modal" style="display: inline; float: left;">Delete Configuration?</h4> + <h4 class="modal-title d-inline float-left">Delete Configuration?</h4> <button type="button" class="close" data-dismiss="modal" aria-label="Close"> <span aria-hidden="true">×</span> </button> @@ -55,17 +56,20 @@ <form id="config_delete_form"> {% csrf_token %} </form> - <div class="modal-footer"> - <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button> - <button type="button" class="btn btn-primary" onclick="document.getElementById('modal_warning').style['max-height'] = '500px';">Delete</button> - </div> - <div id="modal_warning" class="modal-footer" style="max-height:0px;" > - <div style="text-align:center; margin: 5px"> - <h3>Are You Sure?</h3> - <p>This cannot be undone</p> - <button class="btn" onclick="document.getElementById('modal_warning').style['max-height'] = '0px';">Nevermind</button> - <button class="btn btn-danger" data-dismiss="modal" onclick="submit_delete_form();">I'm Sure</button> + <div class="modal-footer d-flex flex-column"> + <div class="mb-2"> + <button type="button" class="btn btn-outline-secondary" data-dismiss="modal">Close</button> + <button type="button" class="btn btn-danger" data-toggle="collapse" data-target="#warning">Delete</button> </div> + <div class="collapse w-100 text-center border-top" id="warning"> + <div class="p-4"> + <h3>Are You Sure?</h3> + <p>This cannot be undone</p> + <button class="btn btn-outline-secondary" data-dismiss="modal">Nevermind</button> + <button class="btn btn-danger" data-dismiss="modal" onclick="submit_delete_form();">I'm Sure</button> + </div> + </div> + </div> </div> </div> </div> diff --git a/src/templates/account/image_list.html b/src/templates/account/image_list.html index 068e096..a626710 100644 --- a/src/templates/account/image_list.html +++ b/src/templates/account/image_list.html @@ -1,36 +1,10 @@ {% extends "base.html" %} {% block content %} <h2>Images I Own</h2> -<div class="card_container"> +<div class="row"> {% for image in images %} - <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> - <li class="list-group-item">name: {{image.name}}</li> - <li class="list-group-item">description: {{image.description}}</li> - <li class="list-group-item">host profile: {{image.host_type.name}}</li> - </ul> - </div> - <div class="card-footer"> - <button - class="btn btn-danger w-100" - onclick='delete_image({{image.id}});' - data-toggle="modal" - data-target="#imageModal" - >Delete</button> - </div> - </div> -{% endfor %} -</div> -<h2>Public Images</h2> -<div class="card_container"> - {% for image in public_images %} - <div class="card"> + <div class="p-2 col-12 col-md-6 col-lg-4 col-xl-3"> + <div class="card h-100"> <div class="card-header"> <h3>Image {{image.id}}</h3> </div> @@ -43,6 +17,34 @@ <li class="list-group-item">host profile: {{image.host_type.name}}</li> </ul> </div> + <div class="card-footer"> + <button class="btn btn-danger w-100" onclick='delete_image({{image.id}});' + data-toggle="modal" data-target="#imageModal"> + Delete + </button> + </div> + </div> + </div> +{% endfor %} +</div> +<h2>Public Images</h2> +<div class="row"> + {% for image in public_images %} + <div class="p-2 col-12 col-md-6 col-lg-4 col-xl-3"> + <div class="card h-100"> + <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> + <li class="list-group-item">name: {{image.name}}</li> + <li class="list-group-item">description: {{image.description}}</li> + <li class="list-group-item">host profile: {{image.host_type.name}}</li> + </ul> + </div> + </div> </div> {% endfor %} </div> @@ -52,7 +54,6 @@ var used_images = {{used_images|safe|default:"{}"}}; function delete_image(image_id) { current_image_id = image_id; - document.getElementById('modal_warning').style['max-height'] = '0px'; var warning_header = document.getElementById("warning_header"); var warning_text = document.getElementById("warning_text"); var delete_image_button = document.getElementById("final_delete_b"); @@ -94,11 +95,11 @@ } } </script> -<div class="modal fade" id="imageModal" tabindex="-1" role="dialog" aria-labelledby="my_modal" aria-hidden="true"> - <div class="modal-dialog" style="width: 450px;" role="document"> +<div class="modal fade" id="imageModal" tabindex="-1" role="dialog" aria-hidden="true"> + <div class="modal-dialog" role="document"> <div class="modal-content"> <div class="modal-header"> - <h4 class="modal-title" id="my_modal" style="display: inline; float: left;">Delete Configuration?</h4> + <h4 class="modal-title d-inline float-left">Delete Configuration?</h4> <button type="button" class="close" data-dismiss="modal" aria-label="Close"> <span aria-hidden="true">×</span> </button> @@ -106,17 +107,20 @@ <form id="image_delete_form"> {% csrf_token %} </form> - <div class="modal-footer"> - <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button> - <button type="button" class="btn btn-primary" onclick="document.getElementById('modal_warning').style['max-height'] = '500px';">Delete</button> - </div> - <div id="modal_warning" class="modal-footer" style="max-height:0px;" > - <div style="text-align:center; margin: 5px"> - <h3 id="warning_header">Are You Sure?</h3> - <p id="warning_text">This cannot be undone</p> - <button class="btn" onclick="document.getElementById('modal_warning').style['max-height'] = '0px';">Nevermind</button> - <button id="final_delete_b" class="btn btn-danger" data-dismiss="modal" onclick="submit_delete_form();">I'm Sure</button> + <div class="modal-footer d-flex flex-column"> + <div class="mb-2"> + <button type="button" class="btn btn-outline-secondary" data-dismiss="modal">Close</button> + <button type="button" class="btn btn-danger" data-toggle="collapse" data-target="#warning">Delete</button> </div> + <div class="collapse w-100 text-center border-top" id="warning"> + <div class="p-4"> + <h3 id="warning_header">Are You Sure?</h3> + <p id="warning_text">This cannot be undone</p> + <button class="btn btn-outline-secondary" data-dismiss="modal">Nevermind</button> + <button id="final_delete_b" class="btn btn-danger" data-dismiss="modal" onclick="submit_delete_form();">I'm Sure</button> + </div> + </div> + </div> </div> </div> </div> diff --git a/src/templates/account/resource_list.html b/src/templates/account/resource_list.html index f92f78e..1203534 100644 --- a/src/templates/account/resource_list.html +++ b/src/templates/account/resource_list.html @@ -1,25 +1,27 @@ {% extends "base.html" %} {% block content %} -<div class="card_container"> +<div class="row"> {% for resource in resources %} - <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="card-footer"> - <button - class="btn btn-danger w-100" - onclick='delete_resource({{resource.id}});' - data-toggle="modal" - data-target="#resModal" - >Delete</button> + <div class="col-12 col-md-6 col-lg-4 col-xl-3 p-2"> + <div class="card h-100"> + <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="card-footer"> + <button + class="btn btn-danger w-100" + onclick='delete_resource({{resource.id}});' + data-toggle="modal" + data-target="#resModal" + >Delete</button> + </div> </div> </div> {% endfor %} @@ -63,7 +65,6 @@ warning_title.appendChild(title); warning_text.appendChild(text); - document.getElementById('modal_warning').style['max-height'] = '0px'; } function list_configs(configs) { @@ -87,11 +88,11 @@ req.send(formData); } </script> -<div class="modal fade" id="resModal" tabindex="-1" role="dialog" aria-labelledby="my_modal" aria-hidden="true"> - <div class="modal-dialog" style="width: 450px;" role="document"> +<div class="modal fade" id="resModal" tabindex="-1" role="dialog" aria-hidden="true"> + <div class="modal-dialog" role="document"> <div class="modal-content"> <div class="modal-header"> - <h4 class="modal-title" id="my_modal" style="display: inline; float: left;">Delete Resource?</h4> + <h4 class="modal-title d-inline float-left">Delete Resource?</h4> <button type="button" class="close" data-dismiss="modal" aria-label="Close"> <span aria-hidden="true">×</span> </button> @@ -99,18 +100,21 @@ <form id="res_delete_form"> {% csrf_token %} </form> - <div class="modal-footer"> - <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button> - <button type="button" class="btn btn-primary" onclick="document.getElementById('modal_warning').style['max-height'] = '500px';">Delete</button> - </div> - <div id="modal_warning" class="modal-footer" style="max-height:0px;" > - <div style="text-align:center; margin: 5px"> - <h3 id="config_warning">Are You Sure?</h3> - <p id="warning_subtext">This cannot be undone</p> - <ul id="config_list"></ul> - <button class="btn" onclick="document.getElementById('modal_warning').style['max-height'] = '0px';">Nevermind</button> - <button class="btn btn-danger" id="confirm_delete_button" data-dismiss="modal" onclick="submit_delete_form();">I'm Sure</button> + <div class="modal-footer d-flex flex-column"> + <div class="mb-2"> + <button type="button" class="btn btn-outline-secondary" data-dismiss="modal">Close</button> + <button type="button" class="btn btn-danger" data-toggle="collapse" data-target="#warning">Delete</button> </div> + <div class="collapse w-100 text-center border-top" id="warning"> + <div class="p-4"> + <h3 id="config_warning">Are You Sure?</h3> + <p id="warning_subtext">This cannot be undone</p> + <ul id="config_list"></ul> + <button class="btn btn-outline-secondary" data-dismiss="modal">Nevermind</button> + <button class="btn btn-danger" id="confirm_delete_button" data-dismiss="modal" onclick="submit_delete_form();">I'm Sure</button> + </div> + </div> + </div> </div> </div> </div> diff --git a/src/templates/account/userprofile_update_form.html b/src/templates/account/userprofile_update_form.html index 6ab8242..5ddb867 100644 --- a/src/templates/account/userprofile_update_form.html +++ b/src/templates/account/userprofile_update_form.html @@ -16,7 +16,7 @@ Generate </a> </p> - <p style="word-wrap: break-word;">{{ token.key }}</p> + <p class="text-break">{{ token.key }}</p> <p></p> {% buttons %} diff --git a/src/templates/base.html b/src/templates/base.html index 62a9ed5..f59740d 100644 --- a/src/templates/base.html +++ b/src/templates/base.html @@ -5,95 +5,34 @@ <!-- 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) { - $.ajax({ - type: "POST", - url: "/", - data: { - "create": type - }, - beforeSend: function (request) { - request.setRequestHeader("X-CSRFToken", - $('input[name="csrfmiddlewaretoken"]').val() - ); - } - }).done(function (data) { - window.location.replace("/wf/"); - }).fail(function (jqxHR, textstatus) { - alert("Something went wrong..."); - }); - } - - function continue_wf() { - window.location.replace("/wf/"); - } - - function toggle_create_drop() { - drop_div = document.getElementById("create_drop"); - - if (drop_div.style.display === "none") { - drop_div.style.display = "inherit"; - } else { - drop_div.style.display = "none"; - } - } -</script> -<style> - .navbar { - min-width: 200px; - } - - .create_drop { - display: none; - width: 100%; - } - - .create_drop button { - width: 100%; - } - - .drop_btn { - border-radius: 0px; - background-color: CCCCCC - } - - .drop_btn:hover { - color: #555; - border-top: 1px solid #E7E7E7; - border-bottom: 1px solid #E7E7E7; - } - - #wrapper { - height: 100vh; - } -</style> +<script src="/static/js/dashboard.js"></script> {% endblock %} {% block basecontent %} <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"> + <nav class="navbar navbar-light bg-light navbar-fixed-top border-bottom py-0 mb-0" role="navigation"> + <div class="container-fluid pb-2 pb-sm-0"> + <!-- Logo --> + <div class="col-12 col-sm order-1 order-sm-2 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' %}> + <a class="navbar-brand d-none d-lg-inline" 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" + <!-- Sidebar button --> + <div class="col-6 col-sm-2 d-flex order-1 order-lg-3 d-lg-none"> + <button class="btn border mx-auto" type="button" data-toggle="collapse" data-target="#sidebar" aria-expanded="false" aria-controls="sidebar"> - <i class="fas fa-bars"></i> + <i class="fas fa-bars d-inline"></i> </button> </div> - <div class="col-2 order-3"> - <ul class="nav ml-auto"> + <!-- Login dropdown --> + <div class="col-6 col-sm-2 order-3 d-flex"> + <ul class="nav mx-auto mr-sm-0"> <li class="dropdown ml-auto"> <a class="nav-link p-0 text-dark p-2" data-toggle="dropdown" href="#"> {% if request.user.username %} @@ -119,19 +58,18 @@ Login with Jira </a> {% endif %} - </div> <!-- End dropdown-menu --> - </li> <!-- End dropdown --> + </div> + </li> </ul> - </div> <!-- End top right account menu --> + </div> </div> </nav> - <!-- /.navbar-top-links --> <!-- 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="container-fluid d-flex flex-grow-1 px-0 align-items-start flex-column"> + <div class="row flex-grow-1 w-100 mx-0 align-content-start flex-lg-grow-1"> <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"> + <nav class="navbar navbar-expand-lg border-bottom p-0 w-100 sidebar"> <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"> @@ -146,19 +84,19 @@ <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"> + <a href="#" onclick="create_workflow(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"> + <a href="#" onclick="create_workflow(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"> + <a href="#" onclick="create_workflow(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"> + <a href="#" onclick="create_workflow(3)" class="list-group-item list-group-item-action list-group-item-secondary"> Create a Snapshot </a> - <a href="#" onclick="cwf(4)" class="list-group-item list-group-item-action list-group-item-secondary"> + <a href="#" onclick="create_workflow(4)" class="list-group-item list-group-item-action list-group-item-secondary"> Configure OPNFV </a> </div> @@ -176,9 +114,6 @@ <a href="{% url 'booking:stats' %}" class="list-group-item list-group-item-action bg-light"> Booking Statistics </a> - <!-- <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> @@ -206,17 +141,13 @@ </div> </div> </nav> - <!--/.well --> </div> - <!--/span--> - - <div class="col flex-grow-1 d-flex flex-column"> + <div class="col overflow-auto flex-grow-1 d-flex flex-column h-100"> {% if title %} <div class="row flex-shrink-1"> <div class="col-lg-12"> <h1 class="page-header">{{ title }}</h1> </div> - <!-- /.col-lg-12 --> </div> {% endif %} <div id="bsm">{% bootstrap_messages %}</div> @@ -224,13 +155,7 @@ {% block content %} {% endblock content %} </div> - <!--/span--> - - </div> - <!--/row--> </div> - <!-- /#page-wrapper --> </div> -<!-- /#wrapper --> {% endblock basecontent %} diff --git a/src/templates/booking/booking_calendar.html b/src/templates/booking/booking_calendar.html index ddcb45d..450c97f 100644 --- a/src/templates/booking/booking_calendar.html +++ b/src/templates/booking/booking_calendar.html @@ -23,9 +23,7 @@ <div id='calendar'> </div> </div> - <!-- /.panel-body --> </div> - <!-- /.panel --> </div> </div> diff --git a/src/templates/booking/booking_detail.html b/src/templates/booking/booking_detail.html index 918f5af..99de747 100644 --- a/src/templates/booking/booking_detail.html +++ b/src/templates/booking/booking_detail.html @@ -4,174 +4,164 @@ {% block extrahead %} {{block.super}} -<script src="https://cdn.rawgit.com/google/code-prettify/master/loader/run_prettify.js?lang=yaml"></script> + <script src="https://cdn.rawgit.com/google/code-prettify/master/loader/run_prettify.js?lang=yaml"></script> {% endblock %} {% block content %} -<style> -#modal_warning { - transition: max-height 0.5s ease-out; - overflow: hidden; -} - -</style> - <div class="container-fluid"> <div class="row"> <div class="col-12 col-lg-5"> <div class="card mb-4"> <div class="card-header d-flex"> - <h4 style="display: inline;">Overview</h4> + <h4 class="d-inline">Overview</h4> <button data-toggle="collapse" data-target="#panel_overview" class="btn btn-outline-secondary ml-auto">Expand</button> </div> - <div class="card-body collapse show" id="panel_overview"> - <table class="table"> - <tr> - <td>Purpose</td> - <td>{{ booking.purpose }}</td> - </tr> - <tr> - <td>Project</td> - <td>{{ booking.project }}</td> - </tr> - <tr> - <td>Start Time</td> - <td>{{ booking.start }}</td> - </tr> - <tr> - <td>End Time</td> - <td>{{ booking.end }}</td> - </tr> - <tr> - <td>Pod Definition</td> - <td>{{ booking.resource.template }}</td> - </tr> - <tr> - <td>Pod Configuration</td> - <td>{{ booking.config_bundle }}</td> - </tr> - <tr> - <td>Lab Deployed At</td> - <td>{{ booking.lab }}</td> - </tr> - </table> + <div class="collapse show" id="panel_overview"> + <div class="card-body"> + <table class="table"> + <tr> + <td>Purpose</td> + <td>{{ booking.purpose }}</td> + </tr> + <tr> + <td>Project</td> + <td>{{ booking.project }}</td> + </tr> + <tr> + <td>Start Time</td> + <td>{{ booking.start }}</td> + </tr> + <tr> + <td>End Time</td> + <td>{{ booking.end }}</td> + </tr> + <tr> + <td>Pod Definition</td> + <td>{{ booking.resource.template }}</td> + </tr> + <tr> + <td>Pod Configuration</td> + <td>{{ booking.config_bundle }}</td> + </tr> + <tr> + <td>Lab Deployed At</td> + <td>{{ booking.lab }}</td> + </tr> + </table> + </div> </div> </div> <div class="row"> <div class="col-lg-12"> <div class="card"> <div class="card-header d-flex"> - <h4 style="display: inline;">Pod</h4> + <h4 class="d-inline">Pod</h4> <button data-toggle="collapse" data-target="#pod_panel" class="btn btn-outline-secondary ml-auto">Expand</button> </div> - <div class="card-body collapse show" id="pod_panel"> - <table class="table"> - {% for host in booking.resource.hosts.all %} - <tr> - <td><h4>{{host.template.resource.name}}</h4></td> - <td> - <table class="table"> - <tr> - <td>Hostname:</td> - <td>{{host.template.resource.name}}</td> - </tr> - <tr> - <td>Profile:</td> - <td>{{host.name}}</td> - </tr> - <tr> - <td>Role:</td> - <td>{{host.config.opnfvRole}}</td> - </tr> - <tr> - <td>Image:</td> - <td id="host_image_{{host.id}}"> - {{host.config.image}} - <button - style="margin-left:10px;" - class="btn btn-primary" - data-toggle="modal" - data-target="#imageModal" - onclick="set_image_dropdown('{{host.profile.name}}', {{host.id}});" - >Change/Reset</button></td> - </tr> - <tr> - <td>RAM:</td> - <td>{{host.profile.ramprofile.first.amount}}G, - {{host.profile.ramprofile.first.channels}} channels</td> - </tr> - <tr> - <td>CPU:</td> - <td> - <table class="table"> - <tr> - <td>Arch:</td> - <td>{{host.profile.cpuprofile.first.architecture}}</td> - </tr> - <tr> - <td>Cores:</td> - <td>{{host.profile.cpuprofile.first.cores}}</td> - </tr> - <tr> - <td>Sockets:</td> - <td>{{host.profile.cpuprofile.first.cpus}}</td> - </tr> - </table> - </td> - </tr> - <tr> - <td>DISK:</td> - <td> - <table class="table"> - <tr> - <td>Size:</td> - <td>{{host.profile.storageprofile.first.size}} GiB</td> - </tr> - <tr> - <td>Type:</td> - <td>{{host.profile.storageprofile.first.media_type}}</td> - </tr> - <tr> - <td>Mount Point:</td> - <td>{{host.profile.storageprofile.first.name}}</td> - </tr> - </table> - </td> - </tr> - <tr> - <td>Interfaces:</td> - <td> - <style> - .borderless td { - border: none !important; - } - </style> - <table class="table"> - {% for intprof in host.profile.interfaceprofile.all %} - <tr> - <td> - <table class="table borderless"> + <div class="collapse show" id="pod_panel"> + <div class="card-body"> + <table class="table"> + {% for host in booking.resource.hosts.all %} + <tr> + <td><h4>{{host.template.resource.name}}</h4></td> + <td> + <table class="table"> + <tr> + <td>Hostname:</td> + <td>{{host.template.resource.name}}</td> + </tr> + <tr> + <td>Profile:</td> + <td>{{host.name}}</td> + </tr> + <tr> + <td>Role:</td> + <td>{{host.config.opnfvRole}}</td> + </tr> + <tr> + <td>Image:</td> + <td id="host_image_{{host.id}}"> + {{host.config.image}} + <button + class="btn btn-primary ml-4" + data-toggle="modal" + data-target="#imageModal" + onclick="set_image_dropdown('{{host.profile.name}}', {{host.id}});" + >Change/Reset</button></td> + </tr> + <tr> + <td>RAM:</td> + <td>{{host.profile.ramprofile.first.amount}}G, + {{host.profile.ramprofile.first.channels}} channels</td> + </tr> + <tr> + <td>CPU:</td> + <td> + <table class="table"> + <tr> + <td>Arch:</td> + <td>{{host.profile.cpuprofile.first.architecture}}</td> + </tr> <tr> - <td>Name:</td> - <td>{{intprof.name}}</td> + <td>Cores:</td> + <td>{{host.profile.cpuprofile.first.cores}}</td> </tr> <tr> - <td>Speed:</td> - <td>{{intprof.speed}}</td> + <td>Sockets:</td> + <td>{{host.profile.cpuprofile.first.cpus}}</td> </tr> </table> - </td> - </tr> - {% endfor %} - </table> - </td> - </tr> - </table> - </td> - {% endfor %} - </tr> - </table> + </td> + </tr> + <tr> + <td>DISK:</td> + <td> + <table class="table"> + <tr> + <td>Size:</td> + <td>{{host.profile.storageprofile.first.size}} GiB</td> + </tr> + <tr> + <td>Type:</td> + <td>{{host.profile.storageprofile.first.media_type}}</td> + </tr> + <tr> + <td>Mount Point:</td> + <td>{{host.profile.storageprofile.first.name}}</td> + </tr> + </table> + </td> + </tr> + <tr> + <td>Interfaces:</td> + <td> + <table class="table"> + {% for intprof in host.profile.interfaceprofile.all %} + <tr> + <td> + <table class="table 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> + </td> + </tr> + </table> + </td> + {% endfor %} + </tr> + </table> + </div> </div> </div> </div> @@ -180,105 +170,72 @@ <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> + <h4 class="d-inline">Deployment Progress</h4> + <p>These are the different tasks that have to be completed before your deployment is ready</p> <button data-toggle="collapse" data-target="#panel_tasks" class="btn btn-outline-secondary ml-auto">Expand</button> </div> - <div class="card-body collapse show" id="panel_tasks"> - <table class="table"> - <style> - .progress { - display: inline-block; - border: 3px solid #f3f3f3; - border-radius: 50%; - border-top: 3px solid #12aebb; - width: 20px; - height: 20px; - -webkit-animation: spin 2s linear infinite; /* Safari */ - animation: spin 2s linear infinite; - } - - @keyframes spin { - 0% {transform: rotate(0deg);} - 100% {transform: rotate(360deg);} - } - - .new { - display: inline-block; - width: 20px; - height: 20px; - background: #f3f3f3; - border-radius: 50%; - animation: fadeInOut 1s infinite alternate; - - } - @keyframes fadeInOut { - from { opacity: 0;} - } - .done { - display: inline-block; - width: 20px; - height: 20px; - background: #40B976; - border-radius: 50%; - } - </style> - <tr> - <th></th> - <th>Status</th> - <th>Lab Response</th> - <th>Type</th> - </tr> - {% for task in booking.job.get_tasklist %} - <tr> - <td> - {% if task.status < 100 %} - <div class="new"></div> - {% elif task.status < 200 %} - <div class="progress"></div> - {% else %} - <div class="done"></div> - {% endif %} - </td> - <td> - {% if task.status < 100 %} - PENDING - {% elif task.status < 200 %} - IN PROGRESS - {% else %} - DONE - {% endif %} - </td> - <td> - {% if task.message %} - {% if task.type_str == "Access Task" and user_id != task.config.user.id %} - Message from Lab: <pre>--secret--</pre> - {% else %} - Message from Lab: <pre>{{ task.message }}</pre> - {% endif %} - {% else %} - No response provided (yet) - {% endif %} - </td> - <td> - {{ task.type_str }} - </td> - </tr> - {% endfor %} - </table> + <div class="collapse show" id="panel_tasks"> + <div class="card-body"> + <table class="table"> + <tr> + <th></th> + <th>Status</th> + <th>Lab Response</th> + <th>Type</th> + </tr> + {% for task in booking.job.get_tasklist %} + <tr> + <td> + {% if task.status < 100 %} + <div class="rounded-circle bg-secondary square-20"></div> + {% elif task.status < 200 %} + <div class="spinner-border text-primary square-20"></div> + {% else %} + <div class="rounded-circle bg-success square-20"></div> + {% endif %} + </td> + <td> + {% if task.status < 100 %} + PENDING + {% elif task.status < 200 %} + IN PROGRESS + {% else %} + DONE + {% endif %} + </td> + <td> + {% if task.message %} + {% if task.type_str == "Access Task" and user_id != task.config.user.id %} + Message from Lab: <pre>--secret--</pre> + {% else %} + Message from Lab: <pre>{{ task.message }}</pre> + {% endif %} + {% else %} + No response provided (yet) + {% endif %} + </td> + <td> + {{ task.type_str }} + </td> + </tr> + {% endfor %} + </table> + </div> </div> </div> <div class="row"> <div class="col"> <div class="card"> <div class="card-header d-flex"> - <h4 style="display: inline;">PDF</h4> + <h4 class="d-inline">PDF</h4> <button data-toggle="collapse" data-target="#pdf_panel" class="btn btn-outline-secondary ml-auto">Expand</button> </div> - <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}} - </pre> + <div class="collapse show" id="pdf_panel"> + <div class="card-body p-0"> + <pre class="prettyprint lang-yaml m-0 p-4 border-0"> + {{pdf}} + </pre> + </div> </div> </div> </div> @@ -289,10 +246,10 @@ <div class="modal fade" id="imageModal" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel" aria-hidden="true"> - <div class="modal-dialog" style="width: 450px;" role="document"> + <div class="modal-dialog" role="document"> <div class="modal-content"> <div class="modal-header"> - <h4 class="modal-title" id="exampleModalLabel" style="display: inline; float: left;">Host Image</h4> + <h4 class="modal-title d-inline float-left" id="exampleModalLabel">Host Image</h4> <button type="button" class="close" data-dismiss="modal" aria-label="Close"> <span aria-hidden="true">×</span> </button> @@ -300,23 +257,24 @@ <div class="modal-body"> <form id="image_host_form"> {% csrf_token %} - <select class="form-control" style="width: 80%; margin-left: 10%" id="image_select" name="image_id"> + <select class="form-control" id="image_select" name="image_id"> </select> <input id="host_id_input" type="hidden" name="host_id"> </input> </form> </div> - <div class="modal-footer"> - <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button> - <button type="button" class="btn btn-primary" onclick="document.getElementById('modal_warning').style['max-height'] = '500px';">Reset Host</button> - </div> - <div id="modal_warning" class="modal-footer" style="max-height:0px;" > - <div style="text-align:center; margin: 5px"> + <div class="modal-footer d-flex flex-column"> + <div> + <button type="button" class="btn btn-outline-secondary" data-dismiss="modal">Close</button> + <button type="button" class="btn btn-danger" data-toggle="collapse" data-target="#modal_warning" aria-expanded="false">Reset Host</button> + </div> + <div class="border-top collapse mt-3 py-2 text-center w-100" id="modal_warning"> <h3>Are You Sure?</h3> <p>This will wipe the disk and reimage the host</p> - <button class="btn" onclick="document.getElementById('modal_warning').style['max-height'] = '0px';">Nevermind</button> + <button class="btn btn-outline-secondary" data-dismiss="modal">Nevermind</button> <button class="btn btn-danger" data-dismiss="modal" onclick="submit_image_form();">I'm Sure</button> </div> + </div> </div> </div> </div> @@ -342,7 +300,7 @@ dropdown.options.add(opt); } - document.getElementById("modal_warning").style['max-height'] = '0px'; + document.getElementById("modal_warning").classList.add("collapse"); } function submit_image_form() { diff --git a/src/templates/booking/quick_deploy.html b/src/templates/booking/quick_deploy.html index 07f3d89..6776980 100644 --- a/src/templates/booking/quick_deploy.html +++ b/src/templates/booking/quick_deploy.html @@ -2,88 +2,61 @@ {% load staticfiles %} {% load bootstrap4 %} {% block content %} -<style> - .grid_container { - display: grid; - grid-template-columns: repeat(12, 1fr); - padding: 30px; - } - .grid_element { - border-radius: 5px; - border: 1px solid #ccc; - margin: 10px; - padding: 7px; - } - .grid_element_wide { - grid-column-start: span 12; - } - .grid_element_half { - grid-column-start: span 6; - } - .grid_element_1third { - grid-column-start: span 4; - } - .grid_element_2third { - grid-column-start: span 8; - } - .collaborator_pane { - display: flex; - flex-direction: column; - } - #id_length { - -moz-appearance: none; - border: none; - box-shadow: none; - } - input[type=range]::-moz-range-track { - background: #cccccc; - } - - .grid_element { - overflow: hidden; - } -</style> {% bootstrap_form_errors form type='non_fields' %} <form id="quick_booking_form" action="/booking/quick/" method="POST" class="form"> -{% csrf_token %} -<div class="grid_container"> -<div class="grid_element host_select_pane grid_element_wide"> -<p>Please select a host type you wish to book. Only available types are shown.</p> -{% bootstrap_field form.filter_field show_label=False %} -</div> -<div class="grid_element booking_info_pane grid_element_1third"> - {% bootstrap_field form.purpose %} - {% bootstrap_field form.project %} - {% bootstrap_field form.length %} - <p style="display:inline;">Days: </p><output id="daysout" style="display:inline;">0</output> - <script> - document.getElementById("id_length").setAttribute("oninput", "daysout.value=this.value"); - document.getElementById("daysout").value = document.getElementById("id_length").value; - </script> -</div> -<div class="grid_element collaborator_pane grid_element_1third"> - <label>Collaborators</label> - {{ form.users }} -</div> -<div class="grid_element_1third"> - <div class="configuration_pane grid_element"> - {% bootstrap_field form.hostname %} - {% bootstrap_field form.image %} + {% csrf_token %} + <div class="container-fluid"> + <div class="row mx-0 px-0"> + <div class="col-12 mx-0 px-0 mt-2"> + <p class="my-0">Please select a host type you wish to book. Only available types are shown.</p> + {% bootstrap_field form.filter_field show_label=False %} + </div> + </div> + <div class="row"> + <div class="col-12 col-lg-3 px-1 my-2"> + <div class="col border rounded py-2 h-100"> + {% bootstrap_field form.purpose %} + {% bootstrap_field form.project %} + {% bootstrap_field form.length %} + <span>Days: </span><output id="daysout">0</output> + <script> + document.getElementById("id_length").setAttribute("oninput", "daysout.value=this.value"); + document.getElementById("daysout").value = document.getElementById("id_length").value; + </script> + </div> + </div> + <div class="col-12 col-lg-3 px-1 my-2"> + <div class="col border rounded py-2 h-100"> + <label>Collaborators</label> + {{ form.users }} + </div> + </div> + <div class="col-12 col-lg-3 px-1 my-2"> + <div class="col border rounded py-2 h-100"> + {% bootstrap_field form.hostname %} + {% bootstrap_field form.image %} + </div> + </div> + <div class="col-12 col-lg-3 px-1 my-2"> + <div class="col border rounded py-2 h-100"> + <strong>OPNFV: (Optional)</strong> + {% bootstrap_field form.installer %} + {% bootstrap_field form.scenario %} + </div> + </div> + <div class="col-12 d-flex px-0 mt-2 justify-content-end"> + <button id="quick_booking_confirm" onclick="submit_form();" type="button" class="btn btn-success">Confirm</button> + </div> + </div> </div> - <div class="configuration_pane grid_element"> - <strong>OPNFV: (Optional)</strong> - {% bootstrap_field form.installer %} - {% bootstrap_field form.scenario %} - </div> -</div> -</div> -<script type="text/javascript"> +</form> +<script type="text/javascript"> function submit_form() { - //formats data for form submission - multi_filter_widget.finish(); + run_form_callbacks(); + document.getElementById("quick_booking_form").submit(); } function hide_dropdown(drop_id) { @@ -94,13 +67,6 @@ 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){ @@ -108,42 +74,33 @@ 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 }}; + var sup_image_dict = {{image_filter | safe}}; + var sup_installer_dict = {{installer_filter | safe}}; + var sup_scenario_dict = {{scenario_filter | safe}}; - function imageHider() { + function imageFilter() { var drop = document.getElementById("id_image"); - - hide_dropdown("id_image"); - 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 + for (const childNode of drop.childNodes) { + var image_object = sup_image_dict[childNode.value]; + if (image_object) //weed out empty option { - if( image_object.host_profile == host_pk && image_object.lab == lab_pk ) - { - drop.childNodes[i].style.display = "inherit"; - drop.childNodes[i].disabled = false; - } + childNode.disabled = !(image_object.host_profile == host_pk && image_object.lab == lab_pk); } } } - imageHider(); + imageFilter(); $('#id_installer').children().hide(); $('#id_scenario').children().hide(); - Array.from(document.getElementsByClassName("grid-item-select-btn")).forEach(function(element) { - element.addEventListener('click', imageHider); + Array.from(document.getElementsByClassName("grid-item-select-btn")).forEach(function (element) { + element.addEventListener('click', imageFilter); }); function installerHider() { @@ -175,13 +132,11 @@ } for (var i = 0; i < dropdown.childNodes.length; i++) { - if (dropdown.childNodes[i].value in opts && !(dropdown.childNodes[i].value in {}) ) { + 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> {% endblock %} diff --git a/src/templates/booking/stats.html b/src/templates/booking/stats.html index 8bc68cd..ed34731 100644 --- a/src/templates/booking/stats.html +++ b/src/templates/booking/stats.html @@ -43,11 +43,11 @@ function getData(){ {% block content %} <div class="container-fluid"> <div class="row"> - <div class="col"> + <div class="col-auto"> <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 class="form-group d-flex align-content-center"> + <input id="number_days" type="number" class="form-control d-inline-block w-auto" min="1" step="1"/> + <button class="btn btn-primary ml-1" onclick="getData();">Submit</button> </div> </div> </div> diff --git a/src/templates/booking/steps/booking_confirm.html b/src/templates/booking/steps/booking_confirm.html deleted file mode 100644 index 40c30a9..0000000 --- a/src/templates/booking/steps/booking_confirm.html +++ /dev/null @@ -1,25 +0,0 @@ -{% extends "workflow/viewport-element.html" %} -{% load staticfiles %} - -{% load bootstrap4 %} - -{% block content %} - -<style> - #form_div { - width: 100%; - padding: 5%; - text-align: center; - } -</style> - -<div id="form_div"> -<p>confirm booking</p> -<p>{{info|default:"booking info"}}</p> -<form action="/wf/workflow/" method="post"> - {% csrf_token %} - {{form|default:"<p> No Form Loaded</p>"}} - <input type="submit" value="Submit" style="display: none;"/> -</form> -</div> -{% endblock content %}
\ No newline at end of file diff --git a/src/templates/booking/steps/booking_meta.html b/src/templates/booking/steps/booking_meta.html index 710d4ee..f12496e 100644 --- a/src/templates/booking/steps/booking_meta.html +++ b/src/templates/booking/steps/booking_meta.html @@ -5,83 +5,34 @@ {% block content %} -<style> - .bkmeta_panel { - 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;*/ - } - - .panel_wrap { - width: 100%; - display: grid; - grid-template-columns: 45% 10% 45%; - border: none; - } - - #id_length { - -moz-appearance: none; - border: none; - box-shadow: none; - } - input[type=range]::-moz-range-track { - background: #cccccc; - } -</style> - {% bootstrap_form_errors form type='non_fields' %} -<form id="booking_meta_form" action="/wf/workflow/" method="POST" class="form"> +<form id="step_form" method="POST" class="form"> {% csrf_token %} -<div id="form_div"> - <div class="panel_wrap"> - <div class="panel bkmeta_panel"> - {% bootstrap_field form.purpose %} - {% bootstrap_field form.project %} - {% bootstrap_field form.length %} - <p style="display:inline;">Days: </p><output id="daysout" style="display:inline;">0</output> - <script> - document.getElementById("id_length").setAttribute("oninput", "daysout.value=this.value"); - document.getElementById("daysout").value = document.getElementById("id_length").value; - </script> - {% bootstrap_field form.info_file %} - <p>You must provide a url to your project's INFO.yaml file if you are a PTL and you are trying to book a POD with multiple servers in it.</p> - {% bootstrap_field form.deploy_opnfv %} - </div> - <div class="panel panel_center"> - </div> - <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 id="form_div" class="container-fluid"> + <div class="row"> + <div class="p-4 col"> + {% bootstrap_field form.purpose %} + {% bootstrap_field form.project %} + {% bootstrap_field form.length %} + <span>Days: </span><output id="daysout">0</output> + <script> + document.getElementById("id_length").oninput = function() { daysout.value=this.value; } + document.getElementById("daysout").value = document.getElementById("id_length").value; + </script> + {% bootstrap_field form.info_file %} + <p>You must provide a url to your project's INFO.yaml file if you are a PTL and you are trying to book a POD with multiple servers in it.</p> + {% bootstrap_field form.deploy_opnfv %} + </div> + <div class="p-4 col"> + <p>You may add collaborators on your booking to share resources with coworkers.</p> + {% bootstrap_field form.users label="Collaborators" %} + </div> </div> - - {% buttons %} - <button type="submit" style="display: none;" class="btn btn-success">Confirm</button> - {% endbuttons %} + <div class="panel_wrap"> + {% buttons %} + <button type="submit" class="btn btn-success d-none">Confirm</button> + {% endbuttons %} </div> </div> </form> {% endblock content %} - -{% block onleave %} -var ajaxForm = $("#booking_meta_form"); -var formData = ajaxForm.serialize(); -req = new XMLHttpRequest(); -req.open("POST", "/wf/workflow/", false); -req.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); -req.onerror = function() { alert("problem submitting form"); } -req.send(formData); -{% endblock %} diff --git a/src/templates/booking/steps/resource_select.html b/src/templates/booking/steps/resource_select.html deleted file mode 100644 index 382316f..0000000 --- a/src/templates/booking/steps/resource_select.html +++ /dev/null @@ -1,73 +0,0 @@ -{% extends "workflow/viewport-element.html" %} -{% load staticfiles %} - -{% load bootstrap4 %} - -{% block content %} - -<style> - #resource_form_div { - width: 100%; - padding: 5%; - } - - .panel { - /*border: solid 1px black;*/ - border: none; - } - .select_panels { - width: 100%; - display: grid; - grid-template-columns: 45% 10% 45%; - - } - - .panel_center { - text-align: center; - border: none; - - - } - .panel_center p{ - font-size: 20pt; - } -</style> - -<div id="resource_form_div"> - <div class="select_panels"> - <div class="panel_chooser panel"> - <form id="resource_select_form" method="post" action="" class="form" id="resourceselectorform"> - {% csrf_token %} - {{ form|default:"<p>no form loaded</p>" }} - {% buttons %} - - {% endbuttons %} - </form> - </div> - <div class="panel_center panel"><p>OR</p></div> - <div class="panel_add panel"> - <button class="btn {% if disabled %} disabled {% endif %}" - style="width: 100%; height: 100%;" - {% if not disabled %}onclick="parent.add_wf(1)" - {% endif %}>Create Resource</button> - </div> - -</div> -</div> -<script> - {% if disabled %} - disable(); - {% endif %} -</script> - -{% endblock content %} -{% block onleave %} -var form = $("#resource_select_form"); -var formData = form.serialize(); -var 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"); } -req.send(formData); -{% endblock %} - diff --git a/src/templates/booking/steps/swconfig_select.html b/src/templates/booking/steps/swconfig_select.html deleted file mode 100644 index 60a0df7..0000000 --- a/src/templates/booking/steps/swconfig_select.html +++ /dev/null @@ -1,73 +0,0 @@ -{% extends "workflow/viewport-element.html" %} -{% load staticfiles %} - -{% load bootstrap4 %} - -{% block content %} - -<style> - #sw_form_div { - width: 100%; - padding: 5%; - } - - .panel { - /*border: solid 1px black;*/ - border: none; - } - .select_panels { - width: 100%; - display: grid; - grid-template-columns: 45% 10% 45%; - - } - - .panel_center { - text-align: center; - border: none; - - - } - .panel_center p{ - font-size: 20pt; - } -</style> - -<div id="sw_form_div"> - <div class="select_panels"> - <div class="panel_chooser panel"> - <form id="software_select_form" method="post" action="" class="form" id="swselectorform"> - {% csrf_token %} - {{ form|default:"<p>no form loaded</p>" }} - {% buttons %} - - {% endbuttons %} - </form> - </div> - <div class="panel_center panel"><p>OR</p></div> - <div class="panel_add panel"> - <button class="btn {% if disabled %} disabled {% endif %}" - style="width: 100%; height: 100%;" - {% if not disabled %}onclick="parent.add_wf(2)" - {% endif %}>Create Config</button> - </div> - <script> - {% if disabled %} - disable(); - {% endif %} - </script> - -</div> -</div> - -{% endblock content %} - -{% block onleave %} -var form = $("#software_select_form"); -var formData = form.serialize(); -var 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"); } -req.send(formData); -{% endblock %} diff --git a/src/templates/config_bundle/steps/config_software.html b/src/templates/config_bundle/steps/config_software.html index 68417bc..6fe0ac1 100644 --- a/src/templates/config_bundle/steps/config_software.html +++ b/src/templates/config_bundle/steps/config_software.html @@ -5,7 +5,7 @@ {% block content %} -<form action="/wf/workflow/" method="POST" id="software_config_form" class="form"> +<form method="POST" id="step_form" class="form"> {% csrf_token %} <p>Give it a name:</p> {% bootstrap_field form.name %} @@ -16,13 +16,3 @@ {% endblock content %} - -{% block onleave %} -var ajaxForm = $("#software_config_form"); -var formData = ajaxForm.serialize(); -req = new XMLHttpRequest(); -req.open("POST", "/wf/workflow/", false); -req.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); -req.onerror = function() { alert("problem submitting form"); } -req.send(formData); -{% endblock %} diff --git a/src/templates/config_bundle/steps/define_software.html b/src/templates/config_bundle/steps/define_software.html index 87e5997..43f3f5d 100644 --- a/src/templates/config_bundle/steps/define_software.html +++ b/src/templates/config_bundle/steps/define_software.html @@ -27,29 +27,19 @@ {% block tablejs %} <script> - document.getElementById("radio_{{headnode}}").checked = true; -</script> -{% endblock tablejs %} - - -{% block onleave %} -var parents = document.getElementsByClassName("table_hidden_input_parent"); -for(var i=0; i<parents.length; i++){ - var node = parents[i]; - var radio = node.getElementsByClassName("my_radio")[0]; - var checkbox = radio.nextElementSibling; - if(radio.checked){ - checkbox.value = "True"; + function radio_pre_submit(){ + var parents = document.getElementsByClassName("table_hidden_input_parent"); + for(const node of parents){ + const radio = node.getElementsByClassName("my_radio")[0]; + const checkbox = radio.nextElementSibling; + if(radio.checked){ + checkbox.value = "True"; + } + } } -} -var form = $("#table_formset"); -var formData = form.serialize(); -var 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"); } -req.send(formData); -{% endblock onleave %} + form_submission_callbacks.push(radio_pre_submit); +</script> +{% endblock tablejs %} diff --git a/src/templates/config_bundle/steps/pick_installer.html b/src/templates/config_bundle/steps/pick_installer.html index 31a06de..c3b505d 100644 --- a/src/templates/config_bundle/steps/pick_installer.html +++ b/src/templates/config_bundle/steps/pick_installer.html @@ -9,7 +9,7 @@ <h1>Please choose a config bundle first</h1> {% else %} -<form id="installer_form" action="/wf/workflow/" method="POST" id="installer_config_form" class="form"> +<form id="step_form" method="POST" class="form"> {% csrf_token %} <p>Choose your installer:</p> {% bootstrap_field form.installer %} @@ -20,13 +20,3 @@ {% endif %} {% endblock content %} - -{% block onleave %} -var form = $("#installer_form"); -var formData = form.serialize(); -var 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"); } -req.send(formData); -{% endblock %} diff --git a/src/templates/config_bundle/steps/table_formset.html b/src/templates/config_bundle/steps/table_formset.html index 18edc72..d25621e 100644 --- a/src/templates/config_bundle/steps/table_formset.html +++ b/src/templates/config_bundle/steps/table_formset.html @@ -15,10 +15,10 @@ {% block content %} {% if error %} - <h1 style="text-align:center;">{{ error }}</h1> + <h1 class="text-center">{{ error }}</h1> {% else %} -<div style="padding: 5%;"> - <form method="post" action="" class="form" id="table_formset"> +<div class="p-2"> + <form method="post" class="form" id="step_form"> <!-- formset, requires special consideration --> {% csrf_token %} <div class="row"> @@ -51,14 +51,3 @@ {% block tablejs %} {% endblock tablejs %} {% endblock extrajs %} - - -{% block onleave %} -var form = $("#table_formset"); -var formData = form.serialize(); -var 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"); } -req.send(formData); -{% endblock %} diff --git a/src/templates/dashboard/genericselect.html b/src/templates/dashboard/genericselect.html index 441d8dc..863d33f 100644 --- a/src/templates/dashboard/genericselect.html +++ b/src/templates/dashboard/genericselect.html @@ -1,86 +1,22 @@ {% extends "workflow/viewport-element.html" %} -{% load staticfiles %} {% load bootstrap4 %} {% block content %} -<style> - #page-wrapper { - display: flex; - flex-direction: column; - } - - #{{select_type}}_form_div div { - } - - #{{select_type}}_form_div > * { - margin-left: 10px; - margin-right: 10px; - margin-bottom: 20px; - } - - #{{select_type}}_form_div div * { - } - - #{{select_type}}_form_div { - flex: 1; - margin: 30px; - display: flex; - flex-direction: column; - } - - #select_section { - flex: 1; - display: flex; - flex-direction: column; - } - - #{{select_type}}_select_form { - flex: 1; - display: flex; - flex-direction: column; - } - - .autocomplete { - flex: 1; - } - - #create_section { - } - - #select_header_section { - } - - h3 { - margin-top: 0; - margin-bottom: 0; - vertical-align: middle; - } - - .divider { - border-top: 1px solid #ccc; - } - - -</style> - -<div id="{{select_type}}_form_div"> +<div id="select_form_div" class="h-100 border d-flex flex-column p-4"> <h3 id="create_section">Create a Resource <button class="btn btn-primary {% if disabled %} disabled {% endif %}" - {% if not disabled %}onclick="parent.add_wf({{addable_type_num}})" + {% if not disabled %}onclick="add_workflow({{addable_type_num}})" {% endif %}>Here </button> </h3> - <div class="divider"></div> + <div class="border-top"></div> <h3 id="select_header_section">Or select from the list below:</h3> - <div id="select_section"> - <form id="{{select_type}}_select_form" method="post" action="" class="form" id="{{select_type}}selectorform"> + <div id="select_section" class="d-flex flex-column"> + <form id="step_form" method="post" action="" class="form d-flex flex-column"> {% csrf_token %} {{ form|default:"<p>no form loaded</p>" }} - {% buttons %} - - {% endbuttons %} </form> </div> </div> @@ -92,13 +28,3 @@ </script> {% endblock content %} -{% block onleave %} -var form = $("#{{select_type}}_select_form"); -var formData = form.serialize(); -var 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"); } -req.send(formData); -{% endblock %} - diff --git a/src/templates/dashboard/grid.html b/src/templates/dashboard/grid.html deleted file mode 100644 index ca47f44..0000000 --- a/src/templates/dashboard/grid.html +++ /dev/null @@ -1,10 +0,0 @@ -{% extends "base.html" %} -{% load staticfiles %} -{% if script %} <script>{{ script }}</script> {% endif %} -{% block content %} - {% for item in grid_items %} - <div class="grid-item" style="border:2px; border-style:solid; border-color:black; margin:3px"> - {{ item }} - </div> - {% endfor %} -{% endblock content %} diff --git a/src/templates/dashboard/host_profile_detail.html b/src/templates/dashboard/host_profile_detail.html index abc7648..f65d4fe 100644 --- a/src/templates/dashboard/host_profile_detail.html +++ b/src/templates/dashboard/host_profile_detail.html @@ -18,7 +18,6 @@ } ); </script> - <style media="screen"> @media screen and (min-width: 0px) and (max-width: 767px) diff --git a/src/templates/dashboard/lab_detail.html b/src/templates/dashboard/lab_detail.html index 336b32e..3c41caf 100644 --- a/src/templates/dashboard/lab_detail.html +++ b/src/templates/dashboard/lab_detail.html @@ -11,80 +11,92 @@ <div class="col-lg-4"> <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> + <h4>Lab Profile</h4> + <button class="btn btn-outline-secondary ml-auto" data-toggle="collapse" data-target="#panel_overview">Expand</button> </div> - <div id="panel_overview" class="card-body collapse show"> - <table class="table"> - <tr> - <td>Lab Name: </td><td>{{lab.name}}</td> - </tr> - <tr> - <td>Lab Location: </td><td>{{lab.location}}</td> - </tr> - <tr> - <td>Lab Email: </td> - <td>{{lab.contact_email}}</td> - </tr> - {% if lab.contact_phone %} - <tr> - <td>Lab Phone: </td> - <td>{{lab.contact_phone}}</td> - </tr> - {% endif %} - <tr> - <td>Lab Status: </td> - {% if lab.status < 100 %} - <td><div style="border-radius: 50%; background:#40B976; height: 20px; width: 20px; display: inline-block;"></div> - Up</td> - {% elif lab.status < 200 %} - <td> - <div style="border-radius: 50%; background:#eeee00; height: 20px; width: 20px; display: inline-block;"></div> - Temporarily Offline</td> - {% else %} - <td> - <div style="border-radius: 50%; background:#e50000; height: 20px; width: 20px; display: inline-block;"></div> - Offline Indefinitely</td> + <div class="collapse show" id="panel_overview"> + <div class="card-body"> + <table class="table"> + <tr> + <td>Lab Name: </td> + <td>{{lab.name}}</td> + </tr> + <tr> + <td>Lab Location: </td> + <td>{{lab.location}}</td> + </tr> + <tr> + <td>Lab Email: </td> + <td>{{lab.contact_email}}</td> + </tr> + {% if lab.contact_phone %} + <tr> + <td>Lab Phone: </td> + <td>{{lab.contact_phone}}</td> + </tr> {% endif %} - </tr> - </table> + <tr> + <td>Lab Status: </td> + {% if lab.status < 100 %} + <td> + <div class="rounded-circle bg-success square-20 d-inline-block"></div> + Up + </td> + {% elif lab.status < 200 %} + <td> + <div class="rounded-circle bg-warning square-20 d-inline-block"></div> + Temporarily Offline + </td> + {% else %} + <td> + <div class="rounded-circle bg-danger square-20 d-inline-block"></div> + Offline Indefinitely + </td> + {% endif %} + </tr> + </table> + </div> </div> </div> <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> + <button data-toggle="collapse" data-target="#profile_panel" class="btn btn-outline-secondary ml-auto">Expand</button> </div> - <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-info">Profile</a></td> - </tr> - {% endfor %} - </table> + <div class="collapse show" id="profile_panel"> + <div class="card-body"> + <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-info">Profile</a></td> + </tr> + {% endfor %} + </table> + </div> </div> </div> <div class="card my-2"> <div class="card-header d-flex"> - <h4 style="display: inline;">Networking Capabilities</h4> - <button data-toggle="collapse" data-target="#network_panel" class="btn btn-outline-secondary ml-auto" style="line-height: 1;" >Expand</button> + <h4 class="d-inline">Networking Capabilities</h4> + <button data-toggle="collapse" data-target="#network_panel" class="btn btn-outline-secondary ml-auto">Expand</button> </div> - <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 class="collapse show" id="network_panel"> + <div class="card-body"> + <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> <div class="card my-2"> @@ -92,23 +104,25 @@ <h4>Images</h4> <button data-toggle="collapse" data-target="#image_panel" class="btn btn-outline-secondary ml-auto">Expand</button> </div> - <div class="card-body collapse show" id="image_panel"> - <table class="table"> - <tr> - <th>Name</th> - <th>Owner</th> - <th>For Host Type</th> - <th>Description</th> - </tr> - {% for image in images %} - <tr> - <td>{{image.name}}</td> - <td>{{image.owner}}</td> - <td>{{image.host_type}}</td> - <td>{{image.description}}</td> - </tr> - {% endfor %} - </table> + <div class="collapse show" id="image_panel"> + <div class="card-body"> + <table class="table"> + <tr> + <th>Name</th> + <th>Owner</th> + <th>For Host Type</th> + <th>Description</th> + </tr> + {% for image in images %} + <tr> + <td>{{image.name}}</td> + <td>{{image.owner}}</td> + <td>{{image.host_type}}</td> + <td>{{image.description}}</td> + </tr> + {% endfor %} + </table> + </div> </div> </div> @@ -120,29 +134,31 @@ <button data-toggle="collapse" data-target="#lab_hosts_panel" class="btn btn-outline-secondary ml-auto">Expand</button> </div> - <div class="card-body collapse show" id="lab_hosts_panel"> - <table class="table"> - <tr> - <th>Name</th> - <th>Profile</th> - <th>Booked</th> - <th>Working</th> - <th>Vendor</th> - </tr> - {% for host in lab.host_set.all %} - <tr> - <td>{{host.labid}}</td> - <td>{{host.profile}}</td> - <td>{{host.booked}}</td> - {% if host.working %} - <td style="background-color: #40B976;">{{host.working}}</td> - {% else %} - <td>{{host.working}}</td> - {% endif %} - <td>{{host.vendor}}</td> - </tr> - {% endfor %} - </table> + <div class="collapse show" id="lab_hosts_panel"> + <div class="card-body"> + <table class="table"> + <tr> + <th>Name</th> + <th>Profile</th> + <th>Booked</th> + <th>Working</th> + <th>Vendor</th> + </tr> + {% for host in lab.host_set.all %} + <tr> + <td>{{host.labid}}</td> + <td>{{host.profile}}</td> + <td>{{host.booked}}</td> + {% if host.working %} + <td class="bg-success text-white">{{host.working}}</td> + {% else %} + <td>{{host.working}}</td> + {% endif %} + <td>{{host.vendor}}</td> + </tr> + {% endfor %} + </table> + </div> </div> </div> </div> diff --git a/src/templates/dashboard/lab_list.html b/src/templates/dashboard/lab_list.html index 9cde80c..2efebfc 100644 --- a/src/templates/dashboard/lab_list.html +++ b/src/templates/dashboard/lab_list.html @@ -1,26 +1,28 @@ {% extends "base.html" %} {% block content %} <h2>Labs</h2> -<div class="card_container"> +<div class="row"> {% for lab in labs %} - <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 class="p-2 col-12 col-md-6 col-lg-4 col-xl-3"> + <div class="card h-100"> + <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> </div> </div> {% endfor %} diff --git a/src/templates/dashboard/landing.html b/src/templates/dashboard/landing.html index e6a235f..72f9e6e 100644 --- a/src/templates/dashboard/landing.html +++ b/src/templates/dashboard/landing.html @@ -2,7 +2,7 @@ {% load staticfiles %} {% block content %} -<div class="" style="text-align: center;"> +<div class="text-center"> {% if not request.user.is_anonymous %} {% if not request.user.userprofile.ssh_public_key %} <div class="alert alert-danger" role="alert"> @@ -15,53 +15,11 @@ </div> {% csrf_token %} -<style> - .wf_create { - display: inline-block; - text-align: center; - } - - .wf_create_div { - text-align: center; - } - - .hidden_form { - display: none; - } - - .panel { - border: none; - } - - .panels { - display: grid; - grid-template-columns: 33% 34% 33%; - } - - .landing_container { - 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="container-fluid"> <div class="row"> <!-- About us --> <div class="col-12 col-lg-6 mb-4"> - <h2>About Us:</h2> + <h2 class="border-bottom">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 @@ -73,100 +31,64 @@ </div> <!-- Get started --> <div class="col-12 col-lg-6 mb-4"> - <h2>Get Started:</h2> + <h2 class="border-bottom">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> + <h4 class="text-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;" + <a class="btn btn-primary d-flex flex-column justify-content-center align-content-center border text-white p-4" href="/booking/quick/"> - <p style="font-size: xx-large">Book a Server</p> + <h4>Book a Server</h4> </a> - <p>PTLs can use our advanced options to book multi-node pods. If you are a PTL, you may use the options + <p class="mt-4">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 class="row"> + <div class="col-12 col-xl-4"> + <button class="btn btn-primary w-100" onclick="create_workflow(0)">Book a Pod</button> + </div> + <div class="col-12 col-xl-4"> + <button class="btn btn-primary w-100" onclick="create_workflow(1)">Design a Pod</button> + </div> + <div class="col-12 col-xl-4"> + <button class="btn btn-primary w-100" onclick="create_workflow(2)">Configure a Pod</button> </div> - {% endif %} </div> + {% endif %} </div> <!-- Returning users --> {% if not request.user.is_anonymous %} <div class="col-12 col-lg-6 offset-lg-6 mb-4 mt-lg-4"> - <h2 class="ht-4">Returning Users:</h2> + <h2 class="ht-4 border-bottom">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="row"> + <div class="col-12 col-xl-4"> + <button class="btn btn-primary w-100" onclick="create_workflow(3)">Snapshot a Host</button> + </div> + <div class="col-12 col-xl-4"> + <a class="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> + <button class="btn btn-primary w-100" onclick="continue_workflow()"> + Resume Workflow + </button> </div> - {% endif %} - </div> + {% endif %} </div> </div> {% endif %} </div> </div> -<script type="text/javascript"> - function cwf(type) { - $.ajax({ - type: "POST", - url: "/", - data: { - "create": type - }, - beforeSend: function (request) { - request.setRequestHeader("X-CSRFToken", - $('input[name="csrfmiddlewaretoken"]').val() - ); - } - - }).done(function (data) { - window.location.replace("/wf/"); - }).fail(function (jqxHR, textstatus) { - alert("Something went wrong..."); - }); - } - - function continue_wf() { - window.location.replace("/wf/"); - } -</script> - -<div class="hidden_form" id="form_div"> +<div class="hidden_form d-none" id="form_div"> <form method="post" action="" class="form" id="wf_selection_form"> {% csrf_token %} - - <input type="hidden" id="landing_action"> - - <button type="submit" class="btn btn btn-success"> - Confirm Edit - </button> </form> </div> -{% block vport_comm %} -{% endblock %} -{% endblock content %}
\ No newline at end of file +{% endblock content %} diff --git a/src/templates/dashboard/multiple_select_filter_widget.html b/src/templates/dashboard/multiple_select_filter_widget.html index 4302543..aee5f23 100644 --- a/src/templates/dashboard/multiple_select_filter_widget.html +++ b/src/templates/dashboard/multiple_select_filter_widget.html @@ -1,139 +1,43 @@ -<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; - border-right: 1px; - border-style: solid; - border-color: grey; -} - -.class_grid_wrapper:last-child { - border-right: none; -} - -.grid_wrapper { - display: grid; - grid-template-columns: 1fr 1fr; -} - -.grid-item { - cursor: pointer; - border: 1px solid #cccccc; - border-radius: 5px; - margin: 20px; - height: 200px; - padding: 7px; - 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 > .btn:active, .grid-item > .btn:focus { - outline: none; !important; - box-shadow: none; -} - -.grid-item-description { - flex: 1; -} - -.selected_node { - 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; -} - -.disabled_node:hover {} - -.cleared_node { - background-color: #FFFFFF; -} - -.grid-item-header { - font-weight: bold; - font-size: 20px; - margin-top: 10px; -} - -.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_item > button { - margin: 2px; - justify-self: end; -} - -.dropdown_item > h5 { - margin: auto; -} - -.dropdown_item > input { - padding: 7px; - margin: 2px; - width: 90%; -} - -#dropdown_wrapper { - display: grid; - 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 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" 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"> - {% if obj.multiple %}Add{% else %}Select{% endif %} - </button> +<div class="row"> + {% for object_class, object_list in display_objects %} + <div class="col-12 col-lg-6 d-flex flex-column pt-2 mx-0 px-1"> + <div class="col mx-0 border rounded py-2 flex-grow-1 d-flex flex-column"> + <div class="w-100"> + <h4 class="text-capitalize">{{object_class}}</h4> + </div> + <div id="{{object_class}}" class="row flex-grow-1"> + {% for obj in object_list %} + <div class="col-12 col-md-6 col-xl-4 my-2 d-flex flex-grow-1"> + <div id="{{ obj.id|default:'not_provided' }}" class="card flex-grow-1"> + <div class="card-header"> + <p class="h5 font-weight-bold mt-2">{{obj.name}}</p> + </div> + <div class="card-body"> + <p class="grid-item-description">{{obj.description}}</p> + </div> + <div class="card-footer"> + <button type="button" class="btn btn-success grid-item-select-btn w-100 stretched-link" + onclick="multi_filter_widget.processClick('{{obj.id}}');"> + {% if obj.multiple %} + Add + {% else %} + Select + {% endif %} + </button> + </div> + </div> + </div> + {% endfor %} + </div> </div> - {% endfor %} </div> - </div> -{% endfor %} + {% endfor %} </div> -<div id="dropdown_wrapper"> +<div id="dropdown_row" class="row"> + <div id="dropdown_wrapper" class="col-12 col-lg-6 d-flex flex-column pt-2 mx-0 px-1"> + </div> </div> <script> function multipleSelectFilterWidgetEntry() { @@ -141,8 +45,9 @@ function multipleSelectFilterWidgetEntry() { const filter_items = {{ filter_items|safe }}; const initial_value = {{ initial_value|default_if_none:"{}"|safe }}; - //global variable + //global variables multi_filter_widget = new MultipleSelectFilterWidget(graph_neighbors, filter_items, initial_value); + form_submission_callbacks.push(() => multi_filter_widget.finish()); } multipleSelectFilterWidgetEntry(); diff --git a/src/templates/dashboard/searchable_select_multiple.html b/src/templates/dashboard/searchable_select_multiple.html index 8bcf890..be51989 100644 --- a/src/templates/dashboard/searchable_select_multiple.html +++ b/src/templates/dashboard/searchable_select_multiple.html @@ -1,19 +1,16 @@ -<script src="https://code.jquery.com/jquery-2.2.4.min.js"></script> -<script src="/static/js/dashboard.js"></script> - -<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_counter"> - <p id="added_number">0</p> - <p id="addable_limit">/ {% if selectable_limit > -1 %} {{ selectable_limit }} {% else %} ∞ {% endif %}added</p> +<div id="search_select_outer" class="d-flex flex-column"> + {% if incompatible == "true" %} + <div class="alert alert-danger" role="alert"> + <h3>Warning: Incompatible Configuration</h3> + <p>Please make a different selection, as the current config conflicts with the selected pod</p> + </div> + {% endif %} + <div id="added_counter" class="text-center"> + <span id="added_number">0</span> + <span id="addable_limit">/ {% if selectable_limit > -1 %} {{ selectable_limit }} {% else %} ∞ {% endif %}added</span> </div> - <div id="added_list"> + <div id="added_list" class="pb-2"> </div> @@ -22,143 +19,14 @@ > </input> - <input type="hidden" id="selector" name="{{ name }}" class="form-control" style="display: none;" + <input type="hidden" id="selector" name="{{ name }}" class="form-control d-none" {% if disabled %} disabled {% endif %} > </input> - <div id="scroll_restrictor"> - <ul id="drop_results"></ul> + <div id="scroll_restrictor" class="d-flex pb-4 position-relative"> + <div id="drop_results" class="list-group w-100 z-2 overflow-auto position-absolute mh-30vh"></div> </div> - <style> - #scroll_restrictor { - flex: 1; - position: relative; - overflow-y: auto; - padding-bottom: 10px; - } - - #added_list { - margin-bottom: 5px; - } - - .autocomplete { - display: flex; - flex: 1; - flex-direction: column; - } - #user_field { - font-size: 14pt; - padding: 5px; - height: 40px; - border: 1px solid #ccc; - border-radius: 5px; - - } - - #drop_results{ - list-style-type: none; - padding: 0; - margin: 0; - min-height: 0; - border: solid 1px #ddd; - border-top: none; - border-bottom: none; - visibility: inherit; - flex: 1; - - position: absolute; - width: 100%; - - } - - #drop_results li a{ - font-size: 14pt; - background-color: #f6f6f6; - padding: 7px; - text-decoration: none; - display: block; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } - - #drop_results li a { - border-bottom: 1px solid #ddd; - } - - .list_entry { - border: 1px solid #ccc; - border-radius: 5px; - margin-top: 5px; - vertical-align: middle; - line-height: 40px; - height: 40px; - padding-left: 12px; - width: 100%; - display: flex; - } - - #drop_results li a:hover{ - background-color: #ffffff; - } - - .added_entry_text { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - display: inline; - width: 100%; - } - - .btn-remove { - float: right; - height: 30px; - margin: 4px; - padding: 1px; - max-width: 20%; - width: 15%; - min-width: 70px; - overflow: hidden; - text-overflow: ellipsis; - } - - .entry_tooltip { - display: none; - } - - #drop_results li a:hover .entry_tooltip { - position: absolute; - background: #444; - color: #ddd; - text-align: center; - font-size: 12pt; - border-radius: 3px; - - } - - #drop_results { - max-width: 100%; - display: inline-block; - list-style-type: none; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - } - - #drop_results li { - overflow: hidden; - text-overflow: ellipsis; - } - - #added_counter { - text-align: center; - } - - #added_number, #addable_limit { - display: inline; - } - </style> </div> <script type="text/javascript"> diff --git a/src/templates/dashboard/table.html b/src/templates/dashboard/table.html index 0a37ded..2b4628e 100644 --- a/src/templates/dashboard/table.html +++ b/src/templates/dashboard/table.html @@ -15,20 +15,14 @@ {% block content %} <div class="row"> <div class="col-lg-12"> - <div class="dataTables_wrapper"> - <table class="table table-striped table-bordered table-hover" id="table" cellspacing="0" - width="100%"> - - {% block table %} - {% endblock table %} - - </table> - </div> - <!-- /.table-responsive --> - <!-- /.panel-body --> - <!-- /.panel --> + <div class="dataTables_wrapper table-responsive mw-100"> + <table class="table table-striped table-bordered table-hover" id="table" cellspacing="0" + width="100%"> + {% block table %} + {% endblock table %} + </table> + </div> </div> - <!-- /.col-lg-12 --> </div> {% endblock content %} diff --git a/src/templates/layout.html b/src/templates/layout.html index d37d4f5..205671b 100644 --- a/src/templates/layout.html +++ b/src/templates/layout.html @@ -16,13 +16,12 @@ <link href="{% static "bower_components/bootstrap/dist/css/bootstrap.min.css" %}" rel="stylesheet"> - <!-- MetisMenu CSS --> - <link href="{% static "bower_components/metisMenu/dist/metisMenu.min.css" %}" rel="stylesheet"> - <!-- Custom Fonts --> <link href="{% static "bower_components/Font-Awesome/css/all.min.css" %}" rel="stylesheet" type="text/css"> + <link href="{% static "css/base.css" %}" rel="stylesheet"> + <!-- Favicon --> <link rel="shortcut icon" href="{% static 'favicon.ico' %}"> <script src="https://code.jquery.com/jquery-2.2.4.min.js" diff --git a/src/templates/notifier/inbox.html b/src/templates/notifier/inbox.html index 72207ed..26b6d32 100644 --- a/src/templates/notifier/inbox.html +++ b/src/templates/notifier/inbox.html @@ -5,61 +5,6 @@ {% 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; - } - - .half_width { - width: 50%; - } - - #page-wrapper { - padding: 0px; - } - - .read_notification { - background-color: #efefef; - } - - .scrollable { - overflow-y: auto; - } -</style> <div class="container-fluid d-flex flex-grow-1 flex-column"> <div class="row mt-3 mb-2"> <div class="col-2 px-0"> @@ -73,7 +18,8 @@ <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"> + <span class="text-muted d-none" id="noMessages">No messages available</span> + <div class="list-group rounded-0 rounded-left overflow-auto mh-100 notifications" id="unreadNotifications" data-read="0"> {% for notification in unread_notifications %} <a href="#" @@ -83,7 +29,7 @@ </a> {% endfor %} </div> - <div class="list-group rounded-0 rounded-left scrollable mh-100 notifications" id="readNotifications" data-read="1"> + <div class="list-group rounded-0 rounded-left overflow-auto mh-100 notifications" id="readNotifications" data-read="1"> {% for notification in read_notifications %} <a href="#" @@ -96,15 +42,14 @@ </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> + <iframe name="messageView" class="w-100 h-100" id="inbox-iframe" frameBorder="0" 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; + window.frames["messageView"].location = "notification/" + msg_id; } function setactive(obj) { @@ -112,18 +57,31 @@ $(obj).addClass("active"); } + // Shows messages in the given notification list. + // Shows/hides the 'no messages' span after checking children amount + // given the .notification classed element + function showMessages(notificationList) { + $(".notifications").addClass("d-none"); + if (notificationList.children().length < 1) { + $("#noMessages").removeClass("d-none"); + } else { + $("#noMessages").addClass("d-none"); + notificationList.removeClass("d-none"); + } + } + $(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"); + return showMessages($(".notifications")); } - $(`.notifications[data-read="${read}"]`).removeClass("d-none"); + return showMessages($(`.notifications[data-read="${read}"]`)); }); + showMessages($(".notifications")); }); </script> {% endblock %}
\ No newline at end of file diff --git a/src/templates/notifier/notification.html b/src/templates/notifier/notification.html index 0eafa60..603edea 100644 --- a/src/templates/notifier/notification.html +++ b/src/templates/notifier/notification.html @@ -27,13 +27,12 @@ } </script> -<div> - <h3 class="msg_header">{{notification.title}} - <div class="btn_group"> - <button class="btn btn-primary inbox-btn" onclick="mark_unread()">Mark Unread</button> - <button class="btn btn-danger inbox-btn" onclick="delete_notification()">Delete</button> - </div> - </h3> +<div class="d-flex justify-content-between border-bottom"> + <span class="h3">{{notification.title}}</span> + <div class="btn_group"> + <button class="btn btn-primary inbox-btn" onclick="mark_unread()">Mark Unread</button> + <button class="btn btn-danger inbox-btn" onclick="delete_notification()">Delete</button> + </div> </div> <p class="content-divider"></p> @@ -48,36 +47,4 @@ <form id="notification_action_form" action="." method="post"> {% csrf_token %} </form> - -<style media="screen"> - .card-container { - border: 1px solid #ffffff; - margin-top: 11px; - } - .card { - height: 50px; - margin: 0px; - position: relative; - border-bottom: 1px solid #cccccc; - padding: 0px; - width: 100%; - background-color: #ffffff; - z-index: 5; - } - .sender { - color: #636363; - } - .content-divider { - border-bottom: 1px solid #cccccc; - padding-bottom: 15px; - clear: right; - } - .inbox-btn{ - display: inline; - margin: 3px; - } - .btn_group{ - float: right; - } -</style> {% endblock %} diff --git a/src/templates/resource/hostprofile_detail.html b/src/templates/resource/hostprofile_detail.html index dc20600..c26d774 100644 --- a/src/templates/resource/hostprofile_detail.html +++ b/src/templates/resource/hostprofile_detail.html @@ -6,95 +6,105 @@ <div class="col-lg-6"> <div class="card mb-4"> <div class="card-header d-flex"> - <h4 style="display: inline;">Available at</h4> - <button data-toggle="collapse" data-target="#avilableAt" class="btn ml-auto btn-outline-secondary">Expand</button> + <h4 class="d-inline">Available at</h4> + <button data-toggle="collapse" data-target="#availableAt" class="btn ml-auto btn-outline-secondary">Expand</button> </div> - <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 class="collapse show" id="availableAt"> + <div class="card-body"> + <ul class="list-group"> + {% for lab in hostprofile.labs.all %} + <li class="list-group-item">{{lab.name}}</li> + {% endfor %} + </ul> + </div> </div> </div> <div class="card mb-4"> <div class="card-header d-flex"> - <h4 style="display: inline;">RAM</h4> + <h4 class="d-inline">RAM</h4> <button data-toggle="collapse" data-target="#ramPanel" class="btn ml-auto btn-outline-secondary">Expand</button> </div> - <div class="card-body collapse show" id="ramPanel"> - {{hostprofile.ramprofile.first.amount}}G, - {{hostprofile.ramprofile.first.channels}} channels + <div id="ramPanel" class="collapse show"> + <div class="card-body"> + {{hostprofile.ramprofile.first.amount}}G, + {{hostprofile.ramprofile.first.channels}} channels + </div> </div> </div> <div class="card mb-4"> <div class="card-header d-flex"> - <h4 style="display: inline;">CPU</h4> + <h4 class="d-inline">CPU</h4> <button data-toggle="collapse" data-target="#cpuPanel" class="btn ml-auto btn-outline-secondary">Expand</button> </div> - <div class="card-body collapse show" id="cpuPanel"> - <table class="table"> - <tr> - <td>Arch:</td> - <td>{{hostprofile.cpuprofile.first.architecture}}</td> - </tr> - <tr> - <td>Cores:</td> - <td>{{hostprofile.cpuprofile.first.cores}}</td> - </tr> - <tr> - <td>Sockets:</td> - <td>{{hostprofile.cpuprofile.first.cpus}}</td> - </tr> - </table> + <div class="collapse show" id="cpuPanel"> + <div class="card-body"> + <table class="table"> + <tr> + <td>Arch:</td> + <td>{{hostprofile.cpuprofile.first.architecture}}</td> + </tr> + <tr> + <td>Cores:</td> + <td>{{hostprofile.cpuprofile.first.cores}}</td> + </tr> + <tr> + <td>Sockets:</td> + <td>{{hostprofile.cpuprofile.first.cpus}}</td> + </tr> + </table> + </div> </div> </div> <div class="card mb-4"> <div class="card-header d-flex"> - <h4 style="display: inline;">Disk</h4> + <h4 class="d-inline">Disk</h4> <button data-toggle="collapse" data-target="#diskPanel" class="btn ml-auto btn-outline-secondary">Expand</button> </div> - <div class="card-body collapse show" id="diskPanel"> - <table class="table"> - <tr> - <td>Size:</td> - <td>{{hostprofile.storageprofile.first.size}} GiB</td> - </tr> - <tr> - <td>Type:</td> - <td>{{hostprofile.storageprofile.first.media_type}}</td> - </tr> - <tr> - <td>Mount Point:</td> - <td>{{hostprofile.storageprofile.first.name}}</td> - </tr> - </table> + <div class="collapse show" id="diskPanel"> + <div class="card-body"> + <table class="table"> + <tr> + <td>Size:</td> + <td>{{hostprofile.storageprofile.first.size}} GiB</td> + </tr> + <tr> + <td>Type:</td> + <td>{{hostprofile.storageprofile.first.media_type}}</td> + </tr> + <tr> + <td>Mount Point:</td> + <td>{{hostprofile.storageprofile.first.name}}</td> + </tr> + </table> + </div> </div> </div> </div> <div class="col-lg-6"> <div class="card"> <div class="card-header d-flex"> - <h4 style="display: inline;">Interfaces</h4> + <h4 class="d-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 class="collapse show" id="interfacePanel"> + <div class="card-body"> + <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> diff --git a/src/templates/resource/steps/define_hardware.html b/src/templates/resource/steps/define_hardware.html index 57078e9..efd8a09 100644 --- a/src/templates/resource/steps/define_hardware.html +++ b/src/templates/resource/steps/define_hardware.html @@ -7,18 +7,11 @@ <p>Note that not all labs host every kind of machine. As you make your selections, labs and hosts that are not compatible with your current configuration will become unavailable.</p> -<h4>NOTE: Only PTL's are able to create multi-node PODs. See <a href="https://google.com">here</a> +<h4>NOTE: Only PTL's are able to create multi-node PODs. See + <a href="https://wiki.opnfv.org/display/INF/Lab-as-a-Service+at+the+UNH-IOL">here</a> for more details</h4> -<form id="define_hardware_form" action="/wf/workflow/" method="post"> +<form id="step_form" method="post" class="px-3"> {% csrf_token %} {{form.filter_field|default:"<p>No Form</p>"}} </form> {% endblock content %} -{% block onleave %} -multi_filter_widget.finish(); -var formData = $("#define_hardware_form").serialize(); -req = new XMLHttpRequest(); -req.open('POST', '/wf/workflow/', false); -req.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); -req.send(formData); -{% endblock %} diff --git a/src/templates/resource/steps/host_info.html b/src/templates/resource/steps/host_info.html index bbbafdc..3230d8f 100644 --- a/src/templates/resource/steps/host_info.html +++ b/src/templates/resource/steps/host_info.html @@ -10,7 +10,7 @@ {% else %} -<form id="host_meta_form" method="post" action="/wf/workflow/"> +<form id="step_form" method="post"> {% csrf_token %} <table> <thead> @@ -32,12 +32,3 @@ </form> {% endif %} {% endblock content %} - -{% block onleave %} -var formData = $("#host_meta_form").serialize(); -var req = new XMLHttpRequest(); -req.open("POST", "/wf/workflow/", false); -req.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); -req.onerror = function() { alert("There was a problem submitting the form"); } -req.send(formData); -{% endblock %} diff --git a/src/templates/resource/steps/meta_info.html b/src/templates/resource/steps/meta_info.html index cebd343..b6a17a9 100644 --- a/src/templates/resource/steps/meta_info.html +++ b/src/templates/resource/steps/meta_info.html @@ -5,42 +5,10 @@ {% block content %} -<style> -#resource_meta_form { - padding: 80px; - display: grid; -} - -#resource_meta_form td > * { - width: 100%; - margin-bottom: 20px; - margin-top: 20px; -} - -#resource_meta_form > table > tbody > tr { - border-bottom: 1px solid #cccccc; -} - -#resource_meta_form > table > tbody > tr:last-child { - border-bottom: none; -} - -</style> - -<form id="resource_meta_form" method="post" action="/wf/workflow/"> +<form id="step_form" method="post"> {% csrf_token %} - <table> - {{form}} + <table class="px-4"> + {% bootstrap_form form field_class="px-4" label_class="px-4 mt-2" %} </table> </form> {% endblock content %} - -{% block onleave %} -var ajaxForm = $("#resource_meta_form"); -var formData = ajaxForm.serialize(); -req = new XMLHttpRequest(); -req.open("POST", "/wf/workflow/", false); -req.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); -req.onerror = function() { alert("problem submitting form"); } -req.send(formData); -{% endblock %} diff --git a/src/templates/resource/steps/pod_definition.html b/src/templates/resource/steps/pod_definition.html index 5826ccb..d0a28ed 100644 --- a/src/templates/resource/steps/pod_definition.html +++ b/src/templates/resource/steps/pod_definition.html @@ -7,133 +7,75 @@ <script> var mxLoadStylesheets = false; </script> -<script type="text/javascript" src="/static/js/mxClient.min.js" ></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. --> {% block content %} - <div id="graphParent" - style="position:absolute;overflow:hidden;top:0px;bottom:0px;width:75%;left:0px;"> - <div id="graphContainer" - style="position:relative;overflow:hidden;top:36px;bottom:0px;left:0px;right:0px;background-image:url('/static/img/mxgraph/grid.gif');cursor:default;"> + <div class="row p-0 w-100 mx-0 position-absolute overflow-hidden topToBottom"> + <div id="graphParent" class="col px-0"> + <div class="row"> + <div class="col pr-0"> + <div id="toolbarContainer" class="bg-light pl-4"></div> + </div> + </div> + <!-- Creates a container for the sidebar --> + <div id="graphContainer"></div> </div> - - <!-- Creates a container for the sidebar --> - <div id="toolbarContainer" - style="position:absolute;white-space:nowrap;overflow:hidden;top:0px;left:0px;right:0px;padding:6px;"> - </div> - - <!-- Creates a container for the outline --> - <div id="outlineContainer" - style="position:absolute;overflow:hidden;top:36px;right:0px;width:200px;height:140px;background:transparent;border-style:solid;border-color:black;"> + <div id="network_select" class="p-0 w-25 ml-auto col-2"> + <div class="px-0 mb-2"> + <!-- Creates a container for the outline --> + <div id="outlineContainer" class="border"></div> + </div> + <div> + <button id="btn_add_network" type="button" class="btn btn-primary w-100" onclick="network_step.newNetworkWindow();">Add Network</button> + </div> + <ul id="network_list" class="list-group"> + </ul> + <button type="button" class="d-none" onclick="network_step.submitForm();">Submit</button> </div> </div> - - <style> - p { - word-break: normal; - white-space: normal; - } - #network_select { - background: inherit; - padding: 0px; - padding-top: 0px; - } - #toolbarContainer { - background: #DDDDDD; - height: 36px; - } - #toolbar_extension { - height: 36px; - background: #DDDDDD; - } - #btn_add_network { - width: 100%; - } - #vlan_notice { - margin: 20px; - } - #network_list li { - border-radius: 2px; - margin: 5px; - padding: 5px; - vertical-align: middle; - background: #DDDDDD; - } - #network_list { - list-style-type: none; - padding: 0; - } - .colorblob { - width: 20px; - height: 20px; - border-radius: 50%; - display: inline-block; - vertical-align: middle; - } - .network_innertext { - display: inline-block; - padding-left: 10px; - vertical-align: middle; - padding-bottom: 0px; - margin-bottom: 2px; - } - .mxWindow { - background: #FFFFFF; - } - </style> - - <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="network_step.newNetworkWindow();">Add Network</button> - </div> - <ul id="network_list"> - </ul> - <button type="button" style="display: none" onclick="network_step.submitForm();">Submit</button> - </div> - <form id="xml_form" method="post" action="/wf/workflow/"> + <form id="step_form" method="post"> {% csrf_token %} <input type="hidden" id="hidden_xml_input" name="xml" /> </form> + <script> + //gather context data + let debug = false; + {% if debug %} + debug = true; + {% endif %} -<script> - //gather context data - let debug = false; - {% if debug %} - debug = true; - {% endif %} + let xml = ''; + {% if xml %} + xml = '{{xml|safe}}'; + {% endif %} - let xml = ''; - {% if xml %} - xml = '{{xml|safe}}'; - {% endif %} + let hosts = []; + {% for host in hosts %} + hosts.push({{host|safe}}); + {% endfor %} - 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 added_hosts = []; + {% for host in added_hosts %} + added_hosts.push({{host|safe}}); + {% endfor %} - let removed_host_ids = {{removed_hosts|safe}}; + 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> + network_step = new NetworkStep( + debug, + xml, + hosts, + added_hosts, + removed_host_ids, + document.getElementById('graphContainer'), + document.getElementById('outlineContainer'), + document.getElementById('toolbarContainer'), + document.getElementById('sidebarContainer') + ); + form_submission_callbacks.push(() => network_step.prepareForm()); + </script> {% endblock content %} {% block onleave %} network_step.submitForm(); diff --git a/src/templates/snapshot_workflow/steps/meta.html b/src/templates/snapshot_workflow/steps/meta.html index bea475d..88136d2 100644 --- a/src/templates/snapshot_workflow/steps/meta.html +++ b/src/templates/snapshot_workflow/steps/meta.html @@ -4,14 +4,9 @@ {% load bootstrap4 %} {% block content %} -<style> -.meta_container { - padding: 50px; -} -</style> {% bootstrap_form_errors form type='non_fields' %} -<div class="meta_container"> - <form id="meta_form" action="/wf/workflow/" method="POST" class="form"> +<div class="p-4"> + <form id="step_form" method="POST" class="form"> {% csrf_token %} <div class="form-group"> {% bootstrap_field form.name %} @@ -20,13 +15,3 @@ </form> </div> {% endblock content %} - -{% block onleave %} -var ajaxForm = $("#meta_form"); -var formData = ajaxForm.serialize(); -req = new XMLHttpRequest(); -req.open("POST", "/wf/workflow/", false); -req.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); -req.onerror = function() { alert("problem submitting form"); } -req.send(formData); -{% endblock %} diff --git a/src/templates/snapshot_workflow/steps/select_host.html b/src/templates/snapshot_workflow/steps/select_host.html index f438bac..4243145 100644 --- a/src/templates/snapshot_workflow/steps/select_host.html +++ b/src/templates/snapshot_workflow/steps/select_host.html @@ -5,73 +5,30 @@ {% block content %} -<style> - .booking { - border-style: none; - border-color: black; - border: 2px; - border-radius: 5px; - margin: 20px; - padding-left: 25px; - padding-right: 25px; - padding-bottom: 25px; - box-shadow: 0px 0px 7px 0px rgba(0,0,0,0.75); - transition-property: box-shadow; - transition-duration: 0.1s; - float: left; - } - .booking:hover { - box-shadow: 0px 0px 10px 0px rgba(0,0,0,0.75); - transition-property: box-shadow; - transition-duration: 0.1s; - } - .host { - cursor: pointer; - border-style: solid; - border-color: black; - border-width: 1px; - border-radius: 5px; - margin: 5px; - padding: 5px; - text-align: center; - box-shadow: 0px 0px 2px 0px rgba(0,0,0,0.75); - transition-property: box-shadow; - transition-duration: 0.1s; - } - .host:hover { - box-shadow: 0px 0px 4px 0px rgba(0,0,0,0.75); - transition-property: box-shadow; - transition-duration: 0.1s; - background-color: rgba(144,238,144,0.3); - } - .selected { - background-color: lightgreen !important; - } - .booking_container { - overflow: auto; - padding: 30px; - } -</style> {% bootstrap_form_errors form type='non_fields' %} -<form id="host_select_form" action="/wf/workflow/" method="POST" class="form"> +<form id="step_form" method="POST" class="form"> {% csrf_token %} <input type="hidden" id="hidden_json_input", name="host"/> </form> -<div id="host_select_container" class="booking_container"> +<div class="container-fluid"> + <div class="row" id="host_select_container"> + </div> </div> <script> var selected_host = null; var initial = {{chosen|safe|default:'null'}}; -function select(booking_id, host_name){ +function select(obj){ + var booking_id = $(obj).attr("booking"); + var host_name = $(obj).attr("hostname"); var input = document.getElementById("hidden_json_input"); input.value = JSON.stringify({"booking": booking_id, "name": host_name}); // clear out and highlist host - if(selected_host){ - selected_host.classList.remove("selected"); + if(selected_host != null){ + selected_host.classList.remove("active"); } selected_host = document.getElementById("booking_" + booking_id + "_host_" + host_name); - selected_host.classList.add("selected"); + selected_host.classList.add("active"); } function draw_bookings(){ @@ -79,35 +36,43 @@ function draw_bookings(){ var bookings = []; var container = document.getElementById("host_select_container"); for(var booking_id in booking_hosts){ - var booking = document.createElement("DIV"); - var heading = document.createElement("H3"); - heading.appendChild(document.createTextNode("Booking " + booking_id)); - booking.appendChild(heading); - booking.appendChild(document.createTextNode("start: " + booking_hosts[booking_id].start)); - booking.appendChild(document.createElement("BR")); - booking.appendChild(document.createTextNode("end: " + booking_hosts[booking_id].end)); - booking.appendChild(document.createElement("BR")); - booking.appendChild(document.createTextNode("purpose: " + booking_hosts[booking_id].purpose)); - booking.appendChild(document.createElement("BR")); - booking.appendChild(document.createTextNode("hosts:")); - booking.id = "booking_" + booking_id; - booking.className = "booking"; + // Create a column with a card + var column = $("<div/>", { + class: "col-12 col-md-6 col-lg-3 col-xl-2 my-2" + }).appendTo(container); + var booking = $("<div/>", { + class: "card" + }).appendTo(column); + var heading = $("<div/>", { + class: "card-header" + }).text(`Booking ${booking_id}`).appendTo(booking); + var body = $("<ul/>", { + class: "list-group list-group-flush" + }).appendTo(booking); + var footer = $("<div/>", { + text: "Hosts:", + class: "card-footer d-flex flex-column" + }).appendTo(booking); + + // Append information to the card body + $(`<li class="list-group-item">Start: ${booking_hosts[booking_id].start}</li>`).appendTo(body); + $(`<li class="list-group-item">End: ${booking_hosts[booking_id].end}</li>`).appendTo(body); + $(`<li class="list-group-item">Purpose: ${booking_hosts[booking_id].purpose}</li>`).appendTo(body); + + // Append hosts to footer var hosts = booking_hosts[booking_id].hosts; - for(var i=0; i<hosts.length; i++){ - var host = document.createElement("DIV"); - host.id = "booking_" + booking_id + "_host_" + hosts[i].name; - host.classList.add("host"); - host.appendChild(document.createTextNode(hosts[i].name)); - var hostname = hosts[i].name; - host.booking = booking_id; - host.hostname = hostname; - host.onclick = function() { - select(this.booking, this.hostname); - } - booking.appendChild(host); + for (const host of hosts) { + $("<button/>", { + class: "btn btn-outline-primary w-100 mt-1 hostbtn", + id: `booking_${booking_id}_host_${host.name}`, + text: host.name, + booking: booking_id, + hostname: host.name, + click: function() { + select(this); + } + }).appendTo(footer); } - bookings.push(booking); - container.appendChild(booking); } } draw_bookings(); @@ -116,13 +81,3 @@ if(initial){ } </script> {% endblock content %} - -{% block onleave %} -var ajaxForm = $("#host_select_form"); -var formData = ajaxForm.serialize(); -req = new XMLHttpRequest(); -req.open("POST", "/wf/workflow/", false); -req.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); -req.onerror = function() { alert("problem submitting form"); } -req.send(formData); -{% endblock %} diff --git a/src/templates/workflow/confirm.html b/src/templates/workflow/confirm.html index c1f3440..2f99a41 100644 --- a/src/templates/workflow/confirm.html +++ b/src/templates/workflow/confirm.html @@ -5,159 +5,58 @@ {% block content %} -<style> - #form_div { - width: 100%; - padding: 5%; - text-align: center; - } - #text_display { - text-align: left; - display: inline-block; - } - #text_wrapper { - text-align: center; /*centers child div*/ - } - p { - margin:0; - padding:0; - } -</style> - -<div style="text-align:center;"> +<div class="text-center"> <h3>Confirm Session</h3> </div> -<div id="vlan_warning"></div> -<form id="vlan_form" action="/wf/workflow/" method="post"> - {% csrf_token %} - <input id="vlan_input" name="vlan_input" type="hidden"/> -</form> -<div id="text_wrapper"> - <div id="text_display"> - <pre>{{confirmation_info|escape}}</pre> +<div class="container"> + <div class="row justify-content-center"> + <div class="col-auto"> + <pre>{{confirmation_info|escape}}</pre> + </div> </div> -</div> -<div id="form_div"> -<form id="confirmation_form" action="/wf/workflow/" method="post"> - {% csrf_token %} - <div style="display: none;"> - {{form|default:"<p> No Form Loaded</p>"}} + <div class="row"> + <div class="col"> + <div id="form_div" class="text-center p-4"> + <form id="step_form" action="/workflow/manager/" method="post"> + {% csrf_token %} + <div class="d-none"> + {{form|default:"<p> No Form Loaded</p>"}} + </div> + </form> + <div class="cform_buttons mx-auto"> + <button id="confirm_button" class="btn btn-success" onclick="formconfirm()">Confirm</button> + <button id="cancel_button" class="btn btn-danger" onclick="formcancel()">Cancel</button> + </div> + <div class="d-none"> + <form id="manager_delete_form" action="/workflow/finish/" method="post"> + {% csrf_token %} + </form> + </div> + </div> + </div> </div> -</form> -<div class="cform_buttons"> - <button id="confirm_button" class="btn btn-success" onclick="formconfirm()">Confirm</button> - <button id="cancel_button" class="btn btn-danger" onclick="formcancel()">Cancel</button> </div> - -<div style="display: none;"> -<form id="manager_delete_form" action="/wf/workflow/finish/" method="post"> - {% csrf_token %} -</form> -</div> - <script> var select = document.getElementById("id_confirm"); - function processResponseText(json) - { - var dict = JSON.parse(json); - - if( !dict["redir_url"] ) { - window.top.refresh_iframe(); - } else { - top.window.location.href = dict["redir_url"]; - } - } - - function delete_manager() - { - var form = $("#manager_delete_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 with cleaning up session"); } - req.onreadystatechange = function() { if(req.readyState === 4 ) { - processResponseText(req.responseText); - }} - req.send(formData); - } - - function submitForm() - { - var form = $("#confirmation_form"); - var formData = form.serialize(); - var req = new XMLHttpRequest(); - req.open("POST", "/wf/workflow/", false); - req.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); - req.onerror = function() { alert("problem submitting confirmation"); } - req.onreadystatechange = function() { if(req.readyState === 4 ) { delete_manager(); } } - req.send(formData); - } - - function formconfirm() { select.value = "True"; - submitForm(); + submitStepForm(); } function formcancel() { select.value = "False"; - submitForm(); + submitStepForm(); } - var confirmed = {{bypassed|default:"false"}}; + var confirmed = {{confirm_succeeded|default:"false"}}; if( confirmed ) { - delete_manager(); + pop_workflow(); } </script> -<script> - -function fixVlans() { - document.getElementById("vlan_input").value = "True"; - var form = $("#vlan_form"); - var formData = form.serialize(); - var req = new XMLHttpRequest(); - req.open("POST", "/wf/workflow/", false); - req.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); - req.onerror = function() { alert("problem submitting form"); } - req.onreadystatechange = function() { //replaces current page with response - if(req.readyState === 4 ) { - var d = document.getElementById("vlan_warning").innerHTML = ""; - document.getElementById("confirm_button").disabled = false; - document.getElementById("cancel_button").disabled = false; - } - } - req.send(formData); -} -var problem = {{vlan_warning|default:'false'}}; -if(problem){ - var d = document.getElementById("vlan_warning"); - var h3 = document.createElement("h3"); - h3.innerHTML = "WARNING: Vlans not available"; - var h4 = document.createElement("h4"); - h4.innerHTML = "The vlans you selected are not currently available. Would you like to automatically change them?"; - var button1 = document.createElement("button"); - button1.innerHTML = "Correct Vlans For Me"; - button1.onclick = function() { fixVlans(); } - - var button2 = document.createElement("button"); - button2.innerHTML = "Cancel. I will change my vlans"; - button2.onclick = function() { formcancel(); } - d.appendChild(h3); - d.appendChild(h4); - d.appendChild(button1); - d.appendChild(button2); - document.getElementById("confirm_button").disabled = true; - document.getElementById("cancel_button").disabled = true; -} -</script> -</div> {% block element_messages %} {% endblock element_messages %} {% endblock content %} -{% block onleave %} -{% endblock %} diff --git a/src/templates/workflow/resource_select.html b/src/templates/workflow/resource_select.html deleted file mode 100644 index cd04137..0000000 --- a/src/templates/workflow/resource_select.html +++ /dev/null @@ -1,50 +0,0 @@ -{% extends "workflow/viewport-element.html" %} -{% load staticfiles %} - -{% load bootstrap4 %} - -{% block content %} - -<p>resource selection template</p> - -<style> - .db_pane_wrapper{ - display: grid; - grid-template-columns: 49% 2% 49%; - } - .divider{ - border-style: solid; - height: 100vh; - } - - .hidden_form{ - display: none; - } -</style> - -<div class="db_pane_wrapper"> - <div class="pane_one"> - <p>Select Resource</p> - - </div> - <div class="divider"> - - </div> - <div class="pane_two"> - <button>Create New Resource</button> - </div> -</div> - -<div class="hidden_form" id="form_div"> - <form method="post" action="" class="form" id="resource_selection_form"> - {% csrf_token %} - {% bootstrap_field form.resourcebundle %} - {% buttons %} - <button type="submit" class="btn btn btn-success"> - Confirm Edit - </button> - {% endbuttons %} - </form> -</div> - -{% endblock content %}
\ No newline at end of file diff --git a/src/templates/workflow/viewport-base.html b/src/templates/workflow/viewport-base.html index aa01d7e..4ae2ff3 100644 --- a/src/templates/workflow/viewport-base.html +++ b/src/templates/workflow/viewport-base.html @@ -5,195 +5,23 @@ {% block content %} -<style> - .go_btn { - - position: absolute; - width: 100px; - top: 170px; - height: calc(100% - 170px); - - } - - .go_btn_disabled { - background-color: #ffffff; - } - - .go_forward { - right: 0px; - border-left: none; - } - - .go_back { - left: 251px; - border-right: none; - } - - - .btn_wrapper { - text-align: center; - margin-bottom: 5px; - - } - - {% if DEBUG %} - .add_btn_wrapper { - right: 130px; - top: 10px; - position: absolute; - } - {% endif %} - - #breadcrumbs { - margin-bottom: 0; - } - - .btn_wrapper { - margin: 0; - } - - .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 { - display: inline; - padding: 7px; - margin: 1px; - cursor: default; - font-size: 14pt; - padding-bottom: 4px !important; - border-bottom: 4px solid #41ba78 !important; - } - - .step_hidden { - background: #EFEFEF; - color: #999999; - } - - .step_invalid::after { - content: " \2612"; - color: #CC3300; - } - - .step_valid::after { - content: " \2611"; - color: #41ba78; - } - - .step_untouched::after { - content: " \2610"; - } - - .iframe_div { - width: calc(100% - 450px); - margin-left: 70px; - height: calc(100vh - 155px); - position: absolute; - border: none; - } - - .iframe_elem { - width: 100%; - height: calc(100vh - 155px); - border: none; - } - - #breadcrumbs { - background-color: inherit; - } - - #breadcrumbs.breadcrumb>li { - border: 1px solid #cccccc; - border-left: none; - } - - #breadcrumbs.breadcrumb>li:first-child { - border-left: 1px solid #cccccc; - } - - #breadcrumbs.breadcrumb>li+li:before { - content: ""; - width: 0; - margin: 0; - padding: 0; - } - - #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; - } - - .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')"> + <a class="page-link" href="#" id="gob" onclick="submit_and_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> + <i class="far"></i> </a> </li> <li class="page-item flex-shrink-1 page-control"> - <a class="page-link text-right" href="#" id="gof" onclick="go('next')"> + <a class="page-link text-right" href="#" id="gof" onclick="submit_and_go('next')"> Next <i class="fas fa-forward"></i> </a> </li> @@ -205,282 +33,46 @@ <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> + <div class="col-lg-12"> + <h1 class="d-inline-block" id="view_title"></h1> <span class="description text-muted" id="view_desc"></span> - <p class="step_message" id="view_message"></p> + <p 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> + <button id="cancel_btn" class="btn btn-danger ml-auto" onclick="pop_workflow()">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 class="col-12 d-flex flex-grow-1 px-0"> + <div id="formContainer" class="h-100 w-100"></div> </div> </div> </div> </div> -<div class="btn_wrapper"> -</div> {% csrf_token %} - <script type="text/javascript"> - update_context(); - var step = 0; - var page_count = 0; - var context_data = false; - - function go(to) { - step_on_leave(); - request_leave(to); - } - - function request_leave(to) { - $.ajax({ - type: "GET", - url: "/wf/manager/", - beforeSend: function (request) { - request.setRequestHeader("X-CSRFToken", - $('input[name="csrfmiddlewaretoken"]').val()); - }, - success: function (data) { - confirm_permission(to, data); - update_page(data); - } - }); - } - - function confirm_permission(to, data) { - if (errors_exist(data)) { - if (to != "prev") { - return; - } - } - - 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) { - document.getElementById("viewport-iframe").srcdoc = this.responseText; - } else { - problem(); - } - } else { - problem(); - } - } - req.onerror = problem; - req.send(); - } - - function step_on_leave() { - document.getElementById("viewport-iframe").contentWindow.step_on_leave(); - } - - function errors_exist(data) { - var stat = data['steps'][data['active']]['valid']; - if (stat >= 100 && stat < 200) { - return true; - } else { - return false; - } + function submit_and_go(to) { + submitStepForm(to); } - function update_context() { + $(document).ready(function(){ $.ajax({ - type: "GET", - url: "/wf/manager/", - beforeSend: function (request) { - request.setRequestHeader("X-CSRFToken", - $('input[name="csrfmiddlewaretoken"]').val()); - }, - success: function (data) { - update_page(data); - } + url: "/workflow/manager/", + dataType: "json", + success: update_page }); - } - - 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"; - } - } - - 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("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("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 - $("#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++) { - meta_json["steps"][i]["index"] = i; - var step_btn = create_step(meta_json["steps"][i], i == meta_json["active"]); - $("#topPagination li:last-child").before(step_btn); - } - } - - function create_step(step_json, active) { - var step_dom = document.createElement("li"); - // First create the dom object depending on active or not - if (active) { - step_dom.className = "topcrumb active"; - } else { - step_dom.className = "topcrumb"; - } - $(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).children().first().append("<i class='ml-2 far fa-square'></i>") - stat = ""; - msg = ""; - } else if (code < 200) { - $(step_dom).children().first().append("<i class='ml-2 fas fa-minus-square'></i>") - stat = "invalid"; - msg = step_json['message']; - } else if (code < 300) { - $(step_dom).children().first().append("<i class='ml-2 far fa-check-square'></i>") - stat = "valid"; - msg = step_json['message']; - } - if (step_json['enabled'] == false) { - step_dom.classList.add("disabled"); - } - if (active) { - update_message(msg, stat); - } - - var step_number = step_json['index']; - return step_dom; - } - - 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.send(formData); - } - - function refresh_iframe() { - req = new XMLHttpRequest(); - url = "/wf/workflow/"; - req.open("GET", url, true); - req.onload = function (e) { - var doc = document.getElementById("viewport-iframe").contentWindow.document; - doc.open(); - doc.write(this.responseText); - doc.close(); - } - req.send(); - } - - function write_iframe(contents) { - document.getElementById("viewport-iframe").contentWindow.document.innerHTML = contents; - } - - function redirect_root() { - window.location.replace('/wf/'); - } - - function add_wf(type) { - add_wf_internal(type, false); - } - - function add_edit_wf(type, target) { - add_wf_internal(type, target); - } - - 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) { - request.setRequestHeader("X-CSRFToken", - $('input[name="csrfmiddlewaretoken"]').val() - ); - }, - success: refresh_wf_iframe() - }); - } - - function refresh_wf_iframe() { - window.location = window.location; - } + }); </script> -<div style="display: none;" id="workflow_pop_form_div"> - <form id="workflow_pop_form" action="/wf/workflow/finish/" method="post"> +<!-- lazy load scripts --> +<script type="text/javascript" src="/static/js/mxClient.min.js" ></script> +<!-- end lazy load scripts --> +<div class="d-none" id="workflow_pop_form_div"> + <form id="workflow_pop_form" action="/workflow/finish/" method="post"> {% csrf_token %} </form> </div> -{% endblock content %}
\ No newline at end of file +{% endblock content %} diff --git a/src/templates/workflow/viewport-element.html b/src/templates/workflow/viewport-element.html index 7a7165a..d16c924 100644 --- a/src/templates/workflow/viewport-element.html +++ b/src/templates/workflow/viewport-element.html @@ -1,69 +1,14 @@ -{% extends "layout.html" %} {% load bootstrap4 %} {% load staticfiles %} {% block basecontent %} - <div id="wrapper"> - <!-- Page Content --> - <div id="page-wrapper"> - - {% block content %} - - {% endblock content %} - </div> - <!-- /#page-wrapper --> - </div> - {% block vport_comm %} - <script type="text/javascript"> - var step_count = {{ step_number|default:0 }}; - var active_step = {{ active_step|default:0 }}; - var render_correct = {{ render_correct|default:"false" }}; - var title = "{{ step_title|default:"Workflow Step" }}"; - var description = "{{ description|default:"Contact the admins, because this field should have something else filled in here" }}"; - if(render_correct){ - parent.update_context(); - } - parent.update_description(title, description); - </script> - - {% endblock vport_comm %} - {% block validate_step %} - <script> - - function step_is_valid() - { - valid = confirm("Is this form valid?"); - if( valid ) - { - return true; - } - else{ - return false; - } - } - - function onError() - { - alert("Error: something!"); - } - </script> - - {% endblock validate_step %} - - <script> - function step_on_leave() { - {% block onleave %} - alert("override onleave"); - {% endblock %} - } - </script> + {% block content %} + {% endblock content %} <div class="messages"> {% block element_messages %} {% bootstrap_messages %} {% endblock %} </div> - - <!-- /#wrapper --> {% endblock basecontent %} diff --git a/src/workflow/booking_workflow.py b/src/workflow/booking_workflow.py index 42372ce..3698164 100644 --- a/src/workflow/booking_workflow.py +++ b/src/workflow/booking_workflow.py @@ -7,7 +7,6 @@ # http://www.apache.org/licenses/LICENSE-2.0 ############################################################################## -from django.contrib import messages from django.utils import timezone from datetime import timedelta @@ -171,8 +170,8 @@ class Booking_Meta(WorkflowStep): context['form'] = BookingMetaForm(initial=initial, user_initial=default, owner=owner) return context - def post_render(self, request): - form = BookingMetaForm(data=request.POST, owner=request.user) + def post(self, post_data, user): + form = BookingMetaForm(data=post_data, owner=user) forms = self.repo_get(self.repo.BOOKING_FORMS, {}) @@ -212,9 +211,6 @@ class Booking_Meta(WorkflowStep): self.repo_put(self.repo.BOOKING_MODELS, models) self.repo_put(self.repo.CONFIRMATION, confirm) - messages.add_message(request, messages.SUCCESS, 'Form Validated', fail_silently=True) self.set_valid("Step Completed") else: - messages.add_message(request, messages.ERROR, "Form didn't validate", fail_silently=True) self.set_invalid("Please complete the fields highlighted in red to continue") - return self.render(request) diff --git a/src/workflow/forms.py b/src/workflow/forms.py index ee44ecd..4d5e9e2 100644 --- a/src/workflow/forms.py +++ b/src/workflow/forms.py @@ -15,6 +15,7 @@ from django.template.loader import render_to_string from django.forms.widgets import NumberInput import json +import urllib from account.models import Lab from account.models import UserProfile @@ -120,7 +121,12 @@ class SearchableSelectMultipleField(forms.Field): raise ValidationError("Nothing was selected") else: return [] - data_as_list = json.loads(data) + try: + data_as_list = json.loads(data) + except json.decoder.JSONDecodeError: + data_as_list = None + if not data_as_list: + raise ValidationError("Contents Not JSON") if self.selectable_limit != -1: if len(data_as_list) > self.selectable_limit: raise ValidationError("Too many items were selected") @@ -270,7 +276,11 @@ class MultipleSelectFilterField(forms.Field): super().__init__(**kwargs) def to_python(self, value): - return json.loads(value) + try: + return json.loads(value) + except json.decoder.JSONDecodeError: + pass + raise ValidationError("content is not valid JSON") class FormUtils: @@ -428,6 +438,24 @@ class ConfirmationForm(forms.Form): ) +def validate_step(value): + if value not in ["prev", "next", "current"]: + raise ValidationError(str(value) + " is not allowed") + + +def validate_step_form(value): + try: + urllib.parse.unquote_plus(value) + except Exception: + raise ValidationError("Value is not url encoded data") + + +class ManagerForm(forms.Form): + step = forms.CharField(widget=forms.widgets.HiddenInput, validators=[validate_step]) + step_form = forms.CharField(widget=forms.widgets.HiddenInput, validators=[validate_step_form]) + # other fields? + + class OPNFVSelectionForm(forms.Form): installer = forms.ModelChoiceField(queryset=Installer.objects.all(), required=True) scenario = forms.ModelChoiceField(queryset=Scenario.objects.all(), required=True) diff --git a/src/workflow/models.py b/src/workflow/models.py index 6c6bd9a..9d1fac2 100644 --- a/src/workflow/models.py +++ b/src/workflow/models.py @@ -8,8 +8,7 @@ ############################################################################## -from django.shortcuts import render -from django.contrib import messages +from django.template.loader import get_template from django.http import HttpResponse from django.utils import timezone @@ -219,16 +218,14 @@ class WorkflowStep(object): return context def render(self, request): - self.context = self.get_context() - return render(request, self.template, self.context) + return HttpResponse(self.render_to_string(request)) - def post_render(self, request): - return self.render(request) + def render_to_string(self, request): + template = get_template(self.template) + return template.render(self.get_context(), request) - def test_render(self, request): - if request.method == "POST": - return self.post_render(request) - return self.render(request) + def post(self, post_content, user): + raise Exception("WorkflowStep subclass of type " + str(type(self)) + " has no concrete post() method") def validate(self, request): pass @@ -263,22 +260,18 @@ class AbstractSelectOrCreate(WorkflowStep): def alert_bundle_missing(self): # override in subclasses to change message if field isn't filled out self.set_invalid("Please select a valid bundle") - def post_render(self, request): - context = self.get_context() - form = self.form(request.POST, queryset=self.get_form_queryset()) + def post(self, post_data, user): + form = self.form(post_data, queryset=self.get_form_queryset()) if form.is_valid(): bundle = form.get_validated_bundle() if not bundle: self.alert_bundle_missing() - return render(request, self.template, context) + return self.repo_put(self.select_repo_key, bundle) self.put_confirm_info(bundle) self.set_valid("Step Completed") else: self.alert_bundle_missing() - messages.add_message(request, messages.ERROR, "Form Didn't Validate", fail_silently=True) - - return self.render(request) def get_context(self): default = [] @@ -316,6 +309,9 @@ class Confirmation_Step(WorkflowStep): default_flow_style=False ).strip() + if self.valid == WorkflowStepStatus.VALID: + context["confirm_succeeded"] = "true" + return context def flush_to_db(self): @@ -323,29 +319,24 @@ class Confirmation_Step(WorkflowStep): if errors: return errors - def post_render(self, request): - form = ConfirmationForm(request.POST) + def post(self, post_data, user): + form = ConfirmationForm(post_data) if form.is_valid(): data = form.cleaned_data['confirm'] - context = self.get_context() if data == "True": - context["bypassed"] = "true" errors = self.flush_to_db() if errors: - messages.add_message(request, messages.ERROR, "ERROR OCCURRED: " + errors) + self.set_invalid("ERROR OCCURRED: " + errors) else: - messages.add_message(request, messages.SUCCESS, "Confirmed") + self.set_valid("Confirmed") - return HttpResponse('') elif data == "False": - context["bypassed"] = "true" - messages.add_message(request, messages.SUCCESS, "Canceled") - return render(request, self.template, context) + self.set_valid("Canceled") else: - pass + self.set_invalid("Bad Form Contents") else: - pass + self.set_invalid("Bad Form Contents") class Repository(): diff --git a/src/workflow/opnfv_workflow.py b/src/workflow/opnfv_workflow.py index 7d499ec..a192d6e 100644 --- a/src/workflow/opnfv_workflow.py +++ b/src/workflow/opnfv_workflow.py @@ -74,8 +74,8 @@ class Pick_Installer(WorkflowStep): context["form"] = OPNFVSelectionForm(initial=initial) return context - def post_render(self, request): - form = OPNFVSelectionForm(request.POST) + def post(self, post_data, user): + form = OPNFVSelectionForm(post_data) if form.is_valid(): installer = form.cleaned_data['installer'] scenario = form.cleaned_data['scenario'] @@ -88,8 +88,6 @@ class Pick_Installer(WorkflowStep): else: self.set_invalid("Please select an Installer and Scenario") - return self.render(request) - class Assign_Network_Roles(WorkflowStep): template = 'config_bundle/steps/assign_network_roles.html' @@ -150,11 +148,11 @@ class Assign_Network_Roles(WorkflowStep): confirm['network roles'][role['role']] = role['network'].name self.repo_put(self.repo.CONFIRMATION, confirm) - def post_render(self, request): + def post(self, post_data, user): models = self.repo_get(self.repo.OPNFV_MODELS, {}) config_bundle = self.repo_get(self.repo.SELECTED_CONFIG_BUNDLE) roles = OPNFV_SETTINGS.NETWORK_ROLES - net_role_formset = self.create_netformset(roles, config_bundle, data=request.POST) + net_role_formset = self.create_netformset(roles, config_bundle, data=post_data) if net_role_formset.is_valid(): results = [] for form in net_role_formset: @@ -168,7 +166,6 @@ class Assign_Network_Roles(WorkflowStep): self.update_confirmation() else: self.set_invalid("Please complete all fields") - return self.render(request) class Assign_Host_Roles(WorkflowStep): # taken verbatim from Define_Software in sw workflow, merge the two? @@ -227,8 +224,8 @@ class Assign_Host_Roles(WorkflowStep): # taken verbatim from Define_Software in confirm['host roles'][role['host_name']] = role['role'].name self.repo_put(self.repo.CONFIRMATION, confirm) - def post_render(self, request): - formset = self.create_host_role_formset(data=request.POST) + def post(self, post_data, user): + formset = self.create_host_role_formset(data=post_data) models = self.repo_get(self.repo.OPNFV_MODELS, {}) host_roles = models.get("host_roles", []) @@ -254,8 +251,6 @@ class Assign_Host_Roles(WorkflowStep): # taken verbatim from Define_Software in else: self.set_invalid("Please complete all fields") - return self.render(request) - class MetaInfo(WorkflowStep): template = 'config_bundle/steps/config_software.html' @@ -280,11 +275,11 @@ class MetaInfo(WorkflowStep): confirm['description'] = meta['description'] self.repo_put(self.repo.CONFIRMATION, confirm) - def post_render(self, request): + def post(self, post_data, user): models = self.repo_get(self.repo.OPNFV_MODELS, {}) info = models.get("meta", {}) - form = BasicMetaForm(request.POST) + form = BasicMetaForm(post_data) if form.is_valid(): info['name'] = form.cleaned_data['name'] info['description'] = form.cleaned_data['description'] @@ -294,6 +289,4 @@ class MetaInfo(WorkflowStep): self.set_valid("Complete") else: self.set_invalid("Please correct the errors shown below") - self.repo_put(self.repo.OPNFV_MODELS, models) - return self.render(request) diff --git a/src/workflow/resource_bundle_workflow.py b/src/workflow/resource_bundle_workflow.py index 06737d2..2f4aa5d 100644 --- a/src/workflow/resource_bundle_workflow.py +++ b/src/workflow/resource_bundle_workflow.py @@ -8,8 +8,6 @@ ############################################################################## -from django.shortcuts import render -from django.forms import formset_factory from django.conf import settings import json @@ -22,7 +20,6 @@ from workflow.forms import ( HardwareDefinitionForm, NetworkDefinitionForm, ResourceMetaForm, - GenericHostMetaForm ) from resource_inventory.models import ( GenericResourceBundle, @@ -111,9 +108,9 @@ class Define_Hardware(WorkflowStep): confirm['resource']['lab'] = models['lab'].lab_user.username self.repo_put(self.repo.CONFIRMATION, confirm) - def post_render(self, request): + def post(self, post_data, user): try: - self.form = HardwareDefinitionForm(request.POST) + self.form = HardwareDefinitionForm(post_data) if self.form.is_valid(): self.update_models(self.form.cleaned_data) self.update_confirmation() @@ -122,8 +119,6 @@ class Define_Hardware(WorkflowStep): self.set_invalid("Please complete the fields highlighted in red to continue") except Exception as e: self.set_invalid(str(e)) - self.context = self.get_context() - return render(request, self.template, self.context) class Define_Nets(WorkflowStep): @@ -206,13 +201,13 @@ class Define_Nets(WorkflowStep): return context - def post_render(self, request): + def post(self, post_data, user): models = self.repo_get(self.repo.GRESOURCE_BUNDLE_MODELS, {}) if 'hosts' in models: 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") + xmlData = post_data.get("xml") self.updateModels(xmlData) # update model with xml self.set_valid("Networks applied successfully") @@ -220,7 +215,6 @@ class Define_Nets(WorkflowStep): self.set_invalid("Public network not availble") except Exception as e: self.set_invalid("An error occurred when applying networks: " + str(e)) - return self.render(request) def updateModels(self, xmlData): models = self.repo_get(self.repo.GRESOURCE_BUNDLE_MODELS, {}) @@ -380,8 +374,8 @@ class Resource_Meta_Info(WorkflowStep): context['form'] = ResourceMetaForm(initial={"bundle_name": name, "bundle_description": desc}) return context - def post_render(self, request): - form = ResourceMetaForm(request.POST) + def post(self, post_data, user): + form = ResourceMetaForm(post_data) if form.is_valid(): models = self.repo_get(self.repo.GRESOURCE_BUNDLE_MODELS, {}) name = form.cleaned_data['bundle_name'] @@ -402,62 +396,5 @@ class Resource_Meta_Info(WorkflowStep): confirm_info["description"] = tmp self.repo_put(self.repo.CONFIRMATION, confirm) self.set_valid("Step Completed") - else: self.set_invalid("Please correct the fields highlighted in red to continue") - pass - return self.render(request) - - -class Host_Meta_Info(WorkflowStep): - template = "resource/steps/host_info.html" - title = "Host Info" - description = "We need a little bit of information about your chosen machines" - short_title = "host info" - - def __init__(self, *args, **kwargs): - super(Host_Meta_Info, self).__init__(*args, **kwargs) - self.formset = formset_factory(GenericHostMetaForm, extra=0) - - def get_context(self): - context = super(Host_Meta_Info, self).get_context() - GenericHostFormset = self.formset - models = self.repo_get(self.repo.GRESOURCE_BUNDLE_MODELS, {}) - initial_data = [] - if "hosts" not in models: - context['error'] = "Please go back and select your hosts" - else: - for host in models['hosts']: - profile = host.profile.name - name = host.resource.name - if not name: - name = "" - initial_data.append({"host_profile": profile, "host_name": name}) - context['formset'] = GenericHostFormset(initial=initial_data) - return context - - def post_render(self, request): - models = self.repo_get(self.repo.GRESOURCE_BUNDLE_MODELS, {}) - if 'hosts' not in models: - models['hosts'] = [] - hosts = models['hosts'] - i = 0 - confirm_hosts = [] - GenericHostFormset = self.formset - formset = GenericHostFormset(request.POST) - if formset.is_valid(): - for form in formset: - host = hosts[i] - host.resource.name = form.cleaned_data['host_name'] - i += 1 - confirm_hosts.append({"name": host.resource.name, "profile": host.profile.name}) - models['hosts'] = hosts - self.repo_put(self.repo.GRESOURCE_BUNDLE_MODELS, models) - confirm = self.repo_get(self.repo.CONFIRMATION, {}) - if "resource" not in confirm: - confirm['resource'] = {} - confirm['resource']['hosts'] = confirm_hosts - self.repo_put(self.repo.CONFIRMATION, confirm) - else: - pass - return self.render(request) diff --git a/src/workflow/snapshot_workflow.py b/src/workflow/snapshot_workflow.py index 5414784..4266587 100644 --- a/src/workflow/snapshot_workflow.py +++ b/src/workflow/snapshot_workflow.py @@ -49,15 +49,15 @@ class Select_Host_Step(WorkflowStep): context['chosen'] = chosen return context - def post_render(self, request): - host_data = request.POST.get("host") + def post(self, post_data, user): + host_data = post_data.get("host") if not host_data: self.set_invalid("Please select a host") - return self.render(request) + return host = json.loads(host_data) if 'name' not in host or 'booking' not in host: self.set_invalid("Invalid host selected") - return self.render(request) + return name = host['name'] booking_id = host['booking'] booking = Booking.objects.get(pk=booking_id) @@ -76,7 +76,6 @@ class Select_Host_Step(WorkflowStep): confirm['snapshot'] = snap_confirm self.repo_put(self.repo.CONFIRMATION, confirm) self.set_valid("Success") - return self.render(request) class Image_Meta_Step(WorkflowStep): @@ -97,8 +96,8 @@ class Image_Meta_Step(WorkflowStep): context['form'] = form return context - def post_render(self, request): - form = BasicMetaForm(request.POST) + def post(self, post_data, user): + form = BasicMetaForm(post_data) if form.is_valid(): name = form.cleaned_data['name'] self.repo_put(self.repo.SNAPSHOT_NAME, name) @@ -115,5 +114,3 @@ class Image_Meta_Step(WorkflowStep): self.set_valid("Success") else: self.set_invalid("Please Fill out the Form") - - return self.render(request) diff --git a/src/workflow/sw_bundle_workflow.py b/src/workflow/sw_bundle_workflow.py index 0c558fc..4dc0b8e 100644 --- a/src/workflow/sw_bundle_workflow.py +++ b/src/workflow/sw_bundle_workflow.py @@ -104,7 +104,7 @@ class Define_Software(WorkflowStep): return context - def post_render(self, request): + def post(self, post_data, user): models = self.repo_get(self.repo.CONFIG_MODELS, {}) if "bundle" not in models: models['bundle'] = ConfigBundle(owner=self.repo_get(self.repo.SESSION_USER)) @@ -112,8 +112,8 @@ class Define_Software(WorkflowStep): confirm = self.repo_get(self.repo.CONFIRMATION, {}) hosts = self.get_host_list() - models['headnode_index'] = request.POST.get("headnode", 1) - formset = self.create_hostformset(hosts, data=request.POST) + models['headnode_index'] = post_data.get("headnode", 1) + formset = self.create_hostformset(hosts, data=post_data) has_headnode = False if formset.is_valid(): models['host_configs'] = [] @@ -140,7 +140,7 @@ class Define_Software(WorkflowStep): if not has_headnode: self.set_invalid('Must have one "Headnode" per POD') - return self.render(request) + return self.repo_put(self.repo.CONFIG_MODELS, models) if "configuration" not in confirm: @@ -151,8 +151,6 @@ class Define_Software(WorkflowStep): else: self.set_invalid("Please complete all fields") - return self.render(request) - class Config_Software(WorkflowStep): template = 'config_bundle/steps/config_software.html' @@ -172,7 +170,7 @@ class Config_Software(WorkflowStep): context["form"] = BasicMetaForm(initial=initial) return context - def post_render(self, request): + def post(self, post_data, user): models = self.repo_get(self.repo.CONFIG_MODELS, {}) if "bundle" not in models: models['bundle'] = ConfigBundle(owner=self.repo_get(self.repo.SESSION_USER)) @@ -181,7 +179,7 @@ class Config_Software(WorkflowStep): if "configuration" not in confirm: confirm['configuration'] = {} - form = BasicMetaForm(request.POST) + form = BasicMetaForm(post_data) if form.is_valid(): models['bundle'].name = form.cleaned_data['name'] models['bundle'].description = form.cleaned_data['description'] @@ -194,5 +192,3 @@ class Config_Software(WorkflowStep): self.repo_put(self.repo.CONFIG_MODELS, models) self.repo_put(self.repo.CONFIRMATION, confirm) - - return self.render(request) diff --git a/src/workflow/tests/test_fixtures.py b/src/workflow/tests/test_fixtures.py new file mode 100644 index 0000000..fe16be7 --- /dev/null +++ b/src/workflow/tests/test_fixtures.py @@ -0,0 +1,2 @@ + +MX_GRAPH_MODEL = '<mxGraphModel><root><mxCell id="0"/><mxCell id="1" parent="0"/><mxCell id="host_c" value="{"name":"c","description":"Intel based ProLiant server from HPE"}" style="editable=0" parent="1" vertex="1" connectable="0"><mxGeometry x="75" y="150" width="110" height="175" as="geometry"><mxPoint x="-50" as="offset"/></mxGeometry></mxCell><mxCell id="2" value="{"name":"ens4f1","description":"speed: 10000M type: onboard"}" style="fillColor=blue;editable=0" parent="host_c" vertex="1"><mxGeometry x="90" y="12" width="20" height="20" as="geometry"><mxPoint x="-26" as="offset"/></mxGeometry></mxCell><mxCell id="3" value="{"name":"ens4f0","description":"speed: 10000M type: onboard"}" style="fillColor=blue;editable=0" parent="host_c" vertex="1"><mxGeometry x="90" y="37" width="20" height="20" as="geometry"><mxPoint x="-26" as="offset"/></mxGeometry></mxCell><mxCell id="4" value="{"name":"ens1f2","description":"speed: 10000M type: onboard"}" style="fillColor=blue;editable=0" parent="host_c" vertex="1"><mxGeometry x="90" y="62" width="20" height="20" as="geometry"><mxPoint x="-26" as="offset"/></mxGeometry></mxCell><mxCell id="5" value="{"name":"ens1f1","description":"speed: 10000M type: onboard"}" style="fillColor=blue;editable=0" parent="host_c" vertex="1"><mxGeometry x="90" y="87" width="20" height="20" as="geometry"><mxPoint x="-26" as="offset"/></mxGeometry></mxCell><mxCell id="6" value="{"name":"ens1f0","description":"speed: 10000M type: onboard"}" style="fillColor=blue;editable=0" parent="host_c" vertex="1"><mxGeometry x="90" y="112" width="20" height="20" as="geometry"><mxPoint x="-26" as="offset"/></mxGeometry></mxCell><mxCell id="7" value="{"name":"eno49","description":"speed: 10000M type: onboard"}" style="fillColor=blue;editable=0" parent="host_c" vertex="1"><mxGeometry x="90" y="137" width="20" height="20" as="geometry"><mxPoint x="-22" as="offset"/></mxGeometry></mxCell><mxCell id="network_0" value="{"name":"public","public":true}" style="fillColor=red" parent="1" vertex="1"><mxGeometry x="400" y="-10" width="10" height="1700" as="geometry"/></mxCell><mxCell id="8" value="" style="fillColor=black;opacity=0" parent="network_0" vertex="1"><mxGeometry width="10" height="37.77777777777778" relative="1" as="geometry"/></mxCell><mxCell id="9" value="" style="fillColor=black;opacity=0" parent="network_0" vertex="1"><mxGeometry y="0.022222222222222223" width="10" height="37.77777777777778" relative="1" as="geometry"/></mxCell><mxCell id="10" value="" style="fillColor=black;opacity=0" parent="network_0" vertex="1"><mxGeometry y="0.044444444444444446" width="10" height="37.77777777777778" relative="1" as="geometry"/></mxCell><mxCell id="11" value="" style="fillColor=black;opacity=0" parent="network_0" vertex="1"><mxGeometry y="0.06666666666666667" width="10" height="37.77777777777778" relative="1" as="geometry"/></mxCell><mxCell id="12" value="" style="fillColor=black;opacity=0" parent="network_0" vertex="1"><mxGeometry y="0.08888888888888889" width="10" height="37.77777777777778" relative="1" as="geometry"/></mxCell><mxCell id="13" value="" style="fillColor=black;opacity=0" parent="network_0" vertex="1"><mxGeometry y="0.11111111111111112" width="10" height="37.77777777777778" relative="1" as="geometry"/></mxCell><mxCell id="14" value="" style="fillColor=black;opacity=0" parent="network_0" vertex="1"><mxGeometry y="0.13333333333333333" width="10" height="37.77777777777778" relative="1" as="geometry"/></mxCell><mxCell id="15" value="" style="fillColor=black;opacity=0" parent="network_0" vertex="1"><mxGeometry y="0.15555555555555556" width="10" height="37.77777777777778" relative="1" as="geometry"/></mxCell><mxCell id="16" value="" style="fillColor=black;opacity=0" parent="network_0" vertex="1"><mxGeometry y="0.17777777777777778" width="10" height="37.77777777777778" relative="1" as="geometry"/></mxCell><mxCell id="17" value="" style="fillColor=black;opacity=0" parent="network_0" vertex="1"><mxGeometry y="0.2" width="10" height="37.77777777777778" relative="1" as="geometry"/></mxCell><mxCell id="18" value="" style="fillColor=black;opacity=0" parent="network_0" vertex="1"><mxGeometry y="0.22222222222222224" width="10" height="37.77777777777778" relative="1" as="geometry"/></mxCell><mxCell id="19" value="" style="fillColor=black;opacity=0" parent="network_0" vertex="1"><mxGeometry y="0.24444444444444446" width="10" height="37.77777777777778" relative="1" as="geometry"/></mxCell><mxCell id="20" value="" style="fillColor=black;opacity=0" parent="network_0" vertex="1"><mxGeometry y="0.26666666666666666" width="10" height="37.77777777777778" relative="1" as="geometry"/></mxCell><mxCell id="21" value="" style="fillColor=black;opacity=0" parent="network_0" vertex="1"><mxGeometry y="0.2888888888888889" width="10" height="37.77777777777778" relative="1" as="geometry"/></mxCell><mxCell id="22" value="" style="fillColor=black;opacity=0" parent="network_0" vertex="1"><mxGeometry y="0.3111111111111111" width="10" height="37.77777777777778" relative="1" as="geometry"/></mxCell><mxCell id="23" value="" style="fillColor=black;opacity=0" parent="network_0" vertex="1"><mxGeometry y="0.33333333333333337" width="10" height="37.77777777777778" relative="1" as="geometry"/></mxCell><mxCell id="24" value="" style="fillColor=black;opacity=0" parent="network_0" vertex="1"><mxGeometry y="0.35555555555555557" width="10" height="37.77777777777778" relative="1" as="geometry"/></mxCell><mxCell id="25" value="" style="fillColor=black;opacity=0" parent="network_0" vertex="1"><mxGeometry y="0.37777777777777777" width="10" height="37.77777777777778" relative="1" as="geometry"/></mxCell><mxCell id="26" value="" style="fillColor=black;opacity=0" parent="network_0" vertex="1"><mxGeometry y="0.4" width="10" height="37.77777777777778" relative="1" as="geometry"/></mxCell><mxCell id="27" value="" style="fillColor=black;opacity=0" parent="network_0" vertex="1"><mxGeometry y="0.4222222222222222" width="10" height="37.77777777777778" relative="1" as="geometry"/></mxCell><mxCell id="28" value="" style="fillColor=black;opacity=0" parent="network_0" vertex="1"><mxGeometry y="0.4444444444444445" width="10" height="37.77777777777778" relative="1" as="geometry"/></mxCell><mxCell id="29" value="" style="fillColor=black;opacity=0" parent="network_0" vertex="1"><mxGeometry y="0.4666666666666667" width="10" height="37.77777777777778" relative="1" as="geometry"/></mxCell><mxCell id="30" value="" style="fillColor=black;opacity=0" parent="network_0" vertex="1"><mxGeometry y="0.48888888888888893" width="10" height="37.77777777777778" relative="1" as="geometry"/></mxCell><mxCell id="31" value="" style="fillColor=black;opacity=0" parent="network_0" vertex="1"><mxGeometry y="0.5111111111111112" width="10" height="37.77777777777778" relative="1" as="geometry"/></mxCell><mxCell id="32" value="" style="fillColor=black;opacity=0" parent="network_0" vertex="1"><mxGeometry y="0.5333333333333333" width="10" height="37.77777777777778" relative="1" as="geometry"/></mxCell><mxCell id="33" value="" style="fillColor=black;opacity=0" parent="network_0" vertex="1"><mxGeometry y="0.5555555555555556" width="10" height="37.77777777777778" relative="1" as="geometry"/></mxCell><mxCell id="34" value="" style="fillColor=black;opacity=0" parent="network_0" vertex="1"><mxGeometry y="0.5777777777777778" width="10" height="37.77777777777778" relative="1" as="geometry"/></mxCell><mxCell id="35" value="" style="fillColor=black;opacity=0" parent="network_0" vertex="1"><mxGeometry y="0.6" width="10" height="37.77777777777778" relative="1" as="geometry"/></mxCell><mxCell id="36" value="" style="fillColor=black;opacity=0" parent="network_0" vertex="1"><mxGeometry y="0.6222222222222222" width="10" height="37.77777777777778" relative="1" as="geometry"/></mxCell><mxCell id="37" value="" style="fillColor=black;opacity=0" parent="network_0" vertex="1"><mxGeometry y="0.6444444444444445" width="10" height="37.77777777777778" relative="1" as="geometry"/></mxCell><mxCell id="38" value="" style="fillColor=black;opacity=0" parent="network_0" vertex="1"><mxGeometry y="0.6666666666666667" width="10" height="37.77777777777778" relative="1" as="geometry"/></mxCell><mxCell id="39" value="" style="fillColor=black;opacity=0" parent="network_0" vertex="1"><mxGeometry y="0.6888888888888889" width="10" height="37.77777777777778" relative="1" as="geometry"/></mxCell><mxCell id="40" value="" style="fillColor=black;opacity=0" parent="network_0" vertex="1"><mxGeometry y="0.7111111111111111" width="10" height="37.77777777777778" relative="1" as="geometry"/></mxCell><mxCell id="41" value="" style="fillColor=black;opacity=0" parent="network_0" vertex="1"><mxGeometry y="0.7333333333333334" width="10" height="37.77777777777778" relative="1" as="geometry"/></mxCell><mxCell id="42" value="" style="fillColor=black;opacity=0" parent="network_0" vertex="1"><mxGeometry y="0.7555555555555555" width="10" height="37.77777777777778" relative="1" as="geometry"/></mxCell><mxCell id="43" value="" style="fillColor=black;opacity=0" parent="network_0" vertex="1"><mxGeometry y="0.7777777777777778" width="10" height="37.77777777777778" relative="1" as="geometry"/></mxCell><mxCell id="44" value="" style="fillColor=black;opacity=0" parent="network_0" vertex="1"><mxGeometry y="0.8" width="10" height="37.77777777777778" relative="1" as="geometry"/></mxCell><mxCell id="45" value="" style="fillColor=black;opacity=0" parent="network_0" vertex="1"><mxGeometry y="0.8222222222222223" width="10" height="37.77777777777778" relative="1" as="geometry"/></mxCell><mxCell id="46" value="" style="fillColor=black;opacity=0" parent="network_0" vertex="1"><mxGeometry y="0.8444444444444444" width="10" height="37.77777777777778" relative="1" as="geometry"/></mxCell><mxCell id="47" value="" style="fillColor=black;opacity=0" parent="network_0" vertex="1"><mxGeometry y="0.8666666666666667" width="10" height="37.77777777777778" relative="1" as="geometry"/></mxCell><mxCell id="48" value="" style="fillColor=black;opacity=0" parent="network_0" vertex="1"><mxGeometry y="0.888888888888889" width="10" height="37.77777777777778" relative="1" as="geometry"/></mxCell><mxCell id="49" value="" style="fillColor=black;opacity=0" parent="network_0" vertex="1"><mxGeometry y="0.9111111111111111" width="10" height="37.77777777777778" relative="1" as="geometry"/></mxCell><mxCell id="50" value="" style="fillColor=black;opacity=0" parent="network_0" vertex="1"><mxGeometry y="0.9333333333333333" width="10" height="37.77777777777778" relative="1" as="geometry"/></mxCell><mxCell id="51" value="" style="fillColor=black;opacity=0" parent="network_0" vertex="1"><mxGeometry y="0.9555555555555556" width="10" height="37.77777777777778" relative="1" as="geometry"/></mxCell><mxCell id="52" value="" style="fillColor=black;opacity=0" parent="network_0" vertex="1"><mxGeometry y="0.9777777777777779" width="10" height="37.77777777777778" relative="1" as="geometry"/></mxCell><mxCell id="53" value="{"tagged":false}" style="strokeColor=red" parent="1" source="2" target="13" edge="1"><mxGeometry relative="1" as="geometry"/></mxCell></root></mxGraphModel>' diff --git a/src/workflow/tests/test_steps.py b/src/workflow/tests/test_steps.py index 380102a..39b1f86 100644 --- a/src/workflow/tests/test_steps.py +++ b/src/workflow/tests/test_steps.py @@ -7,275 +7,256 @@ # http://www.apache.org/licenses/LICENSE-2.0 ############################################################################## -from django.test import TestCase -from dashboard.populate_db import Populator -from workflow.tests import constants -from workflow.workflow_factory import WorkflowFactory +""" +This file tests basic functionality of each step class +More in depth case coverage of WorkflowStep.post() must happen elsewhere. +""" + +import json +from unittest import SkipTest, mock + +from django.test import TestCase, RequestFactory +from dashboard.testing_utils import make_lab, make_user, make_os,\ + make_complete_host_profile, make_opnfv_role, make_image, make_grb,\ + make_config_bundle, make_host, make_user_profile, make_generic_host +from workflow import resource_bundle_workflow +from workflow import booking_workflow +from workflow import sw_bundle_workflow from workflow.models import Repository -from workflow.resource_bundle_workflow import Define_Hardware, Define_Nets, Resource_Meta_Info, Host_Meta_Info -from workflow.sw_bundle_workflow import SWConf_Resource_Select, Define_Software, Config_Software -from workflow.booking_workflow import Booking_Resource_Select, SWConfig_Select, Booking_Meta -from django.http import QueryDict, HttpRequest -from django.contrib.auth.models import User -from resource_inventory.models import ( - Scenario, - Installer, - OPNFVRole, - Image, - GenericResourceBundle, - GenericHost, - HostProfile, - GenericResource, - ConfigBundle -) - - -class BaseStepTestCase(TestCase): +from workflow.tests import test_fixtures + + +class TestConfig: + """ + Basic class to instantiate and hold reference + to models we will need often + """ + def __init__(self, usr=None): + self.lab = make_lab() + self.user = usr or make_user() + self.os = make_os() + self.host_prof = make_complete_host_profile(self.lab) + self.host = make_host(self.host_prof, self.lab, name="host1") + + # pod description as required by testing lib + self.topology = { + "host1": { + "type": self.host_prof, + "role": make_opnfv_role(), + "image": make_image(self.lab, 3, self.user, self.os, self.host_prof), + "nets": [ + [{"name": "public", "tagged": True, "public": True}] + ] + } + } + self.grb = make_grb(self.topology, self.user, self.lab)[0] + self.generic_host = make_generic_host(self.grb, self.host_prof, "host1") + + +class StepTestCase(TestCase): + + # after setUp is called, this should be an instance of a step + step = None + + post_data = {} # subclasses will set this @classmethod def setUpTestData(cls): - Populator().populate() - - def makeRepo(self): + super().setUpTestData() + cls.factory = RequestFactory() + cls.user_prof = make_user_profile() + cls.user = cls.user_prof.user + + def setUp(self): + super().setUp() + if self.step is None: + raise SkipTest("Step instance not given") repo = Repository() - repo.el[repo.SESSION_USER] = User.objects.filter(username="user 1").first() - return repo - - def step_test(self, step_type, data): - step = WorkflowFactory().make_step(step_type, self.makeRepo()) - formData = QueryDict(mutable=True) - formData.update(data) - request = HttpRequest() - request.POST = formData - response = step.post_render(request) - context = step.get_context() - return response, context - - -class BookingResourceSelectTestCase(BaseStepTestCase): - - def test_step_with_good_data(self): - grb_model = GenericResourceBundle.objects.filter(owner__username="user 1").first() - grb = [{"small_name": grb_model.name, "expanded_name": "user 1", "id": grb_model.id, "string": ""}] - grb = str(grb).replace("'", '"') - data = {"generic_resource_bundle": grb} - response, context = self.step_test(Booking_Resource_Select, data) - self.assertTrue(True) - - def test_step_with_bad_data(self): # TODO - data = {} - response, context = self.step_test(Booking_Resource_Select, data) - - def test_step_with_empty_data(self): - data = {} - response, context = self.step_test(SWConfig_Select, data) - + self.add_to_repo(repo) + self.step = self.step(1, repo) -class SoftwareConfigSelectTestCase(BaseStepTestCase): + def assertCorrectPostBehavior(self, post_data): + """ + allows subclasses to override and make assertions about + the side effects of self.step.post() + post_data is the data passed into post() + """ + return - def test_step_with_good_data(self): - config_model = ConfigBundle.objects.filter(owner__username="user 1").first() - config = [{"expanded_name": "user 1", "small_name": config_model.name, "id": config_model.id, "string": ""}] - config = str(config).replace("'", '"') - data = {"software_bundle": config} - response, context = self.step_test(SWConfig_Select, data) + def add_to_repo(self, repo): + """ + This method is a hook that allows subclasses to modify + the contents of the repo before the step is created. + """ + return - def test_step_with_bad_data(self): # TODO - data = {} - response, context = self.step_test(SWConfig_Select, data) + def assertValidHtml(self, html_str): + """ + This method should make sure that html_str is a valid + html fragment. + However, I know of no good way of doing this in python + """ + self.assertTrue(isinstance(html_str, str)) + self.assertGreater(len(html_str), 0) - def test_step_with_empty_data(self): - data = {} - response, context = self.step_test(SWConfig_Select, data) + def test_render_to_string(self): + request = self.factory.get("/workflow/manager/") + request.user = self.user + response_html = self.step.render_to_string(request) + self.assertValidHtml(response_html) + def test_post(self, data=None): + post_data = data or self.post_data + self.step.post(post_data, self.user) + self.assertCorrectPostBehavior(data) -class BookingMetaTestCase(BaseStepTestCase): - def test_step_with_good_data(self): - data = {"length": 7, "project": "LaaS", "purpose": "testing"} - user2 = User.objects.get(username="user 2") - john = User.objects.get(username="johnsmith") - users = [ - {"expanded_name": "", "id": user2.id, "small_name": user2.username, "string": user2.email}, - {"expanded_name": "", "id": john.id, "small_name": john.username, "string": john.email} - ] - users = str(users).replace("'", '"') - data['users'] = users - response, context = self.step_test(Booking_Meta, data) +class SelectStepTestCase(StepTestCase): + # ID of model to be sent to the step's form + # can be an int or a list of ints + obj_id = -1 - def test_step_with_bad_data(self): # TODO - data = {} - response, context = self.step_test(Booking_Meta, data) + def setUp(self): + super().setUp() - def test_step_with_empty_data(self): - data = {} - response, context = self.step_test(Booking_Meta, data) + try: + iter(self.obj_id) + except TypeError: + self.obj_id = [self.obj_id] + field_data = json.dumps(self.obj_id) + self.post_data = { + "searchable_select": [field_data] + } -class DefineHardwareTestCase(BaseStepTestCase): - def test_step_with_good_data(self): - hosts = {"host_4": 1, "host_1": 1} - labs = {"lab_1": "true"} - data = {"hosts": hosts, "labs": labs} - response, context = self.step_test(Define_Hardware, data) +class DefineHardwareTestCase(StepTestCase): + step = resource_bundle_workflow.Define_Hardware + post_data = { + "filter_field": { + "lab": { + "lab_35": {"selected": True, "id": 35}}, + "host": { + "host_1": {"selected": True, "id": 1}} + } + } - def test_step_with_bad_data(self): # TODO - data = {} - response, context = self.step_test(Define_Hardware, data) - def test_step_with_empty_data(self): - data = {} - response, context = self.step_test(Define_Hardware, data) +class DefineNetworkTestCase(StepTestCase): + step = resource_bundle_workflow.Define_Nets + post_data = {"xml": test_fixtures.MX_GRAPH_MODEL} -class HostMetaInfoTestCase(BaseStepTestCase): +class ResourceMetaTestCase(StepTestCase): + step = resource_bundle_workflow.Resource_Meta_Info + post_data = { + "bundle_name": "my_bundle", + "bundle_description": "My Bundle" + } - def makeRepo(self): - """ - override to provide step with needed host info - """ - repo = super(HostMetaInfoTestCase, self).makeRepo() - # get models - models = {} - models['bundle'] = GenericResourceBundle() - # make generic hosts - gres1 = GenericResource(bundle=models['bundle']) - prof1 = HostProfile.objects.get(name="Test profile 0") - ghost1 = GenericHost(profile=prof1, resource=gres1) - gres2 = GenericResource(bundle=models['bundle']) - prof2 = HostProfile.objects.get(name="Test profile 3") - ghost2 = GenericHost(profile=prof2, resource=gres2) - models['hosts'] = [ghost1, ghost2] - repo.el[repo.GRESOURCE_BUNDLE_MODELS] = models - return repo +class BookingResourceTestCase(SelectStepTestCase): + step = booking_workflow.Booking_Resource_Select - def test_step_with_good_data(self): - data = {"form-INITIAL_FORMS": 2, "form-MAX_NUM_FORMS": 1000} - data["form-MIN_NUM_FORMS"] = 0 - data["form-TOTAL_FORMS"] = 2 - data['form-0-host_name'] = "first host" - data['form-1-host_name'] = "second host" - response, context = self.step_test(Host_Meta_Info, data) + def add_to_repo(self, repo): + repo.el[repo.SESSION_USER] = self.user - def test_step_with_bad_data(self): # TODO - data = {"form-INITIAL_FORMS": 0, "form-MAX_NUM_FORMS": 1000} - data["form-MIN_NUM_FORMS"] = 0 - data["form-TOTAL_FORMS"] = 0 - response, context = self.step_test(Host_Meta_Info, data) + @classmethod + def setUpTestData(cls): + super().setUpTestData() + conf = TestConfig(usr=cls.user) + cls.obj_id = conf.grb.id - def test_step_with_empty_data(self): - data = {"form-INITIAL_FORMS": 0, "form-MAX_NUM_FORMS": 1000} - data["form-MIN_NUM_FORMS"] = 0 - data["form-TOTAL_FORMS"] = 0 - response, context = self.step_test(Host_Meta_Info, data) +class SoftwareSelectTestCase(SelectStepTestCase): + step = booking_workflow.SWConfig_Select -class DefineNetsTestCase(BaseStepTestCase): + def add_to_repo(self, repo): + repo.el[repo.SESSION_USER] = self.user + repo.el[repo.SELECTED_GRESOURCE_BUNDLE] = self.conf.grb - def test_step_with_good_data(self): - xml = constants.POD_XML - data = {"xml": xml} - response, context = self.step_test(Define_Nets, data) + @classmethod + def setUpTestData(cls): + super().setUpTestData() + cls.conf = TestConfig(usr=cls.user) + host_map = {"host1": cls.conf.generic_host} + config_bundle = make_config_bundle(cls.conf.grb, cls.conf.user, cls.conf.topology, host_map)[0] + cls.obj_id = config_bundle.id - def test_step_with_bad_data(self): # TODO - data = {} - response, context = self.step_test(Define_Nets, data) - def test_step_with_empty_data(self): - data = {} - response, context = self.step_test(Define_Nets, data) +class OPNFVSelectTestCase(SelectStepTestCase): + step = booking_workflow.OPNFV_Select + def add_to_repo(self, repo): + repo.el[repo.SELECTED_CONFIG_BUNDLE] = self.config_bundle -class ResourceMetaInfoTestCase(BaseStepTestCase): + @classmethod + def setUpTestData(cls): + super().setUpTestData() + conf = TestConfig(usr=cls.user) + host_map = {"host1": conf.generic_host} + cls.config_bundle, opnfv_config = make_config_bundle(conf.grb, conf.user, conf.topology, host_map) + cls.obj_id = opnfv_config.id - def test_step_with_good_data(self): - data = {"bundle_description": "description", "bundle_name": "my testing bundle"} - response, context = self.step_test(Resource_Meta_Info, data) - def test_step_with_bad_data(self): # TODO - data = {} - response, context = self.step_test(Resource_Meta_Info, data) +class BookingMetaTestCase(StepTestCase): + step = booking_workflow.Booking_Meta + post_data = { + "length": 14, + "purpose": "Testing", + "project": "Lab as a Service", + "users": ["[-1]"] + } - def test_step_with_empty_data(self): - data = {} - response, context = self.step_test(Resource_Meta_Info, data) + def add_to_repo(self, repo): + repo.el[repo.SESSION_MANAGER] = mock.MagicMock() + @classmethod + def setUpTestData(cls): + super().setUpTestData() + new_user = make_user(username="collaborator", email="different@mail.com") + new_user_prof = make_user_profile(user=new_user) + data = "[" + str(new_user_prof.id) + "]" # list of IDs + cls.post_data['users'] = [data] -class SWConfResourceSelectTestCase(BaseStepTestCase): - def test_step_with_good_data(self): - grb_model = GenericResourceBundle.objects.filter(owner__username="user 1").first() - grb = [{"small_name": grb_model.name, "expanded_name": "user 1", "id": grb_model.id, "string": ""}] - grb = str(grb).replace("'", '"') - data = {"generic_resource_bundle": grb} - response, context = self.step_test(SWConf_Resource_Select, data) +class ConfigResourceSelectTestCase(SelectStepTestCase): + step = sw_bundle_workflow.SWConf_Resource_Select - def test_step_with_bad_data(self): # TODO - data = {} - response, context = self.step_test(SWConf_Resource_Select, data) + def add_to_repo(self, repo): + repo.el[repo.SESSION_USER] = self.user - def test_step_with_empty_data(self): - data = {} - response, context = self.step_test(SWConf_Resource_Select, data) + @classmethod + def setUpTestData(cls): + super().setUpTestData() + conf = TestConfig(usr=cls.user) + cls.obj_id = conf.grb.id + + +class DefineSoftwareTestCase(StepTestCase): + step = sw_bundle_workflow.Define_Software + post_data = { + "form-0-image": 1, + "headnode": 1, + "form-0-headnode": "", + "form-TOTAL_FORMS": 1, + "form-INITIAL_FORMS": 1, + "form-MIN_NUM_FORMS": 0, + "form-MAX_NUM_FORMS": 1000, + } + + def add_to_repo(self, repo): + repo.el[repo.SELECTED_GRESOURCE_BUNDLE] = self.conf.grb + @classmethod + def setUpTestData(cls): + super().setUpTestData() + cls.conf = TestConfig(usr=cls.user) -class DefineSoftwareTestCase(BaseStepTestCase): - def makeRepo(self): - """ - put selected grb in repo for step - """ - repo = super(DefineSoftwareTestCase, self).makeRepo() - grb = GenericResourceBundle.objects.filter(owner__username="user 1").first() - repo.el[repo.SWCONF_SELECTED_GRB] = grb - return repo - - def test_step_with_good_data(self): - data = {"form-INITIAL_FORMS": 3, "form-MAX_NUM_FORMS": 1000} - data["form-MIN_NUM_FORMS"] = 0 - data["form-TOTAL_FORMS"] = 3 - an_image_id = Image.objects.get(name="a host image").id - another_image_id = Image.objects.get(name="another host image").id - control = OPNFVRole.objects.get(name="Controller") - compute = OPNFVRole.objects.get(name="Compute") - jumphost = OPNFVRole.objects.get(name="Jumphost") - data['form-0-image'] = an_image_id - data['form-1-image'] = an_image_id - data['form-2-image'] = another_image_id - data['form-0-role'] = compute.id - data['form-1-role'] = control.id - data['form-2-role'] = jumphost.id - response, context = self.step_test(Define_Software, data) - - def test_step_with_bad_data(self): # TODO - data = {"form-INITIAL_FORMS": 0, "form-MAX_NUM_FORMS": 1000} - data["form-MIN_NUM_FORMS"] = 0 - data["form-TOTAL_FORMS"] = 0 - response, context = self.step_test(Define_Software, data) - - def test_step_with_empty_data(self): - data = {"form-INITIAL_FORMS": 0, "form-MAX_NUM_FORMS": 1000} - data["form-MIN_NUM_FORMS"] = 0 - data["form-TOTAL_FORMS"] = 0 - response, context = self.step_test(Define_Software, data) - - -class ConfigSoftwareTestCase(BaseStepTestCase): - - def test_step_with_good_data(self): - data = {"description": "description", "name": "namey"} - installer = Installer.objects.get(name="Fuel") - scenario = Scenario.objects.get(name="os-nosdn-nofeature-noha") - data['installer'] = installer.id - data['scenario'] = scenario.id - response, context = self.step_test(Config_Software, data) - - def test_step_with_bad_data(self): # TODO - data = {} - response, context = self.step_test(Config_Software, data) - - def test_step_with_empty_data(self): - data = {} - response, context = self.step_test(Config_Software, data) +class ConfigSoftwareTestCase(StepTestCase): + step = sw_bundle_workflow.Config_Software + post_data = { + "name": "config_bundle", + "description": "My Config Bundle" + } diff --git a/src/workflow/tests/test_steps_render.py b/src/workflow/tests/test_steps_render.py deleted file mode 100644 index f3df8f2..0000000 --- a/src/workflow/tests/test_steps_render.py +++ /dev/null @@ -1,43 +0,0 @@ -############################################################################## -# Copyright (c) 2018 Parker Berberian, Sawyer Bergeron, and others. -# -# All rights reserved. This program and the accompanying materials -# are made available under the terms of the Apache License, Version 2.0 -# which accompanies this distribution, and is available at -# http://www.apache.org/licenses/LICENSE-2.0 -############################################################################## - -from django.test import TestCase, Client - - -class SuperViewTestCase(TestCase): - url = "/" - client = Client() - - def test_get(self): - response = self.client.get(self.url) - self.assertLess(response.status_code, 300) - - -class DefineHardwareViewTestCase(SuperViewTestCase): - url = "/wf/workflow/step/define_hardware" - - -class DefineNetworkViewTestCase(SuperViewTestCase): - url = "/wf/workflow/step/define_net" - - -class ResourceMetaViewTestCase(SuperViewTestCase): - url = "/wf/workflow/step/resource_meta" - - -class BookingMetaViewTestCase(SuperViewTestCase): - url = "/wf/workflow/step/booking_meta" - - -class SoftwareSelectViewTestCase(SuperViewTestCase): - url = "/wf/workflow/step/software_select" - - -class ResourceSelectViewTestCase(SuperViewTestCase): - url = "/wf/workflow/step/resource_select" diff --git a/src/workflow/tests/test_workflows.py b/src/workflow/tests/test_workflows.py index 7a53521..293e43d 100644 --- a/src/workflow/tests/test_workflows.py +++ b/src/workflow/tests/test_workflows.py @@ -7,9 +7,9 @@ # http://www.apache.org/licenses/LICENSE-2.0 ############################################################################## +from unittest import SkipTest from django.test import TestCase from workflow.workflow_factory import WorkflowFactory -from dashboard.populate_db import Populator """ @@ -29,8 +29,9 @@ To remove a workflow: class WorkflowTestCase(TestCase): @classmethod - def setUpTestData(cls): - Populator().populate() + def setUpClass(cls): + super().setUpClass() + raise SkipTest("These tests are no good") def setUp(self): self.clear_workflow() diff --git a/src/workflow/urls.py b/src/workflow/urls.py index 5a97904..b1b95a7 100644 --- a/src/workflow/urls.py +++ b/src/workflow/urls.py @@ -9,26 +9,15 @@ from django.conf.urls import url -from django.conf import settings -from workflow.views import step_view, delete_session, manager_view, viewport_view -from workflow.models import Repository -from workflow.resource_bundle_workflow import Define_Hardware, Define_Nets, Resource_Meta_Info -from workflow.booking_workflow import SWConfig_Select, Booking_Resource_Select, Booking_Meta +from workflow.views import manager_view, viewport_view, add_workflow, remove_workflow, create_workflow app_name = 'workflow' urlpatterns = [ - url(r'^workflow/$', step_view, name='workflow'), - url(r'^workflow/finish/$', delete_session, name='delete_session'), url(r'^manager/$', manager_view, name='manager'), + url(r'^add/$', add_workflow, name='add_workflow'), + url(r'^create/$', create_workflow, name='create_workflow'), + url(r'^pop/$', remove_workflow, name='remove_workflow'), url(r'^$', viewport_view, name='viewport') ] - -if settings.TESTING: - urlpatterns.append(url(r'^workflow/step/define_hardware$', Define_Hardware("", Repository()).test_render)) - urlpatterns.append(url(r'^workflow/step/define_net$', Define_Nets("", Repository()).test_render)) - urlpatterns.append(url(r'^workflow/step/resource_meta$', Resource_Meta_Info("", Repository()).test_render)) - urlpatterns.append(url(r'^workflow/step/booking_meta$', Booking_Meta("", Repository()).test_render)) - urlpatterns.append(url(r'^workflow/step/software_select$', SWConfig_Select("", Repository()).test_render)) - urlpatterns.append(url(r'^workflow/step/resource_select$', Booking_Resource_Select("", Repository()).test_render)) diff --git a/src/workflow/views.py b/src/workflow/views.py index 7ed9031..9ff444d 100644 --- a/src/workflow/views.py +++ b/src/workflow/views.py @@ -8,14 +8,12 @@ ############################################################################## -from django.http import HttpResponseGone, JsonResponse +from django.http import HttpResponse from django.shortcuts import render -from django.urls import reverse import uuid from workflow.workflow_manager import ManagerTracker, SessionManager -from booking.models import Booking import logging logger = logging.getLogger(__name__) @@ -31,77 +29,38 @@ def attempt_auth(request): return None -def get_redirect_response(result): - if not result: - return {} - - # need to get type of result, and switch on the type - # since has_result, result must be populated with a valid object - if isinstance(result, Booking): - return { - 'redir_url': reverse('booking:booking_detail', kwargs={'booking_id': result.id}) - } - else: - return {} - - -def delete_session(request): +def remove_workflow(request): manager = attempt_auth(request) if not manager: - return HttpResponseGone("No session found that relates to current request") + return no_workflow(request) - not_last_workflow, result = manager.pop_workflow() + has_more_workflows, result = manager.pop_workflow() - if not_last_workflow: # this was not the last workflow, so don't redirect away - return JsonResponse({}) - else: + if not has_more_workflows: # this was the last workflow, so delete the reference to it in the tracker del ManagerTracker.managers[request.session['manager_session']] - return JsonResponse(get_redirect_response(result)) + return manager.render(request) -def step_view(request): +def add_workflow(request): manager = attempt_auth(request) if not manager: - # no manager found, redirect to "lost" page return no_workflow(request) - if request.GET.get('step') is not None: - if request.GET.get('step') == 'next': - manager.go_next() - elif request.GET.get('step') == 'prev': - manager.go_prev() - else: - raise Exception("requested action for new step had malformed contents: " + request.GET.get('step')) - return manager.render(request) + try: + workflow_type = int(request.POST.get('workflow_type')) + except ValueError: + return HttpResponse(status=400) + + manager.add_workflow(workflow_type=workflow_type) + return manager.render(request) # do we want this? def manager_view(request): manager = attempt_auth(request) - if not manager: - return HttpResponseGone("No session found that relates to current request") - - if request.method == 'GET': - # no need for this statement if only intercepting post requests - - # return general context for viewport page - return manager.status(request) - - if request.method == 'POST': - if request.POST.get('add') is not None: - logger.debug("add found") - target_id = None - if 'target' in request.POST: - target_id = int(request.POST.get('target')) - manager.add_workflow(workflow_type=int(request.POST.get('add')), target_id=target_id) - elif request.POST.get('edit') is not None and request.POST.get('edit_id') is not None: - logger.debug("edit found") - manager.add_workflow(workflow_type=request.POST.get('edit'), edit_object=int(request.POST.get('edit_id'))) - elif request.POST.get('cancel') is not None: - if not manager.pop_workflow(): - del ManagerTracker.managers[request.session['manager_session']] + return no_workflow(request) - return manager.status(request) + return manager.handle_request(request) def viewport_view(request): @@ -112,16 +71,27 @@ def viewport_view(request): if manager is None: return no_workflow(request) - if request.method == 'GET': - return render(request, 'workflow/viewport-base.html') - else: - pass + if request.method != 'GET': + return HttpResponse(status=405) + return render(request, 'workflow/viewport-base.html') + + +def create_workflow(request): + if request.method != 'POST': + return HttpResponse(status=405) + workflow_type = request.POST.get('workflow_type') + try: + workflow_type = int(workflow_type) + except Exception: + return HttpResponse(status=400) + mgr_uuid = create_session(workflow_type, request=request,) + request.session['manager_session'] = mgr_uuid + return HttpResponse() def create_session(wf_type, request): - wf = int(wf_type) smgr = SessionManager(request=request) - smgr.add_workflow(workflow_type=wf, target_id=request.POST.get("target")) + smgr.add_workflow(workflow_type=wf_type, target_id=request.POST.get("target")) manager_uuid = uuid.uuid4().hex ManagerTracker.getInstance().managers[manager_uuid] = smgr @@ -129,10 +99,7 @@ def create_session(wf_type, request): def no_workflow(request): - - logger.debug("There is no active workflow") - - return render(request, 'workflow/no_workflow.html', {'title': "Not Found"}) + return render(request, 'workflow/no_workflow.html', {'title': "Not Found"}, status=404) def login(request): diff --git a/src/workflow/workflow_manager.py b/src/workflow/workflow_manager.py index 80b8a67..4677829 100644 --- a/src/workflow/workflow_manager.py +++ b/src/workflow/workflow_manager.py @@ -9,6 +9,8 @@ from django.http import JsonResponse +from django.http.request import QueryDict +from django.urls import reverse from booking.models import Booking from workflow.workflow_factory import WorkflowFactory @@ -19,6 +21,7 @@ from resource_inventory.models import ( HostConfiguration, OPNFVConfig ) +from workflow.forms import ManagerForm import logging logger = logging.getLogger(__name__) @@ -30,10 +33,9 @@ class SessionManager(): def __init__(self, request=None): self.workflows = [] - self.owner = request.user - self.factory = WorkflowFactory() + self.result = None def set_step_statuses(self, superclass_type, desired_enabled=True): workflow = self.active_workflow() @@ -45,10 +47,7 @@ class SessionManager(): else: step.disable() - def add_workflow(self, workflow_type=None, target_id=None, **kwargs): - if target_id is not None: - self.prefill_repo(target_id, workflow_type) - + def add_workflow(self, workflow_type=None, **kwargs): repo = Repository() if(len(self.workflows) >= 1): defaults = self.workflows[-1].repository.get_child_defaults() @@ -63,6 +62,11 @@ class SessionManager(): ) ) + def get_redirect(self): + if isinstance(self.result, Booking): + return reverse('booking:booking_detail', kwargs={'booking_id': self.result.id}) + return "/" + def pop_workflow(self): multiple_wfs = len(self.workflows) > 1 if multiple_wfs: @@ -70,29 +74,51 @@ class SessionManager(): key = self.workflows[-1].repository.el[Repository.RESULT_KEY] result = self.workflows[-1].repository.el[Repository.RESULT] self.workflows[-2].repository.el[key] = result - self.workflows.pop() - current_repo = self.workflows[-1].repository - return (multiple_wfs, current_repo.el[current_repo.RESULT]) + prev_workflow = self.workflows.pop() + if self.workflows: + current_repo = self.workflows[-1].repository + else: + current_repo = prev_workflow.repository + self.result = current_repo.el[current_repo.RESULT] + return multiple_wfs, self.result def status(self, request): - try: - meta_json = [] - for step in self.active_workflow().steps: - meta_json.append(step.to_json()) - responsejson = {} - responsejson["steps"] = meta_json - responsejson["active"] = self.active_workflow().repository.el['active_step'] - responsejson["workflow_count"] = len(self.workflows) - return JsonResponse(responsejson, safe=False) - except Exception: - pass + return { + "steps": [step.to_json() for step in self.active_workflow().steps], + "active": self.active_workflow().repository.el['active_step'], + "workflow_count": len(self.workflows) + } + + def handle_post(self, request): + form = ManagerForm(request.POST) + if form.is_valid(): + self.get_active_step().post( + QueryDict(form.cleaned_data['step_form']), + user=request.user + ) + # change step + if form.cleaned_data['step'] == 'prev': + self.go_prev() + if form.cleaned_data['step'] == 'next': + self.go_next() + else: + pass # Exception? + + def handle_request(self, request): + if request.method == 'POST': + self.handle_post(request) + return self.render(request) def render(self, request, **kwargs): - # filter out when a step needs to handle post/form data - # if 'workflow' in post data, this post request was meant for me, not step - if request.method == 'POST' and request.POST.get('workflow', None) is None: - return self.active_workflow().steps[self.active_workflow().active_index].post_render(request) - return self.active_workflow().steps[self.active_workflow().active_index].render(request) + if self.workflows: + return JsonResponse({ + "meta": self.status(request), + "content": self.get_active_step().render_to_string(request), + }) + else: + return JsonResponse({ + "redirect": self.get_redirect() + }) def post_render(self, request): return self.active_workflow().steps[self.active_workflow().active_index].post_render(request) |