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(/&nbsp;/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(/&nbsp;/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(/&nbsp;/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(/&nbsp;/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(/&nbsp;/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 %}