aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJustin Choquette <jchoquette@iol.unh.edu>2023-08-08 11:33:57 -0400
committerJustin Choquette <jchoquette@iol.unh.edu>2023-08-18 11:59:01 -0400
commitecadb07367d31c0929212618e120130f54af78da (patch)
treef626ef347f6fa7cb7f9ee962539a5f769bc3d471
parenta6168306c08e8d5b207b9acc48869180d194ff01 (diff)
MVP
Change-Id: Ib590302f49e7e66f8d04841fb6cb97baf623f51a Signed-off-by: Justin Choquette <jchoquette@iol.unh.edu>
-rw-r--r--src/account/views.py3
-rw-r--r--src/api/utils.py12
-rw-r--r--src/api/views.py98
-rw-r--r--src/booking/urls.py4
-rw-r--r--src/dashboard/tasks.py3
-rw-r--r--src/dashboard/views.py28
-rw-r--r--src/laas_dashboard/urls.py1
-rw-r--r--src/resource_inventory/urls.py10
-rw-r--r--src/resource_inventory/views.py47
-rw-r--r--src/static/js/workflows/book-a-pod.js80
-rw-r--r--src/static/js/workflows/common-models.js7
-rw-r--r--src/static/js/workflows/design-a-pod.js51
-rw-r--r--src/static/js/workflows/workflow.js21
-rw-r--r--src/templates/base/account/settings.html4
-rw-r--r--src/templates/base/base.html2
-rw-r--r--src/templates/base/booking/booking_detail.html64
-rw-r--r--src/templates/base/booking/booking_table.html4
-rw-r--r--src/templates/base/dashboard/lab_detail.html78
-rw-r--r--src/templates/base/resource/hostprofile_detail.html60
-rw-r--r--src/templates/base/resource/hosts.html20
-rw-r--r--src/templates/base/workflow/book_a_pod.html11
-rw-r--r--src/templates/base/workflow/design_a_pod.html12
22 files changed, 435 insertions, 185 deletions
diff --git a/src/account/views.py b/src/account/views.py
index a975a2e..a392349 100644
--- a/src/account/views.py
+++ b/src/account/views.py
@@ -48,7 +48,8 @@ def account_settings_view(request):
context = {
"preference_form": AccountPreferencesForm(instance=profile),
"company_form": SetCompanyForm(initial={'company': ipa_user['ou']}),
- "existing_keys": ipa_user['ipasshpubkey'] if 'ipasshpubkey' in ipa_user else []
+ "existing_keys": ipa_user['ipasshpubkey'] if 'ipasshpubkey' in ipa_user else [],
+ "ipa_username": profile.ipa_username
}
return render(request, template, context)
diff --git a/src/api/utils.py b/src/api/utils.py
index c32205e..d6aa68a 100644
--- a/src/api/utils.py
+++ b/src/api/utils.py
@@ -96,7 +96,8 @@ def get_booking_prereqs_validator(user_profile):
return {
"form": None,
"exists": "false",
- "action": "no user"
+ "action": "no user",
+ "result": 0
}
if (not "ou" in ipa_user) or (ipa_user["ou"] == ""):
@@ -104,7 +105,8 @@ def get_booking_prereqs_validator(user_profile):
return {
"form": SetCompanyForm(),
"exists": "true",
- "action": "/api/ipa/workflow-company"
+ "action": "/api/ipa/workflow-company",
+ "result": 1
}
if (not "ipasshpubkey" in ipa_user) or (ipa_user["ipasshpubkey"] == []):
@@ -112,11 +114,13 @@ def get_booking_prereqs_validator(user_profile):
return {
"form": SetSSHForm(),
"exists": "true",
- "action": "/api/ipa/workflow-ssh"
+ "action": "/api/ipa/workflow-ssh",
+ "result": 2,
}
return {
"form": None,
"exists": "false",
- "action": ""
+ "action": "",
+ "result": -1
} \ No newline at end of file
diff --git a/src/api/views.py b/src/api/views.py
index 98dd3dc..dbe00e8 100644
--- a/src/api/views.py
+++ b/src/api/views.py
@@ -34,6 +34,7 @@ from api.forms import DowntimeForm
from account.models import UserProfile, Lab
from booking.models import Booking
from api.models import LabManagerTracker,AutomationAPIManager, APILog
+from api.utils import get_booking_prereqs_validator
import yaml
import uuid
@@ -250,18 +251,32 @@ def make_booking(request):
print("incoming data is ", data)
# todo - test this
- ipa_users = list(UserProfile.objects.get(user=request.user).ipa_username) # add owner's ipa username to list of allowed users to be sent to liblaas
+ ipa_users = []
+ ipa_users.append(UserProfile.objects.get(user=request.user).ipa_username) # add owner's ipa username to list of allowed users to be sent to liblaas
- for user in list(data["allowed_users"]):
+ for user in data["allowed_users"]:
collab_profile = UserProfile.objects.get(user=User.objects.get(username=user))
- if (collab_profile.ipa_username == "" or collab_profile.ipa_username == None):
- return JsonResponse(
- data={},
- status=406, # Not good practice but a quick solution until blob validation is fully supported within django instead of the frontend
- safe=False
- )
- else:
+ prereq_validator = get_booking_prereqs_validator(collab_profile)
+
+ if prereq_validator["result"] == -1:
+ # All good
ipa_users.append(collab_profile.ipa_username)
+ else:
+ message = "There is an issue with one of your chosen collaborators."
+ if prereq_validator["result"] == 0:
+ # No IPA username
+ message = str(collab_profile) + " has not linked their IPA account yet. Please ask them to log into the LaaS dashboard, or remove them from the booking to continue."
+ elif prereq_validator["result"] == 1:
+ # No Company
+ message = str(collab_profile) + " has not set their company yet. Please ask them to log into the LaaS dashboard, go to the settings page and add it. Otherwise, remove them from the booking to continue."
+ elif prereq_validator["result"] == 2:
+ # No SSH
+ message = str(collab_profile) + " has not added an SSH public key yet. Please ask them to log into the LaaS dashboard, go to the settings page and add it. Otherwise, remove them from the booking to continue."
+ return JsonResponse(
+ data={"message": message, "error": True},
+ status=200,
+ safe=False
+ )
bookingBlob = {
"template_id": data["template_id"],
@@ -274,7 +289,8 @@ def make_booking(request):
"purpose": data["metadata"]["purpose"],
"project": data["metadata"]["project"],
"length": int(data["metadata"]["length"])
- }
+ },
+ "origin": "anuket" if os.environ.get("TEMPLATE_OVERRIDE_DIR") == 'laas' else "lfedge"
}
print("allowed users are ", bookingBlob["allowed_users"])
@@ -296,10 +312,12 @@ def make_booking(request):
# Now create it in liblaas
bookingBlob["metadata"]["booking_id"] = str(booking.id)
- liblaas_endpoint = os.environ.get("LIBLAAS_BASE_URL") + 'booking/create'
+ liblaas_endpoint = liblaas_base_url + 'booking/create'
liblaas_response = requests.post(liblaas_endpoint, data=json.dumps(bookingBlob), headers={'Content-Type': 'application/json'})
+ print("response from liblaas is", vars(liblaas_response))
if liblaas_response.status_code != 200:
- print("received non success from liblaas")
+ print("received non success from liblaas, deleting booking from dashboard")
+ booking.delete()
return JsonResponse(
data={},
status=500,
@@ -482,7 +500,6 @@ def liblaas_request(request) -> JsonResponse:
if request.method != 'POST':
return JsonResponse({"error" : "405 Method not allowed"})
- liblaas_base_url = os.environ.get("LIBLAAS_BASE_URL")
post_data = json.loads(request.body)
print("post data is " + str(post_data))
http_method = post_data["method"]
@@ -523,13 +540,13 @@ def liblaas_request(request) -> JsonResponse:
)
def liblaas_templates(request):
- liblaas_url = os.environ.get("LIBLAAS_BASE_URL") + "template/list/" + UserProfile.objects.get(user=request.user).ipa_username
+ liblaas_url = liblaas_base_url + "template/list/" + UserProfile.objects.get(user=request.user).ipa_username
print("api call to " + liblaas_url)
return requests.get(liblaas_url)
def delete_template(request):
endpoint = json.loads(request.body)["endpoint"]
- liblaas_url = os.environ.get("LIBLAAS_BASE_URL") + endpoint
+ liblaas_url = liblaas_base_url + endpoint
print("api call to ", liblaas_url)
try:
response = requests.delete(liblaas_url)
@@ -544,9 +561,14 @@ def delete_template(request):
status=500,
safe=False
)
+
+def booking_status(request, booking_id):
+ print("booking id is", booking_id)
+ statuses = get_booking_status(Booking.objects.get(id=booking_id))
+ return HttpResponse(json.dumps(statuses))
def get_booking_status(bookingObject):
- liblaas_url = os.environ.get("LIBLAAS_BASE_URL") + "booking/" + bookingObject.aggregateId + "/status"
+ liblaas_url = liblaas_base_url + "booking/" + bookingObject.aggregateId + "/status"
print("Getting booking status at: ", liblaas_url)
response = requests.get(liblaas_url)
try:
@@ -556,7 +578,7 @@ def get_booking_status(bookingObject):
return []
def liblaas_end_booking(aggregateId):
- liblaas_url = os.environ.get('LIBLAAS_BASE_URL') + "booking/" + str(aggregateId) + "/end"
+ liblaas_url = liblaas_base_url + "booking/" + str(aggregateId) + "/end"
print("Ending booking at ", liblaas_url)
response = requests.delete(liblaas_url)
try:
@@ -646,3 +668,45 @@ def ipa_add_ssh_from_workflow(request):
key_as_list.append(request.POST["ssh_public_key"])
ipa_set_ssh(profile, key_as_list)
return redirect("workflow:book_a_pod")
+
+def list_hosts(request):
+ if request.method != "GET":
+ return HttpResponse(status=405)
+
+ dashboard = 'lfedge' if liblaas_base_url == 'lfedge' else 'anuket'
+
+ liblaas_url = os.environ.get('LIBLAAS_BASE_URL') + "flavor/hosts/" + dashboard
+ print("Listing hosts at ", liblaas_url)
+ response = requests.get(liblaas_url)
+ try:
+ return JsonResponse(
+ data = json.loads(response.content),
+ status=200,
+ safe=False
+ )
+ except Exception as e:
+ print("Failed to list hosts!", e)
+ return JsonResponse(
+ data = {},
+ status=500,
+ safe=False
+ )
+
+def list_flavors(request):
+ if (request.method) != "GET":
+ return HttpResponse(status=405)
+
+ response = requests.get(liblaas_base_url + "flavor")
+ try:
+ return JsonResponse(
+ data = json.loads(response.content),
+ status=200,
+ safe=False
+ )
+ except Exception as e:
+ print("Failed to list flavors!", e)
+ return JsonResponse(
+ data = {},
+ status=500,
+ safe=False
+ )
diff --git a/src/booking/urls.py b/src/booking/urls.py
index 9784fc5..c6b9d40 100644
--- a/src/booking/urls.py
+++ b/src/booking/urls.py
@@ -26,6 +26,7 @@ Including another URLconf
2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls'))
"""
from django.conf.urls import url
+from django.urls import path
from booking.views import (
booking_detail_view,
@@ -34,9 +35,12 @@ from booking.views import (
BookingListView,
)
+from api.views import booking_status
+
app_name = 'booking'
urlpatterns = [
url(r'^detail/(?P<booking_id>[0-9]+)/$', booking_detail_view, name='detail'),
+ url(r'^detail/(?P<booking_id>[0-9]+)/status$', booking_status, name='detail'),
url(r'^(?P<booking_id>[0-9]+)/$', booking_detail_view, name='booking_detail'),
url(r'^delete/$', BookingDeleteView.as_view(), name='delete_prefix'),
url(r'^delete/(?P<booking_id>[0-9]+)/$', BookingDeleteView.as_view(), name='delete'),
diff --git a/src/dashboard/tasks.py b/src/dashboard/tasks.py
index ac4b006..281db92 100644
--- a/src/dashboard/tasks.py
+++ b/src/dashboard/tasks.py
@@ -17,9 +17,7 @@ from api.views import liblaas_end_booking
# todo - make a task to check for expired bookings
@shared_task
def end_expired_bookings():
- print("Celery task for end_expired_bookings() has been triggered")
cleanup_set = Booking.objects.filter(end__lte=timezone.now(), ).filter(complete=False)
- print("Newly expired bookings: ", cleanup_set)
for booking in cleanup_set:
booking.complete = True
if (booking.aggregateId):
@@ -28,4 +26,3 @@ def end_expired_bookings():
else:
print("booking " + str(booking.id) + " has no agg id")
booking.save()
- print("Finished end_expired_bookings()")
diff --git a/src/dashboard/views.py b/src/dashboard/views.py
index f250a3c..909b695 100644
--- a/src/dashboard/views.py
+++ b/src/dashboard/views.py
@@ -8,7 +8,7 @@
# http://www.apache.org/licenses/LICENSE-2.0
##############################################################################
-
+import json
from django.shortcuts import get_object_or_404
from django.views.generic import TemplateView
from django.shortcuts import render
@@ -21,7 +21,7 @@ from api.utils import get_ipa_migration_form, ipa_query_user
from api.views import ipa_conflict_account
from booking.models import Booking
from dashboard.forms import *
-
+from api.views import list_flavors, list_hosts
from laas_dashboard import settings
@@ -40,14 +40,18 @@ def lab_detail_view(request, lab_name):
user = request.user
lab = get_object_or_404(Lab, name=lab_name)
-
- # images = Image.objects.filter(from_lab=lab).filter(public=True)
- images = []
- # if user:
- # images = images | Image.objects.filter(from_lab=lab).filter(owner=user)
-
- # hosts = ResourceQuery.filter(lab=lab)
- hosts = []
+ flavors_list = json.loads(list_flavors(request).content)
+ host_list = json.loads(list_hosts(request).content)
+ flavor_map = {}
+ for flavor in flavors_list:
+ flavor_map[flavor['flavor_id']] = flavor['name']
+
+
+ # Apparently Django Templating lacks many features that regular Jinja offers, so I need to get creative
+ for host in host_list:
+ id = host["flavor"]
+ name = flavor_map[id]
+ host["flavor"] = {"id": id, "name": name}
return render(
request,
@@ -56,8 +60,8 @@ def lab_detail_view(request, lab_name):
'title': "Lab Overview",
'lab': lab,
# 'hostprofiles': ResourceProfile.objects.filter(labs=lab),
- 'images': images,
- 'hosts': hosts
+ 'flavors': flavors_list,
+ 'hosts': host_list
}
)
diff --git a/src/laas_dashboard/urls.py b/src/laas_dashboard/urls.py
index f78e9ff..a7b29fb 100644
--- a/src/laas_dashboard/urls.py
+++ b/src/laas_dashboard/urls.py
@@ -41,6 +41,7 @@ urlpatterns = [
url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')),
url(r'^api/', include('api.urls')),
url(r'^oidc/', include('mozilla_django_oidc.urls')),
+ url(r'^resource/', include('resource_inventory.urls', namespace='resource')),
]
if settings.DEBUG is True:
diff --git a/src/resource_inventory/urls.py b/src/resource_inventory/urls.py
index f9bd07e..f230509 100644
--- a/src/resource_inventory/urls.py
+++ b/src/resource_inventory/urls.py
@@ -7,3 +7,13 @@
# which accompanies this distribution, and is available at
# http://www.apache.org/licenses/LICENSE-2.0
##############################################################################
+
+from django.conf.urls import url
+from django.urls import path
+
+from resource_inventory.views import host_list_view, profile_view
+app_name = 'resource'
+urlpatterns = [
+ url(r'^list/$', host_list_view, name='host-list'),
+ path('profile/<str:resource_id>', profile_view),
+] \ No newline at end of file
diff --git a/src/resource_inventory/views.py b/src/resource_inventory/views.py
index f903394..837cc73 100644
--- a/src/resource_inventory/views.py
+++ b/src/resource_inventory/views.py
@@ -6,3 +6,50 @@
# which accompanies this distribution, and is available at
# http://www.apache.org/licenses/LICENSE-2.0
##############################################################################
+
+import json
+from django.shortcuts import render
+from django.http import HttpResponse
+from api.views import list_hosts, list_flavors
+
+
+def host_list_view(request):
+ if request.method == "GET":
+ host_list = json.loads(list_hosts(request).content)
+ flavor_list = json.loads(list_flavors(request).content)
+ flavor_map = {}
+ for flavor in flavor_list:
+ flavor_map[flavor['flavor_id']] = flavor['name']
+
+ # Apparently Django Templating lacks many features that regular Jinja offers, so I need to get creative
+ for host in host_list:
+ id = host["flavor"]
+ name = flavor_map[id]
+ host["flavor"] = {"id": id, "name": name}
+
+ template = "resource/hosts.html"
+ context = {
+ "hosts": host_list,
+ "flavor_map": flavor_map
+ }
+ return render(request, template, context)
+
+ return HttpResponse(status=405)
+
+
+def profile_view(request, resource_id):
+ if request.method == "GET":
+ flavor_list = json.loads(list_flavors(request).content)
+ selected_flavor = {}
+ for flavor in flavor_list:
+ if flavor["flavor_id"] == resource_id:
+ selected_flavor = flavor
+ break
+
+ template = "resource/hostprofile_detail.html"
+ context = {
+ "flavor": selected_flavor
+ }
+ return render(request, template, context)
+
+ return HttpResponse(status=405) \ No newline at end of file
diff --git a/src/static/js/workflows/book-a-pod.js b/src/static/js/workflows/book-a-pod.js
index 3f83849..ddea556 100644
--- a/src/static/js/workflows/book-a-pod.js
+++ b/src/static/js/workflows/book-a-pod.js
@@ -6,13 +6,13 @@ const steps = {
SELECT_TEMPLATE: 0,
CLOUD_INIT: 1,
BOOKING_DETAILS: 2,
- ADD_COLLABS: 3,
- BOOKING_SUMMARY: 4
+ ADD_COLLABS: 2,
+ BOOKING_SUMMARY: 3
}
class BookingWorkflow extends Workflow {
constructor(savedBookingBlob) {
- super(["select_template", "cloud_init", "booking_details" ,"add_collabs", "booking_summary"])
+ super(["select_template", "cloud_init", "booking_details" , "booking_summary"])
// if (savedBookingBlob) {
// this.resume_workflow()
@@ -24,7 +24,12 @@ const steps = {
async startWorkflow() {
this.userTemplates = await LibLaaSAPI.getTemplatesForUser() // List<TemplateBlob>
- GUI.displayTemplates(this.userTemplates);
+ const flavorsList = await LibLaaSAPI.getLabFlavors("UNH_IOL")
+ this.labFlavors = new Map(); // Map<UUID, FlavorBlob>
+ for (const fblob of flavorsList) {
+ this.labFlavors.set(fblob.flavor_id, fblob);
+ }
+ GUI.displayTemplates(this.userTemplates, this.labFlavors);
GUI.modifyCollabWidget();
this.setEventListeners();
document.getElementById(this.sections[0]).scrollIntoView({behavior: 'smooth'});
@@ -240,28 +245,35 @@ const steps = {
/** Async / await is more infectious than I thought, so all functions that rely on an API call will need to be async */
async onclickConfirm() {
+ // disable button
+ const button = document.getElementById("booking-confirm-button");
+ $("html").css("cursor", "wait");
+ button.setAttribute('disabled', 'true');
const complete = this.isCompleteBookingInfo();
if (!complete[0]) {
- showError(complete[1]);
- this.step = complete[2]
- document.getElementById(this.sections[complete[2]]).scrollIntoView({behavior: 'smooth'});
+ showError(complete[1], complete[2]);
+ $("html").css("cursor", "default");
+ button.removeAttribute('disabled');
return
}
const response = await LibLaaSAPI.makeBooking(this.bookingBlob);
+ if (!response) {
+ showError("The selected resources for this booking are unavailable at this time. Please select a different resource or try again later.", -1)
+ }
if (response.bookingId) {
- showError("The booking has been successfully created.")
- window.location.href = "../../";
+ showError("The booking has been successfully created.", -1)
+ window.location.href = "../../booking/detail/" + response.bookingId + "/"; // todo
+ return;
} else {
- if (response.status == 406) {
- showError("One or more collaborators is missing SSH keys or has not configured their IPA account.")
+ if (response.error == true) {
+ showError(response.message, -1)
} else {
- showError("The booking could not be created at this time.")
+ showError("The booking could not be created at this time.", -1)
}
}
- // if (confirm("Are you sure you would like to create this booking?")) {
-
- // }
+ $("html").css("cursor", "default");
+ button.removeAttribute('disabled');
}
}
@@ -288,16 +300,45 @@ class GUI {
}
/** Takes a list of templateBlobs and creates a selectable card for each of them */
- static displayTemplates(templates) {
+ static displayTemplates(templates, flavor_map) {
const templates_list = document.getElementById("default_templates_list");
for (const t of templates) {
- const newCard = this.makeTemplateCard(t);
+ const newCard = this.makeTemplateCard(t, this.calculateAvailability(t, flavor_map));
templates_list.appendChild(newCard);
}
}
- static makeTemplateCard(templateBlob) {
+ static calculateAvailability(templateBlob, flavor_map) {
+ const local_map = new Map()
+
+ // Map flavor uuid to amount in template
+ for (const host of templateBlob.host_list) {
+ const existing_count = local_map.get(host.flavor)
+ if (existing_count) {
+ local_map.set(host.flavor, existing_count + 1)
+ } else {
+ local_map.set(host.flavor, 1)
+ }
+ }
+
+ let lowest_count = Number.POSITIVE_INFINITY;
+ for (const [key, val] of local_map) {
+ const curr_count = Math.floor(flavor_map.get(key).available_count / val)
+ if (curr_count < lowest_count) {
+ lowest_count = curr_count;
+ }
+ }
+
+ return lowest_count;
+ }
+
+ static makeTemplateCard(templateBlob, available_count) {
+ const isAvailable = available_count > 0;
+ let availability_text = isAvailable ? 'Resources Available' : 'Resources Unavailable';
+ let color = isAvailable ? 'text-success' : 'text-danger';
+ let disabled = !isAvailable ? 'disabled = "true"' : '';
+
const col = document.createElement('div');
col.classList.add('col-3', 'my-1');
col.innerHTML= `
@@ -307,9 +348,10 @@ class GUI {
</div>
<div class="card-body">
<p class="grid-item-description">` + templateBlob.pod_desc +`</p>
+ <p class="grid-item-description ` + color + `">` + availability_text + `</p>
</div>
<div class="card-footer">
- <button type="button" class="btn btn-success grid-item-select-btn w-100 stretched-link"
+ <button type="button"` + disabled + ` class="btn btn-success grid-item-select-btn w-100 stretched-link"
onclick="workflow.onclickSelectTemplate(this.parentNode.parentNode, '` + templateBlob.id +`')">Select</button>
</div>
</div>
diff --git a/src/static/js/workflows/common-models.js b/src/static/js/workflows/common-models.js
index 65fedb1..dc479c2 100644
--- a/src/static/js/workflows/common-models.js
+++ b/src/static/js/workflows/common-models.js
@@ -169,11 +169,16 @@ class FlavorBlob {
this.flavor_id = incomingBlob.flavor_id; // UUID (String)
this.name = incomingBlob.name; // String
this.interfaces = []; // List<String>
- // images are added after
+ this.images = []; // List<ImageBlob>
+ this.available_count = incomingBlob.available_count;
if (incomingBlob.interfaces) {
this.interfaces = incomingBlob.interfaces;
}
+
+ if (incomingBlob.images) {
+ this.images = incomingBlob.images;
+ }
}
}
diff --git a/src/static/js/workflows/design-a-pod.js b/src/static/js/workflows/design-a-pod.js
index 58f8b85..2083f7d 100644
--- a/src/static/js/workflows/design-a-pod.js
+++ b/src/static/js/workflows/design-a-pod.js
@@ -89,7 +89,6 @@ class DesignWorkflow extends Workflow {
this.labImages = new Map(); // Map<UUID, ImageBlob>
for (const fblob of flavorsList) {
- fblob.images = await LibLaaSAPI.getImagesForFlavor(fblob.flavor_id);
for (const iblob of fblob.images) {
this.labImages.set(iblob.image_id, iblob)
}
@@ -110,18 +109,17 @@ class DesignWorkflow extends Workflow {
this.step = steps.ADD_RESOURCES;
if (this.templateBlob.lab_name == null) {
- showError("Please select a lab before adding resources.");
- this.goTo(steps.SELECT_LAB);
+ showError("Please select a lab before adding resources.", steps.SELECT_LAB);
return;
}
if (this.templateBlob.host_list.length >= 8) {
- showError("You may not add more than 8 hosts to a single pod.")
+ showError("You may not add more than 8 hosts to a single pod.", -1)
return;
}
this.resourceBuilder = null;
- GUI.refreshAddHostModal(this.userTemplates);
+ GUI.refreshAddHostModal(this.userTemplates, this.labFlavors);
$("#resource_modal").modal('toggle');
}
@@ -248,6 +246,7 @@ class DesignWorkflow extends Workflow {
for (const [index, host] of this.resourceBuilder.user_configs.entries()) {
const new_host = new HostConfigBlob(host);
this.templateBlob.host_list.push(new_host);
+ this.labFlavors.get(host.flavor).available_count--
}
// Add networks
@@ -274,6 +273,7 @@ class DesignWorkflow extends Workflow {
for (let existing_host of this.templateBlob.host_list) {
if (hostname == existing_host.hostname) {
this.removeHostFromTemplateBlob(existing_host);
+ this.labFlavors.get(existing_host.flavor).available_count++;
GUI.refreshHostStep(this.templateBlob.host_list, this.labFlavors, this.labImages);
GUI.refreshNetworkStep(this.templateBlob.networks);
GUI.refreshConnectionStep(this.templateBlob.host_list);
@@ -282,7 +282,7 @@ class DesignWorkflow extends Workflow {
}
}
- showError("didnt remove");
+ showError("didnt remove", -1);
}
@@ -297,8 +297,7 @@ class DesignWorkflow extends Workflow {
this.step = steps.ADD_NETWORKS;
if (this.templateBlob.lab_name == null) {
- showError("Please select a lab before adding networks.");
- this.goTo(steps.SELECT_LAB);
+ showError("Please select a lab before adding networks.", steps.SELECT_LAB);
return;
}
@@ -561,8 +560,7 @@ class DesignWorkflow extends Workflow {
this.step = steps.POD_SUMMARY;
const simpleValidation = this.simpleStepValidation();
if (!simpleValidation[0]) {
- showError(simpleValidation[1])
- this.goTo(simpleValidation[2]);
+ showError(simpleValidation[1], simpleValidation[2])
return;
}
@@ -632,7 +630,7 @@ class GUI {
/** Resets the host modal inner html
* Takes a list of templateBlobs
*/
- static refreshAddHostModal(template_list) {
+ static refreshAddHostModal(template_list, flavor_map) {
document.getElementById('add_resource_modal_body').innerHTML = `
<h2>Resource</h2>
<div id="template-cards" class="row align-items-center justify-content-start">
@@ -663,13 +661,39 @@ class GUI {
const template_cards = document.getElementById('template-cards');
for (let template of template_list) {
- template_cards.appendChild(this.makeTemplateCard(template));
+ template_cards.appendChild(this.makeTemplateCard(template, this.calculateAvailability(template, flavor_map)));
}
}
+ static calculateAvailability(templateBlob, flavor_map) {
+ const local_map = new Map()
+
+ // Map flavor uuid to amount in template
+ for (const host of templateBlob.host_list) {
+ const existing_count = local_map.get(host.flavor)
+ if (existing_count) {
+ local_map.set(host.flavor, existing_count + 1)
+ } else {
+ local_map.set(host.flavor, 1)
+ }
+ }
+
+ let lowest_count = Number.POSITIVE_INFINITY;
+ for (const [key, val] of local_map) {
+ const curr_count = Math.floor(flavor_map.get(key).available_count / val)
+ if (curr_count < lowest_count) {
+ lowest_count = curr_count;
+ }
+ }
+
+ return lowest_count;
+ }
+
/** Makes a card to be displayed in the add resource modal for a given templateBlob */
- static makeTemplateCard(templateBlob) {
+ static makeTemplateCard(templateBlob, available_count) {
+ let color = available_count > 0 ? 'text-success' : 'text-danger';
+ // let disabled = available_count == 0 ? 'disabled = "true"' : '';
const col = document.createElement('div');
col.classList.add('col-12', 'col-md-6', 'col-xl-3', 'my-3');
col.innerHTML= `
@@ -679,6 +703,7 @@ class GUI {
</div>
<div class="card-body">
<p class="grid-item-description">` + templateBlob.pod_desc +`</p>
+ <p class="grid-item-description ` + color + `">Resources available:` + available_count +`</p>
</div>
<div class="card-footer">
<button type="button" class="btn btn-success grid-item-select-btn w-100 stretched-link"
diff --git a/src/static/js/workflows/workflow.js b/src/static/js/workflows/workflow.js
index f3f39e9..97bf8f4 100644
--- a/src/static/js/workflows/workflow.js
+++ b/src/static/js/workflows/workflow.js
@@ -29,7 +29,6 @@ class LibLaaSAPI {
/** POSTs to dashboard, which then auths and logs the requests, makes the request to LibLaaS, and passes the result back to here.
Treat this as a private function. Only use the async functions when outside of this class */
static makeRequest(method, endpoint, workflow_data) {
- console.log("Making request: %s, %s, %s", method, endpoint, workflow_data.toString())
const token = document.getElementsByName('csrfmiddlewaretoken')[0].value
return new Promise((resolve, reject) => {// -> HttpResponse
$.ajax(
@@ -130,7 +129,6 @@ class LibLaaSAPI {
static async makeTemplate(templateBlob) { // -> UUID or null
templateBlob.owner = user;
- console.log(JSON.stringify(templateBlob))
return await this.handleResponse(this.makeRequest(HTTP.POST, endpoint.MAKE_TEMPLATE, templateBlob));
}
@@ -230,12 +228,13 @@ class Workflow {
}
goTo(step_number) {
- while (step_number > this.step) {
- this.goNext();
- }
-
- while (step_number < this.step) {
- this.goPrev();
+ if (step_number < 0) return;
+ this.step = step_number
+ document.getElementById(this.sections[this.step]).scrollIntoView({behavior: 'smooth'});
+ if (this.step == this.sections.length - 1) {
+ document.getElementById('next').setAttribute('disabled', '');
+ } else if (this.step == 1) {
+ document.getElementById('prev').removeAttribute('disabled');
}
}
@@ -245,9 +244,11 @@ function apiError(info) {
showError("Unable to fetch " + info +". Please try again later or contact support.")
}
-function showError(message) {
+// global variable needed for a scrollintoview bug affecting chrome
+let alert_destination = -1;
+function showError(message, destination) {
+ alert_destination = destination;
const text = document.getElementById('alert_modal_message');
-
text.innerText = message;
$("#alert_modal").modal('show');
}
diff --git a/src/templates/base/account/settings.html b/src/templates/base/account/settings.html
index d1939b7..611ae38 100644
--- a/src/templates/base/account/settings.html
+++ b/src/templates/base/account/settings.html
@@ -7,6 +7,9 @@
{% csrf_token %}
<input id="hidden_key_list" type="hidden" name="ssh_key_list" value="">
<div class="form-group">
+ <label>Your IPA Username</label>
+ <input type="text" class="form-control" disabled="true" style="width: 300px;" value="{{ ipa_username }}">
+ <p>To change your password and access advanced account management, please go <a href="http://os-passwd.iol.unh.edu">here</a></p>
{{ company_form }}
{{ preference_form }}
<br>
@@ -42,7 +45,6 @@ $(window).on('load', function() {
key_list.push('{{key}}')
{% endfor %}
update_json_list()
- console.log(key_list)
});
diff --git a/src/templates/base/base.html b/src/templates/base/base.html
index cd4269c..9f1a598 100644
--- a/src/templates/base/base.html
+++ b/src/templates/base/base.html
@@ -138,7 +138,7 @@
Lab Info <i class="fas fa-angle-down rotate"></i>
</a>
<div class="collapse" id="labInfo">
- <a href="" class="list-group-item list-group-item-action nav-bg">
+ <a href="{% url 'resource:host-list' %}" class="list-group-item list-group-item-action nav-bg">
Hosts
</a>
<a href="{% url 'booking:list' %}" class="list-group-item list-group-item-action nav-bg">
diff --git a/src/templates/base/booking/booking_detail.html b/src/templates/base/booking/booking_detail.html
index e4687ad..33b0486 100644
--- a/src/templates/base/booking/booking_detail.html
+++ b/src/templates/base/booking/booking_detail.html
@@ -61,8 +61,7 @@ code {
<div class="card mb-3">
<div class="card-header d-flex">
<h4 class="d-inline">Deployment Progress</h4>
- <p>These are the different tasks that have to be completed before your deployment is ready.
- If this is taking a really long time, let us know <a href="mailto:{{contact_email}}">here!</a></p>
+ <p class="mx-3">Your resources are being prepared. If this is taking a really long time, please contact us <a href="mailto:{{contact_email}}">here!</a></p>
<!-- <button data-toggle="collapse" data-target="#panel_tasks" class="btn btn-outline-secondary ml-auto">Expand</button> -->
</div>
<div class="collapse show" id="panel_tasks">
@@ -75,23 +74,18 @@ code {
{% for host in statuses %}
<tr>
<td>
- <!-- Success,
- Reimage,
- InProgress,
- Failure,
- Import, -->
- {% if host.status is 'Success' %}
+ {% if 'Success' in host.status %}
<div class="rounded-circle bg-success square-20"></div>
- {% elif host.status is 'InProgress' %}
- <div class="spinner-border text-primary square-20"></div>
+ {% elif 'Fail' in host.status %}
+ <div class="rounded-circle bg-danger square-20"></div>
{% else %}
- <div class="rounded-circle bg-secondary square-20"></div>
+ <div class="spinner-border text-primary square-20"></div>
{% endif %}
</td>
<td>
{{ host.hostname }}
</td>
- <td>
+ <td id="{{host.instance_id}}">
{{ host.status }}
</td>
</tr>
@@ -102,4 +96,50 @@ code {
</div>
</div>
+<div class="row">
+ <div class="col-5">
+ <div class="card mb-3">
+ <div class="card-header d-flex">
+ Diagnostic Information
+ </div>
+ <div>
+ <table class="table m-0">
+ <tr>
+ <th></th>
+ <th>Aggregate ID</th>
+ <td>{{booking.aggregateId}}</td>
+ </tr>
+ </table>
+ </div>
+ </div>
+ </div>
+</div>
+
+<script>
+setInterval(function(){
+ fetchBookingStatus();
+}, 5000);
+
+
+async function fetchBookingStatus() {
+ req = new XMLHttpRequest();
+ var url = "status";
+ req.open("GET", url, true);
+ req.onerror = function() { alert("oops"); }
+ req.onreadystatechange = function() {
+ if(req.readyState === 4) {
+ statuses = JSON.parse(req.responseText)
+ updateStatuses(statuses)
+ }
+ }
+ req.send();
+}
+
+async function updateStatuses(statuses) {
+ for (const s of statuses) {
+ document.getElementById(s.instance_id).innerText = s.status
+ }
+}
+</script>
+
{% endblock content %}
diff --git a/src/templates/base/booking/booking_table.html b/src/templates/base/booking/booking_table.html
index b4a713a..c7743ca 100644
--- a/src/templates/base/booking/booking_table.html
+++ b/src/templates/base/booking/booking_table.html
@@ -8,7 +8,6 @@
<th>Project</th>
<th>Start</th>
<th>End</th>
- <th>Operating System</th>
</tr>
</thead>
<tbody>
@@ -29,9 +28,6 @@
<td>
{{ booking.end }}
</td>
- <td>
- {{ booking.resource.get_head_node.config.image.os.name }}
- </td>
</tr>
{% endfor %}
</tbody>
diff --git a/src/templates/base/dashboard/lab_detail.html b/src/templates/base/dashboard/lab_detail.html
index 3d90a51..cd096f6 100644
--- a/src/templates/base/dashboard/lab_detail.html
+++ b/src/templates/base/dashboard/lab_detail.html
@@ -12,7 +12,6 @@
<div class="card mb-3">
<div class="card-header d-flex">
<h4>Lab Profile</h4>
- <button class="btn btn-outline-secondary ml-auto" data-toggle="collapse" data-target="#panel_overview">Expand</button>
</div>
<div class="collapse show" id="panel_overview">
<div class="overflow-auto">
@@ -61,16 +60,15 @@
<div class="card my-3">
<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">Expand</button>
</div>
<div class="collapse show" id="profile_panel">
<div class="overflow-auto">
<table class="table m-0">
- {% for profile in hostprofiles %}
+ {% for flavor in flavors %}
<tr>
- <td>{{profile.name}}</td>
- <td>{{profile.description}}</td>
- <td><a href="/resource/profiles/{{ profile.id }}" class="btn btn-info">Profile</a></td>
+ <td>{{flavor.name}}</td>
+ <td>{{flavor.description}}</td>
+ <td><a href="/resource/profile/{{ flavor.flavor_id }}" class="btn btn-info">Profile</a></td>
</tr>
{% endfor %}
</table>
@@ -81,7 +79,6 @@
<div class="card my-3">
<div class="card-header d-flex">
<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="collapse show" id="network_panel">
@@ -96,63 +93,50 @@
</table>
</div>
</div>
- <div class="card my-3">
- <div class="card-header d-flex">
- <h4>Images</h4>
- <button data-toggle="collapse" data-target="#image_panel" class="btn btn-outline-secondary ml-auto">Expand</button>
- </div>
- <div class="collapse show" id="image_panel">
- <div class="overflow-auto">
- <table class="table m-0">
- <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>
</div>
<div class="col-lg-8">
<div class="card mb-3">
<div class="card-header d-flex">
<h4>Lab Hosts</h4>
- <button data-toggle="collapse" data-target="#lab_hosts_panel" class="btn btn-outline-secondary ml-auto">Expand</button>
</div>
<div class="collapse show" id="lab_hosts_panel">
<table class="table m-0">
<tr>
<th>Name</th>
+ <th>Architecture</th>
<th>Profile</th>
<th>Booked</th>
<th>Working</th>
- <th>Vendor</th>
</tr>
{% for host in hosts %}
- <tr>
- <td>{{host.name}}</td>
- <td>{{host.profile}}</td>
- <td>{{host.booked|yesno:"Yes,No"}}</td>
- {% if host.working %}
- <td class="bg-success text-white">Yes</td>
- {% else %}
- <td class="bg-danger text-white">No</td>
- {% endif %}
- <td>{{host.vendor}}</td>
- </tr>
- {% endfor %}
+ <tr>
+ <td>
+ {{ host.name }}
+ </td>
+ <td>
+ {{ host.arch }}
+ </td>
+ <td>
+ <a href="../../resource/profile/{{ host.flavor.id }}">{{ host.flavor.name }}</a>
+ </td>
+ <td>
+ {% if host.allocation != null %}
+ Yes
+ {% else %}
+ No
+ {% endif %}
+ </td>
+ <td>
+ {% if host.allocation.reason == "maintenance" %}
+ No
+ {% else %}
+ Yes
+ {% endif %}
+ </td>
+ </tr>
+ {% endfor %}
</table>
</div>
</div>
diff --git a/src/templates/base/resource/hostprofile_detail.html b/src/templates/base/resource/hostprofile_detail.html
index 0b3262c..1cf2000 100644
--- a/src/templates/base/resource/hostprofile_detail.html
+++ b/src/templates/base/resource/hostprofile_detail.html
@@ -2,51 +2,46 @@
{% load staticfiles %}
{% block content %}
+<h1>{{ flavor.name }}</h1>
<div class="row">
<div class="col-lg-6">
<div class="card mb-4">
<div class="card-header d-flex">
<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="collapse show" id="availableAt">
<ul class="list-group list-group-flush">
- {% for lab in hostprofile.labs.all %}
- <li class="list-group-item">{{lab.name}}</li>
- {% endfor %}
+ <li class="list-group-item">UNH IOL</li>
</ul>
</div>
</div>
<div class="card mb-4">
<div class="card-header d-flex">
<h4 class="d-inline">RAM</h4>
- <button data-toggle="collapse" data-target="#ramPanel" class="btn ml-auto btn-outline-secondary">Expand</button>
</div>
<div id="ramPanel" class="collapse show">
<div class="card-body">
- {{hostprofile.ramprofile.first.amount}}G,
- {{hostprofile.ramprofile.first.channels}} channels
+ {{flavor.ram.value}} {{flavor.ram.unit}}
</div>
</div>
</div>
<div class="card mb-4">
<div class="card-header d-flex">
<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="collapse show" id="cpuPanel">
<table class="table">
<tr>
<td>Arch:</td>
- <td>{{hostprofile.cpuprofile.first.architecture}}</td>
+ <td>{{ flavor.arch }}</td>
</tr>
<tr>
<td>Cores:</td>
- <td>{{hostprofile.cpuprofile.first.cores}}</td>
+ <td>{{ flavor.cpu_count }}</td>
</tr>
<tr>
<td>Sockets:</td>
- <td>{{hostprofile.cpuprofile.first.cpus}}</td>
+ <td>{{ flavor.sockets }}</td>
</tr>
</table>
</div>
@@ -54,31 +49,29 @@
<div class="card mb-4">
<div class="card-header d-flex">
<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="collapse show" id="diskPanel">
<table class="table">
<tr>
- <td>Size:</td>
- <td>{{hostprofile.storageprofile.first.size}} GiB</td>
+ <td>Disk Size:</td>
+ <td>{{flavor.disk_size.value}} {{flavor.disk_size.unit}}</td>
</tr>
<tr>
- <td>Type:</td>
- <td>{{hostprofile.storageprofile.first.media_type}}</td>
+ <td>Root Size:</td>
+ <td>{{flavor.root_size.value}} {{flavor.root_size.unit}}</td>
</tr>
<tr>
- <td>Mount Point:</td>
- <td>{{hostprofile.storageprofile.first.name}}</td>
+ <td>Swap Size:</td>
+ <td>{{flavor.swap_size.value}} {{flavor.swap_size.unit}}</td>
</tr>
</table>
</div>
</div>
</div>
<div class="col-lg-6">
- <div class="card">
+ <div class="card mb-4">
<div class="card-header d-flex">
<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="collapse show" id="interfacePanel">
<table class="table">
@@ -89,10 +82,31 @@
</tr>
</thead>
<tbody>
- {% for intprof in hostprofile.interfaceprofile.all %}
+ {% for interface in flavor.interfaces %}
+ <tr>
+ <td>{{interface.name}}</td>
+ <td>{{interface.speed.value}} {{interface.speed.unit}}</td>
+ </tr>
+ {% endfor %}
+ </tbody>
+ </table>
+ </div>
+ </div>
+ <div class="card">
+ <div class="card-header d-flex">
+ <h4 class="d-inline">Images</h4>
+ </div>
+ <div class="collapse show" id="interfacePanel">
+ <table class="table">
+ <thead>
+ <tr>
+ <th>Name</th>
+ </tr>
+ </thead>
+ <tbody>
+ {% for image in flavor.images %}
<tr>
- <td>{{intprof.name}}</td>
- <td>{{intprof.speed}}</td>
+ <td>{{image.name}}</td>
</tr>
{% endfor %}
</tbody>
diff --git a/src/templates/base/resource/hosts.html b/src/templates/base/resource/hosts.html
index 9fc50ce..e01916a 100644
--- a/src/templates/base/resource/hosts.html
+++ b/src/templates/base/resource/hosts.html
@@ -5,25 +5,37 @@
<thead>
<tr>
<th>Name</th>
+ <th>Architecture</th>
<th>Profile</th>
<th>Booked</th>
<th>Working</th>
</tr>
</thead>
<tbody>
- {% for host in hosts %}
+ {% for host in hosts %}
<tr>
<td>
{{ host.name }}
</td>
<td>
- <a href="profiles/{{ host.profile.id }}">{{ host.profile }}</a>
+ {{ host.arch }}
</td>
<td>
- {{ host.booked|yesno:"Yes,No" }}
+ <a href="../profile/{{ host.flavor.id }}">{{ host.flavor.name }}</a>
</td>
<td>
- {{ host.working|yesno:"Yes,No" }}
+ {% if host.allocation != null %}
+ Yes
+ {% else %}
+ No
+ {% endif %}
+ </td>
+ <td>
+ {% if host.allocation.reason == "maintenance" %}
+ No
+ {% else %}
+ Yes
+ {% endif %}
</td>
</tr>
{% endfor %}
diff --git a/src/templates/base/workflow/book_a_pod.html b/src/templates/base/workflow/book_a_pod.html
index 7448dc5..8a0fb47 100644
--- a/src/templates/base/workflow/book_a_pod.html
+++ b/src/templates/base/workflow/book_a_pod.html
@@ -24,7 +24,7 @@
<div class="scroll-container w-100 h-100 p-0">
<div class="scroll-area pt-5 mx-5" id="select_template">
<h1 class="mt-4"><u>Book a Pod</u></h1>
- <h2 class="mt-4 mb-3">Select Host Or Template:</h2>
+ <h2 class="mt-4 mb-3">Select Host Or Template<span class="text-danger">*</span></h2>
<div class="card-deck align-items-center">
<div class="col-12" id="template_list">
@@ -46,7 +46,7 @@
</div>
<div class="scroll-area pt-5 mx-5" id="booking_details">
- <h2 class="mt-4 mb-3">Booking Details</h2>
+ <h2 class="mt-4 mb-3">Booking Details<span class="text-danger">*</span></h2>
<div class="form-group mb-0">
<div class="row align-items-center my-4">
<div class="col-xl-6 col-md-8 col-11">
@@ -66,9 +66,6 @@
<input id="input_length" type="range" min="1" max="21" value="1" class="form-control form-control-lg col-xl-5 col-9 p-0" placeholder="Length" oninput="workflow.onchangeDays()">
</div>
</div>
- </div>
-
- <div class="scroll-area pt-5 mx-5" id="add_collabs">
<h2 class="mt-4 mb-3">Add Collaborators:</h2>
<div class="row">
<div class="col-xl-6 col-md-8 col-11 p-0 border border-dark">
@@ -99,7 +96,7 @@
</div>
<div class="row align-items-center mt-5">
<!-- <button class="btn btn-danger cancel-book-button p-0 mr-2 col-xl-2 col-md-3 col-5" onclick="workflow.onclickCancel()">Cancel</button> -->
- <button class="btn btn-success cancel-book-button p-0 ml-2 col-xl-2 col-md-3 col-5" onclick="workflow.onclickConfirm()">Book</button>
+ <button id="booking-confirm-button" class="btn btn-success cancel-book-button p-0 ml-2 col-xl-2 col-md-3 col-5" onclick="workflow.onclickConfirm()">Book</button>
</div>
</div>
@@ -144,7 +141,7 @@
<h5 id="alert_modal_message"></h5>
</div>
<div class="modal-footer d-flex justify-content-center">
- <button class="btn btn-success" data-dismiss="modal" id="alert-modal-submit" onclick="">Confirm</button>
+ <button class="btn btn-success" data-dismiss="modal" id="alert-modal-submit" onclick="workflow.goTo(alert_destination)">Confirm</button>
</div>
</div>
</div>
diff --git a/src/templates/base/workflow/design_a_pod.html b/src/templates/base/workflow/design_a_pod.html
index ab3f11b..32bd332 100644
--- a/src/templates/base/workflow/design_a_pod.html
+++ b/src/templates/base/workflow/design_a_pod.html
@@ -31,14 +31,14 @@
<div class="scroll-area pt-5 mx-5" id="select_lab">
<!-- Ideally the 'Design a Pod' header would be anchored to the top of the page below the arrow -->
<h1 class="mt-4"><u>Design a Pod</u></h1>
- <h2 class="mt-4 mb-3">Select a Lab:</h2>
+ <h2 class="mt-4 mb-3">Select a Lab<span class="text-danger">*</span></h2>
<div class="row card-deck" id="lab_cards">
</div>
</div>
<!-- Add Resources -->
<div class="scroll-area pt-5 mx-5" id="add_resources">
- <h2 class="mt-4 mb-3">Add Resources:</h2>
+ <h2 class="mt-4 mb-3">Add Resources<span class="text-danger">*</span></h2>
<div class="row card-deck align-items-center" id="host_cards">
<div class="col-xl-3 col-md-6 col-12" id="add_resource_plus_card">
<div class="card align-items-center border-0">
@@ -51,7 +51,7 @@
<!-- Add Networks -->
<div class="scroll-area pt-5 mx-5" id="add_networks">
- <h2 class="mt-4 mb-3">Add Networks:</h2>
+ <h2 class="mt-4 mb-3">Add Networks<span class="text-danger">*</span></h2>
<div class="row card-deck align-items-center" id="network_card_deck">
<div class="col-xl-3 col-md-6 col-12" id="add_network_plus_card">
<div class="card align-items-center border-0">
@@ -64,14 +64,14 @@
<!-- Configure Connections-->
<div class="scroll-area pt-5 mx-5" id="configure_connections">
- <h2 class="mt-4 mb-3">Configure Connections:</h2>
+ <h2 class="mt-4 mb-3">Configure Connections<span class="text-danger">*</span></h2>
<div class="row card-deck align-items-center" id="connection_cards">
</div>
</div>
<!-- Pod Details-->
<div class="scroll-area pt-5 mx-5" id="pod_details">
- <h2 class="mt-4 mb-3">Pod Details</h2>
+ <h2 class="mt-4 mb-3">Pod Details<span class="text-danger">*</span></h2>
<div class="form-group">
<div class="row align-items-center my-4">
<div class="col-xl-6 col-md-8 col-11">
@@ -208,7 +208,7 @@
<h5 id="alert_modal_message"></h5>
</div>
<div class="modal-footer d-flex justify-content-center">
- <button class="btn btn-success" data-dismiss="modal" id="alert-modal-submit" onclick="">Confirm</button>
+ <button class="btn btn-success" data-dismiss="modal" id="alert-modal-submit" onclick="workflow.goTo(alert_destination)">Confirm</button>
</div>
</div>
</div>