diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index fbfd1bfdc6..faa37b563e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -20,7 +20,7 @@ jobs: - os: ubuntu-latest ruby: "3.0.2" bundler: "2.3.5" - # Test versions from RHEL8 & RHEL9 + # Test versions from Debian 12 - os: ubuntu-latest ruby: "3.1.2" bundler: "2.3.7" @@ -28,6 +28,10 @@ jobs: - os: ubuntu-latest ruby: "3.2.2" bundler: "2.4.10" + # Test versions from RHEL8 & RHEL9 + - os: ubuntu-latest + ruby: "3.3.5" + bundler: "2.5.16" runs-on: ${{ matrix.os }} name: Unit tests @@ -129,7 +133,7 @@ jobs: - x86_64 - aarch64 - ppc64le - version: ["3.1"] + version: ["4.0"] exclude: # Amazon 2023 on aarch64 is very slow and will time out - dist: amzn2023 @@ -137,12 +141,15 @@ jobs: # Amazon 2023 doesn't have ppc64le containers - dist: amzn2023 arch: ppc64le - # Ubuntu doesn't have way to get NodeJS 14+ on ppc64le + # Ubuntu and Debian doesn't have way to get NodeJS 20+ on ppc64le - dist: ubuntu-20.04 arch: ppc64le - # Ubuntu doesn't have way to get NodeJS 14+ on ppc64le - dist: ubuntu-22.04 arch: ppc64le + - dist: ubuntu-24.04 + arch: ppc64le + - dist: debian-12 + arch: ppc64le runs-on: ${{ matrix.os }} name: E2E test ${{ matrix.dist }}-${{ matrix.arch }} diff --git a/.github/workflows/update.yml b/.github/workflows/update.yml index 38acf9c489..ad72d90eb1 100644 --- a/.github/workflows/update.yml +++ b/.github/workflows/update.yml @@ -31,6 +31,11 @@ jobs: path: ~/vendor/bundle key: ${{ runner.os }}-${{ matrix.ruby }}-gems-${{ hashFiles('apps/*/Gemfile.lock') }} + - name: Setup os dependencies + run: | + sudo apt-get update + sudo apt-get install -y libsqlite3-dev + - name: Setup Bundler run: | bundle config path ~/vendor/bundle diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 482e578eed..d191cd82a6 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -9,6 +9,7 @@ stages: - deploy variables: + OOD_BUILD_REPO: '4.0' GIT_STRATEGY: clone GIT_DEPTH: 0 OOD_PACKAGING_DEBUG: 'true' @@ -26,14 +27,11 @@ build-nightly: - OOD_PACKAGING_DIST: [el8] OOD_PACKAGING_ARCH: [x86_64, aarch64, ppc64le] OOD_PACKAGING_GPG_PRIVATE_KEY: /systems/osc_certs/gpg/ondemand/ondemand.sec - - OOD_PACKAGING_DIST: [el9, debian-12] + - OOD_PACKAGING_DIST: [el9] OOD_PACKAGING_ARCH: [x86_64, aarch64, ppc64le] - # Ubuntu doesn't have way to get NodeJS 14+ on ppc64le - - OOD_PACKAGING_DIST: [ubuntu-20.04, ubuntu-22.04] + # Ubuntu and Debian don't have way to get NodeJS 20+ on ppc64le + - OOD_PACKAGING_DIST: [ubuntu-20.04, ubuntu-22.04, ubuntu-24.04, debian-12] OOD_PACKAGING_ARCH: [x86_64, aarch64] - # Ubuntu 24.04 on aarch64 is slow and will time out - - OOD_PACKAGING_DIST: [ubuntu-24.04] - OOD_PACKAGING_ARCH: [x86_64, ppc64le] # Amazon 2023 on aarch64 is slow and will time out - OOD_PACKAGING_DIST: [amzn2023] OOD_PACKAGING_ARCH: [x86_64] @@ -50,6 +48,8 @@ build: # Re-enable once Gitlab instance using plugin to integrate with Github # - if: '$CI_PIPELINE_SOURCE == "external_pull_request_event"' - if: '$CI_COMMIT_BRANCH !~ /^(master|release_[0-9]\.[0-9])$/ && $CI_COMMIT_TAG == null' + variables: + VERSION: '$OOD_BUILD_REPO.0' script: - bundle exec rake package:build[$OOD_PACKAGING_DIST,$OOD_PACKAGING_ARCH] parallel: @@ -57,14 +57,11 @@ build: - OOD_PACKAGING_DIST: [el8] OOD_PACKAGING_ARCH: [x86_64, aarch64, ppc64le] OOD_PACKAGING_GPG_PRIVATE_KEY: /systems/osc_certs/gpg/ondemand/ondemand.sec - - OOD_PACKAGING_DIST: [el9, debian-12] + - OOD_PACKAGING_DIST: [el9] OOD_PACKAGING_ARCH: [x86_64, aarch64, ppc64le] - # Ubuntu doesn't have way to get NodeJS 14+ on ppc64le - - OOD_PACKAGING_DIST: [ubuntu-20.04, ubuntu-22.04] + # Ubuntu and Debian don't have way to get NodeJS 20+ on ppc64le + - OOD_PACKAGING_DIST: [ubuntu-20.04, ubuntu-22.04, ubuntu-24.04, debian-12] OOD_PACKAGING_ARCH: [x86_64, aarch64] - # Ubuntu 24.04 on aarch64 is slow and will time out - - OOD_PACKAGING_DIST: [ubuntu-24.04] - OOD_PACKAGING_ARCH: [x86_64, ppc64le] # Amazon 2023 on aarch64 is slow and will time out - OOD_PACKAGING_DIST: [amzn2023] OOD_PACKAGING_ARCH: [x86_64] diff --git a/CHANGELOG.md b/CHANGELOG.md index d47f512bdf..ac017c74fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - Batch connect apps now respond to form_header to display a header in [3763](https://github.com/OSC/ondemand/pull/3763). - auto_clusters now set maximums for auto_cores in [3778](https://github.com/OSC/ondemand/pull/3778). - UIDs can now be returned by the mapper script in [3795](https://github.com/OSC/ondemand/pull/3795). +- XDMoD jobs widget now shows CPU, Memory and walltime in [3789](https://github.com/OSC/ondemand/pull/3789). +- Global batch connect form items can now be defined in ondemand.d files in [3840](https://github.com/OSC/ondemand/pull/3840). ### Changed - Script models have been renamed to Launcher in [3397](https://github.com/OSC/ondemand/pull/3397). @@ -55,6 +57,13 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - The shell app now has configurations for ping ponging. Ping pongs are disabled by default, will only ping pong for a certain duration after inactivity and the connections will close altogether after a certian duration regardless of activity in [3805](https://github.com/OSC/ondemand/pull/3805) and [3810](https://github.com/OSC/ondemand/pull/3810). +- Empty directories can now be downloaded in [3841](https://github.com/OSC/ondemand/pull/3841). +- Batch Connect applications always lowercase ids for normalization for dynamic javascript in [3867](https://github.com/OSC/ondemand/pull/3867). + - This includes auto modules in [3905](https://github.com/OSC/ondemand/pull/3905). +- Batch connect applications always cast select_options to an array in [3872](https://github.com/OSC/ondemand/pull/3872). +- test, package and development gems are no longer installed in production in [3906](https://github.com/OSC/ondemand/pull/3906). +- A single cluster form item is now hidden, not fixed, allowing dynamic directives to work on single clusters in + [3931](https://github.com/OSC/ondemand/pull/3931). ### Fixed - Ensure that the asset directory is clean when building in [3356](https://github.com/OSC/ondemand/pull/3356). @@ -67,12 +76,18 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - Download buttons will now be hidden for certain files like pipes in [3654](https://github.com/OSC/ondemand/pull/3654). - Favorite file paths now consult the Allowlist in [3526](https://github.com/OSC/ondemand/pull/3526). - The ood_portal.conf now accounts for /dex (dex_uri) when enabling maintenance mode in [3736](https://github.com/OSC/ondemand/pull/3736). -- mod_ood_proxy now correctly proxies for httpd 2.4.62 in [3728](https://github.com/OSC/ondemand/pull/3728) - and [3776](https://github.com/OSC/ondemand/pull/3776). +- mod_ood_proxy now correctly proxies for httpd 2.4.62 in [3728](https://github.com/OSC/ondemand/pull/3728), + [3776](https://github.com/OSC/ondemand/pull/3776) and [3791](https://github.com/OSC/ondemand/pull/3791). - ood_auth_map now accounts for more than just \w for usernames in [3753](https://github.com/OSC/ondemand/pull/3753). - Pipes and fifos no longer show as downloadable in [3718](https://github.com/OSC/ondemand/pull/3718). - Allowlist compuations have been optimized in [3804](https://github.com/OSC/ondemand/pull/3804). - data_field widgets now initialize their value to today in [3817](https://github.com/OSC/ondemand/pull/3817). +- Batch Connect cache files now correct serialize in [3819](https://github.com/OSC/ondemand/pull/3819). +- Uploads always succeed even when the chown operation afterwards fails in [3856](https://github.com/OSC/ondemand/pull/3856). +- Exceptions in dashboard widgets are correct rescued in [3873](https://github.com/OSC/ondemand/pull/3873). +- Select all in the files app will only select the visible rows in [3925](https://github.com/OSC/ondemand/pull/3925). +- Batch jobs now specify workdir, fixing issues with submit_host jobs in [3913](https://github.com/OSC/ondemand/pull/3913). +- Javascript that queires for atch connect sessions will create an alert div and stop polling if it fails in [3915](https://github.com/OSC/ondemand/pull/3915). ### Security diff --git a/Gemfile b/Gemfile index 3557b7119a..a26a16e505 100644 --- a/Gemfile +++ b/Gemfile @@ -8,7 +8,7 @@ gem 'rake' gem 'dotenv', '~> 2.1' group :package do - gem 'ood_packaging', '~> 0.15.1' + gem 'ood_packaging', '~> 0.16.2' end group :test do diff --git a/Gemfile.lock b/Gemfile.lock index 306e85419d..c94ba19057 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -96,7 +96,7 @@ GEM oga (3.3) ast ruby-ll (~> 2.1) - ood_packaging (0.15.1) + ood_packaging (0.16.2) rake (~> 13.0.1) open_uri_redirections (0.2.1) parallel (1.21.0) @@ -106,8 +106,7 @@ GEM rake (13.0.6) regexp_parser (2.1.1) require_all (3.0.0) - rexml (3.3.6) - strscan + rexml (3.3.9) rspec (3.10.0) rspec-core (~> 3.10.0) rspec-expectations (~> 3.10.0) @@ -160,7 +159,6 @@ GEM net-telnet (= 0.1.1) sfl stringify-hash (0.0.2) - strscan (3.1.0) thor (1.2.1) unicode-display_width (2.1.0) vmfloaty (1.5.0) @@ -179,7 +177,7 @@ DEPENDENCIES beaker-docker (~> 1.4.0) beaker-rspec dotenv (~> 2.1) - ood_packaging (~> 0.15.1) + ood_packaging (~> 0.16.2) rake rspec rubocop diff --git a/apps/dashboard/Gemfile b/apps/dashboard/Gemfile index d5ae331192..3fad5f2970 100644 --- a/apps/dashboard/Gemfile +++ b/apps/dashboard/Gemfile @@ -2,7 +2,7 @@ source 'https://rubygems.org' # Bundle edge Rails instead: gem 'rails', github: 'rails/rails' -gem 'rails', '7.0.8.4' +gem 'rails', '7.0.8.5' # Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder gem 'jbuilder', '~> 2.0' @@ -46,6 +46,8 @@ end gem 'nokogiri', '~> 1.15', '< 1.16' gem 'net-imap', '~> 0.3', '< 0.4' gem 'public_suffix', '~> 5.0', '< 6.0' +gem 'turbo-rails', '2.0.7' +gem 'zeitwerk', '2.6.18' # Extra third-party gems gem 'dotenv-rails', '~> 2.1' @@ -63,7 +65,6 @@ gem 'rest-client', '~> 2.0' gem 'jsbundling-rails', '~> 1.0' gem 'cssbundling-rails', '~> 1.1' -gem 'turbo-rails', '~> 2.0' # should upgrade to propshaft - only have an issue with fontawesome icons gem 'sprockets-rails', '>= 2.0.0' diff --git a/apps/dashboard/Gemfile.lock b/apps/dashboard/Gemfile.lock index 21074a073f..fa14b8578a 100644 --- a/apps/dashboard/Gemfile.lock +++ b/apps/dashboard/Gemfile.lock @@ -1,67 +1,67 @@ GEM remote: https://rubygems.org/ specs: - actioncable (7.0.8.4) - actionpack (= 7.0.8.4) - activesupport (= 7.0.8.4) + actioncable (7.0.8.5) + actionpack (= 7.0.8.5) + activesupport (= 7.0.8.5) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailbox (7.0.8.4) - actionpack (= 7.0.8.4) - activejob (= 7.0.8.4) - activerecord (= 7.0.8.4) - activestorage (= 7.0.8.4) - activesupport (= 7.0.8.4) + actionmailbox (7.0.8.5) + actionpack (= 7.0.8.5) + activejob (= 7.0.8.5) + activerecord (= 7.0.8.5) + activestorage (= 7.0.8.5) + activesupport (= 7.0.8.5) mail (>= 2.7.1) net-imap net-pop net-smtp - actionmailer (7.0.8.4) - actionpack (= 7.0.8.4) - actionview (= 7.0.8.4) - activejob (= 7.0.8.4) - activesupport (= 7.0.8.4) + actionmailer (7.0.8.5) + actionpack (= 7.0.8.5) + actionview (= 7.0.8.5) + activejob (= 7.0.8.5) + activesupport (= 7.0.8.5) mail (~> 2.5, >= 2.5.4) net-imap net-pop net-smtp rails-dom-testing (~> 2.0) - actionpack (7.0.8.4) - actionview (= 7.0.8.4) - activesupport (= 7.0.8.4) + actionpack (7.0.8.5) + actionview (= 7.0.8.5) + activesupport (= 7.0.8.5) rack (~> 2.0, >= 2.2.4) rack-test (>= 0.6.3) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.2.0) - actiontext (7.0.8.4) - actionpack (= 7.0.8.4) - activerecord (= 7.0.8.4) - activestorage (= 7.0.8.4) - activesupport (= 7.0.8.4) + actiontext (7.0.8.5) + actionpack (= 7.0.8.5) + activerecord (= 7.0.8.5) + activestorage (= 7.0.8.5) + activesupport (= 7.0.8.5) globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (7.0.8.4) - activesupport (= 7.0.8.4) + actionview (7.0.8.5) + activesupport (= 7.0.8.5) builder (~> 3.1) erubi (~> 1.4) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.1, >= 1.2.0) - activejob (7.0.8.4) - activesupport (= 7.0.8.4) + activejob (7.0.8.5) + activesupport (= 7.0.8.5) globalid (>= 0.3.6) - activemodel (7.0.8.4) - activesupport (= 7.0.8.4) - activerecord (7.0.8.4) - activemodel (= 7.0.8.4) - activesupport (= 7.0.8.4) - activestorage (7.0.8.4) - actionpack (= 7.0.8.4) - activejob (= 7.0.8.4) - activerecord (= 7.0.8.4) - activesupport (= 7.0.8.4) + activemodel (7.0.8.5) + activesupport (= 7.0.8.5) + activerecord (7.0.8.5) + activemodel (= 7.0.8.5) + activesupport (= 7.0.8.5) + activestorage (7.0.8.5) + actionpack (= 7.0.8.5) + activejob (= 7.0.8.5) + activerecord (= 7.0.8.5) + activesupport (= 7.0.8.5) marcel (~> 1.0) mini_mime (>= 1.1.0) - activesupport (7.0.8.4) + activesupport (7.0.8.5) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) minitest (>= 5.1) @@ -101,7 +101,7 @@ GEM cssbundling-rails (1.4.1) railties (>= 6.0.0) dalli (3.2.8) - date (3.3.4) + date (3.4.0) domain_name (0.6.20240107) dotenv (2.8.1) dotenv-rails (2.8.1) @@ -111,28 +111,29 @@ GEM activesupport i18n erubi (1.13.0) - execjs (2.9.1) + execjs (2.10.0) ffi (1.16.3) globalid (1.2.1) activesupport (>= 6.1) http-accept (1.7.0) http-cookie (1.0.7) domain_name (~> 0.5) - i18n (1.14.5) + i18n (1.14.6) concurrent-ruby (~> 1.0) - jbuilder (2.12.0) + jbuilder (2.13.0) actionview (>= 5.0.0) activesupport (>= 5.0.0) jsbundling-rails (1.3.1) railties (>= 6.0.0) local_time (1.0.3) coffee-rails + logger (1.6.1) lograge (0.14.0) actionpack (>= 4) activesupport (>= 4) railties (>= 4) request_store (~> 1.0) - loofah (2.22.0) + loofah (2.23.1) crass (~> 1.0.2) nokogiri (>= 1.12.0) mail (2.8.1) @@ -143,13 +144,14 @@ GEM marcel (1.0.4) matrix (0.4.2) method_source (1.1.0) - mime-types (3.5.2) + mime-types (3.6.0) + logger mime-types-data (~> 3.2015) - mime-types-data (3.2024.0903) + mime-types-data (3.2024.1105) mini_mime (1.1.5) mini_portile2 (2.8.7) minitest (5.25.1) - mocha (2.4.5) + mocha (2.5.0) ruby2_keywords (>= 0.0.5) multi_json (1.15.0) mustermann (3.0.3) @@ -164,7 +166,7 @@ GEM net-smtp (0.5.0) net-protocol netrc (0.11.0) - nio4r (2.7.3) + nio4r (2.7.4) nokogiri (1.15.6) mini_portile2 (~> 2.8.2) racc (~> 1.4) @@ -182,30 +184,30 @@ GEM pry (0.14.2) coderay (~> 1.1) method_source (~> 1.0) - psych (5.1.2) + psych (5.2.0) stringio public_suffix (5.1.1) racc (1.8.1) - rack (2.2.9) + rack (2.2.10) rack-protection (3.2.0) base64 (>= 0.1.0) rack (~> 2.2, >= 2.2.4) rack-test (2.1.0) rack (>= 1.3) - rails (7.0.8.4) - actioncable (= 7.0.8.4) - actionmailbox (= 7.0.8.4) - actionmailer (= 7.0.8.4) - actionpack (= 7.0.8.4) - actiontext (= 7.0.8.4) - actionview (= 7.0.8.4) - activejob (= 7.0.8.4) - activemodel (= 7.0.8.4) - activerecord (= 7.0.8.4) - activestorage (= 7.0.8.4) - activesupport (= 7.0.8.4) + rails (7.0.8.5) + actioncable (= 7.0.8.5) + actionmailbox (= 7.0.8.5) + actionmailer (= 7.0.8.5) + actionpack (= 7.0.8.5) + actiontext (= 7.0.8.5) + actionview (= 7.0.8.5) + activejob (= 7.0.8.5) + activemodel (= 7.0.8.5) + activerecord (= 7.0.8.5) + activestorage (= 7.0.8.5) + activesupport (= 7.0.8.5) bundler (>= 1.15.0) - railties (= 7.0.8.4) + railties (= 7.0.8.5) rails-dom-testing (2.2.0) activesupport (>= 5.0.0) minitest @@ -213,9 +215,9 @@ GEM rails-html-sanitizer (1.6.0) loofah (~> 2.21) nokogiri (~> 1.14) - railties (7.0.8.4) - actionpack (= 7.0.8.4) - activesupport (= 7.0.8.4) + railties (7.0.8.5) + actionpack (= 7.0.8.5) + activesupport (= 7.0.8.5) method_source rake (>= 12.2) thor (~> 1.0) @@ -232,7 +234,7 @@ GEM http-cookie (>= 1.0.2, < 2.0) mime-types (>= 1.16, < 4.0) netrc (~> 0.8) - rexml (3.3.7) + rexml (3.3.9) rss (0.3.1) rexml ruby2_keywords (0.0.5) @@ -262,18 +264,18 @@ GEM actionpack (>= 6.1) activesupport (>= 6.1) sprockets (>= 3.0.0) - stringio (3.1.1) + stringio (3.1.2) thor (1.3.2) tilt (2.4.0) timecop (0.9.10) - timeout (0.4.1) - turbo-rails (2.0.6) + timeout (0.4.2) + turbo-rails (2.0.7) actionpack (>= 6.0.0) activejob (>= 6.0.0) railties (>= 6.0.0) tzinfo (2.0.6) concurrent-ruby (~> 1.0) - webrick (1.8.2) + webrick (1.9.0) websocket (1.2.11) websocket-driver (0.7.6) websocket-extensions (>= 0.1.0) @@ -310,7 +312,7 @@ DEPENDENCIES ood_support (~> 0.0.2) pry public_suffix (~> 5.0, < 6.0) - rails (= 7.0.8.4) + rails (= 7.0.8.5) redcarpet (~> 3.3) rest-client (~> 2.0) rss (~> 0.2) @@ -320,8 +322,9 @@ DEPENDENCIES sinatra-contrib sprockets-rails (>= 2.0.0) timecop (~> 0.9) - turbo-rails (~> 2.0) + turbo-rails (= 2.0.7) webrick + zeitwerk (= 2.6.18) zip_kit (~> 6.2) BUNDLED WITH diff --git a/apps/dashboard/app/assets/stylesheets/application.scss b/apps/dashboard/app/assets/stylesheets/application.scss index e36d0543d6..6b715bc322 100644 --- a/apps/dashboard/app/assets/stylesheets/application.scss +++ b/apps/dashboard/app/assets/stylesheets/application.scss @@ -173,6 +173,7 @@ small.form-text { @import "editor"; @import "icon_picker"; @import "pinned_apps"; +@import "xdmod"; @import "support_ticket"; @import "data_tables"; @import "projects"; diff --git a/apps/dashboard/app/assets/stylesheets/projects.scss b/apps/dashboard/app/assets/stylesheets/projects.scss index f47270cb14..f1c68188ee 100644 --- a/apps/dashboard/app/assets/stylesheets/projects.scss +++ b/apps/dashboard/app/assets/stylesheets/projects.scss @@ -38,3 +38,14 @@ font-size: 1.4em; float: right; } + +.launcher-title { + font-size: 1em; + font-weight: bold; +} + +.launcher-button { + color: white; + width: 100%; + margin: 0.25rem; +} \ No newline at end of file diff --git a/apps/dashboard/app/assets/stylesheets/xdmod.scss b/apps/dashboard/app/assets/stylesheets/xdmod.scss new file mode 100644 index 0000000000..67a06c8f6b --- /dev/null +++ b/apps/dashboard/app/assets/stylesheets/xdmod.scss @@ -0,0 +1,59 @@ +/** Job Analytics **/ +#jobsPanelDiv { + .hiddenRow { + padding: 0 !important; + } + + i.app-icon { + width: 0.9rem; + height: 0.9rem; + font-size: 0.9rem; + } + + tr { + position: relative; + } + + tr[aria-expanded=true] .closed { + display: none; + } + + tr[aria-expanded=false] .open { + display: none; + } + + tr[aria-expanded=true] td:not(.job-analytics) { + padding-bottom: 45px; + } + + tr.error[aria-expanded=true] td:not(.job-analytics) { + padding-bottom: 200px; + } + + tr.error td.job-analytics { + border-bottom: none; + } + + td.job-analytics { + position: absolute; + top: 35px; + left: 0; + z-index: 1000; + width: 100%; + padding: 0; + + div.job-analytics-content { + display: flex; + justify-content: space-between; + padding: 0.5rem 0.5rem; + + strong { + font-weight: 600; + } + + .badge { + vertical-align: 1px; + } + } + } +} \ No newline at end of file diff --git a/apps/dashboard/app/controllers/active_jobs_controller.rb b/apps/dashboard/app/controllers/active_jobs_controller.rb index f830f63cfd..ece6423244 100644 --- a/apps/dashboard/app/controllers/active_jobs_controller.rb +++ b/apps/dashboard/app/controllers/active_jobs_controller.rb @@ -79,11 +79,11 @@ def get_job(jobid, cluster) ActiveJobs::Jobstatusdata.new(data, cluster, true) rescue OodCore::JobAdapterError - OpenStruct.new(name: jobid, error: "No job details because job has already left the queue." , status: status_label("completed") ) + OpenStruct.new(name: jobid, error: "No job details because job has already left the queue." , status: "completed" ) rescue => e Rails.logger.info("#{e}:#{e.message}") Rails.logger.info(e.backtrace.join("\n")) - OpenStruct.new(name: jobid, error: "No job details available.\n" + e.backtrace.to_s, status: status_label("") ) + OpenStruct.new(name: jobid, error: "No job details available.\n" + e.backtrace.to_s, status: "" ) end end diff --git a/apps/dashboard/app/controllers/launchers_controller.rb b/apps/dashboard/app/controllers/launchers_controller.rb index 65d8b25312..51d9f3575b 100644 --- a/apps/dashboard/app/controllers/launchers_controller.rb +++ b/apps/dashboard/app/controllers/launchers_controller.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -# The controller for apps pages /dashboard/projects/:project_id/scripts +# The controller for apps pages /dashboard/projects/:project_id/launchers class LaunchersController < ApplicationController before_action :find_project @@ -14,14 +14,15 @@ class LaunchersController < ApplicationController :auto_batch_clusters, :auto_batch_clusters_exclude, :auto_batch_clusters_fixed, :bc_num_slots, :bc_num_slots_fixed, :bc_num_slots_min, :bc_num_slots_max, :bc_num_hours, :bc_num_hours_fixed, :bc_num_hours_min, :bc_num_hours_max, - :auto_job_name, :auto_job_name_fixed + :auto_job_name, :auto_job_name_fixed, + :auto_log_location, :auto_log_location_fixed ].freeze def new @script = Launcher.new(project_dir: @project.directory) end - # POST /dashboard/projects/:project_id/scripts + # POST /dashboard/projects/:project_id/launchers def create opts = { project_dir: @project.directory }.merge(create_script_params[:launcher]) @script = Launcher.new(opts) @@ -36,12 +37,12 @@ def create end end - # GET /projects/:project_id/scripts/:id/edit + # GET /projects/:project_id/launchers/:id/edit # edit def edit end - # DELETE /projects/:project_id/scripts/:id + # DELETE /projects/:project_id/launchers/:id def destroy if @script.destroy redirect_to project_path(params[:project_id]), notice: I18n.t('dashboard.jobs_scripts_deleted') @@ -50,8 +51,8 @@ def destroy end end - # POST /projects/:project_id/scripts/:id/save - # save the script after editing + # POST /projects/:project_id/launchers/:id/save + # save the launcher after editing def save @script.update(save_script_params[:launcher]) @@ -62,7 +63,7 @@ def save end end - # POST /projects/:project_id/scripts/:id/submit + # POST /projects/:project_id/launchers/:id/submit # submit the job def submit opts = submit_script_params[:launcher].to_h.symbolize_keys diff --git a/apps/dashboard/app/controllers/projects_controller.rb b/apps/dashboard/app/controllers/projects_controller.rb index f7d8f85542..cb77117e73 100644 --- a/apps/dashboard/app/controllers/projects_controller.rb +++ b/apps/dashboard/app/controllers/projects_controller.rb @@ -163,7 +163,7 @@ def stop_job private def templates - templates = Project.templates.map do |project| + Project.templates.map do |project| label = project.title data = { 'data-description' => project.description, @@ -171,12 +171,6 @@ def templates } [label, project.directory, data] end - - if templates.size.positive? - templates.prepend(['', '', { 'data-description': '', 'data-icon': '' }]) - else - [] - end end def project_params diff --git a/apps/dashboard/app/helpers/batch_connect/sessions_helper.rb b/apps/dashboard/app/helpers/batch_connect/sessions_helper.rb index 79823e7703..14bd4435b4 100644 --- a/apps/dashboard/app/helpers/batch_connect/sessions_helper.rb +++ b/apps/dashboard/app/helpers/batch_connect/sessions_helper.rb @@ -91,7 +91,7 @@ def relaunch(session) return unless batch_connect_app.valid? user_context = session.user_context - params = batch_connect_app.attributes.map{|attribute| ["batch_connect_session_context[#{attribute.id}]", user_context.fetch(attribute.id, '')]}.to_h + params = batch_connect_app.attributes.map{|attribute| ["batch_connect_session_context[#{attribute.id}]", user_context.fetch(attribute.id, nil)]}.to_h.compact title = "#{t('dashboard.batch_connect_sessions_relaunch_title')} #{session.title} #{t('dashboard.batch_connect_sessions_word')}" button_to( batch_connect_session_contexts_path(token: batch_connect_app.token), @@ -103,7 +103,7 @@ def relaunch(session) data: { toggle: "tooltip", placement: "left" }, params: params ) do - "#{fa_icon('sync', classes: nil, title: '')}".html_safe + "#{fa_icon('sync', classes: nil, title: nil)}".html_safe end end @@ -119,7 +119,7 @@ def edit(session) data: { toggle: "tooltip", placement: "left" }, params: {session_id: session.id} ) do - "#{fa_icon('pen', classes: nil, title: '')}".html_safe + "#{fa_icon('pen', classes: nil, title: nil)}".html_safe end end diff --git a/apps/dashboard/app/helpers/dashboard_helper.rb b/apps/dashboard/app/helpers/dashboard_helper.rb index d7515eafef..a963bcb485 100644 --- a/apps/dashboard/app/helpers/dashboard_helper.rb +++ b/apps/dashboard/app/helpers/dashboard_helper.rb @@ -42,12 +42,20 @@ def render_widget(widget) begin render partial: "widgets/#{widget}" rescue SyntaxError, StandardError => e - render partial: 'shared/widget_error', locals: { error: e, widget: widget.to_s } + render_error_widget(e, widget.to_s) + # rubocop:disable Lint/RescueException - because these can throw all sorts of errors. + rescue Exception => e + # rubocop:enable Lint/RescueException + render_error_widget(e, widget.to_s) end end private + def render_error_widget(error, widget_name) + render(partial: 'shared/widget_error', locals: { error: error, widget: widget_name }) + end + def default_dashboard_layout if xdmod? if pinned_apps? || motd? diff --git a/apps/dashboard/app/helpers/launchers_helper.rb b/apps/dashboard/app/helpers/launchers_helper.rb index 4ee86e1e6e..b186e49af2 100644 --- a/apps/dashboard/app/helpers/launchers_helper.rb +++ b/apps/dashboard/app/helpers/launchers_helper.rb @@ -70,6 +70,10 @@ def auto_cores_template create_editable_widget(script_form_double, attrib) end + def auto_log_location_template + attrib = SmartAttributes::AttributeFactory.build_auto_log_location + create_editable_widget(script_form_double, attrib) + end # We need a form builder to build the template divs. These are # templates so that they are not a part of the _actual_ form (yet). # Otherwise you'd have required fields that you cannot actually edit diff --git a/apps/dashboard/app/javascript/alert.js b/apps/dashboard/app/javascript/alert.js index 3817533008..b2ca7aac62 100644 --- a/apps/dashboard/app/javascript/alert.js +++ b/apps/dashboard/app/javascript/alert.js @@ -3,6 +3,7 @@ export function alert(message) { const div = alertDiv(message); const main = document.getElementById('main_container'); main.prepend(div); + div.scrollIntoView({ behavior: 'smooth' }); } function alertDiv(message) { diff --git a/apps/dashboard/app/javascript/dynamic_forms.js b/apps/dashboard/app/javascript/dynamic_forms.js index bc4d4b5628..a088382ad7 100644 --- a/apps/dashboard/app/javascript/dynamic_forms.js +++ b/apps/dashboard/app/javascript/dynamic_forms.js @@ -93,27 +93,13 @@ function mountainCaseWords(str) { function snakeCaseWords(str) { if(str === undefined) return undefined; - let snakeCase = ""; - - str.split('').forEach((c, index) => { - if(c === '-' || c === '_') { - snakeCase += '_'; - } else if (index == 0) { - snakeCase += c.toLowerCase(); - } else if(c == c.toUpperCase() && isNaN(c)) { - const nextIsUpper = (index + 1 !== str.length) ? str[index + 1] === str[index + 1].toUpperCase() : true; - const nextIsNum = !isNaN(str[index + 1]); - if ((str[index-1] === '_' || nextIsUpper) && !nextIsNum) { - snakeCase += c.toLowerCase(); - } else { - snakeCase += `_${c.toLowerCase()}`; - } - } else { - snakeCase += c; - } - }); + // find all the captial case words and if none are found, we'll just bascially + // return the same string. + const rex = /([A-Z]{1}[a-z]*[0-9]*)|.+/g; + const words = str.match(rex); - return snakeCase; + // filter out emtpy matches to avoid having a _ at the end. + return words.filter(word => word != '').map(word => word.toLowerCase()).join('_'); } /** diff --git a/apps/dashboard/app/javascript/files/data_table.js b/apps/dashboard/app/javascript/files/data_table.js index 0f4ec18cdc..209abd6261 100644 --- a/apps/dashboard/app/javascript/files/data_table.js +++ b/apps/dashboard/app/javascript/files/data_table.js @@ -61,7 +61,7 @@ jQuery(function () { $('#select_all').on('click', function() { if ($(this).is(":checked")) { - table.getTable().rows().select(); + table.getTable().rows({ search: 'applied' }).select(); } else { table.getTable().rows().deselect(); } diff --git a/apps/dashboard/app/javascript/launcher_edit.js b/apps/dashboard/app/javascript/launcher_edit.js index 013a2c3d58..738ea6b3d1 100644 --- a/apps/dashboard/app/javascript/launcher_edit.js +++ b/apps/dashboard/app/javascript/launcher_edit.js @@ -19,6 +19,10 @@ const newFieldData = { label: "Job Name", help: "The name the job will have." }, + auto_log_location: { + label: "Log Location", + help: "The destination of the job's log output." + }, bc_num_slots: { label: "Nodes", help: "How many nodes the job will run on." @@ -166,16 +170,38 @@ function addInProgressField(event) { function updateAutoEnvironmentVariable(event) { const aev_name = event.target.value; const labelString = event.target.dataset.labelString; + const idString = `launcher_auto_environment_variable_${aev_name}`; + const nameString = `launcher[auto_environment_variable_${aev_name}]`; var input_field = event.target.parentElement.children[2].children[1]; input_field.removeAttribute('readonly'); - input_field.id = `launcher_auto_environment_variable_${aev_name}`; - input_field.name = `launcher[auto_environment_variable_${aev_name}]`; + input_field.id = idString; + input_field.name = nameString; if (labelString.match(/Environment( |\s)Variable/)) { const label_field = event.target.parentElement.children[2].children[0]; label_field.innerHTML = `Environment Variable: ${aev_name}`; } + + // Update the checkbox so that environment variables can be fixed when created + let fixedBoxGroup = event.target.parentElement.children[3].children[0].children[0]; + + let checkbox = fixedBoxGroup.children[0]; + checkbox.id = `${idString}_fixed`; + checkbox.name = `launcher[auto_environment_variable_${aev_name}_fixed]`; + checkbox.setAttribute('data-fixed-toggler', idString); + + // Update hidden field if attribute is already fixed, otherwise just update label + let labelIndex = 2; + if(fixedBoxGroup.children.length == 3) { + let hiddenField = fixedBoxGroup.children[1]; + hiddenField.name = nameString; + } else { + labelIndex = 1; + } + + let fixedLabel = fixedBoxGroup.children[labelIndex]; + fixedLabel.setAttribute('for', `${idString}_fixed`); } function fixExcludeBasedOnSelect(selectElement) { @@ -258,12 +284,6 @@ function enableOrDisableSelectOption(event) { const optionToToggle = selectOptions.filter(opt => opt.text == choice)[0]; const selectOptionsEnabled = selectOptions.filter(opt => !opt.disabled); - if(selectOptionsEnabled.length <= 1 && toggleAction == 'remove') { - alert("Cannot remove the last option available") - event.target.disabled = false; - return - } - if(toggleAction == 'add') { enableRemoveOption(li); removeFromExcludeInput(excludeId, choice); @@ -278,6 +298,28 @@ function enableOrDisableSelectOption(event) { selectOptionsEnabled.filter(opt => opt.text !== choice)[0].selected = true; } } + enableOrDisableLastOption(li.parentElement); +} + +function enableOrDisableLastOption(optionsOl) { + const optionLis = Array.from(optionsOl.children); + + const optionsEnabled = Array.from(optionLis.filter((child) => { + return !child.classList.contains('text-strike'); + })); + + if(optionsEnabled.length > 1) { + // Make sure there are no options that have both the add and remove button disabled + const bothButtonsDisabled = optionsEnabled.filter((option) => { + return option.querySelectorAll('button:disabled').length == 2; + }); + for(const option of bothButtonsDisabled) { + enableRemoveOption(option); + } + } else { + // Disable the remove button on the last option + enableRemoveOption(optionsEnabled[0], true); + } } function getExcludeList(excludeElementId) { @@ -329,6 +371,8 @@ function initSelect(selectElement) { enableAddOption(configItem); } }); + + enableOrDisableLastOption(selectOptionsConfig[0].parentElement); } diff --git a/apps/dashboard/app/javascript/turbo_shim.js b/apps/dashboard/app/javascript/turbo_shim.js index 17494d2967..4c12a473ac 100644 --- a/apps/dashboard/app/javascript/turbo_shim.js +++ b/apps/dashboard/app/javascript/turbo_shim.js @@ -6,6 +6,7 @@ */ import { setInnerHTML } from './utils'; +import { alert } from './alert'; export function replaceHTML(id, html) { const ele = document.getElementById(id); @@ -24,7 +25,15 @@ export function replaceHTML(id, html) { export function pollAndReplace(url, delay, id, callback) { fetch(url, { headers: { Accept: "text/vnd.turbo-stream.html" } }) - .then(response => response.ok ? Promise.resolve(response) : Promise.reject(response.text())) + .then((response) => { + if(response.status == 200) { + return Promise.resolve(response); + } else if(response.status == 401) { + return Promise.reject("This page cannot update becase you are no longer authenticated. Please refresh the page to log back in.") + } else { + return Promise.reject(response.text()); + } + }) .then((r) => r.text()) .then((html) => replaceHTML(id, html)) .then(() => { @@ -34,7 +43,11 @@ export function pollAndReplace(url, delay, id, callback) { } }) .catch((err) => { - console.log('Cannot retrieve partial due to error:'); + if (typeof err == 'string') { + alert(err); + } else { + alert('This page has encountered an unexpected error. Please refresh the page.'); + } console.log(err); }); } diff --git a/apps/dashboard/app/javascript/utils.js b/apps/dashboard/app/javascript/utils.js index 6e13fb5e0f..913a850760 100644 --- a/apps/dashboard/app/javascript/utils.js +++ b/apps/dashboard/app/javascript/utils.js @@ -1,3 +1,4 @@ +import {analyticsPath} from "./config"; export function cssBadgeForState(state){ switch (state) { @@ -60,11 +61,17 @@ export function bindFullPageSpinnerEvent() { // open links in javascript and display an alert export function openLinkInJs(event) { event.preventDefault(); - const href = event.target.href; + let href = event.target.href; - // do nothing if there's no href. - if(href == null){ - return; + // event.target could be a child of the anchor, so try that. + if(href == null) { + const closestAnchor = event.target.closest('a'); + if(closestAnchor.hasChildNodes(event.target)) { + href = closestAnchor.href; + } else { + // event.target is not a child of an anhcor, so there's nothing to do. + return; + } } if(window.open(href) == null) { @@ -102,3 +109,12 @@ export function setInnerHTML(element, html) { currentElement.parentNode.replaceChild(newElement, currentElement); }); } + +// Helper method to report errors from the front end via AJAX +export function reportErrorForAnalytics(path, error) { + // error - report back for analytics purposes + const analyticsUrl = new URL(analyticsPath(path), document.location); + analyticsUrl.searchParams.append('error', error); + // Fire and Forget + fetch(analyticsUrl); +} diff --git a/apps/dashboard/app/javascript/xdmod.js b/apps/dashboard/app/javascript/xdmod.js index 01371a4a04..7715bfaced 100644 --- a/apps/dashboard/app/javascript/xdmod.js +++ b/apps/dashboard/app/javascript/xdmod.js @@ -1,13 +1,14 @@ import _ from 'lodash'; import {xdmodUrl, analyticsPath} from './config'; -import {today, startOfYear, thirtyDaysAgo} from './utils'; +import {today, startOfYear, thirtyDaysAgo, reportErrorForAnalytics} from './utils'; import { jobsPanel } from './xdmod/jobs'; import Handlebars from 'handlebars'; const jobsPageLimit = 10; const jobHelpers = { + realm: 'Jobs', title: function(){ return "Recently Completed Jobs"; }, @@ -44,19 +45,19 @@ const jobHelpers = { return `${month}/${day}`; }, - job_url: function(id){ return `${xdmodUrl()}/#job_viewer?action=show&realm=SUPREMM&jobref=${id}`; }, - cpu_label: function(cpu){ - let value = (parseFloat(cpu)*100).toFixed(1), - label = "N/A"; + job_url: function(id){ return `${xdmodUrl()}/#job_viewer?action=show&realm=${this.realm}&jobref=${id}`; }, + efficiency_label: function(efficiencyValue, inverse = false){ + const value = (parseFloat(efficiencyValue)*100).toFixed(1); + let label = "N/A"; if(! isNaN(value)){ let severity = "warning"; - if(cpu > 0.74){ - severity = "success"; + if(efficiencyValue > 0.74){ + severity = inverse ? "danger" : "success"; } - else if(cpu < 0.25){ - severity = "danger"; + else if(efficiencyValue < 0.25){ + severity = inverse ? "success" : "danger"; } label = `${Handlebars.escapeExpression(value.toString().padStart(4,0))}`; @@ -84,12 +85,12 @@ var efficiencyHelpers = { } }; -function promiseLoginToXDMoD(xdmodUrl){ +function promiseLoginToXDMoD(){ return new Promise(function(resolve, reject){ var promise_to_receive_message_from_iframe = new Promise(function(resolve, reject){ window.addEventListener("message", function(event){ - if (event.origin !== xdmodUrl){ + if (event.origin !== xdmodUrl()){ console.log('Received message from untrusted origin, discarding'); return; } @@ -106,8 +107,8 @@ function promiseLoginToXDMoD(xdmodUrl){ }, false); }); - fetch(xdmodUrl + '/rest/auth/idpredirect?returnTo=%2Fgui%2Fgeneral%2Flogin.php') - .then(response => response.ok ? Promise.resolve(response) : Promise.reject()) + fetch(xdmodUrl() + '/rest/auth/idpredirect?returnTo=%2Fgui%2Fgeneral%2Flogin.php') + .then(response => response.ok ? Promise.resolve(response) : Promise.reject(new Error('Login failed: IDP redirect failed'))) .then(response => response.json()) .then(function(data){ return new Promise(function(resolve, reject){ @@ -153,6 +154,7 @@ var promiseLoggedIntoXDMoD = (function(){ }) .then((user_data) => { if(user_data && user_data.success && user_data.results && user_data.results.person_id){ + jobHelpers.realm = user_data.results.raw_data_allowed_realms?.includes('SUPREMM') ? 'SUPREMM' : 'Jobs'; return Promise.resolve(user_data); } else{ @@ -169,7 +171,7 @@ function jobsUrl(user){ url.searchParams.set('_dc', Date.now()); url.searchParams.set('start_date', thirtyDaysAgo()); url.searchParams.set('end_date', today()); - url.searchParams.set('realm', user?.results?.raw_data_allowed_realms?.includes('SUPREMM') ? 'SUPREMM' : 'Jobs'); + url.searchParams.set('realm', jobHelpers.realm); url.searchParams.set('limit', jobsPageLimit); url.searchParams.set('start', 0); url.searchParams.set('verbose', true); @@ -239,10 +241,7 @@ function createJobsWidget() { console.error(error); renderJobs({error: error}); - // error - report back for analytics purposes - const analyticsUrl = new URL(analyticsPath('xdmod_jobs_widget_error'), document.location); - analyticsUrl.searchParams.append('error', error); - fetch(analyticsUrl); + reportErrorForAnalytics('xdmod_jobs_widget_error', error); }); } @@ -254,7 +253,7 @@ function createEfficiencyWidgets() { return; } - promiseLoggedIntoXDMoD(xdmodUrl) + promiseLoggedIntoXDMoD() .then((user_data) => fetch(aggregateDataUrl(user_data), { credentials: 'include' })) .then(response => response.ok ? Promise.resolve(response) : Promise.reject(new Error(response.statusText))) .then(response => response.json()) @@ -287,17 +286,16 @@ function createEfficiencyWidgets() { renderJobsEfficiency({error: error}); renderCoreHoursEfficiency({error: error}); - // error - report back for analytics purposes - const analyticsUrl = new URL(analyticsPath('xdmod_jobs_widget_error'), document.location); - analyticsUrl.searchParams.append('error', error); - fetch(analyticsUrl); + reportErrorForAnalytics('xdmod_jobs_widget_error', error); }); } jQuery(() => { - createJobsWidget(); - createEfficiencyWidgets(); - // initialize the panels renderJobs({ loading: true }); -}); \ No newline at end of file + renderJobsEfficiency({nodata: true, msg: 'LOADING...'}); + renderCoreHoursEfficiency({nodata: true, msg: 'LOADING...'}); + + createJobsWidget(); + createEfficiencyWidgets(); +}); diff --git a/apps/dashboard/app/javascript/xdmod/jobs.js b/apps/dashboard/app/javascript/xdmod/jobs.js index ac32aa4ee1..5c48fc8fbe 100644 --- a/apps/dashboard/app/javascript/xdmod/jobs.js +++ b/apps/dashboard/app/javascript/xdmod/jobs.js @@ -1,5 +1,7 @@ 'use strict'; +import {reportErrorForAnalytics} from '../utils'; + export function jobsPanel(context, helpers){ const div = document.createElement('div'); div.classList.add('xdmod'); @@ -77,15 +79,17 @@ function table(context, helpers) { //
ID | \ -Name | \ -Date | \ -CPU | \ +Analytics Toggle | \ +ID | \ +Name | \ +Date | \ +Analytics | \
---|---|---|---|---|---|---|---|---|
{{local_job_id}} | @@ -113,6 +117,19 @@ function tableRows(context, helpers) { jobs.forEach(job => { const tr = document.createElement('tr'); tr.title = `${job.job_name} - ${job.local_job_id}`; + // Job Analytics metadata => Required for the AJAX request and collapse behaviour + tr.setAttribute('data-xdmod-jobid', job.jobid); + tr.setAttribute('data-bs-toggle', 'collapse'); + tr.setAttribute('data-bs-target', `#details_${job.jobid}`); + tr.setAttribute('aria-expanded', 'false'); + + // Job analytics collapse icons + const td0 = document.createElement('td'); + td0.innerHTML = ` + ` //// {{local_job_id}} @@ -132,10 +149,21 @@ function tableRows(context, helpers) { td3.innerText = helpers.date(job); // | {{cpu_label cpu_user}} | + // Not used with new analytics data + // const td4 = document.createElement('td'); + // td4.innerHTML = helpers.efficiency_label(job.cpu_user); + + // Add job analytics placeholder const td4 = document.createElement('td'); - td4.innerHTML = helpers.cpu_label(job.cpu_user); + td4.id = `details_${job.jobid}`; + td4.classList.add('job-analytics', 'collapse'); + td4.innerHTML = '