diff --git a/static/css/home.css b/static/css/home.css index 0c3c52851a9307059db012641f4c4c413356afd4..2550f1f6f6e7db794e8ef402c37f15c36c7f631b 100644 --- a/static/css/home.css +++ b/static/css/home.css @@ -1,8 +1,8 @@ tr.summary-tr { - height: 73px; - cursor: pointer; + height: 73px } + .new-spawner-tr { --bs-table-bg: #fff; --bs-table-striped-bg: #fff; diff --git a/static/js/home/dropdown-options.js b/static/js/home/dropdown-options.js index 39c18b4c864c1dca5c14d1bbe759cebe9473267f..e0c600e23622789bc13489dbd8dd7f0390e4b178 100644 --- a/static/js/home/dropdown-options.js +++ b/static/js/home/dropdown-options.js @@ -1,6 +1,3 @@ -/* -* 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 @@ -170,33 +167,13 @@ define(["jquery", "home/utils"], function ( } 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) { @@ -213,10 +190,12 @@ define(["jquery", "home/utils"], function ( } } select.attr("required", true); - _toggle_show_reservation(true); + $(`#${id}-reservation-select-div`).show(); + $(`#${id}-reservation-hr`).show(); } else { - _toggle_show_reservation(false); + $(`#${id}-reservation-select-div`).hide(); + $(`#${id}-reservation-hr`).hide(); } updateLabConfigSelect(select, value, currentVal); } diff --git a/static/js/home/handle-events.js b/static/js/home/handle-events.js index 60a8c238a8057b851bf35574b841d8c316e37428..1d114de14d0feb1714f91fa1f2a7227c586757ef 100644 --- a/static/js/home/handle-events.js +++ b/static/js/home/handle-events.js @@ -1,6 +1,4 @@ -/* -* Callbacks related to interacting with table rows -*/ +// Callbacks related to interacting with table rows require(["jquery", "home/utils", "home/dropdown-options"], function ( $, utils, @@ -65,26 +63,13 @@ require(["jquery", "home/utils", "home/dropdown-options"], function ( } }); - $("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_input(id, key, showInput, pattern) { + function _toggle_show_input(id, key, pattern) { + const showInput = $(`input#${id}-${key}-cb-input`)[0].checked; if (showInput) { $(`#${id}-${key}-input-div`).show(); $(`#${id}-${key}-input`).attr("required", true); @@ -112,45 +97,26 @@ require(["jquery", "home/utils", "home/dropdown-options"], function ( const userInputInfo = (serviceInfo.JupyterLab.options[values.service] || {}).userInput || {}; var customImageInput = $(`input#${id}-image-input`); var customMountInput = $(`#${id}-image-mount-input`); - var registryAuthsInputs = $(`#${id}-image-private-url-input, #${id}-image-private-user-input, #${id}-image-private-pass-input`); - - 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 allUserInputDivs = $(`#${id}-image-input-div, #${id}-image-mount-cb-input-div, #${id}-image-mount-input-div`) var inputRequired = userInputInfo.required || false; if (!inputRequired) { dropdowns.resetInputElement(customImageInput, false); dropdowns.resetInputElement(customMountInput, false); - dropdowns.resetInputElement(registryAuthsInputs, false); - - registryAuthsInputDivs.hide(); allUserInputDivs.hide(); } else { dropdowns.resetInputElement(customImageInput, true); - - // Set default values for the private docker registry authentications to false => fields are hidden and not required - $(`#${id}-image-private-cb-input`)[0].checked = false; - dropdowns.resetInputElement(registryAuthsInputs, false); allUserInputDivs.show(); - // Enable user data mount by default $(`#${id}-image-mount-cb-input`)[0].checked = userInputInfo.defaultMountEnabled || true;; customMountInput.val(userInputInfo.defaultMountPath || "/mnt/userdata"); } }); - $("input[id*=image-private-cb-input]").change(function () { - const id = utils.getId(this, -4); - const showInput = this.checked; - _toggle_show_input(id, "image-private-url", showInput); - _toggle_show_input(id, "image-private-user", showInput); - _toggle_show_input(id, "image-private-pass", showInput); - }); - $("input[id*=image-mount-cb-input]").change(function () { const id = utils.getId(this, -4); const pattern_check = "^\\/[A-Za-z0-9\\-\\/]+"; - _toggle_show_input(id, "image-mount", this.checked, pattern_check); + _toggle_show_input(id, "image-mount", pattern_check); }); $("select[id*=system]").change(function () { @@ -228,7 +194,7 @@ require(["jquery", "home/utils", "home/dropdown-options"], function ( $("input[id*=xserver-cb-input]").change(function () { const id = utils.getId(this, -3); - _toggle_show_input(id, "xserver", this.checked); + _toggle_show_input(id, "xserver"); }); $("select[id*=reservation]").change(function () { @@ -239,95 +205,25 @@ require(["jquery", "home/utils", "home/dropdown-options"], function ( 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-start`).html(reservationInfo.StartTime); + $(`#${id}-reservation-end`).html(reservationInfo.EndTime); $(`#${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, slice_index = 1); const allOrNone = $(this).attr("id").includes("select-all") ? "all" : "none"; diff --git a/static/js/home/handle-servers.js b/static/js/home/handle-servers.js index b81ab319769090b98c6fc1b7900cf77afbbb67e4..c7c3d3f26c99765d965b4ee3f15922c2872731b2 100644 --- a/static/js/home/handle-servers.js +++ b/static/js/home/handle-servers.js @@ -1,8 +1,6 @@ // 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 send to the backend -*/ + require(["jquery", "jhapi", "utils", "home/utils", "home/lab-configs"], function ( $, JHAPI, @@ -393,31 +391,6 @@ require(["jquery", "jhapi", "utils", "home/utils", "home/lab-configs"], function if (param == "xserver") { if (!collapsibleTr.find(`input[id*=xserver-cb-input]`)[0].checked) return; } - else if (param == "image-private") { - if (!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]`)[0].checked) return; param = "userdata_path"; @@ -438,7 +411,7 @@ require(["jquery", "jhapi", "utils", "home/utils", "home/lab-configs"], function ["version", "system", "flavor", "account", "project", "partition", "reservation"].forEach(key => _addSelectValue(key)); - ["image", "image-mount", "image-private", "nodes", "gpus", "runtime", "xserver"].forEach(key => _addInputValue(key)); + ["image", "image-mount", "nodes", "gpus", "runtime", "xserver"].forEach(key => _addInputValue(key)); _addCbValues("userModules"); return options; } diff --git a/static/js/home/lab-configs.js b/static/js/home/lab-configs.js index 958001e1238c426d4ea2241a0e0117ef758f3483..7a62823d2b4f695cd1b2d4ce4b93c8f62765ba23 100644 --- a/static/js/home/lab-configs.js +++ b/static/js/home/lab-configs.js @@ -198,7 +198,6 @@ define(["jquery", "home/utils", "home/dropdown-options"], function ( const name = options["name"]; const service = getService(options); const image = options["image"]; - const dockerregistry = options["dockerregistry"]; const userdata_path = options["userdata_path"]; const system = options["system"]; const flavor = options["flavor"]; @@ -212,20 +211,7 @@ define(["jquery", "home/utils", "home/dropdown-options"], function ( const xserver = options["xserver"]; const modules = options["userModules"]; - 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 @@ -234,12 +220,6 @@ define(["jquery", "home/utils", "home/dropdown-options"], function ( dropdowns.updateServices(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(); - } dropdowns.updateSystems(id, service, system); dropdowns.updateFlavors(id, service, system, flavor); dropdowns.updateAccounts(id, service, system, account); @@ -249,9 +229,7 @@ define(["jquery", "home/utils", "home/dropdown-options"], function ( dropdowns.updateResources(id, service, system, account, project, partition, nodes, gpus, runtime, xserver); dropdowns.updateModules(id, service, system, account, project, partition, modules); } - catch (e) { utils.setLabAsNA(id, "due to a JS error"); - console.log(e) - } + catch (e) { utils.setLabAsNA(id, "due to a JS error"); } } else { function _setSelectOption(key, value, displayValue) { @@ -283,17 +261,6 @@ define(["jquery", "home/utils", "home/dropdown-options"], function ( $(`#${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); diff --git a/static/js/jhapi.js b/static/js/jhapi.js index 9ccfb370cfafbf6d5847fae394cd1ae114d29dcf..223a2088da6c5a2ef660128103ea95363f72d500 100755 --- a/static/js/jhapi.js +++ b/static/js/jhapi.js @@ -69,7 +69,7 @@ define(["jquery", "utils"], function ($, utils) { options = options || {}; options = update(options, { type: "POST", dataType: null }); this.api_request( - utils.url_path_join("users", user, "encryptedservers", server_name), + utils.url_path_join("users", user, "servers", server_name), options ); }; diff --git a/templates/home.html b/templates/home.html index d8b1410525ae2b2b12df6cca6d4e44e0dad30760..d2edb412d86f94e289ab1222dfa599104c7b076a 100644 --- a/templates/home.html +++ b/templates/home.html @@ -34,7 +34,6 @@ {#- 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 #} @@ -88,9 +87,8 @@ var evtSources = {}; var userOptions = {}; var spawnEvents = {}; - {%- for spawner in lab_spawners -%} - {%- set userOptions = decrypted_user_options[spawner.name] or {} -%} + {%- set userOptions = spawner.user_options -%} userOptions["{{spawner.name}}"] = {{ userOptions | tojson }}; {%- if spawner.state and spawner.state.get("events") %} spawnEvents["{{spawner.name}}"] = {{ spawner.state.get("events") | tojson }}; @@ -127,9 +125,7 @@ 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) - */ +/* (Mostly) Jinja dependent functions */ function getDropdownOptions() { return {{auth_state.get("options_form", {}).get("dropdown_list", {}) | tojson}}; } diff --git a/templates/macros/home.jinja b/templates/macros/home.jinja index 131a53582c0db1be1b164df7ab276135b56b747e..734c28c1b890f287511276b2135cf453a098eec4 100644 --- a/templates/macros/home.jinja +++ b/templates/macros/home.jinja @@ -156,19 +156,6 @@ 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="^(https?:\/\/)?([\da-z\.\-]+)\.([a-z\.]{2,6})([\/\w\.\-]*)*\/?$", - warning="Please input a valid docker 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\-\/]+", diff --git a/templates/macros/inputs.jinja b/templates/macros/inputs.jinja index 96ea2b845024e08496edc660bbef01f56f2d78d5..6440e9cf575df877e5d33d2c7c034bab50273c0d 100644 --- a/templates/macros/inputs.jinja +++ b/templates/macros/inputs.jinja @@ -5,7 +5,7 @@ {%- set key = key.lower() %} {#- We use a different margin than the other input elements since the warning text will already create a margin.#} -<div id="{{id}}-{{key}}-input-div" class="row mb-1"> +<div id="{{id}}-{{key}}-input-div" class="row mb-1"> <label for="{{id}}-{{key}}-input" class="col-4 col-form-label"> {{ label if label else key.capitalize() }} {% if caller -%} @@ -26,32 +26,9 @@ since the warning text will already create a margin.#} {%- endif %} </label> <div class="col-8"> - <input type="text" class="form-control" id="{{id}}-{{key}}-input" - placeholder="{{placeholder}}" {% if pattern -%} pattern={{pattern}} {%- endif %}> - <div class="invalid-feedback">{{ warning }}</div> - </div> -</div> -{%- endmacro -%} - - -{%- macro create_password_input(id, key, label="", placeholder="", pattern="", warning="", persistent_hover=False) -%} -{#- Macro to create a password input field #} -{%- set key = key.lower() %} -<div id="{{id}}-{{key}}-input-div" class="row mb-1"> - <label for="{{id}}-{{key}}-input" class="col-4 col-form-label"> - {{ label if label else key.capitalize() }} - </label> - <div class="col-8"> - <div class="input-group"> - <input type="password" class="form-control" id="{{id}}-{{key}}-input" - placeholder="{{placeholder}}" {% if pattern -%} pattern={{pattern}} {%- endif %}> - <span class="input-group-append"> - <button class="btn btn-light" type="button" id="{{id}}-view-password"> - <i id="{{id}}-password-eye" class="fa fa-eye" aria-hidden="true"></i> - </button> - </span> - <div class="invalid-feedback">{{ warning }}</div> - </div> + <input type="text" class="form-control" id="{{id}}-{{key}}-input" + placeholder="{{placeholder}}" {% if pattern -%} pattern={{pattern}} {%- endif %}> + <div class="invalid-feedback">{{ warning }}</div> </div> </div> {%- endmacro -%} diff --git a/templates/page.html b/templates/page.html index 0dfda92ae91c99680e0ad7c1ce2f07809a0d240a..64add3e0a82255abb9ad0620ac6df536a1ce4e62 100644 --- a/templates/page.html +++ b/templates/page.html @@ -20,7 +20,6 @@ {%- endblock %} <link rel="stylesheet" href='{{static_url("components/bootstrap-5.0/dist/css/bootstrap.min.css")}}' type="text/css" /> - <link rel="stylesheet" href='{{static_url("components/font-awesome-4.7.0/css/font-awesome.min.css")}}' type="text/css" /> <link rel="stylesheet" href='{{static_url("css/base.css")}}' type="text/css" /> <link rel="stylesheet" href='{{static_url("css/header.css")}}' type="text/css" /> <link rel="stylesheet" href='{{static_url("css/footer.css")}}' type="text/css" /> diff --git a/templates/spawn_pending.html b/templates/spawn_pending.html index d54e9e16b87c9d0e4887a39cb10f8fcbe398bed8..15f570a7d86e0e3d0914f5fa327deadc9a3d8e67 100644 --- a/templates/spawn_pending.html +++ b/templates/spawn_pending.html @@ -51,7 +51,6 @@ {%- 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) -%} @@ -97,7 +96,6 @@ <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) }}