diff --git a/.gitignore b/.gitignore index cc53b3f6..55a81587 100644 --- a/.gitignore +++ b/.gitignore @@ -2,12 +2,18 @@ .DS_Store # bundle -Gemfile -Gemfile.lock +# Anchored to the repo root so the misc/Gemfile template stays tracked — the +# install docs tell administrators to copy it here. +/Gemfile +/Gemfile.lock vendor/ .bundle -TODO_ja.md + +# Local site configuration, copied from conf.yml.erb.sample conf.yml.erb +# Passenger restart trigger +tmp/ + # Regenerated from docs by misc/tests/run_tests.rb misc/tests/samples/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 94c0e197..2f6af533 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,43 @@ # Changelog +## [Unreleased] +### Add +- Add an OOD-integrated (embedded) mode that renders Open Composer inside Open OnDemand's own navbar and footer, via a reverse-proxy dashboard initializer under `ood_integration/` and without an iframe. +- Add an All Templates page (`/all_templates`) listing every template as one flat list, including applications hidden from the index page. +- Add custom templates: save the current form values as a reusable template, then rename, re-describe, delete or reorder them from the index page. +- Add a "Load or Create a Script from Disk" file browser that opens an existing batch script in the generic scheduler form with its location and name pre-filled. +- Add New Script and New Custom Template pickers for choosing which application to start from. +- Add a Nodes page listing the cluster's compute nodes with their state, CPU, memory and generic resources, with one dynamically discovered column per GRES type. +- Add a `module_load` widget: a dropdown of every available version of an environment module, which rewrites the existing `module load` line in place so surrounding script lines are preserved. +- Add a `dependent_module_select` widget whose module list follows the value of another widget. +- Add `remember_last` to `select` widgets, restoring the user's previous choice from browser storage. +- Add script-to-form parsing, so editing the script pane, or opening a job from the history page, reconstructs the form from the script's scheduler directives. +- Add an empty-script submission warning (`empty_script_warning`). +- Add a Job Efficiency panel to the history page (`history_efficiency`), reporting wall time, CPU, memory and, for GPU jobs, GPU utilization and GPU memory. +- Add output and error columns to the history page, with an in-page file viewer. +- Add "Cancel All Jobs", "Delete All History" and "Refresh" actions to the history page, plus per-job cancellation with progress reporting. +- Add optional SSH key provisioning on submit (`ensure_ssh_key`), disabled by default. +- Add a `modules_list_url` setting for the module catalog behind the module widgets. +- Add `home_format`, `category_badge_colors`, `accent_color`, `body_text_color`, `gpu_memory` and `favicon` settings. +- Add `hidden`, `documentation` and `tags` manifest keys, and allow `category` to be given as a list. + +### Changed +- Use `sacct` as the source of truth on the history page, so jobs submitted outside Open Composer also appear, with their script fetched live. +- Move the generic per-scheduler script templates to `generic_apps_dir`, reachable at `/_generic/` and used as the fallback when an application's `form.yml` is missing. +- Replace the hard-coded accent palette and GPU memory capacities with settings, so a site can be restyled without editing a view. +- Reorganize `conf.yml.erb.sample` into ten numbered sections, matching section 2 of `docs/install.html`. +- Pass `scheduler_env` to every scheduler command, not only to submission, cancellation and status queries. +- Extend the `form.yml` smoke tests to cover the new widgets, and fall back to macOS's bundled `jsc` when `node` is unavailable. + +### Fixed +- Anchor the `enable--\n" + html += "\n" + @table_index += 1 + html + output_help(key, value) + end + + # JavaScript to initialize a dependent_module_select: watches a driver widget and re-fetches + # the module version list whenever the driver's selected value changes prefix group. + def output_dependent_module_select_js(key, value) + driver = value['driver'].to_s + modules = value['options'] || [] + sn = @script_name.to_s + + mod_map_js = modules.map { |m| + parts = [] + parts << "prefix: #{m['prefix'].to_s.to_json}" if m['prefix'] + parts << "contains: #{m['contains'].to_s.to_json}" if m['contains'] + parts << "module: #{m['module'].to_s.to_json}" + "{#{parts.join(', ')}}" + }.join(", ") + + <<~JS + (function() { + var sel = document.getElementById(#{key.to_json}); + var driver = document.getElementById(#{driver.to_json}); + if (!sel) return; + var modMap = [#{mod_map_js}]; + var sn = #{sn.to_json}; + var lastModule = null; + + function moduleForValue(val) { + var s = String(val); + for (var i = 0; i < modMap.length; i++) { + if (modMap[i].prefix && s.startsWith(modMap[i].prefix)) return modMap[i].module; + if (modMap[i].contains && s.includes(modMap[i].contains)) return modMap[i].module; + } + return modMap.length > 0 ? modMap[modMap.length - 1].module : ''; + } + + function loadModules(moduleName) { + if (!moduleName || moduleName === lastModule) return; + lastModule = moduleName; + var urlParams = new URLSearchParams(window.location.search); + var cluster = urlParams.get('_cluster_name') || ''; + fetch(sn + '/_module_avail?module=' + encodeURIComponent(moduleName) + '&cluster=' + encodeURIComponent(cluster)) + .then(function(r) { return r.json(); }) + .then(function(mods) { + sel.innerHTML = ''; + if (!mods.length) { + var opt = document.createElement('option'); + opt.value = ''; opt.dataset.value = ''; opt.textContent = 'No modules found'; + sel.appendChild(opt); return; + } + mods.forEach(function(m) { + var opt = document.createElement('option'); + opt.value = m; opt.dataset.value = m; opt.textContent = m; + sel.appendChild(opt); + }); + if (sel.selectedIndex === -1) sel.selectedIndex = 0; + sel.dispatchEvent(new Event('change')); + }) + .catch(function() { + sel.innerHTML = ''; + }); + } + + function updateFromDriver() { + if (!driver) return; + var idx = driver.selectedIndex; + var driverVal = (idx >= 0 && driver.options[idx] && driver.options[idx].dataset.value) + ? driver.options[idx].dataset.value + : (driver.value || ''); + loadModules(moduleForValue(driverVal)); + } + + updateFromDriver(); + if (driver) driver.addEventListener('change', updateFromDriver); + })(); + JS + end + # Output a path widget. def output_path_html(key, value, script_content, submit_content, app_name, dir_name) favorites = value['favorites'] ? value['favorites'].select { |path| File.exist?(path) } : [] @@ -554,7 +775,7 @@ def output_path_html(key, value, script_content, submit_content, app_name, dir_n 'submit' end if type - html += "oninput=\"ocForm.confirmOverwrite('#{type}', '#{key}', function(){ocForm.updateArea('#{type}', '#{key}')})\" " + html += "oninput=\"ocForm.updateArea('#{type}', '#{key}')\" " html += "onfocus=\"ocForm.storePreviousValue('#{key}')\"" html += " style=\"background-color: #{@conf["submit_color"]};\"" if type == 'submit' else @@ -638,7 +859,7 @@ def output_path_html(key, value, script_content, submit_content, app_name, dir_n HTML html += "" \ + "\n " \ + : "" + <<~HTML