From a45e17dafdc684dbe5bc9e732a7dfc7f49187a2e Mon Sep 17 00:00:00 2001 From: Tim Kreuzer <t.kreuzer@fz-juelich.de> Date: Wed, 6 Nov 2024 17:30:32 +0100 Subject: [PATCH] work in progress --- jupyterhub_custom_config.yaml | 198 +++++++++++++++++-- templates/home.html | 9 +- templates/macros/custom.jinja | 347 ++++++++++++++++++++++++++++++++++ templates/macros/home.jinja | 233 +++++++++++++++++------ templates/macros/inputs.jinja | 97 ++++++++++ 5 files changed, 803 insertions(+), 81 deletions(-) create mode 100644 templates/macros/custom.jinja diff --git a/jupyterhub_custom_config.yaml b/jupyterhub_custom_config.yaml index bebc661..1ffbd8a 100644 --- a/jupyterhub_custom_config.yaml +++ b/jupyterhub_custom_config.yaml @@ -195,6 +195,40 @@ selfapihandler: - options_form services: JupyterLab: + frontend: + key: "jupyterlab" + navsidebar: + - labconfig: + show: true + label: Labs configss + - kernels: + show: true + label: Kernels and Extensions + - resources: + show: true + label: Resources + tabs: + labconfig: + - key: displaynamecfg + show: true + type: inputtext + options: + value: "" + placeholder: "Give your lab a namess" + if_empty: "Unnamed JupyterLab" + label: + type: text + value: "Namess" + - key: versioncfg + show: true + type: dropdown + options: + values_func: get_versions + invalidFeedback: "Please choose a Version." + label: + type: text + value: "Versionss" + - type: hr allowedGroups: - default optionsName: Version @@ -218,18 +252,154 @@ services: - Dummy - LocalDummy frontend: - navsidebar: - - kernels: - - options: - - service-4-2: - system: - type: dropdown - values_func: get_4_2_system + key: service-4-2 + tabs: + kernels: + show: true + options: + - key: kernel + type: multiple_checkboxes + values_func: nav_default_env_kernel + show: true + - type: hr + - key: extensions + type: multiple_checkboxes + values_func: nav_default_env_extensions + show: true + - type: hr + - key: proxies + type: multiple_checkboxes + values_func: nav_default_env_proxies + show: true + - type: hr + - key: helper + type: multiple_checkboxes + values_func: nav_default_env_selectall + show: true + resources: + show: + type: function + func: get_default_env_show_resources + options: + - key: runtime + show: + type: function + func: get_default_env_show_runtime + type: number + options: + values_func: get_default_env_runtime + invalidFeedback_func: get_errormsg_runtime + label: + type: function + value: "Runtime (minutes)" + value_func: get_default_env_runtime_label + - key: nodes + show: + type: function + func: get_default_env_show_nodes + type: number + options: + values_func: get_default_env_nodes + invalidFeedback_func: get_errormsg_nodes + label: + type: function + value: "Nodes" + value_func: get_default_env_nodes_label + - key: gpus + show: + type: function + func: get_default_env_show_gpus + type: number + options: + values_func: get_default_env_gpus + invalidFeedback_func: get_errormsg_gpus + label: + type: function + value: "GPUs" + value_func: get_default_env_gpus_label + - key: xserver + show: + type: function + func: get_default_env_show_xserver + type: number + options: + default: 0 + values_func: get_default_env_xserver + invalidFeedback_func: get_errormsg_xserver + label: + type: textcheckbox + value: "Use XServer GPU index" + value_func: get_default_env_xserver_label + options: + default: false + labconfig: + show: true + options: + - key: system show: true + type: dropdown + options: + values_func: get_hpc_system + invalidFeedback: "Please choose a System." label: type: text value: "System--:" + - key: account + show: true + type: dropdown + options: + values_func: get_hpc_account + invalidFeedback: "Please choose an Account." + label: + type: text + value: "Account" + - key: project + show: true + type: dropdown + options: + values_func: get_hpc_project + invalidFeedback: "Please choose a Project." + label: + type: text + value: "Project" + - key: partition + show: true + type: dropdown + options: + values_func: get_hpc_partition + invalidFeedback: "Please choose a Partition." + label: + type: text + value: "Partition" + - key: reservation + show: true + type: dropdown + options: + values_func: get_hpc_reservation + invalidFeedback: "Please choose a Partition." + label: + type: text + value: "Reservation" + - key: reservationsummary + show: true + type: reservationsummary + options: + values_func: get_hpc_reservationsummary + - key: flavor + show: true + type: dropdown + options: + values_func: get_flavor + invalidFeedback: "Please choose a Flavor." + label: + type: text + value: "Flavor" + - key: flavorsummary + show: true + type: flavor + options: + text: "Available Flavors" + values_func: get_flavorsummary "3.6": name: "JupyterLab - 3.6" kernelSet: "3.6" @@ -660,17 +830,7 @@ reservationCheck: interval: 300 addUsers: [] setAllActive: false - systems: - JURECA: - host: jureca.fz-juelich.de - JUWELS: - host: juwels.fz-juelich.de - JEDI: - host: login.jedi.fz-juelich.de - JUSUF: - host: jusuf.fz-juelich.de - DEEP: - host: deep.fz-juelich.de + systems: [] hostname: jupyter-jsc-dev2.fz-juelich.de announcement: show: false diff --git a/templates/home.html b/templates/home.html index 6eb9d18..080c687 100644 --- a/templates/home.html +++ b/templates/home.html @@ -1,4 +1,5 @@ {%- extends "page.html" -%} +{%- import "macros/custom.jinja" as custom -%} {%- import "macros/home.jinja" as home -%} {%- import "macros/svgs.jinja" as svg -%} @@ -24,7 +25,7 @@ {%- endif -%} {%- endfor -%} {%- set software_key = "JupyterLab" %} -{%- set software = "jupyterlab" %} +{%- set software_prefix = "jupyterlab" %} {%- set new = "new" -%} {# id for new software entry #} {%- block main -%} @@ -69,14 +70,14 @@ </th> <th scope="row" colspan="100%" class="text-center">New JupyterLab</th> </tr> - {{ home.create_collapsible_tr(software, None, {}, custom_config, lab_id=new) }} + {{ 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(spawner_name, user_options) }} - {{ home.create_collapsible_tr(software, spawner_name, user_options, custom_config) }} + {{ 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> diff --git a/templates/macros/custom.jinja b/templates/macros/custom.jinja new file mode 100644 index 0000000..917f6d5 --- /dev/null +++ b/templates/macros/custom.jinja @@ -0,0 +1,347 @@ +{%- import "macros/svgs.jinja" as svg -%} + +{%- macro nav_default_env_kernel(custom_config) -%} +<div class="alert bg-info alert-dismissible fade show" style="color: #023d6b;" role="alert"> + <h4 class="alert-heading"> + {{ svg.announcement_svg | safe }} + <span class="align-middle"> {{ custom_config.get("announcement", {}).get("title", "") | safe }} </span> + </h4> + {{ custom_config.get("announcement", {}).get("body", "") | safe }} + <button type="button" class="btn-close" data-bs-dismiss="alert"></button> +</div> +{%- endmacro -%} + +{%- macro create_summary_tr(spawner_name, user_options, lab_id="", share_structure=false) -%} + +{%- set name = user_options.get("name", "") -%} +{%- if lab_id != "" %} {%- set id = software ~ "-" ~ lab_id -%} +{%- elif spawner_name %} {%- set id = software ~ "-" ~ spawner_name -%} +{%- else %} {%- set id = software ~ "-new-jupyterlab" -%} +{%- endif -%} + +{%- set system = user_options.get("system", "") -%} +{%- set flavor = user_options.get("flavor", "") -%} +{%- set partition = user_options.get("partition", "") -%} +{%- set project = user_options.get("project", "") -%} +{%- set runtime = user_options.get("runtime", "") -%} +{%- set nodes = user_options.get("nodes", "") -%} +{%- set gpus = user_options.get("gpus", "") -%} +<tr data-server-id="{{id}}" class="summary-tr"> + <td class="details-td" data-bs-target="#{{id}}-collapse"> + <div class="d-flex mx-auto accordion-icon {% if not share_structure -%}collapsed {%- endif %}mx-4"></div> + </td> + <th scope="row" class="name-td">{{ name }}</th> + <td class="config-td"> + <div style="max-height: 152px; overflow: auto;"> + {%- macro _config_td_entry(label, value, alignment="start", key="") -%} + {%- if not key %} {% set key = label.lower() %} {% endif -%} + {%- set breakpoints = "col-12 col-lg-4" %} + <div id="{{id}}-config-td-div-{{key}}" + class="col text-lg-{{alignment}} {{breakpoints}}" + {% if not value %}style="display: none;"{% endif %}> + <span class="text-muted" style="font-size: smaller;">{{ label }}</span><br> + <span id="{{id}}-config-td-{{key}}">{{ value }}</span> + </div> + {%- endmacro -%} + {%- set row_margins = "mx-3 mb-1" -%} + {%- set row_breakpoints = "col-12 col-md-6 col-lg-12" -%} + <div class="row {{row_margins}} justify-content-between"> + <div class="row {{row_breakpoints}}"> + {{ _config_td_entry("System", system, "start") }} + {{ _config_td_entry("Flavor", flavor, "center") }} + {{ _config_td_entry("Partition", partition, "center") }} + {{ _config_td_entry("Project", project, "end") }} + </div> + <div class="row {{row_breakpoints}}"> + {{ _config_td_entry("Runtime (min)", runtime, key="runtime", alignment="start") }} + {{ _config_td_entry("Nodes", nodes, "center") }} + {{ _config_td_entry("GPUs", gpus, "end") }} + </div> + </div> + </div> + </td> + {%- if s %} + {{ create_lab_progress_td(id) }} + {{ create_action_td(id, s) }} + {%- endif -%} +</tr> +{%- endmacro -%} + +{%- macro create_lab_progress_td(id) -%} +<td class="status-td"> + <div class="d-flex"> + <div class="d-flex flex-column"> + <div class="progress" style="height: 20px; min-width: 100px;"> + <div id="{{id}}-progress-bar" class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar"></div> + </div> + <span id="{{id}}-progress-info-text" class="progress-info-text text-center text-muted" style="font-size: smaller;"></span> + </div> + <div class="d-flex flex-column ms-3"> + <a role="button" class="log-info-btn lh-1 m-auto" style="padding-top: 1px;">{{ svg.logs_svg | safe }}</a> + <a role="button" class="log-info-text text-muted" style="font-size: smaller; text-decoration: none;">Logs</a> + </div> + </div> +</td> +{%- endmacro -%} + +{%- macro create_action_td(id, s, share_structure=false, user_name="") -%} +<td class="actions-td" style="white-space: nowrap;"> + {%- set button_margin = "my-1 me-1" -%} + {#- Save N/A status to page #} + <span id="{{id}}-na-status" class="na-status d-none">0</span> + {#- Create and show appropriate buttons #} + <button type="button" id="{{id}}-na-btn" class="btn btn-secondary btn-na-lab disabled {{button_margin}}" 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; white-space: nowrap;"></div> + {%- if share_structure %} + <button type="button" id="{{id}}-start-btn" class="btn btn-primary btn-start-new-lab ms-auto"> {{ svg.start_svg | safe }} Start </button> + {%- else %} + <button type="button" id="{{id}}-start-btn" class="btn btn-primary btn-start-lab {{button_margin}}" + {% if s and (s.active and not s._stop_pending) -%} style="display: none;" {%- endif %}> + {{ svg.start_svg | safe }} Start + </button> + {%- endif %} + <a type="button" id="{{id}}-open-btn" class="btn btn-success btn-open-lab {{button_margin}}" href='/user/{% if s -%}{{s.user.name}}{%- else -%}{{ user_name }}{%- endif %}/{{id}}' target="_blank" + {% if (not s) or (not s.active or s._stop_pending) -%} style="display: none;" {%- endif %} > + {{ svg.open_svg | safe }} Open + </a> + <button type="button" id="{{id}}-cancel-btn" class="btn btn-danger btn-cancel-lab" + {% if (not s) or (not s.active or s._stop_pending) -%} style="display: none;" {%- endif %} + {% if (not s) or (s.active and s.pending == None) -%} style="display: none;" {%- endif -%}> + {{ svg.stop_svg | safe }} Cancel + </button> + <button type="button" id="{{id}}-stop-btn" class="btn btn-danger btn-stop-lab" + {% if (not s) or (not s.active or s._stop_pending) -%} style="display: none;" {%- endif %} + {% if (not s) or (s.active and s.pending != None) -%} style="display: none;" {%- endif -%}> + {{ svg.stop_svg | safe }} Stop + </button> +</td> +{%- endmacro -%} + +{%- macro create_collapsible_tr(software, spawner_name, user_options, custom_config, lab_id="", share_structure=false) -%} + +{%- set name = user_options.get("name", "") -%} +{%- set new_lab_id = "new-jupyterlab" %} +{%- if lab_id != "" %} {%- set id = software ~ "-" ~ lab_id -%} +{%- elif spawner %} {%- set id = software ~ "-" ~ spawner_name -%} +{%- else %} {%- set id = software ~ "-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" %} + {#- TODO -> Tabs an linker Seite konfigurieren -#} + + <div class="nav flex-column nav-pills p-3 ps-0" id="{{ id }}-tab" role="tablist"> + <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> + {%- endif %} + </div> + {#- TAB NAV CONTENT -#} + {#- We only create empty elements here as they will be filled via JS #} + <div class="tab-content w-100" id="{{ id }}-tabContent"> + {#- 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 #} + {%- if false %} + {%- call inputs.create_text_input(id, "image", + placeholder="e.g. jupyter/datascience-notebook", + pattern="(([A-Za-z0-9][A-Za-z0-9_.\-\/]*)?:?[A-Za-z0-9_.\-]+)?", + 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> + </div> + {%- endif %} + </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 %} + </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 #} + </td> +</tr> +{%- endmacro -%} + +{%- macro create_lab_config_buttons(id, start_button_only=false) -%} +<hr> +<div class="d-flex"> + {%- if start_button_only %} + <button type="button" id="{{id}}-share-btn" class="btn btn-share-lab" style="display: none;" data-toggle="modal" data-target="#{{ id }}-share-link">{{ svg.share_svg | safe }} Share </button> + <button type="button" id="{{id}}-start-btn" class="btn btn-success btn-start-new-lab ms-auto"> {{ svg.start_svg | safe }} Start </button> + <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> + {%- else %} + <button id="{{ id }}-share-btn" type="button" class="btn btn-share-lab me-2" style="display: none;" data-toggle="modal" data-target="#{{ id }}-share-link">{{ svg.share_svg | safe }} Share </button> + <button id="{{ id }}-save-btn" type="button" class="btn btn-success btn-save-lab me-2">{{ svg.save_svg | safe }} Save </button> + <button id="{{ id }}-reset-btn" type="button" class="btn btn-danger btn-reset-lab me-2">{{ svg.reset_svg | safe }} Reset</button> + + <div class="alert fade p-0 m-0" role="alert"> + <span class="align-middle"></span> + <button type="button" class="btn-close align-middle" onClick="$(this).parent().removeClass('show p-1').addClass('p-0');"></button> + </div> + <button type="button" id="{{id}}-delete-btn" class="btn btn-danger btn-delete-lab ms-auto"> {{ svg.delete_svg | safe }} Delete </button> + {%- endif %} + <!-- 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> +</div> +{%- endmacro -%} diff --git a/templates/macros/home.jinja b/templates/macros/home.jinja index 5cb584f..79213a8 100644 --- a/templates/macros/home.jinja +++ b/templates/macros/home.jinja @@ -12,12 +12,12 @@ </div> {%- endmacro -%} -{%- macro create_summary_tr(spawner_name, user_options, lab_id="", share_structure=false) -%} +{%- macro create_summary_tr(software_key, software_prefix, spawner_name, user_options, lab_id="", share_structure=false) -%} {%- set name = user_options.get("name", "") -%} -{%- if lab_id != "" %} {%- set id = software ~ "-" ~ lab_id -%} -{%- elif spawner_name %} {%- set id = software ~ "-" ~ spawner_name -%} -{%- else %} {%- set id = software ~ "-new-jupyterlab" -%} +{%- 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" -%} {%- endif -%} {%- set system = user_options.get("system", "") -%} @@ -120,52 +120,100 @@ </td> {%- endmacro -%} -{%- macro create_collapsible_tr(software, spawner_name, user_options, custom_config, lab_id="", share_structure=false) -%} +{%- macro create_collapsible_tr(software_key, software_prefix, spawner_name, user_options, custom_config, lab_id="", share_structure=false) -%} {%- set name = user_options.get("name", "") -%} -{%- set new_lab_id = "new-jupyterlab" %} -{%- if lab_id != "" %} {%- set id = software ~ "-" ~ lab_id -%} -{%- elif spawner %} {%- set id = software ~ "-" ~ spawner_name -%} -{%- else %} {%- set id = software ~ "-new-jupyterlab" -%} +{%- 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" -%} {%- 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" %} - {#- TODO -> Tabs an linker Seite konfigurieren -#} <div class="nav flex-column nav-pills p-3 ps-0" id="{{ id }}-tab" role="tablist"> - <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> + {#- 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", []) -%} + {%- for key, options in navsidebar.items() -%} + <button class="d-none nav-link {{ nav_tab_margins }}" 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 -%} {%- 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 only create empty elements here as they will be filled via JS #} + {#- 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 #} + <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", []) %} + {%- for key in nav_item.keys() %} + {%- set _id = id ~ "-" ~ version_name ~ "-" ~ key %} + {# ----------- ADD d-none class in here later TODO ---------------- #} + <div class="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(key, []) %} + {%- if tabOption.type == "inputtext" %} + {{ inputs.create_text_input_2(_id ~ "-" ~ tabOption.key, tabOption.options, tabOption.label ) }} + {%- elif tabOption.type == "dropdown" %} + {{ inputs.create_select_2(_id ~ "-" ~ tabOption.key, tabOption.options, tabOption.label ) }} + {%- elif tabOption.type == "hr" %} + <hr> + {%- endif %} + {%- endfor %} + + {# 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(key, {}).get("options", []) %} + {%- if tabOption.type == "dropdown" %} + {{ inputs.create_select_2(_id ~ "-" ~ tabOption.key, tabOption.options, tabOption.label ) }} + {%- elif tabOption.type == "flavorsummary" %} + {{ create_flavor_summary(_id ~ "-" ~ tabOption.key, tabOption.options ) }} + {%- elif tabOption.type == "number" %} + {{ inputs.create_number_input_2(_id ~ "-" ~ tabOption.key, tabOption.options, tabOption.label ) }} + {%- elif tabOption.type == "reservationsummary" %} + {{ create_reservation_summary(_id ~ "-" ~ tabOption.key) }} + {%- endif %} + {%- endfor %} + </form> + {{ create_lab_config_buttons_2(_id, add_save_reset_buttons=true) }} + {#- {{ create_lab_config_buttons(id, id == new_lab_id or share_structure) }} #} + </div> + {%- endfor %} + {%- endfor %} + {%- endfor %} + + {#- 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 #} {%- if false %} + {{ 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")) }} {%- call inputs.create_text_input(id, "image", placeholder="e.g. jupyter/datascience-notebook", pattern="(([A-Za-z0-9][A-Za-z0-9_.\-\/]*)?:?[A-Za-z0-9_.\-]+)?", @@ -224,34 +272,8 @@ {{ 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> - </div> + {%- endif %} - </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"> @@ -263,6 +285,7 @@ </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"> @@ -300,6 +323,100 @@ </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) %} +<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> +</div> +{%- endmacro %} + +{%- macro create_flavor_summary(id, options) -%} +<div id="{{id}}-flavor-legend-div" class="row align-items-center g-0 mt-4"> + <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}}-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/inputs.jinja b/templates/macros/inputs.jinja index 6181e5e..ad0d36c 100644 --- a/templates/macros/inputs.jinja +++ b/templates/macros/inputs.jinja @@ -1,4 +1,58 @@ {%- import "macros/svgs.jinja" as svg -%} +{%- macro create_label(id, label) -%} + <label for="{{id}}-input" class="col-4 col-form-label"> + {%- if label.type == "text" %} + {%- if label.value is string %} + {{ label.value }} + {%- endif %} + {%- elif label.type == "texticon" %} + <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> + {%- elif 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> + {%- elif label.type == "textdropdown" %} + {%- elif label.type == "textcheckbox" %} + {%- if label.value is string %} + {{ label.value }} + {%- endif %} + <input type="checkbox" class="form-check-input" id="{{id}}-cb-input" value="None" {% if label.options.get("default", false) %}checked{% endif %}> + {%- elif label.type == "dropdown" %} + {%- elif label.type == "function" %} + {# This will require an update of the label, depending on the value of other this or other inputs + The label may come with a default text #} + {%- if label.value is string %} + {{ label.value }} + {%- endif %} + {%- endif %} + </label> +{%- endmacro -%} + +{%- macro create_label_scripts(id, label) -%} + {%- if label.type == "textcheckbox" %} + {# I think it's reasonable to put the logic for this checkbox in here #} + <script type="text/javascript"> + const checkbox = document.getElementById('{{id}}-cb-input'); + + function toggleNumberInput() { + document.getElementById('{{id}}-input').disabled = !checkbox.checked; + } + + checkbox.addEventListener('change', toggleNumberInput); + toggleNumberInput(); + </script> + {%- elif label.type == "textdropdown" %} + {%- elif label.type == "dropdown" %} + {%- elif label.type == "function" %} + {%- endif %} +{%- endmacro -%} {%- macro create_text_input(id, key, label="", placeholder="", pattern="", warning="", persistent_hover=False, clazz="") -%} {#- Macro to create a text input #} @@ -33,6 +87,21 @@ since the warning text will already create a margin.#} </div> {%- endmacro -%} +{%- macro create_text_input_2(id, options, label) -%} +{#- Macro to create a text input #} +{#- We use a different margin than the other input elements +since the warning text will already create a margin.#} +<div id="{{id}}-input-div" class="row mb-1"> + {{ create_label(id, label) }} + <div class="col-8"> + <input type="text" class="form-control" id="{{id}}-input" + {% if options.placeholder -%} placeholder="{{options.placeholder}}" {%- endif %} {% if options.pattern -%} pattern={{options.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 #} @@ -56,6 +125,31 @@ since the warning text will already create a margin.#} </div> {%- endmacro -%} +{%- macro create_select_2(id, options, label) -%} +<div id="{{id}}-select-div" class="row mb-3"> + {{ create_label(id, label) }} + <div class="col-8"> + <select id="{{id}}-select" class="form-select" required> + </select> + <div class="invalid-feedback">{{ options.get("invalidFeedback", "Input required") }}</div> + </div> +</div> +{%- endmacro -%} + +{%- macro create_number_input_2(id, options, label) -%} +<div id="{{id}}-input-div" class="row mb-3"> {# style="display: none; #} + {{ create_label(id, label) }} + <div class="col-8"> + <input type="number" id="{{id}}-input" class="form-control" value="{{ options.get("default", -1)}}"> + {#- Set the warning message via JS for different min max values. #} + <div class="invalid-feedback">{{ options.get("invalidFeedback", "Input required") }}</div> + </div> +</div> +{{ create_label_scripts(id, label) }} +{%- endmacro -%} + + + {%- macro create_select(id, key, label="") -%} {#- Macro to create an empty select which should be filled via JS. -#} {%- set key = key.lower() %} @@ -95,3 +189,6 @@ since the warning text will already create a margin.#} {%- macro create_button(id, text) -%} <button id="{{ id }}-{{ text }}-btn" type="button" class="btn btn-success me-2"> {{text}} </button> {%- endmacro -%} + +{%- macro create_multiple_checkboxes(id, value_sets_list) -%} +{%- endmacro -%} \ No newline at end of file -- GitLab