diff --git a/static/images/workshop/login_01.png b/static/images/workshop/login_01.png new file mode 100644 index 0000000000000000000000000000000000000000..77452370e8ce430fd2170911d4f59b32263ca695 Binary files /dev/null and b/static/images/workshop/login_01.png differ diff --git a/static/images/workshop/partition_01.png b/static/images/workshop/partition_01.png new file mode 100644 index 0000000000000000000000000000000000000000..73385950db63ade4573a2bfc334462f28606fc95 Binary files /dev/null and b/static/images/workshop/partition_01.png differ diff --git a/static/images/workshop/project_01.png b/static/images/workshop/project_01.png new file mode 100644 index 0000000000000000000000000000000000000000..1cb770ae5b6f90ca54024eb608f56c3d602fe69d Binary files /dev/null and b/static/images/workshop/project_01.png differ diff --git a/static/js/home/dropdown-options.js b/static/js/home/dropdown-options.js deleted file mode 100644 index eb50a409c2ad364e1782c2b9744e6031fb4d5811..0000000000000000000000000000000000000000 --- a/static/js/home/dropdown-options.js +++ /dev/null @@ -1,586 +0,0 @@ -/* -* This module takes care of updating the user options, which are received from the backend and shown on the UI -*/ -define(["jquery", "home/utils"], function ( - $, - utils -) { - "use strict"; - - var updateServices = function (software, id, value=false) { - const serviceInfo = getServiceInfo(); - - let select = $(`select#${id}-version-select`); - const currentVal = select.val(); - resetInputElement(select); - - const options = (serviceInfo[software].options || {}); - for (const [serviceName, serviceInfo] of Object.entries(options)) { - var displayName = serviceInfo.name; - select.append(`<option value="${serviceName}">${displayName}</option>`); - } - if (!value) value = serviceInfo.JupyterLab.defaultOption; - updateLabConfigSelect(select, value, currentVal); - } - - var updateServicesPrev = function (id, value) { - const dropdownOptions = getDropdownOptions(); - const serviceInfo = getServiceInfo(); - - let select = $(`select#${id}-version-select`); - const currentVal = select.val(); - resetInputElement(select); - var valueName = (serviceInfo.JupyterLab.options[value] || {}).name || "new-jupyterlab"; - for (const service of Object.keys(dropdownOptions).sort().reverse()) { - var serviceName = (serviceInfo.JupyterLab.options[service] || {}).name || service; - if ( valueName.includes("deprecated") || ! serviceName.includes("deprecated")) { - select.append(`<option value="${service}">${serviceName}</option>`); - } - } - if (!value) value = serviceInfo.JupyterLab.defaultOption; - updateLabConfigSelect(select, value, currentVal); - } - - var updateSystems = function (id, service, value) { - const dropdownOptions = getDropdownOptions(); - const systemInfo = getSystemInfo(); - - let select = $(`select#${id}-system-select`); - const currentVal = select.val(); - resetInputElement(select); - - const systemsAllowed = dropdownOptions[service] || {}; - for (const system of Object.keys(systemInfo).sort((a, b) => (systemInfo[a]["weight"] || 99) < (systemInfo[b]["weight"] || 99) ? -1 : 1)) { - if (system in systemsAllowed) select.append(`<option value="${system}">${system}</option>`); - } - updateLabConfigSelect(select, value, currentVal); - } - - var updateFlavors = function (id, service, system, value) { - const systemInfo = getSystemInfo(); - const backendInfo = getBackendServiceInfo(); - - let select = $(`select#${id}-flavor-select`); - const currentVal = select.val(); - - resetInputElement(select); - if ($(`#${id}-na-info`).length && $(`#${id}-na-info`).html().includes("flavor")) { - $(`#${id}-na-btn`).hide(); - $(`#${id}-na-info`).empty().hide(); - if (!window.spawnActive[id]) - $(`#${id}-start-btn`).removeClass("disabled").show(); - } - - let systemFlavors = window.flavorInfo[system]; - if (!systemFlavors) { - // Check if system should have flavor info but doesn't first - let backend = (systemInfo[system] || {}).backendService; - if (backend && (backendInfo[backend].flavorsRequired || backendInfo[backend].userflavors)) { - // If so, we still want to create the flavor info to show the error message - utils.createFlavorInfo(id, system); - utils.setLabAsNA(id, "due to flavor"); - $(`#${id}-flavor-select-div, #${id}-flavor-legend-div, #${id}-flavor-info-div`).show(); - } - else { - // Otherwise, we can just skip showing the flavor info entirely - $(`#${id}-flavor-select-div, #${id}-flavor-legend-div, #${id}-flavor-info-div`).hide(); - } - updateLabConfigSelect(select, value, currentVal); - return; - }; - - // Sort systemFlavors by flavor weights - for (const [flavor, description] of Object.entries(systemFlavors).sort(([, a], [, b]) => (a["weight"] || 99) < (b["weight"] || 99) ? 1 : -1)) { - // Flavor not valid, so skip - if (description.max == 0 || description.current < 0 || description.max == null || description.current == null) continue; - if (description.max == -1 || description.current < description.max) - select.append(`<option value="${flavor}">${description.display_name}</option>`); - } - utils.createFlavorInfo(id, system); - enableTooltips(); // Defined in page.html - $.isEmptyObject(systemFlavors) ? $(`#${id}-flavor-select-div, #${id}-flavor-legend-div, #${id}-flavor-info-div`).hide() : $(`#${id}-flavor-select-div, #${id}-flavor-legend-div, #${id}-flavor-info-div`).show(); - - if (select.html() == "") { - if (window.spawnActive[id]) { - // Lab is active, so we should still append the current flavor to the select - const flavor = window.userOptions[id].flavor; - const description = (systemFlavors[system] || {})[flavor] || {}; - if (flavor) { - select.append(`<option disabled value="${flavor}">${description.display_name || flavor}</option>`); - } - } - else { - // Show info text and disable start - select.append(`<option disabled value="">No flavors currently available</option>`); - utils.setLabAsNA(id, "due to flavor limits"); - } - - select.addClass("disabled"); - select.prop("selectedIndex", 0); - } - updateLabConfigSelect(select, value, currentVal); - } - - var updateAccounts = function (id, service, system, value) { - const dropdownOptions = getDropdownOptions(); - - let select = $(`select#${id}-account-select`); - const currentVal = select.val(); - resetInputElement(select); - - const accountsAllowed = (dropdownOptions[service] || {})[system] || {}; - for (const account of Object.keys(accountsAllowed).sort()) { - select.append(`<option value="${account}">${account}</option>`); - } - $.isEmptyObject(accountsAllowed) ? $(`#${id}-account-select-div`).hide() : $(`#${id}-account-select-div`).show(); - updateLabConfigSelect(select, value, currentVal); - } - - var updateProjects = function (id, service, system, account, value) { - const dropdownOptions = getDropdownOptions(); - - let select = $(`select#${id}-project-select`); - const currentVal = select.val(); - resetInputElement(select); - - const projectsAllowed = ((dropdownOptions[service] || {})[system] || {})[account] || {}; - for (const project of Object.keys(projectsAllowed).sort()) { - select.append(`<option value="${project}">${project}</option>`); - } - $.isEmptyObject(projectsAllowed) ? $(`#${id}-project-select-div`).hide() : $(`#${id}-project-select-div`).show(); - updateLabConfigSelect(select, value, currentVal); - } - - var updatePartitions = function (id, service, system, account, project, value) { - const dropdownOptions = getDropdownOptions(); - const systemInfo = getSystemInfo(); - - let select = $(`select#${id}-partition-select`); - const currentVal = select.val(); - resetInputElement(select); - // Distinguish between login and compute nodes - var loginNodes = []; - var computeNodes = []; - const partitionsAllowed = (((dropdownOptions[service] || {})[system] || {})[account] || {})[project] || {}; - const interactivePartitions = (systemInfo[system] || {}).interactivePartitions || []; - for (const partition of Object.keys(partitionsAllowed).sort()) { - if (interactivePartitions.includes(partition)) loginNodes.push(partition); - else computeNodes.push(partition); - } - // Append options to select in groups - if (loginNodes.length > 0) { - select.append('<optgroup label="Login Nodes">'); - loginNodes.forEach((x) => select.append(`<option value="${x}">${x}</option>`)) - select.append('</optgroup>'); - } - if (computeNodes.length > 0) { - select.append('<optgroup label="Compute Nodes">'); - const systemUpper = system.replace('-', '').toUpperCase(); - if ((window.systemsHealth[systemUpper] || 0) >= (window.systemsHealth.threshold.compute || 40)) { - computeNodes.forEach((x) => select.append(`<option value="${x}" disabled>${x} (in maintenance)</option>`)); - } - else { - computeNodes.forEach((x) => select.append(`<option value="${x}">${x}</option>`)); - } - select.append('</optgroup>'); - } - $.isEmptyObject(partitionsAllowed) ? $(`#${id}-partition-select-div`).hide() : $(`#${id}-partition-select-div`).show(); - updateLabConfigSelect(select, value, currentVal); - } - - var updateReservations = function (id, service, system, account, project, partition, value) { - - function _toggle_show_reservation(show) { - if (show) { - $(`#${id}-reservation-select-div`).show(); - $(`#${id}-reservation-hr`).show(); - } - else { - $(`#${id}-reservation-select-div`).hide(); - $(`#${id}-reservation-hr`).hide(); - } - } - - const dropdownOptions = getDropdownOptions(); - const reservationInfo = getReservationInfo(); - const systemInfo = getSystemInfo(); - - let select = $(`select#${id}-reservation-select`); - const currentVal = select.val(); - resetInputElement(select, false); - - const interactivePartitions = (systemInfo[system] || {}).interactivePartitions || []; - if (interactivePartitions.includes(partition)){ - _toggle_show_reservation(false); - updateLabConfigSelect(select, value, currentVal); - return; - } - - const reservationsAllowed = ((((dropdownOptions[service] || {})[system] || {})[account] || {})[project] || {})[partition] || {}; - if (reservationsAllowed.length > 0 && JSON.stringify(reservationsAllowed) !== JSON.stringify(["None"])) { - for (const reservation of reservationsAllowed) { - if (reservation == "None") select.append(`<option value="${reservation}">${reservation}</option>`); - else { - const reservationName = reservation.ReservationName; - const systemReservationInfo = reservationInfo[system] || {}; - for (const reservationInfo of systemReservationInfo) { - if (reservationInfo.ReservationName == reservationName) { - if (reservationInfo.State == "ACTIVE") select.append(`<option value="${reservationName}">${reservationName}</option>`); - else select.append(`<option value="${reservationName}" disabled style="color: #6c757d;">${reservationName} [INACTIVE]</option>`); - } - } - } - } - select.attr("required", true); - _toggle_show_reservation(true); - } - else { - _toggle_show_reservation(false); - } - updateLabConfigSelect(select, value, currentVal); - } - - var updateResources = function (id, service, system, account, project, partition, nodes, gpus, runtime, xserver) { - const resourceInfo = getResourceInfo(); - let nodesInput = $(`input#${id}-nodes-input`); - let gpusInput = $(`input#${id}-gpus-input`); - let runtimeInput = $(`input#${id}-runtime-input`); - let xserverCheckboxInput = $(`input#${id}-xserver-cb-input`); - let xserverInput = $(`input#${id}-xserver-input`); - let tabWarning = $(`#${id}-resources-tab-warning`); - const currentNodeVal = nodesInput.val(); - const currentGpusVal = gpusInput.val(); - const currentRuntimeVal = runtimeInput.val(); - const currentXserverCbVal = xserverCheckboxInput[0].checked; - const currentXserverVal = xserverInput.val(); - [nodesInput, gpusInput, runtimeInput, xserverInput].forEach(input => resetInputElement(input, false)); - xserverCheckboxInput[0].checked = false; - - $(`#${id}-resources-tab`).show(); - const systemResources = (resourceInfo[service] || {})[system] || {}; - if ($.isEmptyObject(systemResources)) { - $(`#${id}-resources-tab`).addClass("disabled"); - $(`#${id}-resources-tab`).hide(); - tabWarning.addClass("invisible"); - } - else { - const partitionResources = systemResources[partition]; - if ($.isEmptyObject(partitionResources)) { - $(`#${id}-resources-tab`).addClass("disabled"); - $(`#${id}-resources-tab`).hide(); - tabWarning.addClass("invisible"); - } - else { - $(`#${id}-resources-tab`).removeClass("disabled"); - $(`#${id}-resources-tab`).show(); - if ("nodes" in partitionResources) { - let min = (partitionResources.nodes.minmax || [0, 1])[0]; - let max = (partitionResources.nodes.minmax || [0, 1])[1]; - $(`label[for*=${id}-nodes-input]`).text("Nodes [" + min + "," + max + "]"); - let defaultNodes = partitionResources.nodes.default || 0; - updateLabConfigInput(nodesInput, nodes, currentNodeVal, min, max, defaultNodes); - $(`#${id}-nodes-input-div`).show(); - if (!currentNodeVal) tabWarning.removeClass("invisible"); - } - else { - $(`#${id}-nodes-input-div`).hide(); - if (currentNodeVal) tabWarning.removeClass("invisible"); - } - - if ("gpus" in partitionResources) { - let min = (partitionResources.gpus.minmax || [0, 1])[0]; - let max = (partitionResources.gpus.minmax || [0, 1])[1]; - $(`label[for*=${id}-gpus-input]`).text("GPUs [" + min + "," + max + "]"); - let defaultGpus = partitionResources.gpus.default || 0; - updateLabConfigInput(gpusInput, gpus, currentGpusVal, min, max, defaultGpus); - $(`#${id}-gpus-input-div`).show(); - if (!currentGpusVal) tabWarning.removeClass("invisible"); - } - else { - $(`#${id}-gpus-input-div`).hide(); - if (currentGpusVal) tabWarning.removeClass("invisible"); - } - - if ("runtime" in partitionResources) { - let min = (partitionResources.runtime.minmax || [0, 1])[0]; - let max = (partitionResources.runtime.minmax || [0, 1])[1]; - $(`label[for*=${id}-runtime-input]`).text("Runtime (minutes) [" + min + "," + max + "]"); - let defaultRuntime = partitionResources.runtime.default || 0; - updateLabConfigInput(runtimeInput, runtime, currentRuntimeVal, min, max, defaultRuntime); - $(`#${id}-runtime-input-div`).show(); - if (!currentRuntimeVal) tabWarning.removeClass("invisible"); - } - else { - $(`#${id}-runtime-input-div`).hide(); - if (currentRuntimeVal) tabWarning.removeClass("invisible"); - } - - if ("xserver" in partitionResources) { - let cblabel = partitionResources.xserver.cblabel || "Activate XServer"; - $(`label[for*=${id}-xserver-cb-input]`).text(cblabel); - var min = (partitionResources.xserver.minmax || [0, 1])[0]; - var max = (partitionResources.xserver.minmax || [0, 1])[1]; - let label = partitionResources.xserver.label || "Use XServer GPU Index"; - $(`label[for*=${id}-xserver-input]`).text(label + " [" + min + "," + max + "]"); - - if (xserver) { xserverCheckboxInput[0].checked = true; } - else { - xserver = partitionResources.xserver.default || 0; - if (!currentXserverVal) tabWarning.removeClass("invisible"); - // Determine if XServer checkbox should be shown - if (partitionResources.xserver.checkbox || false) { - $(`#${id}-xserver-cb-input-div`).show(); - if (currentXserverCbVal) xserverCheckboxInput[0].checked = true; - else { - if (partitionResources.xserver.default_checkbox || false) - xserverCheckboxInput[0].checked = true; - else xserverCheckboxInput[0].checked = false; - } - if (!currentXserverCbVal && xserverCheckboxInput[0].checked) tabWarning.removeClass("invisible"); - } - else { - $(`#${id}-xserver-cb-input-div`).hide(); - xserverCheckboxInput[0].checked = true; - if (!currentXserverCbVal) tabWarning.removeClass("invisible"); - } - } - updateLabConfigInput(xserverInput, xserver, currentXserverVal, min, max, min, false); - if (xserverCheckboxInput[0].checked) $(`#${id}-xserver-input-div`).show(); - else $(`#${id}-xserver-input-div`).hide(); - } - else { - $(`#${id}-xserver-cb-input-div`).hide(); - $(`#${id}-xserver-input-div`).hide(); - if (currentXserverCbVal || currentXserverVal) tabWarning.removeClass("invisible"); - } - } - } - } - - var updateModules = function updateModules(id, service, system, account, project, partition, values) { - const moduleInfo = getModuleInfo(); - const systemInfo = getSystemInfo(); - const interactivePartitions = (systemInfo[system] || {}).interactivePartitions || []; - - var tabWarning = $(`#${id}-modules-tab-warning`); - var currentOptions = []; - $(`#${id}-modules-form`).find(`input[type=checkbox]`).each(function () { - currentOptions.push($(this).val()); - }) - - var defaultOptions = []; - var enableModulesTab = false; - - for (const [moduleSet, modules] of Object.entries(moduleInfo)) { - $(`#${id}-${moduleSet}-div`).hide(); - var insertIndex = -1; - for (const [module, moduleInfo] of Object.entries(modules)) { - if (moduleInfo.sets.includes(service)) { - if (moduleInfo.allowed_systems && !moduleInfo.allowed_systems.includes(system)) { - // Module not in allowed systems, so do nothing. - } - else { - if (moduleInfo.compute_only && interactivePartitions.includes(partition)) { - // Module is compute only, but partition is interactive, so do nothing. - } - else if (moduleInfo.interactive_only && !interactivePartitions.includes(partition)) { - // Module is interactive only, but partition is compute, so do nothing. - } - else { - $(`#${id}-${moduleSet}-div`).show(); - enableModulesTab = true; - defaultOptions.push(module); - // If checkbox already exists, do nothing - // Else create it and set the default value - if (!currentOptions.includes(module)) { - let parent = $(`#${id}-${moduleSet}-checkboxes-div`); - var checked = ""; - if (typeof moduleInfo.default == "boolean") { - var checked = moduleInfo.default ? "checked" : ""; - } else if ( typeof moduleInfo.default == "object" && service in moduleInfo.default ) { - var checked = ( moduleInfo.default[service] || false) ? "checked" : ""; - } else if ( typeof moduleInfo.default == "object" && "default" in moduleInfo.default ) { - var checked = moduleInfo.default.default ? "checked" : ""; - } else { - var checked = ""; - } - // let checked = moduleInfo.default ? "checked" : ""; - let module_cols = "col-sm-6 col-md-4 col-lg-3"; - let cbHtml = ` - <div id="${id}-${module}-cb-div" class="form-check ${module_cols}"> - <input type="checkbox" class="form-check-input" id="${id}-${module}-check" value="${module}" ${checked}> - <label class="form-check-label" for="${id}-${module}-check"> - <span class="align-middle">${moduleInfo.displayName}</span> - <a href="${moduleInfo.href}" target="_blank" class="module-info text-muted ms-3"> - <span>${getInfoSvg()}</span> - <div class="module-info-link-div d-inline-block"> - <span class="module-info-link" id="${module}-info-link"> - ${getLinkSvg()} - </span> - </div> - </a> - </label> - </input> - </div> - ` - // No checkboxes exist yet, so we can simply append to the parent div - if (parent.children().length == 0) { - parent.append(cbHtml); - } - // Otherwise, we need to determine where to insert the new checkbox - else { - // Get the current element at index - var target = parent.children().eq(insertIndex); - target.before(cbHtml); - } - // Show tab warning to indicate changes in checkbox options - tabWarning.removeClass("invisible"); - } - insertIndex++; - } - } - } - } - } - // Remove checkboxes which still exist but should not anymore - var shouldRemove = currentOptions.filter(x => !defaultOptions.includes(x)); - for (const module of shouldRemove) { - $(`#${id}-${module}-cb-div`).remove(); - // Show tab warning to indicate changes in checkbox options - tabWarning.removeClass("invisible"); - } - - // Set values according to previous values. - if (values) { - // Loop through all checkboxes and only check those in values. - $(`#${id}-modules`).find("input[type=checkbox]").each((i, cb) => { - if (values.includes(cb.value)) cb.checked = true; - else cb.checked = false; - }) - } - - if (enableModulesTab) { - $(`#${id}-modules-tab`).removeClass("disabled"); - $(`#${id}-modules-tab`).show(); - } - else { - $(`#${id}-modules-tab`).addClass("disabled"); - $(`#${id}-modules-tab`).hide(); - tabWarning.addClass("invisible"); - } - } - - /* - Util functions - */ - var resetInputElement = function (element, required = true) { - element.html(""); - element.val(null); - element.removeClass("text-muted disabled"); - if (required) { - if(! element.hasClass("optional")){ - element.attr("required", required); - } - } else { - element.removeAttr("required"); - } - } - - var updateLabConfigSelect = function (select, value, lastSelected) { - } - - var updateLabConfigSelect2 = function (select, value, lastSelected) { - // For some systems, e.g. cloud, some options are not available - if (select.html() == "") { - select.append("<option disabled>Not available</option>"); - select.addClass("text-muted").removeAttr("required"); - } - // If there is only one option, we disable the dropdown - const numberOfOptions = select.children().length - if (numberOfOptions == 1) { - select.addClass("disabled"); - } - if (value) select.val(value); - else { - // Check if the last value is contained in the new options, - // otherwise just select the first value. - var index = 0; - select.children().each(function (i, option) { - if ($(option).val() == lastSelected) { - index = i; - /* Although index should be used to set the value and - avoid the (index == 0) query, indices don't work directly - when there are optiongroups in the select. So we set - it via the .val() function regardless. */ - select.val(lastSelected); - return; - } - }) - if (index == 0) select.prop("selectedIndex", index); - } - select[0].dispatchEvent(new Event("change")); - } - - var updateLabConfigInput = function (input, value, lastSelected, min, max, defaultValue, required = true) { - input.attr({ "min": min, "max": max }); - if (required) input.attr("required", required); - else input.removeAttr("required"); - // Set message for invalid feedback - input.siblings(".invalid-feedback") - .text(`Please choose a number between ${min} and ${max}.`); - if (value) { - input.val(value); - } - // Check if we can keep the old value - else if (lastSelected != "" && lastSelected >= min && lastSelected <= max) { - input.val(lastSelected); - } - else { - input.val(defaultValue); - } - } - - var updateR2dType = function (id, r2dType) { - // const dropdownOptions = getDropdownOptions(); - const repos = getBinderRepos().repos || []; - - let select = $(`select#${id}-type-select`); - const currentVal = select.val(); - resetInputElement(select); - - repos.forEach((repo) => select.append(`<option value="${repo}">${repo}</option>`)); - - updateLabConfigSelect(select, r2dType, currentVal); - } - - var updateR2dNotebookTypes = function(id, r2dNotebookType){ - const notebookTypes = getBinderRepos().notebookTypes || []; - - let select = $(`select#${id}-notebook_type-select`); - const currentVal = select.val(); - resetInputElement(select); - - notebookTypes.forEach((nbType) => select.append(`<option value="${nbType}">${nbType}</option>`)) - updateLabConfigSelect(select, r2dNotebookType, currentVal); - } - - var updateDropdowns = { - updateServices: updateServices, - updateSystems: updateSystems, - updateFlavors: updateFlavors, - updateAccounts: updateAccounts, - updateProjects: updateProjects, - updatePartitions: updatePartitions, - updateReservations: updateReservations, - updateResources: updateResources, - updateModules: updateModules, - updateR2dType: updateR2dType, - updateR2dNotebookTypes: updateR2dNotebookTypes, - resetInputElement: resetInputElement, - updateLabConfigSelect: updateLabConfigSelect, - updateLabConfigInput: updateLabConfigInput, - } - - return updateDropdowns; - -}) diff --git a/static/js/home/handle-events.js b/static/js/home/handle-events.js deleted file mode 100644 index 5376b14530c5c9fd4388b5e20bf590cd4bdafd90..0000000000000000000000000000000000000000 --- a/static/js/home/handle-events.js +++ /dev/null @@ -1,451 +0,0 @@ -/* -* Callbacks related to interacting with table rows -*/ -require(["jquery", "home/utils", "home/dropdown-options"], function ( - $, - utils, - dropdowns -) { - "use strict"; - - /* *************** */ - /* TABLE UI EVENTS */ - /* *************** */ - - // Toggle a labs corresponding collapsible table row - // when it's summary table row is clicked. - $(".summary-tr").on("click", function () { - let id = $(this).data("server-id"); - let accordionIcon = $(this).find(".accordion-icon"); - let collapse = $(`.collapse[id*=${id}]`); - let shown = collapse.hasClass("show"); - if (shown) accordionIcon.addClass("collapsed"); - else accordionIcon.removeClass("collapsed"); - new bootstrap.Collapse(collapse); - }); - - // ... but not when the action td button are clicked. - $(".actions-td button").on("click", function (event) { - event.preventDefault(); - event.stopPropagation(); - }); - - // We show warning icons when the content of tabs change. - // Hide those warning icons once the tab is activated. - $("button[role=tab]").on("click", function () { - let warning = $(this).find("[id$=tab-warning]"); - warning.addClass("invisible"); - }); - - // Toggle log tabs on log button or log info text click - $(".log-info-btn, .log-info-text").on("click", function (event) { - let id = $(this).parents("tr").data("server-id"); - let collapse = $(`.collapse[id*=${id}]`); - let shown = collapse.hasClass("show"); - // Prevent collapse from closing if it is - // already open, but not showing the logs tab. - if (shown && !$(`#${id}-logs-tab`).hasClass("active")) { - event.preventDefault(); - event.stopPropagation(); - } - // Change to the log tab. - var trigger = $(`#${id}-logs-tab`); - var tab = new bootstrap.Tab(trigger); - tab.show(); - }); - - // Show selected logs. - $("select[id*=log-select]").change(function () { - const id = utils.getId(this); - const val = $(this).val(); - var log = $(`#${id}-log`); - log.html(""); - for (const event of spawnEvents[id][val]) { - utils.appendToLog(log, event["html_message"]); - } - }); - - $("button[id*=view-password]").on("click", function (event) { - const id = utils.getId(this); - const passInput = $(`#${id}-image-private-pass-input`)[0] - const eye = $(`#${id}-password-eye`)[0] - if (passInput.type === 'password') { - passInput.type = 'text'; - eye.classList.remove('fa-eye'); - eye.classList.add('fa-eye-slash'); - } else { - passInput.type = 'password'; - eye.classList.add('fa-eye'); - eye.classList.remove('fa-eye-slash'); - } - }); - - /* *************** */ - /* LAB CONFIG */ - /* *************** */ - - function _toggle_show_element(id, key, type, showInput, pattern) { - let element = $(`#${id}-${key}-${type}`); - if (showInput) { - $(`#${id}-${key}-${type}-div`).show(); - if(element.hasClass("optional")){ - element.removeAttr("required"); - } - else element.attr("required", true); - if (pattern) element.attr("pattern", pattern); - } else { - $(`#${id}-${key}-${type}-div`).hide(); - element.removeAttr("required pattern"); - } - } - - function _toggle_show_repo2Docker(id){ - var repo2DockerInputs = ["repo", "gitref", "notebook"]; - var repo2DockerSelects = ["type", "notebook_type"] - - var values = utils.getLabConfigSelectValues(id); - const show = (values.service || "") == "repo2docker"; - - for(let key of repo2DockerInputs){ - let element = $(`#${id}-${key}-input`); - dropdowns.resetInputElement(element); - _toggle_show_element(id, key, "input", show); - } - repo2DockerSelects.forEach(key => _toggle_show_element(id, key, "select", show)); - - if (show) { - dropdowns.updateR2dType(id, null); - dropdowns.updateR2dNotebookTypes(id, null); - } - } - - function _toggle_show_customImage(id, userInputInfo){ - var customDockerInputs = ["image"] - var customDockerInputsMounts = ["image-mount"] - var customDockerInputsPrivate = ["image-private-url", "image-private-user", "image-private-pass"] - var registryAuthsInputDivs = $(`#${id}-image-private-url-input-div,#${id}-image-private-user-input-div, #${id}-image-private-pass-input-div`) - var allUserInputDivs = $(`#${id}-image-input-div, #${id}-image-private-cb-input-div, #${id}-image-mount-cb-input-div, #${id}-image-mount-input-div`) - - var values = utils.getLabConfigSelectValues(id); - const show = (values.service || "") == "custom"; - - for(let key of customDockerInputs){ - let element = $(`#${id}-${key}-input`); - dropdowns.resetInputElement(element, show); - _toggle_show_element(id, key, "input", show); - } - - // set default values for mount userdata - var mount_cb_checked = userInputInfo.defaultMountEnabled || true; - - if (mount_cb_checked) { - for(let key of customDockerInputsMounts){ - let element = $(`#${id}-${key}-input`); - dropdowns.resetInputElement(element, show && mount_cb_checked); - _toggle_show_element(id, key, "input", show && mount_cb_checked); - } - } - - for(let key of customDockerInputsPrivate){ - let element = $(`#${id}-${key}-input`); - dropdowns.resetInputElement(element, false); - _toggle_show_element(id, key, "input", false); - } - - if (show) { - $(`#${id}-image-mount-cb-input`)[0].checked = mount_cb_checked; - $(`#${id}-image-mount-input`).val(userInputInfo.defaultMountPath || "/mnt/userdata"); - allUserInputDivs.show(); - } else { - registryAuthsInputDivs.hide(); - allUserInputDivs.hide(); - } - } - - function _toggle_show_share_button(id, values, force_hide=false, force_show=false){ - const shareInfo = getShareInfo(); - const software = "JupyterLab"; - const service = values.service || ""; - const system = values.system || ""; - if ( force_show || ( (! force_hide) && ( shareInfo[software] ) && ( (shareInfo[software][service] || []).includes(system) ) ) ) { - $(`#${id}-share-btn`).show(); - } else { - $(`#${id}-share-btn`).hide(); - } - } - - $("select[id*=verfffsion]").change(function () { - const id = utils.getId(this); - const values = utils.getLabConfigSelectValues(id); - if (!$(this).hasClass("no-update")) { - try { - dropdowns.updateSystems(id, values.service); - } - catch (e) { - utils.setLabAsNA(id, "due to a JS error"); - console.log(e); - } - } - - const serviceInfo = getServiceInfo(); - const userInputInfo = (serviceInfo.JupyterLab.options[values.service] || {}).userInput || {}; - _toggle_show_customImage(id, userInputInfo); - _toggle_show_repo2Docker(id); - }); - - $("select[id*=type]").change(function () { - const id = utils.getId(this); - const values = utils.getLabConfigSelectValues(id); - if (!$(this).hasClass("no-update")) { - try { - dropdowns.updateSystems(id, values.service); - } - catch (e) { - utils.setLabAsNA(id, "due to a JS error"); - console.log(e); - } - } - - if ( ["GitHub"].includes(values.r2dtype) ){ - let label = $(`label[for="${id}-repo-input"]`); - let input = $(`#${id}-repo-input`); - label.text("GitHub repository name or URL"); - input.attr("placeholder", "GitHub repository name or URL"); - } - }); - - $("select[id*=notebook_type]").change(function () { - const id = utils.getId(this); - const values = utils.getLabConfigSelectValues(id); - if (!$(this).hasClass("no-update")) { - try { - dropdowns.updateSystems(id, values.service); - } - catch (e) { - utils.setLabAsNA(id, "due to a JS error"); - console.log(e); - } - } - - if ( ["GitHub"].includes(values.r2dtype) ){ - let label = $(`label[for="${id}-notebook-input"]`); - let input = $(`#${id}-notebook-input`); - if ( values.r2dnotebooktype == "File") { - label.text("Path to a notebook file (optional)"); - input.attr("placeholder", "Path to a notebook file (optional)"); - } else { - label.text("URL to open (optional)"); - input.attr("placeholder", "URL to open (optional)"); - } - } - }); - - $("input[id*=image-private-cb-input]").change(function () { - const id = utils.getId(this, -4); - const values = utils.getLabConfigSelectValues(id); - const showInput = this.checked; - _toggle_show_element(id, "image-private-url", "input", showInput); - _toggle_show_element(id, "image-private-user", "input", showInput); - _toggle_show_element(id, "image-private-pass", "input", showInput); - - // If private registry is used, we disable the share button - _toggle_show_share_button(id, values, showInput); - }); - - $("input[id*=image-mount-cb-input]").change(function () { - const id = utils.getId(this, -4); - const pattern_check = "^\\/[A-Za-z0-9\\-\\/]+"; - _toggle_show_element(id, "image-mount", "input", this.checked, pattern_check); - }); - - $("select[id*=system]").change(function () { - const id = utils.getId(this); - const values = utils.getLabConfigSelectValues(id); - if (!$(this).hasClass("no-update")) { - try { - dropdowns.updateFlavors(id, values.service, values.system); - dropdowns.updateAccounts(id, values.service, values.system); - } - catch (e) { - utils.setLabAsNA(id, "due to a JS error"); - console.log(e); - } - } - - // Check if the chosen version is deprecated for the system - const serviceInfo = getServiceInfo(); - const systemInfo = getSystemInfo(); - // First check for system specific default option, then for general one - var defaultOption = (((systemInfo[values.system] || {}).services || {}).JupyterLab || {}).defaultOption || serviceInfo.JupyterLab.defaultOption; - if (defaultOption && values.service != defaultOption) { - // Not using default/latest version, show a warning message - let reason = "<span style=\"color:darkorange;\">uses deprecated version</span>"; - $(`#${id}-spawner-info`).show().html(reason); - } - else { - $(`#${id}-spawner-info`).hide().html(""); - } - _toggle_show_share_button(id, values); - }); - - $("select[id*=account]").change(function () { - const id = utils.getId(this); - const values = utils.getLabConfigSelectValues(id); - if (!$(this).hasClass("no-update")) { - try { - dropdowns.updateProjects(id, values.service, values.system, values.account); - } - catch (e) { - utils.setLabAsNA(id, "due to a JS error"); - console.log(e); - } - } - }); - - $("select[id*=project]").change(function () { - const id = utils.getId(this); - const values = utils.getLabConfigSelectValues(id); - if (!$(this).hasClass("no-update")) { - try { - dropdowns.updatePartitions(id, values.service, values.system, values.account, values.project); - } - catch (e) { - utils.setLabAsNA(id, "due to a JS error"); - console.log(e); - } - } - }); - - $("select[id*=partition]").change(function () { - const id = utils.getId(this); - const values = utils.getLabConfigSelectValues(id); - if (!$(this).hasClass("no-update")) { - try { - dropdowns.updateReservations(id, values.service, values.system, values.account, values.project, values.partition); - dropdowns.updateResources(id, values.service, values.system, values.account, values.project, values.partition); - dropdowns.updateModules(id, values.service, values.system, values.account, values.project, values.partition); - } - catch (e) { - utils.setLabAsNA(id, "due to a JS error"); - console.log(e); - } - } - }); - - $("input[id*=xserver-cb-input]").change(function () { - const id = utils.getId(this, -3); - _toggle_show_element(id, "xserver", "input", this.checked); - }); - - $("select[id*=reservation]").change(function () { - const reservationInfo = getReservationInfo(); - - const id = utils.getId(this); - const value = $(this).val(); - if (value) { - if (value == "None") { - $(`#${id}-reservation-info-div`).hide(); - $(`#${id}-runtime-input`).trigger("change"); - // } - return; - } - const systemReservationInfo = reservationInfo[utils.getLabConfigSelectValues(id)["system"]] || []; - for (const reservationInfo of systemReservationInfo) { - if (reservationInfo.ReservationName == value) { - $(`#${id}-reservation-start`).html(`${reservationInfo.StartTime} (Europe/Berlin)`); - $(`#${id}-reservation-end`).html(`${reservationInfo.EndTime} (Europe/Berlin)`); - $(`#${id}-reservation-state`).html(reservationInfo.State); - $(`#${id}-reservation-details`).html( - JSON.stringify(reservationInfo, null, 2)); - } - } - $(`#${id}-reservation-info-div`).show(); - $(`#${id}-runtime-input`).trigger("change"); - } - else { - $(`#${id}-reservation-info-div`).hide(); - } - }); - - $("input[id*=runtime-input").change(function () { - - function _getTimeInMinutes(startTime, endTime){ - const elapsedTime = endTime - startTime; - const elapsedSeconds = elapsedTime / 1000; // Convert milliseconds to seconds - const elapsedMinutes = elapsedSeconds / 60; // Convert seconds to minutes - - return elapsedMinutes; - }; - - function _resetErrors(id, element) { - - var resourceInfo = getResourceInfo(); - const values = utils.getLabConfigSelectValues(id); - const partitionResources = ((resourceInfo[values.service] || {})[values.system] || {})[values.partition] || {}; - if (partitionResources.runtime != undefined) { - let min = (partitionResources.runtime.minmax || [0, 1])[0]; - let max = (partitionResources.runtime.minmax || [0, 1])[1]; - element.siblings(".invalid-feedback").text(`Please choose a number between ${min} and ${max}.`); - } - tabWarning.addClass("invisible"); - element.removeClass("is-invalid"); - } - - const id = utils.getId(this); - const reservationInfo = getReservationInfo(); - const systemReservationInfo = reservationInfo[utils.getLabConfigSelectValues(id)["system"]] || []; - var tabWarning = $(`#${id}-resources-tab-warning`); - - if (reservationInfo) { - const currentReservation = $(`#${id}-reservation-select`).val(); - - if (currentReservation == "None") { - _resetErrors(id, $(this)); - return; - } - for (const reservationInfo of systemReservationInfo) { - if (reservationInfo.ReservationName == currentReservation) { - const resStart = reservationInfo.StartTime; - const resEnd = reservationInfo.EndTime; - - const nowString = Date().toLocaleString("en-US", {timeZone: "Europe/Berlin"}); - const now = new Date(nowString).getTime(); - const reservStart = new Date(resStart).getTime(); - const startTime = (reservStart > now)? reservStart : now; - const endTime = new Date(resEnd).getTime(); - - var reservationTime = _getTimeInMinutes(startTime, endTime); - var currentRuntimeVal = $(this)[0].value; - - if(currentRuntimeVal > reservationTime){ - // a buffer of 10 minutes, which is used only for the error message to avoid users copy-pasting the maximum time and their job landing in the queue forever - let buffer = 10; - const timeLeft = Math.floor(reservationTime - buffer); - $(this).siblings(".invalid-feedback").text(`Your reservation ends on ${resEnd}. Do not set a runtime which exceeds this limit, e.g., ${timeLeft} minutes.`); - $(this).addClass("is-invalid"); - - tabWarning.removeClass("invisible"); - } - else { - _resetErrors(id, $(this)); - } - } - } - } - }); - - $("input.module-selector").click(function () { - const id = utils.getId(this, -3); - const allOrNone = $(this).attr("id").includes("select-all") ? "all" : "none"; - var checkboxes = $(`#${id}-modules-form`).find("input[type=checkbox]"); - if (allOrNone == "all") { - $(`#${id}-modules-select-none`)[0].checked = false; - checkboxes.each((i, cb) => { cb.checked = true; }); - } - else if (allOrNone == "none") { - $(`#${id}-modules-select-all`)[0].checked = false; - checkboxes.each((i, cb) => { cb.checked = false; }); - } - }); - -}) diff --git a/static/js/home/handle-servers.js b/static/js/home/handle-servers.js deleted file mode 100644 index 9d5055ac4f3b4ea99dd0c83dd78a45e28f18fa3a..0000000000000000000000000000000000000000 --- a/static/js/home/handle-servers.js +++ /dev/null @@ -1,505 +0,0 @@ -// Copyright (c) Jupyter Development Team. -// Distributed under the terms of the Modified BSD License. -/* -* This module is responsible for JupyterLab start/stop/cancel/delete etc. events. It also prepares the user options to be sent to the backend -*/ -require(["jquery", "jhapi", "utils", "home/utils", "home/lab-configs"], function ( - $, - JHAPI, - utils, - custom_utils, - lab -) { - "use strict"; - - var base_url = window.jhdata.base_url; - var user = window.jhdata.user; - var api = new JHAPI(base_url); - - function cancelServer() { - var [tr, id] = _getTrAndId(this); - _disableTrButtons(tr); - api.cancel_named_server(user, id, { - success: function () { - console.log("cancel success"); - custom_utils.setSpawnActive(id, false); - }, - error: function () { - console.log("cancel error"); - } - }); - } - - function stopServer() { - var [tr, id] = _getTrAndId(this); - _disableTrButtons(tr); - custom_utils.updateProgressState(id, "stopping"); - api.stop_named_server(user, id, { - success: function () { - console.log("stop success"); - let running = false; - _enableTrButtons(tr, running); - // Reset progress - custom_utils.updateProgressState(id, "reset"); - custom_utils.setSpawnActive(id, false); - }, - error: function (xhr) { - console.log("stop error"); - custom_utils.updateProgressState(id, "stop_failed"); - tr.find(".btn-open-lab, .btn-cancel-lab").removeClass("disabled"); - $(`#${id}-log`) - .append($('<div class="log-div">') - .html`Could not stop server. Error: ${xhr.responseText}`); - } - }); - } - - function deleteServer() { - var that = $(this); - var [collapsibleTr, id] = _getTrAndId(this); - _disableTrButtons(collapsibleTr); - api.delete_named_server(user, id, { - success: function () { - $(`tr[data-server-id=${id}]`).each(function () { - $(this).remove(); - }); - custom_utils.setSpawnActive(id, false); - }, - error: function (xhr) { - var alert = that.siblings(".alert"); - const displayName = _getDisplayName(collapsibleTr); - _showErrorAlert(alert, displayName, xhr.responseText); - } - }); - } - - function startServer() { - var [tr, id] = _getTrAndId(this); - var collapsibleTr = tr.siblings(`.collapsible-tr[data-server-id=${id}]`); - _disableTrButtons(tr); - - // Validate the form and start spawn only after validation - try { - $(`form[id*=${id}]`).submit(); - } - catch (e) { - let running = false; - _enableTrButtons(tr, running); - return; - } - custom_utils.updateProgressState(id, "reset"); - $(`#${id}-log`).html(""); - - var options = _createDataDict(collapsibleTr); - // Update the summary row according to the values set in the collapsibleTr - _updateTr(tr, id, options); - // Open a new tab for spawn_pending.html - // Need to create it here for JS context reasons - var newTab = window.open("about:blank"); - api.start_named_server(user, id, { - data: JSON.stringify(options), - success: function () { - // Save latest log to time stamp and empty it - custom_utils.updateSpawnEvents(window.spawnEvents, id); - window.userOptions[id] = options; - // Open the spawn url in the new tab - newTab.location.href = utils.url_path_join(base_url, "spawn", user, id); - // Hook up event-stream for progress - var evtSources = window.evtSources; - if (!(id in evtSources)) { - var progressUrl = utils.url_path_join(jhdata.base_url, "api/users", jhdata.user, "servers", id, "progress"); - progressUrl = progressUrl + "?_xsrf=" + window.jhdata.xsrf_token; - evtSources[id] = new EventSource(progressUrl); - evtSources[id].onmessage = function (e) { - onEvtMessage(e, id); - } - } - // Successfully sent request to start the lab, enable row again - let running = true; - custom_utils.setSpawnActive(id, "pending"); - _enableTrButtons(tr, running); - }, - error: function (xhr) { - newTab.close(); - // If cookie is not valid anymore, refresh the page. - // This should redirect the user to the login page. - if (xhr.status == 403) { - document.location.reload(); - return; - } - custom_utils.updateProgressState(id, "failed"); - // Update progress in log - let details = $("<details>") - .append($("<summary>") - .html(`Could not request spawn. Error: ${xhr.responseText}`)) - .append($("<pre>") - .html(custom_utils.parseJSON(xhr.responseText))); - $(`#${id}-log`).append( - $("<div>").addClass("log-div").html(details) - ); - // Spawn attempt finished, enable row again - let running = false; - _enableTrButtons(tr, running); - } - }); - } - - function startNewServer() { - function _uuidv4hex() { - return ([1e7, 1e3, 4e3, 8e3, 1e11].join('')).replace(/[018]/g, c => - (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)); - } - - function _uuidWithLetterStart() { - let uuid = _uuidv4hex(); - let char = Math.random().toString(36).match(/[a-zA-Z]/)[0]; - return char + uuid.substring(1); - } - - const uuid = _uuidWithLetterStart(); - // Start button is in collapsible tr for new labs - var [collapsibleTr, id] = _getTrAndId(this); - _disableTrButtons(collapsibleTr); - - // Validate the form and start spawn only after validation - try { - $(`form[id*=${id}]`).submit(); - } - catch (e) { - collapsibleTr.find("button").removeClass("disabled"); - return; - } - - var options = _createDataDict(collapsibleTr); - // Open a new tab for spawn_pending.html - // Need to create it here for JS context reasons - var newTab = window.open("about:blank"); - api.start_named_server(user, uuid, { - data: JSON.stringify(options), - success: function () { - var url = utils.url_path_join(base_url, "spawn", user, uuid); - newTab.location.href = url; - // Reload page to add spawner to table - location.reload(); - }, - error: function (xhr) { - newTab.close(); - // If cookie is not valid anymore, refresh the page. - // This should redirect the user to the login page. - if (xhr.status == 403) { - document.location.reload(); - return; - } - // Show information about why the start failed - let details = $("<details>") - .append($("<summary>") - .html(`Could not request spawn. Error: ${xhr.responseText}`)) - .append($("<pre>") - .html(custom_utils.parseJSON(xhr.responseText))); - $(`#${id}-log`).append( - $("<div>").addClass("log-div").html(details) - ); - collapsibleTr.find("button").removeClass("disabled"); - } - }); - } - - function shareLab() { - // Start button is in collapsible tr for new labs - var [collapsibleTr, id] = _getTrAndId(this); - // _disableTrButtons(collapsibleTr); - - var options = _createDataDict(collapsibleTr); - - function showShareDialogue(url) { - - $(`#${id}-copy-btn`).click(function() { - - const shareUrl = $(`#${id}-share-link .modal-body a`).attr('href'); - navigator.clipboard.writeText(shareUrl).then(function() { - - $(`#${id}-copy-btn`).tooltip('dispose').attr('title', 'Copied'); - $(`#${id}-copy-btn`).tooltip('show'); - }, function(err) { - console.error('Could not copy text: ', err); - }); - }); - - let shareableURL = url; - - $(`#${id}-share-link .modal-title`).text(`Share Lab ${options["name"]}`); - $(`#${id}-share-link .modal-body a`).text(`${shareableURL}`); - try { - shareableURL = new URL(url); - $(`#${id}-share-link .modal-body a`).attr('href', shareableURL); - } catch (error) {} - - $(`#${id}-share-link`).modal('show'); - } - //--------------------------------------------------- - - // Open a new tab for spawn_pending.html - // Need to create it here for JS context reasons - // var newTab = window.open("about:blank"); - // let protocol = window.location.protocol; - var urlStr = utils.url_path_join(window.origin, base_url, "share"); - api.share_server({ - data: JSON.stringify(options), - success: function (resp) { - urlStr = utils.url_path_join(window.origin, base_url, "share", "user_options", resp).replace("//", "/"); - showShareDialogue(urlStr); - }, - error: function (xhr) { - // newTab.close(); - // If cookie is not valid anymore, refresh the page. - // This should redirect the user to the login page. - if (xhr.status == 403) { - document.location.reload(); - return; - } - - let err = `Failed to create a share link for this lab. Error: ${xhr.responseText}`; - showShareDialogue(err); - } - }); - } - - $(".btn-start-lab").click(startServer); - $(".btn-start-new-lab").click(startNewServer); - $(".btn-cancel-lab").click(cancelServer); - $(".btn-stop-lab").click(stopServer); - $(".btn-delete-lab").click(deleteServer); - $(".btn-share-lab").click(shareLab); - - /* - Validate form before starting a new lab - */ - $("form").submit(function (event) { - event.preventDefault(); - event.stopPropagation(); - - if (!$(this)[0].checkValidity()) { - $(this).addClass('was-validated'); - // Show the tab where the error was thrown - var tab_id = $(this).attr("id").replace("-form", "-tab"); - var tab = new bootstrap.Tab($("#" + tab_id)); - tab.show(); - // Open the collapsibleTr if it was hidden - const id = custom_utils.getId(this); - var tr = $(`.summary-tr[data-server-id=${id}`); - if (!$(`${id}-collapse`).css("display") == "none") { - tr.trigger("click"); - } - throw { - name: "FormValidationError", - toString: function () { - return this.name; - } - }; - } else { - $(this).removeClass('was-validated'); - } - }); - - - /* - Save and revert changes to spawner - */ - function saveChanges() { - var [collapsibleTr, id] = _getTrAndId(this); - var tr = $(`.summary-tr[data-server-id=${id}]`); - var alert = $(this).siblings(".alert"); - - const displayName = _getDisplayName(collapsibleTr); - const options = _createDataDict(collapsibleTr); - api.update_named_server(user, id, { - data: JSON.stringify(options), - success: function () { - _updateTr(tr, id, options); - // Update global user options - window.userOptions[id] = options; - alert.children("span") - .text(`Successfully updated ${displayName}.`); - alert - .removeClass("alert-danger p-0") - .addClass("alert-success show p-1"); - }, - error: function (xhr) { - _showErrorAlert(alert, displayName, xhr.responseText); - } - }); - } - - function revertChanges() { - const id = custom_utils.getId(this); - var alert = $(this).siblings(".alert"); - - const options = window.userOptions[id]; - const name = options.name; - // Do not send start_id when updating lab config - delete options.start_id; - - api.update_named_server(user, id, { - data: JSON.stringify(options), - success: function () { - $(`#${id}-name-input`).val(name); - // Reset all user inputs to the values saved in the global user options - let available = lab.checkIfAvailable(id, options); - lab.setUserOptions(id, options, available); - // Remove all tab warnings since manual changes shouldn't cause warnings - $("[id$=tab-warning]").addClass("invisible"); - // Show first tab after resetting values - var trigger = $(`#${id}-collapse`).find(".nav-link").first(); - var tab = new bootstrap.Tab(trigger); - tab.show(); - alert.children("span") - .text(`Successfully reverted settings for ${name}.`); - alert - .removeClass("alert-danger p-0") - .addClass("alert-success show p-1"); - }, - error: function (xhr) { - _showErrorAlert(alert, name, xhr.responseText); - } - }); - } - - $(".btn-save-lab").click(saveChanges); - $(".btn-reset-lab").click(revertChanges); - - /* - Util functions - */ - function _getDisplayName(collapsibleTr) { - var displayName = collapsibleTr.find("input[id*=name]").val(); - if (displayName == "") displayName = "Unnamed JupyterLab"; - return displayName; - } - - function _getTrAndId(element) { - let tr = $(element).parents("tr"); - let id = tr.data("server-id"); - return [tr, id]; - } - - function _disableTrButtons(tr) { - // Disable buttons - tr.find(".btn").addClass("disabled"); - } - - function _enableTrButtons(tr, running) { - if (running) { - // Show open/cancel for starting labs - tr.find(".btn-na-lab, .btn-start-lab").addClass("disabled").hide(); - tr.find(".btn-open-lab, .btn-cancel-lab").show(); - // Disable until fitting event received from EventSource - tr.find(".btn-open-lab, .btn-cancel-lab").addClass("disabled"); - } - else { - // Show start or na for non-running labs - var na = tr.find(".na-status").text() || 0; - if (na != "0") { - tr.find(".btn-na-lab").removeClass("disabled").show(); - tr.find(".btn-start-lab").addClass("disabled").hide(); - } - else { - tr.find(".btn-na-lab").addClass("disabled").hide(); - tr.find(".btn-start-lab").removeClass("disabled").show(); - } - tr.find(".btn-open-lab, .btn-cancel-lab, .btn-stop-lab") - .addClass("disabled").hide(); - } - } - - function _showErrorAlert(alert, name, text) { - alert.children("span") - .text(`Could not update ${name}. Error: ${text}`); - alert - .removeClass("alert-success p-0") - .addClass("alert-danger show p-1"); - } - - - function _updateTr(tr, id, options) { - tr.find(".name-td").text(options.name); - function _updateTd(key) { - let configTdDiv = tr.find(`#${id}-config-td-div-${key}`); - if (options[key]) configTdDiv.show(); - else configTdDiv.hide(); - let configDiv = tr.find(`#${id}-config-td-${key}`); - configDiv.text(options[key]); - } - ["system", "flavor", "partition", "project", - "runtime", "nodes", "gpus"].forEach(key => _updateTd(key)); - } - - function _createDataDict(collapsibleTr) { - var options = {} - options.name = _getDisplayName(collapsibleTr); - - function _addSelectValue(param) { - var select = collapsibleTr.find(`select[id*=${param}]`); - var value = select.val(); - if (param == "version") { - param = "profile"; - value = "JupyterLab/" + value; - } - if (value) options[param] = value; - } - - function _addInputValue(param) { - var input = collapsibleTr.find(`input[id*=${param}]`).not(`[type=checkbox]`); - var value = input.val(); - if (param == "xserver") { - if (collapsibleTr.find(`input[id*=xserver-cb-input]`).length && collapsibleTr.find(`input[id*=xserver-cb-input]`)[0] && !collapsibleTr.find(`input[id*=xserver-cb-input]`)[0].checked) return; - } - else if (param == "image-private") { - if (collapsibleTr.find(`input[id*=image-private-cb-input]`).length && collapsibleTr.find(`input[id*=image-private-cb-input]`)[0] && !collapsibleTr.find(`input[id*=image-private-cb-input]`)[0].checked) return; - param = "dockerregistry"; - - let registry_url = ""; - const credentials = {} - - for(let i = 0; i < input.length; i++) { - let el = input[i] - if(el.id.indexOf("private-url") !== -1){ - registry_url = el.value; - } - if(el.id.indexOf("user") !== -1){ - credentials["username"] = el.value; - } - if(el.id.indexOf("pass") !== -1){ - credentials["password"] = el.value; - } - } - const auth_values = {} - auth_values[registry_url] = credentials - const auths = {"auths" : auth_values } - // Encode user credentials for the private docker repository in base64 - value = btoa(JSON.stringify(auths)); - } - else if (param == "image-mount") { - if (collapsibleTr.find(`input[id*=image-mount-cb-input]`).length && collapsibleTr.find(`input[id*=image-mount-cb-input]`)[0] && !collapsibleTr.find(`input[id*=image-mount-cb-input]`)[0].checked) return; - param = "userdata_path"; - } - if (value) options[param] = value; - } - - function _addCbValues(param) { - var checkboxes = collapsibleTr - .find('form[id*=modules-form]') - .find(`input[type=checkbox]`); - var values = []; - checkboxes.each(function () { - if (this.checked) values.push($(this).val()); - }); - options[param] = values; - } - - ["version", "system", "flavor", "account", - "project", "partition", "reservation", "type", "notebook_type"].forEach(key => _addSelectValue(key)); - ["image", "image-mount", "image-private", "nodes", "gpus", "runtime", "xserver", "repo", "gitref", "notebook"].forEach(key => _addInputValue(key)); - _addCbValues("userModules"); - return options; - } -}); diff --git a/static/js/home/lab-configs.js b/static/js/home/lab-configs.js deleted file mode 100644 index 6ac51610e08e597953acdfb73647639ddc8fbfdd..0000000000000000000000000000000000000000 --- a/static/js/home/lab-configs.js +++ /dev/null @@ -1,383 +0,0 @@ -define(["jquery", "home/utils", "home/dropdown-options"], function ( - $, - utils, - dropdowns -) { - "use strict"; - - var checkComputeMaintenance = function (system, partition) { - const systemInfo = getSystemInfo(); - const interactivePartitions = (systemInfo[system] || {}).interactivePartitions || []; - if (!interactivePartitions.includes(partition)) { - const systemUpper = system.replace('-', '').toUpperCase(); - if ((window.systemsHealth[systemUpper] || 0) >= (window.systemsHealth.threshold.compute || 40)) return true; - else return false; - } - } - - var checkIfAvailable = function (id, options) { - var reason = "due to "; - var reason_broken_lab = "This lab is broken.\nPlease delete and recreate."; - - // Check if system is not available due to incident - const systemUpper = options["system"].replace('-', '').toUpperCase(); - if ((window.systemsHealth[systemUpper] || 0) >= (window.systemsHealth.threshold.interactive || 50)) { - reason += "maintenance"; - utils.setLabAsNA(id, reason); - return false; - } - - // Check if system is not available due to groups - const dropdownOptions = getDropdownOptions(); - const service = getService(options); - const systemInfo = getSystemInfo(); - const system = options["system"]; - const flavor = options["flavor"]; - const account = options["account"]; - const project = options["project"]; - const partition = options["partition"]; - const reservation = options["reservation"]; - const nodes = options["nodes"]; - const runtime = options["runtime"]; - const gpus = options["gpus"]; - const xserver = options["xserver"]; - - if (service == undefined) { - utils.setLabAsNA(id, reason_broken_lab); - return false; - } - if (!(service in dropdownOptions)) { - reason += "service version"; - utils.setLabAsNA(id, reason); - return false; - } - if (system !== undefined) { - if (dropdownOptions[service] == undefined) { - utils.setLabAsNA(id, reason_broken_lab); - return false; - } - if (!(system in dropdownOptions[service])) { - reason += "system"; - utils.setLabAsNA(id, reason); - return false; - } - if (!(system in systemInfo)) { - reason += "system"; - utils.setLabAsNA(id, reason); - return false; - } - } - if (flavor !== undefined) { - let systemFlavors = window.flavorInfo[system] || {}; - if (!(flavor in systemFlavors)) { - reason += "flavor"; - utils.setLabAsNA(id, reason); - return false; - } - let flavorDescription = systemFlavors[flavor]; - let spawnerState = window.spawnActive[id]; - if (flavorDescription.max != -1 && (flavorDescription.current || 0) >= flavorDescription.max && !spawnerState) { - reason += "flavor limits"; - utils.setLabAsNA(id, reason); - return false; - } - } - if (account !== undefined) { - if (dropdownOptions[service][system] == undefined) { - utils.setLabAsNA(id, reason_broken_lab); - return false; - } - if (!(account in dropdownOptions[service][system])) { - reason += "account"; - utils.setLabAsNA(id, reason); - return false; - } - } - if (project !== undefined) { - if (dropdownOptions[service][system][account] == undefined) { - utils.setLabAsNA(id, reason_broken_lab); - return false; - } - if (!(project in dropdownOptions[service][system][account])) { - reason += "project"; - utils.setLabAsNA(id, reason); - return false; - } - } - if (partition !== undefined) { - if (dropdownOptions[service][system][account][project] == undefined) { - utils.setLabAsNA(id, reason_broken_lab); - return false; - } - if (!(partition in dropdownOptions[service][system][account][project])) { - reason += "partition"; - utils.setLabAsNA(id, reason); - return false; - } - // Only compute nodes are not available during rolling updates - if (checkComputeMaintenance(system, partition)) { - reason += "maintenance"; - utils.setLabAsNA(id, reason); - return false; - } - } - if (reservation !== undefined && reservation != "None") { - if (dropdownOptions[service][system][account][project][partition] == undefined) { - utils.setLabAsNA(id, reason_broken_lab); - return false; - } - let setFalse = true; - for (const reservation_dict of dropdownOptions[service][system][account][project][partition]) { - if (reservation == reservation_dict.ReservationName) { - if (reservation_dict.State == "ACTIVE") { - setFalse = false; - break; - } - } - } - if (setFalse) { - reason += "reservation"; - utils.setLabAsNA(id, reason); - return false; - } - } - // Resources - const resourceInfo = getResourceInfo(); - const partitionResources = ((resourceInfo[service] || {})[system] || {})[partition] || {}; - if (nodes !== undefined) { - if (partitionResources.nodes == undefined) { - utils.setLabAsNA(id, reason_broken_lab); - return false; - } - let min = (partitionResources.nodes.minmax || [0, 1])[0]; - let max = (partitionResources.nodes.minmax || [0, 1])[1]; - if (nodes < min || nodes > max) { - reason += "number of nodes"; - utils.setLabAsNA(id, reason); - return false; - } - } - if (gpus !== undefined) { - if (partitionResources.gpus == undefined) { - utils.setLabAsNA(id, reason_broken_lab); - return false; - } - let min = (partitionResources.gpus.minmax || [0, 1])[0]; - let max = (partitionResources.gpus.minmax || [0, 1])[1]; - if (gpus < min || gpus > max) { - reason += "number of GPUs"; - utils.setLabAsNA(id, reason); - return false; - } - } - if (runtime !== undefined) { - if (partitionResources.runtime == undefined) { - utils.setLabAsNA(id, reason_broken_lab); - return false; - } - let min = (partitionResources.runtime.minmax || [0, 1])[0]; - let max = (partitionResources.runtime.minmax || [0, 1])[1]; - if (runtime < min || runtime > max) { - reason += "runtime"; - utils.setLabAsNA(id, reason); - return false; - } - } - if (xserver !== undefined) { - if (!("xserver" in partitionResources)) { - reason += "XServer"; - utils.setLabAsNA(id, reason); - return false; - } - } - - return true; - } - - var setUserOptions = function (id, options, available) { - const name = options["name"]; - const service = getService(options); - const system = options["system"]; - - // customDockerImage - const image = options["image"]; - const dockerregistry = options["dockerregistry"]; - const userdata_path = options["userdata_path"]; - - // default - const flavor = options["flavor"]; - const account = options["account"]; - const project = options["project"]; - const partition = options["partition"]; - const reservation = options["reservation"]; - const nodes = options["nodes"]; - const runtime = options["runtime"]; - const gpus = options["gpus"]; - const xserver = options["xserver"]; - const modules = options["userModules"]; - - // repo2Docker values - const r2dType = options["type"]; - const r2dRepo = options["repo"]; - const r2dGitref = options["gitref"] - const r2dNotebook = options["notebook"] - const r2dNotebookType = options["notebook_type"] - - function _updateDockerRegistryFields(id, value){ - $(`#${id}-image-private-cb-input`)[0].checked = true; - // Decode the value of the dockerRegistry. It comes encoded in base64 from the backend - let privateRegistryString = atob(value); - let privateRegistry = JSON.parse(privateRegistryString); - let auths = Object.values(privateRegistry)[0]; // returns a dictionary in the form of {"registry_url" : {"username": <username>, "password": <password>}} - let url = Object.keys(auths)[0]; - $(`#${id}-image-private-url-input`).val(url); - $(`#${id}-image-private-user-input`).val(auths[url]["username"]); - $(`#${id}-image-private-pass-input`).val(auths[url]["password"]); - } - - $(`#${id}-name-input`).val(name); - let registryAuthsInputDivs = $(`#${id}-image-private-url-input-div,#${id}-image-private-user-input-div, #${id}-image-private-pass-input-div`); - if (available) { - /* Set allowed values. Do not rely on change events here as without - passing a value explicitely, the first allowed option would be - chosen regardless of the user option value. */ - try { - dropdowns.updateServices("JupyterLab", id, service); - if (image) $(`#${id}-image-input`).val(image); - if (userdata_path) $(`#${id}-image-mount-input`).val(userdata_path); - if (dockerregistry){ - _updateDockerRegistryFields(id, dockerregistry); - } - else { - registryAuthsInputDivs.hide(); - } - if (r2dRepo) $(`#${id}-repo-input`).val(r2dRepo); - if (r2dGitref) $(`#${id}-gitref-input`).val(r2dGitref); - if (r2dNotebook) $(`#${id}-notebook-input`).val(r2dNotebook); - dropdowns.updateSystems(id, service, system); - dropdowns.updateFlavors(id, service, system, flavor); - dropdowns.updateAccounts(id, service, system, account); - dropdowns.updateProjects(id, service, system, account, project); - dropdowns.updatePartitions(id, service, system, account, project, partition); - dropdowns.updateReservations(id, service, system, account, project, partition, reservation); - dropdowns.updateResources(id, service, system, account, project, partition, nodes, gpus, runtime, xserver); - dropdowns.updateModules(id, service, system, account, project, partition, modules); - dropdowns.updateR2dType(id, r2dType); - dropdowns.updateR2dNotebookTypes(id, r2dNotebookType); - } - catch (e) { utils.setLabAsNA(id, "due to a JS error"); - console.log(e) - } - } - else { - function _setSelectOption(key, value, displayValue) { - if (!displayValue) displayValue = value; - if (value) $(`#${id}-${key}-select`).append(`<option value="${value}">${displayValue}</option>`); - else $(`#${id}-${key}-select-div`).hide(); - dropdowns.updateLabConfigSelect($(`#${id}-${key}-select`), value); - } - - function _setInputValue(key, value) { - if (value) $(`#${id}-${key}-input`).val(value); - else $(`#${id}-${key}-input-div`).hide(); - } - - $(`input[id*=${id}], select[id*=${id}]`).addClass("no-update"); - - const serviceInfo = getServiceInfo(); - let serviceName = (serviceInfo.JupyterLab.options[service] || {}).name || service; - - // Selects which are always visible - $(`#${id}-version-select`).append(`<option value="${service}">${serviceName}</option>`); - _setSelectOption("system", system); - _setInputValue("image", image); - if (userdata_path) { - $(`#${id}-image-mount-cb-input-div`)[0].checked = true; - $(`#${id}-image-mount-cb-input-div`).show(); - } - else { - $(`#${id}-image-mount-cb-input-div`)[0].checked = false; - $(`#${id}-image-mount-cb-input-div`).hide(); - } - if (dockerregistry) { - $(`#${id}-image-private-cb-input-div`)[0].checked = true; - $(`#${id}-image-private-cb-input-div`).show(); - _updateDockerRegistryFields(id, dockerregistry); - } - else { - $(`#${id}-image-private-cb-input-div`)[0].checked = false; - // $(`#${id}-image-private-cb-input-div`).hide(); - registryAuthsInputDivs.hide(); - } - - _setInputValue("image-mount", userdata_path); - _setSelectOption("flavor", flavor, ((window.flavorInfo[system] || {})[flavor] || {}).display_name); - utils.createFlavorInfo(id, system); - _setSelectOption("account", account); - _setSelectOption("project", project); - let maintenance = checkComputeMaintenance(system, partition); - _setSelectOption("partition", maintenance ? `${partition} (in maintenance)` : partition); - - // Reservation - var hasReservationInfo = false; - if (reservation) { - const reservationInfo = getReservationInfo(); - const systemReservationInfo = reservationInfo[system] || []; - for (const info of systemReservationInfo) { - if (info.ReservationName == reservation) { - hasReservationInfo = true; - var inactive = (info.State == "INACTIVE"); - if (inactive) { - $(`#${id}-reservation-select`).append( - `<option value="${reservation}">${reservation} [INACTIVE]</option>` - ); - } - else { - $(`#${id}-reservation-select`).append( - `<option value="${reservation}">${reservation}</option>` - ); - } - $(`#${id}-reservation-select`).trigger("change"); - } - } - if (!hasReservationInfo) - $(`#${id}-reservation-select`).append(`<option value="${reservation}">${reservation}</option>`); - } - else { - $(`#${id}-reservation-select-div`).hide(); - $(`#${id}-reservation-hr`).hide(); - } - if (hasReservationInfo) $(`#${id}-reservation-info-div`).show() - else $(`#${id}-reservation-info-div`).hide(); - - // Resources - if ((nodes || runtime || gpus || xserver) !== undefined) { - _setInputValue("nodes", nodes); - _setInputValue("runtime", runtime); - _setInputValue("gpus", gpus); - // Don't have info about resources, so just never show the xserver checkbox - _setInputValue("xserver", xserver); - } - else { - $(`#${id}-resources-tab`).addClass("disabled"); - $(`#${id}-resources-tab`).hide(); - } - - // Modules - dropdowns.updateModules(id, service, system, account, project, partition, modules); - - // Disable all user input elements if N/A - $(`input[id*=${id}]`).attr("disabled", true); - $(`select[id*=${id}]`).not("[id*=log]").addClass("disabled"); - } - } - - var labConfigs = { - checkComputeMaintenance: checkComputeMaintenance, - checkIfAvailable: checkIfAvailable, - setUserOptions: setUserOptions, - } - - return labConfigs; - -}) \ No newline at end of file diff --git a/static/js/home/utils.js b/static/js/home/utils.js deleted file mode 100644 index 02c3a64fa93ac7a749c09b0c089b975dd2d362da..0000000000000000000000000000000000000000 --- a/static/js/home/utils.js +++ /dev/null @@ -1,280 +0,0 @@ -define(["jquery", "jhapi",], function ( - $, - JHAPI -) { - "use strict"; - var base_url = window.jhdata.base_url; - var api = new JHAPI(base_url); - - const progressStates = { - "running": { - "text": "running", - "background": "bg-success", - "width": 100 - }, - "stop_failed": { - "text": "running (stop failed)", - "background": "bg-success", - "width": 100 - }, - "cancelling": { - "text": "cancelling...", - "background": "bg-danger", - "width": 100 - }, - "stopping": { - "text": "stopping...", - "background": "bg-danger", - "width": 100 - }, - "failed": { - "text": "last spawn failed", - "background": "bg-danger", - "width": 100 - }, - "reset": { - "text": "", - "background": "", - "width": 0 - } - } - - var parseJSON = function (inputString) { - try { - return JSON.stringify(JSON.parse(inputString), null, 2); - } catch (e) { - return inputString; - } - } - - var getId = function (element, slice_index = -2) { - let id_array = $(element).attr("id").split('-'); - let id = id_array.slice(0, slice_index).join('-'); - return id; - } - - var getSpecificValuesInTab = function(id, prefix) { - const values = {}; - $(`[id^="${id}"]`).filter('select, input').each(function() { - const id = $(this).attr('id'); - const suffixLength = $(this).prop("tagName").length + 1; - const key = id.substring(prefix.length, id.length - suffixLength); - if ($(this).is(':checkbox')) { - values[key] = $(this).is(':checked'); // Checkbox value - } else { - values[key] = $(this).val(); // Standard value - } - }); - return values; - } - - var getLabConfigSelectValues = function (id) { - - return { - "service": $(`select#${id}-version-select`).val(), - "system": $(`select#${id}-system-select`).val(), - "flavor": $(`select#${id}-flavor-select`).val(), - "account": $(`select#${id}-account-select`).val(), - "project": $(`select#${id}-project-select`).val(), - "partition": $(`select#${id}-partition-select`).val(), - "r2dtype": $(`select#${id}-type-select`).val(), - "r2dnotebooktype": $(`select#${id}-notebook_type-select`).val(), - } - } - - var setLabAsNA = function (id, reason) { - $(`#${id}-start-btn, #${id}-open-btn, #${id}-cancel-btn, #${id}-stop-btn`).addClass("disabled").hide(); - $(`#${id}-na-btn`).show(); - $(`#${id}-na-status`).html(1); - $(`#${id}-na-info`).html(reason).show(); - } - - var setSpawnActive = function (id, active) { - window.spawnActive[id] = active; - } - - var updateProgressState = function (id, state) { - $(`#${id}-progress-bar`) - .width(progressStates[state].width) - .removeClass("bg-success bg-danger") - .addClass(progressStates[state].background) - .html(""); - $(`#${id}-progress-info-text`).html(progressStates[state].text); - } - - var appendToLog = function (log, htmlMsg) { - try { htmlMsg = htmlMsg.replace(/ /g, ' '); } - catch (e) { return; } // Not a valid htmlMsg - // Only append if a log message has not been appended yet - var exists = false; - log.children().each(function (i, e) { - let logMsg = $(e).html(); - if (htmlMsg == logMsg) exists = true; - }) - if (!exists) - log.append($('<div class="log-div">').html(htmlMsg)); - } - - var updateSpawnEvents = function (spawnEvents, id) { - if (spawnEvents[id]["latest"].length) { - var re = /([0-9]+(-[0-9]+)+).*[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?/; - for (const [_, event] of spawnEvents[id]["latest"].entries()) { - const startMsg = event.html_message || event.message; - const startTimeMatch = re.exec(startMsg); - if (startTimeMatch) { - const startTime = startTimeMatch[0]; - // Have we already created a log entry for this time? - let logOptions = $(`#${id}-log-select option`); - let logTimes = $.map(logOptions, function (option) { - return option.value; - }); - // If yes, we do not need to do anything anymore - if (logTimes.includes(startTime)) return; - // Otherwise, save the current events to the startTime, - // update the log select and reset the "latest" events. - spawnEvents[id][startTime] = spawnEvents[id]["latest"]; - spawnEvents[id]["latest"] = []; - $(`#${id}-log-select`) - .append(`<option value="${startTime}">${startTime}</option>`) - .val("latest"); - break; - } - } - // We didn't manage to find a time, so update with no timestamp - if ((spawnEvents[id]["latest"].length)) { - spawnEvents[id]["previous"] = spawnEvents[id]["latest"]; - spawnEvents[id]["latest"] = []; - } - } - } - - var createFlavorInfo = function (id, system) { - $(`#${id}-flavor-info-div`).empty(); - - const systemFlavors = window.flavorInfo[system] || {}; - for (const [_, description] of Object.entries(systemFlavors).sort(([, a], [, b]) => (a["weight"] || 99) < (b["weight"] || 99) ? 1 : -1)) { - var current = description.current || 0; - var maxAllowed = description.max; - // Flavor not valid, so skip - if (maxAllowed == 0 || current < 0 || maxAllowed == null || current == null) continue; - - var bgColor = "bg-primary"; - // Infinite allowed - if (maxAllowed == -1) { - var progressTooltip = `${current} used`; - var maxAllowedLabel = '∞'; - if (current == 0) { - var currentWidth = 0; - var maxAllowedWidth = 100; - } - else { - var currentWidth = 20; - var maxAllowedWidth = 80; - } - } - else { - var progressTooltip = `${current} out of ${maxAllowed} used`; - var maxAllowedLabel = maxAllowed - current; - var currentWidth = current / maxAllowed * 100; - var maxAllowedWidth = maxAllowedLabel / maxAllowed * 100; - - if (maxAllowedLabel < 0) { - maxAllowedLabel = 0; - maxAllowedWidth = 0; - bgColor = "bg-danger"; - } - } - - var diagramHtml = ` - <div class="row align-items-center g-0 mt-4"> - <div class="col-4"> - <span>${description.display_name}</span> - <a class="lh-1 ms-3" style="padding-top: 1px;" - data-bs-toggle="tooltip" data-bs-placement="right" title="${description.description}"> - ${getInfoSvg()} - </a> - </div> - <div class="progress col ms-2 fw-bold" style="height: 20px;" - data-bs-toggle="tooltip" data-bs-placement="top" title="${progressTooltip}"> - <div class="progress-bar ${bgColor}" role="progressbar" style="width: ${currentWidth}%">${current}</div> - <div class="progress-bar bg-success" role="progressbar" style="width: ${maxAllowedWidth}%">${maxAllowedLabel}</div> - </div> - </div> - ` - $(`#${id}-flavor-info-div`).append(diagramHtml); - } - - // The lab has a flavor configured or is a new lab, but we could not get any flavor information - if (((window.userOptions[id] || {}).flavor || id == "new-jupyterlab") && $.isEmptyObject(systemFlavors)) { - var noFlavorsHtml = ` - <div class="row g-0 mt-3"> - <div class="col-4"></div> - <div class="col ms-2 fw-bold text-danger">No flavors could be fetched. Try logging out and back in to fix the issue.</div> - </div> - `; - $(`#${id}-flavor-info-div`).append(noFlavorsHtml); - } - } - - // Updates number of users in ther footer - var updateNumberOfUsers = function () { - api.api_request("usercount", { - success: function (data) { - // Get all systems from footer and track if updated - var systems = {}; - $("div[id^='ampel'").each((i, e) => { - let system = $(e).attr("id").split('-')[1]; - systems[system] = false; - }) - // Update systems with info from request - for (const [system, usercount] of Object.entries(data)) { - switch (system) { - case 'jupyterhub': - $("#jupyter-users").html(usercount); - systems['jupyter'] = true; - break; - case 'JSC-Cloud': - $(`#jsccloud-users`).html(usercount['total']); - systems['jsccloud'] = true; - break; - default: - $(`#${system.toLowerCase()}-users`).html(usercount['total']); - systems[`${system.toLowerCase()}`] = true; - var partitionInfos = ""; - for (const [partition, users] of Object.entries(usercount['partitions'])) { - partitionInfos += `\n${partition}: ${users}`; - } - $(`#${system.toLowerCase()}-users`) - .parents("[data-bs-toggle]") - .attr("data-bs-original-title", `Number of active servers${partitionInfos}`); - } - } - // If there was no info about a system, set running labs to 0 and reset tooltip - for (const [system, systemInfo] of Object.entries(systems)) { - if (systemInfo == false) { - $(`#${system}-users`).html(0); - $(`#${system.toLowerCase()}-users`) - .parents("[data-toggle]") - .attr("data-bs-original-title", `Number of active servers`); - } - } - } - }) - } - - var utils = { - parseJSON: parseJSON, - getId: getId, - getSpecificValuesInTab: getSpecificValuesInTab, - getLabConfigSelectValues: getLabConfigSelectValues, - setLabAsNA: setLabAsNA, - setSpawnActive: setSpawnActive, - updateProgressState: updateProgressState, - appendToLog: appendToLog, - updateSpawnEvents: updateSpawnEvents, - createFlavorInfo: createFlavorInfo, - updateNumberOfUsers: updateNumberOfUsers, - }; - - return utils; -}) \ No newline at end of file diff --git a/static/js/jhapi.js b/static/js/jhapi.js index 23f64e6e76ca6b14f3dda120772019a00112b1a5..862696c7510f639fe2255a1cc9eceb4d210eb493 100755 --- a/static/js/jhapi.js +++ b/static/js/jhapi.js @@ -103,8 +103,7 @@ define(["jquery", "utils"], function ($, utils) { options = update(options, { type: "POST" }); options.data = JSON.stringify({ failed: true, - progress: 100, - html_message: "<details><summary>Start cancelled by user.</summary>You clicked the cancel button.</details>" + progress: 100 }); this.api_request( utils.url_path_join("users/progress/events", user, server_name), @@ -118,7 +117,7 @@ define(["jquery", "utils"], function ($, utils) { options.data = JSON.stringify({ failed: true, progress: 100, - html_message: "<details><summary>Start cancelled by user.</summary>You clicked the cancel button.</details>" + html_message: "<details><summary>Start cancelled.</summary></details>" }); this.api_request( utils.url_path_join("users/progress/events", user), @@ -218,18 +217,6 @@ define(["jquery", "utils"], function ($, utils) { ); }; - JHAPI.prototype.remove_2fa = function (user, options) { - options = options || {}; - options = update(options, { type: "DELETE", dataType: null }); - this.api_request(utils.url_path_join("2FA"), options); - }; - - JHAPI.prototype.activate_2fa = function (user, options) { - options = options || {}; - options = update(options, { type: "POST", dataType: null }); - this.api_request(utils.url_path_join("2FA"), options); - }; - JHAPI.prototype.shutdown_hub = function (data, options) { options = options || {}; options = update(options, { type: "POST" }); diff --git a/templates/footer.html b/templates/footer.html index dad8e1e4bd524553890cf7fbcd025cef62066fe2..931ea1331854254c9c0da7d48bf9700dad5b3499 100644 --- a/templates/footer.html +++ b/templates/footer.html @@ -17,7 +17,7 @@ <footer class="navbar mt-auto p-0"> <div id="footer-top" class="container-fluid justify-content-evenly p-4"> {#- We create a carousel to be able to show all systems in the footer #} - <div id="footerSystemsCarousel" class="carousel carousel-dark slide w-100" data-bs-ride="carousel" data-bs-interval="10000"> + <div id="footerSystemsCarousel" data-sse-usercount class="carousel carousel-dark slide w-100" data-bs-ride="carousel" data-bs-interval="10000"> <div id="carousel-inner" class="carousel-inner"> <!-- Carousel items will be injected here dynamically via JavaScript --> </div> @@ -57,9 +57,8 @@ {%- block script -%} <script type="text/javascript"> -require(["jquery", "home/utils"], function ( - $, - utils +require(["jquery"], function ( + $ ) { "use strict"; @@ -148,13 +147,16 @@ require(["jquery", "home/utils"], function ( } const ampelHtml = ` - <div id="ampel-${systemLower}" class="text-center"> + <div id="ampel-${systemLower}" class="text-center" + data-system=${system} + data-systemlower=${systemLower} + > <img class="ampel-img" src="${staticUrl}/images/footer/systems/${systemLower}.svg?v=${imgVersion}" /> <a id="ampel-${systemLower}-tooltip" href="https://status.jsc.fz-juelich.de/services/${systemId}" target="_blank" class="align-middle" - data-bs-toggle="tooltip" + data-bs-toggle="tooltip" data-bs-placement="top"> ${displayName} </a> @@ -259,18 +261,48 @@ require(["jquery", "home/utils"], function ( $(document).ready(function() { createCarouselPages(); updateSystemHoverTooltips(); - utils.updateNumberOfUsers(); }) - if (!(window.location.pathname.endsWith("home") || window.location.pathname.includes("spawn-pending"))) { - console.log("setup SSE") - let userSpawnerNotificationUrl = `${jhdata.base_url}api/users/${jhdata.user}/notifications/spawners?_xsrf=${window.jhdata.xsrf_token}`; - evtSourcesGlobal["footer"] = new EventSource(userSpawnerNotificationUrl); - evtSourcesGlobal["footer"].onmessage = (e) => { - utils.updateNumberOfUsers(); - }; - } - + + $(`[data-sse-usercount]`).on("sse", function (event, data) { + var systems = {}; + $("div[id^='ampel']").each((i, e) => { + let system = $(e).attr("id").split('-')[1]; + systems[system] = false; + }) + // Update systems with info from request + for (const [system, usercount] of Object.entries(data)) { + switch (system) { + case 'jupyterhub': + $("#jupyter-users").html(usercount); + systems['jupyter'] = true; + break; + case 'JSC-Cloud': + $(`#jsccloud-users`).html(usercount['total']); + systems['jsccloud'] = true; + break; + default: + $(`#${system.toLowerCase()}-users`).html(usercount['total']); + systems[`${system.toLowerCase()}`] = true; + var partitionInfos = ""; + for (const [partition, users] of Object.entries(usercount['partitions'])) { + partitionInfos += `\n${partition}: ${users}`; + } + $(`#${system.toLowerCase()}-users`) + .parents("[data-bs-toggle]") + .attr("data-bs-original-title", `Number of active servers${partitionInfos}`); + } + } + // If there was no info about a system, set running labs to 0 and reset tooltip + for (const [system, systemInfo] of Object.entries(systems)) { + if (systemInfo == false) { + $(`#${system}-users`).html(0); + $(`#${system.toLowerCase()}-users`) + .parents("[data-toggle]") + .attr("data-bs-original-title", `Number of active servers`); + } + } + }); }) </script> {%- endblock -%} diff --git a/templates/header.html b/templates/header.html index 69698b3c9f429a01101540444de8926bb10e2a56..cd2ab61d9fec26676651a2439a65c0e26d9147c5 100644 --- a/templates/header.html +++ b/templates/header.html @@ -34,6 +34,9 @@ <div class="d-flex"> {%- if user %} <li class="nav-item"><a id="{{prefix}}start-nav-item" class="nav-link text-decoration-none" href="{{ base_url }}home">JupyterLab</a></li> + {%- if auth_state and "geant:dfn.de:fz-juelich.de:jsc:jupyter:workshop_instructors" in auth_state.get("groups", []) %} + <li class="nav-item"><a id="{{prefix}}workshop-manage-nav-item" class="nav-link text-decoration-none" href="{{ base_url }}workshopmanager">Manage Workshops</a></li> + {%- endif -%} {%- if user.admin %} <li class="nav-item"><a id="{{prefix}}admin-nav-item" class="nav-link text-decoration-none" href="{{ base_url }}admin">Admin</a></li> {%- endif -%} diff --git a/templates/home.html b/templates/home.html index b37808aad30948a90276261446a31cfa4650eaef..81eea4456d720b0d30bde1e3a94fdf8fcfd9c8b9 100644 --- a/templates/home.html +++ b/templates/home.html @@ -1,499 +1,55 @@ {%- extends "page.html" -%} -{%- import "macros/home.jinja" as home -%} -{%- import "macros/svgs.jinja" as svg -%} {%- block stylesheet -%} <link rel="stylesheet" href='{{static_url("css/home.css")}}' type="text/css"/> {%- endblock -%} -{#- Set some convenience variables -#} -{%- set lab_spawners = [] -%} -{%- for s in spawners -%} - {%- if s.user_options -%} - {%- if ( - "profile" in s.user_options - and s.user_options.get("profile").startswith("JupyterLab") - ) - or ( - "service" in s.user_options - and s.user_options.get("service").startswith("JupyterLab") - ) - -%} - {%- do lab_spawners.append(s) -%} - {%- endif -%} - {%- endif -%} -{%- endfor -%} -{%- set software_key = "JupyterLab" %} -{%- set software_prefix = "jupyterlab" %} -{%- set new = "new" -%} {# id for new software entry #} - {%- block main -%} -<div class="container-fluid p-4"> - {#- ANNOUNCEMENT #} - {%- if custom_config.get("announcement", {}).get("show", False) %} - {{ home.create_announcement(custom_config) }} - {%- endif -%} - {#- REAUTHENTICATE #} - {%- if auth_state and auth_state.get("reauthenticate", False) %} - <div class="alert bg-info alert-dismissible fade show" style="color: #023d6b;" role="alert"> - <span class="align-middle"> Your access to the HPC-systems has been updated. <a style="text-decoration-line: underline; "href={{base_url}}logout?alldevices=false&stop=false&next=oauth_login> Please click here to reload. </a></span> - <button type="button" class="btn-close" data-bs-dismiss="alert"></button> - </div> - {%- endif -%} - - {#- TABLE #} - <p>You can configure your existing JupyterLabs by expanding the corresponding table row.</p> - - <div class="table-responsive-md"> - <table id="jupyterlabs-table" class="table table-bordered table-striped table-hover table-light align-middle"> - {#- TABLE HEAD #} - <thead class="table-secondary"> - <tr> - <th scope="col" width="1%"></th> - <th scope="col" width="20%">Name</th> - <th scope="col">Configuration</th> - <th scope="col" width="10%;">Status</th> - <th scope="col" width="10%;">Actions</th> - </tr> - </thead> - {#- TABLE BODY #} - <tbody> - {#- New JupyterLab row #} - <tr data-server-id="{{ software_prefix }}-{{ new }}" class="new-spawner-tr summary-tr"> - <th scope="col" class="details-td"> - <div class="d-flex mx-4"> - <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="bi bi-plus-lg m-auto" viewBox="0 0 16 16"> - <path fill-rule="evenodd" d="M8 2a.5.5 0 0 1 .5.5v5h5a.5.5 0 0 1 0 1h-5v5a.5.5 0 0 1-1 0v-5h-5a.5.5 0 0 1 0-1h5v-5A.5.5 0 0 1 8 2Z"/> - </svg> - </div> - </th> - <th scope="row" colspan="100%" class="text-center">New JupyterLab</th> - </tr> - {{ home.create_collapsible_tr(software_key, software_prefix, None, {}, custom_config, lab_id=new) }} - {#- Existing JupyterLab rows -#} - {#- By looping through lab_spawners #} - {# {%- for spawner in lab_spawners -%} - {%- set spawner_name = user.spawners[spawner.name].name -%} - {%- set user_options = spawner.user_options -%} - {{ home.create_summary_tr(software_key, software_prefix, spawner_name, user_options) }} - {{ home.create_collapsible_tr(software_key, software_prefix, spawner_name, user_options, custom_config) }} - {%- endfor %} #} - </tbody> - </table> - </div> {#- table responsive #} -</div> {#- container fluid #} -{%- endblock -%} - +{%- import "macros/table/config/home.jinja" as config %} +{%- import "macros/svgs.jinja" as svg -%} +{%- import "macros/table/variables.jinja" as vars with context %} -{%- block script -%} -<script> -{#- Manually sets some cancel related variables -#} -{%- set cancel_progress_activation = 0 -%} -{#- Percentage when cancel should be disabled again - since it is already in progress -#} -{%- set cancel_progress_deactivation = 99 %} +{%- set pagetype = vars.pagetype_home %} -{#- Save some global variables #} -var evtSources = {}; -var userOptions = {}; -var spawnEvents = {}; +{%- set table_rows = {vars.first_row_id: {}} %} -{%- for spawner in lab_spawners -%} - {%- set userOptions = decrypted_user_options[spawner.name] or {} -%} -userOptions["{{spawner.name}}"] = {{ userOptions | tojson }}; - {%- if spawner.state and spawner.state.get("events") %} -spawnEvents["{{spawner.name}}"] = {{ spawner.state.get("events") | tojson }}; - {%- else -%} - {#- We still want to show a "latest" entry for the log dropdown, - so we manually create an entry for spawners without events #} -spawnEvents["{{spawner.name}}"] = {"latest": []}; - {%- endif -%} +{%- for spawner in user.spawners.values() %} {%- endfor %} -var spawnActive = {}; -{% for spawner in user.spawners.values() -%} - {% if spawner.name -%} - {% if spawner.pending -%} - spawnActive["{{spawner.name}}"] = "pending"; - {% elif spawner.ready -%} - spawnActive["{{spawner.name}}"] = "ready"; - {% else -%} - spawnActive["{{spawner.name}}"] = false; - {%- endif %} +{%- for spawner in spawners %} + {%- if ( spawner.name and spawner.name in user.spawners.keys() ) or + ( spawner.user_options and spawner.user_options.get("name", false) ) %} + {%- set _ = table_rows.update({spawner.name: spawner.user_options}) %} {%- endif %} {%- endfor %} -var systemsHealth = { -threshold: { - interactive: {{custom_config.get("incidentCheck", {}).get("healthThreshold", {}).get("interactive", 50) | int}}, - compute: {{custom_config.get("incidentCheck", {}).get("healthThreshold", {}).get("compute", 40) | int}} - } -}; -{%- for system, system_info in incidents.items() %} -systemsHealth["{{system}}"] = {{ system_info.health }}; -{%- endfor %} - -var flavorInfo = {{ outpostflavors | tojson }}; -</script> - -<script> -/* (Mostly) Jinja dependent functions: - * Define get funtions to enable the use of jinja variables (supplied by the backend) within the JavaScript classes (otherwise they are not reachable) - */ -function getHostname() { - return {{hostname | tojson}} || {}; -} - -function getEntitlements() { - return {{auth_state.get("entitlements", []) | tojson}} || []; -} - -function getMapSystems() { - return {{custom_config.get("mapSystems", {}) | tojson }}; -} - -function getDropdownOptions() { - return {{auth_state.get("options_form", {}).get("dropdown_list", {}) | tojson}}; -} - -function getService(options) { - if ("profile" in options) - return options["profile"].split('/')[1]; - else - return options["service"].split('/')[1]; -} - -function getServiceInfo() { - return {{custom_config.get("services") | tojson}} || {}; -} - -function getShareInfo() { - return {{custom_config.get("share") | tojson}} || {}; -} - -function getBinderRepos() { - return {{custom_config.get("binderRepos") | tojson}} || {}; -} - - -function getBackendServiceInfo() { - return {{custom_config.get("backendServices") | tojson}} || {}; -} - -function getSystemInfo() { - return {{custom_config.get("systems") | tojson}} || {}; -} - -function getReservationInfo() { - return {{auth_state.get("options_form").get("reservations") | tojson}} || {}; -} - -function getResourceInfo() { - return {{auth_state.get("options_form").get("resources") | tojson}} || {}; -} +{%- from "macros/table/table.jinja" import tables with context %} +{%- import "macros/table/content.jinja" as functions with context %} +<div id="toastContainer" class="position-fixed top-0 end-0 p-3"></div> +{{ tables( + config.frontend_config, + functions.home_description, + functions.home_headerlayout, + functions.home_defaultheader, + functions.home_firstheader, + functions.row_content, + { + "stop": "workshopButtonStop", + "start": "workshopButtonStart", + "open": "homeOpen", + "cancel": "workshopButtonCancel", + "del": "homeButtonDelete" + }, + functions.sse_functions +) }} -function getModuleInfo() { - return {{custom_config.get("userModules", {}) | tojson}} || {}; -} -function getInfoSvg() { - return `{{ svg.info_svg | safe }}`; -} -function getLinkSvg() { - return `{{ svg.link_svg | safe }}`; -} - -function get_4_2_system(kwargs) { - return ["JUSYSTEM1", "JUSYSTEM2", "JSC-Cloud"]; -} - -function onEvtMessage(event, id) { - function _updateProgress(infoText, background="", html="") { - $(`#${id}-progress-bar`) - .width(100) - .removeClass("bg-success bg-danger") - .addClass(background) - .html(html); - $(`#${id}-progress-info-text`).html(infoText); - } - - const evt = JSON.parse(event.data); - spawnEvents[id]["latest"].push(evt); - let tr = $(`.summary-tr[data-server-id=${id}]`); - if (evt.progress !== undefined && evt.progress != 0) { - if (evt.progress == 100) { // Spawn finished - evtSources[id].close(); - delete evtSources[id]; - if (evt.failed) { // Spawn failed - spawnActive[id] = false; - // All other UI updates all handled by the evtSourcesGlobal["home"] - // so that they happend after the stop has finished in the backend - } - else if (evt.ready) { // Spawn successful - spawnActive[id] = "ready"; - _updateProgress("running", "bg-success"); - _updateLabButtons(id, true); - } - } - else { // Spawn in progress - spawnActive[id] = "pending"; - let collapsibleTr = $(`.collapsible-tr[data-server-id=${id}]`); - let collapseBtns = collapsibleTr.find("button").not(".nav-link"); - collapseBtns.addClass("disabled"); - if (evt.progress == {{cancel_progress_deactivation}}) { - _updateProgress("cancelling...", "bg-danger", `<b>${evt.progress}%</b>`) - tr.find(".btn-cancel-lab").addClass("disabled"); - } - else { - _updateProgress("spawning...", "", `<b>${evt.progress}%</b>`) - if (evt.progress >= {{cancel_progress_activation}} - && evt.progress < {{cancel_progress_deactivation}}) { - tr.find(".btn-cancel-lab").removeClass("disabled"); - } - } - } - } - - if (evt.html_message !== undefined) { - var htmlMsg = evt.html_message - } else if (evt.message !== undefined) { - var htmlMsg = evt.message; - } - if (htmlMsg) { - // Only append if latest log is selected - if ($(`#${id}-log-select`).val() == "latest") { - // appendToLog($(`#${id}-log`), htmlMsg); - try { htmlMsg = htmlMsg.replace(/ /g, ' '); } - catch (e) { return; } // Not a valid htmlMsg - // Only append if a log message has not been appended yet - var exists = false; - $(`#${id}-log`).children().each(function (i, e) { - let logMsg = $(e).html(); - if (htmlMsg == logMsg) exists = true; - }) - if (!exists) - $(`#${id}-log`).append($('<div class="log-div">').html(htmlMsg)); - } - } -} - -function _updateLabButtons(id, running) { - let tr = $(`.summary-tr[data-server-id=${id}]`); - let collapsibleTr = $(`.collapsible-tr[data-server-id=${id}]`); - let collapseBtns = collapsibleTr.find("button").not(".nav-link"); - if (running) { - // Show open/cancel for starting labs - tr.find(".btn-na-lab, .btn-start-lab, .btn-cancel-lab") - .addClass("disabled") - .hide(); - tr.find(".btn-open-lab, .btn-stop-lab") - .removeClass("disabled") - .show(); - } - else { - // Show start or na for non-running labs - var na = tr.find(".na-status").text() || 0; - if (na != "0") { - tr.find(".btn-na-lab").removeClass("disabled").show(); - tr.find(".btn-start-lab").addClass("disabled").hide(); - } - else { - tr.find(".btn-na-lab").hide() - tr.find(".btn-start-lab").removeClass("disabled").show(); - } - tr.find(".btn-open-lab, .btn-cancel-lab, .btn-stop-lab") - .addClass("disabled").hide(); - } - collapseBtns.removeClass("disabled"); -} -</script> - -<script> -require(["jquery", "jhapi", "home/utils", "home/dropdown-options", "home/lab-configs"], function ( - $, - JHAPI, - utils, - dropdowns, - lab -) { - "use strict"; - - var base_url = window.jhdata.base_url; - var api = new JHAPI(base_url); - - /* - On page load - */ - $(document).ready(function() { - const sharedOptions = JSON.stringify({{ spawner_options_form_values | safe }}); - if (sharedOptions) { - const sharedOp = JSON.parse(sharedOptions); - let id = "{{ server_name }}"; - let available = lab.checkIfAvailable(id, sharedOp); - lab.setUserOptions(id, sharedOp, available); - } - else { - {# for (const id of Object.keys(userOptions)) { - let available = lab.checkIfAvailable(id, userOptions[id]); - lab.setUserOptions(id, userOptions[id], available); - } - updateSpawnProgress(); -#} - - {# Set initial value, which will trigger to fill other dropdowns #} - const defaultOptionTab = "{{ custom_config.get("services", {}).get(software_key, {}).get("frontend", {}).get("versionsSelect", {}).get("tab", "") }}"; - const defaultOptionKey = "{{ custom_config.get("services", {}).get(software_key, {}).get("frontend", {}).get("versionsSelect", {}).get("key", "") }}"; - const defaultOptionValue = "{{ custom_config.get("services", {}).get(software_key, {}).get("frontend", {}).get("versionsSelect", {}).get("defaultValue", "") }}"; - let select = $(`select[id*=${defaultOptionTab}-${defaultOptionKey}]`); - {%- for versionName, versionOptions in custom_config.get("services", {}).get(software_key, {}).get("options", {}).items() %} - select.append(`<option value="{{versionName}}">{{versionOptions.get("name", versionName)}}</option>`); - {%- endfor %} - select.val(defaultOptionValue); - setTimeout(() => { - select.trigger("change"); - }, 50); - - {# dropdowns.updateServices("{{ software_key }}", "{{ software }}-{{ new }}"); #} - - {# $("#{{ software }}-{{ new }}-log-select").prepend(`<option value="latest">latest</option>`); #} - } - - // Remove all tab warnings since initial changes shouldn't cause warnings - $("[id$=tab-warning]").addClass("invisible"); - }) - - // Add event source for user spawner events - let userSpawnerNotificationUrl = `${jhdata.base_url}api/users/${jhdata.user}/notifications/spawners?_xsrf=${window.jhdata.xsrf_token}`; - evtSourcesGlobal["home"] = new EventSource(userSpawnerNotificationUrl); - evtSourcesGlobal["home"].onmessage = (e) => { - const data = JSON.parse(e.data); - const spawning = data.spawning || []; - const stopped = data.stoppedall || []; - var spawnEvents = window.spawnEvents; - utils.updateNumberOfUsers(); - - // Create eventListeners for new labs if they don't exist - for (const id of spawning) { - utils.setSpawnActive(id, "pending"); - if (!(id in spawnEvents)) { - spawnEvents[id] = { "latest": [] }; - } - if (!(id in evtSources)) { - utils.updateSpawnEvents(spawnEvents, id); - - let progressUrl = `${window.jhdata.base_url}api/users/${window.jhdata.user}/servers/${id}/progress?_xsrf=${window.jhdata.xsrf_token}`; - evtSources[id] = new EventSource(progressUrl); - evtSources[id].onmessage = function (e) { - onEvtMessage(e, id); - } - // Reset progress bar and log for new spawns - $(`#${id}-progress-bar`) - .width(0).html("") - .removeClass("bg-danger bg-success"); - $(`#${id}-progress-info-text`).html(""); - $(`#${id}-log`).html(""); - // Update buttons to reflect pending state - let tr = $(`tr.summary-tr[data-server-id=${id}]`); - // _enableTrButtonsRunning - tr.find(".btn-na-lab, .btn-start-lab").hide(); - tr.find(".btn-open-lab, .btn-cancel-lab").show().addClass("disabled"); - } - } - - for (const id of stopped) { - if (!id) continue; // Filter out labs with no name - utils.updateProgressState(id, "reset"); - // Change buttons to start or N/A - _updateLabButtons(id, false); - } - // We have updated flavor information, check if we need to update any flavor UI elements - window.flavorInfo = data.outpostflavors || window.flavorInfo; - for (const id of Object.keys(userOptions)) { - const values = utils.getLabConfigSelectValues(id); - dropdowns.updateFlavors(id, values.service, values.system, values.flavor); - } - - const sharedOptions = JSON.stringify({{ spawner_options_form_values | safe }}); - if (sharedOptions) { - const sharedOp = JSON.parse(sharedOptions); - let id = sharedOp["name"]; - // const sharedValues = utils.getLabConfigSelectValues(id); - // dropdowns.updateFlavors(id, sharedValues.service, sharedValues.system, sharedValues.flavor); - } - else { - // The jinja2 variable "new" points to the id of a new JupyterLab, e.g. "new-jupyterlab" - const newLabValues = utils.getLabConfigSelectValues("{{ software }}-{{ new }}"); - dropdowns.updateFlavors("{{ software }}-{{ new }}", newLabValues.service, newLabValues.system, newLabValues.flavor); - } - }; - - /* - Jinja dependent function definitions - */ - function updateSpawnProgress() { - {%- for spawner in lab_spawners %} - var id = "{{spawner.name}}"; - var latestEvents = spawnEvents[id]["latest"] || []; - // Append log messages - for (const event of latestEvents) { - utils.appendToLog($(`#${id}-log`), event["html_message"]); - } - // Add options to log select and select the latest log by default - var logSelect = $(`#${id}-log-select`); - for (const log in spawnEvents[id]) { - if (log == "latest") logSelect.prepend(`<option value="${log}">${log}</option>`); - else logSelect.append(`<option value="${log}">${log}</option>`); - } - logSelect.val("latest"); - - {%- set s = user.spawners[spawner.name] -%} - {%- if s.active -%} - {%- if s.ready %} - utils.updateProgressState(id, "running"); - {%- elif not s._stop_pending %} - var tr = $(`#${id}.summary-tr`); - tr.find(".btn-open-lab").addClass("disabled"); - var currentProgress = 0; - if (latestEvents.length > 0) { - let lastEvent = latestEvents.slice(-1)[0]; - currentProgress = lastEvent.progress; - } - if (currentProgress < {{cancel_progress_activation}} - || currentProgress >= {{cancel_progress_deactivation}}) { - tr.find(".stop, .cancel").addClass("disabled"); - } - // Disable the delete button during the spawn process. - var collapse = $(`.collapsible-tr[data-server-id=${id}]`); - collapse.find(".btn-delete-lab").addClass("disabled"); - // Update progress with percentage also - $(`#${id}-progress-bar`) - .width(100) - .html(`<b>${currentProgress}%</b>`); - $(`#${id}-progress-info-text`).html("spawning..."); - // Add an event listener to catch and display updates. - var progressUrl = `${jhdata.base_url}api/users/${jhdata.user}/servers/${id}/progress?_xsrf=${window.jhdata.xsrf_token}`; - evtSources[id] = new EventSource(progressUrl); - {%- endif %} - {%- elif s._failed or s._cancel_event_yielded %} - var id = "{{s.name | safe}}"; - utils.updateProgressState(id, "failed"); - {%- endif -%} - - {%- endfor %} - - for (const id in evtSources) { - evtSources[id].onmessage = function (e) { - onEvtMessage(e, id); - } - } - } +{%- endblock -%} -}) -</script> -<script src='{{static_url("js/home/handle-events.js", include_version=True) }}' type="text/javascript" charset="utf-8"></script> -<script src='{{static_url("js/home/handle-servers.js", include_version=True) }}' type="text/javascript" charset="utf-8"></script> -<script> -$("nav [id$=nav-item]").removeClass("active"); -$("#start-nav-item, #collapse-start-nav-item").addClass("active"); -</script> -{%- endblock -%} +{%- block script -%} +{%- import "macros/table/variables.jinja" as vars with context %} +{%- set pagetype = vars.pagetype_home %} +{%- import "macros/table/config/home.jinja" as config with context %} +{%- include "macros/table/elements_js.jinja" with context %} +{%- endblock %} diff --git a/templates/macros/home.jinja b/templates/macros/home.jinja index be375fc6be18e473a277bb095b5e2b10fb8ff097..618800c52520d95b90996eae974e48cff67a98f6 100644 --- a/templates/macros/home.jinja +++ b/templates/macros/home.jinja @@ -12,12 +12,12 @@ </div> {%- endmacro -%} -{%- macro create_summary_tr(software_key, software_prefix, spawner_name, user_options, lab_id="", share_structure=false) -%} +{%- macro create_summary_tr(s, user_options, lab_id="", share_structure=false) -%} {%- set name = user_options.get("name", "") -%} -{%- if lab_id != "" %} {%- set id = software_prefix ~ "-" ~ lab_id -%} -{%- elif spawner_name %} {%- set id = software_prefix ~ "-" ~ spawner_name -%} -{%- else %} {%- set id = software_prefix ~ "-new-jupyterlab" -%} +{%- if lab_id != "" %} {%- set id = lab_id -%} +{%- elif s %} {%- set id = s.name -%} +{%- else %} {%- set id = "new-jupyterlab" -%} {%- endif -%} {%- set system = user_options.get("system", "") -%} @@ -120,103 +120,175 @@ </td> {%- endmacro -%} -{%- macro create_collapsible_tr(software_key, software_prefix, spawner_name, user_options, custom_config, lab_id="", share_structure=false) -%} +{%- macro create_collapsible_tr(s, user_options, custom_config, lab_id="", share_structure=false) -%} {%- set name = user_options.get("name", "") -%} -{%- set new_lab_id = software_prefix ~ "-new" %} -{%- if lab_id != "" %} {%- set id = software_prefix ~ "-" ~ lab_id -%} -{%- elif spawner %} {%- set id = software_prefix ~ "-" ~ spawner_name -%} -{%- else %} {%- set id = software_prefix ~ "-new" -%} +{%- set new_lab_id = "new-jupyterlab" %} +{%- if lab_id != "" %} {%- set id = lab_id -%} +{%- elif s %} {%- set id = s.name -%} +{%- else %} {%- set id = "new-jupyterlab" -%} {%- endif -%} <tr data-server-id="{{id}}" class="collapsible-tr" style="--bs-table-accent-bg: transparent;"> <td colspan="100%" class="p-0"> {#- Remove padding to hide td when collapsed #} <div class="collapse{% if share_structure %} show {% endif %}" id="{{id}}-collapse"> <div class="d-flex align-items-start m-3"> - - - {#- TAB NAV PILLS -#} {%- set nav_tab_margins = "mb-3" %} <div class="nav flex-column nav-pills p-3 ps-0" id="{{ id }}-tab" role="tablist"> - {#- In the custom config we defined the required sidebar navigations for this service - We create a tab + button for each of them, and fill them with all required values. - Later, we will se what we want to show / hide. - This will be updated, everytime an event is triggered. - #} - {%- for navsidebar in custom_config.services.get(software_key, {}).get("frontend", {}).get("navsidebar", []) -%} - {%- set counter = loop.index0 -%} - {%- for key, options in navsidebar.items() -%} - <button class="nav-link {{ nav_tab_margins }} {% if counter > 0 %}d-none{% endif %}" id="{{ id }}-{{ key }}-tab" data-bs-toggle="pill" data-bs-target="#{{ id }}-{{ key }}" type="button" role="tab"> - <span>{{ options.label }}</span> - <span id="{{id}}-resources-tab-warning" class="d-flex invisible"> - {{ svg.warning_svg | safe }} - <span class="visually-hidden">settings changed</span> - </span> - </button> - {%- endfor -%} - {%- endfor -%} + <button class="nav-link active {{ nav_tab_margins }}" id="{{ id }}-config-tab" data-bs-toggle="pill" data-bs-target="#{{ id }}-config" type="button" role="tab">Lab Config</button> + <button class="nav-link {{ nav_tab_margins }}" id="{{ id }}-resources-tab" data-bs-toggle="pill" data-bs-target="#{{ id }}-resources" type="button" role="tab"> + <span>Resources</span> + <span id="{{id}}-resources-tab-warning" class="d-flex invisible"> + {{ svg.warning_svg | safe }} + <span class="visually-hidden">settings changed</span> + </span> + </button> + <button class="nav-link {{ nav_tab_margins }}" id="{{ id }}-modules-tab" data-bs-toggle="pill" data-bs-target="#{{ id }}-modules" type="button" role="tab"> + <span>Kernels and Extensions</span> + <span id="{{id}}-modules-tab-warning" class="d-flex invisible"> + {{ svg.warning_svg | safe }} + <span class="visually-hidden">settings changed</span> + </span> + </button> {%- if id != new_lab_id %} - <button class="nav-link {{ nav_tab_margins }}" id="{{ id }}-logs-tab" data-bs-toggle="pill" data-bs-target="#{{ id }}-logs" type="button" role="tab">Logs</button> + <button class="nav-link {{ nav_tab_margins }}" id="{{ id }}-logs-tab" data-bs-toggle="pill" data-bs-target="#{{ id }}-logs" type="button" role="tab">Logs</button> {%- endif %} </div> - - - {#- TAB NAV CONTENT -#} - {#- We create all elements for all tabs, but everything will be hidden. - It will be filled with values later via JS, same goes for display/hidden #} + {#- We only create empty elements here as they will be filled via JS #} <div class="tab-content w-100" id="{{ id }}-tabContent"> - {# Create a block for each version of the software, always start with the general configuration of the software - We only add `tabs` which are also available in `navsidecar` #} - {%- for version_name, version_options in custom_config.services.get(software_key, {}).get("options", {}).items() %} - {%- for nav_item in custom_config.services.get(software_key, {}).get("frontend", {}).get("navsidebar", []) %} - {%- set counter = loop.index0 -%} - {%- for tab_key in nav_item.keys() %} - {%- set _id = id ~ "-" ~ version_name ~ "-" ~ tab_key %} - <div class="{% if counter > 0 %}d-none{% endif %} tab-pane fade show active" id="{{ _id }}" role="tabpanel"> - <form id="{{ _id }}-form"> - {# This blocks comes from the software.frontend configuration and is added to all versions #} - {%- for tabOption in custom_config.services.get(software_key, {}).get("frontend", {}).get("tabs", {}).get(tab_key, []) %} - {%- if tabOption.show is boolean %} - {%- set show = tabOption.show %} - {%- else %} - {%- set show = false %} - {%- endif %} - {%- if tabOption.type == "inputtext" %} - {{ inputs.create_text_input_2(_id, software_key, tab_key, tabOption.key, tabOption.options, tabOption.get("label", {}), show ) }} - {%- elif tabOption.type == "dropdown" %} - {{ inputs.create_select_2(_id, software_key, tab_key, tabOption.key, tabOption.options, tabOption.get("label", {}), true ) }} - {%- elif tabOption.type == "hr" %} - <hr> - {%- endif %} - {%- endfor %} - {# Version specific #} - <div id={{ _id }}-specific> - {# This block is specific for each version of the software - In the end we can simply show the right div and hide the others #} - {%- for tabOption in version_options.get("frontend", {}).get("tabs", {}).get(tab_key, {}).get("options", []) %} - {%- if tabOption.type == "dropdown" %} - {{ inputs.create_select_2(_id, software_key, tab_key, tabOption.key, tabOption.options, tabOption.label, true ) }} - {%- elif tabOption.type == "flavorsummary" %} - {{ create_flavor_summary(_id, software_key, tab_key, tabOption.key, tabOption.options, true ) }} - {%- elif tabOption.type == "number" %} - {{ inputs.create_number_input_2(_id, software_key, tab_key, tabOption.key, tabOption.options, tabOption.label, true ) }} - {%- elif tabOption.type == "reservationsummary" %} - {{ create_reservation_summary(_id, software_key, tab_key, tabOption.key, true) }} - {%- elif tabOption.type == "multiple_checkboxes" %} - {{ inputs.create_multiple_checkboxes(_id, software_key, tab_key, tabOption.key, tabOption.options, tabOption.label, custom_config, true ) }} - {%- endif %} - {%- endfor %} - </div> - </form> - {{ create_lab_config_buttons_2(_id, add_save_reset_buttons=true) }} - {#- {{ create_lab_config_buttons(id, id == new_lab_id or share_structure) }} #} + {#- Lab Config #} + <div class="tab-pane fade show active" id="{{ id }}-config" role="tabpanel"> + <form id="{{id}}-lab-config-form"> + {{ inputs.create_text_input(id, "name", placeholder="Give your lab a name") }} + {{ inputs.create_select(id, "version", custom_config.get("services").get("JupyterLab").get("optionsName", "Version")) }} + {#- Docker images #} + {%- call inputs.create_text_input(id, "image", + placeholder="e.g. jupyter/datascience-notebook", + warning="Please enter a valid docker image, e.g. jupyter/datascience-notebook", + persistent_hover=True) %} + A public registry docker image starting a single-user notebook server. + <a href='https://jupyter-docker-stacks.readthedocs.io/en/latest/using/selecting.html' target='_blank' style='color: white !important;'>Examples.</a> + {%- endcall %} + {#- Fields for private docker repository #} + {{ inputs.create_checkbox_input(id, "image-private-cb", "Private repository")}} + {%- call inputs.create_text_input(id, "image-private-url", "Image Repository", + placeholder="URL for your image registry", + pattern="^(([a-zA-Z0-9.\-]+)(:[0-9]+)?\/)?([a-zA-Z0-9._\-]+\/)*[a-zA-Z0-9._\-]+(:[a-zA-Z0-9._\-]+|@[A-Fa-f0-9]+(:[A-Fa-f0-9]+)*)?$", + warning="Please enter a valid docker repository URL") %} + {%- endcall %} + {%- call inputs.create_text_input(id, "image-private-user", "Username", + placeholder="Please enter your username") %} + Username for the private docker repository + {%- endcall %} + {{ inputs.create_password_input(id, "image-private-pass", "Password", placeholder="Please enter your password")}} + {#-----#} + {{ inputs.create_checkbox_input(id, "image-mount-cb", "Mount user data")}} + {%- call inputs.create_text_input(id, "image-mount", "User data path", + pattern="^\/[A-Za-z0-9\-\/]+", + warning="Please input a valid Unix-style path, e.g. /mnt/userdata") %} + Path to which your persistent user data will be mounted + {%- endcall %} + <hr> + {#- Repo2Docker fields #} + {{ inputs.create_select(id, key="type", label="Repository") }} + {{ inputs.create_text_input(id, key="repo", label="Repository URL", placeholder="GitHub repository name or URL") }} + {{ inputs.create_text_input(id, key="gitref", label="Git ref (branch,tag, or commit)", placeholder="HEAD") }} + {{ inputs.create_text_input(id, key="notebook", label="URL path to notebook (optional)", placeholder="URL path to notebook (optional)", clazz="optional") }} + {{ inputs.create_select(id, key="notebook_type", label="Notebook Type") }} + + {#- Standard HPC system fields #} + {{ inputs.create_select(id, "system") }} + {{ inputs.create_select(id, "flavor") }} + <div id="{{id}}-flavor-legend-div" class="row align-items-center g-0 mt-4"> + <span class="col-4 fw-bold">Available Flavors</span> + <div class="col d-flex align-items-center ms-2"> + {%- set box_style = "height: 15px; width: 15px; border-radius: 0.25rem;"%} + <div style="{{box_style}} background-color: #198754;"></div> + <span class="ms-1">= Free</span> + <span class="mx-2"></span> + <div style="{{box_style}} background-color: #023d6b;"></div> + <span class="ms-1">= Used</span> + <span class="mx-2"></span> + <div style="{{box_style}} background-color: #dc3545;"></div> + <span class="ms-1">= Limit exceeded</span> + </div> + </div> + <div id="{{id}}-flavor-info-div" class="mb-3"></div> + {{ inputs.create_select(id, "account") }} + {{ inputs.create_select(id, "project") }} + {{ inputs.create_select(id, "partition") }} + <hr id="{{id}}-reservation-hr"> + {{ inputs.create_select(id, "reservation") }} + <div id="{{id}}-reservation-info-div" class="row mb-3"> + {%- set reservation_info_classes = "col-4 fw-bold"%} + <div id="{{id}}-reservation-info" class="col-8 offset-4"> + <div class="row"> + <span class="{{ reservation_info_classes }}">Start Time:</span> + <span id="{{id}}-reservation-start" class="col-auto"></span> + </div> + <div class="row"> + <span class="{{ reservation_info_classes }}">End Time:</span> + <span id="{{id}}-reservation-end" class="col-auto"></span> + </div> + <div class="row"> + <span class="{{ reservation_info_classes }}">State:</span> + <span id="{{id}}-reservation-state" class="col-auto"></span> + </div> + <div class="mt-1"> + <details> + <summary class="fw-bold">Detailed reservation information:</summary> + <pre id="{{id}}-reservation-details"></pre> + {#- TODO: Fix horizontal width upon expanding the detail #} + </details> + </div> </div> - {%- set first_navitem = false %} + </div> + </form> + {{ create_lab_config_buttons(id, id == new_lab_id or share_structure) }} + </div> + {#- Lab Resources #} + <div class="tab-pane fade" id="{{ id }}-resources" role="tabpanel"> + <form id="{{id}}-resources-form"> + {{ inputs.create_number_input(id, "nodes") }} + {{ inputs.create_number_input(id, "gpus", "GPUs") }} + {{ inputs.create_number_input(id, "runtime", "Runtime (minutes)") }} + {{ inputs.create_checkbox_input(id, "xserver-cb", "Activate XServer")}} + {{ inputs.create_number_input(id, "xserver", "Use XServer GPU Index") }} + </form> + {{ create_lab_config_buttons(id, id == new_lab_id or share_structure) }} + </div> + {#- User-selected Modules #} + <div class="tab-pane fade" id="{{ id }}-modules" role="tabpanel"> + <form id="{{id}}-modules-form"> + {%- for module_set in custom_config.get("userModules", {}).keys() %} + <div id="{{id}}-{{module_set}}-div"> + <h4>{{ module_set | title}}</h4> + <div id="{{id}}-{{module_set}}-checkboxes-div" class="row g-0"></div> + </div> {%- endfor %} - {%- endfor %} - {%- endfor %} + </form> + <hr> + <div id="{{id}}-modules-selector-div" class="row g-0"> + {%- set module_cols = "col-sm-6 col-md-4 col-lg-3" %} + <div class="form-check {{module_cols}}"> + <input class="form-check-input module-selector" type="checkbox" id="{{id}}-modules-select-all"> + <label class="form-check-label" for="{{id}}-modules-select-all">Select all</label> + </div> + <div class="form-check {{module_cols}}"> + <input class="form-check-input module-selector" type="checkbox" id="{{id}}-modules-select-none"> + <label class="form-check-label" for="{{id}}-modules-select-none">Deselect all</label> + </div> + </div> + {{ create_lab_config_buttons(id, id == new_lab_id or share_structure) }} + </div> + {#- Lab Logs #} + <div class="tab-pane fade" id="{{ id }}-logs" role="tabpanel"> + {{ inputs.create_select(id, "log") }} + <div id="{{id}}-log" class="card card-body"></div> + {{ create_lab_config_buttons(id, id == new_lab_id or share_structure) }} + </div> </div> {#- End of tab content #} </div> {#- End of d-flex #} </div> {#- End of collapse #} @@ -224,100 +296,6 @@ </tr> {%- endmacro -%} -{%- macro create_share_modal(id) %} -<!-- Modal --> -<div class="modal fade" id="{{ id }}-share-link" role="dialog" tabindex="-1"> - <div class="modal-dialog modal-dialog-centered"> - - <!-- Modal content--> - <div class="modal-content"> - <div class="modal-header"> - <h4 class="modal-title">Share Lab</h4> - <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"> - </div> - <div class="modal-body"> - <p>Share your lab via URL</p> - <a href=""></a> - </div> - <div class="modal-footer"> - <button type="button" class="btn btn-default" data-bs-dismiss="modal">Close</button> - <button type="button" id="{{id}}-copy-btn" class="btn btn-outline-primary" data-bs-toggle="tooltip" data-bs-placement="top" title="Copy to clipboard">Copy</button> - </div> - </div> - - </div> -</div> -{%- endmacro %} - -{%- macro create_reservation_summary(id, software_key, tab_key, input_key, show) %} -<div id="{{id}}-{{input_key}}-reservation-info-div" class="row mb-3{% if not show %} d-none{% endif %}"> - {%- set reservation_info_classes = "col-4 fw-bold"%} - <div id="{{id}}-{{input_key}}-reservation-info" class="col-8 offset-4"> - <div class="row"> - <span class="{{ reservation_info_classes }}">Start Time:</span> - <span id="{{id}}-{{input_key}}-reservation-start" class="col-auto"></span> - </div> - <div class="row"> - <span class="{{ reservation_info_classes }}">End Time:</span> - <span id="{{id}}-{{input_key}}-reservation-end" class="col-auto"></span> - </div> - <div class="row"> - <span class="{{ reservation_info_classes }}">State:</span> - <span id="{{id}}-{{input_key}}-reservation-state" class="col-auto"></span> - </div> - <div class="mt-1"> - <details> - <summary class="fw-bold">Detailed reservation information:</summary> - <pre id="{{id}}-{{input_key}}-reservation-details"></pre> - {#- TODO: Fix horizontal width upon expanding the detail #} - </details> - </div> - </div> -</div> -{%- endmacro %} - -{%- macro create_flavor_summary(id, software_key, tab_key, input_key, options, show) -%} -<div id="{{id}}-{{input_key}}-flavor-legend-div" class="row align-items-center g-0 mt-4{% if not show %} d-none{% endif %}"> - <span class="col-4 fw-bold">{{ options.text }}</span> - <div class="col d-flex align-items-center ms-2"> - {%- set box_style = "height: 15px; width: 15px; border-radius: 0.25rem;"%} - <div style="{{box_style}} background-color: #198754;"></div> - <span class="ms-1">= Free</span> - <span class="mx-2"></span> - <div style="{{box_style}} background-color: #023d6b;"></div> - <span class="ms-1">= Used</span> - <span class="mx-2"></span> - <div style="{{box_style}} background-color: #dc3545;"></div> - <span class="ms-1">= Limit exceeded</span> - </div> -</div> -<div id="{{id}}-{{input_key}}-flavor-info-div" class="mb-3"></div> -{%- endmacro %} - -{%- macro create_lab_config_buttons_2(id, add_save_reset_buttons=true) -%} -<hr> -<div class="d-flex"> - {# everyone gets a share button, we decide later if we would like to show it #} - <button type="button" id="{{id}}-share-btn" class="btn btn-share-lab d-none" data-toggle="modal" data-target="#{{ id }}-share-link">{{ svg.share_svg | safe }} Share </button> - - {%- if add_save_reset_buttons %} - <button type="button" id="{{ id }}-save-btn" class="btn btn-success btn-save-lab me-2">{{ svg.save_svg | safe }} Save </button> - <button type="button" id="{{ id }}-reset-btn" class="btn btn-danger btn-reset-lab me-2">{{ svg.reset_svg | safe }} Reset</button> - {%- endif %} - - {# If Lab is N/A we will use this div to show information later #} - <button type="button" id="{{id}}-na-btn" class="btn btn-secondary btn-na-lab disabled ms-auto me-2" style="display: none;"> - {{ svg.na_svg | safe }} N/A - </button> - <div id="{{id}}-na-info" class="text-muted my-auto" style="display: none; font-size: smaller;"></div> - - - <button type="button" id="{{id}}-delete-btn" class="btn btn-danger btn-delete-lab ms-auto"> {{ svg.delete_svg | safe }} Delete</button> - - {{ create_share_modal(id) }} -</div> -{%- endmacro -%} - {%- macro create_lab_config_buttons(id, start_button_only=false) -%} <hr> <div class="d-flex"> diff --git a/templates/macros/svgs.jinja b/templates/macros/svgs.jinja index 0663de1eb020c077668d27823fdfe48fcc3f0d55..c32638c22fbc90451a3bf24b33aec3b24e60d222 100644 --- a/templates/macros/svgs.jinja +++ b/templates/macros/svgs.jinja @@ -2,6 +2,11 @@ <path d="M8 0c-.176 0-.35.006-.523.017l.064.998a7.117 7.117 0 0 1 .918 0l.064-.998A8.113 8.113 0 0 0 8 0zM6.44.152c-.346.069-.684.16-1.012.27l.321.948c.287-.098.582-.177.884-.237L6.44.153zm4.132.271a7.946 7.946 0 0 0-1.011-.27l-.194.98c.302.06.597.14.884.237l.321-.947zm1.873.925a8 8 0 0 0-.906-.524l-.443.896c.275.136.54.29.793.459l.556-.831zM4.46.824c-.314.155-.616.33-.905.524l.556.83a7.07 7.07 0 0 1 .793-.458L4.46.824zM2.725 1.985c-.262.23-.51.478-.74.74l.752.66c.202-.23.418-.446.648-.648l-.66-.752zm11.29.74a8.058 8.058 0 0 0-.74-.74l-.66.752c.23.202.447.418.648.648l.752-.66zm1.161 1.735a7.98 7.98 0 0 0-.524-.905l-.83.556c.169.253.322.518.458.793l.896-.443zM1.348 3.555c-.194.289-.37.591-.524.906l.896.443c.136-.275.29-.54.459-.793l-.831-.556zM.423 5.428a7.945 7.945 0 0 0-.27 1.011l.98.194c.06-.302.14-.597.237-.884l-.947-.321zM15.848 6.44a7.943 7.943 0 0 0-.27-1.012l-.948.321c.098.287.177.582.237.884l.98-.194zM.017 7.477a8.113 8.113 0 0 0 0 1.046l.998-.064a7.117 7.117 0 0 1 0-.918l-.998-.064zM16 8a8.1 8.1 0 0 0-.017-.523l-.998.064a7.11 7.11 0 0 1 0 .918l.998.064A8.1 8.1 0 0 0 16 8zM.152 9.56c.069.346.16.684.27 1.012l.948-.321a6.944 6.944 0 0 1-.237-.884l-.98.194zm15.425 1.012c.112-.328.202-.666.27-1.011l-.98-.194c-.06.302-.14.597-.237.884l.947.321zM.824 11.54a8 8 0 0 0 .524.905l.83-.556a6.999 6.999 0 0 1-.458-.793l-.896.443zm13.828.905c.194-.289.37-.591.524-.906l-.896-.443c-.136.275-.29.54-.459.793l.831.556zm-12.667.83c.23.262.478.51.74.74l.66-.752a7.047 7.047 0 0 1-.648-.648l-.752.66zm11.29.74c.262-.23.51-.478.74-.74l-.752-.66c-.201.23-.418.447-.648.648l.66.752zm-1.735 1.161c.314-.155.616-.33.905-.524l-.556-.83a7.07 7.07 0 0 1-.793.458l.443.896zm-7.985-.524c.289.194.591.37.906.524l.443-.896a6.998 6.998 0 0 1-.793-.459l-.556.831zm1.873.925c.328.112.666.202 1.011.27l.194-.98a6.953 6.953 0 0 1-.884-.237l-.321.947zm4.132.271a7.944 7.944 0 0 0 1.012-.27l-.321-.948a6.954 6.954 0 0 1-.884.237l.194.98zm-2.083.135a8.1 8.1 0 0 0 1.046 0l-.064-.998a7.11 7.11 0 0 1-.918 0l-.064.998zM4.5 7.5a.5.5 0 0 0 0 1h7a.5.5 0 0 0 0-1h-7z"/> </svg>'-%} +{%- set plus_svg = '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="bi bi-plus-lg m-auto" viewBox="0 0 16 16"> + <path fill-rule="evenodd" d="M8 2a.5.5 0 0 1 .5.5v5h5a.5.5 0 0 1 0 1h-5v5a.5.5 0 0 1-1 0v-5h-5a.5.5 0 0 1 0-1h5v-5A.5.5 0 0 1 8 2Z"></path> +</svg>' +-%} + {%- set start_svg = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-play-fill" viewBox="0 0 16 16"> <path d="m11.596 8.697-6.363 3.692c-.54.313-1.233-.066-1.233-.697V4.308c0-.63.692-1.01 1.233-.696l6.363 3.692a.802.802 0 0 1 0 1.393z"></path> </svg>'-%} diff --git a/templates/macros/table/config/home.jinja b/templates/macros/table/config/home.jinja new file mode 100644 index 0000000000000000000000000000000000000000..7526f81b7712ceb6800ad85dec0086c0c51ae9c9 --- /dev/null +++ b/templates/macros/table/config/home.jinja @@ -0,0 +1,722 @@ +{%- set frontend_config = { + "services": { + "default": "jupyterlab", + "options": { + "jupyterlab": { + "fillingOrder": ["option", "system", "account", "project", "partition"], + "navbar": { + "labconfig": { + "show": true, + "displayName": "Lab Config" + }, + "modules": { + "displayName": "Kernels and Extensions", + "dependency": { + "option": [ + "lmod" + ] + } + }, + "resources": { + "show": false, + "displayName": "Resources", + "trigger": { + "partition": "resourceButton" + }, + "dependency": { + "system": [ + "unicore" + ] + } + }, + "logs": { + "show": true, + "firstRow": false, + "displayName": "Logs" + } + }, + "tabs": { + "labconfig": { + "center": { + "name": { + "input": { + "type": "text", + "options": { + "collect": true, + "enabled": true, + "show": true, + "placeholder": "Give your lab a name" + } + }, + "label": { + "type": "text", + "width": 4, + "value": "Name" + }, + }, + "option": { + "input": { + "type": "select", + "options": { + "collect": true, + "show": true + } + }, + "label": { + "type": "text", + "value": "Select Version" + }, + "trigger": { + "init": "workshopManagerFillOptions" + } + }, + "hr1": { + "input": { + "type": "hr" + } + }, + "system": { + "input": { + "type": "select", + "options": { + "collect": true, + "show": true + } + }, + "label": { + "type": "text", + "value": "Systems" + }, + "trigger": { + "init": "workshopManagerUpdateSystem", + "option": "workshopManagerUpdateSystem" + } + }, + "lrzcb": { + "input": { + "type": "checkbox", + "options": { + "enabled": true, + "default": false + } + }, + "label": { + "type": "text", + "value": "Some Checkbox" + }, + "dependency": { + "option": [ + "lmod" + ], + "system": [ + "kube" + ] + } + }, + "repotype": { + "input": { + "type": "select", + "options": { + "enabled": true + } + }, + "label": { + "type": "text", + "value": "Repository Type" + }, + "trigger": { + "init": "setR2DType" + }, + "dependency": { + "option": [ + "repo2docker" + ] + } + }, + "repourl": { + "input": { + "type": "text", + "options": { + "enabled": "show", + "placeholder": "GitHub repository name or URL", + "patternDisabled": "^([a-zA-Z0-9._-]+\\/[a-zA-Z0-9._-]+(\\.git)?|https?:\\/\\/[a-zA-Z0-9._-]+(\\.[a-z]{2,})?(:\\d+)?\\/[a-zA-Z0-9._-]+\\/[a-zA-Z0-9._-]+(\\.git)?)(\\/)?$" + } + }, + "label": { + "type": "text", + "value": "Repository name or URL" + }, + "dependency": { + "option": [ + "repo2docker" + ] + } + }, + "reporef": { + "input": { + "type": "text", + "options": { + "enabled": true, + "placeholder": "HEAD", + "patternDisabled": "^([a-zA-Z0-9._-]+|([a-f0-9]{7,40})|([a-zA-Z0-9._-]+\\/[a-zA-Z0-9._-]+)|([a-zA-Z0-9._-]+@~\\d+)|@~\\d+)$" + } + }, + "label": { + "type": "text", + "value": "Git ref (branch, tag, or commit)" + }, + "dependency": { + "option": [ + "repo2docker" + ] + } + }, + "repopath": { + "input": { + "type": "text", + "options": { + "enabled": false, + "placeholder": "Path to a notebook file (optional)", + "patternDisabled": "^(\\/?[a-zA-Z0-9/_-]+(?:\\.[a-zA-Z0-9]+)?(?:\\/[a-zA-Z0-9/_-]+(?:\\.[a-zA-Z0-9]+)?)*|[a-zA-Z0-9/_-]+)$" + } + }, + "label": { + "type": "textcheckbox", + "value": "Open notebook or path (optional)", + "options": { + "default": false + } + }, + "dependency": { + "option": [ + "repo2docker" + ] + } + }, + "repopathtype": { + "input": { + "type": "select", + "options": { + "enabled": false, + "show": false + } + }, + "label": { + "type": "textcheckbox", + "value": "Notebook Type", + "options": { + "default": false + } + }, + "trigger": { + "init": "setR2DPathType", + "repopath": "workshopManagerRepoPathType" + }, + "dependency": { + "option": [ + "repo2docker" + ] + } + }, + "image": { + "input": { + "type": "text", + "options": { + "value": "jupyter/datascience-notebook", + "placeholder": "jupyter/datascience-notebook", + "patternDisabled": "^(([a-zA-Z0-9.\\-]+)(:[0-9]+)?\\/)?([a-zA-Z0-9._\\-]+\\/)*[a-zA-Z0-9._\\-]+(:[a-zA-Z0-9._\\-]+|@[A-Fa-f0-9]{64})?$" + } + }, + "label": { + "type": "text", + "value": "Image" + }, + "dependency": { + "option": [ + "custom" + ] + } + }, + "privaterepo": { + "input": { + "type": "text", + "options": { + "enabled": false, + "placeholder": "myregistry.com:5000/myuser/myrepo", + "patternDisabled": "^([a-zA-Z0-9.\\-]+(:[0-9]+)?\\/)?[a-zA-Z0-9._\\-]+\\/[a-zA-Z0-9._\\-]+$" + } + }, + "label": { + "type": "texticoncheckbox", + "value": "Private image registry", + "icontext": "Use private images from your own registry", + "options": { + "default": false + } + }, + "dependency": { + "option": [ + "custom" + ] + } + }, + "privaterepousername": { + "input": { + "type": "text", + "options": { + "show": false, + "placeholder": "Enter your username" + } + }, + "label": { + "type": "texticon", + "value": "Username", + "icontext": "Username for the private registry", + "options": { + "default": false + } + }, + "trigger": { + "privaterepo": "toggleExternalCB" + }, + "dependency": { + "option": [ + "custom" + ] + } + }, + "privaterepopassword": { + "input": { + "type": "text", + "options": { + "secret": true, + "show": false, + "placeholder": "Enter your password" + } + }, + "label": { + "type": "texticon", + "value": "Password", + "icontext": "Password for the private registry", + "options": { + "default": false + } + }, + "trigger": { + "privaterepo": "toggleExternalCB" + }, + "dependency": { + "option": [ + "custom" + ] + } + }, + "mountuserdata": { + "input": { + "type": "text", + "options": { + "enabled": true, + "placeholder": "/home/jovyan/work", + "value": "/home/jovyan/work", + "pattern": "^\\/(?:[^\\/\\0]+\\/)*[^\\/\\0]*$" + } + }, + "label": { + "type": "textcheckbox", + "value": "Mount user data", + "options": { + "default": false + } + }, + "dependency": { + "option": [ + "custom", + "repo2docker" + ] + } + }, + "account": { + "input": { + "type": "select", + "options": { + "enabled": true, + "group": "hpc" + } + }, + "label": { + "type": "text", + "value": "Account" + }, + "trigger": { + "system": "workshopUpdateAccount" + }, + "dependency": { + "system": [ + "unicore" + ] + } + }, + "project": { + "input": { + "type": "select", + "options": { + "enabled": true, + "group": "hpc" + } + }, + "label": { + "type": "text", + "value": "Project" + }, + "trigger": { + "account": "workshopManagerUpdateProject" + }, + "dependency": { + "system": [ + "unicore" + ] + } + }, + "partition": { + "input": { + "type": "select", + "options": { + "enabled": true, + "group": "hpc" + } + }, + "label": { + "type": "text", + "value": "Partition" + }, + "trigger": { + "project": "workshopManagerUpdatePartition", + "system": "workshopManagerUpdatePartition" + }, + "dependency": { + "system": [ + "unicore" + ] + } + }, + "reservation": { + "input": { + "type": "select", + "options": { + "show": false, + "group": "hpc" + } + }, + "label": { + "type": "text", + "value": "Reservation" + }, + "trigger": { + "partition": "workshopManagerUpdateReservation", + "project": "workshopManagerUpdateReservation", + "system": "workshopManagerUpdateReservation" + }, + "triggerOnChange": "workshopManagerUpdateReservation" + }, + "reservationinfo": { + "input": { + "type": "reservationinfo", + "options": { + "show": false + } + }, + "triggerSuffix": "input-div", + "trigger": { + "reservation": "updateReservationInfo" + } + }, + "flavor": { + "input": { + "type": "select", + "options": { + "enabled": true + } + }, + "label": { + "type": "text", + "value": "Flavor" + }, + "trigger": { + "system": "workshopManagerUpdateFlavor" + }, + "dependency": { + "system": [ + "kube" + ] + } + }, + "flavorlegend": { + "input": { + "type": "flavorlegend" + }, + "dependency": { + "system": [ + "kube" + ] + }, + "triggerSuffix": "input-div" + }, + "flavorinfo": { + "input": { + "type": "flavorinfo" + }, + "triggerSuffix": "input-div", + "trigger": { + "system": "updateFlavorInfo" + }, + "dependency": { + "system": [ + "kube" + ] + } + } + }, + }, + "modules": { + "center": { + "extensions": { + "input": { + "type": "multiple_checkboxes" + }, + "label": { + "type": "header", + "value": "Extensions" + }, + "options": { + "group": "modules", + "setName": "extensionSet" + }, + "triggerSuffix": "checkboxes-div", + "trigger": { + "option": "updateMultipleCheckboxes" + }, + "dependency": { + "option": [ + "lmod" + ] + } + }, + "kernels": { + "input": { + "type": "multiple_checkboxes" + }, + "options": { + "group": "modules", + "setName": "kernelSet" + }, + "label": { + "type": "header", + "value": "Kernels" + }, + "triggerSuffix": "checkboxes-div", + "trigger": { + "option": "updateMultipleCheckboxes" + }, + "dependency": { + "option": [ + "lmod" + ] + } + }, + "proxies": { + "input": { + "type": "multiple_checkboxes" + }, + "options": { + "group": "modules", + "setName": "proxySet" + }, + "label": { + "type": "header", + "value": "Proxies" + }, + "triggerSuffix": "checkboxes-div", + "trigger": { + "option": "updateMultipleCheckboxes" + }, + "dependency": { + "option": [ + "lmod" + ] + } + }, + "selecthelper": { + "input": { + "type": "selecthelper" + }, + "triggerSuffix": "input-div" + } + }, + }, + "resources": { + "center": { + "nodes": { + "input": { + "type": "number", + "options": { + "show": false, + "group": "resources", + "collectstatic": true + } + }, + "label": { + "type": "text", + "value": "Nodes" + }, + "trigger": { + "system": "workshopManagerUpdateResourcesElementTrigger", + "partition": "workshopManagerUpdateResourcesElementTrigger" + }, + "dependency": { + "system": [ + "unicore" + ] + } + }, + "runtime": { + "input": { + "type": "number", + "options": { + "show": false, + "group": "resources", + "collectstatic": true + } + }, + "label": { + "type": "text", + "value": "Runtime" + }, + "trigger": { + "system": "workshopManagerUpdateResourcesElementTrigger", + "partition": "workshopManagerUpdateResourcesElementTrigger" + }, + "dependency": { + "system": [ + "unicore" + ] + } + }, + "gpus": { + "input": { + "type": "number", + "options": { + "show": false, + "group": "resources", + "collectstatic": true + } + }, + "label": { + "type": "text", + "value": "GPUs" + }, + "trigger": { + "system": "workshopManagerUpdateResourcesElementTrigger", + "partition": "workshopManagerUpdateResourcesElementTrigger" + }, + "dependency": { + "system": [ + "unicore" + ] + } + }, + "xserver": { + "input": { + "type": "number", + "options": { + "show": false, + "group": "resources", + "collectstatic": true + } + }, + "label": { + "type": "textcheckbox", + "value": "Use XServer GPU index", + "options": { + "default": false + } + }, + "trigger": { + "system": "workshopManagerUpdateResourcesElementTrigger", + "partition": "workshopManagerUpdateResourcesElementTrigger" + }, + "dependency": { + "system": [ + "unicore" + ] + } + } + }, + }, + "logs": { + "center": { + "logcontainer": { + "input": { + "type": "logcontainer" + } + } + }, + }, + "buttonrow": { + "center": { + "buttonrow": { + "input": { + "type": "buttons", + "options": { + "buttons": [ + "share", + "save", + "reset", + "delete", + "startgreen" + ], + "share": { + "text": "Share", + "trigger": "workshopManagerShowLink", + "dependency": { + "option": [ + "repo2docker", + "custom" + ] + } + }, + "startgreen": { + "text": "Start", + "trigger": "homeButtonNew", + "defaultRow": false, + "alignRight": true + }, + "save": { + "trigger": "workshopManagerButtonSave", + "firstRow": false + }, + "reset": { + "firstRow": false, + "trigger": "workshopManagerButtonReset" + }, + "delete": { + "firstRow": false, + "trigger": "homeButtonDelete", + "alignRight": true + } + } + } + } + } + } + }, + "default": { + "tab": "labconfig", + "options": { + "option": "4.2", + "system": "JUWELS" + } + } + } + } + } +}%} \ No newline at end of file diff --git a/templates/macros/table/config/workshop.jinja b/templates/macros/table/config/workshop.jinja new file mode 100644 index 0000000000000000000000000000000000000000..f6a4c761e644ebabc60a089b58cdc0e39f38c79b --- /dev/null +++ b/templates/macros/table/config/workshop.jinja @@ -0,0 +1,667 @@ +{%- set frontend_config = { + "services": { + "default": "jupyterlab", + "options": { + "jupyterlab": { + "navbar": { + "labconfig": { + "show": true, + "displayName": "Lab Config" + }, + "modules": { + "displayName": "Kernels and Extensions", + "dependency": { + "option": [ + "lmod" + ] + } + }, + "resources": { + "show": false, + "displayName": "Resources", + "trigger": { + "partition": "resourceButton" + }, + "dependency": { + "system": [ + "unicore" + ] + } + }, + "logs": { + "show": true, + "displayName": "Logs" + } + }, + "tabs": { + "labconfig": { + "center": { + "name": { + "input": { + "type": "text", + "options": { + "collect": true, + "enabled": true, + "show": true, + "placeholder": "Give your lab a name" + } + }, + "label": { + "type": "text", + "width": 4, + "value": "Name" + }, + "trigger": { + "init": "workshopLabName" + } + }, + "option": { + "input": { + "type": "select", + "options": { + "collect": true, + "show": true + } + }, + "label": { + "type": "text", + "value": "Select Version" + }, + "trigger": { + "init": "workshopManagerFillOptions" + } + }, + "hr1": { + "input": { + "type": "hr" + } + }, + "system": { + "input": { + "type": "select", + "options": { + "collect": true, + "show": true + } + }, + "label": { + "type": "text", + "value": "Systems" + }, + "trigger": { + "init": "workshopManagerUpdateSystem", + "option": "workshopManagerUpdateSystem" + } + }, + "repotype": { + "input": { + "type": "select", + "options": { + "enabled": true + } + }, + "label": { + "type": "text", + "value": "Repository Type" + }, + "trigger": { + "init": "setR2DType" + }, + "dependency": { + "option": [ + "repo2docker" + ] + } + }, + "repourl": { + "input": { + "type": "text", + "options": { + "enabled": "show", + "placeholder": "GitHub repository name or URL", + "patternDisabled": "^([a-zA-Z0-9._-]+\\/[a-zA-Z0-9._-]+(\\.git)?|https?:\\/\\/[a-zA-Z0-9._-]+(\\.[a-z]{2,})?(:\\d+)?\\/[a-zA-Z0-9._-]+\\/[a-zA-Z0-9._-]+(\\.git)?)(\\/)?$" + } + }, + "label": { + "type": "text", + "value": "Repository name or URL" + }, + "dependency": { + "option": [ + "repo2docker" + ] + } + }, + "reporef": { + "input": { + "type": "text", + "options": { + "enabled": true, + "placeholder": "HEAD", + "patternDisabled": "^([a-zA-Z0-9._-]+|([a-f0-9]{7,40})|([a-zA-Z0-9._-]+\\/[a-zA-Z0-9._-]+)|([a-zA-Z0-9._-]+@~\\d+)|@~\\d+)$" + } + }, + "label": { + "type": "text", + "value": "Git ref (branch, tag, or commit)" + }, + "dependency": { + "option": [ + "repo2docker" + ] + } + }, + "repopath": { + "input": { + "type": "text", + "options": { + "enabled": false, + "placeholder": "Path to a notebook file (optional)", + "patternDisabled": "^(\\/?[a-zA-Z0-9/_-]+(?:\\.[a-zA-Z0-9]+)?(?:\\/[a-zA-Z0-9/_-]+(?:\\.[a-zA-Z0-9]+)?)*|[a-zA-Z0-9/_-]+)$" + } + }, + "label": { + "type": "textcheckbox", + "value": "Open notebook or path (optional)", + "options": { + "default": false + } + }, + "dependency": { + "option": [ + "repo2docker" + ] + } + }, + "repopathtype": { + "input": { + "type": "select", + "options": { + "enabled": false, + "show": false + } + }, + "label": { + "type": "textcheckbox", + "value": "Notebook Type", + "options": { + "default": false + } + }, + "trigger": { + "init": "setR2DPathType", + "repopath": "workshopManagerRepoPathType" + }, + "dependency": { + "option": [ + "repo2docker" + ] + } + }, + "image": { + "input": { + "type": "text", + "options": { + "value": "jupyter/datascience-notebook", + "placeholder": "jupyter/datascience-notebook", + "patternDisabled": "^(([a-zA-Z0-9.\\-]+)(:[0-9]+)?\\/)?([a-zA-Z0-9._\\-]+\\/)*[a-zA-Z0-9._\\-]+(:[a-zA-Z0-9._\\-]+|@[A-Fa-f0-9]{64})?$" + } + }, + "label": { + "type": "text", + "value": "Image" + }, + "dependency": { + "option": [ + "custom" + ] + } + }, + "privaterepo": { + "input": { + "type": "text", + "options": { + "enabled": false, + "placeholder": "myregistry.com:5000/myuser/myrepo", + "patternDisabled": "^([a-zA-Z0-9.\\-]+(:[0-9]+)?\\/)?[a-zA-Z0-9._\\-]+\\/[a-zA-Z0-9._\\-]+$" + } + }, + "label": { + "type": "texticoncheckbox", + "value": "Private image registry", + "icontext": "Use private images from your own registry", + "options": { + "default": false + } + }, + "dependency": { + "option": [ + "custom" + ] + } + }, + "privaterepousername": { + "input": { + "type": "text", + "options": { + "show": false, + "placeholder": "Enter your username" + } + }, + "label": { + "type": "texticon", + "value": "Username", + "icontext": "Username for the private registry", + "options": { + "default": false + } + }, + "trigger": { + "privaterepo": "toggleExternalCB" + }, + "dependency": { + "option": [ + "custom" + ] + } + }, + "privaterepopassword": { + "input": { + "type": "text", + "options": { + "show": false, + "placeholder": "Enter your password" + } + }, + "label": { + "type": "texticon", + "value": "Password", + "icontext": "Password for the private registry", + "options": { + "default": false + } + }, + "trigger": { + "privaterepo": "toggleExternalCB" + }, + "dependency": { + "option": [ + "custom" + ] + } + }, + "mountuserdata": { + "input": { + "type": "text", + "options": { + "enabled": true, + "placeholder": "/home/jovyan/work", + "value": "/home/jovyan/work", + "pattern": "^\\/(?:[^\\/\\0]+\\/)*[^\\/\\0]*$" + } + }, + "label": { + "type": "textcheckbox", + "value": "Mount user data", + "options": { + "default": true + } + }, + "dependency": { + "option": [ + "custom" + ] + } + }, + "account": { + "input": { + "type": "select", + "options": { + "enabled": true + } + }, + "label": { + "type": "text", + "value": "Account" + }, + "trigger": { + "system": "workshopUpdateAccount" + }, + "dependency": { + "system": [ + "unicore" + ] + } + }, + "project": { + "input": { + "type": "select", + "options": { + "enabled": true + } + }, + "label": { + "type": "text", + "value": "Project" + }, + "trigger": { + "account": "workshopManagerUpdateProject" + }, + "dependency": { + "system": [ + "unicore" + ] + } + }, + "partition": { + "input": { + "type": "select", + "options": { + "enabled": true + } + }, + "label": { + "type": "text", + "value": "Partition" + }, + "trigger": { + "project": "workshopManagerUpdatePartition", + "system": "workshopManagerUpdatePartition" + }, + "dependency": { + "system": [ + "unicore" + ] + } + }, + "reservation": { + "input": { + "type": "select", + "options": { + "show": false + } + }, + "label": { + "type": "text", + "value": "Reservation" + }, + "trigger": { + "partition": "workshopManagerUpdateReservation", + "project": "workshopManagerUpdateReservation", + "system": "workshopManagerUpdateReservation" + }, + "triggerOnChange": "workshopManagerUpdateReservation" + }, + "reservationinfo": { + "input": { + "type": "reservationinfo", + "options": { + "show": false + } + }, + "triggerSuffix": "input-div", + "trigger": { + "reservation": "updateReservationInfo" + } + }, + "flavor": { + "input": { + "type": "select", + "options": { + "enabled": true + } + }, + "label": { + "type": "text", + "value": "Flavor" + }, + "trigger": { + "system": "workshopManagerUpdateFlavor" + }, + "dependency": { + "system": [ + "kube" + ] + } + }, + "flavorlegend": { + "input": { + "type": "flavorlegend" + }, + "dependency": { + "system": [ + "kube" + ] + }, + "triggerSuffix": "input-div" + }, + "flavorinfo": { + "input": { + "type": "flavorinfo" + }, + "triggerSuffix": "input-div", + "trigger": { + "system": "updateFlavorInfo" + }, + "dependency": { + "system": [ + "kube" + ] + } + } + }, + }, + "modules": { + "center": { + "extensions": { + "input": { + "type": "multiple_checkboxes" + }, + "label": { + "type": "header", + "value": "Extensions" + }, + "options": { + "group": "modules", + "setName": "extensionSet" + }, + "triggerSuffix": "checkboxes-div", + "trigger": { + "option": "updateMultipleCheckboxes" + }, + "dependency": { + "option": [ + "lmod" + ] + } + }, + "kernels": { + "input": { + "type": "multiple_checkboxes" + }, + "options": { + "group": "modules", + "setName": "kernelSet" + }, + "label": { + "type": "header", + "value": "Kernels" + }, + "triggerSuffix": "checkboxes-div", + "trigger": { + "option": "updateMultipleCheckboxes" + }, + "dependency": { + "option": [ + "lmod" + ] + } + }, + "proxies": { + "input": { + "type": "multiple_checkboxes" + }, + "options": { + "group": "modules", + "setName": "proxySet" + }, + "label": { + "type": "header", + "value": "Proxies" + }, + "triggerSuffix": "checkboxes-div", + "trigger": { + "option": "updateMultipleCheckboxes" + }, + "dependency": { + "option": [ + "lmod" + ] + } + }, + "selecthelper": { + "input": { + "type": "selecthelper" + }, + "triggerSuffix": "input-div" + } + }, + }, + "resources": { + "center": { + "nodes": { + "input": { + "type": "number", + "options": { + "show": false, + "group": "resources", + "collectstatic": true + } + }, + "label": { + "type": "text", + "value": "Nodes" + }, + "trigger": { + "system": "workshopManagerUpdateResourcesElementTrigger", + "partition": "workshopManagerUpdateResourcesElementTrigger" + }, + "dependency": { + "system": [ + "unicore" + ] + } + }, + "runtime": { + "input": { + "type": "number", + "options": { + "show": false, + "group": "resources", + "collectstatic": true + } + }, + "label": { + "type": "text", + "value": "Runtime" + }, + "trigger": { + "system": "workshopManagerUpdateResourcesElementTrigger", + "partition": "workshopManagerUpdateResourcesElementTrigger" + }, + "dependency": { + "system": [ + "unicore" + ] + } + }, + "gpus": { + "input": { + "type": "number", + "options": { + "show": false, + "group": "resources", + "collectstatic": true + } + }, + "label": { + "type": "text", + "value": "GPUs" + }, + "trigger": { + "system": "workshopManagerUpdateResourcesElementTrigger", + "partition": "workshopManagerUpdateResourcesElementTrigger" + }, + "dependency": { + "system": [ + "unicore" + ] + } + }, + "xserver": { + "input": { + "type": "number", + "options": { + "show": false, + "group": "resources", + "collectstatic": true + } + }, + "label": { + "type": "textcheckbox", + "value": "Use XServer GPU index", + "options": { + "default": false + } + }, + "trigger": { + "system": "workshopManagerUpdateResourcesElementTrigger", + "partition": "workshopManagerUpdateResourcesElementTrigger" + }, + "dependency": { + "system": [ + "unicore" + ] + } + } + }, + }, + "logs": { + "center": { + "logcontainer": { + "input": { + "type": "logcontainer" + } + } + }, + }, + "buttonrow": { + "center": { + "buttonrow": { + "input": { + "type": "buttons", + "options": { + "buttons": [ + "reset" + ], + "reset": { + "firstRow": false, + "trigger": "workshopManagerButtonReset" + } + } + } + } + } + } + }, + "default": { + "tab": "labconfig", + "options": { + "option": "4.2", + "system": "JUWELS" + } + } + } + } + } +}%} \ No newline at end of file diff --git a/templates/macros/table/config/workshop_manager.jinja b/templates/macros/table/config/workshop_manager.jinja new file mode 100644 index 0000000000000000000000000000000000000000..c74a320ebe0efb72ea4c49de4764e12241abbe5b --- /dev/null +++ b/templates/macros/table/config/workshop_manager.jinja @@ -0,0 +1,775 @@ +{% set frontend_config = { + "services": { + "default": "jupyterlab", + "options": { + "jupyterlab": { + "navbar": {}, + "tabs": { + "default": { + "left": { + "workshopid": { + "input": { + "type": "text", + "options": { + "collect": true, + "alwaysDisabled": true, + "enabled": false, + "show": true, + "placeholder": "An ID will be generated for you", + "placeholderInstructor": "Choose a descriptive ID", + "pattern": "[a-z][a-z0-9_\\-]*", + "warning": "Allowed chars: a-z 0-9 _ - . Must start with a lowercase latter ([a-z][a-z0-9_-]*)", + "group": "none" + } + }, + "trigger": { + "init": "workshopManagerWorkshopId" + }, + "label": { + "type": "texticonclick", + "width": 6, + "value": "Workshop ID:", + "icontext": "For more information check out <a href='https://jupyterjsc.pages.jsc.fz-juelich.de/docs/jupyterjsc/users/jupyterlab/4.2/' target='_blank'>documentation</a>" + } + }, + "description": { + "input": { + "type": "text", + "options": { + "collect": true, + "show": true, + "required": true, + "group": "none", + "warning": "A description of your workshop is required. This will be displayed to users to help them select the appropriate workshop." + } + }, + "label": { + "type": "text", + "width": 6, + "value": "Description:" + } + }, + "enddate": { + "input": { + "type": "date", + "options": { + "show": true, + "enabled": false, + "group": "none", + "instructor": "enabled" + } + }, + "label": { + "type": "text", + "width": 6, + "value": "Available until:", + "options": { + "name": "public" + } + } + }, + "public": { + "input": { + "type": "checkbox", + "options": { + "group": "none", + "instructor": "show", + "show": false, + "default": false, + "enabled": true + } + }, + "label": { + "type": "text", + "width": 6, + "value": "List workshop at /hub/workshops:" + } + }, + "expertmode": { + "input": { + "type": "checkbox", + "options": { + "default": false, + "group": "none", + "instructor": "show", + "enabled": true + } + }, + "trigger": { + "init": "workshopManagerToggleExpertMode" + }, + "triggerOnChange": "workshopManagerToggleExpertMode", + "label": { + "type": "texticon", + "icontext": "Expert Mode allows you to select multiple Options + Systems", + "width": 6, + "value": "Enable expert mode" + } + }, + "buttons": { + "input": { + "type": "buttons", + "options": { + "summaryButtons": [ + "new", + "open" + ], + "buttons": [ + "reset", + "delete", + "share", + "save", + "new" + ], + "share": { + "text": "Show Link", + "trigger": "workshopManagerShowLink", + "align-right": true, + "firstRow": false + }, + "save": { + "trigger": "workshopManagerButtonSave", + "firstRow": false + }, + "new": { + "trigger": "workshopManagerButtonNew", + "align-right": true, + "text": "Create Workshop", + "textFirst": true, + "firstRow": true + }, + "reset": { + "firstRow": false, + "trigger": "workshopManagerButtonReset" + }, + "delete": { + "firstRow": false, + "trigger": "workshopManagerButtonDelete" + } + } + } + } + }, + "right": { + "option": { + "input": { + "type": "select", + "options": { + "show": true, + "enabled": false + } + }, + "label": { + "type": "textcheckbox", + "value": "Select Version", + "options": { + "default": false + } + }, + "trigger": { + "init": "workshopManagerFillOptions" + } + }, + "system": { + "input": { + "type": "select", + "options": { + "show": true, + "enabled": false + } + }, + "label": { + "type": "textcheckbox", + "value": "Systems", + "options": { + "default": false + } + }, + "trigger": { + "init": "workshopManagerUpdateSystem", + "option": "workshopManagerUpdateSystem" + } + }, + "repotype": { + "input": { + "type": "select", + "options": { + "enabled": false + } + }, + "label": { + "type": "textcheckbox", + "value": "Repository Type", + "options": { + "default": false + } + }, + "trigger": { + "init": "setR2DType" + }, + "dependency": { + "option": [ + "repo2docker" + ] + } + }, + "repourl": { + "input": { + "type": "text", + "options": { + "enabled": false, + "placeholder": "GitHub repository name or URL", + "patternDisabled": "^([a-zA-Z0-9._-]+\\/[a-zA-Z0-9._-]+(\\.git)?|https?:\\/\\/[a-zA-Z0-9._-]+(\\.[a-z]{2,})?(:\\d+)?\\/[a-zA-Z0-9._-]+\\/[a-zA-Z0-9._-]+(\\.git)?)(\\/)?$" + } + }, + "label": { + "type": "textcheckbox", + "value": "Repository name or URL", + "options": { + "default": false + } + }, + "dependency": { + "option": [ + "repo2docker" + ] + } + }, + "reporef": { + "input": { + "type": "text", + "options": { + "enabled": false, + "placeholder": "HEAD", + "patternDisabled": "^([a-zA-Z0-9._-]+|([a-f0-9]{7,40})|([a-zA-Z0-9._-]+\\/[a-zA-Z0-9._-]+)|([a-zA-Z0-9._-]+@~\\d+)|@~\\d+)$" + } + }, + "label": { + "type": "textcheckbox", + "value": "Git ref (branch, tag, or commit)", + "options": { + "default": false + } + }, + "dependency": { + "option": [ + "repo2docker" + ] + } + }, + "repopath": { + "input": { + "type": "text", + "options": { + "enabled": false, + "placeholder": "Path to a notebook file (optional)", + "patternDisabled": "^(\\/?[a-zA-Z0-9/_-]+(?:\\.[a-zA-Z0-9]+)?(?:\\/[a-zA-Z0-9/_-]+(?:\\.[a-zA-Z0-9]+)?)*|[a-zA-Z0-9/_-]+)$" + } + }, + "label": { + "type": "textcheckbox", + "value": "Open notebook or path (optional)", + "options": { + "default": false + } + }, + "dependency": { + "option": [ + "repo2docker" + ] + } + }, + "repopathtype": { + "input": { + "type": "select", + "options": { + "enabled": false, + "show": false + } + }, + "label": { + "type": "textcheckbox", + "value": "Notebook Type", + "options": { + "default": false + } + }, + "trigger": { + "init": "setR2DPathType", + "repopath": "workshopManagerRepoPathType" + }, + "dependency": { + "option": [ + "repo2docker" + ] + } + }, + "image": { + "input": { + "type": "text", + "options": { + "enabled": false, + "value": "jupyter/datascience-notebook", + "placeholder": "jupyter/datascience-notebook", + "patternDisabled": "^(([a-zA-Z0-9.\\-]+)(:[0-9]+)?\\/)?([a-zA-Z0-9._\\-]+\\/)*[a-zA-Z0-9._\\-]+(:[a-zA-Z0-9._\\-]+|@[A-Fa-f0-9]{64})?$" + } + }, + "label": { + "type": "textcheckbox", + "value": "Image", + "options": { + "default": false + } + }, + "dependency": { + "option": [ + "custom" + ] + } + }, + "privaterepo": { + "input": { + "type": "text", + "options": { + "enabled": false, + "placeholder": "myregistry.com:5000/myuser/myrepo", + "patternDisabled": "^([a-zA-Z0-9.\\-]+(:[0-9]+)?\\/)?[a-zA-Z0-9._\\-]+\\/[a-zA-Z0-9._\\-]+$" + } + }, + "label": { + "type": "textcheckbox", + "value": "Private image repository", + "options": { + "default": false + } + }, + "dependency": { + "option": [ + "custom" + ] + } + }, + "mountuserdata": { + "input": { + "type": "text", + "options": { + "enabled": false, + "placeholder": "/home/jovyan/work", + "value": "/home/jovyan/work", + "pattern": "^\\/(?:[^\\/\\0]+\\/)*[^\\/\\0]*$" + } + }, + "label": { + "type": "textcheckbox", + "value": "Mount user data", + "options": { + "default": false + } + }, + "dependency": { + "option": [ + "custom" + ] + } + }, + "extensions": { + "input": { + "type": "select", + "options": { + "group": "modules", + "enabled": false, + "size": 4, + "multiple": true, + "setName": "extensionSet" + } + }, + "label": { + "type": "textcheckbox", + "value": "Extensions", + "options": { + "default": false + } + }, + "trigger": { + "option": "workshopManagerUpdateModuleWorkshop" + }, + "dependency": { + "option": [ + "lmod" + ] + } + }, + "kernels": { + "input": { + "type": "select", + "options": { + "group": "modules", + "enabled": false, + "size": 4, + "multiple": true, + "setName": "kernelSet" + } + }, + "label": { + "type": "textcheckbox", + "value": "Kernels", + "options": { + "default": false + } + }, + "trigger": { + "option": "workshopManagerUpdateModuleWorkshop" + }, + "dependency": { + "option": [ + "lmod" + ] + } + }, + "proxies": { + "input": { + "type": "select", + "options": { + "group": "modules", + "enabled": false, + "size": 4, + "multiple": true, + "setName": "proxySet" + } + }, + "label": { + "type": "textcheckbox", + "value": "Proxies", + "options": { + "default": false + } + }, + "trigger": { + "option": "workshopManagerUpdateModuleWorkshop" + }, + "dependency": { + "option": [ + "lmod" + ] + } + }, + "project": { + "input": { + "type": "select", + "options": { + "enabled": false, + "multiple": true, + "size": 4 + } + }, + "label": { + "type": "textcheckbox", + "options": { + "default": false + }, + "value": "Project" + }, + "trigger": { + "system": "workshopManagerUpdateProject" + }, + "dependency": { + "system": [ + "unicore" + ] + } + }, + "defaultvaluesproject": { + "input": { + "type": "select", + "options": { + "show": false, + "collect": false, + "enabled": false, + "parent": "project", + "group": "defaultvalues" + } + }, + "label": { + "type": "textcheckbox", + "options": { + "default": false + }, + "value": "Specify default project" + }, + "trigger": { + "project": "defaultValue" + }, + "dependency": { + "system": [ + "unicore" + ] + } + }, + "partition": { + "input": { + "type": "select", + "options": { + "enabled": false, + "multiple": true, + "size": 4 + } + }, + "label": { + "type": "textcheckbox", + "value": "Partition", + "options": { + "default": false + } + }, + "trigger": { + "project": "workshopManagerUpdatePartition", + "system": "workshopManagerUpdatePartition" + }, + "dependency": { + "system": [ + "unicore" + ] + } + }, + "defaultvaluespartition": { + "input": { + "type": "select", + "options": { + "show": false, + "collect": false, + "enabled": false, + "parent": "partition", + "group": "defaultvalues" + } + }, + "label": { + "type": "textcheckbox", + "options": { + "default": false + }, + "value": "Specify default partition" + }, + "trigger": { + "partition": "defaultValue" + }, + "dependency": { + "system": [ + "unicore" + ] + } + }, + "reservation": { + "input": { + "type": "select", + "options": { + "show": false, + "collectstatic": true, + "enabled": false, + "multiple": true, + "size": 4 + } + }, + "label": { + "type": "textcheckbox", + "options": { + "default": false + }, + "triggerOnChange": "toggleCollectCB", + "value": "Reservation" + }, + "trigger": { + "system": "workshopManagerUpdateReservation", + "partition": "workshopManagerUpdateReservation", + "project": "workshopManagerUpdateReservation" + }, + "dependency": { + "system": [ + "unicore" + ] + } + }, + "defaultvaluesreservation": { + "input": { + "type": "select", + "options": { + "show": false, + "collect": false, + "enabled": false, + "parent": "reservation", + "group": "defaultvalues" + } + }, + "label": { + "type": "textcheckbox", + "options": { + "default": false + }, + "value": "Specify default reservation" + }, + "trigger": { + "reservation": "defaultValue" + }, + "dependency": { + "system": [ + "unicore" + ] + } + }, + "nodes": { + "input": { + "type": "number", + "options": { + "show": false, + "group": "resources", + "enabled": false + } + }, + "label": { + "type": "textcheckbox", + "value": "Nodes", + "options": { + "default": false + } + }, + "trigger": { + "system": "workshopManagerUpdateResourcesElementTrigger", + "partition": "workshopManagerUpdateResourcesElementTrigger" + }, + "dependency": { + "system": [ + "unicore" + ] + } + }, + "runtime": { + "input": { + "type": "number", + "options": { + "show": false, + "group": "resources" + } + }, + "label": { + "type": "textcheckbox", + "value": "Runtime", + "options": { + "default": false + } + }, + "trigger": { + "system": "workshopManagerUpdateResourcesElementTrigger", + "partition": "workshopManagerUpdateResourcesElementTrigger" + }, + "dependency": { + "system": [ + "unicore" + ] + } + }, + "gpus": { + "input": { + "type": "number", + "options": { + "show": false, + "enabled": false, + "group": "resources" + } + }, + "label": { + "type": "textcheckbox", + "value": "GPUs", + "options": { + "default": false + } + }, + "trigger": { + "system": "workshopManagerUpdateResourcesElementTrigger", + "partition": "workshopManagerUpdateResourcesElementTrigger" + }, + "dependency": { + "system": [ + "unicore" + ] + } + }, + "xserver": { + "input": { + "type": "number", + "options": { + "show": false, + "group": "resources" + } + }, + "label": { + "type": "textcheckbox", + "value": "Use XServer GPU index", + "options": { + "default": false + } + }, + "trigger": { + "system": "workshopManagerUpdateResourcesElementTrigger", + "partition": "workshopManagerUpdateResourcesElementTrigger" + }, + "dependency": { + "system": [ + "unicore" + ] + } + }, + "flavor": { + "input": { + "type": "select", + "options": { + "enabled": false + } + }, + "label": { + "type": "textcheckbox", + "options": { + "default": false + }, + "value": "Flavor" + }, + "trigger": { + "system": "workshopManagerUpdateFlavor" + }, + "dependency": { + "system": [ + "kube" + ] + } + }, + "envvariables": { + "input": { + "type": "textgrower", + "options": { + "enabled": false, + "required": true, + "show": true, + "pattern": "^[A-Za-z_][A-Za-z0-9_]*=[^\\s]+$", + "placeholder": "KEY=VALUE" + } + }, + "label": { + "type": "textcheckbox", + "value": "Add environment variables", + "options": { + "default": false + } + } + } + } + } + }, + "default": { + "tab": "default", + "options": { + "option": "4.2", + "system": "JUWELS" + } + } + } + } + } +} %} \ No newline at end of file diff --git a/templates/macros/table/content.jinja b/templates/macros/table/content.jinja new file mode 100644 index 0000000000000000000000000000000000000000..b97b59f3006a0b434f242a5b4b1ba00e3b84971b --- /dev/null +++ b/templates/macros/table/content.jinja @@ -0,0 +1,362 @@ +{%- import "macros/table/elements.jinja" as table_elements with context %} +{%- import "macros/svgs.jinja" as svg -%} + +{% macro workshopmanager_description() %} + <p>{{ db_workshops }}</p> + <h2>Workshop Manager</h2> + <p>Select the options users might be able to use during your workshop.</p> + <p>Use shift or ctrl to select multiple items. <a style="color:#fff" href="https://jupyterjsc.pages.jsc.fz-juelich.de/docs/jupyterjsc/" target="_">Click here for more information.</a></p> +{% endmacro %} + +{% macro workshopmanager_headerlayout() %} + <th scope="col" width="1%"></th> + <th scope="col" width="20%">Name</th> + <th scope="col">Description</th> + <th scope="col" class="text-center" width="10%">Action</th> +{% endmacro %} + +{% macro workshopmanager_defaultheader(service_id, row_id, row_options) %} + <th scope="row" class="name-td">{{ row_id }}</th> + <th scope="row" class="description-td">{{ row_options.get("user_options", {}).get("description", "") }}</th> + <th scope="row" class="url-td text-center"> + <button type="button" id="{{ service_id }}-{{row_id}}-open-workshop-btn" class="btn btn-success open-workshop-btn" data-target="#{{ row_id }}-workshop-link" onclick="window.open('https://{{ hostname }}{{ base_url }}workshops/{{ row_id }}');">{{ svg.open_svg | safe }} Open</button> + </th> +{% endmacro %} + +{% macro workshopmanager_firstheader(service_id, row_id, row_options) %} + <th scope="row" class="name-td">New Workshop</th> + <th scope="row" class="description-td">Design a simplified set of options for your workshop to make it more accessible for your students.</th> + <th scope="row" class="url-td text-center"> + <button type="button" data-service="{{ service_id }}" data-row="{{ row_id }}" id="{{ service_id }}-header-{{row_id}}-new-btn" class="btn btn-primary" data-target="#{{ row_id }}-workshop-link">{{ svg.plus_svg | safe }} Create</button> + </th> +{% endmacro %} + +{% macro workshopmanager_row_content(service_id, service_options, row_id, tab_id) %} + {%- for side in service_options.get("tabs", {}).get(tab_id, {}).keys() %} + <div class="col-6"> + {%- for element_id, element_options in service_options.get("tabs", {}).get(tab_id, {}).get(side, {}).items() %} + {{ table_elements.create_element(service_id, row_id, tab_id, element_id, element_options) }} + {%- endfor %} + </div> + {%- endfor %} +{% endmacro %} + +{% macro workshop_headerlayout() %} + <th scope="col" width="1%"></th> + <th scope="col" width="20%">Name</th> + <th scope="col">Description</th> + <th scope="col" class="text-center" width="10%">Status</th> + <th scope="col" class="text-center" width="10%">Action</th> +{% endmacro %} + +{% macro workshop_firstheader(service_id, row_id, row_options) %} + <th scope="row" class="name-td">Workshop {{ workshop_id }}</th> + <th scope="row" class="description-td">{{ db_workshops.get(row_id, {}).get("user_options", {}).get("description") }}</th> + <th scope="row" class="status-td"> + <div class="d-flex justify-content-center"> + <div class="d-flex flex-column"> + <div class="d-flex justify-content-center progress" style="background-color: #d3e4f4; height: 20px; min-width: 100px;"> + <div id="{{ service_id }}-{{ row_id }}-progress-bar" data-service="{{ service_id }}" data-row="{{ row_id }}" data-sse-progress class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" style="width: 0px; margin-right: auto;"></div> + <span id="{{ service_id }}-{{ row_id }}-progress-text" style="position: absolute; width: 100px; text-align: center; line-height: 20px; color: black">0%</span> + </div> + <span id="{{ service_id }}-{{ row_id }}-progress-info-text" class="progress-info-text text-center text-muted" style="font-size: smaller;"></span> + </div> + </div> + </th> + <th scope="row" class="url-td text-center" style="white-space: nowrap"> + <button type="button" + id="{{ service_id }}-{{row_id}}-open-btn-header" + class="btn btn-success" + data-service="{{ service_id }}" + data-row="{{ row_id }}" + data-element="open" + {%- if not spawner.active %} + style="display: none" + {%- endif %} + > + {{ svg.open_svg | safe }} Open + </button> + <button type="button" + id="{{ service_id }}-{{row_id}}-stop-btn-header" + class="btn btn-danger" + data-service="{{ service_id }}" + data-row="{{ row_id }}" + data-element="stop" + {%- if not spawner.ready %} + style="display: none" + {%- endif %} + > + {{ svg.stop_svg | safe }} Stop + </button> + <button type="button" + id="{{ service_id }}-{{row_id}}-cancel-btn-header" + class="btn btn-danger" + data-service="{{ service_id }}" + data-row="{{ row_id }}" + data-element="cancel" + {%- if spawner.ready or not spawner.active %} + style="display: none" + {%- endif %} + > + {{ svg.stop_svg | safe }} Cancel + </button> + <button type="button" + id="{{ service_id }}-{{row_id}}-start-btn-header" + class="btn btn-primary" + data-service="{{ service_id }}" + data-row="{{ row_id }}" + data-element="start" + {%- if spawner.active %} + style="display: none" + {%- endif %} + > + {{ svg.start_svg | safe }} Start + </button> + </th> +{% endmacro %} + + +{% macro sse_functions() %} + $(`[data-sse-progress][id$='-tabContent-div']`).on("sse", function (event, data) { + const $this = $(this); + const serviceId = $this.attr("data-service"); + const rowId = $this.attr("data-row"); + if ( Object.keys(data).includes(rowId) ){ + const ready = data[rowId]?.ready ?? false; + const failed = data[rowId]?.failed ?? false; + const progress = data[rowId]?.progress ?? 10; + if ( ready ) { + updateHeaderButtons(serviceId, rowId, "waiting"); + const url = data[rowId]?.url ?? "{{ url }}"; + checkAndOpenUrl(serviceId, rowId, url); + } else if ( failed ) { + updateHeaderButtons(serviceId, rowId, "stopped"); + $(`button[id^='${serviceId}-${rowId}-'][id$='-btn']`).prop("disabled", false); + } else if ( progress == 99 ) { + updateHeaderButtons(serviceId, rowId, "cancelling"); + $(`button[id^='${serviceId}-${rowId}-'][id$='-btn']`).prop("disabled", true); + } else { + updateHeaderButtons(serviceId, rowId, "starting"); + $(`button[id^='${serviceId}-${rowId}-'][id$='-btn']`).prop("disabled", true); + } + appendToLog(serviceId, rowId, data[rowId]); + } + }); + + $(`[data-sse-progress].progress-bar`).on("sse", function (event, data) { + const $this = $(this); + const rowId = $this.attr("data-row"); + const serviceId = $this.attr("data-service"); + if ( Object.keys(data).includes(rowId) ){ + const ready = data[rowId]?.ready ?? false; + const failed = data[rowId]?.failed ?? false; + const progress = data[rowId]?.progress ?? 10; + let status = "starting"; + if ( ready ) status = "connecting"; + else if ( failed ) status = "stopped"; + else if ( progress == 99 ) status = "cancelling"; + else if ( progress == 0 ) status = ""; + progressBarUpdate(serviceId, rowId, status, progress); + } + }); +{% endmacro %} + +{% macro workshop_description() %} + <p>{{ db_workshops }}</p> +{% endmacro %} + +{% macro home_description() %} + <p>You can configure your existing JupyterLabs by expanding the corresponding table row.</p> +{% endmacro %} + + +{% macro home_headerlayout() %} + <th scope="col" width="1%"></th> + <th scope="col" width="20%">Name</th> + <th scope="col">Configuration</th> + <th scope="col" class="text-center" width="10%">Status</th> + <th scope="col" class="text-center" width="10%">Action</th> +{% endmacro %} + +{% macro home_firstheader(service_id, row_id, row_options) %} + <th scope="row" colspan="100%" class="text-center">New JupyterLab</th> +{% endmacro %} + + +{% macro home_defaultheader(service_id, row_id, row_options) %} + {%- for s in spawners %} + {%- if s.name == row_id %} + {%- set spawner = user.spawners.get(s.name, s) %} + {%- set ready = spawner.ready %} + {%- set active = spawner.active %} + {%- set failed = spawner.failed %} + {%- set progress = 0 %} + {%- if ready %} + {%- set progress = 100 %} + {%- elif active %} + {%- set progress = (spawner.events | last | default({}, true)).get("progress", 0) %} + {%- elif spawner.events %} + {%- set progress = 0 %} + {%- endif %} + + <th scope="row" class="name-td">{{ spawner.user_options.get("name", "") }}</th> + <td scope="row" class="config-td"> + <div style="max-height: 152px; overflow: auto;"> + <div class="row mx-3 mb-1 justify-content-between"> + <div id="{{ service_id }}-{{ row_id }}-config-td-div" class="row col-12 col-md-6 col-lg-12 d-flex align-items-center"> + <div id="{{ service_id }}-{{ row_id }}-config-td-option-div" class="col text-lg-center col-12 col-lg-3"> + <span class="text-muted" style="font-size: smaller;">Option</span><br> + <span id="{{ service_id }}-{{ row_id }}-config-td-option">{{ spawner.user_options.get("option", false) }}</span> + </div> + <div id="{{ service_id }}-{{ row_id }}-config-td-system-div" class="col text-lg-start col-12 col-lg-3"> + <span class="text-muted" style="font-size: smaller;">System</span><br> + <span id="{{ service_id }}-{{ row_id }}-config-td-system">{{ spawner.user_options.get("system", false) }}</span> + </div> + {%- if spawner.user_options.get("project", false) %} + <div id="{{ service_id }}-{{ row_id }}-config-td-project-div" class="col text-lg-start col-12 col-lg-3"> + <span class="text-muted" style="font-size: smaller;">Project</span><br> + <span id="{{ service_id }}-{{ row_id }}-config-td-project">{{ spawner.user_options.get("project", "") }}</span> + </div> + {%- endif %} + {%- if spawner.user_options.get("partition", false) %} + <div id="{{ service_id }}-{{ row_id }}-config-td-partition-div" class="col text-lg-start col-12 col-lg-3"> + <span class="text-muted" style="font-size: smaller;">Partition</span><br> + <span id="{{ service_id }}-{{ row_id }}-config-td-partition">{{ spawner.user_options.get("partition", "") }}</span> + </div> + {%- endif %} + </div> + </div> + </div> + </td> + + <th scope="row" class="status-td"> + <div class="d-flex justify-content-center"> + <div class="d-flex flex-column"> + <div class="d-flex justify-content-center progress" style="background-color: #d3e4f4; height: 20px; min-width: 100px;"> + <div id="{{ service_id }}-{{ row_id }}-progress-bar" + data-service="{{ service_id }}" + data-row="{{ row_id }}" + data-sse-progress + class=" + {%- if ready %} + bg-success + {%- endif %} + progress-bar progress-bar-striped progress-bar-animated + " + role="progressbar" + style="width: {{ progress }}px; margin-right: auto;" + ></div> + <span id="{{ service_id }}-{{ row_id }}-progress-text" + style="position: absolute; + width: 100px; + text-align: center; + line-height: 20px; + {% if progress >= 60 %} + color: white + {% else %} + color: black + {% endif -%} + " + >{{ progress }}%</span> + </div> + <span id="{{ service_id }}-{{ row_id }}-progress-info-text" class="progress-info-text text-center text-muted" style="font-size: smaller;"> + {%- if ready %} + running + {%- elif active %} + starting + {%- elif progress == 99 %} + cancelling + {%- endif %} + </span> + </div> + </div> + </th> + <th scope="row" class="url-td text-center" style="white-space: nowrap"> + <button type="button" + id="{{ service_id }}-{{row_id}}-open-btn-header" + class="btn btn-success" + data-service="{{ service_id }}" + data-row="{{ row_id }}" + data-element="open" + {%- if not spawner.active %} + style="display: none" + {%- endif %}> + {{ svg.open_svg | safe }} Open + </button> + <button type="button" + id="{{ service_id }}-{{row_id}}-stop-btn-header" + class="btn btn-danger" + data-service="{{ service_id }}" + data-row="{{ row_id }}" + data-element="stop" + {%- if not spawner.ready %} + style="display: none" + {%- endif %} + > + {{ svg.stop_svg | safe }} Stop + </button> + <button type="button" + id="{{ service_id }}-{{row_id}}-cancel-btn-header" + class="btn btn-danger" + data-service="{{ service_id }}" + data-row="{{ row_id }}" + data-element="cancel" + {%- if spawner.ready or not spawner.active %} + style="display: none" + {%- endif %} + > + {{ svg.stop_svg | safe }} Cancel + </button> + <button type="button" + id="{{ service_id }}-{{row_id}}-start-btn-header" + class="btn btn-primary" + data-service="{{ service_id }}" + data-row="{{ row_id }}" + data-element="start" + {%- if spawner.active %} + style="display: none" + {%- endif %} + > + {{ svg.start_svg | safe }} Start + </button> + <button type="button" + id="{{ service_id }}-{{row_id}}-na-btn-header" + class="btn btn-secondary btn-na-lab disabled" + data-service="{{ service_id }}" + data-row="{{ row_id }}" + data-element="na" + style="display: none" + > + {{ svg.na_svg | safe }} N/A + </button> + <button type="button" + id="{{ service_id }}-{{row_id}}-del-btn-header" + class="btn btn-danger" + data-service="{{ service_id }}" + data-row="{{ row_id }}" + data-element="del" + style="display: none" + > + {{ svg.delete_svg | safe }} + </button> + </th> + + {%- endif %} + {%- endfor %} +{%- endmacro %} + +{% macro workshop_user_not_ready() %} + <h1 class="user-documentation-info" style="text-align: center; color: red">Account not ready yet!</h1> + <div class="user-documentation-info"> + Your account is not ready for {{ db_workshops.get(workshop_id, {}).get("user_options", {}) }}. + </div> +{% endmacro %} + + +{% macro row_content(service_id, service_options, row_id, tab_id) %} + <div class="col-12"> + {%- for element_id, element_options in service_options.get("tabs", {}).get(tab_id, {}).get("center", {}).items() %} + {{ table_elements.create_element(service_id, row_id, tab_id, element_id, element_options) }} + {%- endfor %} + </div> +{% endmacro %} diff --git a/templates/macros/table/elements.jinja b/templates/macros/table/elements.jinja new file mode 100644 index 0000000000000000000000000000000000000000..ab94ddc25f4627490ebd8a75d7a24989c8fab687 --- /dev/null +++ b/templates/macros/table/elements.jinja @@ -0,0 +1,836 @@ +{%- import "macros/svgs.jinja" as svg -%} +{%- include "macros/table/table_js.jinja" %} + + +{%- macro create_button( + service_id, + row_id, + button, + button_options +)%} + {%- set clazz = "" %} + {%- set svgicon = "" %} + {%- set buttontext = "" %} + {%- if button == "share" %} + {%- set clazz = "" %} + {%- set svgicon = svg.share_svg | safe %} + {%- set buttontext = button_options.get("text", "Share") %} + {%- elif button == "reset" %} + {%- set clazz = "btn-danger" %} + {%- set svgicon = svg.reset_svg | safe %} + {%- set buttontext = button_options.get("text", "Reset") %} + {%- elif button == "delete" %} + {%- set clazz = "btn-danger" %} + {%- set svgicon = svg.delete_svg | safe %} + {%- set buttontext = button_options.get("text", "Delete") %} + {%- elif button == "save" %} + {%- set clazz = "btn-success" %} + {%- set svgicon = svg.save_svg | safe %} + {%- set buttontext = button_options.get("text", "Save") %} + {%- elif button == "create" %} + {%- set clazz = "btn-primary" %} + {%- set svgicon = svg.plus_svg | safe %} + {%- set buttontext = button_options.get("text", "Create") %} + {%- elif button == "start" %} + {%- set clazz = "btn-success" %} + {%- set svgicon = svg.start_svg | safe %} + {%- set buttontext = button_options.get("text", "Start") %} + {%- elif button == "startblue" %} + {%- set clazz = "btn-primary" %} + {%- set svgicon = svg.start_svg | safe %} + {%- set buttontext = button_options.get("text", "Start") %} + {%- elif button == "startgreen" %} + {%- set clazz = "btn-success" %} + {%- set svgicon = svg.start_svg | safe %} + {%- set buttontext = button_options.get("text", "Start") %} + {%- elif button == "new" %} + {%- set clazz = "btn-primary" %} + {%- set svgicon = svg.plus_svg | safe %} + {%- set buttontext = button_options.get("text", "New") %} + {%- elif button == "open" %} + {%- set clazz = "btn-success" %} + {%- set svgicon = svg.open_svg | safe %} + {%- set buttontext = button_options.get("text", "Open") %} + {%- elif button == "retry" %} + {%- set clazz = "btn-success" %} + {%- set svgicon = svg.retry_svg | safe %} + {%- set buttontext = button_options.get("text", "Retry") %} + {%- elif button == "cancel" %} + {%- set clazz = "btn-danger" %} + {%- set svgicon = svg.stop_svg | safe %} + {%- set buttontext = button_options.get("text", "Cancel") %} + {%- elif button == "stop" %} + {%- set clazz = "btn-success" %} + {%- set svgicon = svg.stop_svg | safe %} + {%- set buttontext = button_options.get("text", "Stop") %} + {%- endif %} + <button + type="button" + data-service="{{ service_id }}" + data-row="{{ row_id }}" + id="{{ service_id }}-{{ row_id }}-{{ button }}-btn" + class="btn {{ clazz }} + {% if button_options.get("alignRight", false) -%} ms-auto {%- else -%} me-2 {%- endif %}" + {%- for specific_key, specific_values in button_options.get("dependency", {}).items() %} + data-dependency-{{ specific_key }}="true" + {%- for specific_value in specific_values %} + data-dependency-{{ specific_key }}-{{ specific_value }}="true" + {%- endfor %} + {%- endfor %} + > + {%- if button_options.get("textFirst", false ) %} + {{ buttontext }} + {{ svgicon | safe }} + {%- else %} + {{ svgicon | safe }} + {{ buttontext }} + {%- endif %} + </button> +{%- endmacro %} + + + +{%- macro create_workshop_modal( + service_id, + row_id, + workshop_id) +%} + <div class="modal fade" id="{{ service_id }}-{{ row_id }}-workshop-modal" role="dialog" tabindex="-1"> + <div class="modal-dialog modal-dialog-centered"> + + <!-- Modal content--> + <div class="modal-content"> + <div class="modal-header"> + <h4 class="modal-title" style="color: black">Share Workshop</h4> + <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"> + </div> + <div class="modal-body"> + <p style="color: black">Share your workshop via URL</p> + <a href="" target="_blank"></a> + </div> + <div class="modal-footer"> + <button type="button" class="btn btn-default" data-bs-dismiss="modal">Close</button> + <button type="button" data-service="{{ service_id }}" data-row="{{ row_id }}" id="{{ service_id }}-{{ row_id }}-workshop-modal-copy-btn" class="btn btn-outline-primary" data-bs-toggle="tooltip" data-service="{{ service_id }}" data-row="{{ row_id }}" data-bs-placement="top" title="Copy to clipboard">Copy</button> + </div> + </div> + </div> + </div> +{%- endmacro %} + +{%- macro create_buttons( + service_id, + row_id, + element_options +)%} + <hr> + <div class="d-flex" id="{{ service_id }}-{{ row_id }}-buttons-div" role="dialog" tabindex="-1"> + {%- for button in element_options.get("input", {}).get("options", {}).get("buttons",[] ) %} + {%- set button_options = element_options.get("input", {}).get("options", {}).get(button, {}) %} + {%- if ( row_id == vars.first_row_id and button_options.get("firstRow", true) ) or + ( row_id != vars.first_row_id and button_options.get("defaultRow", true) ) %} + {{ create_button(service_id, row_id, button, button_options) }} + {%- endif %} + {%- endfor %} + </div> + {%- if pagetype == vars.pagetype_workshopmanager %} + {{ create_workshop_modal(service_id, row_id) }} + {%- endif %} +{%- endmacro %} + +{%- macro create_trigger( + id_prefix, + service_id, + row_id, + tab_id, + element_id, + element_options, + full_id +) %} +{%- endmacro %} + +{%- macro create_label( + id_prefix, + service_id, + row_id, + tab_id, + element_id, + element_options +)%} + {# + <label class="col-{{ label.get("width", "4") }} col-form-label" for="{{ id_prefix }}-{{ element_id }}-input"> + #} + {%- set label = element_options.get("label", {}) %} + <div class="col-{{ label.get("width", "4") }} col-form-label d-flex align-items-start justify-content-between"> + <label for="{{ id_prefix }}-{{ element_id }}-input" class="d-flex align-items-center w-100"> + {%- if label.type in ["text", "texticon", "texticonclick", "texticonclickcheckbox", "textcheckbox", "texticoncheckbox"] %} + {%- if label.value is string %} + {{ label.value }} + {%- endif %} + {%- endif %} + {%- if label.type in ["texticon", "texticoncheckbox"] %} + <a class="lh-1 ms-3" style="padding-top: 1px;" + data-bs-toggle="tooltip" data-bs-placement="right" data-bs-html="true" + title="{{ label.icontext }}"> + {{ svg.info_svg | safe }} + </a> + {%- endif %} + {%- if label.type == "texticonclick" %} + <button type="button" class="btn" + data-bs-toggle="tooltip" data-bs-placement="right" data-bs-html="true" + title="{{ label.icontext }}"> + {{ svg.info_svg | safe }} + <span class="text-muted" style="font-size: smaller">(click me)</span> + </button> + {%- endif %} + {%- if label.type == "textdropdown" %} + {%- endif %} + {%- if label.type in ["textcheckbox", "texticoncheckbox", "texticonclickcheckbox"] %} + <input type="checkbox" + {%- if label.get('options', {}).get("align-right", true ) %} + style="margin-left: auto;" + {%- else %} + style="margin-left: .5em;" + {%- endif %} + class="form-check-input" + id="{{ id_prefix }}-{{ element_id }}-input-cb" + data-service="{{ service_id }}" + data-row="{{ row_id }}" + data-tab="{{ tab_id }}" + data-element="{{ element_id }}" + {%- for specific_key, specific_values in element_options.get("dependency", {}).items() %} + data-dependency-{{ specific_key }}="true" + {%- for specific_value in specific_values %} + data-dependency-{{ specific_key }}-{{ specific_value }}="true" + {%- endfor %} + {%- endfor %} + + {%- set ignore_keys = ["name", "value", "show", "align-right", "placeholder", "pattern", "warning", "required"] %} + {%- for key, value in label.get('options', {}).items() %} + {%- if key not in ignore_keys %} + {%- if value is string or value is boolean %} + data-{{key}}="{{ value | lower }}" + {%- endif %} + {%- endif %} + {%- endfor %} + + {# add default group #} + {%- if "group" not in label.get('options', {}).keys() %} + data-group="default" + {%- endif %} + + {%- set checked = label.get('options', {}).get('default', false) %} + data-enabled="{{ label.get('options', {}).get('enabled', true) | lower }}" + {%- if not label.get('options', {}).get('enabled', true) %} + disabled="true" + {%- endif %} + data-label-input="true" + data-checked={{ checked | lower }} + {%- if checked %} + checked + {%- endif -%} + {%- if label.get('options', {}).get('name', false) %} + name="{{ label.get('options', {}).get('name', false) }}" + {%- endif %} + /> + {%- endif %} + {%- if label.type == "dropdown" %} + {%- endif %} + {%- if label.type == "function" %} + {%- endif %} + {%- if label.type == "header" %} + <h4>{{ label.value }}</h4> + {%- endif %} + </label> + </div> + {# + #} +{%- endmacro %} + + +{%- macro create_multiple_checkboxes( + id_prefix, + service_id, + row_id, + tab_id, + element_id, + element_options +) -%} + {#- User-selected Modules #} + <div id="{{ id_prefix }}-{{ element_id }}-input-div" + {{ element_parameters(service_id, row_id, tab_id, element_id, element_options, define_show=true) }} + > + {{ create_label(id_prefix, service_id, row_id, tab_id, element_id, element_options) }} + <div id="{{ id_prefix }}-{{ element_id }}-checkboxes-div" class="row g-0" + {{ element_parameters(service_id, row_id, tab_id, element_id, element_options) }} + > + </div> + </div> + {# create_trigger suffix: checkboxes-div #} +{%- endmacro %} + +{%- macro create_label_input( + id_prefix, + service_id, + row_id, + tab_id, + element_id, + element_options +) %} + <div id="{{ id_prefix }}-{{ element_id }}-input-div" + class="row mb-1" + {{ element_parameters(service_id, row_id, tab_id, element_id, element_options, define_show=true) }} + > + {{ create_label(id_prefix, service_id, row_id, tab_id, element_id, element_options) }} + </div> +{%- endmacro %} + + +{%- macro element_parameters( + service_id, + row_id, + tab_id, + element_id, + element_options, + define_show=false, + collect=true +) %} + {# data parameter for all elements #} + data-service="{{ service_id }}" + data-row="{{ row_id }}" + data-tab="{{ tab_id }}" + data-type="{{ element_options.get('input', {}).get('type', 'default') }}" + data-element="{{ element_id }}" + + {%- set ignore_keys = ["name", "value", "show", "align-right", "placeholder", "pattern", "warning", "required", "collect-static"] %} + {%- for key, value in element_options.get('input', {}).get('options', {}).items() %} + {%- if key not in ignore_keys %} + {%- if value is string or value is boolean %} + data-{{key}}="{{ value | lower }}" + {%- endif %} + {%- endif %} + {%- endfor %} + + {# add default group #} + {%- if "group" not in element_options.get('input', {}).get('options', {}).keys() %} + data-group="default" + {%- endif %} + {%- if collect %} + {%- if "collect" not in element_options.get('input', {}).get('options', {}).keys() %} + data-collect=false + {%- endif %} + {%- endif %} + {%- if element_options.get('input', {}).get('options', {}).get('collectstatic', false) %} + data-collect-static + {%- endif %} + + + {# data parameter for input elements #} + {%- if element_options.get("input", {}).get("options", {}).get("size", false) %} + size={{ element_options.get("input", {}).get("options", {}).get("size", 1) }} + {%- endif %} + {%- if element_options.get("input", {}).get("options", {}).get("multiple", false) %} + multiple + {%- endif %} + {%- if not element_options.get('input', {}).get('options', {}).get('enabled', true) and + not ( is_instructor and element_options.get("input", {}).get("options", {}).get("instructor", "") == "enabled" ) + %} + disabled + {%- endif %} + {%- if element_options.get("input", {}).get("options", {}).get("required", false) %} + required + {%- endif %} + {%- if element_options.get("input", {}).get("options", {}).get("pattern", "") %} + pattern="{{ element_options.get("input", {}).get("options", {}).get("pattern", "") }}" + {%- endif %} + {%- if element_options.get("input", {}).get("options", {}).get("placeholder", "") %} + placeholder="{{ element_options.get("input", {}).get("options", {}).get("placeholder", "") }}" + {%- endif %} + {%- if element_options.get("input", {}).get("options", {}).get("value", "") %} + value="{{ element_options.get("input", {}).get("options", {}).get("value", "") }}" + {%- endif %} + + {# If it's only available in a specific environment, hide it always by default #} + + {%- for specific_key, specific_values in element_options.get("dependency", {}).items() %} + data-dependency-{{ specific_key }}="true" + {%- for specific_value in specific_values %} + data-dependency-{{ specific_key }}-{{ specific_value }}="true" + {%- endfor %} + {%- endfor %} + {%- if define_show %} + {%- if element_options.get("input", {}).get("options", {}).get("show", true) or + ( is_instructor and element_options.get("input", {}).get("options", {}).get("instructor", "") == "show" ) + %} + data-show="true" + {%- else %} + data-show="false" + {%- endif %} + {%- if not element_options.get("input", {}).get("options", {}).get("show", false) and + not ( is_instructor and element_options.get("input", {}).get("options", {}).get("instructor", "") == "show" ) + %} + style="display: none" + {%- endif %} + {%- endif %} +{%- endmacro %} + +{%- macro create_text_input( + id_prefix, + service_id, + row_id, + tab_id, + element_id, + element_options +) %} + <div id="{{ id_prefix }}-{{ element_id }}-input-div" class="row mb-1" + {{ element_parameters(service_id, row_id, tab_id, element_id, element_options, define_show=true) }} + > + {{ create_label(id_prefix, service_id, row_id, tab_id, element_id, element_options) }} + <div class="col-{{ 12 - element_options.get("label", {}).get("width", "4") | int }} d-flex flex-column justify-content-center"> + {%- if element_options.get("input", {}).get("options", {}).get("secret", false) %} + <div class="input-group"> + {%- endif %} + <input type="{%- if element_options.get("input", {}).get("options", {}).get("secret", false) -%}password{%- else -%}text{%- endif -%}" + class="form-control" + {{ element_parameters(service_id, row_id, tab_id, element_id, element_options) }} + name="{{ element_options.get('input', {}).get('options', {}).get('name', element_id) }}" + id="{{ id_prefix }}-{{ element_id }}-input" + /> + {%- if element_options.get("input", {}).get("options", {}).get("secret", false) %} + <span class="input-group-append"> + <button class="btn btn-light" type="button" + data-service="{{ service_id }}" + data-row="{{ row_id }}" + data-element="{{ element_id }}" + id="{{ id_prefix }}-{{ element_id }}-view-password" + > + <i id="{{ id_prefix }}-{{ element_id }}-password-eye" class="fa fa-eye" aria-hidden="true"></i> + </button> + </span> + {%- endif %} + <div class="invalid-feedback">{{ element_options.get("input", {}).get("options", {}).get("warning", "") }}</div> + {%- if element_options.get("input", {}).get("options", {}).get("secret", false) %} + </div> + {%- endif %} + </div> + </div> +{%- endmacro %} + +{%- macro create_textgrower_input( + id_prefix, + service_id, + row_id, + tab_id, + element_id, + element_options +) %} + <div id="{{ id_prefix }}-{{ element_id }}-input-div" class="row mb-1" + {{ element_parameters(service_id, row_id, tab_id, element_id, element_options, define_show=true) }} + > + {{ create_label(id_prefix, service_id, row_id, tab_id, element_id, element_options) }} + <div data-count=1 class="container col-{{ 12 - element_options.get("label", {}).get("width", "4") | int }} d-flex flex-column justify-content-center"> + + <div class="input-group" style="display: flex; align-items: center; margin-bottom: 10px;"> + <input id="{{ id_prefix }}-1-{{ element_id }}-input" type="{%- if element_options.get("input", {}).get("options", {}).get("secret", false) -%}password{%- else -%}text{%- endif -%}" + class="form-control" + {{ element_parameters(service_id, row_id, tab_id, element_id, element_options) }} + name="{{ element_options.get('input', {}).get('options', {}).get('name', element_id) }}" + /> + <button data-service="{{ service_id }}" data-row="{{ row_id }}" data-tab="{{ tab_id }}" data-collect-static data-element="{{ element_id }}" data-textgrower-btn-type="add" data-collect="false" {% if not element_options.get('input', {}).get('options', {}).get('enabled', true) -%} disabled {% endif -%} type="button" id="{{ id_prefix }}-1-addbtn-{{ element_id }}-input" data-btn-type="add" style="margin-left: 8px;" class="btn btn-primary">{{ svg.plus_svg | safe }}</button> + </div> + <div class="invalid-feedback">{{ element_options.get("input", {}).get("options", {}).get("warning", "") }}</div> + </div> + </div> +{%- endmacro %} + +{%- macro create_date_input( + id_prefix, + service_id, + row_id, + tab_id, + element_id, + element_options +) %} + <div id="{{ id_prefix }}-{{ element_id }}-input-div" + class="row mb-1" + {{ element_parameters(service_id, row_id, tab_id, element_id, element_options, define_show=true) }} + > + {{ create_label(id_prefix, service_id, row_id, tab_id, element_id, element_options) }} + <div class="col-{{ 12 - element_options.get("label", {}).get("width", "4") | int }} d-flex flex-column justify-content-center"> + <input type="date" + {{ element_parameters(service_id, row_id, tab_id, element_id, element_options) }} + class="form-control" + name="{{ element_options.get('input', {}).get('options', {}).get('name', element_id) }}" + id="{{ id_prefix }}-{{ element_id }}-input" + value="{{ today_plus_half_year }}" + min="{{ today }}" + max="{{ today_plus_one_year }}" + /> + <div class="invalid-feedback">{{ element_options.get("input", {}).get("options", {}).get("warning", "") }}</div> + </div> + </div> +{%- endmacro %} + + +{%- macro create_number_input( + id_prefix, + service_id, + row_id, + tab_id, + element_id, + element_options +) %} + {% set name_ = element_options.get('input', {}).get('options', {}).get('name', element_id) %} + <div id="{{ id_prefix }}-{{ element_id }}-input-div" class="row mb-2" + {{ element_parameters(service_id, row_id, tab_id, element_id, element_options, define_show=true) }} + > + {{ create_label(id_prefix, service_id, row_id, tab_id, element_id, element_options) }} + <div class="col-{{ 12 - element_options.get("label", {}).get("width", "4") | int }} d-flex flex-column justify-content-center"> + <input type="number" + {{ element_parameters(service_id, row_id, tab_id, element_id, element_options) }} + name="{{ name_ }}" + class="form-control" + id="{{ id_prefix }}-{{ element_id }}-input" + {%- if element_options.get("input", {}).get("options", {}).get("required", false) %} + required + {%- endif %} + {%- if element_options.get("input", {}).get("options", {}).get("pattern", "") %} + pattern="{{ element_options.get("input", {}).get("options", {}).get("pattern", "") }}" + {%- endif %} + {%- if element_options.get("input", {}).get("options", {}).get("placeholder", "") %} + placeholder="{{ element_options.get("input", {}).get("options", {}).get("placeholder", "") }}" + {%- endif %} + {%- if element_options.get("input", {}).get("options", {}).get("value", "") %} + value="{{ element_options.get("input", {}).get("options", {}).get("value", "") }}" + {%- endif %} + {%- if not element_options.get("input", {}).get("options", {}).get("enabled", true) %} + disabled + {%- endif %} + /> + <div class="invalid-feedback">{{ element_options.get("input", {}).get("options", {}).get("warning", "") }}</div> + </div> + </div> +{%- endmacro %} + +{%- macro create_checkbox_input( + id_prefix, + service_id, + row_id, + tab_id, + element_id, + element_options +) %} + <div id="{{ id_prefix }}-{{ element_id }}-input-div" class="row mb-2" + {{ element_parameters(service_id, row_id, tab_id, element_id, element_options, define_show=true) }} + > + {{ create_label(id_prefix, service_id, row_id, tab_id, element_id, element_options) }} + <div class="col-{{ 12 - element_options.get("label", {}).get("width", "4") | int }} d-flex flex-column justify-content-center"> + <input type="checkbox" + name="{{ element_options.get('input', {}).get('options', {}).get('name', element_id) }}" + {{ element_parameters(service_id, row_id, tab_id, element_id, element_options) }} + class="form-check-input" + id="{{ id_prefix }}-{{ element_id }}-input" + {%- if element_options.get("input", {}).get("options", {}).get("required", false) %} + required + {%- endif %} + {%- if element_options.get("input", {}).get("options", {}).get("pattern", "") %} + pattern="{{ element_options.get("input", {}).get("options", {}).get("pattern", "") }}" + {%- endif %} + {%- if element_options.get("input", {}).get("options", {}).get("placeholder", "") %} + placeholder="{{ element_options.get("input", {}).get("options", {}).get("placeholder", "") }}" + {%- endif %} + {%- if element_options.get("input", {}).get("options", {}).get("value", "") %} + value="{{ element_options.get("input", {}).get("options", {}).get("value", "") }}" + {%- endif %} + {%- if not element_options.get("input", {}).get("options", {}).get("enabled", true) %} + disabled + {%- endif %} + {% if element_options.get('input', {}).get('options', {}).get('default', false) %} + checked + {%- endif %} + /> + <div class="invalid-feedback">{{ element_options.get("input", {}).get("options", {}).get("warning", "") }}</div> + </div> + </div> +{%- endmacro %} + +{%- macro create_select_input( + id_prefix, + service_id, + row_id, + tab_id, + element_id, + element_options +) %} + {# Versions #} + <div id="{{ id_prefix }}-{{ element_id }}-input-div" class="row mb-1" + {{ element_parameters(service_id, row_id, tab_id, element_id, element_options, define_show=true) }} + > + {{ create_label(id_prefix, service_id, row_id, tab_id, element_id, element_options) }} + <div class="col-{{ 12 - element_options.get("label", {}).get("width", "4") | int }} d-flex flex-column justify-content-center"> + <select + name="{{ element_options.get('input', {}).get('options', {}).get('name', element_id) }}" + id="{{ id_prefix }}-{{ element_id }}-input" + class="form-select" + {{ element_parameters(service_id, row_id, tab_id, element_id, element_options) }} + > + </select> + <div class="invalid-feedback">{{ element_options.get("input", {}).get("options", {}).get("warning", "You have to select at least one item.") }}</div> + </div> + </div> +{%- endmacro %} + +{%- macro create_reservation_info( + id_prefix, + service_id, + row_id, + tab_id, + element_id, + element_options +)%} + <div id="{{ id_prefix }}-{{ element_id }}-input-div" class="row mb-3" + {{ element_parameters(service_id, row_id, tab_id, element_id, element_options, define_show=true) }} + > + {%- set reservation_info_classes = "col-4 fw-bold"%} + <div id="{{ id_prefix }}-{{ element_id }}-info" class="col-8 offset-4"> + <div class="row"> + <span class="{{ reservation_info_classes }}">Start Time:</span> + <span id="{{ id_prefix }}-{{ element_id }}-start" class="col-auto"></span> + </div> + <div class="row"> + <span class="{{ reservation_info_classes }}">End Time:</span> + <span id="{{ id_prefix }}-{{ element_id }}-end" class="col-auto"></span> + </div> + <div class="row"> + <span class="{{ reservation_info_classes }}">State:</span> + <span id="{{ id_prefix }}-{{ element_id }}-state" class="col-auto"></span> + </div> + <div class="mt-1"> + <details> + <summary class="fw-bold">Detailed reservation information:</summary> + <pre id="{{ id_prefix }}-{{ element_id }}-details"></pre> + </details> + </div> + </div> + </div> + {# create_trigger suffix: input-div #} +{%- endmacro %} + +{%- macro create_flavor_legend( + id_prefix, + service_id, + row_id, + tab_id, + element_id, + element_options +)%} + <div id="{{ id_prefix }}-{{ element_id }}-input-div" class="row align-items-center g-0 mt-4" + {{ element_parameters(service_id, row_id, tab_id, element_id, element_options, define_show=true) }} + > + <span class="col-4 fw-bold">Available Flavors</span> + <div class="col d-flex align-items-center ms-2"> + {%- set box_style = "height: 15px; width: 15px; border-radius: 0.25rem;"%} + <div style="{{box_style}} background-color: #198754;"></div> + <span class="ms-1">= Free</span> + <span class="mx-2"></span> + <div style="{{box_style}} background-color: #023d6b;"></div> + <span class="ms-1">= Used</span> + <span class="mx-2"></span> + <div style="{{box_style}} background-color: #dc3545;"></div> + <span class="ms-1">= Limit exceeded</span> + </div> + </div> + {# create_trigger suffix: input-div #} +{%- endmacro %} + +{%- macro create_flavor_info( + id_prefix, + service_id, + row_id, + tab_id, + element_id, + element_options +)%} + <div id="{{ id_prefix }}-{{ element_id }}-input-div" data-sse-flavors class="mb-3" + {{ element_parameters(service_id, row_id, tab_id, element_id, element_options, define_show=true) }} + ></div> +{# create_trigger suffix: input-div #} +{%- endmacro %} + +{%- macro create_selecthelper( + id_prefix, + service_id, + row_id, + tab_id, + element_id, + element_options +)%} + <div id="{{ id_prefix }}-{{ element_id }}-input-div" class="row g-0" + {{ element_parameters(service_id, row_id, tab_id, element_id, element_options, define_show=true) }} + > + <hr> + <div class="form-check col-sm-6 col-md-4 col-lg-3"> + <input class="form-check-input data-service="{{ service_id }}" data-row="{{ row_id }}" data-tab="{{ tab_id }}" module-selector" type="checkbox" id="{{ id_prefix }}-{{ element_id }}-select-all"> + <label class="form-check-label" for="{{ id_prefix }}-{{ element_id }}-select-all">Select all</label> + </div> + <div class="form-check col-sm-6 col-md-4 col-lg-3"> + <input class="form-check-input data-service="{{ service_id }}" data-row="{{ row_id }}" data-tab="{{ tab_id }}" module-selector" type="checkbox" id="{{ id_prefix }}-{{ element_id }}-select-none"> + <label class="form-check-label" for="{{ id_prefix }}-{{ element_id }}-select-none">Deselect all</label> + </div> + </div> +{# create_trigger suffix: input-div #} +{%- endmacro %} + +{%- macro create_logcontainer( + id_prefix, + service_id, + row_id, + tab_id, + element_id, + element_options +)%} + <div id="{{ id_prefix }}-{{ element_id }}-input" class="card card-body text-black row g-0" + {{ element_parameters(service_id, row_id, tab_id, element_id, element_options) }} + > + <div class="log-div"> + Logs collected during the Start process will be shown here. + </div> + </div> +{# create_trigger suffix: input-div #} +{%- endmacro %} + +{%- macro create_element( + service_id, + row_id, + tab_id, + element_id, + element_options +) %} + {% set id_prefix = service_id ~ '-' ~ row_id ~ '-' ~ tab_id %} + {%- if element_options.get("input", {}).get("type", "") == "text" %} + {{ create_text_input( + id_prefix, + service_id, + row_id, + tab_id, + element_id, + element_options + )}} + {%- elif element_options.get("input", {}).get("type", "") == "textgrower" %} + {{ create_textgrower_input( + id_prefix, + service_id, + row_id, + tab_id, + element_id, + element_options + )}} + {%- elif element_options.get("input", {}).get("type", "") == "label" %} + {{ create_label_input( + id_prefix, + service_id, + row_id, + tab_id, + element_id, + element_options + )}} + {%- elif element_options.get("input", {}).get("type", "") == "date" %} + {{ create_date_input( + id_prefix, + service_id, + row_id, + tab_id, + element_id, + element_options + )}} + {%- elif element_options.get("input", {}).get("type", "") == "number" %} + {{ create_number_input( + id_prefix, + service_id, + row_id, + tab_id, + element_id, + element_options + )}} + {%- elif element_options.get("input", {}).get("type", "") == "checkbox" %} + {{ create_checkbox_input( + id_prefix, + service_id, + row_id, + tab_id, + element_id, + element_options + )}} + {%- elif element_options.get("input", {}).get("type", "") == "buttons" %} + {{ create_buttons( + service_id, + row_id, + element_options + )}} + {%- elif element_options.get("input", {}).get("type", "") == "select" %} + {{ create_select_input( + id_prefix, + service_id, + row_id, + tab_id, + element_id, + element_options + )}} + {%- elif element_options.get("input", {}).get("type", "") == "reservationinfo" %} + {{ create_reservation_info( + id_prefix, + service_id, + row_id, + tab_id, + element_id, + element_options + )}} + {%- elif element_options.get("input", {}).get("type", "") == "flavorlegend" %} + {{ create_flavor_legend( + id_prefix, + service_id, + row_id, + tab_id, + element_id, + element_options + )}} + {%- elif element_options.get("input", {}).get("type", "") == "flavorinfo" %} + {{ create_flavor_info( + id_prefix, + service_id, + row_id, + tab_id, + element_id, + element_options + )}} + {%- elif element_options.get("input", {}).get("type", "") == "multiple_checkboxes" %} + {{ create_multiple_checkboxes( + id_prefix, + service_id, + row_id, + tab_id, + element_id, + element_options + )}} + {%- elif element_options.get("input", {}).get("type", "") == "selecthelper" %} + {{ create_selecthelper( + id_prefix, + service_id, + row_id, + tab_id, + element_id, + element_options + )}} + {%- elif element_options.get("input", {}).get("type", "") == "logcontainer" %} + {{ create_logcontainer( + id_prefix, + service_id, + row_id, + tab_id, + element_id, + element_options + )}} + {%- elif element_options.get("input", {}).get("type", "") == "hr" %} + <hr> + {%- endif %} +{%- endmacro %} + diff --git a/templates/macros/table/elements_js.jinja b/templates/macros/table/elements_js.jinja new file mode 100644 index 0000000000000000000000000000000000000000..7f3865c1a57e3151e432bd56d12b228db588fc62 --- /dev/null +++ b/templates/macros/table/elements_js.jinja @@ -0,0 +1,646 @@ +{%- import "macros/svgs.jinja" as svg -%} +{%- include "macros/table/table_js.jinja" %} +{%- import "macros/table/variables.jinja" as vars with context %} + +<script> + $(document).ready(function() { + require(["jquery", "jhapi", "utils"], function ( + $, + JHAPI, + utils + ) { + var base_url = window.jhdata.base_url; + var api = new JHAPI(base_url); + var user = window.jhdata.user; + const logDebug = false; + + let optionElement; + + {%- for service_id, service_options in config.frontend_config.get("services", {}).get("options", {}).items() %} + // Configure element specific elements: + {%- for tab_id, tab_options in service_options.get("tabs", {}).items() %} + {%- for side in tab_options.keys() %} + {%- for element_id, element_options in tab_options.get(side, {}).items() %} + {%- if element_options.get("input", {}).get("type", "") == "buttons" %} + {%- for button in element_options.get("input", {}).get("options", {}).get("buttons", []) %} + {% set trigger = element_options.get("input", {}).get("options", {}).get(button, {}).get("trigger", false) %} + {%- if trigger %} + {%- set button_options = element_options.get("input", {}).get("options", {}).get(button, {}) %} + $(`[id^='{{ service_id }}-'][id$='-{{ button }}-btn']`).on("click", function() { + logDebug && console.log("Button click {{ button }}"); + const $this = $(this); + const serviceId = $this.attr("data-service"); + const rowId = $this.attr("data-row"); + {{ button_options.get("trigger", "") }}(serviceId, rowId, "{{ button }}", {{ button_options | tojson }}, user, api, base_url, utils); + logDebug && console.log("Button click {{ button }} done"); + }); + {%- endif %} + {%- endfor %} + {%- else %} + {%- set trigger_suffix = element_options.get("triggerSuffix", "input") %} + {%- for trigger_key, trigger_func in element_options.get("trigger", {}).items() %} + $(`[id^='{{ service_id }}-'][id$='-{{ element_id }}-{{ trigger_suffix}}']`).on("trigger_{{ trigger_key }}", function (event) { + if (event.target !== this) { + return; // Ignore events bubbling up from child elements + } + logDebug && console.log("update {{ element_id }}. Triggered by: ({{ trigger_key }})"); + const $this = $(this); + const serviceId = $this.attr("data-service"); + const rowId = $this.attr("data-row"); + const preValue = $this.val(); + const dependencies = {{ element_options.get("dependency", {}) | tojson }}; + let trigger = true; + {%- if trigger_key != "init" %} + for (const [key, allowedValues] of Object.entries(dependencies)) { + const currentValues = val(getInputElement(serviceId, rowId, key)); + trigger = currentValues.some(value => { + const mappedValue = mappingDict?.[serviceId]?.[key]?.[value] ?? value; + return allowedValues.includes(mappedValue); + }); + if ( !trigger ) { + break; + } + } + {%- endif %} + if ( trigger ) { + {{ trigger_func }}("{{ trigger_key }}", serviceId, rowId, "{{ tab_id }}", "{{ element_id }}", {{ element_options | tojson }}); + $this.trigger("change"); + logDebug && console.log("update {{ element_id }}. Triggered by: ({{ trigger_key }}) done"); + } else { + logDebug && console.log("update {{ element_id }}. Triggered by: ({{ trigger_key }}) no function call"); + } + }); + {%- endfor %} + {%- if element_options.get("triggerOnChange", "") %} + $(`[id^='{{ service_id }}-'][id$='-{{ tab_id }}-{{ element_id }}-{{ trigger_suffix}}']`).on("change", function () { + const $this = $(this); + const serviceId = $this.attr("data-service"); + const rowId = $this.attr("data-row"); + {{ element_options["triggerOnChange"] }}("onChange", serviceId, rowId, "{{ tab_id }}", "{{ element_id }}", {{ element_options | tojson }}); + }); + {%- endif %} + {%- set label_options = element_options.get("label", {}) %} + {%- if label_options.get("type", "text") in ["textcheckbox", "texticoncheckbox", "texticonclickcheckbox"] %} + {%- for trigger_key, trigger_func in label_options.get("trigger", {}).items() %} + $(`[id^='{{ service_id }}-'][id$='-{{ tab_id }}-{{ element_id }}-input-cb']`).on("trigger_{{ trigger_key }}"), function (event) { + if (event.target !== this) { + return; // Ignore events bubbling up from child elements + } + const $this = $(this); + const serviceId = $this.attr("data-service"); + const rowId = $this.attr("data-row"); + logDebug && console.log("update {{ element_id }}. Triggered by: ({{ trigger_key }})"); + {{ trigger_func }}("{{ trigger_key }}", serviceId, rowId, "{{ tab_id }}", "{{ element_id }}", {{ label_options.get("options", {}) | tojson }}); + logDebug && console.log("update {{ element_id }}. Triggered by: ({{ trigger_key }}) done"); + }); + {%- endfor %} + $(`[id^='{{ service_id }}-'][id$='-{{ tab_id }}-{{ element_id }}-input-cb']`).on("change", function() { + const $this = $(this); + const serviceId = $this.attr("data-service"); + const rowId = $this.attr("data-row"); + const checked = $this.prop("checked"); + $(`[id^='${serviceId}-${rowId}-'][id$='-{{ element_id }}-input']`).prop("disabled", !checked); + $(`[id^='${serviceId}-${rowId}-'][id$='-{{ element_id }}-input']:not([data-collect-static])`).attr("data-collect", checked); + if ( !checked ) { + $(`[id^='${serviceId}-${rowId}-'][id$='-{{ element_id }}-input']`).removeClass("is-invalid"); + } + + {%- if label_options.get("triggerOnChange", "") %} + {{ label_options["triggerOnChange"] }}("change", serviceId, rowId, "{{ tab_id }}", "{{ element_id }}", {{ label_options.get("options", {}) | tojson }}); + {%- endif %} + $(`[id^='${serviceId}-${rowId}-']`).trigger(`trigger_{{ element_id }}`); + logDebug && console.log("update {{ element_id }}. Trigger Change by: {{ trigger_key }}"); + $(`[id^='${serviceId}-${rowId}-'][id$='-{{ element_id }}-input']`).trigger("change"); + logDebug && console.log("update {{ element_id }}. Trigger Change by: {{ trigger_key }} done"); + }); + {%- endif %} + {%- endif %} + {%- endfor %} + {%- endfor %} + {%- endfor %} + {%- for button_id, button_options in service_options.navbar.items() %} + $(`button[id^='{{ service_id }}-'][id$='-{{ button_id }}-navbar-button']`).on("show", function (event) { + const $this = $(this); + const rowId = $this.attr("data-row"); + const serviceId = $this.attr("data-service"); + let show = true; + if ( !$this.data('show') ) { + const dependencies = {{ button_options.get("dependency", {}) | tojson }}; + for (const [key, allowedValues] of Object.entries(dependencies)) { + const currentValues = val(getInputElement(serviceId, rowId, key)); + show = currentValues.some(value => { + const mappedValue = mappingDict[serviceId]?.[key]?.[value] ?? value; + return allowedValues.includes(mappedValue); + }); + if ( !show ) { + break; + } + } + } + if ( show ) { + $this.addClass("show"); + $this.attr("style", ""); + $this.show(); + $(`#${serviceId}-${rowId}-{{ button_id }}-tab-input-warning`).addClass("invisible"); + } + }); + $(`button[id^='{{ service_id }}-'][id$='-{{ button_id }}-navbar-button']`).on("hide", function (event) { + const $this = $(this); + const rowId = $this.attr("data-row"); + const serviceId = $this.attr("data-service"); + $this.removeClass("show"); + $this.attr("style", "{{ style_hide }}"); + $this.hide(); + $(`[id^='${serviceId}-'][id$='-${rowId}-{{ button_id }}-tab-input-warning']`).addClass("invisible"); + }); + $(`button[id^='{{ service_id }}-'][id$='-{{ button_id }}-navbar-button']`).on("activate", function (event) { + const $this = $(this); + const rowId = $this.attr("data-row"); + const serviceId = $this.attr("data-service"); + $(`div[id^='${serviceId}-${rowId}-'][id$='-{{ button_id }}-contenttab-div']`).addClass("show"); + $(`div[id^='${serviceId}-${rowId}-'][id$='-{{ button_id }}-contenttab-div']`).show(); + $(`[id^='${serviceId}-${rowId}-'][id$='-{{ button_id }}-tab-input-warning']`).addClass("invisible"); + }); + $(`button[id^='{{ service_id }}-'][id$='-{{ button_id }}-navbar-button']`).on("deactivate", function (event) { + const $this = $(this); + const rowId = $this.attr("data-row"); + const serviceId = $this.attr("data-service"); + $(`div[id^='${serviceId}-${rowId}-'][id$='-{{ button_id }}-contenttab-div']`).removeClass("show"); + $(`div[id^='${serviceId}-${rowId}-'][id$='-{{ button_id }}-contenttab-div']`).hide(); + }); + $(`button[id^='{{ service_id }}-'][id$='-{{ button_id }}-navbar-button']`).on("click", function (event) { + $this = $(this); + const rowId = $this.attr("data-row"); + const serviceId = $this.attr("data-service"); + $(`button[id^='${serviceId}-${rowId}-'][id$='-navbar-button']:not([data-tab='{{ button_id }}'])`).trigger("deactivate"); + $this.trigger("activate"); + }); + {%- for trigger_key, trigger_func in button_options.get("trigger", {}).items() %} + $(`button[id^='{{ service_id }}-'][id$='-{{ button_id }}-navbar-button']`).on("trigger_{{ trigger_key }}", function (event) { + if (event.target !== this) { + return; // Ignore events bubbling up from child elements + } + $this = $(this); + const rowId = $this.attr("data-row"); + const serviceId = $this.attr("data-service"); + let trigger = true; + const dependencies = {{ button_options.get("dependency", {}) | tojson }}; + for (const [key, allowedValues] of Object.entries(dependencies)) { + const currentValues = val(getInputElement(serviceId, rowId, key)); + trigger = currentValues.some(value => { + const mappedValue = mappingDict[serviceId]?.[key]?.[value] ?? value; + return allowedValues.includes(mappedValue); + }); + if ( !trigger ) { + break; + } + } + if ( trigger ) { + {{ trigger_func }}("{{ button_id }}", serviceId, rowId); + } + }) + {%- endfor %} + + {%- endfor %} + {%- if pagetype == vars.pagetype_workshopmanager %} + let modalWasVisible = false; + new IntersectionObserver((entries) => { + entries.forEach(entry => { + if (entry.isIntersecting) { + modalWasVisible = true; + } else if (modalWasVisible) { + let serviceId = $('#service-input').val(); + let workshopId = $(`input[data-row='{{ vars.first_row_id }}'][id$='-workshopid-input']`).val(); + if ( !Array.isArray(workshopId) ){ + workshopId = [workshopId]; + } + window.location.href = window.location.origin + window.location.pathname + "?service=" + serviceId + "&row=" + workshopId + "&v" + new Date().getTime(); + } + }); + }).observe(document.getElementById('{{ service_id }}-{{ vars.first_row_id }}-workshop-modal')); + {%- endif %} + {%- endfor %} + // show / hide dependent elements + $(`select[id$='-input']`).change(function (event) { + const $this = $(this); + const serviceId = $this.attr("data-service"); + const rowId = $this.attr("data-row"); + const elementId = $this.attr("data-element"); + logDebug && console.log(`${elementId} changed ... `); + $(`[id^='${serviceId}-${rowId}-']`).trigger(`trigger_${elementId}`); + + const isDisabled = $this.prop("disabled"); + let newValues = $this.val(); + if ( !isDisabled && !(!newValues || (Array.isArray(newValues) && newValues.length === 0)) ) { + if ( !Array.isArray(newValues) ){ + newValues = [newValues]; + } + // const mappedValues = newValues.map(newValue => mappingDict[serviceId]?.[elementId]?.[newValue] ?? newValue); + const mappedValues = [...new Set(newValues.map(newValue => mappingDict[serviceId]?.[elementId]?.[newValue] ?? newValue))]; + const excludes = `:not(${mappedValues.map(value => `[data-dependency-${elementId}-${value}]`).join(',')})`; + let prefixSelector = ""; + let selector = ""; + + // Show + set to Active (add collect) + prefixSelector = `div[id^='${serviceId}-${rowId}-'][id$='-input-div'][data-show='true']`; + selector = mappedValues.map(value => `${prefixSelector}[data-dependency-${elementId}-${value}]`).join(','); + $(`${selector}`).show(); + + prefixSelector = `[id^='${serviceId}-${rowId}-'][id$='-input']:not([data-collect-static])`; + selector = mappedValues.map(value => `${prefixSelector}[data-dependency-${elementId}-${value}]`).join(','); + $(`${selector}`).attr("data-collect", true); + + // trigger all new activated label cb, to ensure normally hidden inputs are shown + prefixSelector = `[id^='${serviceId}-${rowId}-'][id$='-input-cb']`; + selector = mappedValues.map(value => `${prefixSelector}[data-dependency-${elementId}-${value}]`).join(','); + $(`${selector}`).trigger("change"); + + // show navbar buttons + prefixSelector = `button[id^='${serviceId}-${rowId}-'][id$='-navbar-button'][data-show="true"]`; + selector = mappedValues.map(value => `${prefixSelector}[data-dependency-${elementId}-${value}]`).join(','); + $(`${selector}`).trigger("show"); + $(`${selector}`).trigger("change"); + + // show buttons + prefixSelector = `button[id^='${serviceId}-${rowId}-'][id$='-btn']`; + selector = mappedValues.map(value => `${prefixSelector}[data-dependency-${elementId}-${value}]`).join(','); + $(`${selector}`).show(); + + {# Show / Hide dependency values #} + // hide + ignore + $(`div[id^='${serviceId}-${rowId}-'][id$='input-div'][data-dependency-${elementId}]${excludes}`).hide(); + $(`[id^='${serviceId}-${rowId}-'][id$='-input'][data-dependency-${elementId}]${excludes}`).attr("data-collect", false); + + // hide navbar buttons + selector = `button[id^='${serviceId}-${rowId}-'][id$='-navbar-button'][data-dependency-${elementId}]${excludes}`; + $(`${selector}`).trigger("hide"); + + // hide buttons + selector = `button[id^='${serviceId}-${rowId}-'][id$='-btn'][data-dependency-${elementId}]${excludes}`; + $(`${selector}`).hide(); + } else { + // hide + ignore all specific inputs + $(`div[id^='${serviceId}-${rowId}-'][id$='input-div'][data-dependency-${elementId}]`).hide(); + $(`[id^='${serviceId}-${rowId}-'][id$='input'][data-dependency-${elementId}]`).attr("data-collect", false); + } + }); + + + {%- if pagetype == vars.pagetype_workshopmanager %} + $(`[id$='-workshop-modal-copy-btn']`).on("click", function (){ + const $this = $(this); + const serviceId = $this.attr("data-service"); + const rowId = $this.attr("data-row"); + const workshopUrl = $(`#${serviceId}-${rowId}-workshop-modal .modal-body a`).attr('href'); + navigator.clipboard.writeText(workshopUrl).then(function() { + $(`#${serviceId}-${rowId}-workshop-modal-copy-btn`).tooltip('dispose').attr('title', 'Copied'); + $(`#${serviceId}-${rowId}-workshop-modal-copy-btn`).tooltip('show'); + }, function(err) { + console.error('Could not copy text: ', err); + }); + }); + + window.workshopManagerShowModal = function (serviceId, rowId, workshopId) { + let workshopUrl = new URL(utils.url_path_join(window.origin, base_url, "workshops", workshopId).replace("//", "/")); + $(`#${serviceId}-${rowId}-workshop-modal .modal-title`).text(`Share Workshop ${workshopId}`); + $(`#${serviceId}-${rowId}-workshop-modal .modal-body a`).text(`${workshopUrl}`); + $(`#${serviceId}-${rowId}-workshop-modal .modal-body a`).attr('href', workshopUrl); + $(`#${serviceId}-${rowId}-workshop-modal`).modal('show'); + } + {%- endif %} + + // secret input text fields --> + $(`button[id$='-view-password']`).on("click", function (event) { + const $this = $(this); + const serviceId = $this.attr("data-service"); + const rowId = $this.attr("data-row"); + const elementId = $this.attr("data-element"); + const passInput = $(`input[id^='${serviceId}-${rowId}-'][id$='-${elementId}-input']`); + const eye = $(`i[id^='${serviceId}-${rowId}-'][id$='-${elementId}-password-eye']`); + {# + const passInput = $('input[id*={{ id_prefix }}-{{ element_id }}-input]')[0]; + const eye = $('i[id*={{ id_prefix }}-{{ element_id }}-password-eye]')[0]; + #} + if (passInput.prop("type") === "password") { + passInput.prop("type", "text"); + eye.removeClass("fa-eye"); + eye.addClass("fa-eye-slash"); + } else { + passInput.prop("type", "password"); + eye.addClass("fa-eye"); + eye.removeClass("fa-eye-slash"); + } + }); + // <-- secret input text fields + + $(document).on("click", `button[data-textgrower-btn-type='add'][id$='-input']`, function (event) { + const $this = $(this); + const serviceId = $this.attr("data-service"); + const rowId = $this.attr("data-row"); + const tabId = $this.attr("data-tab"); + const elementId = $this.attr("data-element"); + + const parentContainer = $this.closest('.container'); + const countElements = parseInt(parentContainer.attr("data-count")) + 1; + parentContainer.attr("data-count", countElements); + const firstInputElement = $(`[id^='${serviceId}-${rowId}-${tabId}'][id$='-1-${elementId}-input']`); + const dataType = firstInputElement.attr("data-type"); + const group = firstInputElement.attr("data-group") ?? "default"; + const name = firstInputElement.attr("name") ?? elementId; + const type = firstInputElement.attr("type") ?? "text"; + let pattern = firstInputElement.attr("pattern"); + if ( !pattern ) { + pattern = ""; + } + let placeholder = firstInputElement.attr("placeholder"); + if ( !placeholder ) { + placeholder = ""; + } + + const newInputGroup = ` + <div class="input-group" style="display: flex; align-items: center; margin-bottom: 10px;"> + <input id="${serviceId}-${rowId}-${tabId}-${countElements}-${elementId}-input" type="${type}" + class="form-control" + data-service="${serviceId}" + data-row="${rowId}" + data-tab="${tabId}" + data-type="${dataType}" + data-element="${elementId}" + data-group="${group}" + data-collect="true" + data-collect-static + name="${name}" + type="${type}" + pattern="${pattern}" + placeholder="${placeholder}" + /> + <button data-collect-static data-textgrower-btn-type="del" data-element="${elementId}" data-service="${serviceId}" data-row="${rowId}" data-tab="${tabId}" data-collect="false" type="button" id="${serviceId}-${rowId}-${tabId}-${countElements}-delbtn-${elementId}-input" style="margin-left: 8px;" class="btn btn-danger">{{ svg.delete_svg | safe }}</button> + <button data-collect-static data-textgrower-btn-type="add" data-element="${elementId}" data-service="${serviceId}" data-row="${rowId}" data-tab="${tabId}" data-collect="false" type="button" id="${serviceId}-${rowId}-${tabId}-${countElements}-addbtn-${elementId}-input" style="margin-left: 8px;" class="btn btn-primary">{{ svg.plus_svg | safe }}</button> + </div> + `; + parentContainer.append(newInputGroup); + }); + $(document).on("click", `button[data-textgrower-btn-type='del'][id$='-input']`, function (event) { + $(this).closest('.input-group').remove(); + }); + + + $(`[data-sse-flavors]`).on("sse", function (event, data) { + const $this = $(this); + const serviceId = $this.attr("data-service"); + const rowId = $this.attr("data-row"); + for ( const [system, systemFlavors] of Object.entries(data) ) { + kubeOutpostFlavors[system] = systemFlavors; + } + const currentSystem = $(`[id^='${serviceId}-${rowId}-'][id$='-system-input']`); + if ( currentSystem.length && currentSystem.attr("data-collect") == "true" ) { + if ( Object.keys(data).includes(currentSystem.val()) ){ + setFlavorInfo(serviceId, rowId, currentSystem.val(), kubeOutpostFlavors[currentSystem.val()]); + } + } + }); + + $(`input[id$='-select-all']`).on("click", function () { + const $this = $(this); + const serviceId = $this.attr("data-service"); + const rowId = $this.attr("data-row"); + const tabId = $this.attr("data-tab"); + if ( $this.prop("checked") ) { + $(`input[id^='${serviceId}-${rowId}-${tabId}-'][id$='-input']`).prop("checked", true); + $(`[id^='${serviceId}-${rowId}-${tabId}-'][id$='-select-none']`).prop("checked", false); + } + }) + $(`input[id$='-select-none']`).on("click", function () { + const $this = $(this); + const serviceId = $this.attr("data-service"); + const rowId = $this.attr("data-row"); + const tabId = $this.attr("data-tab"); + if ( $this.prop("checked") ) { + $(`input[id^='${serviceId}-${rowId}-${tabId}-'][id$='-input']`).prop("checked", false); + $(`[id^='${serviceId}-${rowId}-${tabId}-'][id$='-select-all']`).prop("checked", false); + } + }) + + $(".summary-tr").on("click", function (event) { + if (![event.target, event.target.parentElement, event.target.parentElement?.parentElement] + .some(el => el?.tagName === "BUTTON")) { + const $this = $(this); + const id = $this.data("server-id"); + let accordionIcon = $(this).find(".accordion-icon"); + let collapse = $(`.collapse[id^=${id}]`); + if ( collapse.length > 0 ) { + let shown = collapse.hasClass("show"); + if (shown) accordionIcon.addClass("collapsed"); + else accordionIcon.removeClass("collapsed"); + new bootstrap.Collapse(collapse); + } + } + }); + + + logDebug && console.log(`Fill elements ...`); + $(`[id$='-input']`).trigger("trigger_init"); + logDebug && console.log(`Fill elements ... done`); + + // Set Defaults, as configured in config file + {%- for service_id, service_options in config.frontend_config.get("services", {}).get("options", {}).items() %} + logDebug && console.log("Set default values ( {{ service_id }} ) ..."); + {%- set default_tab_id = service_options.get("default", {}).get("tab", "default") %} + {%- for option_key, option_value in service_options.get("default", {}).get("options", {}).items() %} + optionElement = $(`[id^='{{ service_id }}-'][id$='-{{ default_tab_id }}-{{ option_key }}-input']`); + if ( optionElement.is("select") && optionElement.find(`option[value="{{ option_value }}"]:not(:disabled)`).length ){ + optionElement.val("{{ option_value }}"); + optionElement.trigger("change"); + } else if ( optionElement.is("input") ){ + optionElement.val("{{ option_value }}"); + optionElement.trigger("change"); + } + {%- endfor %} + {%- if pagetype == vars.pagetype_workshopmanager %} + {%- for row_id, values in db_workshops.items() %} + workshopManagerFillExistingRow("{{ service_id }}", "{{ row_id }}", {{ values | tojson }}); + {%- endfor %} + {%- elif pagetype == vars.pagetype_workshop %} + workshopManagerFillExistingRow("{{ service_id }}", "{{ spawner.name }}", {{ db_workshops.get(spawner.name, {}) | tojson }}); + {%- elif pagetype == vars.pagetype_home %} + // homeFillExistingRow + {%- for s in spawners %} + // console.log( {{ s }}); + // console.log( {{ user.spawners.get(s.name, s) }}); + {%- set spawner = user.spawners.get(s.name, s) %} + // console.log( {{ spawner.name }}); + // console.log( {{ spawner.events }}); + {%- if spawner.events %} + // console.log("y"); + {%- endif %} + {%- if spawner.user_options and spawner.user_options.get("name", false) %} + homeFillExistingRow("{{ service_id }}", "{{ spawner.name }}", {{ spawner.user_options | tojson }}, {{ service_options.get("fillingOrder", []) | tojson }}); + {%- if spawner.events %} + {%- for event in spawner.events %} + appendToLog("{{ service_id }}", "{{ spawner.name }}", {{ event | tojson }}); + {%- endfor %} + {%- endif %} + {%- endif %} + {%- endfor %} + {%- endif %} + logDebug && console.log("Set default values ( {{ service_id }} ) done"); + + {%- endfor %} + + {%- if pagetype == vars.pagetype_workshop %} + let service = ""; + let name = ""; + let lastEvent = false; + let updateProgressBar = false; + {# iterate through all spawners #} + service = "{{ spawner.user_options.get("service", "jupyterlab") | lower }}"; + name = "{{ spawner.name }}"; + events = []; + {%- if spawner and spawner.events %} + events = {{ spawner.events | tojson }}; + {%- endif %} + lastEvent = events.length > 0 ? events[events.length - 1] : false; + clearLogs(service, name); + if ( lastEvent ) { + {%- if spawner.cancel_pending or spawner.active %} + updateProgressBar = true; + {%- else %} + updateProgressBar = lastEvent.progress != 100; + {%- endif %} + } + events.forEach( event => { + if ( updateProgressBar ) { + let ready = event.ready ?? false; + let failed = event.failed ?? false; + let progress = event.progress ?? 0; + let status = "starting"; + if ( ready ) status = "running"; + else if ( failed ) status = "stopped"; + else if ( progress == 99 ) status = "cancelling"; + else if ( progress == 0 ) status = ""; + progressBarUpdate(serviceId, rowId, status, progress); + } + appendToLog(service, name, event); + }); + if ( updateProgressBar ) { + $(`#${service}-${name}-logs-navbar-button`).trigger("click"); + } + {# Set Buttons in correct state #} + // status: ["running", "starting", "na", "stopping", "cancelling", "stopped"] + let status = "stopped"; + {%- if spawner.cancel_pending %} + status = "cancelling"; + {%- elif not spawner.ready and spawner.active %} + status = "starting"; + {%- elif spawner.ready %} + status = "running"; + {% endif %} + updateHeaderButtons(service, name, status); + + const workshopValues = {{ db_workshops | tojson }}?.['{{ spawner.name }}']?.user_options || {}; + const serviceId = "{{ spawner.user_options.get("service", "jupyterlab") | lower }}"; + + + $(`input[id^='${serviceId}-{{ spawner.name }}-'][id$='-input']`).each( function () { + const $this = $(this); + const dataGroup = $this.attr("data-group"); + const key = $this.attr("data-element"); + let keys = []; + let newValue = ""; + if ( ["none", "default"].includes(dataGroup) ) { + keys = Object.keys(workshopValues); + if ( keys.includes(key) ) { + newValue = workshopValues[key]; + } + } else { + keys = Object.keys(workshopValues?.[dataGroup] || {}); + if ( keys.includes(key) ) { + newValue = workshopValues[dataGroup][key]; + } + } + if ( newValue ) { + $this.attr("data-alwaysdisabled", "true"); + $this.val(newValue); + $this.attr("value", newValue); + $this.prop("disabled", true); + + const labelElement = $(`#${$this.prop('id')}-cb`); + if ( labelElement ) { + labelElement.prop("checked", "true"); + labelElement.prop("disabled", "true"); + labelElement.trigger("change"); + } + } + }); + + + $(`select[id^='${serviceId}-{{ spawner.name }}-'][id$='-input']`).each( function () { + const $this = $(this); + const dataGroup = $this.attr("data-group"); + const key = $this.attr("data-element"); + let keys = []; + let newValue = ""; + if ( ["none", "default"].includes(dataGroup) ) { + keys = Object.keys(workshopValues); + if ( keys.includes(key) ) { + newValue = workshopValues[key]; + } + } else { + keys = Object.keys(workshopValues?.[dataGroup] || {}); + if ( keys.includes(key) ) { + newValue = workshopValues[dataGroup][key]; + } + } + if ( newValue ) { + $this.val(newValue); + $this.attr("value", newValue); + + const labelElement = $(`#${$this.prop('id')}-cb`); + if ( labelElement ) { + labelElement.prop("checked", "true"); + labelElement.prop("disabled", "true"); + labelElement.trigger("change"); + } + } + }); + for ( const [key, value] of Object.entries(workshopValues?.defaultvalues || {}) ) { + const inputElement = $(`[id^='${serviceId}-{{ spawner.name }}-'][id$='-${key}-input']`); + inputElement.val(value); + inputElement.trigger("change"); + } + + {%- if spawner and spawner.events %} + fillLogContainer(serviceId, "{{ spawner.name }}", {{ spawner.events | tojson }}); + {%- else %} + defaultLogs(serviceId, "{{ spawner.name }}"); + {%- endif %} + {%- endif %} + + const queryString = window.location.search; + const urlParams = new URLSearchParams(queryString); + if (urlParams.has('row') && urlParams.has('service')) { + + // Get the value of the "row" parameter + const serviceValue = urlParams.get('service'); + const rowValue = urlParams.get('row'); + + $(`div[id$='-table-div']:not([id^='${serviceValue}-'])`).hide(); + $(`div[id$='-table-div'][id^='${serviceValue}-']`).show(); + + $(`div[id$='-collapse']:not([id^='${serviceValue}-${rowValue}-collapse'])`).removeClass("show"); + $(`div[id^='${serviceValue}-${rowValue}-collapse']`).addClass("show"); + let x = document.getElementById(`${serviceValue}-${rowValue}-summary-tr`) + if ( x ) x.scrollIntoView(); + if ( urlParams.has('showlogs') ) $(`[id^='${serviceValue}-${rowValue}-'][id$='-logs-navbar-button']`).trigger("click"); + } + {%- if pagetype == vars.pagetype_workshop %} + logDebug && console.log(`Fill elements ...`); + $(`[id$='-input']`).trigger("trigger_init"); + logDebug && console.log(`Fill elements ... done`); + {%- endif %} + + {%- if pagetype == vars.pagetype_workshop %} + {%- endif %} + }); + }); +</script> diff --git a/templates/macros/table/helpers/options_js.jinja b/templates/macros/table/helpers/options_js.jinja new file mode 100644 index 0000000000000000000000000000000000000000..44f526aafb1defee6e9d925cb094f92375c9b9e9 --- /dev/null +++ b/templates/macros/table/helpers/options_js.jinja @@ -0,0 +1,88 @@ +{# lmod --> #} + function getModuleValues(serviceId, rowId, name, setName) { + const options = val(getInputElement(serviceId, rowId, "option")); + let values = []; + let keys = new Set(); + options.forEach(option => { + if (getServiceConfig(serviceId)?.options?.[option]?.[setName]) { + const nameSet = getServiceConfig(serviceId)?.options[option]?.[setName]; + Object.entries(userModulesConfig[name]) + .filter(([key, value]) => value.sets && value.sets.includes(nameSet)) + .forEach( ([key, value]) => { + if ( !keys.has(key) ) { + keys.add(key); + values.push([ + key, + value.displayName, + typeof value.default === 'object' && value.default !== null ? value.default.default : value.default, + value.href + ]); + } + }); + } + }); + return values; + } + + + function updateModule(serviceId, rowId, tabId, elementId, elementOptions, name, setName) { + const containerDiv = $(`div[id^='${serviceId}-${rowId}-${modulesTabName}-'][id$='${elementId}-checkboxes-div']`); + const inputDiv = $(`div[id^='${serviceId}-${rowId}-${modulesTabName}-'][id$='${elementId}-input-div']`); + let values = getModuleValues(serviceId, rowId, name, setName); + + // for workshops we will disable all checkboxes, so users cannot change the selection + let workshopPreset = false; + let workshopPresetChecked = []; + {%- if pagetype == vars.pagetype_workshop and db_workshops %} + const workshopValues = {{ db_workshops | tojson }}.user_options || {}; + if ( Object.keys(workshopValues).includes("userModules") && Object.keys(workshopValues.userModules).includes(name) ){ + workshopPreset = true; + const modules = workshopValues.userModules[name]; + if ( modules.length > 0 ) { + workshopPresetChecked = modules; + } + } + {%- endif %} + + // Ensure the container exists + if (containerDiv.length > 0 && values.length > 0) { + const idPrefix = containerDiv.attr('id').replace(/-checkboxes-div$/, ""); + containerDiv.html(''); + values.forEach(function (item) { + let isChecked = ''; + let isDisabled = ''; + if ( workshopPreset ) { + if ( workshopPresetChecked.includes(item[0]) ){ + isChecked = 'checked'; + } + isDisabled = 'disabled="true"'; + } else { + isChecked = item[2] ? 'checked' : ''; + } + // Create the new div block + const newDiv = $(` + <div id="${idPrefix}-${item[0]}-input-div" class="form-check col-sm-6 col-md-4 col-lg-3"> + <input type="checkbox" name="${item[0]}" class="form-check-input" id="${idPrefix}-${item[0]}-input" value="${item[0]}" ${isChecked} ${isDisabled}/> + <label class="form-check-label" for="${idPrefix}-${item[0]}-input"> + <span class="align-middle">${item[1]}</span> + <a href="${item[3]}" target="_blank" class="module-info text-muted ms-3"> + <span>{{ svg.info_svg | safe }}</span> + <div class="module-info-link-div d-inline-block"> + <span class="module-info-link" id="nbdev-info-link"> {{ svg.link_svg | safe }}</span> + </div> + </a> + </label> + </div> + `); + // Append the new div to the container + containerDiv.append(newDiv); + // Add toggle function to each checkbox + $(`#${idPrefix}-${item[0]}-input`).on("click", function (event) { + $(`input[id^='${serviceId}-${rowId}-${modulesTabName}-'][id$='-select-all']`).prop("checked", false); + $(`input[id^='${serviceId}-${rowId}-${modulesTabName}-'][id$='-select-none']`).prop("checked", false); + }); + }); + } + inputDiv.show(); + } +{# <-- lmod #} \ No newline at end of file diff --git a/templates/macros/table/helpers/systems_js.jinja b/templates/macros/table/helpers/systems_js.jinja new file mode 100644 index 0000000000000000000000000000000000000000..8f884ec1ab8b183cdb8a758772c7e4d5c11b2463 --- /dev/null +++ b/templates/macros/table/helpers/systems_js.jinja @@ -0,0 +1,630 @@ +{# Kube Systems --> #} + const kubeOutpostFlavors = {{ auth_state.outpost_flavors | tojson }}; + function _getKubeSystems() { + return Object.keys(systemConfig).filter(system => { + const backendService = systemConfig[system].backendService; + // Check if the backend service type is "kube" + return backendServicesConfig[backendService]?.type === "kube"; + }); + } + + const kubeSystems = _getKubeSystems(); + + function getAvailableKubeFlavorsS(systems) { + let ret = []; + + systems.forEach(system => { + const allFlavors = kubeOutpostFlavors[system]; + if ( allFlavors ) { + ret.push(...Object.keys(allFlavors) + .filter(key => allFlavors[key].max != 0) // do not use flavor.max == 0 + .filter(key => allFlavors[key].current < allFlavors[key].max || allFlavors[key].max == -1 ) // must be room for new jupyterlabs + .sort((a, b) => allFlavors[a].weight - allFlavors[b].weight) // sort by weight + .map(key => [key, allFlavors[key].display_name])); // get keyname + displayname + } + }); + return ret; + } + + function getUnavailableKubeFlavorsS(systems) { + let ret = []; + + systems.forEach(system => { + const allFlavors = kubeOutpostFlavors[system]; + + if ( allFlavors ) { + ret.push(...Object.keys(allFlavors) + .filter(key => allFlavors[key].max != 0) // do not use flavor.max == 0 + .filter(key => allFlavors[key].current >= allFlavors[key].max && allFlavors[key].max != -1) + .sort((a, b) => allFlavors[a].weight - allFlavors[b].weight) + .map(key => [key, allFlavors[key].display_name])); + } + }); + return ret; + } +{# <-- Kube Systems #} + +{# Unicore Systems --> #} + {# + JavaScript functions to get user specific information for the HPC systems which support UNICORE. + #} + + const resPattern = /^urn:(?<namespace>.+?(?=:res:)):res:(?<systempartition>[^:]+):(?<project>[^:]+):act:(?<account>[^:]+):(?<accounttype>[^:]+)$/; + const unicoreEntitlements = {{ auth_state.entitlements | list | tojson }}; + const unicorePreferredUsername = {{ auth_state.preferred_username | tojson }}; + const unicoreReservations = {{ reservations | tojson }}; + const unicoreMapSystems = {{ custom_config.mapSystems | tojson }}; + const unicoreMapPartitions = {{ custom_config.mapPartitions | tojson }}; + const unicoreDefaultPartitions = {{ custom_config.defaultPartitions | tojson }}; + + function extractEntitlementResources(entitlement) { + const match = resPattern.exec(entitlement); + if (match) { + // Access named capture groups using match.groups + let system_ = unicoreMapSystems[match.groups.systempartition.toLowerCase()]; + let partition_ = unicoreMapPartitions[match.groups.systempartition.toLowerCase()]; + if ( Object.keys(resourcesConfig[system_] ?? {}).includes(partition_) ){ + return { + systempartition: match.groups.systempartition, + project: match.groups.project, + account: match.groups.account, + accounttype: match.groups.accounttype + }; + } + } + return null; // Return null if no match is found + } + + function _getUnicoreAccountType() { + for (let entitlement of unicoreEntitlements) { + const entitlementInfo = extractEntitlementResources(entitlement); + + if (entitlementInfo && entitlementInfo.account === unicorePreferredUsername) { + return entitlementInfo.accounttype; + } + } + return null; + } + + const unicoreAccountType = _getUnicoreAccountType(); + + function _getUnicoreSystemPartitions() { + const systemPartitions = unicoreEntitlements + .map(extractEntitlementResources) + .filter(Boolean) + .map(tmp => tmp.systempartition); + + if (unicoreAccountType === "normal") { + return [...new Set(systemPartitions)]; + } + + if (unicoreAccountType === "secondary") { + return [...new Set( + systemPartitions.filter(systempartition => { + return unicoreEntitlements + .map(extractEntitlementResources) // Extract entitlement resources + .filter(Boolean) + .some(tmp => tmp.systempartition === systempartition && tmp.account === unicorePreferredUsername); + }) + )]; + } + + return []; + } + + const unicoreSystemPartitions = _getUnicoreSystemPartitions(); + + function _getUnicoreSystems() { + // Get all systems corresponding to the system partitions + let systems = unicoreSystemPartitions + .map(key => unicoreMapSystems[key.toLowerCase()]) // Map system partitions to their respective systems + .filter(system => system); // Remove falsy values (null, undefined, etc.) + + // If the unicoreAccountType is "normal", return all systems (no filtering) + if (unicoreAccountType === "normal") { + return [...new Set(systems)]; // Remove duplicates using Set + } + + // If the unicoreAccountType is "secondary", filter systems based on unicorePreferredUsername + if (unicoreAccountType === "secondary") { + return [...new Set( + systems.filter(system => { + // Filter systems where the system matches the unicorePreferredUsername in unicoreEntitlements + return unicoreEntitlements + .map(extractEntitlementResources) // Extract entitlement resources + .filter(Boolean) // Remove falsy values (null, undefined, etc.) + .some(tmp => tmp.systempartition && unicoreMapSystems[tmp.systempartition.toLowerCase()] === system && tmp.account === unicorePreferredUsername); + }) + )]; // Remove duplicates using Set + } + + // Return an empty array if unicoreAccountType is neither "normal" nor "secondary" + return []; + } + + const unicoreSystems = _getUnicoreSystems(); + + function _getAllUnicoreAccountsBySystemPartition() { + const accountsBySystemPartition = {}; // The output dictionary where key is systempartition and value is list of accounts + + // Initialize accounts list for each systempartition + unicoreSystemPartitions.forEach(function(systempartition) { + accountsBySystemPartition[systempartition] = new Set(); // Using Set to store unique accounts for each systempartition + }); + + // Iterate through all unicoreEntitlements and populate the accounts for the relevant unicoreSystemPartitions + unicoreEntitlements.forEach(function(entitlement) { + const entitlementInfo = extractEntitlementResources(entitlement); + + if (entitlementInfo && unicoreSystemPartitions.includes(entitlementInfo.systempartition)) { + accountsBySystemPartition[entitlementInfo.systempartition].add(entitlementInfo.account); + } + }); + + // Filter accounts based on unicoreAccountType + Object.keys(accountsBySystemPartition).forEach(function(systempartition) { + // If the unicoreAccountType is "secondary", only keep accounts that match unicorePreferredUsername + if (unicoreAccountType === "secondary") { + accountsBySystemPartition[systempartition] = [...accountsBySystemPartition[systempartition]] + .filter(account => account === unicorePreferredUsername); + } else { + // For "normal" account type, return all accounts + accountsBySystemPartition[systempartition] = [...accountsBySystemPartition[systempartition]]; + } + }); + return accountsBySystemPartition; + } + + const unicoreAccountsBySystemPartition = _getAllUnicoreAccountsBySystemPartition(); + + function getUnicoreProjectsBySystemPartition() { + const projectsBySystemPartition = {}; // Output dictionary where key is systempartition and value is list of projects + + // Initialize projects list for each systempartition + unicoreSystemPartitions.forEach(function(systempartition) { + projectsBySystemPartition[systempartition] = new Set(); // Using Set to store unique projects + }); + + // Iterate through all unicoreEntitlements and populate the projects for the relevant unicoreSystemPartitions + unicoreEntitlements.forEach(function(entitlement) { + const entitlementInfo = extractEntitlementResources(entitlement); + + // Check if entitlement has valid info and if it matches the system partitions list + if (entitlementInfo && unicoreSystemPartitions.includes(entitlementInfo.systempartition)) { + // Only add project if account matches the unicorePreferredUsername when unicoreAccountType is secondary + if (unicoreAccountType === "normal" || entitlementInfo.account === unicorePreferredUsername) { + projectsBySystemPartition[entitlementInfo.systempartition].add(entitlementInfo.project); + } + } + }); + + // Convert Set to Array for each systempartition in the output dictionary + Object.keys(projectsBySystemPartition).forEach(function(systempartition) { + projectsBySystemPartition[systempartition] = [...projectsBySystemPartition[systempartition]]; + }); + + return projectsBySystemPartition; + } + + function getUnicorePartitions() { + let partitions = new Set(); + + // Iterate over unicoreSystemPartitions and add the corresponding partition names to the set + unicoreSystemPartitions.forEach((partition) => { + const partitionName = unicoreMapPartitions[partition.toLowerCase()]; + if (partitionName) { + partitions.add(partitionName); + } + }); + + // Add default partitions to the set + Object.keys(unicoreDefaultPartitions).forEach((partition) => { + unicoreDefaultPartitions[partition].forEach((defaultPartition) => { + partitions.add(defaultPartition); + }); + }); + + // If the unicoreAccountType is "normal", return all partitions (no filtering) + if (unicoreAccountType === "normal") { + return [...partitions]; // Convert Set to Array (removes duplicates) + } + + // If the unicoreAccountType is "secondary", filter the partitions based on unicorePreferredUsername + if (unicoreAccountType === "secondary") { + return [...new Set( + [...partitions].filter(partition => { + // Check if the partition matches the preferred username from unicoreEntitlements + return unicoreEntitlements + .map(extractEntitlementResources) // Extract entitlement resources + .filter(Boolean) // Remove falsy values (null, undefined, etc.) + .some(tmp => tmp.systempartition && unicoreMapPartitions[tmp.systempartition.toLowerCase()] === partition && tmp.account === unicorePreferredUsername); + }) + )]; // Remove duplicates using Set + } + + // Return an empty array if unicoreAccountType is neither "normal" nor "secondary" + return []; + } + + function getUnicoreAccountsS(systems) { + const accounts = new Set(); // A Set to ensure accounts are unique + + // Iterate over the unicoreEntitlements to collect accounts related to the system + systems.forEach(system => { + unicoreEntitlements.forEach(function(entitlement) { + const entitlementInfo = extractEntitlementResources(entitlement); + + // Check if the entitlement is for the provided system + if (entitlementInfo && unicoreMapSystems[entitlementInfo.systempartition.toLowerCase()] === system) { + if (unicoreAccountType === "normal") { + // If unicoreAccountType is "normal", add all accounts for the system + accounts.add(entitlementInfo.account); + } else if (unicoreAccountType === "secondary") { + // If unicoreAccountType is "secondary", only add the account if it matches unicorePreferredUsername + if (entitlementInfo.account === unicorePreferredUsername) { + accounts.add(entitlementInfo.account); + } + } + } + }); + }); + + // Return the accounts as an array, since we're using a Set to avoid duplicates + return [...accounts]; + } + + function getUnicoreProjectsSA(systems, accounts) { + const projects = []; // Initialize an empty array to store the list of projects + + // Iterate through entitlements to find all projects for the system and account + systems.forEach(system => { + accounts.forEach(account => { + unicoreEntitlements.forEach(function(entitlement) { + const entitlementInfo = extractEntitlementResources(entitlement); + + // Check if entitlement matches the provided system + if (entitlementInfo) { + const mappedSystem = unicoreMapSystems[entitlementInfo.systempartition.toLowerCase()]; + if (mappedSystem === system) { + if (unicoreAccountType === "normal") { + // If unicoreAccountType is "normal", we check if the account matches + if (entitlementInfo.account === account) { + projects.push(entitlementInfo.project); // Add project to the list + } + } else if (unicoreAccountType === "secondary") { + // If unicoreAccountType is "secondary", only consider the entitlement if the account matches the preferred username + if (entitlementInfo.account === unicorePreferredUsername) { + if (entitlementInfo.account === account) { + projects.push(entitlementInfo.project); // Add project to the list + } + } + } + } + } + }); + }); + }); + return [...new Set(projects)]; + } + + function getUnicoreProjectsS(systems) { + const projects = []; // Initialize an empty array to store the list of projects + + // Iterate through entitlements to find all projects for the system and account + systems.forEach(system => { + unicoreEntitlements.forEach(function(entitlement) { + const entitlementInfo = extractEntitlementResources(entitlement); + + // Check if entitlement matches the provided system + if (entitlementInfo) { + const mappedSystem = unicoreMapSystems[entitlementInfo.systempartition.toLowerCase()]; + if (mappedSystem === system) { + projects.push(entitlementInfo.project); + } + } + }); + }); + return [...new Set(projects)]; + } + + function getUnicorePartitionsSAP(systems, accounts=[], projects=[]) { + // Initialize the result list of partitions + let partitions = []; + let interactivePartitions = []; + let allPartitions = []; + systems.forEach(system => { + accounts.forEach(account => { + projects.forEach(project => { + // 1. Add interactive partitions for the given system (if any) + + // 2. Get the system partitions for the given system and account/project (with entitlement checking) + const allPartitions_ = new Set(); // Using Set to ensure unique entries + + let interactiveAdded = false; + + // Iterate over the entitlements to get partitions for the specified system, account, and project + unicoreEntitlements.forEach(function(entitlement) { + const entitlementInfo = extractEntitlementResources(entitlement); + if (entitlementInfo && unicoreMapSystems[entitlementInfo.systempartition.toLowerCase()] === system && (entitlementInfo.project === project || project === "_all_")) { + // Apply unicoreAccountType logic + if (unicoreAccountType === "normal" || account === "_all_") { + if ( !interactiveAdded ){ + interactiveAdded = true; + const interactivePartitions_ = systemConfig[system]?.interactivePartitions || []; + interactivePartitions = [...new Set([...interactivePartitions, ...interactivePartitions_])]; // Start with interactive partitions + } + // For normal accounts, match the exact account + if (entitlementInfo.account === account || account === "_all_") { + allPartitions_.add(unicoreMapPartitions[entitlementInfo.systempartition.toLowerCase()]); + } + } else if (unicoreAccountType === "secondary") { + // For secondary accounts, only match if the account is the preferred username + if ( !interactiveAdded ){ + interactiveAdded = true; + const interactivePartitions_ = systemConfig[system]?.interactivePartitions || []; + interactivePartitions = [...new Set([...interactivePartitions, ...interactivePartitions_])]; // Start with interactive partitions + } + if (entitlementInfo.account === unicorePreferredUsername && entitlementInfo.account === account) { + allPartitions_.add(unicoreMapPartitions[entitlementInfo.systempartition.toLowerCase()]); + } + } + } + // 3. Add the partitions from entitlements to the list (remove duplicates automatically due to Set) + allPartitions = [...new Set([...allPartitions, ...allPartitions_])]; + }); + + // 4. Add default partitions for the given system + + Object.keys(unicoreDefaultPartitions).forEach(function(systempartition) { + let system_ = unicoreMapSystems[systempartition]; + if ( system_ === system ) { + // Check if the systempartition matches + if (unicoreDefaultPartitions[systempartition]) { + // Add the corresponding partition from the unicoreMapPartitions object + if (allPartitions.includes(unicoreMapPartitions[systempartition])) { + unicoreDefaultPartitions[systempartition].forEach(function(defaultPartition) { + allPartitions.push(unicoreMapPartitions[defaultPartition.toLowerCase()]); + }); + } + } + } + }); + }); + }); + }); + + // 5. Return the list of partitions (interactive first, then others, with defaults added) + return [...new Set([...interactivePartitions, ...allPartitions])]; + } + + function getAllUnicoreReservations() { + // Initialize an empty array to store reservation names + let reservations = []; + + // Iterate over each system in the reservations object + Object.keys(unicoreReservations).forEach(system => { + // For each system, iterate over the reservations array + unicoreReservations[system].forEach(reservation => { + // Add the entire reservation object to the list (instead of just ReservationName) + reservations.push(reservation); + }); + }); + + // Return the list of all reservations + return reservations; + } + + function getUnicoreReservationsS(systems) { + // Check if the system exists in the reservations object + let reservations = []; + systems.forEach(system => { + if (unicoreReservations[system]) { + // Map the reservations for the system and return an array of ReservationNames + reservations.push(unicoreReservations[system].filter(reservation => reservation).map(reservation => reservation)); + } + }); + return reservations; + } + + function getUnicoreReservationsSAPP(systems, accounts, projects, partitions) { + // Check if the system exists in the reservations object + let reservations = []; + + systems.forEach(system => { + accounts.forEach(account => { + projects.forEach(project => { + partitions.forEach(partition => { + if (!unicoreReservations[system]) { + return; + } + + // Check if the partition is interactive for the given system + const isInteractivePartition = systemConfig[system] && systemConfig[system].interactivePartitions.includes(partition); + + // If the partition is interactive, do not return any reservations for it + if (isInteractivePartition) { + return; + } + + // Filter the reservations for the given system based on the provided account, project, and partition + reservations.push( + ...unicoreReservations[system].filter(reservation => { + const partitionMatches = (reservation.PartitionName === "" || reservation.PartitionName === partition || partition === "_all_"); + const usersMatch = (reservation.Users === "" || reservation.Users.split(",").includes(account) || account === "_all_"); + const accountsMatch = (reservation.Accounts === "" || reservation.Accounts === project || project === "_all_"); + return partitionMatches && usersMatch && accountsMatch; + }) + ); + }); + }); + }); + }); + return [...new Set(reservations)]; + } + + + function getUnicoreValues(serviceId, rowId, elementId) { + const inputElement = getInputElement(serviceId, rowId, elementId); + const labelElementCB = getLabelCBElement(serviceId, rowId, elementId); + //if (inputElement.length == 0 || inputElement.attr("data-collect") === "false" ) { + if (inputElement.length == 0 || inputElement.is("[disabled]") ) { + // Input does not exist, or is disabled. Use the keyword _all_ instead. + return ["_all_"]; + } else { + return val(inputElement); + } + + } + + function getAccountOptions(serviceId, rowId) { + const systems = val(getInputElement(serviceId, rowId, "system")); + const accounts = getUnicoreAccountsS(systems); + if (accounts.includes(unicorePreferredUsername)) { + accounts.sort(account => account === unicorePreferredUsername ? -1 : 1); + } + return accounts.map(item => [item, item]); + } + + function getProjectOptions(serviceId, rowId) { + const systems = val(getInputElement(serviceId, rowId, "system")); + let projects = []; + const accountInput = getInputElement(serviceId, rowId, "account"); + const accounts = val(accountInput); + if ( accountInput.length > 0 && accounts.length > 0 && accounts[0] ){ + // Account Option exists, let's take it into account + const accounts = val(accountInput); + projects = getUnicoreProjectsSA(systems, accounts); + } else { + // Acount selection does not exists (e.g. in workshopManager) + projects = getUnicoreProjectsS(systems); + } + return projects.map(item => [item, item]); + } + + function getPartitionOptions(serviceId, rowId) { + const systems = val(getInputElement(serviceId, rowId, "system")); + const accounts = getUnicoreValues(serviceId, rowId, "account"); + const projects = getUnicoreValues(serviceId, rowId, "project"); + let partitions = getUnicorePartitionsSAP(systems, accounts, projects); + + return partitions.map(item => [item, item]); + } + + function getPartitionAndInteractivePartition(serviceId, rowId) { + const systems = val(getInputElement(serviceId, rowId, "system")); + const partitions = getPartitionOptions(serviceId, rowId); + let interactivePartitionsLength = 0; + let interactivePartitionAdded = []; + partitions.forEach(partition => { + let partition_ = partition[0]; + systems.forEach(system => { + if ( (systemConfig[system]?.interactivePartitions || []).includes(partition_) ) { + if ( !interactivePartitionAdded.includes(partition_) ) { + interactivePartitionsLength += 1; + interactivePartitionAdded.push(partition_); + } + } + }); + }); + return [partitions, interactivePartitionsLength]; + } + + function getReservationOptions(serviceId, rowId) { + const systems = val(getInputElement(serviceId, rowId, "system")); + const accounts = getUnicoreValues(serviceId, rowId, "account"); + const projects = getUnicoreValues(serviceId, rowId, "project"); + const partitions = getUnicoreValues(serviceId, rowId, "partition"); + + return getUnicoreReservationsSAPP(systems, accounts, projects, partitions); + } +{# <-- Unicore Systems #} + +{# All Systems --> #} + function _getAllSystems() { + // Combine both lists and remove duplicates using a Set + let allSystems = [...new Set([...unicoreSystems, ...kubeSystems])]; + + {%- if pagetype == vars.pagetype_workshop %} + const db_workshops = {{ db_workshops | tojson }}; + let allowedSystems = Object.values(db_workshops)[0]?.user_options?.system ?? false; + if ( allowedSystems ) { + allSystems = allSystems.filter(system => allowedSystems.includes(system)); + } + {%- endif %} + + return allSystems; + } + + const allSystems = _getAllSystems(); + + function getAvailableSystemOptions(serviceId, options) { + let ret = []; + options.forEach(option => { + if (getServiceConfig(serviceId)?.options) { + const subSystems1 = getServiceConfig(serviceId).options[option].allowedLists.systems; + ret.push(...allSystems.filter(system => subSystems1.includes(system))); + } else { + // return all systems, if it's not reduced by the option + ret.push(...allSystems); + } + }); + + const uniqueSystems = [...new Set(ret)]; + uniqueSystems.sort((a, b) => (systemConfig[a].weight || 0) - (systemConfig[b].weight || 0)); + + return uniqueSystems.map(item => [item, item]); + } + + function getMissingSystemOptions(serviceId, rowId, options) { + let availableSystems = getAvailableSystemOptions(serviceId, options); + let missingSystems = allSystems.filter(system => !availableSystems.map(([key, value]) => key).includes(system)); + + {%- if pagetype == vars.pagetype_workshop %} + const db_workshops = {{ db_workshops | tojson }}; + let allowedSystems = db_workshops[rowId]?.user_options?.system ?? false; + if ( allowedSystems ) { + missingSystems = missingSystems.filter(system => allowedSystems.includes(system)); + } + {%- endif %} + return missingSystems.map(item => [item, item]); + } + + + function getSystemValues(serviceId, rowId, element) { + let systems = val(getInputElement(serviceId, rowId, "system")); + let values = []; + if ( element === "system" ){ + values = systems; + } else { + let systemTypesChecked = []; + systems.forEach(system => { + const backendService = systemConfig[system]?.backendService; + const systemType = backendServicesConfig[backendService]?.type; + if ( !systemTypesChecked.includes(systemType) ) { + systemTypesChecked.push(systemType); + let value = $(`[id^='${serviceId}-${rowId}-'][id$='-${element}-input']`).val(); + if ( value ) { + if (!Array.isArray(value)) { + value = [value]; + } + values.push(...value); + } + } + }); + } + return values; + } + + + function getSystemTypes(serviceId, rowId) { + const systems = getSystemValues(serviceId, rowId, "system"); + let systemTypes = []; + systems.forEach(system => { + const systemType = mappingDict[serviceId]?.["system"]?.[system] ?? system; + if ( !systemTypes.includes(systemType) ){ + systemTypes.push(systemType); + } + }); + return systemTypes; + } +{# <-- All Systems #} \ No newline at end of file diff --git a/templates/macros/table/table.jinja b/templates/macros/table/table.jinja new file mode 100644 index 0000000000000000000000000000000000000000..ec408b59628f008328b80547efc69f35fbdef84c --- /dev/null +++ b/templates/macros/table/table.jinja @@ -0,0 +1,149 @@ +{%- macro tables( + frontend_config, + macro_description, + macro_headerlayout, + macro_defaultheader, + macro_firstheader, + macro_row_content, + header_button_functions={}, + sse_functions=false +)%} + <div id="global-content-div" class="container-fluid p-4"> + <input id="service-input" class="form-control" data-collect="true" data-group="default" data-type="input" name="service" data-element="service" value="{{ frontend_config.get("services", {}).get("default", "jupyterlab") }}" style="display: none"/> + {#- TABLE #} + {%- for service_id, service_options in frontend_config.get("services", {}).get("options", {}).items() %} + {%- set is_first_service = loop.first %} + <div id="{{ service_id }}-table-div" class="table-responsive-md"> + {%- if macro_description %} + {{ macro_description() }} + {%- endif %} + <table id="{{ service_id }}-table" class="table table-bordered table-striped table-hover table-light align-middle"> + {#- TABLE HEAD #} + <thead class="table-secondary"> + <tr> + {%- if macro_headerlayout %} + {{ macro_headerlayout() }} + {%- endif %} + </tr> + </thead> + {#- TABLE BODY #} + <tbody> + {# - List existing workshops #} + {%- for row_id, row_options in table_rows.items() %} + {%- set is_first_row = loop.first %} + <!-- summary of row --> + <tr id="{{ service_id }}-{{ row_id }}-summary-tr" data-server-id="{{ service_id }}-{{ row_id }}" class="summary-tr"> + <td class="details-td" data-bs-target="#{{ row_id }}-collapse"> + {%- if loop.first %} + <div class="d-flex mx-4"> + {{ svg.plus_svg | safe }} + </div> + {%- else %} + <div class="d-flex mx-auto accordion-icon collapsed mx-4"></div> + {%- endif %} + </td> + {%- if loop.index0 and macro_defaultheader %} + {{ macro_defaultheader(service_id, row_id, row_options) }} + {%- else %} + {%- if macro_firstheader %} + {{ macro_firstheader(service_id, row_id, row_options) }} + {%- endif %} + {%- endif %} + </tr> + + <!-- collapsible row --> + <tr data-server-id="{{ service_id }}-{{ row_id }}" class="collapsible-tr" style="--bs-table-accent-bg: transparent;"> + <td colspan="100%" class="p-0"> + <div class="collapse {%- if loop.first and (table_rows | length == 1) %} show {%- endif -%}" id="{{ service_id }}-{{row_id}}-collapse"> + <div class="d-flex align-items-start m-3"> + {%- if service_options.navbar | length > 0 %} + <div class="nav flex-column nav-pills p-3 ps-0" style="min-width: 15% !important" id="{{ service_id }}-{{ row_id }}-tab-button-div" role="tablist"> + {%- for button_id, button_options in service_options.navbar.items() %} + {%- if ( is_first_row and button_options.get("firstRow", true) ) or + ( (not is_first_row) and button_options.get("defaultRow", true) ) + %} + {%- set style_hide = 'height: 0 !important; overflow: hidden !important; padding-top: 0 !important; padding-bottom: 0 !important; border: none !important; margin: 0 !important;' %} + <button + class="nav-link {{ 'active' if show else '' }} {{ button_options.get("margins", "mb-3") }} {%- if loop.index0 == 0 %} active {%- endif -%}" + id="{{ service_id }}-{{ row_id }}-{{ button_id }}-navbar-button" + {%- if not button_options.get("show", false) %} + {# + Instead of just .hide() it, we want to keep the width of the buttons, + so the interface does not wabble around when showing / hiding buttons. + #} + style="{{ style_hide }}" + {%- endif %} + name="{{ button_id }}" + data-tab="{{ button_id }}" + data-service="{{ service_id }}" + data-row="{{ row_id }}" + data-bs-toggle="pill" + data-bs-target="#{{ service_id }}-{{ row_id }}-{{ button_id }}" + {%- if button_options.get("show", true) %} + data-show="true" + {%- endif %} + type="button" + {%- for specific_key, specific_values in button_options.get("dependency", {}).items() %} + data-dependency-{{ specific_key }}="true" + {%- for specific_value in specific_values %} + data-dependency-{{ specific_key }}-{{ specific_value }}="true" + {%- endfor %} + {%- endfor %} + role="tab"> + <span>{{ button_options.get("displayName", "Unknown Button") }}</span> + <span id="{{ service_id }}-{{ row_id }}-{{ button_id }}-tab-input-warning" class="d-flex invisible"> + {{ svg.warning_svg | safe }} + <span class="visually-hidden">settings changed</span> + </span> + </button> + {%- endif %} + {%- endfor %} + </div> + {%- endif %} + <div class="tab-content w-100" data-row="{{ row_id }}" data-service="{{ service_id }}" data-sse-progress id="{{ service_id }}-{{ row_id }}-tabContent-div"> + <form id="{{ service_id }}-{{ row_id }}-form"> + {%- for tab_id, tab_options in service_options.get("tabs", {}).items() %} + <div class="tab-pane fade {%- if loop.first or tab_id == "buttonrow" %} show active"{%- else -%}" style="display: none" {%- endif %} id="{{ service_id }}-{{ row_id }}-{{ tab_id }}-contenttab-div" role="tabpanel"> + <div class="row"> + {{ macro_row_content(service_id, service_options, row_id, tab_id) }} + </div> + </div> + {%- endfor %} + </form> + </div> + </div> + </div> + </td> + </tr> + {%- endfor %} + </tbody> + </table> + </div> {#- table responsive #} + {%- endfor %} + </div> {#- container fluid #} + <script> + require(["jquery", "jhapi", "utils"], function ( + $, + JHAPI, + utils + ) { + "use strict"; + + var base_url = window.jhdata.base_url; + var user = window.jhdata.user; + var api = new JHAPI(base_url); + + + + {%- for button_key, button_func in header_button_functions.items() %} + $(`button[id$='-{{ button_key }}-btn-header']`).on("click", function() { + const $this = $(this); + {{ button_func }}($this.attr('data-service'), $this.attr('data-row'), $this.attr('data-element'), {}, user, api, base_url, utils) + }); + {%- endfor %} + {%- if sse_functions %} + {{ sse_functions() }} + {%- endif %} + }); + </script> +{%- endmacro %} diff --git a/templates/macros/table/table_js.jinja b/templates/macros/table/table_js.jinja new file mode 100644 index 0000000000000000000000000000000000000000..f71eec239c268eab6faae71253386a70e5fed1c4 --- /dev/null +++ b/templates/macros/table/table_js.jinja @@ -0,0 +1,1884 @@ +{# + Different sites may use the functions slighty different +#} + +{%- import "macros/table/variables.jinja" as vars with context %} +{%- import "macros/svgs.jinja" as svg -%} + +<script type="text/javascript"> + + + + // table_js_start + + + // Define the regex pattern with named capture groups + const serviceConfig = {{ custom_config.services | tojson }}; + const userModulesConfig = {{ custom_config.userModules | tojson }}; + const systemConfig = {{ custom_config.systems | tojson }}; + const resourcesConfig = {{ custom_config.resources | tojson }}; + const backendServicesConfig = {{ custom_config.backendServices | tojson }}; + + const mappingDict = {} + + {% include 'macros/table/helpers/systems_js.jinja' with context %} + + Object.entries(serviceConfig) + .forEach(([key, value]) => { + const serviceId = value.serviceId ?? key; + if ( !Object.keys(mappingDict).includes(serviceId) ){ + mappingDict[serviceId] = { + "serviceKey": key, + "system": {}, + "option": {} + }; + } + Object.entries(value.options).forEach(([optionKey, optionValue]) => { + mappingDict[serviceId]["option"][optionKey] = optionValue.type ?? optionKey; + }); + allSystems.forEach(system => { + const backendService = systemConfig[system].backendService; + const systemType = backendServicesConfig[backendService]?.type ?? system; + if (!Object.keys(mappingDict[serviceId]["system"]).includes(systemType)) { + mappingDict[serviceId]["system"][system] = systemType; + } + }); + }); + + function getServiceConfig(serviceId) { + const key = mappingDict[serviceId]["serviceKey"]; + return serviceConfig[key]; + } + + function val(obj) { + let ret = ""; + if ( obj.is("input[type='checkbox']") ) { + ret = obj.prop('checked'); + } else if ( obj.is("select") ) { + ret = obj.val(); + if ( !Array.isArray(ret) ){ + ret = [ret]; + } + } else { + ret = obj.val(); + } + return ret; + } + + function getInputElement(serviceId, rowId, elementId) { + return $(`[id^='${serviceId}-${rowId}-'][id$='-${elementId}-input']`); + } + + function getLabelCBElement(serviceId, rowId, elementId) { + return $(`input[id^='${serviceId}-${rowId}-'][id$='-${elementId}-input-cb']`); + } + + function getInputDiv(serviceId, rowId, elementId) { + return $(`div[id^='${serviceId}-${rowId}-'][id$='-${elementId}-input-div']`); + } + + function getLabel(inputElement) { + return $(`label[for='${inputElement.prop("id")}']`); + } + + function getInvalidFeedback(inputDiv) { + return inputDiv.find(".invalid-feedback"); + } + + function getOptionTypes(serviceId, rowId) { + const options = val(getInputElement(serviceId, rowId, "option")); + let ret = []; + options.forEach(option => { + ret.push(mappingDict[serviceId]?.["option"]?.[option] ?? option); + }); + return ret; + } + + + + + {# Fill Input elements -> #} + + function fillSelect(elementId, select, values_, groups = {}, inactive_values = [], inactive_text = "N/A") { + let values = values_; + {%- if pagetype == vars.pagetype_workshop and db_workshops %} + const key = select.attr("name"); + const rowId = select.attr("data-row"); + const workshopValues = {{ db_workshops | tojson }}?.[rowId]?.user_options || {}; + if ( Object.keys(workshopValues).includes(key) ) { + let valueKeys = workshopValues[key]; + if ( !Array.isArray(valueKeys) ){ + valueKeys = [valueKeys]; + } + values = []; + values_.forEach( item => { + if ( valueKeys.includes(item[0]) ){ + values.push(item); + } + }); + groups = {}; + } + {%- endif %} + const labelElement = $(`label[for='${select.attr("id")}']`); + const checkBox = labelElement.find("input[type='checkbox']"); + let preValue = select.val(); + select.html(""); + let valueIndex = 0; + + for (const groupLabel in groups) { + if (groups.hasOwnProperty(groupLabel)) { + const groupSize = groups[groupLabel]; + select.append(`<optgroup label="${groupLabel}">`); + for (let i = 0; i < groupSize; i++) { + if (valueIndex < values.length) { + select.append(`<option value="${values[valueIndex][0]}">${values[valueIndex][1]}</option>`); + valueIndex++; + } + } + select.append(`</optgroup>`); + } + } + + while (valueIndex < values.length) { + select.append(`<option value="${values[valueIndex][0]}">${values[valueIndex][1]}</option>`); + valueIndex++; + } + + // Add a horizontal line if there are inactive options + if (inactive_values.length > 0) { + select.append('<hr>'); + } + + // Add inactive options at the end of the dropdown + inactive_values.forEach(([key, value]) => { + select.append(`<option value="${key}" disabled>${value} (${inactive_text})</option>`); + }); + + if ( preValue && select.find(`option[value="${preValue}"]:not(:disabled)`).length) { + select.val(preValue); + } else { + if ( select.prop("multiple") ) { + select.val(null); + } else { + if (values.length > 0){ + select.val(values[0][0]); + } else { + console.error(`Could not fill object. Check configuration.`); + + {%- if pagetype == vars.pagetype_workshop %} + workshopNotUsable(select); + {%- endif %} + } + } + } + } + + {# <- Fill Input elements #} + {%- if pagetype == vars.pagetype_workshop %} + function workshopNotUsable(element) { + const helpDiv = $('#workshopnotusable'); + if ( helpDiv.children().length === 0 ) { + const serviceId = element.attr("data-service"); + const rowId = element.attr("data-row"); + const elementName = element.attr("data-element"); + const workshop = {{ db_workshops | tojson }}?.[rowId]?.user_options || {}; + const workshopId = workshop.workshopid; + + const workshopSystems = workshop?.system || []; + let workshopProject = workshop?.project || []; + if ( !Array.isArray(workshopProject) ){ + workshopProject = [workshopProject]; + } + let workshopPartition = workshop?.partition || []; + if ( !Array.isArray(workshopPartition) ){ + workshopPartition = [workshopPartition]; + } + + var partitionLinkText = ""; + var partitionLinkText2 = ""; + var projectInviteText = ""; + if ( workshopProject.length === 0 ) { + projectInviteText = ` + <li style="color: #333;">Enter the Project id, that was handed out during the workshop invivation. If in doubt, ask the workshop instructor for the project id.</li> + ` + partitionLinkText = ` + <li style="color: #333;">Visit <a href="https://judoor.fz-juelich.de" target="_blank">JuDoor</a> and sign in with the credentials you've used to log into here.</li> + <li style="color: #333;">Click on the project of this workshop.</li> + <li style="color: #333;">Click on "Request access for resources".</li> + <img src="{{ static_url("images/workshop/partition_01.png") }}" alt="Login Procedure" style="width: 100%; max-width: 400px; margin-top: 10px; border: 1px solid #ddd; border-radius: 5px;"> + ` + } else { + workshopProject.forEach(project => { + projectInviteText += ` + <li style="color: #333;">Enter "${project}" into Project id, add some additional information and clickon "Join project".</li> + ` + }) + if ( workshopProject.length === 1 ) { + partitionLinkText = ` + <li style="color: #333;">Visit <a href="https://judoor.fz-juelich.de/projects/${workshopProject[0]}/request" target="_blank">JuDoor</a> and sign in with the credentials you've used to log into here.</li> + + ` + } else { + partitionLinkText = ` + <li style="color: #333;">Visit <a href="https://judoor.fz-juelich.de/" target="_blank">JuDoor</a> and sign in with the credentials you've used to log into here.</li> + <li style="color: #333;">Click on the projects of this workshop ( ${workshopProject} ). Repeat the "request access for resources" for each project.</li> + <li style="color: #333;">Click on "Request access for resources".</li> + <img src="{{ static_url("images/workshop/partition_01.png") }}" alt="Login Procedure" style="width: 100%; max-width: 400px; margin-top: 10px; border: 1px solid #ddd; border-radius: 5px;"> + ` + } + } + + if ( workshopPartition.length === 0 ) { + partitionLinkText2 = ` + <li style="color: #333;">Select all partitions.</li> + ` + } else { + partitionLinkText2 = ` + <li style="color: #333;">Select these partitions: ${workshopPartition}.</li> + ` + } + var missingSystems = allSystems.filter(key => workshopSystems.includes(key)); + var stepLogin = ""; + var stepSystem = ""; + var stepProject = ""; + var stepPartition = ""; + // User doesn't have a access to a single system in the workshop + // Maybe we can check this in the feature via auth_state entitlements + stepLogin = ` + <details style="margin-bottom: 15px;"> + <summary style="font-weight: bold; margin-left: 10px; font-size: 16px; color: #0056b3; cursor: pointer;"> + - Use the correct AAI during the Login process (click here for more information) + </summary> + <div style="margin-left: 20px; margin-top: 10px;"> + <p style="color: #333;">When using HPC resources, you have to use the JSC Account during the login process.</p> + <ul> + <li style="color: #333;">Click on <a href="https://{{ hostname }}{{ base_url }}logout" target="_blank">Logout</a></li> + <li style="color: #333;">Click on <a href="https://{{ hostname }}{{ base_url }}login?next=%2Fhub%2Fworkshops%2F${workshopId}" target="_blank">Login</a> (make sure to come back to this page ("/workshops/${workshopId}") after logging in).</li> + <ul> + <li style="color: #333;">Click on "Sign In"</li> + <li style="color: #333;">Click on "Show other sign in options"</li> + <img src="{{ static_url("images/workshop/login_01.png") }}" alt="Login Procedure" style="width: 100%; max-width: 400px; margin-top: 10px; border: 1px solid #ddd; border-radius: 5px;"> + <li style="color: #333;">Click on "Sign in with JSC Account"</li> + <li style="color: #333;">Enter your JSC Account credentials and click on Login. Don't have an account yet? Click on register and follow the process. For more information look into the <a href="https://www.fz-juelich.de/en/ias/jsc/services/user-support/how-to-get-access-to-systems/judoor" target="_blank"> JuDoor documentation</a>.</li> + </ul> + </ul> + </div> + </details> + ` + stepProject = ` + <details style="margin-bottom: 15px;"> + <summary style="font-weight: bold; margin-left: 10px; font-size: 16px; color: #0056b3; cursor: pointer;"> + - Join Projects + </summary> + <div style="margin-left: 20px; margin-top: 10px;"> + <p style="color: #333;">When using HPC resources, you have to join a project, before you're allowed to use resources.</p> + <ul> + <li style="color: #333;">Visit <a href="https://judoor.fz-juelich.de" target="_blank">JuDoor</a> and sign in with the credentials you've used to log into here.</li> + <li style="color: #333;">Click on "Join a project"</li> + <img src="{{ static_url("images/workshop/project_01.png") }}" alt="Join Project" style="width: 100%; max-width: 400px; margin-top: 10px; border: 1px solid #ddd; border-radius: 5px;"> + ${projectInviteText} + <li style="color: #333;">You will receive an email. Follow the steps in this mail.</li> + <li style="color: #333;">For more information about joining projects look into the <a href="https://www.fz-juelich.de/en/ias/jsc/services/user-support/how-to-get-access-to-systems/judoor" target="_blank">JuDoor documentation</a></li> + </ul> + </div> + </details> + ` + + stepSystem = ` + <details style="margin-bottom: 15px; "> + <summary style="font-weight: bold; margin-left: 10px; font-size: 16px; color: #0056b3; cursor: pointer;"> + - Accept System Usage Policy + </summary> + <div style="margin-left: 20px; margin-top: 10px;"> + <p style="color: #333;">When using HPC resources, you have to accept the usage policy of a system, before you're allowed to use resources.</p> + <ul> + <li style="color: #333;">Visit <a href="https://judoor.fz-juelich.de" target="_blank">JuDoor</a> and sign in with the credentials you've used to log into here.</li> + <li style="color: #333;">You have to "sign the usage agreement" for the systems you want to use.</li> + <li style="color: #333;">It may take up to 30 minutes for your account to be fully updated and ready on the system after completing the steps.</li> + <li style="color: #333;">For more information look into the <a href="https://www.fz-juelich.de/en/ias/jsc/services/user-support/how-to-get-access-to-systems/judoor" target="_blank">JuDoor documentation</a></li> + </ul> + </div> + </details> + ` + stepPartition = ` + <details style="margin-bottom: 15px; "> + <summary style="font-weight: bold; margin-left: 10px; font-size: 16px; color: #0056b3; cursor: pointer;"> + - Request access for resources + </summary> + <div style="margin-left: 20px; margin-top: 10px;"> + <p style="color: #333;">When using HPC resources, you have to accept the usage policy of a system, before you're allowed to use resources.</p> + <ul> + <li style="color: #333;">Visit <a href="https://judoor.fz-juelich.de" target="_blank">JuDoor</a> and sign in with the credentials you've used to log into here.</li> + ${partitionLinkText} + ${partitionLinkText2} + <li style="color: #333;">Click on "Inform PIs and PAs about your request.</li> + <li style="color: #333;">The PI or PA has to accept your request.</li> + <li style="color: #333;">It may take up to 30 minutes for your account to be fully updated and ready on the system after completing the steps.</li> + <li style="color: #333;">For more information look into the <a href="https://www.fz-juelich.de/en/ias/jsc/services/user-support/how-to-get-access-to-systems/judoor" target="_blank">JuDoor documentation</a></li> + </ul> + </div> + </details> + ` + + + var genericHtml = ` + <div style="width: 80%; margin: auto; margin-top: 20px; margin-bottom: 20px; padding: 20px; border: 1px solid #ccc; border-radius: 10px; background-color: #f9f9f9;"> + <h2 style="text-align: center; color: #333;">Workshop "${workshop.workshopid}" not available for you</h2> + <p style="text-align: center; color: #666;">Your account is not yet ready to access this workshop. Please complete the steps below to proceed.</p> + + <div style="margin-top: 20px;"> + ${stepLogin} + ${stepProject} + ${stepSystem} + ${stepPartition} + </div> + <p style="text-align: center; color: darkorange;">It may take up to 60 minutes for the systems to fully process account updates. Any start attempts during this time might fail.</p> + </div> + ` + helpDiv.append(genericHtml); + $(`#global-content-div`).hide(); + } + } + {%- endif %} + + {# Button Helper functions --> #} + + function dictHasKey(obj, key) { + // Check if the key exists at the current level + if (Object.hasOwn(obj, key)) { + return true; + } + + // Traverse through nested objects or arrays + for (const k in obj) { + if (typeof obj[k] === "object" && obj[k] !== null) { + if (dictHasKey(obj[k], key)) { + return true; + } + } + } + + // If the key is not found + return false; + } + + function validateInput(inputElement) { + const labelElement = $(`label[for='${inputElement.attr("id")}']`); + const checkBox = labelElement.find("input[type='checkbox']"); + if ( checkBox.length > 0 && !checkBox.prop("checked") ) { + return true; + } else if( !inputElement[0].checkValidity() ) { + inputElement.addClass('is-invalid'); + inputElement.siblings('.invalid-feedback').show(); + return false; + } else { + inputElement.removeClass('is-invalid'); + inputElement.siblings('.invalid-feedback').hide(); + return true; + } + } + + function validateSelect(selectElement) { + const labelElement = $(`label[for='${selectElement.attr("id")}']`); + const checkBox = labelElement.find("input[type='checkbox']"); + if ( checkBox.length > 0 && !checkBox.prop("checked") ) { + return true; + } else if (selectElement.val() === "" + || selectElement.val() === undefined) + { + selectElement.addClass('is-invalid'); + selectElement.siblings('.invalid-feedback').show(); + return false; + } else { + selectElement.removeClass('is-invalid'); + selectElement.siblings('.invalid-feedback').hide(); + return true; + } + } + + function validateForm(serviceId, rowId) { + const form = $(`form[id='${serviceId}-${rowId}-form']`); + let ret = true; + form.find(`[id$='-input']:not(:disabled):not([data-collect="false"])`).each(function () { + let $this = $(this); + const valid = $this.is("input") ? validateInput($this) : $this.is("select") ? validateSelect($this) : false; + if ( !valid ) { + console.error("The following element is invalid: "); + console.log($this); + // If the user is looking at a different tab, we should highlight the button in the navbar + const buttonDiv = $(`#${serviceId}-${rowId}-tab-button-div`); + const activeTab = buttonDiv.find('.active').attr('name'); + const inputTab = $this.attr('data-tab'); + if ( inputTab !== activeTab ){ + buttonDiv.find(`button[data-tab='${inputTab}']`).click(); + } + ret = false; + } + }); + if ( ret ) { + form.find(`[id$='-input'].is-invalid`).removeClass('is-invalid'); + form.find(`[id$='-input'].invalid-feedback`).hide(); + } + return ret; + } + + function homeFillExistingRow(serviceId, rowId, user_options, fillingOrder) { + const excludes = `:not(${fillingOrder.map(value => `[data-element='${value}']`).join(',')})` + let available = true; + let availableDescription = ""; + // It's important to fill the user options in the right order + fillingOrder.forEach(key => { + if ( available ) { + const inputElement = $(`[id^='${serviceId}-${rowId}-'][id$='-${key}-input']`); + const dataGroup = inputElement.attr("data-group"); + const dataType = inputElement.attr("data-type"); + let newValue = ""; + if ( ["none", "default"].includes(dataGroup) ) { + newValue = user_options?.[key] ?? ""; + } else { + newValue = user_options?.[dataGroup]?.[key] ?? ""; + } + if ( newValue ) { + if ( dataType == "select" ){ + if (inputElement.find(`option[value="${newValue}"]`).length > 0) { + inputElement.val(newValue); + inputElement.trigger("change"); + } else { + available = false; + availableDescription = `${key} ${newValue} is not available for your account. Please try re-logging in.`; + console.log(`${key} ${newValue} currently not available`); + } + } else if (dataType == "number" ) { + const min = inputElement.attr("min"); + const max = inputElement.attr("max"); + if (newValue && newValue >= min && newValue <= max) { + inputElement.val(newValue); + inputElement.trigger("change"); + } else { + available = false; + availableDescription = `${key} ${newValue} is not in allowed range [${min}, ${max}].`; + console.log(`${key} ${newValue} currently not available`); + } + } else { + inputElement.val(newValue); + inputElement.trigger("change"); + } + } else if (inputElement.is("input[type='checkbox']") ) { + inputElement.prop("checked", false); + inputElement.trigger("change"); + } + } + }); + + const unorderedElements = $(`[id^='${serviceId}-${rowId}-'][id$='-input']${excludes}`); + unorderedElements.each(function () { + if ( available ) { + const inputElement = $(this); + const key = inputElement.attr("data-element"); + const dataGroup = inputElement.attr("data-group"); + + let newValue = ""; + if ( ["none", "default"].includes(dataGroup) ) { + newValue = user_options?.[key] ?? ""; + } else { + newValue = user_options?.[dataGroup]?.[key] ?? ""; + } + if (inputElement.is("input[type='checkbox']") ) { + if ( newValue ) { + inputElement.prop("checked", true); + inputElement.trigger("change"); + } else { + inputElement.prop("checked", false); + inputElement.trigger("change"); + } + } else if ( newValue ) { + inputElement.val(newValue); + inputElement.trigger("change"); + } + } + }); + if ( !available ) { + console.log(`tr.collapsible-tr[data-server-id='${serviceId}-${rowId}']`); + $(`tr.collapsible-tr[data-server-id='${serviceId}-${rowId}']`).remove(); + console.log("Header NA"); + updateHeaderButtons(serviceId, rowId, "na"); + let description = ` + <div id="${serviceId}-${rowId}-config-td-nadescription-div" class="col text-lg-center col-12 col-lg-12"> + <span id="${serviceId}-${rowId}-config-td-nadescription">${availableDescription}</span> + </div> + `; + const headerDescription = $(`#${serviceId}-${rowId}-config-td-div`); + headerDescription.addClass("justify-content-center"); + $(`#${serviceId}-${rowId}-config-td-div`).html(description); + } + } + + function workshopManagerFillExistingRow(serviceId, rowId, workshopDict) { + const form = $(`form[id='${serviceId}-${rowId}-form']`); + + // We must run through the data groups in the correct order, to allow correct trigger behavior + const selectors = [ + "[id$='-input'][data-group='none']", + "[id$='-input'][data-group='default']", + "[id$='-input']:not([data-group='none']):not([data-group='default']):not([data-group='defaultvalues'])", + "[id$='-input'][data-group='defaultvalues']", + ] + selectors.forEach( selector => { + form.find(`${selector}`).each(function () { + const $this = $(this); + const id = $this.prop('id'); + let key = $this.attr('data-element'); + key = $this.attr('data-parent') || key; + const dataGroup = $this.attr('data-group'); + if ( dataGroup === "defaultvalues" ) { + $this.trigger(`trigger_${key}`); + } + let keys = ""; + let newValue = ""; + if ( ["none", "default"].includes(dataGroup) ) { + keys = Object.keys(workshopDict?.["user_options"]); + if ( keys.includes(key) ) { + newValue = workshopDict["user_options"][key]; + } + } + else { + keys = Object.keys(workshopDict?.["user_options"]?.[dataGroup] || {}); + if ( keys.includes(key) ) { + newValue = workshopDict["user_options"][dataGroup][key]; + } + } + + const parentInputDiv = $(`#${id}-div`); + const labelInput = $(`#${id}-cb`); + if ( newValue ) { + $this.attr("data-collect", true); + if ( $this.is("input[type='checkbox']") ) { + $this.prop('checked', !!newValue); + } else { + $this.val(newValue); + } + // enable, since it's part of the stored user_options + const alwaysDisabled = $this.attr('data-alwaysdisabled') || false; + if ( !alwaysDisabled ) { + $this.prop("disabled", false); + } + if ( labelInput && labelInput.length > 0 ) { + labelInput.prop("disable", false); + labelInput.prop("checked", "checked"); + } + parentInputDiv.show(); + } else { + // Set to default values + if ( $this.is("input[type='checkbox']") ) { + const checked = $this.attr("data-default"); + $this.prop('checked', !!$this.attr('data-checked')); + } + if ( $this.attr('data-enabled') != undefined ) { + if ( $this.attr('data-enabled') === "true" ) { + $this.prop('disabled', false); + } else { + $this.prop('disabled', true); + } + } + if ( labelInput && labelInput.length > 0 ) { + const labelInputEnable = labelInput.attr('data-enabled') === "true"; + const labelInputCheck = labelInput.attr('data-checked') === "true"; + labelInput.prop("disable", !labelInputEnable); + labelInput.prop("checked", labelInputCheck); + } + } + $this.trigger("change"); + }); + }); + + if ( !isWorkshopInstructor() ) { + console.log("No Instructor"); + // double check to hide / disable the instructor elements. + form.find(`input[data-instructor]`).prop("disabled", true); + form.find(`div[data-instructor="show"][id$="-input-div"]`).hide(); + } + } + + function collectWorkshopOptions(serviceId, rowId) { + const form = $(`form[id='${serviceId}-${rowId}-form']`); + const options = {}; + form.find(`input[data-group="none"][id$='-input'], input[data-group="none"][id$='-cb-input']`).each(function () { + const $this = $(this); + let value = ""; + if ( !$this.prop("disabled") || $this.attr('data-group') === "none" ) { + if ( $this.is("input[type='checkbox']") ){ + value = $this.prop('checked'); + } else { + if ( Array.isArray(value) && values.length == 1 ) { + value = value[0]; + } + value = $this.val(); + } + options[$this.attr('name')] = value; + } + }); + return options; + } + + function collectSelectedOptions(serviceId, rowId, allCheckboxes=false) { + const form = $(`form[id='${serviceId}-${rowId}-form']`); + let ret = {}; + // collect all inputs in default group + form.find(`[id^='${serviceId}-${rowId}-'][id$='-input'][data-collect="true"]:not([data-group="none"])`).each(function () { + let $this = $(this).first(); + let dataGroupValue = $this.attr('data-group'); + let value = ""; + let addValue = true; + let id = $this.prop("id"); + let labelInput = $(`#${id}-cb`); + let parent = $this.attr("data-parent"); + let name = parent || $this.attr("name"); + if ( parent ) { + let parentElement = $(`[id^='${serviceId}-${rowId}-'][id$='-${parent}-input']`); + addValue = parentElement.attr("data-collect") === "true"; + } + if ( addValue ) { + if ( labelInput.length > 0 && !labelInput.prop('checked') ) { + addValue = false; + } else { + if ( $this.is("input[type='checkbox']") ){ + value = $this.prop('checked'); + if ( !value && !allCheckboxes ) { + addValue = false; + } + } else { + if ( Array.isArray(value) && values.length == 1 ) { + value = value[0]; + } + value = $this.val(); + } + } + } + + if ( addValue ) { + if ( dataGroupValue === "default" ) { + ret[$this.attr('name')] = value; + } else if ( dataGroupValue != "none" ) { + if (!Object.keys(ret).includes(dataGroupValue)) { + ret[dataGroupValue] = {} + } + ret[dataGroupValue][name] = value; + } + } + }); + let profile = ""; + if ( Object.keys(ret).includes("option") ) profile = ret.option; + else profile = serviceId; + ret["profile"] = profile; + ret["service"] = serviceId; + + if ( !Object.keys(ret).includes("name") || !ret?.name ) { + ret["name"] = `Unnamed ${serviceId}`; + } + + console.log("Collected Options in frontend:"); + console.log(ret); + return ret; + } + + {# <-- Button Helper functions #} + + {# Workshop Manager --> #} + {# WorkshopManager.helper --> #} + function isWorkshopInstructor() { + {%- if is_instructor %} + return true; + {%- else %} + return false; + {%- endif %} + } + + function isFirstRow(rowId) { + return rowId === "{{ vars.first_row_id }}"; + } + {# <-- WorkshopManager.helper #} + + {# WorkshopManager.none.workshopid --> #} + function workshopManagerWorkshopId(trigger, serviceId, rowId, tabId, elementId, elementOptions) { + const $this = $(`input[id^="${serviceId}-${rowId}-"][id$="-${elementId}-input"]`); + if ( !isFirstRow(rowId) ){ + $this.val(rowId); + } else { + if ( isWorkshopInstructor() ) { + $this.prop("disabled", false); + $this.prop("placeholder", elementOptions?.["input"]?.["options"]?.["placeholderInstructor"] || "W"); + } + } + } + {# <-- WorkshopManager.none.workshopid #} + + {# WorkshopManager.default.option --> #} + function workshopManagerFillOptions(trigger, serviceId, rowId, tabId, elementId, elementOptions) { + {# I think that's not needed + let valueKeys = []; + if (getServiceConfig(serviceId)?.options) { + valueKeys = Object.keys(getServiceConfig(serviceId).options); + } + let values = Object.entries(getServiceConfig(serviceId).options) + .filter(([key, value]) => valueKeys.includes(key)) + .map(([key, value]) => [key, value.name]); + #} + let values = getServiceConfig(serviceId).options; + + {%- if pagetype == vars.pagetype_workshop %} + const db_workshops = {{ db_workshops | tojson }}; + {# Only allow options, which are available for the selected systems #} + let allowedSystems = db_workshops[rowId]?.user_options?.system ?? false; + if ( allowedSystems ) { + if ( !Array.isArray(allowedSystems) ){ + allowedSystems = [allowedSystems]; + } + let allowedOptions = {}; + for ( const [key, valueInformation] of Object.entries(values) ) { + allowedSystems.forEach(system => { + const systemsPerOption = getServiceConfig(serviceId)?.options?.[key]?.allowedLists?.systems ?? []; + if ( systemsPerOption.includes(system) && !allowedOptions.hasOwnProperty(key) ) { + allowedOptions[key] = valueInformation; + } + }); + } + values = allowedOptions; + } + {%- endif %} + + const optionInput = $(`#${serviceId}-${rowId}-${tabId}-option-input`); + + fillSelect("init", optionInput, Object.entries(values).map(([key, value]) => [key, value.name]), {}, [], "N/A"); + } + {# <-- WorkshopManager.default.option #} + + {# WorkshopManager.default.system --> #} + function workshopManagerUpdateSystem(trigger, serviceId, rowId, tabId, elementId, elementOptions) { + const optionInput = $(`#${serviceId}-${rowId}-${tabId}-option-input`); + const systemInput = $(`#${serviceId}-${rowId}-${tabId}-system-input`); + + const options = val(optionInput); + + if ( optionInput.prop("disabled") ){ + // If option is disabled -> make all systems available + fillSelect(elementId, systemInput, allSystems.map(item => [item, item])); + } else { + // Update available systems + let inactiveText = "N/A" + let displayNames = []; + options.forEach(option => { + if (getServiceConfig(serviceId)?.options) { + displayNames.push(getServiceConfig(serviceId).options[option].name); + } + }) + let displayName = displayNames.join(", "); + inactiveText = `N/A for ${displayName}` + + fillSelect(elementId, systemInput, getAvailableSystemOptions(serviceId, options), {}, getMissingSystemOptions(serviceId, rowId, options), inactiveText); + } + } + + {# <-- WorkshopManager.default.system #} + + {# WorkshopManager.default.unicore.project --> #} + function workshopManagerUpdateProject(trigger, serviceId, rowId, tabId, elementId, elementOptions) { + const values = getProjectOptions(serviceId, rowId); + const inputElement = getInputElement(serviceId, rowId, "project"); + fillSelect(elementId, inputElement, values); + // inputElement.trigger("change"); + } + {# WorkshopManager.default.unicore.project --> #} + + {# WorkshopManager.default.unicore.partition --> #} + function workshopManagerUpdatePartition(trigger, serviceId, rowId, tabId, elementId, elementOptions) { + const [partitions, interactivePartitionsLength] = getPartitionAndInteractivePartition(serviceId, rowId); + const inputElement = getInputElement(serviceId, rowId, "partition"); + fillSelect(elementId, inputElement, partitions, {"Login Nodes": interactivePartitionsLength, "Compute Nodes": -1}); + // inputElement.trigger("change"); + } + {# <-- WorkshopManager.default.unicore.partition #} + + {# WorkshopManager.default.unicore.reservation --> #} + function toggleCollectCB(trigger, serviceId, rowId, tabId, elementId, elementOptions) { + const labelChecked = val(getLabelCBElement(serviceId, rowId, elementId)); + const inputDiv = getInputDiv(serviceId, rowId, elementId); + const inputElement = getInputElement(serviceId, rowId, elementId); + if ( !inputElement.is(":visible") ) { + inputElement.attr("data-collect", false); + } else { + if ( labelChecked ) { + inputElement.attr("data-collect", true); + } else { + inputElement.attr("data-collect", false); + } + } + } + function workshopManagerUpdateReservation(trigger, serviceId, rowId, tabId, elementId, elementOptions) { + + const systemInput = getInputElement(serviceId, rowId, "system"); + const elementDiv = getInputDiv(serviceId, rowId, elementId); + const reservationInput = getInputElement(serviceId, rowId, elementId); + + let reservations = getReservationOptions(serviceId, rowId); + + {%- if pagetype == vars.pagetype_workshop and db_workshops %} + const db_workshops = {{ db_workshops | tojson }}; + {# Only allow options, which are available for the selected systems #} + let allowedReservations = db_workshops[rowId]?.user_options?.reservation ?? false; + if ( allowedReservations ) { + if ( !Array.isArray(allowedReservations) ){ + allowedReservations = [allowedReservations]; + } + let forcedreservations = []; + const currentSystem = val(getInputElement(serviceId, rowId, "system")); + if ( currentSystem.length === 1 && currentSystem[0] && unicoreReservations.hasOwnProperty(currentSystem[0]) ) { + allowedReservations.forEach(singleWorkshopReservation => { + let singleWorkshopToAdd = unicoreReservations[currentSystem[0]].filter(item => item.ReservationName == singleWorkshopReservation); + if ( singleWorkshopToAdd.length === 1 ) { + forcedreservations.push(singleWorkshopToAdd[0]); + } + }); + } + reservations = forcedreservations; + reservationInput.attr("data-collect", true); + } + {%- endif %} + if ( !systemInput.prop("disabled") && reservations.length > 0 ) { + + activeReservationNames = reservations + .filter(item => item.State === "ACTIVE") + .map(item => [item.ReservationName, item.ReservationName]); + inactiveReservationNames = reservations + .filter(item => item.State === "INACTIVE") + .map(item => [item.ReservationName, item.ReservationName]); + const allReservationsSorted = [ + ["None", "None"], + ...activeReservationNames, + ...inactiveReservationNames + ]; + + let groups = { + "No reservation": 1 + } + if ( activeReservationNames.length > 0 ) { + groups["Active"] = activeReservationNames.length; + } + if ( inactiveReservationNames.length > 0 ) { + groups["Inactive"] = inactiveReservationNames.length; + } + fillSelect(elementId, reservationInput, allReservationsSorted, groups); + const labelCB = getLabelCBElement(serviceId, rowId, "reservation"); + if ( labelCB.length ) { + if ( labelCB.prop("checked") ) { + reservationInput.attr("data-collect", true); + } else { + reservationInput.attr("data-collect", false); + } + } else { + reservationInput.attr("data-collect", true); + } + if ( val(reservationInput)[0] == "None" ) { + reservationInput.attr("data-collect", false); + } + elementDiv.show(); + } else { + reservationInput.attr("data-collect", false); + elementDiv.hide(); + $(`div[id^='${serviceId}-${rowId}-'][id$='-reservationinfo-input-div']`).hide(); + } + } + + function defaultValue(trigger, serviceId, rowId, tabId, elementId, elementOptions) { + const dependentElement = elementOptions?.input?.options?.parent || ""; + if ( !dependentElement ) { + console.log(`Custom Config not configured correctly for ${elementId}. Add "parent" to elementOptions.`); + } + + const selectedParentValues = $(`select[id^='${serviceId}-${rowId}-'][id$='-${dependentElement}-input']`).val(); + const parentLabelCB = $(`input[id^='${serviceId}-${rowId}-'][id$='-${dependentElement}-input-cb']`); + const inputParentElement = $(`select[id^='${serviceId}-${rowId}-'][id$='-${dependentElement}-input']`); + + const inputElement = $(`select[id^='${serviceId}-${rowId}-'][id$='-${elementId}-input']`); + const labelCB = $(`input[id^='${serviceId}-${rowId}-'][id$='-${elementId}-input-cb']`); + const inputDiv = $(`div[id^='${serviceId}-${rowId}-'][id$='-${elementId}-input-div']`); + + if ( parentLabelCB.prop("checked") && inputParentElement.attr("data-collect") && selectedParentValues.length > 1 ) { + fillSelect(elementId, inputElement, selectedParentValues.map(item => [item, item])); + inputDiv.show(); + if ( labelCB.prop("checked") && inputParentElement.attr("data-collect") === "true") { + inputElement.attr("data-collect", true); + } else { + inputElement.attr("data-collect", false); + } + } else { + inputDiv.hide(); + inputElement.attr("data-collect", false); + } + } + + function updateReservationInfo(trigger, serviceId, rowId, tabId, elementId, elementOptions) { + const reservation_values = val(getInputElement(serviceId, rowId, "reservation")); + let reservation = "None"; + if ( reservation_values.length > 0 ){ + reservation = reservation_values[0]; + } + const reservationInfoDiv = $(`[id^='${serviceId}-${rowId}-'][id$='-reservationinfo-input-div']`); + if ( !reservation || reservation === "None" ) { + reservationInfoDiv.hide(); + } else { + const currentSystem = val(getInputElement(serviceId, rowId, "system")); + if ( currentSystem.length === 1 && currentSystem[0] && unicoreReservations.hasOwnProperty(currentSystem[0])) { + const reservations = unicoreReservations[currentSystem[0]].filter(item => item.ReservationName == reservation); + for (const reservationInfo of reservations) { + if (reservationInfo.ReservationName == reservation) { + reservationInfoDiv.find(`span[id$="-start"]`).html(`${reservationInfo.StartTime} (Europe/Berlin)`); + reservationInfoDiv.find(`span[id$="-end"]`).html(`${reservationInfo.EndTime} (Europe/Berlin)`); + reservationInfoDiv.find(`span[id$="-state"]`).html(reservationInfo.State); + reservationInfoDiv.find(`pre[id$="-details"]`).html(JSON.stringify(reservationInfo, null, 2)); + } + } + reservationInfoDiv.show(); + } else { + reservationInfoDiv.hide(); + } + } + } + {# <-- WorkshopManager.default.unicore.reservation #} + + + {# WorkshopManager.default.unicore.nodesRuntimeGPUXservers --> #} + function workshopManagerUpdateResourcesElementTrigger(trigger, serviceId, rowId, tabId, elementId, elementOptions) { + const systems = getUnicoreValues(serviceId, rowId, "system"); + const partitions = getUnicoreValues(serviceId, rowId, "partition"); + let _partitions = []; + + const labelOptions = elementOptions.label ?? {}; + + const inputDiv = getInputDiv(serviceId, rowId, elementId); + const inputElement = getInputElement(serviceId, rowId, elementId); + const labelElement = getLabel(inputElement); + const labelElementCBValue = val(getLabelCBElement(serviceId, rowId, elementId)); + const invalidFeedback = getInvalidFeedback(inputDiv); + + let minmaxavail = false; + let min = -1; + let max = -1; + let label = (labelOptions.value === undefined || labelOptions.value === null) ? "No Label" : labelOptions.value; + let show = false; + let defaultValue = 1; + let collectInformation = true; + + if ( collectInformation ){ + systems.forEach(system => { + if (partitions.length === 1 && partitions[0] === "_all_") { + _partitions = Object.keys(resourcesConfig[system] ?? {}); + } else { + _partitions = partitions; + } + _partitions.forEach(partition => { + const elementOptions = resourcesConfig[system]?.[partition]?.[elementId] ?? {}; + if (Object.keys(elementOptions).length !== 0 ) { + show = true; + const minmax = elementOptions.minmax || false; + defaultValue = (elementOptions["default"] === undefined || elementOptions["default"] === null) ? defaultValue : elementOptions["default"]; + if ( minmax ) { + if ( !minmaxavail ) { + minmaxavail = true; + min = minmax[0]; + max = minmax[1]; + } else { + if ( minmax[0] < min ){ + min = minmax[0]; + } + if ( minmax[1] > max ){ + max = minmax[1]; + } + } + } + } + }); + }); + } + if ( show ) { + if ( !collectInformation ) { + label = `${label} [${defaultValue}]`; + invalidFeedback.html(`Value ${defaultValue} was chosen by workshop instructor.`) + inputElement.attr("min", min); + inputElement.attr("max", max); + } else if ( minmaxavail ){ + label = `${label} [${min}, ${max}]`; + invalidFeedback.html(`Please choose a number between ${min} and ${max}.`); + inputElement.attr("min", min); + inputElement.attr("max", max); + } else { + invalidFeedback.html("Please choose a valid number."); + inputElement.removeAttr("min"); + inputElement.removeAttr("max"); + } + if ( inputElement.attr("data-alwaysdisabled") != "true" ) { + inputElement.attr("value", defaultValue); + } + + labelElement.contents().filter(function () { + return this.nodeType === Node.TEXT_NODE; + }).first().replaceWith(label); + + // Checkbox logic + const checkBoxElement = labelElement.find("input[type='checkbox']"); + if ( checkBoxElement.length !== 0 ) { + const checkBoxDefault = labelOptions?.options?.default ?? false; + checkBoxElement.prop("checked", checkBoxDefault); + inputElement.prop("disabled", !checkBoxDefault); + } else { + if ( inputElement.attr("data-alwaysdisabled") != "true" ) { + inputElement.prop("disabled", false); + } + } + inputDiv.show(); + if ( labelElementCBValue !== undefined ) { + inputElement.attr("data-collect", labelElementCBValue); + } else { + inputElement.attr("data-collect", true); + } + {%- if pagetype == vars.pagetype_workshop %} + + const workshops = {{ db_workshops | tojson }} || {}; + const workshopValues = workshops?.[rowId]?.user_options; + if ( Object.keys(workshopValues).includes(elementId) ){ + console.log(`Yeah - ${elementId} is defined`); + } + {%- endif %} + } else { + inputDiv.hide(); + inputElement.attr("data-collect", false); + } + } + {# <-- WorkshopManager.default.unicore.nodesRuntimeGPUXservers #} + + + {# WorkshopManager.default.kube.flavor --> #} + function workshopManagerUpdateFlavor(trigger, serviceId, rowId, tabId, elementId, elementOptions) { + const systems = val(getInputElement(serviceId, rowId, "system")); + const systemTypes = getSystemTypes(serviceId, rowId); + if ( systemTypes.includes("kube") ){ + const selectInput = getInputElement(serviceId, rowId, "flavor"); + fillSelect(elementId, selectInput, getAvailableKubeFlavorsS(systems), {}, getUnavailableKubeFlavorsS(systems), "maximum reached"); + // selectInput.trigger("change"); + } + } + {# <-- WorkshopManager.default.kube.flavor #} + + + {# Workshop.labconfig.flavorinfo --> #} + function setFlavorInfo(serviceId, rowId, system, flavors={}) { + const inputDiv = $(`div[id^='${serviceId}-${rowId}-'][id$='-flavorinfo-input-div']`); + inputDiv.empty(); + if ( system ) { + let allFlavors = flavors; + if ( allFlavors != undefined ) { + allFlavors = kubeOutpostFlavors[system]; + } + if ( allFlavors ){ + for (const [_, description] of Object.entries(allFlavors) + .filter(([key, value]) => value.max != 0) + .sort(([, a], [, b]) => { + const weightA = a["weight"] || 99; + const weightB = b["weight"] || 99; + return weightA > weightB ? 1 : -1; + })) { + var current = description.current || 0; + var maxAllowed = description.max; + // Flavor not valid, so skip + if (maxAllowed == 0 || current < 0 || maxAllowed == null || current == null) continue; + + var bgColor = "bg-primary"; + // Infinite allowed + if (maxAllowed == -1) { + var progressTooltip = `${current} used`; + var maxAllowedLabel = '∞'; + if (current == 0) { + var currentWidth = 0; + var maxAllowedWidth = 100; + } + else { + var currentWidth = 20; + var maxAllowedWidth = 80; + } + } + else { + var progressTooltip = `${current} out of ${maxAllowed} used`; + var maxAllowedLabel = maxAllowed - current; + var currentWidth = current / maxAllowed * 100; + var maxAllowedWidth = maxAllowedLabel / maxAllowed * 100; + + if (maxAllowedLabel < 0) { + maxAllowedLabel = 0; + maxAllowedWidth = 0; + bgColor = "bg-danger"; + } + } + + var diagramHtml = ` + <div class="row align-items-center g-0 mt-4"> + <div class="col-4"> + <span>${description.display_name}</span> + <a class="lh-1 ms-3" style="padding-top: 1px;" + data-bs-toggle="tooltip" data-bs-placement="right" title="${description.description}"> + {{ svg.info_svg | safe }} + </a> + </div> + <div class="progress col ms-2 fw-bold" style="height: 20px;" + data-bs-toggle="tooltip" data-bs-placement="top" title="${progressTooltip}"> + <div class="progress-bar ${bgColor}" role="progressbar" style="width: ${currentWidth}%">${current}</div> + <div class="progress-bar bg-success" role="progressbar" style="width: ${maxAllowedWidth}%">${maxAllowedLabel}</div> + </div> + </div> + ` + inputDiv.append(diagramHtml); + } + } + } + + // The lab has a flavor configured or is a new lab, but we could not get any flavor information + {# + if (((window.userOptions[id] || {}).flavor || id == "new-jupyterlab") && $.isEmptyObject(systemFlavors)) { + var noFlavorsHtml = ` + <div class="row g-0 mt-3"> + <div class="col-4"></div> + <div class="col ms-2 fw-bold text-danger">No flavors could be fetched. Try logging out and back in to fix the issue.</div> + </div> + `; + $(`#${serviceId}-${rowId}-${tabId}-systemtype-kube-flavorinfo-info-div`).append(noFlavorsHtml); + } + #} + } + + function updateFlavorInfo(trigger, serviceId, rowId, tabId, elementId, elementOptions) { + const systems = val(getInputElement(serviceId, rowId, "system")); + const systemTypes = getSystemTypes(serviceId, rowId); + if ( systemTypes.includes("kube") && systems.length == 1 ){ + setFlavorInfo(serviceId, rowId, systems[0]); + // $(`[id^='${serviceId}-${rowId}-'][id$='-flavorinfo-info-div']`).show(); + } + } + {# <-- Workshop.labconfig.flavorinfo #} + + {# WorkshopManager.default.lmod.modules --> #} + function workshopManagerUpdateModuleWorkshop(trigger, serviceId, rowId, tabId, elementId, elementOptions) { + const optiontypes = getOptionTypes(serviceId, rowId); + if ( optiontypes.includes("lmod") ) { + const values = getModuleValues(serviceId, rowId, elementId, elementOptions.input.options.setName); + const elementSelect = $(`select[id^='${serviceId}-${rowId}-'][id$='-${elementId}-input']`); + fillSelect(elementId, elementSelect, values); + const activeValues = values.filter(item => item[2]).map(item => item[0]); + // elementSelect.val(activeValues).trigger("change"); + } + } + {# <-- WorkshopManager.default.lmod.modules #} + + {# WorkshopManager.default.repo2docker.repopathtype --> #} + function R2DgetRepoPathType(serviceId, rowId, tabId, elementId) { + return {{ custom_config.get("binderRepos", {}).get("notebookTypes", ["File", "URL"]) | tojson }}.map(item => [item, item]); + } + + function setR2DPathType(trigger, serviceId, rowId, tabId, elementId, elementOptions) { + const inputElement = getInputElement(serviceId, rowId, "repopathtype"); + fillSelect(elementId, inputElement, R2DgetRepoPathType()); + } + + function workshopManagerRepoPathType(trigger, serviceId, rowId, tabId, elementId, elementOptions) { + const repoPathChecked = val(getLabelCBElement(serviceId, rowId, "repopath")); + + const repoPathTypeDiv = getInputDiv(serviceId, rowId, "repopathtype"); + const repoPathTypeInput = getInputElement(serviceId, rowId, "repopathtype"); + const repoPathTypeLabel = getLabelCBElement(serviceId, rowId, "repopathtype"); + if ( !repoPathChecked ) { + repoPathTypeInput.prop("disabled", true); + repoPathTypeLabel.prop("checked", false); + repoPathTypeLabel.prop("disabled", true); + repoPathTypeDiv.hide(); + } else { + repoPathTypeDiv.show(); + repoPathTypeLabel.prop("disabled", false); + const repoPathTypeChecked = repoPathTypeLabel.prop("checked"); + repoPathTypeInput.prop("disabled", !repoPathTypeChecked); + } + } + {# <-- WorkshopManager.default.repo2docker.repopathtype #} + + {# WorkshopManager.default.repo2docker.repotype --> #} + function R2DgetRepoType() { + return {{ custom_config.get("binderRepos", {}).get("repos", ["GitHub"]) | tojson }}.map(item => [item, item]); + } + + function setR2DType(trigger, serviceId, rowId, tabId, elementId, elementOptions) { + const repo2dockerSelect = $(`[id^="${serviceId}-${rowId}-"][id$="-${elementId}-input"]`); + fillSelect(elementId, repo2dockerSelect, R2DgetRepoType()); + } + {# <-- WorkshopManager.default.repo2docker.repotype #} + + {# WorkshopManager.default.expertmode --> #} + function workshopManagerToggleExpertMode(trigger, serviceId, rowId, tabId, elementId, elementOptions) { + const optionInput = $(`[id^="${serviceId}-${rowId}-"][id$="-option-input"]`); + const systemInput = $(`[id^="${serviceId}-${rowId}-"][id$="-system-input"]`); + + if ( val(getInputElement(serviceId, rowId, elementId)) ){ + // if checked: set systems + options to multiple + [optionInput, systemInput].forEach(input => { + input.prop("size", 4); + input.prop("multiple", true); + }) + } else { + [optionInput, systemInput].forEach(input => { + input.prop("size", 1); + input.prop("multiple", false); + }) + } + } + {# <-- WorkshopManager.default.expertmode #} + + {# WorkshopManager.button.helper --> #} + + function showToast(message, type = "danger") { + const toast = $(` + <div class="toast align-items-center text-white bg-${type} border-0" role="alert" aria-live="assertive" style="opacity: 0.9 !important" aria-atomic="true"> + <div class="d-flex"> + <div class="toast-body"> + ${message} + </div> + <button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button> + </div> + </div> + `); + $("#toastContainer").append(toast); + const bsToast = new bootstrap.Toast(toast[0]); + bsToast.show(); + } + + function getAPIOptions() { + return { + dataType: null, + tryCount: 5, + error: function (jqXHR, textStatus, errorThrown) { + if (jqXHR.status == 503) { + this.tryCount--; + if (this.tryCount >= 0) { + $.ajax(this); + return; + } + return; + } + if (jqXHR.status == 403) { + return; + } + showToast("Request to Server failed. Try refreshing website"); + console.error("API Request failed:", textStatus, errorThrown); + } + } + } + + function getWorkshopManagerAPIUrl(serviceId, rowId, utils, base_url) { + if ( isFirstRow(rowId) ) { + const workshopId = $(`input[id^='${serviceId}-${rowId}-'][id$='-workshopid-input']`).val(); + if ( workshopId ) { + return utils.url_path_join("workshops", workshopId); + } else { + return "workshops"; + } + } else { + return utils.url_path_join("workshops", rowId); + } + } + {# <-- WorkshopManager.button.helper #} + + {# WorkshopManager.button.new --> #} + function workshopManagerButtonNewSave(serviceId, rowId, buttonId, button_options, user, api, base_url, utils, show_modal=false) { + const options = getAPIOptions(); + const form = $(`form[id^='${serviceId}-${rowId}-form']`); + const valid = validateForm(serviceId, rowId); + if ( !valid ) { + console.log(`Invalid Form for ${serviceId}-${rowId}`); + return; + } + let userOptions = collectSelectedOptions(serviceId, rowId, allCheckboxes=true); + let workshopData = collectWorkshopOptions(serviceId, rowId); + + options["data"] = JSON.stringify({ + ...userOptions, + ...workshopData + }); + options["success"] = function (resp) { + if ( show_modal ) { + workshopManagerShowModal(serviceId, rowId, resp); + } + }; + options["type"] = "POST"; + + api.api_request( + getWorkshopManagerAPIUrl(serviceId, rowId, utils, base_url), + options + ); + } + function workshopManagerButtonNew(serviceId, rowId, buttonId, button_options, user, api, base_url, utils) { + workshopManagerButtonNewSave(serviceId, rowId, buttonId, button_options, user, api, base_url, utils, true); + } + {# <-- WorkshopManager.button.new #} + + {# WorkshopManager.button.reset --> #} + function workshopManagerButtonReset(serviceId, rowId, buttonId, button_options, user, api, base_url, utils) { + const options = getAPIOptions(); + options["type"] = "GET"; + options["success"] = function (resp) { + workshopManagerFillExistingRow(serviceId, rowId, resp); + } + api.api_request( + getWorkshopManagerAPIUrl(serviceId, rowId, utils, base_url), + options + ); + } + {# <-- WorkshopManager.button.reset #} + + {# WorkshopManager.button.share --> #} + function workshopManagerShowLink(serviceId, rowId, tabId, buttonId, button_options, user, api, base_url, utils) { + workshopManagerShowModal($this.attr("data-service"), $this.attr("data-row"), rowId); + } + {# <-- WorkshopManager.button.share #} + + {# WorkshopManager.button.delete --> #} + function workshopManagerButtonDelete(serviceId, rowId, buttonId, button_options, user, api, base_url, utils) { + const options = getAPIOptions(); + options["type"] = "DELETE"; + options["success"] = function () { + $(`tr[data-server-id=${serviceId}-${rowId}]`).each(function () { + $(this).remove(); + }); + console.log(`Delete of ${serviceId}-${rowId} successful`); + }; + api.api_request( + getWorkshopManagerAPIUrl(serviceId, rowId, utils, base_url), + options + ); + } + {# <-- WorkshopManager.button.delete #} + + {# WorkshopManager.button.stop --> #} + function updateHeaderButtons(serviceId, rowId, status) { + // status: ["running", "starting", "na", "stopping", "cancelling", "stopped", "waiting"] + let toShow = []; + let toDisable = []; + if ( status == "running" ) { + toShow = ["open", "stop"]; + } else if ( status == "waiting" ) { + toShow = ["open", "stop"]; + toDisable = ["open"]; + } else if ( status == "starting" ) { + toShow = ["cancel"]; + } else if ( status == "na" ) { + toShow = ["na", "del"]; + toDisable = ["na"]; + } else if ( status == "stopping" ) { + toShow = ["open", "stop"]; + toDisable = ["open", "stop"]; + } else if ( status == "cancelling" ) { + toShow = ["cancel"]; + toDisable = ["cancel"]; + } else if ( status == "stopped" ) { + toShow = ["start"]; + toDisable = []; + } else if ( status == "disable" ) { + toDisable = ["open", "stop", "cancel", "start", "del"]; + } + const baseSelector = `button[id^="${serviceId}-${rowId}"][id$="-btn-header"]`; + + // Enable buttons + const toDisableExcludeSelector = toDisable + .map(item => `:not([id$="-${item}-btn-header"])`) + .join(""); + $(`${baseSelector}${toDisableExcludeSelector}`).prop("disabled", false); + + // Disable buttons + toDisable.forEach(item => { + $(`button[id^="${serviceId}-${rowId}"][id$="-${item}-btn-header"]`).prop("disabled", true); + }); + + if ( status != "disable" ) { + // Hide buttons + const toShowExcludeSelector = toShow + .map(item => `:not([id$="-${item}-btn-header"])`) + .join(""); + $(`${baseSelector}${toShowExcludeSelector}`).hide(); + + // Show buttons + toShow.forEach(item => { + $(`button[id^="${serviceId}-${rowId}"][id$="-${item}-btn-header"]`).show(); + }); + } + } + + function getCurrentTimestamp() { + const now = new Date(); + + const berlinTime = new Date( + now.toLocaleString('en-US', { timeZone: 'Europe/Berlin' }) + ); + + const year = berlinTime.getFullYear(); + const month = String(berlinTime.getMonth() + 1).padStart(2, '0'); + const day = String(berlinTime.getDate()).padStart(2, '0'); + const hours = String(berlinTime.getHours()).padStart(2, '0'); + const minutes = String(berlinTime.getMinutes()).padStart(2, '0'); + const seconds = String(berlinTime.getSeconds()).padStart(2, '0'); + const milliseconds = String(now.getMilliseconds()).padStart(3, '0'); + + return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}.${milliseconds}`; + } + + function getStopEvent(buttonId) { + const event = { + "progress": 100, + "failed": true, + "ready": false, + "html_message": `<details><summary>${getCurrentTimestamp()}: Start cancelled by user.</summary>${buttonId} button was triggered.</details>` + } + return event; + } + + function workshopButtonStop(serviceId, rowId, buttonId, button_options, user, api, base_url, utils) { + const options = getAPIOptions(); + options["success"] = function (data, textStatus, jqXHR) { + updateHeaderButtons(serviceId, rowId, "stopped"); + progressBarUpdate(serviceId, rowId, "", 0); + appendToLog(serviceId, rowId, getStopEvent(buttonId)); + } + updateHeaderButtons(serviceId, rowId, "stopping"); + progressBarUpdate(serviceId, rowId, "stopping", 100); + api.stop_named_server(user, rowId, options); + } + {# <-- WorkshopManager.button.stop #} + {# WorkshopManager.button.cancel --> #} + function workshopButtonCancel(serviceId, rowId, buttonId, button_options, user, api, base_url, utils) { + const options = getAPIOptions(); + options["success"] = function (data, textStatus, jqXHR) { + console.log("Stopped"); + console.log(serviceId); + console.log(rowId); + updateHeaderButtons(serviceId, rowId, "stopped"); + progressBarUpdate(serviceId, rowId, "", 0); + appendToLog(serviceId, rowId, getStopEvent(buttonId)); + } + updateHeaderButtons(serviceId, rowId, "cancelling"); + progressBarUpdate(serviceId, rowId, "cancelling", 99); + api.cancel_named_server(user, rowId, options); + } + {# <-- WorkshopManager.button.cancel #} + + {# WorkshopManager.button.start --> #} + function workshopButtonStart(serviceId, rowId, buttonId, button_options, user, api, base_url, utils) { + homeButtonStart(serviceId, rowId, buttonId, button_options, user, api, base_url, utils); + } + {# <-- WorkshopManager.button.start #} + + {# WorkshopManager.button.open --> #} + async function checkAndOpenUrl(serviceId, rowId, url, retries = 50, delay = 500) { + // wait for 3 successful responses + let successCounter = 0; + for (let attempt = 1; attempt <= retries; attempt++) { + try { + const response = await fetch(`${url}/api`, { + method: 'GET', + mode: 'no-cors', + }); + if (response.ok || response.status == 405) { + successCounter += 1; + if ( successCounter > 8 ) { + window.open(url, "_blank"); + updateHeaderButtons(serviceId, rowId, "running"); + progressBarUpdate(serviceId, rowId, "running", 100); + $(`button[id^='${serviceId}-${rowId}-'][id$='-btn']`).prop("disabled", false); + return; + } + } + } catch (error) { + showToast(`Exception while sending request to ${url}.`, type="warning"); + console.error(`Attempt ${attempt}: Network error or invalid URL -`, error); + } + + if (attempt < retries) { + // Wait for the specified delay before retrying + await new Promise(resolve => setTimeout(resolve, delay)); + } else { + updateHeaderButtons(serviceId, rowId, "running"); + progressBarUpdate(serviceId, rowId, "running", 100); + showToast(`Cannot connect to started Server. Try to open manually. If this does not work try restarting the Server.`); + console.error("Maximum retries reached. Unable to access the website."); + } + } + } + + function homeOpen(serviceId, rowId, buttonId, button_options, user, api, base_url, utils) { + window.open(`/user/{{ user.name}}/${rowId}`, "_blank"); + } + + function workshopButtonOpen(serviceId, rowId, buttonId, button_options, user, api, base_url, utils) { + window.open("{{ url }}", "_blank"); + } + {# <-- WorkshopManager.button.open #} + + + {# WorkshopManager.button.save --> #} + function workshopManagerButtonSave(serviceId, rowId, buttonId, button_options, user, api, base_url, utils) { + workshopManagerButtonNewSave(serviceId, rowId, buttonId, button_options, user, api, base_url, utils, false); + } + {# <-- WorkshopManager.button.save #} + + {# <-- Workshop Manager #} + + {# Workshop --> #} + {# Workshop.labconfig.custom.username --> #} + function toggleExternalCB(trigger, serviceId, rowId, tabId, elementId, elementOptions) { + const labelChecked = val(getLabelCBElement(serviceId, rowId, trigger)); + const inputDiv = getInputDiv(serviceId, rowId, elementId); + const inputElement = getInputElement(serviceId, rowId, elementId); + if ( labelChecked ) { + inputDiv.show(); + inputElement.attr("data-collect", true); + } else { + inputDiv.hide(); + inputElement.attr("data-collect", false); + } + } + {# <-- Workshop.labconfig.custom.username #} + + {# Workshop.labconfig.unicore.account --> #} + function workshopUpdateAccount(trigger, serviceId, rowId, tabId, elementId, elementOptions) { + const values = getAccountOptions(serviceId, rowId); + const inputElement = getInputElement(serviceId, rowId, "account"); + fillSelect(elementId, inputElement, values); + // inputElement.trigger("change"); + } + {# Workshop.labconfig.unicore.account --> #} + + {# Workshop.modules --> #} + function getModuleValues(serviceId, rowId, name, setName) { + const options = val(getInputElement(serviceId, rowId, "option")); + let values = []; + let keys = new Set(); + options.forEach(option => { + if (getServiceConfig(serviceId)?.options?.[option]?.[setName]) { + const nameSet = getServiceConfig(serviceId)?.options[option]?.[setName]; + Object.entries(userModulesConfig[name]) + .filter(([key, value]) => value.sets && value.sets.includes(nameSet)) + .forEach( ([key, value]) => { + if ( !keys.has(key) ) { + keys.add(key); + values.push([ + key, + value.displayName, + typeof value.default === 'object' && value.default !== null ? value.default.default : value.default, + value.href + ]); + } + }); + } + }); + return values; + } + + function updateMultipleCheckboxes(trigger, serviceId, rowId, tabId, elementId, elementOptions) { + $(`input[id^='${serviceId}-${rowId}-${tabId}-'][id$='-select-all']`).prop("checked", false); + $(`input[id^='${serviceId}-${rowId}-${tabId}-'][id$='-select-none']`).prop("checked", false); + + const containerDiv = $(`div[id^='${serviceId}-${rowId}-'][id$='${elementId}-checkboxes-div']`); + const inputDiv = $(`div[id^='${serviceId}-${rowId}-'][id$='${elementId}-input-div']`); + const values = getModuleValues(serviceId, rowId, elementId, elementOptions.options.setName); + + let workshopPreset = false; + let workshopPresetChecked = []; + const group = elementOptions.options.group || tabId; + const name = elementOptions.options.name || elementId; + {%- if pagetype == vars.pagetype_workshop and db_workshops %} + const workshops = {{ db_workshops | tojson }} || {}; + const workshopValues = workshops?.[rowId]?.user_options; + if ( Object.keys(workshopValues).includes(group) && Object.keys(workshopValues[group]).includes(name) ){ + workshopPreset = true; + const modules = workshopValues[group][name]; + if ( modules.length > 0 ) { + workshopPresetChecked = modules; + } + } + {%- endif %} + // Ensure the container exists + if (containerDiv.length > 0 && values.length > 0) { + const idPrefix = containerDiv.attr('id').replace(/-checkboxes-div$/, ""); + containerDiv.html(''); + values.forEach(function (item) { + let isChecked = ''; + let isDisabled = ''; + if ( workshopPreset ) { + if ( workshopPresetChecked.includes(item[0]) ){ + isChecked = 'checked'; + } + isDisabled = 'disabled="true"'; + } else { + isChecked = item[2] ? 'checked' : ''; + } + let dependencies = ''; + if ( elementOptions.dependency ){ + for (const [specificKey, specificValues] of Object.entries(elementOptions.dependency)) { + dependencies += ` data-dependency-${specificKey}="true"`; + specificValues.forEach(specificValue => { + dependencies += ` data-dependency-${specificKey}-${specificValue}="true"`; + }); + } + } + + // Create the new div block + const newDiv = $(` + <div id="${idPrefix}-${item[0]}-input-div" class="form-check col-sm-6 col-md-4 col-lg-3"> + <input type="checkbox" name="${item[0]}" data-collect="true" ${dependencies} + data-checked="${isChecked}" data-group="${group}" data-element="${item[0]}" data-type="checkbox" data-row="${rowId}" data-tab="${tabId}" class="form-check-input" id="${idPrefix}-${item[0]}-input" value="${item[0]}" ${isChecked} ${isDisabled}/> + <label class="form-check-label" for="${idPrefix}-${item[0]}-input"> + <span class="align-middle">${item[1]}</span> + <a href="${item[3]}" target="_blank" class="module-info text-muted ms-3"> + <span>{{ svg.info_svg | safe }}</span> + <div class="module-info-link-div d-inline-block"> + <span class="module-info-link" id="nbdev-info-link"> {{ svg.link_svg | safe }}</span> + </div> + </a> + </label> + </div> + `); + // Append the new div to the container + containerDiv.append(newDiv); + // Add toggle function to each checkbox + $(`#${idPrefix}-${item[0]}-input`).on("click", function (event) { + $(`input[id^='${serviceId}-${rowId}-'][id$='-select-all']`).prop("checked", false); + $(`input[id^='${serviceId}-${rowId}-'][id$='-select-none']`).prop("checked", false); + }); + }); + } + inputDiv.show(); + } + {# <-- Workshop.modules #} + {# Workshop.navbar.resources --> #} + function resourceButton(trigger, serviceId, rowId) { + const systems = getUnicoreValues(serviceId, rowId, "system"); + const partitions = getUnicoreValues(serviceId, rowId, "partition"); + let showResources = false; + systems.forEach( (system) => { + if ( !showResources ) { + partitions.forEach( (partition) => { + if ( !showResources && (Object.keys(resourcesConfig[system])).includes(partition) ) { + if ( !(systemConfig[system]?.interactivePartitions || []).includes(partition) ) { + showResources = true; + } + } + }); + } + }); + if ( showResources ) { + $(`button[id^="${serviceId}-${rowId}-${trigger}-navbar-button"]`).trigger("show"); + } else { + $(`button[id^="${serviceId}-${rowId}-${trigger}-navbar-button"]`).trigger("hide"); + } + } + {# <-- Workshop.navbar.resources #} + + {# Workshop.labconfig.name --> #} + {%- if pagetype == vars.pagetype_workshop %} + function workshopLabName(trigger, serviceId, rowId, tabId, elementId, elementOptions) { + const inputName = getInputElement(serviceId, rowId, elementId); + const user_options = {{ spawner.user_options | tojson }} || {}; + const displayName = user_options.name || "Workshop {{ workshop_id }}"; + inputName.val(displayName); + } + {%- endif %} + {# <-- Workshop.labconfig.name #} + + {# Workshop.logs.logcontainer --> #} + + function fillLogContainer(serviceId, rowId, events) { + clearLogs(serviceId, rowId); + events.forEach(event => { + appendToLog(serviceId, rowId, event); + }) + } + + function clearLogs(serviceId, rowId) { + const logInputElement = $(`[id^='${serviceId}-${rowId}-logs'][id$='-logcontainer-input']`); + logInputElement.html(""); + } + + function defaultLogs(serviceId, rowId) { + const logInputElement = $(`[id^='${serviceId}-${rowId}-logs'][id$='-logcontainer-input']`); + logInputElement.html("Logs collected during the Start process will be shown here."); + } + + function appendToLog(serviceId, rowId, event) { + const logInputElement = $(`[id^='${serviceId}-${rowId}-logs'][id$='-logcontainer-input']`); + let htmlMsg = ""; + if (event.html_message !== undefined) { + htmlMsg = event.html_message + } else if (event.message !== undefined) { + htmlMsg = event.message; + } + if ( !htmlMsg && event.failed ) { + htmlMsg = "Server stopped"; + } + if ( htmlMsg ) { + try { + htmlMsg = htmlMsg.replace(/ /g, ' '); + } catch (e) { + console.log("Could not append Log Message"); + console.log(e); + return; + } + let exists = false; + const childCount = logInputElement.children().length; + logInputElement.children().each(function (i, e) { + let logMsg = $(e).html(); + if (htmlMsg == logMsg) exists = true; + }) + if (!exists) + logInputElement.append($(`<div id="${serviceId}-${rowId}-logs-logcontainer-element${childCount}" class="log-div">`).html(htmlMsg)); + let element = $(`#${serviceId}-${rowId}-logs-logcontainer-element${childCount}`); + + if ( event.progress === 100 && element.find("details") ) { + element.find("details").attr("open", true); + } + + } + } + {# <-- Workshop.logs.logcontainer #} + + {# Workshop.header.progressBar --> #} + function progressBarUpdate(serviceId, rowId, status, progress) { + const progressBarElement = $(`#${serviceId}-${rowId}-progress-bar`); + const progressTextElement = $(`#${serviceId}-${rowId}-progress-text`); + const progressTextInfoElement = $(`#${serviceId}-${rowId}-progress-info-text`); + let background = ""; + let text = ""; + let color = "black"; + if ( progress >= 60 ) { + color = "white"; + } + if ( status == "connecting" ) { + text = "connecting"; + background = "bg-success"; + } else if ( status == "running" ) { + text = "running"; + background = "bg-success"; + } else if ( status == "stopped" ) { + text = "stopped"; + background = "bg-danger"; + } else if ( status == "cancelling" ) { + text = "cancelling"; + background = "bg-danger"; + } else if ( status == "stopping" ) { + text = "stopping"; + background = "bg-danger"; + } else if ( status == "starting" ) { + text = "starting"; + } else if ( progress == 0 ){ + text = ""; + } + progressBarElement.width(progress).removeClass("bg-success bg-danger bg-primary").addClass(background).html(""); + progressTextElement.css('color', color); + progressTextElement.html(`${progress}%`); + progressTextInfoElement.html(text); + } + {# <-- Workshop.header.progressBar #} + + {# <-- Workshop #} + + {# Home --> #} + function _uuidv4hex() { + return ([1e7, 1e3, 4e3, 8e3, 1e11].join('')).replace(/[018]/g, c => + (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)); + } + function _uuidWithLetterStart() { + let uuid = _uuidv4hex(); + let char = Math.random().toString(36).match(/[a-zA-Z]/)[0]; + return char + uuid.substring(1); + } + + function homeButtonNew(serviceId, rowId, buttonId, button_options, user, api, base_url, utils) { + const newId = _uuidWithLetterStart(); + const options = getAPIOptions(); + + const form = $(`form[id^='${serviceId}-${rowId}-form']`); + const valid = validateForm(serviceId, rowId); + if ( !valid ) { + console.log(`Invalid Form for ${serviceId}-${rowId}`); + return; + } + + let userOptions = collectSelectedOptions(serviceId, rowId); + options["data"] = JSON.stringify(userOptions); + + options["success"] = function (data, textStatus, jqXHR) { + updateHeaderButtons(serviceId, rowId, "starting"); + + const url = new URL(window.location.href); + url.searchParams.set('service', serviceId); + url.searchParams.set('row', newId); + url.searchParams.set('showlogs', true); + window.location.href = url.toString(); + } + api.start_named_server(user, newId, options); + } + + function homeButtonStart(serviceId, rowId, buttonId, button_options, user, api, base_url, utils) { + const options = getAPIOptions(); + + const form = $(`form[id^='${serviceId}-${rowId}-form']`); + const valid = validateForm(serviceId, rowId); + if ( !valid ) { + console.log(`Invalid Form for ${serviceId}-${rowId}`); + return; + } + + let userOptions = collectSelectedOptions(serviceId, rowId); + options["data"] = JSON.stringify(userOptions); + // Ensure SSE is connected to receive all status updates + // sseInit(); + clearLogs(serviceId, rowId); + + options["success"] = function (data, textStatus, jqXHR) { + updateHeaderButtons(serviceId, rowId, "starting"); + } + api.start_named_server(user, rowId, options); + + const toView = document.getElementById(`${serviceId}-${rowId}-summary-tr`) + if ( toView ) toView.scrollIntoView(); + + // show summary-tr + const summaryTr = $(`tr[id^='${serviceId}-${rowId}-summary-tr']`); + const accordionIcon = summaryTr.find(".accordion-icon"); + const collapse = $(`.collapse[id^='${serviceId}-${rowId}-collapse']`); + const shown = collapse.hasClass("show"); + if ( ! shown ) { + accordionIcon.removeClass("collapsed"); + new bootstrap.Collapse(collapse); + } + + const navbarLogsButton = $(`[id^='${serviceId}-${rowId}-'][id$='-logs-navbar-button']`); + if ( navbarLogsButton ) { + navbarLogsButton.trigger("click"); + } + } + + function homeButtonDelete(serviceId, rowId, buttonId, button_options, user, api, base_url, utils) { + updateHeaderButtons(serviceId, rowId, "disable"); + const options = getAPIOptions(); + options["success"] = function () { + $(`tr[data-server-id='${serviceId}-${rowId}']`).each(function () { + $(this).remove(); + }); + console.log(`Delete of ${serviceId}-${rowId} successful`); + } + api.delete_named_server(user, rowId, options); + } + {# <-- Home #} + +</script> diff --git a/templates/macros/table/variables.jinja b/templates/macros/table/variables.jinja new file mode 100644 index 0000000000000000000000000000000000000000..fc476dff68d14176dd4b25cc497a95565efa0d88 --- /dev/null +++ b/templates/macros/table/variables.jinja @@ -0,0 +1,5 @@ +{%- set first_row_id = "__new__" %} +{%- set pagetype_workshop = "workshop" %} +{%- set pagetype_workshopmanager = "workshopmanager" %} +{%- set pagetype_home = "home" %} +{%- set pagetype_share = "share" %} \ No newline at end of file diff --git a/templates/page.html b/templates/page.html index 93eb3a0a92063feada054a16b91a7035a0153b3d..6a3a4121567cf0799ac48032e8f6206208692931 100644 --- a/templates/page.html +++ b/templates/page.html @@ -36,7 +36,36 @@ {% block scripts -%} {%- endblock %} <script> - var evtSourcesGlobal = {}; + var evtSource = undefined; + var testCounter = 0; + + function sseInit() { + let sseUrl = `${jhdata.base_url}api/sse` + if ( jhdata.user ) { + sseUrl = `${jhdata.base_url}api/sse/${jhdata.user}?_xsrf=${window.jhdata.xsrf_token}`; + } + if ( evtSource ) { + evtSource.close(); + } + evtSource = new EventSource(sseUrl); + evtSource.onmessage = (e) => { + try { + const jsonData = JSON.parse(event.data); + console.log(jsonData); + for (const [key, value] of Object.entries(jsonData)) { + console.log(`Trigger ${key}`); + $(`[data-sse-${key}]`).trigger("sse", value); + } + } catch (error) { + console.error("Failed to parse SSE data:", error); + } + }; + evtSource.onerror = (e) => { + console.log("Reconnect EventSource"); + // Reconnect + } + } + require.config({ {%- if version_hash -%} urlArgs: "v={{version_hash}}", @@ -123,11 +152,12 @@ {% block script -%} {%- endblock %} <script> + $(document).ready(function() { + sseInit(); + }); window.onbeforeunload = function() { - if (typeof evtSourcesGlobal !== 'undefined') { - for (const [key, value] of Object.entries(evtSourcesGlobal)) { - value.close(); - } + if (typeof evtSource !== 'undefined') { + evtSource.close(); } } </script> diff --git a/templates/spawn_pending.html b/templates/spawn_pending.html index 3d0bb64fa98ef4dd3ac24f76bf5a7c9819fa95c9..7dce663d497619a75987bd60a2338be8133d6ff3 100644 --- a/templates/spawn_pending.html +++ b/templates/spawn_pending.html @@ -1,310 +1,8 @@ {%- extends "page.html" -%} -{%- import "macros/svgs.jinja" as svg -%} -{%- block stylesheet -%} - <link rel="stylesheet" href='{{ static_url("css/home.css", include_version=True) }}' type="text/css"/> - <link rel="stylesheet" href='{{ static_url("css/spawn.css", include_version=True) }}' type="text/css"/> -{%- endblock -%} +{%- block meta -%} +{% set service = spawner.user_options.get("service", "jupyterlab") +<meta http-equiv="refresh" content="0; url=https://{{hostname}}{{ base_url }}home?service={{ service }}&row={{ spawner.name }}&showlogs" /> +{%- endblock %} -{%- macro create_text_input(label, value) -%} -{%- if value -%} -{%- set key = label.lower() %} -<div id="{{key}}-input-div" class="row mb-3"> - <label for="{{ key }}-input" class="col-4 col-form-label">{{ label }}</label> - <div class="col-8"> - <input type="text" class="form-control" id="{{ key }}-input" value="{{value}}" disabled> - </div> -</div> -{%- endif -%} -{%- endmacro -%} - -{%- macro create_number_input(label, value) -%} -{%- set key = label.lower() -%} -<div id="{{key}}-input-div" class="row mb-3"> - <label for="{{key}}-input" class="col-4 col-form-label">{{ label }}</label> - <div class="col-8"> - <input type="number" id="{{key}}-input" class="form-control" value="{{value}}" disabled> - </div> -</div> -{%- endmacro -%} - -{%- macro create_checkbox_input(label, checked) -%} -{%- set key = label.lower() -%} -<div id="{{key}}-input-div" class="row mb-3 align-items-center"> - <label for="{{key}}-input" class="col-4 col-form-label">{{ label }}</label> - <div class="col-8"> - <input type="checkbox" class="form-check-input" id="{{key}}-input" {%- if checked %} checked {%- endif %} disabled> - </div> -</div> -{%- endmacro -%} - - -{%- set user_options = spawner.user_options -%} -{%- set name = user_options.get("name") -%} -{%- if "profile" in user_options -%} -{%- set service = user_options.get("profile", "JupyterLab/3.6").split('/')[0] -%} -{%- set option = user_options.get("profile", "JupyterLab/3.6").split('/')[1] -%} -{%- else -%} -{%- set service = user_options.get("service", "JupyterLab/3.6").split('/')[0] -%} -{%- set option = user_options.get("service", "JupyterLab/3.6").split('/')[1] -%} -{%- endif -%} -{%- set service_name = custom_config.get("services").get(service).get("options").get(option).get("name") -%} -{%- set version = user_options.get("options", "JupyterLab") -%} -{%- set system = user_options.get("system", None) -%} -{%- set image = user_options.get("image", None) -%} -{%- set flavor = user_options.get("flavor", None) -%} -{%- set account = user_options.get("account", None) -%} -{%- set project = user_options.get("project", None) -%} -{%- set partition = user_options.get("partition", None) -%} -{%- set reservation = user_options.get("reservation", None) -%} -{%- set nodes = user_options.get("nodes", None) -%} -{%- set runtime = user_options.get("runtime", None) -%} -{%- set xserver = user_options.get("xserver", None) -%} -{%- set gpus = user_options.get("gpus", None) -%} -{%- set userModules = user_options.get("userModules", {}) -%} - -{#- Check if we should disable any tabs -#} -{%- set no_resources = True -%} -{%- if nodes != None or gpus != None or runtime != None or xserver != None -%} - {%- set no_resources = False -%} -{%- endif -%} - - -{%- block main -%} -<div class="container-fluid p-4"> - <h1>Your server is starting up...</h1> - <p>You will be redirected automatically when it's ready for you.</p> - - <div class="accordion" id="labInfoAccordion"> - <div class="accordion-item" style="border-bottom-right-radius: .25rem;border-bottom-left-radius: .25rem;"> - <h2 class="accordion-header" id="labInfo"> - <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#labInfoCollapse"> - Lab Info (click to expand) - </button> - </h2> - <div id="labInfoCollapse" class="accordion-collapse collapse" data-bs-parent="#labInfoAccordion"> - <div class="accordion-body text-black"> - <div class="d-flex align-items-start m-3"> - {#- TAB NAV PILLS -#} - {%- set nav_tab_margins = "mb-3" %} - <div class="nav flex-column nav-pills p-3 ps-0" id="tab" role="tablist"> - <button class="nav-link active {{ nav_tab_margins }}" id="config-info-tab" data-bs-toggle="pill" data-bs-target="#config-info" type="button" role="tab">Lab Config</button> - <button class="nav-link {{ nav_tab_margins }} {%- if no_resources %} disabled {%- endif %}" id="resources-info-tab" data-bs-toggle="pill" data-bs-target="#resources-info" type="button" role="tab" >Resources</button> - <button class="nav-link {{ nav_tab_margins }} {%- if not userModules %} disabled {%- endif %}" id="modules-info-tab" data-bs-toggle="pill" data-bs-target="#modules-info" type="button" role="tab" >Kernels and Extensions</span></button> - </div> - {#- TAB NAV CONTENT -#} - <div class="tab-content w-100" id="tabContent"> - <div class="tab-pane fade show active" id="config-info" role="tabpanel"> - {{ create_text_input("Name", name) }} - {{ create_text_input("Version", service_name) }} - {{ create_text_input("Image", image) }} - <hr> - {{ create_text_input("System", system) }} - {{ create_text_input("Flavor", flavor) }} - {{ create_text_input("Account", account) }} - {{ create_text_input("Project", project) }} - {{ create_text_input("Partition", partition) }} - {%- if reservation != None %} - <hr id="reservation-hr"> - {{ create_text_input("Reservation", reservation) }} - {%- endif %} - </div> - <div class="tab-pane fade" id="resources-info" role="tabpanel"> - {%- if not no_resources -%} - {%- if nodes -%} {{ create_number_input("Nodes", nodes) }} {%- endif %} - {%- if gpus -%} {{ create_number_input("GPUs", gpus) }} {%- endif %} - {%- if runtime -%} {{ create_number_input("Runtime", (runtime)|int) }} {%- endif %} - {%- set resources = auth_state.get("options_form", {}).get('resources', {}) -%} - {%- set xserver_options = resources.get(option, {}).get(system, {}).get(partition, {}).get("xserver", {}) -%} - {%- set show_checkbox = xserver_options.get("checkbox", False) -%} - {%- if show_checkbox -%} - {%- set cb_label = xserver_options.get("checkbox_label", "XServer") %} - {{ create_checkbox_input(cb_label, xserver) }} - {%- endif %} - {%- if xserver -%} - {%- set label = xserver_options.get("label", "XServer") %} - {{ create_number_input(label, xserver) }} - {%- endif %} - - {%- endif %} - </div> - <div class="tab-pane fade" id="modules-info" role="tabpanel"> - {%- if userModules -%} - {%- set module_sets = custom_config.get("userModules", {}) %} - {%- for set, modules in module_sets.items() -%} - {% set ns = namespace(first = true) -%} - {%- for module, module_info in modules.items() -%} - {%- if ns.first %} - <h4>{{ set | title}}</h4> - <div class="row g-0"> - {%- endif %} - <div class="form-check col-sm-6 col-md-4 col-lg-3"> - <input type="checkbox" class="form-check-input" id="{{ module }}-check" disabled {%- if module in userModules %} checked {%- endif %}> - <label class="form-check-label" for="{{ module }}-check"> - <span class="align-middle">{{ module_info['displayName'] }}</span> - <a href="{{ module_info['href'] }}" target="_blank" class="text-muted">{{ svg.info_svg | safe }}</a> - </label> - </input> - </div> - {%- set ns.first = false -%} - {%- endfor -%} - </div> - {%- endfor -%} - {%- endif %} - </div> - </div> {#- tab content #} - </div> {#- flex div #} - </div> {#- accordion body #} - </div> {#- accordion collapse #} - </div> {#- accordion item #} - </div> {#- accordion #} - - <div class="card mt-4"> - <div class="card-header d-flex"> - <div class="flex-grow-1"> - <div class="progress" style="height: 20px;"> - <div id="progress-bar" class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" style="width: 100%;"></div> - </div> - <div id="progress-info-text" class="text-center text-muted my-auto w-100" style="font-size: smaller;">spawning...</div> - </div> - <button id="cancel" type="button" class="btn btn-danger ms-4" disabled>{{ svg.stop_svg | safe }} Cancel </button> - <button id="retry" type="button" class="btn btn-primary ms-4" style="display: none;" disabled>{{ svg.retry_svg | safe }} Retry </button> - </div> - <div class="card-body text-black"> - <div id="log"></div> - </div> - </div> - -</div> -{%- endblock -%} - -{%- block script -%} -{%- set cancel_progress_refresh_rate = 1000 -%} -{%- set cancel_progress_activation = 0 -%} -{%- set cancel_progress_deactivation = 99 -%} - -<script> -require(["jquery", "jhapi", "home/utils"], function ( - $, - JHAPI, - utils -) { - var base_url = window.jhdata.base_url; - var user = window.jhdata.user; - var api = new JHAPI(base_url); - var timeout; - - // Cancel server spawn on click - $("#cancel").click(function (event) { - $("#cancel").attr("disabled", true); - api.cancel_named_server(user, "{{spawner.name}}"); - clearTimeout(timeout); - }); - - // Retry server spawn on click - $("#retry").click(function (event) { - $("#retry").attr("disabled", true); - - api.start_named_server(user, "{{spawner.name}}", { - data: JSON.stringify({{spawner.user_options | safe}}), - success: function () { - $("#retry").hide(); - $("#cancel").attr("disabled", true).show(); - $("#progress-bar").removeClass("bg-danger"); - $("#progress-info-text").html("spawning..."); - $("#log").html(""); - updateStatus(); - }, - error: function (xhr, textStatus, errorThrown) { - $("#progress-bar").addClass("bg-danger"); - $("#progress-info-text").html("last spawn failed"); - let details = $("<details>") - .append($("<summary>").html(`Could not request spawn. Error: ${xhr.status} ${errorThrown}`)) - .append($("<pre>").html(JSON.stringify(JSON.parse(xhr.responseText), null, 2))); - let div = $("<div>").addClass("log-div").html(details); - $("#log").html("").append(div); - $("#retry").removeAttr("disabled"); - } - }) - }); - - function updateStatus() { - let id = "{{ spawner.name }}"; - let progressUrl = `${window.jhdata.base_url}api/users/${window.jhdata.user}/servers/${id}/progress?_xsrf=${window.jhdata.xsrf_token}`; - let evtSource = new EventSource(progressUrl); - evtSource.onmessage = (e) => { - const evt = JSON.parse(e.data); - if (evt.progress !== undefined && evt.progress != 0) { - if (evt.progress == 100) { - evtSource.close(); - delete evtSource; - - if (evt.failed) { - // Update UI via spawnStatusChangedEvtSource so that - // it happens after the stop has finished in the backend - clearTimeout(timeout); - } - else { - $(`#progress-bar`).removeClass("bg-danger").addClass("bg-success"); - $("#progress-info-text").html("redirecting..."); - window.location.reload(); - } - } - else { - $("#progress-bar").html('<b>' + evt.progress + '%</b>'); - if (evt.progress >= {{ cancel_progress_activation }}) { - $("#cancel").removeAttr("disabled"); - } - if (evt.progress == {{ cancel_progress_deactivation }}) { - $("#cancel").attr("disabled", true); - $(`#progress-bar`).addClass("bg-danger"); - $("#progress-info-text").html("cancelling..."); - } - if (evt.progress == 95) { - // Refresh if stuck on 95% - timeout = setTimeout(() => window.location.reload(), 120000); - } - } - } - - if (evt.html_message !== undefined) { - var htmlMsg = evt.html_message - } else if (evt.message !== undefined) { - var htmlMsg = evt.message; - } - if (htmlMsg) { - try { htmlMsg = htmlMsg.replace(/ /g, ' '); } - catch (e) { return; } - // Only append if a log message has not been appended yet - var exists = false; - $("#log").children().each(function (i, e) { - let logMsg = $(e).html(); - if (htmlMsg == logMsg) exists = true; - }) - if (!exists) - $("#log").append($('<div class="log-div">').html(htmlMsg)); - } - } - } - - $( document ).ready(function() { - updateStatus(); - - let userSpawnerNotificationUrl = `${jhdata.base_url}api/users/${jhdata.user}/notifications/spawners?_xsrf=${window.jhdata.xsrf_token}`; - evtSourcesGlobal["pending"] = new EventSource(userSpawnerNotificationUrl); - evtSourcesGlobal["pending"].onmessage = (e) => { - const data = JSON.parse(e.data); - utils.updateNumberOfUsers(); - for (const id of data.stopped || []) { - if (id == "{{spawner.name}}") { - $(`#progress-bar`).html("").addClass("bg-danger"); - $("#progress-info-text").html("last spawn failed"); - $("#retry").removeAttr("disabled").show(); - $("#cancel").hide(); - } - } - } - }); -}); -</script> -{%- endblock -%} +{%- block title -%}Jupyter-JSC redirect{%- endblock %} diff --git a/templates/spawn_pending_prev.html b/templates/spawn_pending_prev.html new file mode 100644 index 0000000000000000000000000000000000000000..3d0bb64fa98ef4dd3ac24f76bf5a7c9819fa95c9 --- /dev/null +++ b/templates/spawn_pending_prev.html @@ -0,0 +1,310 @@ +{%- extends "page.html" -%} +{%- import "macros/svgs.jinja" as svg -%} + +{%- block stylesheet -%} + <link rel="stylesheet" href='{{ static_url("css/home.css", include_version=True) }}' type="text/css"/> + <link rel="stylesheet" href='{{ static_url("css/spawn.css", include_version=True) }}' type="text/css"/> +{%- endblock -%} + +{%- macro create_text_input(label, value) -%} +{%- if value -%} +{%- set key = label.lower() %} +<div id="{{key}}-input-div" class="row mb-3"> + <label for="{{ key }}-input" class="col-4 col-form-label">{{ label }}</label> + <div class="col-8"> + <input type="text" class="form-control" id="{{ key }}-input" value="{{value}}" disabled> + </div> +</div> +{%- endif -%} +{%- endmacro -%} + +{%- macro create_number_input(label, value) -%} +{%- set key = label.lower() -%} +<div id="{{key}}-input-div" class="row mb-3"> + <label for="{{key}}-input" class="col-4 col-form-label">{{ label }}</label> + <div class="col-8"> + <input type="number" id="{{key}}-input" class="form-control" value="{{value}}" disabled> + </div> +</div> +{%- endmacro -%} + +{%- macro create_checkbox_input(label, checked) -%} +{%- set key = label.lower() -%} +<div id="{{key}}-input-div" class="row mb-3 align-items-center"> + <label for="{{key}}-input" class="col-4 col-form-label">{{ label }}</label> + <div class="col-8"> + <input type="checkbox" class="form-check-input" id="{{key}}-input" {%- if checked %} checked {%- endif %} disabled> + </div> +</div> +{%- endmacro -%} + + +{%- set user_options = spawner.user_options -%} +{%- set name = user_options.get("name") -%} +{%- if "profile" in user_options -%} +{%- set service = user_options.get("profile", "JupyterLab/3.6").split('/')[0] -%} +{%- set option = user_options.get("profile", "JupyterLab/3.6").split('/')[1] -%} +{%- else -%} +{%- set service = user_options.get("service", "JupyterLab/3.6").split('/')[0] -%} +{%- set option = user_options.get("service", "JupyterLab/3.6").split('/')[1] -%} +{%- endif -%} +{%- set service_name = custom_config.get("services").get(service).get("options").get(option).get("name") -%} +{%- set version = user_options.get("options", "JupyterLab") -%} +{%- set system = user_options.get("system", None) -%} +{%- set image = user_options.get("image", None) -%} +{%- set flavor = user_options.get("flavor", None) -%} +{%- set account = user_options.get("account", None) -%} +{%- set project = user_options.get("project", None) -%} +{%- set partition = user_options.get("partition", None) -%} +{%- set reservation = user_options.get("reservation", None) -%} +{%- set nodes = user_options.get("nodes", None) -%} +{%- set runtime = user_options.get("runtime", None) -%} +{%- set xserver = user_options.get("xserver", None) -%} +{%- set gpus = user_options.get("gpus", None) -%} +{%- set userModules = user_options.get("userModules", {}) -%} + +{#- Check if we should disable any tabs -#} +{%- set no_resources = True -%} +{%- if nodes != None or gpus != None or runtime != None or xserver != None -%} + {%- set no_resources = False -%} +{%- endif -%} + + +{%- block main -%} +<div class="container-fluid p-4"> + <h1>Your server is starting up...</h1> + <p>You will be redirected automatically when it's ready for you.</p> + + <div class="accordion" id="labInfoAccordion"> + <div class="accordion-item" style="border-bottom-right-radius: .25rem;border-bottom-left-radius: .25rem;"> + <h2 class="accordion-header" id="labInfo"> + <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#labInfoCollapse"> + Lab Info (click to expand) + </button> + </h2> + <div id="labInfoCollapse" class="accordion-collapse collapse" data-bs-parent="#labInfoAccordion"> + <div class="accordion-body text-black"> + <div class="d-flex align-items-start m-3"> + {#- TAB NAV PILLS -#} + {%- set nav_tab_margins = "mb-3" %} + <div class="nav flex-column nav-pills p-3 ps-0" id="tab" role="tablist"> + <button class="nav-link active {{ nav_tab_margins }}" id="config-info-tab" data-bs-toggle="pill" data-bs-target="#config-info" type="button" role="tab">Lab Config</button> + <button class="nav-link {{ nav_tab_margins }} {%- if no_resources %} disabled {%- endif %}" id="resources-info-tab" data-bs-toggle="pill" data-bs-target="#resources-info" type="button" role="tab" >Resources</button> + <button class="nav-link {{ nav_tab_margins }} {%- if not userModules %} disabled {%- endif %}" id="modules-info-tab" data-bs-toggle="pill" data-bs-target="#modules-info" type="button" role="tab" >Kernels and Extensions</span></button> + </div> + {#- TAB NAV CONTENT -#} + <div class="tab-content w-100" id="tabContent"> + <div class="tab-pane fade show active" id="config-info" role="tabpanel"> + {{ create_text_input("Name", name) }} + {{ create_text_input("Version", service_name) }} + {{ create_text_input("Image", image) }} + <hr> + {{ create_text_input("System", system) }} + {{ create_text_input("Flavor", flavor) }} + {{ create_text_input("Account", account) }} + {{ create_text_input("Project", project) }} + {{ create_text_input("Partition", partition) }} + {%- if reservation != None %} + <hr id="reservation-hr"> + {{ create_text_input("Reservation", reservation) }} + {%- endif %} + </div> + <div class="tab-pane fade" id="resources-info" role="tabpanel"> + {%- if not no_resources -%} + {%- if nodes -%} {{ create_number_input("Nodes", nodes) }} {%- endif %} + {%- if gpus -%} {{ create_number_input("GPUs", gpus) }} {%- endif %} + {%- if runtime -%} {{ create_number_input("Runtime", (runtime)|int) }} {%- endif %} + {%- set resources = auth_state.get("options_form", {}).get('resources', {}) -%} + {%- set xserver_options = resources.get(option, {}).get(system, {}).get(partition, {}).get("xserver", {}) -%} + {%- set show_checkbox = xserver_options.get("checkbox", False) -%} + {%- if show_checkbox -%} + {%- set cb_label = xserver_options.get("checkbox_label", "XServer") %} + {{ create_checkbox_input(cb_label, xserver) }} + {%- endif %} + {%- if xserver -%} + {%- set label = xserver_options.get("label", "XServer") %} + {{ create_number_input(label, xserver) }} + {%- endif %} + + {%- endif %} + </div> + <div class="tab-pane fade" id="modules-info" role="tabpanel"> + {%- if userModules -%} + {%- set module_sets = custom_config.get("userModules", {}) %} + {%- for set, modules in module_sets.items() -%} + {% set ns = namespace(first = true) -%} + {%- for module, module_info in modules.items() -%} + {%- if ns.first %} + <h4>{{ set | title}}</h4> + <div class="row g-0"> + {%- endif %} + <div class="form-check col-sm-6 col-md-4 col-lg-3"> + <input type="checkbox" class="form-check-input" id="{{ module }}-check" disabled {%- if module in userModules %} checked {%- endif %}> + <label class="form-check-label" for="{{ module }}-check"> + <span class="align-middle">{{ module_info['displayName'] }}</span> + <a href="{{ module_info['href'] }}" target="_blank" class="text-muted">{{ svg.info_svg | safe }}</a> + </label> + </input> + </div> + {%- set ns.first = false -%} + {%- endfor -%} + </div> + {%- endfor -%} + {%- endif %} + </div> + </div> {#- tab content #} + </div> {#- flex div #} + </div> {#- accordion body #} + </div> {#- accordion collapse #} + </div> {#- accordion item #} + </div> {#- accordion #} + + <div class="card mt-4"> + <div class="card-header d-flex"> + <div class="flex-grow-1"> + <div class="progress" style="height: 20px;"> + <div id="progress-bar" class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" style="width: 100%;"></div> + </div> + <div id="progress-info-text" class="text-center text-muted my-auto w-100" style="font-size: smaller;">spawning...</div> + </div> + <button id="cancel" type="button" class="btn btn-danger ms-4" disabled>{{ svg.stop_svg | safe }} Cancel </button> + <button id="retry" type="button" class="btn btn-primary ms-4" style="display: none;" disabled>{{ svg.retry_svg | safe }} Retry </button> + </div> + <div class="card-body text-black"> + <div id="log"></div> + </div> + </div> + +</div> +{%- endblock -%} + +{%- block script -%} +{%- set cancel_progress_refresh_rate = 1000 -%} +{%- set cancel_progress_activation = 0 -%} +{%- set cancel_progress_deactivation = 99 -%} + +<script> +require(["jquery", "jhapi", "home/utils"], function ( + $, + JHAPI, + utils +) { + var base_url = window.jhdata.base_url; + var user = window.jhdata.user; + var api = new JHAPI(base_url); + var timeout; + + // Cancel server spawn on click + $("#cancel").click(function (event) { + $("#cancel").attr("disabled", true); + api.cancel_named_server(user, "{{spawner.name}}"); + clearTimeout(timeout); + }); + + // Retry server spawn on click + $("#retry").click(function (event) { + $("#retry").attr("disabled", true); + + api.start_named_server(user, "{{spawner.name}}", { + data: JSON.stringify({{spawner.user_options | safe}}), + success: function () { + $("#retry").hide(); + $("#cancel").attr("disabled", true).show(); + $("#progress-bar").removeClass("bg-danger"); + $("#progress-info-text").html("spawning..."); + $("#log").html(""); + updateStatus(); + }, + error: function (xhr, textStatus, errorThrown) { + $("#progress-bar").addClass("bg-danger"); + $("#progress-info-text").html("last spawn failed"); + let details = $("<details>") + .append($("<summary>").html(`Could not request spawn. Error: ${xhr.status} ${errorThrown}`)) + .append($("<pre>").html(JSON.stringify(JSON.parse(xhr.responseText), null, 2))); + let div = $("<div>").addClass("log-div").html(details); + $("#log").html("").append(div); + $("#retry").removeAttr("disabled"); + } + }) + }); + + function updateStatus() { + let id = "{{ spawner.name }}"; + let progressUrl = `${window.jhdata.base_url}api/users/${window.jhdata.user}/servers/${id}/progress?_xsrf=${window.jhdata.xsrf_token}`; + let evtSource = new EventSource(progressUrl); + evtSource.onmessage = (e) => { + const evt = JSON.parse(e.data); + if (evt.progress !== undefined && evt.progress != 0) { + if (evt.progress == 100) { + evtSource.close(); + delete evtSource; + + if (evt.failed) { + // Update UI via spawnStatusChangedEvtSource so that + // it happens after the stop has finished in the backend + clearTimeout(timeout); + } + else { + $(`#progress-bar`).removeClass("bg-danger").addClass("bg-success"); + $("#progress-info-text").html("redirecting..."); + window.location.reload(); + } + } + else { + $("#progress-bar").html('<b>' + evt.progress + '%</b>'); + if (evt.progress >= {{ cancel_progress_activation }}) { + $("#cancel").removeAttr("disabled"); + } + if (evt.progress == {{ cancel_progress_deactivation }}) { + $("#cancel").attr("disabled", true); + $(`#progress-bar`).addClass("bg-danger"); + $("#progress-info-text").html("cancelling..."); + } + if (evt.progress == 95) { + // Refresh if stuck on 95% + timeout = setTimeout(() => window.location.reload(), 120000); + } + } + } + + if (evt.html_message !== undefined) { + var htmlMsg = evt.html_message + } else if (evt.message !== undefined) { + var htmlMsg = evt.message; + } + if (htmlMsg) { + try { htmlMsg = htmlMsg.replace(/ /g, ' '); } + catch (e) { return; } + // Only append if a log message has not been appended yet + var exists = false; + $("#log").children().each(function (i, e) { + let logMsg = $(e).html(); + if (htmlMsg == logMsg) exists = true; + }) + if (!exists) + $("#log").append($('<div class="log-div">').html(htmlMsg)); + } + } + } + + $( document ).ready(function() { + updateStatus(); + + let userSpawnerNotificationUrl = `${jhdata.base_url}api/users/${jhdata.user}/notifications/spawners?_xsrf=${window.jhdata.xsrf_token}`; + evtSourcesGlobal["pending"] = new EventSource(userSpawnerNotificationUrl); + evtSourcesGlobal["pending"].onmessage = (e) => { + const data = JSON.parse(e.data); + utils.updateNumberOfUsers(); + for (const id of data.stopped || []) { + if (id == "{{spawner.name}}") { + $(`#progress-bar`).html("").addClass("bg-danger"); + $("#progress-info-text").html("last spawn failed"); + $("#retry").removeAttr("disabled").show(); + $("#cancel").hide(); + } + } + } + }); +}); +</script> +{%- endblock -%} diff --git a/templates/token.html b/templates/token.html index 736ab47e38c28b1a84c188f2dccb635077c3b94f..fcf01d1c48c74e1dbf2a1b6b174adc6d69113c24 100644 --- a/templates/token.html +++ b/templates/token.html @@ -1,3 +1,4 @@ + {%- extends "page.html" -%} diff --git a/templates/workshop.html b/templates/workshop.html new file mode 100644 index 0000000000000000000000000000000000000000..7ca70f18cbc4da0bd1f697d2b4a511dda6d127eb --- /dev/null +++ b/templates/workshop.html @@ -0,0 +1,44 @@ +{%- extends "page.html" -%} + +{%- block stylesheet -%} + <link rel="stylesheet" href='{{static_url("css/home.css")}}' type="text/css"/> +{%- endblock -%} + +{%- block main -%} +{%- import "macros/table/config/workshop.jinja" as config %} +{%- import "macros/svgs.jinja" as svg -%} +{%- import "macros/table/variables.jinja" as vars with context %} + +{%- set pagetype = vars.pagetype_workshop %} + +{%- set table_rows = db_workshops %} + +{%- from "macros/table/table.jinja" import tables with context %} +{%- import "macros/table/content.jinja" as functions with context %} + +<div id="workshopnotusable"></div> +{{ tables( + config.frontend_config, + functions.workshop_description, + functions.workshop_headerlayout, + false, + functions.workshop_firstheader, + functions.row_content, + { + "stop": "workshopButtonStop", + "start": "workshopButtonStart", + "open": "workshopButtonOpen", + "cancel": "workshopButtonCancel" + }, + functions.sse_functions +) }} + +{%- endblock -%} + + +{%- block script -%} +{%- import "macros/table/variables.jinja" as vars with context %} +{%- set pagetype = vars.pagetype_workshop %} +{%- import "macros/table/config/workshop.jinja" as config with context %} +{%- include "macros/table/elements_js.jinja" with context %} +{%- endblock %} diff --git a/templates/workshop_list.html b/templates/workshop_list.html new file mode 100644 index 0000000000000000000000000000000000000000..a6946a9558fe56983021b812ecc4cf554cdc8de1 --- /dev/null +++ b/templates/workshop_list.html @@ -0,0 +1,239 @@ +{%- extends "home.html" -%} +{%- import "macros/home.jinja" as home -%} +{%- import "macros/svgs.jinja" as svg -%} + + +{% block main %} + +{%- macro _create_button(workshop_id, key, btn_class, btn_svg, btn_text, type, align_right=false) %} + <button type="button" id="{{ workshop_id }}-{{ key }}-workshop-btn" class="btn btn-{{ key }}-workshop {{ btn_class }} {% if align_right -%} ms-auto {%- else -%} me-2 {%- endif %}">{{ btn_svg }} {{ btn_text }}</button> + <script> + {# + $(document).ready(function() { + #} + require(["jquery", "jhapi", "utils"], function ( + $, + JHAPI, + utils + ) { + "use strict"; + + var base_url = window.jhdata.base_url; + var api = new JHAPI(base_url); + + $("#{{ workshop_id }}-{{ key }}-workshop-btn").click(function() { + var options = { + } + {%- if key == "reset" %} + {{ fill_row(workshop_id, db_workshops[workshop_id]) }} + {%- elif key == "delete" %} + options["type"] = "DELETE"; + console.log("Delete of {{ workshop_id }} initiated"); + options["success"] = function () { + $("tr[data-server-id={{ workshop_id }}]").each(function () { + $(this).remove(); + }); + console.log("Delete of {{ workshop_id }} successful"); + }; + options["error"] = function (jqXHR, textStatus, errorThrown) { + + console.error("API Request failed:", textStatus, errorThrown); + }; + api.api_request( + utils.url_path_join("workshops", "{{ workshop_id }}"), + options + ) + {%- elif key in ["create", "save"] %} + var form = $("#{{ workshop_id }}-form"); + var valid = validateFormNow(form); + if ( !valid ) { + console.error("Form {{ workshop_id }} is not valid"); + return; + } + + options["type"] = "POST"; + var options_data = {}; + var keywords = {{ options | tojson }}; + var value_names; + var value_displayname; + var select_element; + var option_element; + Object.entries(keywords.select).forEach(function([key, value]) { + if ( $(`#{{ workshop_id }}-${key}-workshop-cb-input`).prop('checked') ) { + select_element = $(`#{{ workshop_id }}-${key}-workshop-select`); + options_data[key] = {}; + value_names = select_element.val(); + if (!Array.isArray(value_names)) { + value_names = [value_names]; + } + value_names.forEach(function(value_name) { + option_element = select_element.find(`option[value="${value_name}"]`); + value_displayname = option_element.html(); + options_data[key][value_name] = value_displayname; + }); + } + }); + keywords.input.forEach(function(key) { + if ( $(`#{{ workshop_id }}-${key}-workshop-cb-input`).prop('checked') ) { + options_data[key] = $(`#{{ workshop_id }}-${key}-workshop-input`).val(); + } + }); + var workshop_id = $("#{{ workshop_id }}-{{ keyname_id }}-workshop-input").val(); + options_data["{{ keyname_description }}"] = $("#{{ workshop_id }}-{{ keyname_description }}-workshop-input").val(); + options_data["{{ keyname_show_public }}"] = $("#{{ workshop_id }}-{{ keyname_show_public }}-workshop-cb-input").prop('checked'); + options_data["{{ keyname_enddate }}"] = $("#{{ workshop_id }}-{{ keyname_enddate }}-workshop-input").val(); + options["data"] = JSON.stringify(options_data); + options["success"] = function () { + form.submit(); + {%- if key == "create" %} + {%- endif %} + }; + options["error"] = function (jqXHR, textStatus, errorThrown) { + console.error("API Request failed:", textStatus, errorThrown); + }; + api.api_request( + utils.url_path_join("workshops", workshop_id), + options + ) + {%- endif %} + }); + }); + {# + }); + #} + </script> +{%- endmacro %} + +{%- macro create_buttons(workshop_id, new_workshop_row) %} + + <div class="d-flex"> + {%- if new_workshop_row %} + {{ _create_button(workshop_id, "create", "btn-primary", svg.plus_svg | safe, "Create") }} + {%- else %} + {{ create_modal(workshop_id) }} + {# Create JavaScript function for Share Button to update + show modal for this workshop_id #} + <script> + require(["jquery", "jhapi", "utils"], function ( + $, + JHAPI, + utils + ) { + "use strict"; + + var base_url = window.jhdata.base_url; + var api = new JHAPI(base_url); + {# Show Modal with workshop URL #} + {%- set sanitizedShareId = workshop_id.replace("-", "_") %} + function showShareDialogue{{ sanitizedShareId }}() { + $("#{{ workshop_id }}-share-workshop-copy-btn").click(function() { + const shareUrl = $("#{{ workshop_id }}-share-workshop-link .modal-body a").attr('href'); + navigator.clipboard.writeText(shareUrl).then(function() { + $("#{{ workshop_id }}-share-workshop-copy-btn").tooltip('dispose').attr('title', 'Copied'); + $("#{{ workshop_id }}-share-workshop-copy-btn").tooltip('show'); + }, function(err) { + console.error('Could not copy text: ', err); + }); + }); + + let workshop_url = utils.url_path_join(window.origin, base_url, "workshops", "{{ workshop_id }}").replace("//", "/"); + $("#{{ workshop_id }}-share-workshop-link .modal-title").text("Share Workshop {{ workshop_id }}"); + $("#{{ workshop_id }}-share-workshop-link .modal-body a").text(`${workshop_url}`); + + let shareableURL = new URL(workshop_url); + $("#{{ workshop_id }}-share-workshop-link .modal-body a").attr('href', shareableURL); + try { + shareableURL = new URL(workshop_url); + $("#{{ workshop_id }}-share-workshop-link .modal-body a").attr('href', shareableURL); + } catch (error) {console.log("no");} + + $("#{{ workshop_id }}-share-workshop-link").modal('show'); + } + $("#{{ workshop_id }}-share-workshop-btn").click(function (event) { + showShareDialogue{{ workshop_id }}(); + }); + }); + </script> + {# Share Button does not need the script logic from _create_button macro, therefore we create it in here #} + <button type="button" id="{{ workshop_id }}-share-workshop-btn" class="btn btn-share-workshop" data-toggle="modal" data-target="#{{ workshop_id }}-share-workshop-link">{{ svg.share_svg | safe }} Share </button> + + + {{ _create_button(workshop_id, "save", "btn-success", svg.save_svg | safe, "Save") }} + {{ _create_button(workshop_id, "reset", "btn-danger", svg.reset_svg | safe, "Reset") }} + {{ _create_button(workshop_id, "delete", "btn-danger", svg.delete_svg | safe, "Delete", align_right=true) }} + {%- endif %} + </div> +{%- endmacro %} + + + +<div class="container-fluid p-4"> + {#- TABLE #} + + <div class="table-responsive-md"> + <h2>Workshop Manager</h2> + <p>Select the options users might be able to use during your workshop.</p> + <p>Use shift or ctrl to select multiple items. <a style="color:#fff" href="https://jupyterjsc.pages.jsc.fz-juelich.de/docs/jupyterjsc/" target="_">Click here for more information.</a></p> + + <table id="jupyterlabs-table" class="table table-bordered table-striped table-hover table-light align-middle"> + {#- TABLE HEAD #} + <thead class="table-secondary"> + <tr> + <th scope="col" width="1%"></th> + <th scope="col" width="20%">Name</th> + <th scope="col">Configuration</th> + <th scope="col" class="text-center" width="10%">Link</th> + </tr> + </thead> + {#- TABLE BODY #} + + + + <tbody> + {# - List existing workshops #} + {%- for workshop_id, workshop_info in db_workshops.items() %} + + <!-- summary of row --> + <tr data-server-id="{{ workshop_id }}" class="summary-tr"> + <td class="details-td" data-bs-target="#{{ workshop_id }}-collapse"> + <div class="d-flex mx-4"> + </div> + </td> + + <th scope="row" class="name-td">{{ workshop_id }}</th> + <th scope="row" class="description-td">{{ workshop_info.user_options.description }}</th> + <th scope="row" class="url-td text-center"> + <button type="button" id="{{workshop_id}}-link-workshop-btn" class="btn btn-primary link-workshop-btn" data-toggle="modal" data-target="#{{ id }}-share-link" onclick="window.location.href='https://{{ hostname }}{{ base_url }}workshops/{{ workshop_id }}';">{{ svg.plus_svg | safe }} Join </button> + </th> + </tr> + {%- endfor %} + </tbody> + </table> + </div> {#- table responsive #} +</div> {#- container fluid #} + + +{%- endblock -%} + + +{%- block script -%} +<script> +require(["jquery", "jhapi", "utils"], function ( + $, + JHAPI, + utils +) { + "use strict"; + + var base_url = window.jhdata.base_url; + var api = new JHAPI(base_url); + + + /* + On page load + */ + $(document).ready(function() { + $("nav [id$=nav-item]").removeClass("active"); + }) +}) +</script> +{%- endblock %} \ No newline at end of file diff --git a/templates/workshop_manager.html b/templates/workshop_manager.html new file mode 100644 index 0000000000000000000000000000000000000000..dd68441b3c252daa8b907218c0ff398cfbbec9cb --- /dev/null +++ b/templates/workshop_manager.html @@ -0,0 +1,38 @@ +{%- extends "page.html" -%} + +{%- block stylesheet -%} + <link rel="stylesheet" href='{{static_url("css/home.css")}}' type="text/css"/> +{%- endblock -%} + +{%- block main -%} +{%- import "macros/table/config/workshop_manager.jinja" as config %} +{%- import "macros/svgs.jinja" as svg %} +{%- import "macros/table/variables.jinja" as vars with context %} + +{%- set pagetype = vars.pagetype_workshopmanager %} + +{%- set table_rows = {vars.first_row_id: {}} %} +{%- set _ = table_rows.update(db_workshops) %} + + +{%- from "macros/table/table.jinja" import tables with context %} +{%- import "macros/table/content.jinja" as functions with context %} + +{{ tables( + config.frontend_config, + functions.workshopmanager_description, + functions.workshopmanager_headerlayout, + functions.workshopmanager_defaultheader, + functions.workshopmanager_firstheader, + functions.workshopmanager_row_content +) }} + +{%- endblock -%} + + +{%- block script -%} +{%- import "macros/table/variables.jinja" as vars with context %} +{%- set pagetype = vars.pagetype_workshopmanager %} +{%- import "macros/table/config/workshop_manager.jinja" as config with context %} +{%- include "macros/table/elements_js.jinja" with context %} +{%- endblock %}