From: Tom Clegg Date: Fri, 3 May 2024 17:33:28 +0000 (-0400) Subject: Merge branch '15397-remove-obsolete-apis' X-Git-Url: https://git.arvados.org/arvados.git/commitdiff_plain/HEAD?hp=e20590d485505f58f7745d74a311ca539c9be940 Merge branch '15397-remove-obsolete-apis' refs #15397 Arvados-DCO-1.1-Signed-off-by: Tom Clegg --- diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000000..f8224e417f --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,40 @@ +# Copyright (C) The Arvados Authors. All rights reserved. +# +# SPDX-License-Identifier: AGPL-3.0 + +name: Arvados Tests + +on: + workflow_dispatch: + pull_request: + branches: + - main + +jobs: + workbench2: + name: Workbench2 Tests + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + - name: Setup buildx + uses: docker/setup-buildx-action@d70bba72b1f3fd22344832f00baa16ece964efeb # v3.3.0 + - name: Build wb2 test container + uses: docker/build-push-action@2cdde995de11925a030ce8070c3d77a52ffcf1c0 # v5.3.0 + with: + context: . + file: "services/workbench2/docker/Dockerfile" + tags: workbench2-test:latest + load: true + cache-from: type=gha + cache-to: type=gha,mode=max + push: false + - name: Run wb2 integration tests + uses: addnab/docker-run-action@4f65fabd2431ebc8d299f8e5a018d79a769ae185 # v3 + with: + image: workbench2-test:latest + options: -v ${{github.workspace}}:/usr/src/arvados -w /usr/src/arvados/services/workbench2 + run: | + yarn install + yarn test --no-watchAll --bail --ci || exit $? + tools/run-integration-tests.sh -a /usr/src/arvados diff --git a/.gitignore b/.gitignore index c156018036..2b98a71967 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,9 @@ docker/*/generated docker/config.yml doc/.site doc/sdk/python/arvados +doc/sdk/python/arvados.html +doc/sdk/python/index.html +doc/sdk/python/search.js doc/sdk/R/arvados doc/sdk/java-v2/javadoc */vendor @@ -32,3 +35,4 @@ _version.py arvados-snakeoil-ca.pem .vagrant packages +.eslintcache diff --git a/.licenseignore b/.licenseignore index 6ddb5c009c..1e1c12a53a 100644 --- a/.licenseignore +++ b/.licenseignore @@ -1,19 +1,19 @@ *agpl-3.0.html *agpl-3.0.txt apache-2.0.txt -apps/workbench/app/assets/javascripts/list.js -apps/workbench/public/webshell/* AUTHORS */bootstrap.css */bootstrap.js *bootstrap-theme.css build/package-test-dockerfiles/centos7/localrepo.repo +build/package-test-dockerfiles/rocky8/localrepo.repo build/package-test-dockerfiles/ubuntu1604/etc-apt-preferences.d-arvados *by-sa-3.0.html *by-sa-3.0.txt *COPYING doc/fonts/* doc/_includes/_config_default_yml.liquid +doc/_includes/_terraform_*_tfvars.liquid doc/user/cwl/federated/* doc/_includes/_federated_cwl.liquid */docker_image @@ -53,6 +53,8 @@ sdk/cwl/tests/tool/blub.txt sdk/cwl/tests/19109-upload-secondary/* sdk/cwl/tests/federation/data/* sdk/cwl/tests/fake-keep-mount/fake_collection_dir/.arvados#collection +sdk/cwl/tests/container_request_9tee4-xvhdp-kk0ja1cl8b2kr1y-arv-mount.txt +sdk/cwl/tests/container_request_9tee4-xvhdp-kk0ja1cl8b2kr1y-crunchstat.txt sdk/go/manifest/testdata/*_manifest sdk/java/.classpath sdk/java/pom.xml @@ -92,4 +94,44 @@ sdk/cwl/tests/wf/hello.txt sdk/cwl/tests/wf/indir1/hello2.txt sdk/cwl/tests/chipseq/data/Genomes/* CITATION.cff -SECURITY.md \ No newline at end of file +SECURITY.md +lib/crunchstat/testdata/* +lib/controller/localdb/testdata/*.pub +sdk/ruby-google-api-client/* +services/api/bin/rails +services/api/bin/rake +services/api/bin/setup +services/api/bin/yarn +services/api/storage.yml +services/api/test.rb.example +services/api/config/boot.rb +services/api/config/environment.rb +services/api/config/initializers/application_controller_renderer.rb +services/api/config/initializers/assets.rb +services/api/config/initializers/backtrace_silencers.rb +services/api/config/initializers/content_security_policy.rb +services/api/config/initializers/cookies_serializer.rb +services/api/config/initializers/filter_parameter_logging.rb +services/api/config/initializers/mime_types.rb +services/api/config/initializers/new_framework_defaults_*.rb +services/api/config/initializers/permissions_policy.rb +services/api/config/initializers/wrap_parameters.rb +services/api/config/locales/en.yml +services/api/config.ru +services/workbench2/*.d.ts +services/workbench2/*.css +services/workbench2/*.scss +services/workbench2/README.md +services/workbench2/public/* +services/workbench2/.yarnrc +services/workbench2/.npmrc +services/workbench2/src/lib/cwl-svg/* +services/workbench2/tools/arvados_config.yml +services/workbench2/cypress/fixtures/files/5mb.bin +services/workbench2/cypress/fixtures/files/cat.png +services/workbench2/cypress/fixtures/files/banner.html +services/workbench2/cypress/fixtures/files/tooltips.txt +services/workbench2/cypress/fixtures/webdav-propfind-outputs.xml +services/workbench2/.yarn/releases/* +services/workbench2/package.json +services/workbench2/yarn.lock diff --git a/AUTHORS b/AUTHORS index fa9fa86d34..cb09dc67ae 100644 --- a/AUTHORS +++ b/AUTHORS @@ -22,3 +22,4 @@ Curii Corporation <*@curii.com> Dante Tsang Codex Genetics Ltd Bruno P. Kinoshita +George Chlipala diff --git a/apps/workbench/.gitignore b/apps/workbench/.gitignore deleted file mode 100644 index fa42a32dd9..0000000000 --- a/apps/workbench/.gitignore +++ /dev/null @@ -1,51 +0,0 @@ -# Ignore the default SQLite database. -/db/*.sqlite3 - -# Ignore all logfiles and tempfiles. -/log/*.log -/log/*.log.gz -/tmp -.byebug_history - -package-lock.json - -/config/.secret_token -/config/initializers/secret_token.rb - -/public/assets - -/config/environments/development.rb -/config/environments/production.rb -/config/application.yml - -# Workbench doesn't need one anyway, so this shouldn't come up, but... -/config/database.yml - -/config/piwik.yml - -# Capistrano files are coming from another repo -/Capfile* -/config/deploy* - -# Themes are coming from another repo -/themes/* - -# This can be a symlink to ../../../doc/.site in dev setups -/public/doc - -# SimpleCov reports -/coverage - -# Dev/test SSL certificates -/self-signed.key -/self-signed.pem - -# Generated git-commit.version file -/git-commit.version - -# npm-rails -/node_modules -/npm-debug.log - -# Generated when building distribution packages -/package-build.version diff --git a/apps/workbench/Gemfile b/apps/workbench/Gemfile deleted file mode 100644 index 00dbad0860..0000000000 --- a/apps/workbench/Gemfile +++ /dev/null @@ -1,110 +0,0 @@ -# Copyright (C) The Arvados Authors. All rights reserved. -# -# SPDX-License-Identifier: AGPL-3.0 - -source 'https://rubygems.org' - -gem 'rails', '~> 5.2.0' -gem 'arvados', '~> 2.1.5' - -gem 'activerecord-nulldb-adapter', git: 'https://github.com/arvados/nulldb' -gem 'multi_json' -gem 'oj' -gem 'sass' -gem 'mime-types' -gem 'responders', '~> 2.0' - -# Pin sprockets to < 4.0 to avoid issues when upgrading rails to 5.2 -# See: https://github.com/rails/sprockets-rails/issues/443 -gem 'sprockets', '~> 3.0' - -# Gems used only for assets and not required -# in production environments by default. -group :assets do - gem 'sassc-rails' - gem 'uglifier', '~> 2.0' - - # See https://github.com/rails/execjs#readme for more supported runtimes -end - -group :development, :test, :performance do - gem 'byebug' - # Pinning launchy because 2.5 requires ruby >= 2.4, which arvbox currently - # doesn't have because of SSO. - gem 'launchy', '~> 2.4.0' -end - -group :development do - gem 'ruby-debug-passenger' - gem 'rack-mini-profiler', require: false - gem 'flamegraph', require: false - #gem 'web-console', '~> 2.0' -end - -group :test, :diagnostics, :performance do - gem 'minitest', '~> 5.10.3' - gem 'selenium-webdriver', '~> 3' - gem 'capybara', '~> 2.5.0' - gem 'poltergeist', '~> 1.5.1' - gem 'headless', '~> 1.0.2' -end - -group :test, :performance do - gem 'rails-perftest' - gem 'ruby-prof' - gem 'rvm-capistrano' - # Note: "require: false" here tells bunder not to automatically - # 'require' the packages during application startup. Installation is - # still mandatory. - gem 'simplecov', '~> 0.7', require: false - gem 'simplecov-rcov', require: false - gem 'mocha', require: false - gem 'rails-controller-testing' -end - -gem 'jquery-rails' -gem 'bootstrap-sass', '~> 3.4.1' -gem 'bootstrap-x-editable-rails' -gem 'bootstrap-tab-history-rails' - -gem 'angularjs-rails', '~> 1.3.8' - -gem 'sshkey' - -# To use ActiveModel has_secure_password -# gem 'bcrypt-ruby', '~> 3.0.0' - -# To use Jbuilder templates for JSON -# gem 'jbuilder' - -# Use unicorn as the app server -# gem 'unicorn' - -# Deploy with Capistrano -# gem 'capistrano' - -gem 'passenger', :group => :production -gem 'andand' -gem 'RedCloth' - -gem 'piwik_analytics' -gem 'httpclient', '~> 2.5' - -# This fork has Rails 4 compatible routes -gem 'themes_for_rails', git: 'https://github.com/arvados/themes_for_rails' - -gem "deep_merge", :require => 'deep_merge/rails_compat' - -gem 'morrisjs-rails' -gem 'raphael-rails' - -gem 'lograge' -gem 'logstash-event' - -gem 'safe_yaml' - -gem 'npm-rails' - -# arvados-google-api-client and googleauth (and thus arvados) gems -# depend on signet, but signet 0.12 is incompatible with ruby 2.3. -gem 'signet', '< 0.12' diff --git a/apps/workbench/Gemfile.lock b/apps/workbench/Gemfile.lock deleted file mode 100644 index a22214a28d..0000000000 --- a/apps/workbench/Gemfile.lock +++ /dev/null @@ -1,359 +0,0 @@ -GIT - remote: https://github.com/arvados/nulldb - revision: d8e0073b665acdd2537c5eb15178a60f02f4b413 - specs: - activerecord-nulldb-adapter (0.3.9) - activerecord (>= 2.0.0) - -GIT - remote: https://github.com/arvados/themes_for_rails - revision: ddf6e592b3b6493ea0c2de7b5d3faa120ed35be0 - specs: - themes_for_rails (0.5.1) - rails (>= 3.0.0) - -GEM - remote: https://rubygems.org/ - specs: - RedCloth (4.3.2) - actioncable (5.2.8.1) - actionpack (= 5.2.8.1) - nio4r (~> 2.0) - websocket-driver (>= 0.6.1) - actionmailer (5.2.8.1) - actionpack (= 5.2.8.1) - actionview (= 5.2.8.1) - activejob (= 5.2.8.1) - mail (~> 2.5, >= 2.5.4) - rails-dom-testing (~> 2.0) - actionpack (5.2.8.1) - actionview (= 5.2.8.1) - activesupport (= 5.2.8.1) - rack (~> 2.0, >= 2.0.8) - rack-test (>= 0.6.3) - rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.0, >= 1.0.2) - actionview (5.2.8.1) - activesupport (= 5.2.8.1) - builder (~> 3.1) - erubi (~> 1.4) - rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.0, >= 1.0.3) - activejob (5.2.8.1) - activesupport (= 5.2.8.1) - globalid (>= 0.3.6) - activemodel (5.2.8.1) - activesupport (= 5.2.8.1) - activerecord (5.2.8.1) - activemodel (= 5.2.8.1) - activesupport (= 5.2.8.1) - arel (>= 9.0) - activestorage (5.2.8.1) - actionpack (= 5.2.8.1) - activerecord (= 5.2.8.1) - marcel (~> 1.0.0) - activesupport (5.2.8.1) - concurrent-ruby (~> 1.0, >= 1.0.2) - i18n (>= 0.7, < 2) - minitest (~> 5.1) - tzinfo (~> 1.1) - addressable (2.8.0) - public_suffix (>= 2.0.2, < 5.0) - andand (1.3.3) - angularjs-rails (1.3.15) - arel (9.0.0) - arvados (2.1.5) - activesupport (>= 3) - andand (~> 1.3, >= 1.3.3) - arvados-google-api-client (>= 0.7, < 0.8.9) - faraday (< 0.16) - i18n (~> 0) - json (>= 1.7.7, < 3) - jwt (>= 0.1.5, < 2) - arvados-google-api-client (0.8.7.4) - activesupport (>= 3.2, < 5.3) - addressable (~> 2.3) - autoparse (~> 0.3) - extlib (~> 0.9) - faraday (~> 0.9) - googleauth (~> 0.3) - launchy (~> 2.4) - multi_json (~> 1.10) - retriable (~> 1.4) - signet (~> 0.6) - autoparse (0.3.3) - addressable (>= 2.3.1) - extlib (>= 0.9.15) - multi_json (>= 1.0.0) - autoprefixer-rails (9.5.1.1) - execjs - bootstrap-sass (3.4.1) - autoprefixer-rails (>= 5.2.1) - sassc (>= 2.0.0) - bootstrap-tab-history-rails (0.1.0) - railties (>= 3.1) - bootstrap-x-editable-rails (1.5.1.1) - railties (>= 3.0) - builder (3.2.4) - byebug (11.0.1) - capistrano (2.15.9) - highline - net-scp (>= 1.0.0) - net-sftp (>= 2.0.0) - net-ssh (>= 2.0.14) - net-ssh-gateway (>= 1.1.0) - capybara (2.5.0) - mime-types (>= 1.16) - nokogiri (>= 1.3.3) - rack (>= 1.0.0) - rack-test (>= 0.5.4) - xpath (~> 2.0) - childprocess (0.9.0) - ffi (~> 1.0, >= 1.0.11) - cliver (0.3.2) - concurrent-ruby (1.1.10) - crass (1.0.6) - deep_merge (1.2.1) - docile (1.3.1) - erubi (1.10.0) - execjs (2.7.0) - extlib (0.9.16) - faraday (0.15.4) - multipart-post (>= 1.2, < 3) - ffi (1.10.0) - flamegraph (0.9.5) - globalid (1.0.0) - activesupport (>= 5.0) - googleauth (0.9.0) - faraday (~> 0.12) - jwt (>= 1.4, < 3.0) - memoist (~> 0.16) - multi_json (~> 1.11) - os (>= 0.9, < 2.0) - signet (~> 0.7) - headless (1.0.2) - highline (2.0.2) - httpclient (2.8.3) - i18n (0.9.5) - concurrent-ruby (~> 1.0) - jquery-rails (4.3.3) - rails-dom-testing (>= 1, < 3) - railties (>= 4.2.0) - thor (>= 0.14, < 2.0) - json (2.5.1) - jwt (1.5.6) - launchy (2.4.3) - addressable (~> 2.3) - lograge (0.10.0) - actionpack (>= 4) - activesupport (>= 4) - railties (>= 4) - request_store (~> 1.0) - logstash-event (1.2.02) - loofah (2.19.1) - crass (~> 1.0.2) - nokogiri (>= 1.5.9) - mail (2.7.1) - mini_mime (>= 0.1.1) - marcel (1.0.2) - memoist (0.16.2) - metaclass (0.0.4) - method_source (1.0.0) - mime-types (3.2.2) - mime-types-data (~> 3.2015) - mime-types-data (3.2019.0331) - mini_mime (1.1.2) - mini_portile2 (2.8.0) - minitest (5.10.3) - mocha (1.8.0) - metaclass (~> 0.0.1) - morrisjs-rails (0.5.1.2) - railties (> 3.1, < 6) - multi_json (1.15.0) - multipart-post (2.1.1) - net-scp (2.0.0) - net-ssh (>= 2.6.5, < 6.0.0) - net-sftp (2.1.2) - net-ssh (>= 2.6.5) - net-ssh (5.2.0) - net-ssh-gateway (2.0.0) - net-ssh (>= 4.0.0) - nio4r (2.5.8) - nokogiri (1.13.10) - mini_portile2 (~> 2.8.0) - racc (~> 1.4) - npm-rails (0.2.1) - rails (>= 3.2) - oj (3.7.12) - os (1.1.1) - passenger (6.0.15) - rack - rake (>= 0.8.1) - piwik_analytics (1.0.2) - actionpack - activesupport - rails (>= 3.0.0) - poltergeist (1.5.1) - capybara (~> 2.1) - cliver (~> 0.3.1) - multi_json (~> 1.0) - websocket-driver (>= 0.2.0) - public_suffix (4.0.6) - racc (1.6.1) - rack (2.2.4) - rack-mini-profiler (1.0.2) - rack (>= 1.2.0) - rack-test (2.0.2) - rack (>= 1.3) - rails (5.2.8.1) - actioncable (= 5.2.8.1) - actionmailer (= 5.2.8.1) - actionpack (= 5.2.8.1) - actionview (= 5.2.8.1) - activejob (= 5.2.8.1) - activemodel (= 5.2.8.1) - activerecord (= 5.2.8.1) - activestorage (= 5.2.8.1) - activesupport (= 5.2.8.1) - bundler (>= 1.3.0) - railties (= 5.2.8.1) - sprockets-rails (>= 2.0.0) - rails-controller-testing (1.0.4) - actionpack (>= 5.0.1.x) - actionview (>= 5.0.1.x) - activesupport (>= 5.0.1.x) - rails-dom-testing (2.0.3) - activesupport (>= 4.2.0) - nokogiri (>= 1.6) - rails-html-sanitizer (1.4.4) - loofah (~> 2.19, >= 2.19.1) - rails-perftest (0.0.7) - railties (5.2.8.1) - actionpack (= 5.2.8.1) - activesupport (= 5.2.8.1) - method_source - rake (>= 0.8.7) - thor (>= 0.19.0, < 2.0) - rake (13.0.6) - raphael-rails (2.1.2) - rb-fsevent (0.10.3) - rb-inotify (0.10.0) - ffi (~> 1.0) - request_store (1.4.1) - rack (>= 1.4) - responders (2.4.1) - actionpack (>= 4.2.0, < 6.0) - railties (>= 4.2.0, < 6.0) - retriable (1.4.1) - ruby-debug-passenger (0.2.0) - ruby-prof (0.17.0) - rubyzip (1.3.0) - rvm-capistrano (1.5.6) - capistrano (~> 2.15.4) - safe_yaml (1.0.5) - sass (3.7.4) - sass-listen (~> 4.0.0) - sass-listen (4.0.0) - rb-fsevent (~> 0.9, >= 0.9.4) - rb-inotify (~> 0.9, >= 0.9.7) - sassc (2.0.1) - ffi (~> 1.9) - rake - sassc-rails (2.1.0) - railties (>= 4.0.0) - sassc (>= 2.0) - sprockets (> 3.0) - sprockets-rails - tilt - selenium-webdriver (3.141.0) - childprocess (~> 0.5) - rubyzip (~> 1.2, >= 1.2.2) - signet (0.11.0) - addressable (~> 2.3) - faraday (~> 0.9) - jwt (>= 1.5, < 3.0) - multi_json (~> 1.10) - simplecov (0.16.1) - docile (~> 1.1) - json (>= 1.8, < 3) - simplecov-html (~> 0.10.0) - simplecov-html (0.10.2) - simplecov-rcov (0.2.3) - simplecov (>= 0.4.1) - sprockets (3.7.2) - concurrent-ruby (~> 1.0) - rack (> 1, < 3) - sprockets-rails (3.4.2) - actionpack (>= 5.2) - activesupport (>= 5.2) - sprockets (>= 3.0.0) - sshkey (2.0.0) - thor (1.2.1) - thread_safe (0.3.6) - tilt (2.0.9) - tzinfo (1.2.10) - thread_safe (~> 0.1) - uglifier (2.7.2) - execjs (>= 0.3.0) - json (>= 1.8.0) - websocket-driver (0.7.5) - websocket-extensions (>= 0.1.0) - websocket-extensions (0.1.5) - xpath (2.1.0) - nokogiri (~> 1.3) - -PLATFORMS - ruby - -DEPENDENCIES - RedCloth - activerecord-nulldb-adapter! - andand - angularjs-rails (~> 1.3.8) - arvados (~> 2.1.5) - bootstrap-sass (~> 3.4.1) - bootstrap-tab-history-rails - bootstrap-x-editable-rails - byebug - capybara (~> 2.5.0) - deep_merge - flamegraph - headless (~> 1.0.2) - httpclient (~> 2.5) - jquery-rails - launchy (~> 2.4.0) - lograge - logstash-event - mime-types - minitest (~> 5.10.3) - mocha - morrisjs-rails - multi_json - npm-rails - oj - passenger - piwik_analytics - poltergeist (~> 1.5.1) - rack-mini-profiler - rails (~> 5.2.0) - rails-controller-testing - rails-perftest - raphael-rails - responders (~> 2.0) - ruby-debug-passenger - ruby-prof - rvm-capistrano - safe_yaml - sass - sassc-rails - selenium-webdriver (~> 3) - signet (< 0.12) - simplecov (~> 0.7) - simplecov-rcov - sprockets (~> 3.0) - sshkey - themes_for_rails! - uglifier (~> 2.0) - -BUNDLED WITH - 2.2.19 diff --git a/apps/workbench/README.textile b/apps/workbench/README.textile deleted file mode 100644 index 18380ac3fe..0000000000 --- a/apps/workbench/README.textile +++ /dev/null @@ -1,27 +0,0 @@ -###. Copyright (C) The Arvados Authors. All rights reserved. -.... -.... SPDX-License-Identifier: AGPL-3.0 - -h1. Developing Workbench - -This document includes information to help developers who would like to contribute to Workbench. If you just want to install it, please refer to our "Workbench installation guide":http://doc.arvados.org/install/install-workbench-app.html. - -h2. Running tests - -The Workbench application includes a series of integration tests. When you run these, it starts the API server in a test environment, with all of its fixtures loaded, then tests Workbench by starting that server and making requests against it. - -In order for this to work, you must have Firefox installed (or Iceweasel, if you're running Debian), as well as the X Virtual Frame Buffer driver. - -
-$ sudo apt-get install iceweasel xvfb
-
- -If you install the Workbench Bundle in deployment mode, you must also install the API server Bundle in deployment mode, and vice versa. If your Bundle installs have mismatched modes, the integration tests will fail with "Gem not found" errors. - -h2. Writing tests - -Integration tests are written with Capybara, which drives a fully-featured Web browser to interact with Workbench exactly as a user would. - -If your test requires JavaScript support, your test method should start with the line @Capybara.current_driver = Capybara.javascript_driver@. Otherwise, Capybara defaults to a simpler browser for speed. - -In most tests, you can directly call "Capybara's Session methods":http://rubydoc.info/github/jnicklas/capybara/Capybara/Session to drive the browser and check its state. If you need finer-grained control, refer to the "full Capybara documentation":http://rubydoc.info/github/jnicklas/capybara/Capybara. diff --git a/apps/workbench/Rakefile b/apps/workbench/Rakefile deleted file mode 100644 index 037f9013ac..0000000000 --- a/apps/workbench/Rakefile +++ /dev/null @@ -1,11 +0,0 @@ -#!/usr/bin/env rake -# Copyright (C) The Arvados Authors. All rights reserved. -# -# SPDX-License-Identifier: AGPL-3.0 - -# Add your own tasks in files placed in lib/tasks ending in .rake, -# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. - -require File.expand_path('../config/application', __FILE__) - -ArvadosWorkbench::Application.load_tasks diff --git a/apps/workbench/app/assets/images/dax.png b/apps/workbench/app/assets/images/dax.png deleted file mode 100644 index c511f0ec51..0000000000 Binary files a/apps/workbench/app/assets/images/dax.png and /dev/null differ diff --git a/apps/workbench/app/assets/images/mouse-move.gif b/apps/workbench/app/assets/images/mouse-move.gif deleted file mode 100644 index 497b1596dc..0000000000 Binary files a/apps/workbench/app/assets/images/mouse-move.gif and /dev/null differ diff --git a/apps/workbench/app/assets/images/pipeline-running.gif b/apps/workbench/app/assets/images/pipeline-running.gif deleted file mode 100644 index 64e9009133..0000000000 Binary files a/apps/workbench/app/assets/images/pipeline-running.gif and /dev/null differ diff --git a/apps/workbench/app/assets/images/rails.png b/apps/workbench/app/assets/images/rails.png deleted file mode 100644 index d5edc04e65..0000000000 Binary files a/apps/workbench/app/assets/images/rails.png and /dev/null differ diff --git a/apps/workbench/app/assets/images/spinner_32px.gif b/apps/workbench/app/assets/images/spinner_32px.gif deleted file mode 100644 index 3288d1035d..0000000000 Binary files a/apps/workbench/app/assets/images/spinner_32px.gif and /dev/null differ diff --git a/apps/workbench/app/assets/images/trash-icon.png b/apps/workbench/app/assets/images/trash-icon.png deleted file mode 100644 index 5c26c2450d..0000000000 Binary files a/apps/workbench/app/assets/images/trash-icon.png and /dev/null differ diff --git a/apps/workbench/app/assets/javascripts/add_group.js b/apps/workbench/app/assets/javascripts/add_group.js deleted file mode 100644 index 23de53d408..0000000000 --- a/apps/workbench/app/assets/javascripts/add_group.js +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright (C) The Arvados Authors. All rights reserved. -// -// SPDX-License-Identifier: AGPL-3.0 - -$(document).on('shown.bs.modal', '#add-group-modal', function(event) { - // Disable the submit button on modal loading - $submit = $('#add-group-submit'); - $submit.prop('disabled', true); - - $('input[type=text]', event.target).val(''); - $('#add-group-error', event.target).hide(); -}).on('input propertychange', '#group_name_input', function(event) { - group_name = $(event.target).val(); - $submit = $('#add-group-submit'); - $submit.prop('disabled', (group_name === null || group_name === "")); -}).on('submit', '#add-group-form', function(event) { - var $form = $(event.target), - $submit = $(':submit', $form), - $error = $('#add-group-error', $form), - group_name = $('input[name="group_name_input"]', $form).val(); - - $submit.prop('disabled', true); - - $error.hide(); - $.ajax('/groups', - {method: 'POST', - dataType: 'json', - data: {group: {name: group_name, group_class: 'role'}}, - context: $form}). - done(function(data, status, jqxhr) { - location.reload(); - }). - fail(function(jqxhr, status, error) { - var errlist = jqxhr.responseJSON.errors; - var errmsg; - if (Array.isArray(errlist)) { - errmsg = errlist.join(); - } else { - errmsg = ("The server returned an error when creating " + - "this group (status " + jqxhr.status + - ": " + errlist + ")."); - } - $error.text(errmsg); - $error.show(); - $submit.prop('disabled', false); - }); - return false; -}); diff --git a/apps/workbench/app/assets/javascripts/add_repository.js b/apps/workbench/app/assets/javascripts/add_repository.js deleted file mode 100644 index efcd19d32a..0000000000 --- a/apps/workbench/app/assets/javascripts/add_repository.js +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright (C) The Arvados Authors. All rights reserved. -// -// SPDX-License-Identifier: AGPL-3.0 - -$(document).on('shown.bs.modal', '#add-repository-modal', function(event) { - $('input[type=text]', event.target).val(''); - $('#add-repository-error', event.target).hide(); -}).on('submit', '#add-repository-form', function(event) { - var $form = $(event.target), - $submit = $(':submit', $form), - $error = $('#add-repository-error', $form), - repo_owner_uuid = $('input[name="add_repo_owner_uuid"]', $form).val(), - repo_prefix = $('input[name="add_repo_prefix"]', $form).val(), - repo_basename = $('input[name="add_repo_basename"]', $form).val(); - - $submit.prop('disabled', true); - $error.hide(); - $.ajax('/repositories', - {method: 'POST', - dataType: 'json', - data: {repository: {owner_uuid: repo_owner_uuid, - name: repo_prefix + repo_basename}}, - context: $form}). - done(function(data, status, jqxhr) { - location.reload(); - }). - fail(function(jqxhr, status, error) { - var errlist = jqxhr.responseJSON.errors; - var errmsg; - if (Array.isArray(errlist)) { - errmsg = errlist.join(); - } else { - errmsg = ("The server returned an error when making " + - "this repository (status " + jqxhr.status + - ": " + errlist + ")."); - } - $error.text(errmsg); - $error.show(); - $submit.prop('disabled', false); - }); - return false; -}); diff --git a/apps/workbench/app/assets/javascripts/ajax_error.js b/apps/workbench/app/assets/javascripts/ajax_error.js deleted file mode 100644 index dd31cc6dbc..0000000000 --- a/apps/workbench/app/assets/javascripts/ajax_error.js +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (C) The Arvados Authors. All rights reserved. -// -// SPDX-License-Identifier: AGPL-3.0 - -$(document).on('ajax:error', function(e, xhr, status, error) { - var errorMessage = '' + status + ': ' + error; - // $btn is the element (button/link) that initiated the failed request. - var $btn = $(e.target); - // Populate some elements with the error text (e.g., a

in an alert div) - $($btn.attr('data-on-error-write')).text(errorMessage); - // Show some elements (e.g., an alert div) - $($btn.attr('data-on-error-show')).show(); - // Hide some elements (e.g., a success/normal div) - $($btn.attr('data-on-error-hide')).hide(); -}).on('ajax:success', function(e) { - var $btn = $(e.target); - $($btn.attr('data-on-success-show')).show(); - $($btn.attr('data-on-success-hide')).hide(); -}); diff --git a/apps/workbench/app/assets/javascripts/angular_shim.js b/apps/workbench/app/assets/javascripts/angular_shim.js deleted file mode 100644 index 5da67285b1..0000000000 --- a/apps/workbench/app/assets/javascripts/angular_shim.js +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright (C) The Arvados Authors. All rights reserved. -// -// SPDX-License-Identifier: AGPL-3.0 - -// Compile any new HTML content that was loaded via jQuery.ajax(). -// Currently this only works for tabs, and only because they emit an -// arv:pane:loaded event after updating the DOM. - -$(document).on('arv:pane:loaded', function(event, $updatedElement) { - if (angular && $updatedElement && angular.element($updatedElement).injector()) { - angular.element($updatedElement).injector().invoke([ - '$compile', function($compile) { - var scope = angular.element($updatedElement).scope(); - $compile($updatedElement)(scope); - }]); - } -}); diff --git a/apps/workbench/app/assets/javascripts/application.js b/apps/workbench/app/assets/javascripts/application.js deleted file mode 100644 index 1898128133..0000000000 --- a/apps/workbench/app/assets/javascripts/application.js +++ /dev/null @@ -1,261 +0,0 @@ -// Copyright (C) The Arvados Authors. All rights reserved. -// -// SPDX-License-Identifier: AGPL-3.0 -// -// This is a manifest file that'll be compiled into application.js, which will include all the files -// listed below. -// -// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts, -// or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path. -// -// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the -// the compiled file. -// -// WARNING: THE FIRST BLANK LINE MARKS THE END OF WHAT'S TO BE PROCESSED, ANY BLANK LINE SHOULD -// GO AFTER THE REQUIRES BELOW. -// -//= require jquery -//= require jquery_ujs -//= require bootstrap -//= require bootstrap/dropdown -//= require bootstrap/tab -//= require bootstrap/tooltip -//= require bootstrap/popover -//= require bootstrap/collapse -//= require bootstrap/modal -//= require bootstrap/button -//= require bootstrap3-editable/bootstrap-editable -//= require bootstrap-tab-history -//= require angular -//= require raphael -//= require morris -//= require jquery.number.min -//= require npm-dependencies -//= require mithril/stream/stream -//= require awesomplete -//= require jssha -//= require_tree . - -Es6ObjectAssign.polyfill() -window.m = Object.assign(window.Mithril, {stream: window.m.stream}) - -jQuery(function($){ - $(document).ajaxStart(function(){ - $('.modal-with-loading-spinner .spinner').show(); - }).ajaxStop(function(){ - $('.modal-with-loading-spinner .spinner').hide(); - }); - - $('[data-toggle=tooltip]').tooltip(); - - $('.expand-collapse-row').on('click', function(event) { - var targets = $('#' + $(this).attr('data-id')); - if (targets.css('display') == 'none') { - $(this).addClass('icon-minus-sign'); - $(this).removeClass('icon-plus-sign'); - } else { - $(this).addClass('icon-plus-sign'); - $(this).removeClass('icon-minus-sign'); - } - targets.fadeToggle(200); - }); - - var ajaxCount = 0; - - $(document). - on('ajax:send', function(e, xhr) { - ajaxCount += 1; - if (ajaxCount == 1) { - $('.loading').fadeTo('fast', 1); - } - }). - on('ajax:complete', function(e, status) { - ajaxCount -= 1; - if (ajaxCount == 0) { - $('.loading').fadeOut('fast', 0); - } - }). - on('ajaxSend', function(e, xhr) { - // jQuery triggers 'ajaxSend' event when starting an ajax call, but - // rails-generated ajax triggers generate 'ajax:send'. Workbench - // event listeners currently expect 'ajax:send', so trigger the - // rails event in response to the jQuery one. - $(document).trigger('ajax:send'); - }). - on('ajaxComplete', function(e, xhr) { - // See comment above about ajaxSend/ajax:send - $(document).trigger('ajax:complete'); - }). - on('click', '.removable-tag a', function(e) { - var tag_span = $(this).parents('[data-tag-link-uuid]').eq(0) - tag_span.fadeTo('fast', 0.2); - $.ajax('/links/' + tag_span.attr('data-tag-link-uuid'), - {dataType: 'json', - type: 'POST', - data: { '_method': 'DELETE' }, - context: tag_span}). - done(function(data, status, jqxhr) { - this.remove(); - }). - fail(function(jqxhr, status, error) { - this.addClass('label-danger').fadeTo('fast', '1'); - }); - return false; - }). - on('click', 'a.add-tag-button', function(e) { - var jqxhr; - var new_tag_uuid = 'new-tag-' + Math.random(); - var tag_head_uuid = $(this).parents('tr').attr('data-object-uuid'); - var new_tag = window.prompt("Add tag for collection "+ - tag_head_uuid, - ""); - if (new_tag == null) - return false; - var new_tag_span = - $(''). - attr('data-tag-link-uuid', new_tag_uuid). - text(new_tag). - css('opacity', '0.2'). - append(' '); - $(this). - parent(). - find('>span'). - append(new_tag_span). - append(' '); - $.ajax($(this).attr('data-remote-href'), - {dataType: 'json', - type: $(this).attr('data-remote-method'), - data: { - 'link[head_uuid]': tag_head_uuid, - 'link[link_class]': 'tag', - 'link[name]': new_tag - }, - context: new_tag_span}). - done(function(data, status, jqxhr) { - this.attr('data-tag-link-uuid', data.uuid). - fadeTo('fast', '1'); - }). - fail(function(jqxhr, status, error) { - this.addClass('label-danger').fadeTo('fast', '1'); - }); - return false; - }). - on('click focusin', 'input.select-on-focus', function(event) { - event.target.select(); - }); - - $(document). - on('ajax:complete ready', function() { - // See http://getbootstrap.com/javascript/#buttons - $('.btn').button(); - }). - on('ready ajax:complete', function() { - $('[data-toggle~=tooltip]').tooltip({container:'body'}); - }). - on('ready ajax:complete', function() { - // This makes the dialog close on Esc key, obviously. - $('.modal').attr('tabindex', '-1') - }). - on('ready', function() { - // Need this to trigger input validation/synchronization callbacks because some browsers - // auto-fill form fields (e.g., when navigating "back" to a page where some text - // had been entered in a search box) without triggering a change or input event. - $('input').each(function(el) { - $(el).trigger($.Event('input', {currentTarget: el})); - }); - }); - - HeaderRowFixer = function(selector) { - this.duplicateTheadTr = function() { - $(selector).each(function() { - var the_table = this; - if ($('>tbody>tr:first>th', the_table).length > 0) - return; - $('>tbody', the_table). - prepend($('>thead>tr', the_table). - clone(). - css('opacity', 0)); - }); - } - this.fixThead = function() { - $(selector).each(function() { - var widths = []; - $('> tbody > tr:eq(1) > td', this).each( function(i,v){ - widths.push($(v).width()); - }); - for(i=0;i - // 'foo/bar\\040baz\\040(1).txt' - var newName; - var nameStub = origName; - var suffixInt = null; - var ok = false; - var lineMatch, linesRe = /(\S+).*/gm; - var fileTokenMatch, fileTokensRe = / \d+:\d+:(\S+)/g; - while (!ok) { - ok = true; - // Add ' (N)' before the filename extension, if any. - newName = (!suffixInt ? nameStub : - nameStub.replace(/(\.[^.]*)?$/, ' ('+suffixInt+')$1')). - replace(/ /g, '\\040'); - while (ok && null !== - (lineMatch = linesRe.exec(manifest))) { - // lineMatch is [theEntireLine, streamName] - while (ok && null !== - (fileTokenMatch = fileTokensRe.exec(lineMatch[0]))) { - // fileTokenMatch is [theEntireToken, fileName] - if (lineMatch[1] + '/' + fileTokenMatch[1] - === - newStreamName + '/' + newName) { - ok = false; - } - } - } - suffixInt = (suffixInt || 0) + 1; - } - return newName; - } - - function getDiscoveryDoc() { - if (!promiseDiscovery) { - promiseDiscovery = $.ajax({ - url: arvadosDiscoveryUri, - crossDomain: true - }).then(function(data, status, xhr) { - discoveryDoc = data; - }); - } - return promiseDiscovery; - } -} diff --git a/apps/workbench/app/assets/javascripts/bootstrap.js b/apps/workbench/app/assets/javascripts/bootstrap.js deleted file mode 100644 index e315ab5356..0000000000 --- a/apps/workbench/app/assets/javascripts/bootstrap.js +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright (C) The Arvados Authors. All rights reserved. -// -// SPDX-License-Identifier: AGPL-3.0 - -(function() { - jQuery(function() { - $("a[rel=popover]").popover(); - $(".tooltip").tooltip(); - return $("a[rel=tooltip]").tooltip(); - }); -}).call(this); diff --git a/apps/workbench/app/assets/javascripts/collections.js b/apps/workbench/app/assets/javascripts/collections.js deleted file mode 100644 index 0752e053d8..0000000000 --- a/apps/workbench/app/assets/javascripts/collections.js +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright (C) The Arvados Authors. All rights reserved. -// -// SPDX-License-Identifier: AGPL-3.0 - -jQuery(function($){ - $(document).on('click', '.toggle-persist button', function() { - var toggle_group = $(this).parents('[data-remote-href]').first(); - var want_persist = !toggle_group.find('button').hasClass('active'); - var want_state = want_persist ? 'persistent' : 'cache'; - toggle_group.find('button'). - toggleClass('active', want_persist). - html(want_persist ? 'Persistent' : 'Cache'); - $.ajax(toggle_group.attr('data-remote-href'), - {dataType: 'json', - type: 'POST', - data: { - value: want_state - }, - context: { - toggle_group: toggle_group, - want_state: want_state, - button: this - } - }). - done(function(data, status, jqxhr) { - var context = this; - // Remove "danger" status in case a previous action failed - $('.btn-danger', context.toggle_group). - addClass('btn-info'). - removeClass('btn-danger'); - // Update last-saved-state - context.toggle_group. - attr('data-persistent-state', context.want_state); - }). - fail(function(jqxhr, status, error) { - var context = this; - var saved_state; - // Add a visual indication that something failed - $(context.button). - addClass('btn-danger'). - removeClass('btn-info'); - // Change to the last-saved-state - saved_state = context.toggle_group.attr('data-persistent-state'); - $(context.button). - toggleClass('active', saved_state == 'persistent'). - html(saved_state == 'persistent' ? 'Persistent' : 'Cache'); - - if (jqxhr.readyState == 0 || jqxhr.status == 0) { - // Request cancelled due to page reload. - // Displaying an alert would be rather annoying. - } else if (jqxhr.responseJSON && jqxhr.responseJSON.errors) { - window.alert("Request failed: " + - jqxhr.responseJSON.errors.join("; ")); - } else { - window.alert("Request failed."); - } - }); - }); -}); diff --git a/apps/workbench/app/assets/javascripts/components/date.js b/apps/workbench/app/assets/javascripts/components/date.js deleted file mode 100644 index 62eacc37de..0000000000 --- a/apps/workbench/app/assets/javascripts/components/date.js +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright (C) The Arvados Authors. All rights reserved. -// -// SPDX-License-Identifier: AGPL-3.0 - -window.LocalizedDateTime = { - view: function(vnode) { - return m('span', new Date(Date.parse(vnode.attrs.parse)).toLocaleString()) - }, -} diff --git a/apps/workbench/app/assets/javascripts/components/edit_tags.js b/apps/workbench/app/assets/javascripts/components/edit_tags.js deleted file mode 100644 index 5e02279ea1..0000000000 --- a/apps/workbench/app/assets/javascripts/components/edit_tags.js +++ /dev/null @@ -1,314 +0,0 @@ -// Copyright (C) The Arvados Authors. All rights reserved. -// -// SPDX-License-Identifier: AGPL-3.0 - -window.SimpleInput = { - view: function(vnode) { - return m('input.form-control', { - style: { - width: '100%', - }, - type: 'text', - placeholder: 'Add ' + vnode.attrs.placeholder, - value: vnode.attrs.value, - onchange: function() { - if (this.value != '') { - vnode.attrs.value(this.value) - } - }, - }, vnode.attrs.value) - }, -} - -window.SelectOrAutocomplete = { - view: function(vnode) { - return m('input.form-control', { - style: { - width: '100%' - }, - type: 'text', - value: vnode.attrs.value, - placeholder: (vnode.attrs.create ? 'Add or select ': 'Select ') + vnode.attrs.placeholder, - }, vnode.attrs.value) - }, - oncreate: function(vnode) { - vnode.state.awesomplete = new Awesomplete(vnode.dom, { - list: vnode.attrs.options, - minChars: 0, - maxItems: 1000000, - autoFirst: true, - sort: false, - }) - vnode.state.create = vnode.attrs.create - vnode.state.options = vnode.attrs.options - // Option is selected from the list. - $(vnode.dom).on('awesomplete-selectcomplete', function(event) { - vnode.attrs.value(this.value) - }) - $(vnode.dom).on('change', function(event) { - if (!vnode.state.create && !(this.value in vnode.state.options)) { - this.value = vnode.attrs.value() - } else { - if (vnode.attrs.value() !== this.value) { - vnode.attrs.value(this.value) - } - } - }) - $(vnode.dom).on('focusin', function(event) { - if (this.value === '') { - vnode.state.awesomplete.evaluate() - vnode.state.awesomplete.open() - } - }) - }, - onupdate: function(vnode) { - vnode.state.awesomplete.list = vnode.attrs.options - vnode.state.create = vnode.attrs.create - vnode.state.options = vnode.attrs.options - }, -} - -window.TagEditorRow = { - view: function(vnode) { - var nameOpts = Object.keys(vnode.attrs.vocabulary().tags) - var valueOpts = [] - var inputComponent = SelectOrAutocomplete - if (nameOpts.length === 0) { - // If there's not vocabulary defined, switch to a simple input field - inputComponent = SimpleInput - } else { - // Name options list - if (vnode.attrs.name() != '' && !(vnode.attrs.name() in vnode.attrs.vocabulary().tags)) { - nameOpts.push(vnode.attrs.name()) - } - // Value options list - if (vnode.attrs.name() in vnode.attrs.vocabulary().tags && - 'values' in vnode.attrs.vocabulary().tags[vnode.attrs.name()]) { - valueOpts = vnode.attrs.vocabulary().tags[vnode.attrs.name()].values - } - } - return m('tr', [ - // Erase tag - m('td', [ - vnode.attrs.editMode && - m('div.text-center', m('a.btn.btn-default.btn-sm', { - style: { - align: 'center' - }, - onclick: function(e) { vnode.attrs.removeTag() } - }, m('i.fa.fa-fw.fa-trash-o'))) - ]), - // Tag key - m('td', [ - vnode.attrs.editMode ? - m('div', {key: 'key'}, [ - m(inputComponent, { - options: nameOpts, - value: vnode.attrs.name, - // Allow any tag name unless 'strict' is set to true. - create: !vnode.attrs.vocabulary().strict, - placeholder: 'key', - }) - ]) - : vnode.attrs.name - ]), - // Tag value - m('td', [ - vnode.attrs.editMode ? - m('div', {key: 'value'}, [ - m(inputComponent, { - options: valueOpts, - value: vnode.attrs.value, - placeholder: 'value', - // Allow any value on tags not listed on the vocabulary. - // Allow any value on tags without values, or the ones - // that aren't explicitly declared to be strict. - create: !(vnode.attrs.name() in vnode.attrs.vocabulary().tags) - || !vnode.attrs.vocabulary().tags[vnode.attrs.name()].values - || vnode.attrs.vocabulary().tags[vnode.attrs.name()].values.length === 0 - || !vnode.attrs.vocabulary().tags[vnode.attrs.name()].strict, - }) - ]) - : vnode.attrs.value - ]) - ]) - } -} - -window.TagEditorTable = { - view: function(vnode) { - return m('table.table.table-condensed.table-justforlayout', [ - m('colgroup', [ - m('col', {width:'5%'}), - m('col', {width:'25%'}), - m('col', {width:'70%'}), - ]), - m('thead', [ - m('tr', [ - m('th'), - m('th', 'Key'), - m('th', 'Value'), - ]) - ]), - m('tbody', [ - vnode.attrs.tags.length > 0 - ? vnode.attrs.tags.map(function(tag, idx) { - return m(TagEditorRow, { - key: tag.rowKey, - removeTag: function() { - vnode.attrs.tags.splice(idx, 1) - vnode.attrs.dirty(true) - }, - editMode: vnode.attrs.editMode, - name: tag.name, - value: tag.value, - vocabulary: vnode.attrs.vocabulary - }) - }) - : m('tr', m('td[colspan=3]', m('center', 'Loading tags...'))) - ]), - ]) - } -} - -var uniqueID = 1 - -window.TagEditorApp = { - appendTag: function(vnode, name, value) { - var tag = {name: m.stream(name), value: m.stream(value), rowKey: uniqueID++} - vnode.state.tags.push(tag) - // Set dirty flag when any of name/value changes to non empty string - tag.name.map(function() { vnode.state.dirty(true) }) - tag.value.map(function() { vnode.state.dirty(true) }) - tag.name.map(m.redraw) - }, - fixTag: function(vnode, tagName) { - // Recover tag if deleted, recover its value if modified - savedTagValue = vnode.state.saved_tags[tagName] - if (savedTagValue === undefined) { - return - } - found = false - vnode.state.tags.forEach(function(tag) { - if (tag.name == tagName) { - tag.value = vnode.state.saved_tags[tagName] - found = true - } - }) - if (!found) { - vnode.state.tags.pop() // Remove the last empty row - vnode.state.appendTag(vnode, tagName, savedTagValue) - } - }, - oninit: function(vnode) { - vnode.state.sessionDB = new SessionDB() - // Get vocabulary - vnode.state.vocabulary = m.stream({'strict':false, 'tags':{}}) - var vocabularyTimestamp = parseInt(Date.now() / 300000) // Bust cache every 5 minutes - m.request('/vocabulary.json?v=' + vocabularyTimestamp).then(vnode.state.vocabulary) - vnode.state.editMode = vnode.attrs.targetEditable - vnode.state.tags = [] - vnode.state.saved_tags = {} - vnode.state.dirty = m.stream(false) - vnode.state.dirty.map(m.redraw) - vnode.state.error = m.stream('') - vnode.state.objPath = 'arvados/v1/' + vnode.attrs.targetController + '/' + vnode.attrs.targetUuid - // Get tags - vnode.state.sessionDB.request( - vnode.state.sessionDB.loadLocal(), - 'arvados/v1/' + vnode.attrs.targetController, - { - data: { - filters: JSON.stringify([['uuid', '=', vnode.attrs.targetUuid]]), - select: JSON.stringify(['properties']) - }, - }).then(function(obj) { - if (obj.items.length == 1) { - o = obj.items[0] - Object.keys(o.properties).forEach(function(k) { - vnode.state.appendTag(vnode, k, o.properties[k]) - }) - if (vnode.state.editMode) { - vnode.state.appendTag(vnode, '', '') - } - // Data synced with server, so dirty state should be false - vnode.state.dirty(false) - vnode.state.saved_tags = o.properties - // Add new tag row when the last one is completed - vnode.state.dirty.map(function() { - if (!vnode.state.editMode) { return } - lastTag = vnode.state.tags.slice(-1).pop() - if (lastTag === undefined || (lastTag.name() !== '' || lastTag.value() !== '')) { - vnode.state.appendTag(vnode, '', '') - } - }) - } - } - ) - }, - view: function(vnode) { - return [ - vnode.state.editMode && - m('div.pull-left', [ - m('a.btn.btn-primary.btn-sm' + (vnode.state.dirty() ? '' : '.disabled'), { - style: { - margin: '10px 0px' - }, - onclick: function(e) { - var tags = {} - vnode.state.tags.forEach(function(t) { - // Only ignore tags with empty key - if (t.name() != '') { - tags[t.name()] = t.value() - } - }) - vnode.state.sessionDB.request( - vnode.state.sessionDB.loadLocal(), - vnode.state.objPath, { - method: 'PUT', - data: {properties: JSON.stringify(tags)} - } - ).then(function(v) { - vnode.state.dirty(false) - vnode.state.error('') - vnode.state.saved_tags = tags - }).catch(function(err) { - if (err.errors !== undefined) { - var re = /protected\ property/i - var protected_props = [] - err.errors.forEach(function(error) { - if (re.test(error)) { - prop = error.split(':')[1].trim() - vnode.state.fixTag(vnode, prop) - protected_props.push(prop) - } - }) - if (protected_props.length > 0) { - errMsg = "Protected properties cannot be updated: " + protected_props.join(', ') - } else { - errMsg = errors.join(', ') - } - } else { - errMsg = err - } - vnode.state.error(errMsg) - }) - } - }, vnode.state.dirty() ? ' Save changes ' : ' Saved '), - m('span', { - style: { - color: '#ff0000', - margin: '0px 10px' - } - }, [ vnode.state.error() ]) - ]), - // Tags table - m(TagEditorTable, { - editMode: vnode.state.editMode, - tags: vnode.state.tags, - vocabulary: vnode.state.vocabulary, - dirty: vnode.state.dirty - }) - ] - }, -} diff --git a/apps/workbench/app/assets/javascripts/components/save_ui_state.js b/apps/workbench/app/assets/javascripts/components/save_ui_state.js deleted file mode 100644 index 3aece3195d..0000000000 --- a/apps/workbench/app/assets/javascripts/components/save_ui_state.js +++ /dev/null @@ -1,90 +0,0 @@ -// Copyright (C) The Arvados Authors. All rights reserved. -// -// SPDX-License-Identifier: AGPL-3.0 - -// SaveUIState avoids losing scroll position due to navigation -// events, and saves/restores other caller-specified UI state. -// -// It does not display any content itself: do not pass any children. -// -// Use of multiple SaveUIState components on the same page is not -// (yet) supported. -// -// The problem being solved: -// -// Page 1 loads some content dynamically (e.g., via infinite scroll) -// after the initial render. User scrolls down, clicks a link, and -// lands on page 2. User clicks the Back button, and lands on page -// 1. Page 1 renders its initial content while waiting for AJAX. -// -// But (without SaveUIState) the document body is small now, so the -// browser resets scroll position to the top of the page. Even if we -// end up displaying the same dynamic content, the user's place on the -// page has been lost. -// -// SaveUIState fixes this by stashing the current body height when -// navigating away from page 1. When navigating back, it restores the -// body height even before the page has loaded, so the browser does -// not reset the scroll position. -// -// SaveUIState also saves/restores arbitrary UI state (like text typed -// in a search box) in response to navigation events. -// -// See CollectionsSearch for an example. -// -// Attributes: -// -// {getter-setter} currentState: the current UI state -// -// {any} defaultState: value to initialize currentState with, if -// nothing is stashed in browser history. -// -// {boolean} forgetSavedHeight: the body height loaded from the -// browser history (if any) is outdated; we should let the browser -// determine the correct body height from the current page -// content. Set this when dynamic content has been reset. -// -// {boolean} saveBodyHeight: save/restore body height as described -// above. -window.SaveUIState = { - saveState: function() { - var state = history.state || {} - state.bodyHeight = window.getComputedStyle(document.body)['height'] - state.currentState = this.currentState() - history.replaceState(state, '') - }, - oninit: function(vnode) { - vnode.state.currentState = vnode.attrs.currentState - var hstate = history.state || {} - - if (vnode.attrs.saveBodyHeight && hstate.bodyHeight) { - document.body.style['min-height'] = hstate.bodyHeight - delete hstate.bodyHeight - } - - if (hstate.currentState) { - vnode.attrs.currentState(hstate.currentState) - delete hstate.currentState - } else { - vnode.attrs.currentState(vnode.attrs.defaultState) - } - - history.replaceState(hstate, '') - }, - oncreate: function(vnode) { - vnode.state.saveState = vnode.state.saveState.bind(vnode.state) - window.addEventListener('beforeunload', vnode.state.saveState) - vnode.state.onupdate(vnode) - }, - onupdate: function(vnode) { - if (vnode.attrs.saveBodyHeight && vnode.attrs.forgetSavedHeight) { - document.body.style['min-height'] = null - } - }, - onremove: function(vnode) { - window.removeEventListener('beforeunload', vnode.state.saveState) - }, - view: function(vnode) { - return null - }, -} diff --git a/apps/workbench/app/assets/javascripts/components/search.js b/apps/workbench/app/assets/javascripts/components/search.js deleted file mode 100644 index 83ed1a68d4..0000000000 --- a/apps/workbench/app/assets/javascripts/components/search.js +++ /dev/null @@ -1,218 +0,0 @@ -// Copyright (C) The Arvados Authors. All rights reserved. -// -// SPDX-License-Identifier: AGPL-3.0 - -window.SearchResultsTable = { - maybeLoadMore: function(dom) { - var loader = this.loader - if (loader.state != loader.READY) - // Can't start getting more items anyway: no point in - // checking anything else. - return - var contentRect = dom.getBoundingClientRect() - var scroller = window // TODO: use dom's nearest ancestor with scrollbars - if (contentRect.bottom < 2 * scroller.innerHeight) { - // We have less than 1 page worth of content available - // below the visible area. Load more. - loader.loadMore() - // Indicate loading is in progress. - window.requestAnimationFrame(m.redraw) - } - }, - oncreate: function(vnode) { - vnode.state.maybeLoadMore = vnode.state.maybeLoadMore.bind(vnode.state, vnode.dom) - window.addEventListener('scroll', vnode.state.maybeLoadMore) - window.addEventListener('resize', vnode.state.maybeLoadMore) - vnode.state.timer = window.setInterval(vnode.state.maybeLoadMore, 200) - vnode.state.loader = vnode.attrs.loader - vnode.state.onupdate(vnode) - }, - onupdate: function(vnode) { - vnode.state.loader = vnode.attrs.loader - }, - onremove: function(vnode) { - window.clearInterval(vnode.state.timer) - window.removeEventListener('scroll', vnode.state.maybeLoadMore) - window.removeEventListener('resize', vnode.state.maybeLoadMore) - }, - view: function(vnode) { - var loader = vnode.attrs.loader - var iconsMap = { - collections: m('i.fa.fa-fw.fa-archive'), - projects: m('i.fa.fa-fw.fa-folder'), - } - var db = new SessionDB() - var sessions = db.loadActive() - return m('table.table.table-condensed', [ - m('thead', m('tr', [ - m('th'), - m('th', 'uuid'), - m('th', 'name'), - m('th', 'last modified'), - ])), - m('tbody', [ - loader.items().map(function(item) { - var session = sessions[item.uuid.slice(0,5)] - var tokenParam = '' - // Add the salted token to search result links from federated - // remote hosts. - if (!session.isFromRails && session.token.indexOf('v2/') == 0) { - tokenParam = session.token - } - return m('tr', [ - m('td', m('form', { - action: item.workbenchBaseURL() + '/' + item.objectType.wb_path + '/' + item.uuid, - method: 'GET' - }, [ - tokenParam !== '' && - m('input[type=hidden][name=api_token]', {value: tokenParam}), - item.workbenchBaseURL() && - m('button.btn.btn-xs.btn-default[type=submit]', { - 'data-original-title': 'show '+item.objectType.description, - 'data-placement': 'top', - 'data-toggle': 'tooltip', - // Bootstrap's tooltip feature - oncreate: function(vnode) { $(vnode.dom).tooltip() }, - }, iconsMap[item.objectType.wb_path]), - ])), - m('td.arvados-uuid', item.uuid), - m('td', item.name || '(unnamed)'), - m('td', m(LocalizedDateTime, {parse: item.modified_at})), - ]) - }), - ]), - loader.state == loader.DONE ? null : m('tfoot', m('tr', [ - m('th[colspan=4]', m('button.btn.btn-xs', { - className: loader.state == loader.LOADING ? 'btn-default' : 'btn-primary', - style: { - display: 'block', - width: '12em', - marginLeft: 'auto', - marginRight: 'auto', - }, - disabled: loader.state == loader.LOADING, - onclick: function() { - loader.loadMore() - return false - }, - }, loader.state == loader.LOADING ? '(loading)' : 'Load more')), - ])), - ]) - }, -} - -window.Search = { - oninit: function(vnode) { - vnode.state.sessionDB = new SessionDB() - vnode.state.sessionDB.autoRedirectToHomeCluster('/search') - vnode.state.searchEntered = m.stream() - vnode.state.searchActive = m.stream() - // When searchActive changes (e.g., when restoring state - // after navigation), update the text field too. - vnode.state.searchActive.map(vnode.state.searchEntered) - // When searchActive changes, create a new loader that filters - // with the given search term. - vnode.state.searchActive.map(function(q) { - var sessions = vnode.state.sessionDB.loadActive() - vnode.state.loader = new MergingLoader({ - children: Object.keys(sessions).map(function(key) { - var session = sessions[key] - var workbenchBaseURL = function() { - return vnode.state.sessionDB.workbenchBaseURL(session) - } - var searchable_objects = [ - { - wb_path: 'projects', - api_path: 'arvados/v1/groups', - filters: [['group_class', '=', 'project']], - description: 'project', - }, - { - wb_path: 'projects', - api_path: 'arvados/v1/groups', - filters: [['group_class', '=', 'filter']], - description: 'project', - }, - { - wb_path: 'collections', - api_path: 'arvados/v1/collections', - filters: [], - description: 'collection', - }, - ] - return new MergingLoader({ - sessionKey: key, - // For every session, search for every object type - children: searchable_objects.map(function(obj_type) { - return new MultipageLoader({ - sessionKey: key, - loadFunc: function(filters) { - // Apply additional type dependant filters - filters = filters.concat(obj_type.filters).concat(ilike_filters(q)) - return vnode.state.sessionDB.request(session, obj_type.api_path, { - data: { - filters: JSON.stringify(filters), - count: 'none', - }, - }).then(function(resp) { - resp.items.map(function(item) { - item.workbenchBaseURL = workbenchBaseURL - item.objectType = obj_type - }) - return resp - }) - }, - }) - }), - }) - }), - }) - }) - }, - view: function(vnode) { - return m('form', { - onsubmit: function() { - vnode.state.searchActive(vnode.state.searchEntered()) - vnode.state.forgetSavedHeight = true - return false - }, - }, [ - m(SaveUIState, { - defaultState: '', - currentState: vnode.state.searchActive, - forgetSavedHeight: vnode.state.forgetSavedHeight, - saveBodyHeight: true, - }), - vnode.state.loader && [ - m('.row', [ - m('.col-md-6', [ - m('.input-group', [ - m('input#search.form-control[placeholder=Search collections and projects]', { - oninput: m.withAttr('value', vnode.state.searchEntered), - value: vnode.state.searchEntered(), - }), - m('.input-group-btn', [ - m('input.btn.btn-primary[type=submit][value="Search"]'), - ]), - ]), - ]), - m('.col-md-6', [ - 'Searching sites: ', - vnode.state.loader.children.length == 0 - ? m('span.label.label-xs.label-danger', 'none') - : vnode.state.loader.children.map(function(child) { - return [m('span.label.label-xs', { - className: child.state == child.LOADING ? 'label-warning' : 'label-success', - }, child.sessionKey), ' '] - }), - ' ', - m('a[href="/sessions"]', 'Add/remove sites'), - ]), - ]), - m(SearchResultsTable, { - loader: vnode.state.loader, - }), - ], - ]) - }, -} diff --git a/apps/workbench/app/assets/javascripts/components/sessions.js b/apps/workbench/app/assets/javascripts/components/sessions.js deleted file mode 100644 index 04ca6ac926..0000000000 --- a/apps/workbench/app/assets/javascripts/components/sessions.js +++ /dev/null @@ -1,108 +0,0 @@ -// Copyright (C) The Arvados Authors. All rights reserved. -// -// SPDX-License-Identifier: AGPL-3.0 - -$(document).on('ready', function() { - var db = new SessionDB(); - db.checkForNewToken(); - db.fillMissingUUIDs(); - db.autoLoadRemoteHosts(); -}); - -window.SessionsTable = { - oninit: function(vnode) { - vnode.state.db = new SessionDB(); - vnode.state.db.autoRedirectToHomeCluster('/sessions'); - vnode.state.db.migrateNonFederatedSessions(); - vnode.state.hostToAdd = m.stream(''); - vnode.state.error = m.stream(); - vnode.state.checking = m.stream(); - }, - view: function(vnode) { - var db = vnode.state.db; - var sessions = db.loadAll(); - return m('.container', [ - m('p', [ - 'You can log in to multiple Arvados sites here, then use the ', - m('a[href="/search"]', 'multi-site search'), - ' page to search collections and projects on all sites at once.' - ]), - m('table.table.table-condensed.table-hover', [ - m('thead', m('tr', [ - m('th', 'status'), - m('th', 'cluster ID'), - m('th', 'username'), - m('th', 'email'), - m('th', 'actions'), - m('th') - ])), - m('tbody', [ - Object.keys(sessions).map(function(uuidPrefix) { - var session = sessions[uuidPrefix]; - return m('tr', [ - session.token && session.user ? [ - m('td', session.user.is_active ? - m('span.label.label-success', 'logged in') : - m('span.label.label-warning', 'inactive')), - m('td', {title: session.baseURL}, uuidPrefix), - m('td', session.user.username), - m('td', session.user.email), - m('td', session.isFromRails ? null : m('button.btn.btn-xs.btn-default', { - uuidPrefix: uuidPrefix, - onclick: m.withAttr('uuidPrefix', db.logout), - }, session.listedHost ? 'Disable ':'Log out ', m('span.glyphicon.glyphicon-log-out'))) - ] : [ - m('td', m('span.label.label-default', 'logged out')), - m('td', {title: session.baseURL}, uuidPrefix), - m('td'), - m('td'), - m('td', m('a.btn.btn-xs.btn-primary', { - uuidPrefix: uuidPrefix, - onclick: db.login.bind(db, session.baseURL), - }, session.listedHost ? 'Enable ':'Log in ', m('span.glyphicon.glyphicon-log-in'))) - ], - m('td', (session.isFromRails || session.listedHost) ? null : - m('button.btn.btn-xs.btn-default', { - uuidPrefix: uuidPrefix, - onclick: m.withAttr('uuidPrefix', db.trash), - }, 'Remove ', m('span.glyphicon.glyphicon-trash')) - ), - ]) - }), - ]), - ]), - m('.row', m('.col-md-6', [ - m('form', { - onsubmit: function() { - vnode.state.error(null) - vnode.state.checking(true) - db.findAPI(vnode.state.hostToAdd()) - .then(db.login) - .catch(function() { - vnode.state.error(true) - }) - .then(vnode.state.checking.bind(null, null)) - return false - }, - }, [ - m('p', [ - 'To add a remote Arvados site, paste the remote site\'s host here (see "ARVADOS_API_HOST" on the "current token" page).', - ]), - m('.input-group', { className: vnode.state.error() && 'has-error' }, [ - m('input.form-control[type=text][name=apiHost][placeholder="zzzzz.arvadosapi.com"]', { - oninput: m.withAttr('value', vnode.state.hostToAdd), - }), - m('.input-group-btn', [ - m('input.btn.btn-primary[type=submit][value="Log in"]', { - disabled: !vnode.state.hostToAdd(), - }), - ]), - ]), - ]), - m('p'), - vnode.state.error() && m('p.alert.alert-danger', 'Request failed. Make sure this is a working API server address.'), - vnode.state.checking() && m('p.alert.alert-info', 'Checking...'), - ])), - ]) - }, -} diff --git a/apps/workbench/app/assets/javascripts/components/test.js b/apps/workbench/app/assets/javascripts/components/test.js deleted file mode 100644 index 4893544022..0000000000 --- a/apps/workbench/app/assets/javascripts/components/test.js +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright (C) The Arvados Authors. All rights reserved. -// -// SPDX-License-Identifier: AGPL-3.0 - -window.TestComponent = { - view: function(vnode) { - return m('div.mithril-test-component', [ - m('p', { - onclick: m.withAttr('zzz', function(){}), - }, [ - 'mithril is working; rendered at t=', - (new Date()).getTime(), - 'ms (click to re-render)', - ]), - ]) - }, -} diff --git a/apps/workbench/app/assets/javascripts/dates.js b/apps/workbench/app/assets/javascripts/dates.js deleted file mode 100644 index ed5f28431c..0000000000 --- a/apps/workbench/app/assets/javascripts/dates.js +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright (C) The Arvados Authors. All rights reserved. -// -// SPDX-License-Identifier: AGPL-3.0 - -jQuery(function($){ -$(document).on('ajax:complete arv:pane:loaded ready', function() { - $('[data-utc-date]').each(function(i, elm) { - // Try matching the date using a couple of different formats. - var v = $(elm).attr('data-utc-date').match(/(\d\d\d\d)-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d) UTC/); - if (!v) { - v = $(elm).attr('data-utc-date').match(/(\d\d\d\d)-(\d\d)-(\d\d)T(\d\d):(\d\d):(\d\d)Z/); - } - - if (v) { - // Create a new date object from the timestamp so the browser can - // render the date based on the locale/timezone. - var ts = new Date(Date.UTC(v[1], v[2]-1, v[3], v[4], v[5], v[6])); - if ($(elm).attr('data-utc-date-opts') && $(elm).attr('data-utc-date-opts').match(/noseconds/)) { - $(elm).text((ts.getHours() > 12 ? (ts.getHours()-12) : ts.getHours()) - + ":" + (ts.getMinutes() < 10 ? '0' : '') + ts.getMinutes() - + (ts.getHours() >= 12 ? " PM " : " AM ") - + ts.toLocaleDateString()); - } else { - $(elm).text(ts.toLocaleTimeString() + " " + ts.toLocaleDateString()); - } - } - }); -}); -}); diff --git a/apps/workbench/app/assets/javascripts/edit_collection.js b/apps/workbench/app/assets/javascripts/edit_collection.js deleted file mode 100644 index 9220ac3c54..0000000000 --- a/apps/workbench/app/assets/javascripts/edit_collection.js +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright (C) The Arvados Authors. All rights reserved. -// -// SPDX-License-Identifier: AGPL-3.0 - -// On loading of a collection, enable the "lock" button and -// disable all file modification controls (upload, rename, delete) -$(document). - ready(function(event) { - $(".btn-collection-file-control").addClass("disabled"); - $(".btn-collection-rename-file-span").attr("title", "Unlock collection to rename file"); - $(".btn-collection-remove-file-span").attr("title", "Unlock collection to remove file"); - $(".btn-remove-selected-files").attr("title", "Unlock collection to remove selected files"); - $(".tab-pane-Upload").addClass("disabled"); - $(".tab-pane-Upload").attr("title", "Unlock collection to upload files"); - $("#Upload-tab").attr("data-toggle", "disabled"); - }). - on('click', '.lock-collection-btn', function(event) { - classes = $(event.target).attr('class') - - if (classes.indexOf("fa-lock") != -1) { - // About to unlock; warn and get confirmation from user - if (confirm("Adding, renaming, and deleting files changes the portable data hash. Are you sure you want to unlock the collection?")) { - $(".lock-collection-btn").removeClass("fa-lock"); - $(".lock-collection-btn").addClass("fa-unlock"); - $(".lock-collection-btn").attr("title", "Lock collection to prevent editing files"); - $(".btn-collection-rename-file-span").attr("title", ""); - $(".btn-collection-remove-file-span").attr("title", ""); - $(".btn-collection-file-control").removeClass("disabled"); - $(".btn-remove-selected-files").attr("title", ""); - $(".tab-pane-Upload").removeClass("disabled"); - $(".tab-pane-Upload").attr("data-original-title", ""); - $("#Upload-tab").attr("data-toggle", "tab"); - } else { - // User clicked "no" and so do not unlock - } - } else { - // Lock it back - $(".lock-collection-btn").removeClass("fa-unlock"); - $(".lock-collection-btn").addClass("fa-lock"); - $(".lock-collection-btn").attr("title", "Unlock collection to edit files"); - $(".btn-collection-rename-file-span").attr("title", "Unlock collection to rename file"); - $(".btn-collection-remove-file-span").attr("title", "Unlock collection to remove file"); - $(".btn-collection-file-control").addClass("disabled"); - $(".btn-remove-selected-files").attr("title", "Unlock collection to remove selected files"); - $(".tab-pane-Upload").addClass("disabled"); - $(".tab-pane-Upload").attr("data-original-title", "Unlock collection to upload files"); - $("#Upload-tab").attr("data-toggle", "disabled"); - } - }); diff --git a/apps/workbench/app/assets/javascripts/editable.js b/apps/workbench/app/assets/javascripts/editable.js deleted file mode 100644 index 939506c2ec..0000000000 --- a/apps/workbench/app/assets/javascripts/editable.js +++ /dev/null @@ -1,121 +0,0 @@ -// Copyright (C) The Arvados Authors. All rights reserved. -// -// SPDX-License-Identifier: AGPL-3.0 - -$.fn.editable.defaults.ajaxOptions = {type: 'post', dataType: 'json'}; -$.fn.editable.defaults.send = 'always'; - -// Default for editing is popup. I experimented with inline which is a little -// nicer in that it shows up right under the mouse instead of nearby. However, -// the inline box is taller than the regular content, which causes the page -// layout to shift unless we make the table rows tall, which leaves a lot of -// wasted space when not editing. Also inline can get cut off if the page is -// too narrow, when the popup box will just move to do the right thing. -//$.fn.editable.defaults.mode = 'inline'; - -$.fn.editable.defaults.success = function (response, newValue) { - $(document).trigger('editable:success', [this, response, newValue]); -}; - -$.fn.editable.defaults.params = function (params) { - var a = {}; - var key = params.pk.key; - a.id = $(this).attr('data-object-uuid') || params.pk.id; - a[key] = params.pk.defaults || {}; - // Remove null values. Otherwise they get transmitted as empty - // strings in request params. - for (i in a[key]) { - if (a[key][i] == null) - delete a[key][i]; - } - a[key][params.name] = params.value; - if (!a.id) { - a['_method'] = 'post'; - } else { - a['_method'] = 'put'; - } - return a; -}; - -$.fn.editable.defaults.validate = function (value) { - if (value == "***invalid***") { - return "Invalid selection"; - } -} - -$(document). - on('ready ajax:complete', function() { - $('.editable'). - not('.editable-done-setup'). - addClass('editable-done-setup'). - editable({ - success: function(response, newValue) { - // If we just created a new object, stash its UUID - // so we edit it next time instead of creating - // another new object. - if (!$(this).attr('data-object-uuid') && response.uuid) { - $(this).attr('data-object-uuid', response.uuid); - } - if (response.href) { - $(this).editable('option', 'url', response.href); - } - if ($(this).attr('data-name')) { - var textileAttr = $(this).attr('data-name') + 'Textile'; - if (response[textileAttr]) { - $(this).attr('data-textile', response[textileAttr]); - } - } - return; - }, - error: function(response, newValue) { - var errlist = response.responseJSON.errors; - var errmsg; - if (Array.isArray(errlist)) { - errmsg = errlist.join(); - } else { - errmsg = ("The server returned an error when making " + - "this update (status " + response.status + - ": " + errlist + ")."); - } - return errmsg; - } - }). - on('hidden', function(e, reason) { - // After saving a new attribute, update the same - // information if it appears elsewhere on the page. - if (reason != 'save') return; - var html = $(this).html(); - if( $(this).attr('data-textile') ) { - html = $(this).attr('data-textile'); - $(this).html(html); - } - var uuid = $(this).attr('data-object-uuid'); - var attr = $(this).attr('data-name'); - var edited = this; - if (uuid && attr) { - $("[data-object-uuid='" + uuid + "']" + - "[data-name='" + attr + "']").each(function() { - if (this != edited) - $(this).html(html); - }); - } - }); - }). - on('ready ajax:complete', function() { - $("[data-toggle~='x-editable']"). - not('.editable-done-setup'). - addClass('editable-done-setup'). - click(function(e) { - e.stopPropagation(); - $($(this).attr('data-toggle-selector')).editable('toggle'); - }); - }); - -$.fn.editabletypes.text.defaults.tpl = '' - -$.fn.editableform.buttons = '\ -\ -\ -' diff --git a/apps/workbench/app/assets/javascripts/event_log.js b/apps/workbench/app/assets/javascripts/event_log.js deleted file mode 100644 index e576ba97a3..0000000000 --- a/apps/workbench/app/assets/javascripts/event_log.js +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright (C) The Arvados Authors. All rights reserved. -// -// SPDX-License-Identifier: AGPL-3.0 - -/* - * This js establishes a websockets connection with the API Server. - */ - -/* Subscribe to websockets event log. Do nothing if already connected. */ -function subscribeToEventLog () { - // if websockets are not supported by browser, do not subscribe for events - websocketsSupported = ('WebSocket' in window); - if (websocketsSupported == false) { - return; - } - - // check if websocket connection is already stored on the window - event_log_disp = $(window).data("arv-websocket"); - if (event_log_disp == null) { - // need to create new websocket and event log dispatcher - websocket_url = $('meta[name=arv-websocket-url]').attr("content"); - if (websocket_url == null) - return; - - event_log_disp = new WebSocket(websocket_url); - - event_log_disp.onopen = onEventLogDispatcherOpen; - event_log_disp.onmessage = onEventLogDispatcherMessage; - - // store websocket in window to allow reuse when multiple divs subscribe for events - $(window).data("arv-websocket", event_log_disp); - } -} - -/* Send subscribe message to the websockets server. Without any filters - arguments, this subscribes to all events */ -function onEventLogDispatcherOpen(event) { - this.send('{"method":"subscribe"}'); -} - -/* Trigger event for all applicable elements waiting for this event */ -function onEventLogDispatcherMessage(event) { - parsedData = JSON.parse(event.data); - object_uuid = parsedData.object_uuid; - - if (!object_uuid) { - return; - } - - // if there are any listeners for this object uuid or "all", trigger the event - matches = ".arv-log-event-listener[data-object-uuid=\"" + object_uuid + "\"],.arv-log-event-listener[data-object-uuids~=\"" + object_uuid + "\"],.arv-log-event-listener[data-object-uuid=\"all\"],.arv-log-event-listener[data-object-kind=\"" + parsedData.object_kind + "\"]"; - $(matches).trigger('arv-log-event', parsedData); -} - -/* Automatically connect if there are any elements on the page that want to - receive event log events. */ -$(document).on('ajax:complete ready', function() { - var a = $('.arv-log-event-listener'); - if (a.length > 0) { - subscribeToEventLog(); - } -}); diff --git a/apps/workbench/app/assets/javascripts/filterable.js b/apps/workbench/app/assets/javascripts/filterable.js deleted file mode 100644 index bf859c350a..0000000000 --- a/apps/workbench/app/assets/javascripts/filterable.js +++ /dev/null @@ -1,203 +0,0 @@ -// Copyright (C) The Arvados Authors. All rights reserved. -// -// SPDX-License-Identifier: AGPL-3.0 - -// filterable.js shows/hides content when the user operates -// search/select widgets. For "infinite scroll" content, it passes the -// filters to the server and retrieves new content. For other content, -// it filters the existing DOM elements using jQuery show/hide. -// -// Usage: -// -// 1. Add the "filterable" class to each filterable content item. -// Typically, each item is a 'tr' or a 'div class="row"'. -// -//

-//
First row
-//
Second row
-//
-// -// 2. Add the "filterable-control" class to each search/select widget. -// Also add a data-filterable-target attribute with a jQuery selector -// for an ancestor of the filterable items, i.e., the container in -// which this widget should apply filtering. -// -// -// -// Supported widgets: -// -// -// -// The input value is used as a regular expression. Rows with content -// matching the regular expression are shown. -// -// -// -// When the user selects the "Foo" option, rows with -// data-example-attr="foo" are shown, and all others are hidden. When -// the user selects the "Show all" option, all rows are shown. -// -// -// -// Merges on- or off-value with other params in query. Only works with -// infinite-scroll. -// -// Notes: -// -// When multiple filterable-control widgets operate on the same -// data-filterable-target, items must pass _all_ filters in order to -// be shown. -// -// If one data-filterable-target is the parent of another -// data-filterable-target, results are undefined. Don't do this. -// -// Combining "select" filterable-controls with infinite-scroll is not -// yet supported. - -function updateFilterableQueryNow($target) { - var newquery = $target.data('filterable-query-new'); - var params = $target.data('infinite-content-params-filterable') || {}; - params.filters = ilike_filters(newquery); - $(".modal-dialog-preview-pane").html(""); - $target.data('infinite-content-params-filterable', params); - $target.data('filterable-query', newquery); -} - -$(document). - on('ready ajax:success', function() { - // Copy any initial input values into - // data-filterable-query[-new]. - $('input[type=text].filterable-control').each(function() { - var $this = $(this); - var $target = $($this.attr('data-filterable-target')); - if ($target.data('filterable-query-new') === undefined) { - $target.data('filterable-query', $this.val()); - $target.data('filterable-query-new', $this.val()); - updateFilterableQueryNow($target); - } - }); - $('[data-infinite-scroller]').on('refresh-content', '[data-filterable-query]', function(e) { - // If some other event causes a refresh-content event while there - // is a new query waiting to cooloff, we should use the new query - // right away -- otherwise we'd launch an extra ajax request that - // would have to be reloaded as soon as the cooloff period ends. - if (this != e.target) - return; - if ($(this).data('filterable-query') == $(this).data('filterable-query-new')) - return; - updateFilterableQueryNow($(this)); - }); - }). - on('change', 'input[type=checkbox].filterable-control', function(e) { - if (this != e.target) return; - var $target = $($(this).attr('data-filterable-target')); - var currentquery = $target.data('filterable-query'); - if (currentquery === undefined) currentquery = ''; - if ($target.is('[data-infinite-scroller]')) { - var datakey = 'infiniteContentParamsFrom'+this.id; - var whichvalue = $(this).is(':checked') ? 'on-value' : 'off-value'; - if (JSON.stringify($target.data(datakey)) == JSON.stringify($(this).data(whichvalue))) - return; - $target.data(datakey, $(this).data(whichvalue)); - updateFilterableQueryNow($target); - $target.trigger('refresh-content'); - } - }). - on('paste keyup input', 'input[type=text].filterable-control', function(e) { - var regexp; - if (this != e.target) return; - var $target = $($(this).attr('data-filterable-target')); - var currentquery = $target.data('filterable-query'); - if (currentquery === undefined) currentquery = ''; - if ($target.is('[data-infinite-scroller]')) { - // We already know how to load content dynamically, so we - // can do all filtering on the server side. - - if ($target.data('infinite-cooloff-timer') > 0) { - // Clear a stale refresh-after-delay timer. - clearTimeout($target.data('infinite-cooloff-timer')); - } - // Stash the new query string in the filterable container. - $target.data('filterable-query-new', $(this).val()); - if (currentquery == $(this).val()) { - // Don't mess with existing results or queries in - // progress. - return; - } - $target.data('infinite-cooloff-timer', setTimeout(function() { - // If the user doesn't do any query-changing actions - // in the next 1/4 second (like type or erase - // characters in the search box), hide the stale - // content and ask the server for new results. - updateFilterableQueryNow($target); - $target.trigger('refresh-content'); - }, 250)); - } else { - // Target does not have infinite-scroll capability. Just - // filter the rows in the browser using a RegExp. - regexp = undefined; - try { - regexp = new RegExp($(this).val(), 'i'); - } catch(e) { - if (e instanceof SyntaxError) { - // Invalid/partial regexp. See 'has-error' below. - } else { - throw e; - } - } - $target. - toggleClass('has-error', regexp === undefined). - addClass('filterable-container'). - data('q', regexp). - trigger('refresh'); - } - }).on('refresh', '.filterable-container', function() { - var $container = $(this); - var q = $(this).data('q'); - var filters = $(this).data('filters'); - $('.filterable', this).hide().filter(function() { - var $row = $(this); - var pass = true; - if (q && !$row.text().match(q)) - return false; - if (filters) { - $.each(filters, function(filterby, val) { - if (!val) return; - if (!pass) return; - pass = false; - $.each(val.split(" "), function(i, e) { - if ($row.attr(filterby) == e) - pass = true; - }); - }); - } - return pass; - }).show(); - - // Show/hide each section heading depending on whether any - // content rows are visible in that section. - $('.row[data-section-heading]', this).each(function(){ - $(this).toggle($('.row.filterable[data-section-name="' + - $(this).attr('data-section-name') + - '"]:visible').length > 0); - }); - - // Load more content if the last result is showing. - $('.infinite-scroller').add(window).trigger('scroll'); - }).on('change', 'select.filterable-control', function() { - var val = $(this).val(); - var filterby = $(this).attr('data-filterable-attribute'); - var $target = $($(this).attr('data-filterable-target')). - addClass('filterable-container'); - var filters = $target.data('filters') || {}; - filters[filterby] = val; - $target. - data('filters', filters). - trigger('refresh'); - }).on('ajax:complete', function() { - $('.filterable-control').trigger('input'); - }); diff --git a/apps/workbench/app/assets/javascripts/ilike_filters.js b/apps/workbench/app/assets/javascripts/ilike_filters.js deleted file mode 100644 index 4f5cd48e1f..0000000000 --- a/apps/workbench/app/assets/javascripts/ilike_filters.js +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright (C) The Arvados Authors. All rights reserved. -// -// SPDX-License-Identifier: AGPL-3.0 - -// ilike_filters() converts a user-entered search query to a list of -// filters using the newly added (as of Arvados 1.5) trigram indexes. It returns -// [] (empty list) if it can't come up with anything valid (e.g., q consists -// entirely of punctuation). -// -// Examples: -// -// "foo" => [["any", "ilike", "%foo%"]] -// "foo.bar" => [["any", "ilike", "%foo.bar%"]] // "." is a word char in ilike queries -// "foo/b-r" => [["any", "ilike", "%foo/b-r%"]] // "/" and "-", too -// "foo_bar" => [["any", "ilike", "%foo\\_bar%"] // "_" should be escaped so it can be used as a literal -// "foo bar" => [["any", "ilike", "%foo%"], ["any", "ilike", "%bar%"]] -// "foo|bar" => [["any", "ilike", "%foo%"], ["any", "ilike", "%bar%"]] -// " oo|bar" => [["any", "ilike", "%oo%"], ["any", "ilike", "%bar%"]] -// "" => [] -// " " => [] -// null => [] -window.ilike_filters = function(q) { - q = (q || '').replace(/[^-\w\.\/]+/g, ' ').trim().replace(/_/g, '\\_') - if (q == '') - return [] - return q.split(" ").map(function(term) { - return ["any", "ilike", "%"+term+"%"] - }) -} \ No newline at end of file diff --git a/apps/workbench/app/assets/javascripts/infinite_scroll.js b/apps/workbench/app/assets/javascripts/infinite_scroll.js deleted file mode 100644 index 3e63858594..0000000000 --- a/apps/workbench/app/assets/javascripts/infinite_scroll.js +++ /dev/null @@ -1,309 +0,0 @@ -// Copyright (C) The Arvados Authors. All rights reserved. -// -// SPDX-License-Identifier: AGPL-3.0 - -// infinite_scroll.js displays a tab's content using automatic scrolling -// when the user scrolls to the bottom of the page and there is more data. -// -// Usage: -// -// 1. Adding infinite scrolling to a tab pane using "show" method -// -// The steps below describe adding scrolling to the project#show action. -// -// a. In the "app/views/projects/" folder add a file for your tab -// (ex: _show_jobs_and_pipelines.html.erb) -// In this file, add a div or tbody with data-infinite-scroller. -// Note: This page uses _show_tab_contents.html.erb so that -// several tabs can reuse this implementation. -// Also add the filters to be used for loading the tab content. -// -// b. Add a file named "_show_contents_rows.html.erb" that loads -// the data (by invoking get_objects_and_names from the controller). -// -// c. In the "app/controllers/projects_controller.rb, -// Update the show method to add a block for "params[:partial]" -// that loads the show_contents_rows partial. -// Optionally, add a "tab_counts" method that loads the total number -// of objects count to be displayed for this tab. -// -// 2. Adding infinite scrolling to the "Recent" tab in "index" page -// The steps below describe adding scrolling to the pipeline_instances index page. -// -// a. In the "app/views/pipeline_instances/_show_recent.html.erb/" file -// add a div or tbody with data-infinite-scroller. -// -// b. Add the partial "_show_recent_rows.html.erb" that displays the -// page contents on scroll using the @objects - -function maybe_load_more_content(event) { - var scroller = this; - var $container = $(event.data.container); - var src; // url for retrieving content - var scrollHeight; - var spinner, colspan; - var serial = Date.now(); - var params; - scrollHeight = scroller.scrollHeight || $('body')[0].scrollHeight; - if ($(scroller).scrollTop() + $(scroller).height() - > - scrollHeight - 50) - { - if (!$container.attr('data-infinite-content-href0')) { - // Remember the first page source url, so we can refresh - // from page 1 later. - $container.attr('data-infinite-content-href0', - $container.attr('data-infinite-content-href')); - } - src = $container.attr('data-infinite-content-href'); - if (!src || !$container.is(':visible')) - // Finished - return; - - // Don't start another request until this one finishes - $container.attr('data-infinite-content-href', null); - spinner = '
'; - if ($container.is('table,tbody,thead,tfoot')) { - // Hack to determine how many columns a new tr should have - // in order to reach full width. - colspan = $container.closest('table'). - find('tr').eq(0).find('td,th').length; - if (colspan == 0) - colspan = '*'; - spinner = ('' + - spinner + - ''); - } - $container.find(".spinner").detach(); - $container.append(spinner); - $container.data('data-infinite-serial', serial); - - if (src == $container.attr('data-infinite-content-href0')) { - // If we're loading the first page, collect filters from - // various sources. - params = mergeInfiniteContentParams($container); - $.each(params, function(k,v) { - if (v instanceof Object) { - params[k] = JSON.stringify(v); - } - }); - } else { - // If we're loading page >1, ignore other filtering - // mechanisms and just use the "next page" URI from the - // previous page's response. Aside from avoiding race - // conditions (where page 2 could have different filters - // than page 1), this allows the server to use filters in - // the "next page" URI to achieve paging. (To apply any - // new filters effectively, we need to load page 1 again - // anyway.) - params = {}; - } - - $.ajax(src, - {dataType: 'json', - type: 'GET', - data: params, - context: {container: $container, src: src, serial: serial}}). - fail(function(jqxhr, status, error) { - var $faildiv; - var $container = this.container; - if ($container.data('data-infinite-serial') != this.serial) { - // A newer request is already in progress. - return; - } - if (jqxhr.readyState == 0 || jqxhr.status == 0) { - message = "Cancelled."; - } else if (jqxhr.responseJSON && jqxhr.responseJSON.errors) { - message = jqxhr.responseJSON.errors.join("; "); - } else { - message = "Request failed."; - } - // TODO: report the message to the user. - console.log(message); - $faildiv = $('
'). - attr('data-infinite-content-href', this.src). - addClass('infinite-retry'). - append(' Oops, request failed. '); - $container.find('div.spinner').replaceWith($faildiv); - }). - done(function(data, status, jqxhr) { - if ($container.data('data-infinite-serial') != this.serial) { - // A newer request is already in progress. - return; - } - $container.find(".spinner").detach(); - $container.append(data.content); - $container.attr('data-infinite-content-href', data.next_page_href); - ping_all_scrollers(); - }); - } -} - -function ping_all_scrollers() { - // Send a scroll event to all scroll listeners that might need - // updating. Adding infinite-scroller class to the window element - // doesn't work, so we add it explicitly here. - $('.infinite-scroller').add(window).trigger('scroll'); -} - -function mergeInfiniteContentParams($container) { - var params = {}; - // Combine infiniteContentParams from multiple sources. This - // mechanism allows each of several components to set and - // update its own set of filters, without having to worry - // about stomping on some other component's filters. - // - // For example, filterable.js writes filters in - // infiniteContentParamsFilterable ("search for text foo") - // without worrying about clobbering the filters set up by the - // tab pane ("only show container requests and pipeline instances - // in this tab"). - $.each($container.data(), function(datakey, datavalue) { - // Note: We attach these data to DOM elements using - // . We store/retrieve them - // using $('element').data('foo-bar'), although - // .data('fooBar') would also work. The "all data" hash - // returned by $('element').data(), however, always has - // keys like 'fooBar'. In other words, where we have a - // choice, we stick with the 'foo-bar' style to be - // consistent with HTML. Here, our only option is - // 'fooBar'. - if (/^infiniteContentParams/.exec(datakey)) { - if (datavalue instanceof Object) { - $.each(datavalue, function(hkey, hvalue) { - if (hvalue instanceof Array) { - params[hkey] = (params[hkey] || []). - concat(hvalue); - } else if (hvalue instanceof Object) { - $.extend(params[hkey], hvalue); - } else { - params[hkey] = hvalue; - } - }); - } - } - }); - return params; -} - -function setColumnSort( $container, $header, direction ) { - // $container should be the tbody or whatever has all the infinite table data attributes - // $header should be the th with a preset data-sort-order attribute - // direction should be "asc" or "desc" - // This function returns the order by clause for this column header as a string - - // First reset all sort directions - $('th[data-sort-order]').removeData('sort-order-direction'); - // set the current one - $header.data('sort-order-direction', direction); - // change the ordering parameter - var paramsAttr = 'infinite-content-params-' + $container.data('infinite-content-params-attr'); - var params = $container.data(paramsAttr) || {}; - params.order = $header.data('sort-order').split(",").join( ' ' + direction + ', ' ) + ' ' + direction; - $container.data(paramsAttr, params); - // show the correct icon next to the column header - $container.trigger('sort-icons'); - - return params.order; -} - -$(document). - on('click', 'div.infinite-retry button', function() { - var $retry_div = $(this).closest('.infinite-retry'); - var $container = $(this).closest('.infinite-scroller-ready') - $container.attr('data-infinite-content-href', - $retry_div.attr('data-infinite-content-href')); - $retry_div. - replaceWith('
'); - ping_all_scrollers(); - }). - on('refresh-content', '[data-infinite-scroller]', function() { - // Clear all rows, reset source href to initial state, and - // (if the container is visible) start loading content. - var first_page_href = $(this).attr('data-infinite-content-href0'); - if (!first_page_href) - first_page_href = $(this).attr('data-infinite-content-href'); - $(this). - html(''). - attr('data-infinite-content-href', first_page_href); - ping_all_scrollers(); - }). - on('ready ajax:complete', function() { - $('[data-infinite-scroller]').each(function() { - if ($(this).hasClass('infinite-scroller-ready')) - return; - $(this).addClass('infinite-scroller-ready'); - - // deal with sorting if there is any, and if it was set on this page for this tab already - if( $('th[data-sort-order]').length ) { - var tabId = $(this).closest('div.tab-pane').attr('id'); - if( hasHTML5History() && history.state !== undefined && history.state !== null && history.state.order !== undefined && history.state.order[tabId] !== undefined ) { - // we will use the list of one or more table columns associated with this header to find the right element - // see sortable_columns as it is passed to render_pane in the various tab .erbs (e.g. _show_jobs_and_pipelines.html.erb) - var strippedColumns = history.state.order[tabId].replace(/\s|\basc\b|\bdesc\b/g,''); - var sortDirection = history.state.order[tabId].split(" ")[1].replace(/,/,''); - $columnHeader = $(this).closest('table').find('[data-sort-order="'+ strippedColumns +'"]'); - setColumnSort( $(this), $columnHeader, sortDirection ); - } else { - // otherwise just reset the sort icons - $(this).trigger('sort-icons'); - } - } - - // $scroller is the DOM element that hears "scroll" - // events: sometimes it's a div, sometimes it's - // window. Here, "this" is the DOM element containing the - // result rows. We pass it to maybe_load_more_content in - // event.data. - var $scroller = $($(this).attr('data-infinite-scroller')); - if (!$scroller.hasClass('smart-scroll') && - 'scroll' != $scroller.css('overflow-y')) - $scroller = $(window); - $scroller. - addClass('infinite-scroller'). - on('scroll resize', { container: this }, maybe_load_more_content). - trigger('scroll'); - }); - }). - on('shown.bs.tab', 'a[data-toggle="tab"]', function(event) { - $(event.target.getAttribute('href') + ' [data-infinite-scroller]'). - trigger('scroll'); - }). - on('click', 'th[data-sort-order]', function() { - var direction = $(this).data('sort-order-direction'); - // reverse the current direction, or do ascending if none - if( direction === undefined || direction === 'desc' ) { - direction = 'asc'; - } else { - direction = 'desc'; - } - - var $container = $(this).closest('table').find('[data-infinite-content-params-attr]'); - - var order = setColumnSort( $container, $(this), direction ); - - // put it in the browser history state if browser allows it - if( hasHTML5History() ) { - var tabId = $(this).closest('div.tab-pane').attr('id'); - var state = history.state || {}; - if( state.order === undefined ) { - state.order = {}; - } - state.order[tabId] = order; - history.replaceState( state, null, null ); - } - - $container.trigger('refresh-content'); - }). - on('sort-icons', function() { - // set or reset the icon next to each sortable column header according to the current direction attribute - $('th[data-sort-order]').each(function() { - $(this).find('i').remove(); - var direction = $(this).data('sort-order-direction'); - if( direction !== undefined ) { - $(this).append(''); - } else { - $(this).append(''); - } - }); - }); diff --git a/apps/workbench/app/assets/javascripts/job_log_graph.js b/apps/workbench/app/assets/javascripts/job_log_graph.js deleted file mode 100644 index f47f4f1110..0000000000 --- a/apps/workbench/app/assets/javascripts/job_log_graph.js +++ /dev/null @@ -1,339 +0,0 @@ -// Copyright (C) The Arvados Authors. All rights reserved. -// -// SPDX-License-Identifier: AGPL-3.0 - -/* Assumes existence of: - window.jobGraphData = []; - window.jobGraphSeries = []; - window.jobGraphSortedSeries = []; - window.jobGraphMaxima = {}; - */ -function processLogLineForChart( logLine ) { - try { - var match = logLine.match(/^(\S+) (\S+) (\S+) (\S+) stderr crunchstat: (\S+) (.*)/); - if( !match ) { - match = logLine.match(/^((?:Sun|Mon|Tue|Wed|Thu|Fri|Sat) (?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) \d{1,2} \d\d:\d\d:\d\d \d{4}) (\S+) (\S+) (\S+) stderr crunchstat: (\S+) (.*)/); - if( match ) { - match[1] = (new Date(match[1] + ' UTC')).toISOString().replace('Z',''); - } - } - if( match ) { - var rawDetailData = ''; - var datum = null; - - // the timestamp comes first - var timestamp = match[1].replace('_','T') + 'Z'; - - // we are interested in "-- interval" recordings - var intervalMatch = match[6].match(/(.*) -- interval (.*)/); - if( intervalMatch ) { - var intervalData = intervalMatch[2].trim().split(' '); - var dt = parseFloat(intervalData[0]); - var dsum = 0.0; - for(var i=2; i < intervalData.length; i += 2 ) { - dsum += parseFloat(intervalData[i]); - } - datum = dsum/dt; - - if( datum < 0 ) { - // not interested in negative deltas - return; - } - - rawDetailData = intervalMatch[2]; - - // for the series name use the task number (4th term) and then the first word after 'crunchstat:' - var series = 'T' + match[4] + '-' + match[5]; - - // special calculation for cpus - if( /-cpu$/.test(series) ) { - // divide the stat by the number of cpus unless the time count is less than the interval length - if( dsum.toFixed(1) > dt.toFixed(1) ) { - var cpuCountMatch = intervalMatch[1].match(/(\d+) cpus/); - if( cpuCountMatch ) { - datum = datum / cpuCountMatch[1]; - } - } - } - - addJobGraphDatum( timestamp, datum, series, rawDetailData ); - } else { - // we are also interested in memory ("mem") recordings - var memoryMatch = match[6].match(/(\d+) cache (\d+) swap (\d+) pgmajfault (\d+) rss/); - if( memoryMatch ) { - rawDetailData = match[6]; - // one datapoint for rss and one for swap - only show the rawDetailData for rss - addJobGraphDatum( timestamp, parseInt(memoryMatch[4]), 'T' + match[4] + "-rss", rawDetailData ); - addJobGraphDatum( timestamp, parseInt(memoryMatch[2]), 'T' + match[4] + "-swap", '' ); - } else { - // not interested - return; - } - } - - window.redraw = true; - } - } catch( err ) { - console.log( 'Ignoring error trying to process log line: ' + err); - } -} - -function addJobGraphDatum(timestamp, datum, series, rawDetailData) { - // check for new series - if( $.inArray( series, jobGraphSeries ) < 0 ) { - var newIndex = jobGraphSeries.push(series) - 1; - jobGraphSortedSeries.push(newIndex); - jobGraphSortedSeries.sort( function(a,b) { - var matchA = jobGraphSeries[a].match(/^T(\d+)-(.*)/); - var matchB = jobGraphSeries[b].match(/^T(\d+)-(.*)/); - var termA = ('000000' + matchA[1]).slice(-6) + matchA[2]; - var termB = ('000000' + matchB[1]).slice(-6) + matchB[2]; - return termA > termB ? 1 : -1; - }); - jobGraphMaxima[series] = null; - window.recreate = true; - } - - if( datum !== 0 && ( jobGraphMaxima[series] === null || jobGraphMaxima[series] < datum ) ) { - if( isJobSeriesRescalable(series) ) { - // use old maximum to get a scale conversion - var scaleConversion = jobGraphMaxima[series]/datum; - // set new maximum and rescale the series - jobGraphMaxima[series] = datum; - rescaleJobGraphSeries( series, scaleConversion ); - } - } - - // scale - var scaledDatum = null; - if( isJobSeriesRescalable(series) && jobGraphMaxima[series] !== null && jobGraphMaxima[series] !== 0 ) { - scaledDatum = datum/jobGraphMaxima[series] - } else { - scaledDatum = datum; - } - // identify x axis point, searching from the end of the array (most recent) - var found = false; - for( var i = jobGraphData.length - 1; i >= 0; i-- ) { - if( jobGraphData[i]['t'] === timestamp ) { - found = true; - jobGraphData[i][series] = scaledDatum; - jobGraphData[i]['raw-'+series] = rawDetailData; - break; - } else if( jobGraphData[i]['t'] < timestamp ) { - // we've gone far enough back in time and this data is supposed to be sorted - break; - } - } - // index counter from previous loop will have gone one too far, so add one - var insertAt = i+1; - if(!found) { - // create a new x point for this previously unrecorded timestamp - var entry = { 't': timestamp }; - entry[series] = scaledDatum; - entry['raw-'+series] = rawDetailData; - jobGraphData.splice( insertAt, 0, entry ); - var shifted = []; - // now let's see about "scrolling" the graph, dropping entries that are too old (>10 minutes) - while( jobGraphData.length > 0 - && (Date.parse( jobGraphData[0]['t'] ) + 10*60000 < Date.parse( jobGraphData[jobGraphData.length-1]['t'] )) ) { - shifted.push(jobGraphData.shift()); - } - if( shifted.length > 0 ) { - // from those that we dropped, were any of them maxima? if so we need to rescale - jobGraphSeries.forEach( function(series) { - // test that every shifted entry in this series was either not a number (in which case we don't care) - // or else approximately (to 2 decimal places) smaller than the scaled maximum (i.e. 1), - // because otherwise we just scrolled off something that was a maximum point - // and so we need to recalculate a new maximum point by looking at all remaining displayed points in the series - if( isJobSeriesRescalable(series) && jobGraphMaxima[series] !== null - && !shifted.every( function(e) { return( !$.isNumeric(e[series]) || e[series].toFixed(2) < 1.0 ) } ) ) { - // check the remaining displayed points and find the new (scaled) maximum - var seriesMax = null; - jobGraphData.forEach( function(entry) { - if( $.isNumeric(entry[series]) && (seriesMax === null || entry[series] > seriesMax)) { - seriesMax = entry[series]; - } - }); - if( seriesMax !== null && seriesMax !== 0 ) { - // set new actual maximum using the new maximum as the conversion conversion and rescale the series - jobGraphMaxima[series] *= seriesMax; - var scaleConversion = 1/seriesMax; - rescaleJobGraphSeries( series, scaleConversion ); - } - else { - // we no longer have any data points displaying for this series - jobGraphMaxima[series] = null; - } - } - }); - } - // add a 10 minute old null data point to keep the chart honest if the oldest point is less than 9.9 minutes old - if( jobGraphData.length > 0 ) { - var earliestTimestamp = jobGraphData[0]['t']; - var mostRecentTimestamp = jobGraphData[jobGraphData.length-1]['t']; - if( (Date.parse( earliestTimestamp ) + 9.9*60000 > Date.parse( mostRecentTimestamp )) ) { - var tenMinutesBefore = (new Date(Date.parse( mostRecentTimestamp ) - 600*1000)).toISOString(); - jobGraphData.unshift( { 't': tenMinutesBefore } ); - } - } - } - -} - -function createJobGraph(elementName) { - delete jobGraph; - var emptyGraph = false; - if( jobGraphData.length === 0 ) { - // If there is no data we still want to show an empty graph, - // so add an empty datum and placeholder series to fool it - // into displaying itself. Note that when finally a new - // series is added, the graph will be recreated anyway. - jobGraphData.push( {} ); - jobGraphSeries.push( '' ); - emptyGraph = true; - } - var graphteristics = { - element: elementName, - data: jobGraphData, - ymax: 1.0, - yLabelFormat: function () { return ''; }, - xkey: 't', - ykeys: jobGraphSeries, - labels: jobGraphSeries, - resize: true, - hideHover: 'auto', - parseTime: true, - hoverCallback: function(index, options, content) { - var s = ''; - for (var i=0; i < jobGraphSortedSeries.length; i++) { - var sortedIndex = jobGraphSortedSeries[i]; - var series = options.ykeys[sortedIndex]; - var datum = options.data[index][series]; - var point = '' - point += "
"; - var labelMatch = options.labels[sortedIndex].match(/^T(\d+)-(.*)/); - point += 'Task ' + labelMatch[1] + ' ' + labelMatch[2]; - point += ": "; - if ( datum !== undefined ) { - if( isJobSeriesRescalable( series ) ) { - datum *= jobGraphMaxima[series]; - } - if( parseFloat(datum) !== 0 ) { - if( /-cpu$/.test(series) ){ - datum = $.number(datum * 100, 1) + '%'; - } else if( datum < 10 ) { - datum = $.number(datum, 2); - } else { - datum = $.number(datum); - } - if(options.data[index]['raw-'+series]) { - datum += ' (' + options.data[index]['raw-'+series] + ')'; - } - } - point += datum; - } else { - continue; - } - point += "
"; - s += point; - } - if (s === '') { - // No Y coordinates? This isn't a real data point, - // it's just the placeholder we use to make sure the - // graph can render when empty. Don't show a tooltip. - return ''; - } - return ("
" + - options.data[index][options.xkey] + - "
" + s); - } - } - if( emptyGraph ) { - graphteristics['axes'] = false; - graphteristics['parseTime'] = false; - graphteristics['hideHover'] = 'always'; - } - $('#' + elementName).html(''); - window.jobGraph = Morris.Line( graphteristics ); - if( emptyGraph ) { - jobGraphData = []; - jobGraphSeries = []; - } -} - -function rescaleJobGraphSeries( series, scaleConversion ) { - if( isJobSeriesRescalable() ) { - $.each( jobGraphData, function( i, entry ) { - if( entry[series] !== null && entry[series] !== undefined ) { - entry[series] *= scaleConversion; - } - }); - } -} - -// that's right - we never do this for the 'cpu' series, which will always be between 0 and 1 anyway -function isJobSeriesRescalable( series ) { - return !/-cpu$/.test(series); -} - -function processLogEventForGraph(event, eventData) { - if( eventData.properties.text ) { - eventData.properties.text.split('\n').forEach( function( logLine ) { - processLogLineForChart( logLine ); - } ); - } -} - -$(document).on('arv-log-event', '#log_graph_div', function(event, eventData) { - processLogEventForGraph(event, eventData); - if (!window.jobGraphShown) { - // Draw immediately, instead of waiting for the 5-second - // timer. - redrawIfNeeded.call(window, this); - } -}); - -function redrawIfNeeded(graph_div) { - if (!window.redraw) { - return; - } - window.redraw = false; - - if (window.recreate) { - // Series have changed: we need to draw an entirely new graph. - // Running createJobGraph in a show() callback ensures the div - // is fully shown when morris uses it to size its svg element. - $(graph_div).show(0, createJobGraph.bind(window, $(graph_div).attr('id'))); - window.jobGraphShown = true; - window.recreate = false; - } else { - window.jobGraph.setData(window.jobGraphData); - } -} - -$(document).on('ready ajax:complete', function() { - $('#log_graph_div').not('.graph-is-setup').addClass('graph-is-setup').each( function( index, graph_div ) { - window.jobGraphShown = false; - window.jobGraphData = []; - window.jobGraphSeries = []; - window.jobGraphSortedSeries = []; - window.jobGraphMaxima = {}; - window.recreate = false; - window.redraw = false; - - $.get('/jobs/' + $(graph_div).data('object-uuid') + '/logs.json', function(data) { - data.forEach( function( entry ) { - processLogEventForGraph({}, entry); - }); - // Update the graph now to show the recent data points - // received via /logs.json (along with any new data points - // we received via websockets while waiting for /logs.json - // to respond). - redrawIfNeeded(graph_div); - }); - - setInterval(redrawIfNeeded.bind(window, graph_div), 5000); - }); -}); diff --git a/apps/workbench/app/assets/javascripts/jquery.number.min.js b/apps/workbench/app/assets/javascripts/jquery.number.min.js deleted file mode 100644 index 4fce02b17e..0000000000 --- a/apps/workbench/app/assets/javascripts/jquery.number.min.js +++ /dev/null @@ -1,2 +0,0 @@ -/*! jQuery number 2.1.5 (c) github.com/teamdf/jquery-number | opensource.teamdf.com/license */ -(function(e){"use strict";function t(e,t){if(this.createTextRange){var n=this.createTextRange();n.collapse(true);n.moveStart("character",e);n.moveEnd("character",t-e);n.select()}else if(this.setSelectionRange){this.focus();this.setSelectionRange(e,t)}}function n(e){var t=this.value.length;e=e.toLowerCase()=="start"?"Start":"End";if(document.selection){var n=document.selection.createRange(),r,i,s;r=n.duplicate();r.expand("textedit");r.setEndPoint("EndToEnd",n);i=r.text.length-n.text.length;s=i+n.text.length;return e=="Start"?i:s}else if(typeof this["selection"+e]!="undefined"){t=this["selection"+e]}return t}var r={codes:{46:127,188:44,109:45,190:46,191:47,192:96,220:92,222:39,221:93,219:91,173:45,187:61,186:59,189:45,110:46},shifts:{96:"~",49:"!",50:"@",51:"#",52:"$",53:"%",54:"^",55:"&",56:"*",57:"(",48:")",45:"_",61:"+",91:"{",93:"}",92:"|",59:":",39:'"',44:"<",46:">",47:"?"}};e.fn.number=function(i,s,o,u){u=typeof u==="undefined"?",":u;o=typeof o==="undefined"?".":o;s=typeof s==="undefined"?0:s;var a="\\u"+("0000"+o.charCodeAt(0).toString(16)).slice(-4),f=new RegExp("[^"+a+"0-9]","g"),l=new RegExp(a,"g");if(i===true){if(this.is("input:text")){return this.on({"keydown.format":function(i){var a=e(this),f=a.data("numFormat"),l=i.keyCode?i.keyCode:i.which,c="",h=n.apply(this,["start"]),p=n.apply(this,["end"]),d="",v=false;if(r.codes.hasOwnProperty(l)){l=r.codes[l]}if(!i.shiftKey&&l>=65&&l<=90){l+=32}else if(!i.shiftKey&&l>=69&&l<=105){l-=48}else if(i.shiftKey&&r.shifts.hasOwnProperty(l)){c=r.shifts[l]}if(c=="")c=String.fromCharCode(l);if(l!=8&&l!=45&&l!=127&&c!=o&&!c.match(/[0-9]/)){var m=i.keyCode?i.keyCode:i.which;if(m==46||m==8||m==127||m==9||m==27||m==13||(m==65||m==82||m==80||m==83||m==70||m==72||m==66||m==74||m==84||m==90||m==61||m==173||m==48)&&(i.ctrlKey||i.metaKey)===true||(m==86||m==67||m==88)&&(i.ctrlKey||i.metaKey)===true||m>=35&&m<=39||m>=112&&m<=123){return}i.preventDefault();return false}if(h==0&&p==this.value.length||a.val()==0){if(l==8){h=p=1;this.value="";f.init=s>0?-1:0;f.c=s>0?-(s+1):0;t.apply(this,[0,0])}else if(c==o){h=p=1;this.value="0"+o+(new Array(s+1)).join("0");f.init=s>0?1:0;f.c=s>0?-(s+1):0}else if(l==45){h=p=2;this.value="-0"+o+(new Array(s+1)).join("0");f.init=s>0?1:0;f.c=s>0?-(s+1):0;t.apply(this,[2,2])}else{f.init=s>0?-1:0;f.c=s>0?-s:0}}else{f.c=p-this.value.length}f.isPartialSelection=h==p?false:true;if(s>0&&c==o&&h==this.value.length-s-1){f.c++;f.init=Math.max(0,f.init);i.preventDefault();v=this.value.length+f.c}else if(l==45&&(h!=0||this.value.indexOf("-")==0)){i.preventDefault()}else if(c==o){f.init=Math.max(0,f.init);i.preventDefault()}else if(s>0&&l==127&&h==this.value.length-s-1){i.preventDefault()}else if(s>0&&l==8&&h==this.value.length-s){i.preventDefault();f.c--;v=this.value.length+f.c}else if(s>0&&l==127&&h>this.value.length-s-1){if(this.value==="")return;if(this.value.slice(h,h+1)!="0"){d=this.value.slice(0,h)+"0"+this.value.slice(h+1);a.val(d)}i.preventDefault();v=this.value.length+f.c}else if(s>0&&l==8&&h>this.value.length-s){if(this.value==="")return;if(this.value.slice(h-1,h)!="0"){d=this.value.slice(0,h-1)+"0"+this.value.slice(h);a.val(d)}i.preventDefault();f.c--;v=this.value.length+f.c}else if(l==127&&this.value.slice(h,h+1)==u){i.preventDefault()}else if(l==8&&this.value.slice(h-1,h)==u){i.preventDefault();f.c--;v=this.value.length+f.c}else if(s>0&&h==p&&this.value.length>s+1&&h>this.value.length-s-1&&isFinite(+c)&&!i.metaKey&&!i.ctrlKey&&!i.altKey&&c.length===1){if(p===this.value.length){d=this.value.slice(0,h-1)}else{d=this.value.slice(0,h)+this.value.slice(h+1)}this.value=d;v=h}if(v!==false){t.apply(this,[v,v])}a.data("numFormat",f)},"keyup.format":function(r){var i=e(this),o=i.data("numFormat"),u=r.keyCode?r.keyCode:r.which,a=n.apply(this,["start"]),f=n.apply(this,["end"]),l;if(a===0&&f===0&&(u===189||u===109)){i.val("-"+i.val());a=1;o.c=1-this.value.length;o.init=1;i.data("numFormat",o);l=this.value.length+o.c;t.apply(this,[l,l])}if(this.value===""||(u<48||u>57)&&(u<96||u>105)&&u!==8&&u!==46&&u!==110)return;i.val(i.val());if(s>0){if(o.init<1){a=this.value.length-s-(o.init<0?1:0);o.c=a-this.value.length;o.init=1;i.data("numFormat",o)}else if(a>this.value.length-s&&u!=8){o.c++;i.data("numFormat",o)}}if(u==46&&!o.isPartialSelection){o.c++;i.data("numFormat",o)}l=this.value.length+o.c;t.apply(this,[l,l])},"paste.format":function(t){var n=e(this),r=t.originalEvent,i=null;if(window.clipboardData&&window.clipboardData.getData){i=window.clipboardData.getData("Text")}else if(r.clipboardData&&r.clipboardData.getData){i=r.clipboardData.getData("text/plain")}n.val(i);t.preventDefault();return false}}).each(function(){var t=e(this).data("numFormat",{c:-(s+1),decimals:s,thousands_sep:u,dec_point:o,regex_dec_num:f,regex_dec:l,init:this.value.indexOf(".")?true:false});if(this.value==="")return;t.val(t.val())})}else{return this.each(function(){var t=e(this),n=+t.text().replace(f,"").replace(l,".");t.number(!isFinite(n)?0:+n,s,o,u)})}}return this.text(e.number.apply(window,arguments))};var i=null,s=null;if(e.isPlainObject(e.valHooks.text)){if(e.isFunction(e.valHooks.text.get))i=e.valHooks.text.get;if(e.isFunction(e.valHooks.text.set))s=e.valHooks.text.set}else{e.valHooks.text={}}e.valHooks.text.get=function(t){var n=e(t),r,s,o=n.data("numFormat");if(!o){if(e.isFunction(i)){return i(t)}else{return undefined}}else{if(t.value==="")return"";r=+t.value.replace(o.regex_dec_num,"").replace(o.regex_dec,".");return(t.value.indexOf("-")===0?"-":"")+(isFinite(r)?r:0)}};e.valHooks.text.set=function(t,n){var r=e(t),i=r.data("numFormat");if(!i){if(e.isFunction(s)){return s(t,n)}else{return undefined}}else{var o=e.number(n,i.decimals,i.dec_point,i.thousands_sep);return t.value=o}};e.number=function(e,t,n,r){r=typeof r==="undefined"?",":r;n=typeof n==="undefined"?".":n;t=!isFinite(+t)?0:Math.abs(t);var i="\\u"+("0000"+n.charCodeAt(0).toString(16)).slice(-4);var s="\\u"+("0000"+r.charCodeAt(0).toString(16)).slice(-4);e=(e+"").replace(".",n).replace(new RegExp(s,"g"),"").replace(new RegExp(i,"g"),".").replace(new RegExp("[^0-9+-Ee.]","g"),"");var o=!isFinite(+e)?0:+e,u="",a=function(e,t){var n=Math.pow(10,t);return""+Math.round(e*n)/n};u=(t?a(o,t):""+Math.round(o)).split(".");if(u[0].length>3){u[0]=u[0].replace(/\B(?=(?:\d{3})+(?!\d))/g,r)}if((u[1]||"").length yD ) return 1 * mult; - // natural sorting through split numeric strings and default strings - for(var cLoc=0, numS=Math.max(xN.length, yN.length); cLoc < numS; cLoc++) { - // find floats not starting with '0', string or 0 if not defined (Clint Priest) - oFxNcL = !(xN[cLoc] || '').match(ore) && parseFloat(xN[cLoc]) || xN[cLoc] || 0; - oFyNcL = !(yN[cLoc] || '').match(ore) && parseFloat(yN[cLoc]) || yN[cLoc] || 0; - // handle numeric vs string comparison - number < string - (Kyle Adams) - if (isNaN(oFxNcL) !== isNaN(oFyNcL)) { return (isNaN(oFxNcL)) ? 1 : -1; } - // rely on string comparison if different types - i.e. '02' < 2 != '02' < '2' - else if (typeof oFxNcL !== typeof oFyNcL) { - oFxNcL += ''; - oFyNcL += ''; - } - if (oFxNcL < oFyNcL) return -1 * mult; - if (oFxNcL > oFyNcL) return 1 * mult; - } - return 0; -}; - -/* -var defaultSort = getSortFunction(); - -module.exports = function(a, b, options) { - if (arguments.length == 1) { - options = a; - return getSortFunction(options); - } else { - return defaultSort(a,b); - } -} -*/ -}); -require.register("javve-to-string/index.js", function(exports, require, module){ -module.exports = function(s) { - s = (s === undefined) ? "" : s; - s = (s === null) ? "" : s; - s = s.toString(); - return s; -}; - -}); -require.register("component-type/index.js", function(exports, require, module){ -/** - * toString ref. - */ - -var toString = Object.prototype.toString; - -/** - * Return the type of `val`. - * - * @param {Mixed} val - * @return {String} - * @api public - */ - -module.exports = function(val){ - switch (toString.call(val)) { - case '[object Date]': return 'date'; - case '[object RegExp]': return 'regexp'; - case '[object Arguments]': return 'arguments'; - case '[object Array]': return 'array'; - case '[object Error]': return 'error'; - } - - if (val === null) return 'null'; - if (val === undefined) return 'undefined'; - if (val !== val) return 'nan'; - if (val && val.nodeType === 1) return 'element'; - - return typeof val.valueOf(); -}; - -}); -require.register("list.js/index.js", function(exports, require, module){ -/* -ListJS with beta 1.0.0 -By Jonny Strömberg (www.jonnystromberg.com, www.listjs.com) -*/ -(function( window, undefined ) { -"use strict"; - -var document = window.document, - getByClass = require('get-by-class'), - extend = require('extend'), - indexOf = require('indexof'); - -var List = function(id, options, values) { - - var self = this, - init, - Item = require('./src/item')(self), - addAsync = require('./src/add-async')(self), - parse = require('./src/parse')(self); - - init = { - start: function() { - self.listClass = "list"; - self.searchClass = "search"; - self.sortClass = "sort"; - self.page = 200; - self.i = 1; - self.items = []; - self.visibleItems = []; - self.matchingItems = []; - self.searched = false; - self.filtered = false; - self.handlers = { 'updated': [] }; - self.plugins = {}; - self.helpers = { - getByClass: getByClass, - extend: extend, - indexOf: indexOf - }; - - extend(self, options); - - self.listContainer = (typeof(id) === 'string') ? document.getElementById(id) : id; - if (!self.listContainer) { return; } - self.list = getByClass(self.listContainer, self.listClass, true); - - self.templater = require('./src/templater')(self); - self.search = require('./src/search')(self); - self.filter = require('./src/filter')(self); - self.sort = require('./src/sort')(self); - - this.items(); - self.update(); - this.plugins(); - }, - items: function() { - parse(self.list); - if (values !== undefined) { - self.add(values); - } - }, - plugins: function() { - for (var i = 0; i < self.plugins.length; i++) { - var plugin = self.plugins[i]; - self[plugin.name] = plugin; - plugin.init(self); - } - } - }; - - - /* - * Add object to list - */ - this.add = function(values, callback) { - if (callback) { - addAsync(values, callback); - return; - } - var added = [], - notCreate = false; - if (values[0] === undefined){ - values = [values]; - } - for (var i = 0, il = values.length; i < il; i++) { - var item = null; - if (values[i] instanceof Item) { - item = values[i]; - item.reload(); - } else { - notCreate = (self.items.length > self.page) ? true : false; - item = new Item(values[i], undefined, notCreate); - } - self.items.push(item); - added.push(item); - } - self.update(); - return added; - }; - - this.show = function(i, page) { - this.i = i; - this.page = page; - self.update(); - return self; - }; - - /* Removes object from list. - * Loops through the list and removes objects where - * property "valuename" === value - */ - this.remove = function(valueName, value, options) { - var found = 0; - for (var i = 0, il = self.items.length; i < il; i++) { - if (self.items[i].values()[valueName] == value) { - self.templater.remove(self.items[i], options); - self.items.splice(i,1); - il--; - i--; - found++; - } - } - self.update(); - return found; - }; - - /* Gets the objects in the list which - * property "valueName" === value - */ - this.get = function(valueName, value) { - var matchedItems = []; - for (var i = 0, il = self.items.length; i < il; i++) { - var item = self.items[i]; - if (item.values()[valueName] == value) { - matchedItems.push(item); - } - } - return matchedItems; - }; - - /* - * Get size of the list - */ - this.size = function() { - return self.items.length; - }; - - /* - * Removes all items from the list - */ - this.clear = function() { - self.templater.clear(); - self.items = []; - return self; - }; - - this.on = function(event, callback) { - self.handlers[event].push(callback); - return self; - }; - - this.off = function(event, callback) { - var e = self.handlers[event]; - var index = indexOf(e, callback); - if (index > -1) { - e.splice(index, 1); - } - return self; - }; - - this.trigger = function(event) { - var i = self.handlers[event].length; - while(i--) { - self.handlers[event][i](self); - } - return self; - }; - - this.reset = { - filter: function() { - var is = self.items, - il = is.length; - while (il--) { - is[il].filtered = false; - } - return self; - }, - search: function() { - var is = self.items, - il = is.length; - while (il--) { - is[il].found = false; - } - return self; - } - }; - - this.update = function() { - var is = self.items, - il = is.length; - - self.visibleItems = []; - self.matchingItems = []; - self.templater.clear(); - for (var i = 0; i < il; i++) { - if (is[i].matching() && ((self.matchingItems.length+1) >= self.i && self.visibleItems.length < self.page)) { - is[i].show(); - self.visibleItems.push(is[i]); - self.matchingItems.push(is[i]); - } else if (is[i].matching()) { - self.matchingItems.push(is[i]); - is[i].hide(); - } else { - is[i].hide(); - } - } - self.trigger('updated'); - return self; - }; - - init.start(); -}; - -module.exports = List; - -})(window); - -}); -require.register("list.js/src/search.js", function(exports, require, module){ -var events = require('events'), - getByClass = require('get-by-class'), - toString = require('to-string'); - -module.exports = function(list) { - var item, - text, - columns, - searchString, - customSearch; - - var prepare = { - resetList: function() { - list.i = 1; - list.templater.clear(); - customSearch = undefined; - }, - setOptions: function(args) { - if (args.length == 2 && args[1] instanceof Array) { - columns = args[1]; - } else if (args.length == 2 && typeof(args[1]) == "function") { - customSearch = args[1]; - } else if (args.length == 3) { - columns = args[1]; - customSearch = args[2]; - } - }, - setColumns: function() { - columns = (columns === undefined) ? prepare.toArray(list.items[0].values()) : columns; - }, - setSearchString: function(s) { - s = toString(s).toLowerCase(); - s = s.replace(/[-[\]{}()*+?.,\\^$|#]/g, "\\$&"); // Escape regular expression characters - searchString = s; - }, - toArray: function(values) { - var tmpColumn = []; - for (var name in values) { - tmpColumn.push(name); - } - return tmpColumn; - } - }; - var search = { - list: function() { - for (var k = 0, kl = list.items.length; k < kl; k++) { - search.item(list.items[k]); - } - }, - item: function(item) { - item.found = false; - for (var j = 0, jl = columns.length; j < jl; j++) { - if (search.values(item.values(), columns[j])) { - item.found = true; - return; - } - } - }, - values: function(values, column) { - if (values.hasOwnProperty(column)) { - text = toString(values[column]).toLowerCase(); - if ((searchString !== "") && (text.search(searchString) > -1)) { - return true; - } - } - return false; - }, - reset: function() { - list.reset.search(); - list.searched = false; - } - }; - - var searchMethod = function(str) { - list.trigger('searchStart'); - - prepare.resetList(); - prepare.setSearchString(str); - prepare.setOptions(arguments); // str, cols|searchFunction, searchFunction - prepare.setColumns(); - - if (searchString === "" ) { - search.reset(); - } else { - list.searched = true; - if (customSearch) { - customSearch(searchString, columns); - } else { - search.list(); - } - } - - list.update(); - list.trigger('searchComplete'); - return list.visibleItems; - }; - - list.handlers.searchStart = list.handlers.searchStart || []; - list.handlers.searchComplete = list.handlers.searchComplete || []; - - events.bind(getByClass(list.listContainer, list.searchClass), 'keyup', function(e) { - var target = e.target || e.srcElement, // IE have srcElement - alreadyCleared = (target.value === "" && !list.searched); - if (!alreadyCleared) { // If oninput already have resetted the list, do nothing - searchMethod(target.value); - } - }); - - // Used to detect click on HTML5 clear button - events.bind(getByClass(list.listContainer, list.searchClass), 'input', function(e) { - var target = e.target || e.srcElement; - if (target.value === "") { - searchMethod(''); - } - }); - - list.helpers.toString = toString; - return searchMethod; -}; - -}); -require.register("list.js/src/sort.js", function(exports, require, module){ -var naturalSort = require('natural-sort'), - classes = require('classes'), - events = require('events'), - getByClass = require('get-by-class'), - getAttribute = require('get-attribute'); - -module.exports = function(list) { - list.sortFunction = list.sortFunction || function(itemA, itemB, options) { - options.desc = options.order == "desc" ? true : false; // Natural sort uses this format - return naturalSort(itemA.values()[options.valueName], itemB.values()[options.valueName], options); - }; - - var buttons = { - els: undefined, - clear: function() { - for (var i = 0, il = buttons.els.length; i < il; i++) { - classes(buttons.els[i]).remove('asc'); - classes(buttons.els[i]).remove('desc'); - } - }, - getOrder: function(btn) { - var predefinedOrder = getAttribute(btn, 'data-order'); - if (predefinedOrder == "asc" || predefinedOrder == "desc") { - return predefinedOrder; - } else if (classes(btn).has('desc')) { - return "asc"; - } else if (classes(btn).has('asc')) { - return "desc"; - } else { - return "asc"; - } - }, - getInSensitive: function(btn, options) { - var insensitive = getAttribute(btn, 'data-insensitive'); - if (insensitive === "true") { - options.insensitive = true; - } else { - options.insensitive = false; - } - }, - setOrder: function(options) { - for (var i = 0, il = buttons.els.length; i < il; i++) { - var btn = buttons.els[i]; - if (getAttribute(btn, 'data-sort') !== options.valueName) { - continue; - } - var predefinedOrder = getAttribute(btn, 'data-order'); - if (predefinedOrder == "asc" || predefinedOrder == "desc") { - if (predefinedOrder == options.order) { - classes(btn).add(options.order); - } - } else { - classes(btn).add(options.order); - } - } - } - }; - var sort = function() { - list.trigger('sortStart'); - options = {}; - - var target = arguments[0].currentTarget || arguments[0].srcElement || undefined; - - if (target) { - options.valueName = getAttribute(target, 'data-sort'); - buttons.getInSensitive(target, options); - options.order = buttons.getOrder(target); - } else { - options = arguments[1] || options; - options.valueName = arguments[0]; - options.order = options.order || "asc"; - options.insensitive = (typeof options.insensitive == "undefined") ? true : options.insensitive; - } - buttons.clear(); - buttons.setOrder(options); - - options.sortFunction = options.sortFunction || list.sortFunction; - list.items.sort(function(a, b) { - return options.sortFunction(a, b, options); - }); - list.update(); - list.trigger('sortComplete'); - }; - - // Add handlers - list.handlers.sortStart = list.handlers.sortStart || []; - list.handlers.sortComplete = list.handlers.sortComplete || []; - - buttons.els = getByClass(list.listContainer, list.sortClass); - events.bind(buttons.els, 'click', sort); - list.on('searchStart', buttons.clear); - list.on('filterStart', buttons.clear); - - // Helpers - list.helpers.classes = classes; - list.helpers.naturalSort = naturalSort; - list.helpers.events = events; - list.helpers.getAttribute = getAttribute; - - return sort; -}; - -}); -require.register("list.js/src/item.js", function(exports, require, module){ -module.exports = function(list) { - return function(initValues, element, notCreate) { - var item = this; - - this._values = {}; - - this.found = false; // Show if list.searched == true and this.found == true - this.filtered = false;// Show if list.filtered == true and this.filtered == true - - var init = function(initValues, element, notCreate) { - if (element === undefined) { - if (notCreate) { - item.values(initValues, notCreate); - } else { - item.values(initValues); - } - } else { - item.elm = element; - var values = list.templater.get(item, initValues); - item.values(values); - } - }; - this.values = function(newValues, notCreate) { - if (newValues !== undefined) { - for(var name in newValues) { - item._values[name] = newValues[name]; - } - if (notCreate !== true) { - list.templater.set(item, item.values()); - } - } else { - return item._values; - } - }; - this.show = function() { - list.templater.show(item); - }; - this.hide = function() { - list.templater.hide(item); - }; - this.matching = function() { - return ( - (list.filtered && list.searched && item.found && item.filtered) || - (list.filtered && !list.searched && item.filtered) || - (!list.filtered && list.searched && item.found) || - (!list.filtered && !list.searched) - ); - }; - this.visible = function() { - return (item.elm.parentNode == list.list) ? true : false; - }; - init(initValues, element, notCreate); - }; -}; - -}); -require.register("list.js/src/templater.js", function(exports, require, module){ -var getByClass = require('get-by-class'); - -var Templater = function(list) { - var itemSource = getItemSource(list.item), - templater = this; - - function getItemSource(item) { - if (item === undefined) { - var nodes = list.list.childNodes, - items = []; - - for (var i = 0, il = nodes.length; i < il; i++) { - // Only textnodes have a data attribute - if (nodes[i].data === undefined) { - return nodes[i]; - } - } - return null; - } else if (item.indexOf("<") !== -1) { // Try create html element of list, do not work for tables!! - var div = document.createElement('div'); - div.innerHTML = item; - return div.firstChild; - } else { - return document.getElementById(list.item); - } - } - - /* Get values from element */ - this.get = function(item, valueNames) { - templater.create(item); - var values = {}; - for(var i = 0, il = valueNames.length; i < il; i++) { - var elm = getByClass(item.elm, valueNames[i], true); - values[valueNames[i]] = elm ? elm.innerHTML : ""; - } - return values; - }; - - /* Sets values at element */ - this.set = function(item, values) { - if (!templater.create(item)) { - for(var v in values) { - if (values.hasOwnProperty(v)) { - // TODO speed up if possible - var elm = getByClass(item.elm, v, true); - if (elm) { - /* src attribute for image tag & text for other tags */ - if (elm.tagName === "IMG" && values[v] !== "") { - elm.src = values[v]; - } else { - elm.innerHTML = values[v]; - } - } - } - } - } - }; - - this.create = function(item) { - if (item.elm !== undefined) { - return false; - } - /* If item source does not exists, use the first item in list as - source for new items */ - var newItem = itemSource.cloneNode(true); - newItem.removeAttribute('id'); - item.elm = newItem; - templater.set(item, item.values()); - return true; - }; - this.remove = function(item) { - list.list.removeChild(item.elm); - }; - this.show = function(item) { - templater.create(item); - list.list.appendChild(item.elm); - }; - this.hide = function(item) { - if (item.elm !== undefined && item.elm.parentNode === list.list) { - list.list.removeChild(item.elm); - } - }; - this.clear = function() { - /* .innerHTML = ''; fucks up IE */ - if (list.list.hasChildNodes()) { - while (list.list.childNodes.length >= 1) - { - list.list.removeChild(list.list.firstChild); - } - } - }; -}; - -module.exports = function(list) { - return new Templater(list); -}; - -}); -require.register("list.js/src/filter.js", function(exports, require, module){ -module.exports = function(list) { - - // Add handlers - list.handlers.filterStart = list.handlers.filterStart || []; - list.handlers.filterComplete = list.handlers.filterComplete || []; - - return function(filterFunction) { - list.trigger('filterStart'); - list.i = 1; // Reset paging - list.reset.filter(); - if (filterFunction === undefined) { - list.filtered = false; - } else { - list.filtered = true; - var is = list.items; - for (var i = 0, il = is.length; i < il; i++) { - var item = is[i]; - if (filterFunction(item)) { - item.filtered = true; - } else { - item.filtered = false; - } - } - } - list.update(); - list.trigger('filterComplete'); - return list.visibleItems; - }; -}; - -}); -require.register("list.js/src/add-async.js", function(exports, require, module){ -module.exports = function(list) { - return function(values, callback, items) { - var valuesToAdd = values.splice(0, 100); - items = items || []; - items = items.concat(list.add(valuesToAdd)); - if (values.length > 0) { - setTimeout(function() { - addAsync(values, callback, items); - }, 10); - } else { - list.update(); - callback(items); - } - }; -}; -}); -require.register("list.js/src/parse.js", function(exports, require, module){ -module.exports = function(list) { - - var Item = require('./item')(list); - - var getChildren = function(parent) { - var nodes = parent.childNodes, - items = []; - for (var i = 0, il = nodes.length; i < il; i++) { - // Only textnodes have a data attribute - if (nodes[i].data === undefined) { - items.push(nodes[i]); - } - } - return items; - }; - - var parse = function(itemElements, valueNames) { - for (var i = 0, il = itemElements.length; i < il; i++) { - list.items.push(new Item(valueNames, itemElements[i])); - } - }; - var parseAsync = function(itemElements, valueNames) { - var itemsToIndex = itemElements.splice(0, 100); // TODO: If < 100 items, what happens in IE etc? - parse(itemsToIndex, valueNames); - if (itemElements.length > 0) { - setTimeout(function() { - init.items.indexAsync(itemElements, valueNames); - }, 10); - } else { - list.update(); - // TODO: Add indexed callback - } - }; - - return function() { - var itemsToIndex = getChildren(list.list), - valueNames = list.valueNames; - - if (list.indexAsync) { - parseAsync(itemsToIndex, valueNames); - } else { - parse(itemsToIndex, valueNames); - } - }; -}; - -}); - - - - - - - - - - - - - - - - - - - - -require.alias("component-classes/index.js", "list.js/deps/classes/index.js"); -require.alias("component-classes/index.js", "classes/index.js"); -require.alias("component-indexof/index.js", "component-classes/deps/indexof/index.js"); - -require.alias("segmentio-extend/index.js", "list.js/deps/extend/index.js"); -require.alias("segmentio-extend/index.js", "extend/index.js"); - -require.alias("component-indexof/index.js", "list.js/deps/indexof/index.js"); -require.alias("component-indexof/index.js", "indexof/index.js"); - -require.alias("javve-events/index.js", "list.js/deps/events/index.js"); -require.alias("javve-events/index.js", "events/index.js"); -require.alias("component-event/index.js", "javve-events/deps/event/index.js"); - -require.alias("timoxley-to-array/index.js", "javve-events/deps/to-array/index.js"); - -require.alias("javve-get-by-class/index.js", "list.js/deps/get-by-class/index.js"); -require.alias("javve-get-by-class/index.js", "get-by-class/index.js"); - -require.alias("javve-get-attribute/index.js", "list.js/deps/get-attribute/index.js"); -require.alias("javve-get-attribute/index.js", "get-attribute/index.js"); - -require.alias("javve-natural-sort/index.js", "list.js/deps/natural-sort/index.js"); -require.alias("javve-natural-sort/index.js", "natural-sort/index.js"); - -require.alias("javve-to-string/index.js", "list.js/deps/to-string/index.js"); -require.alias("javve-to-string/index.js", "list.js/deps/to-string/index.js"); -require.alias("javve-to-string/index.js", "to-string/index.js"); -require.alias("javve-to-string/index.js", "javve-to-string/index.js"); -require.alias("component-type/index.js", "list.js/deps/type/index.js"); -require.alias("component-type/index.js", "type/index.js"); -if (typeof exports == "object") { - module.exports = require("list.js"); -} else if (typeof define == "function" && define.amd) { - define(function(){ return require("list.js"); }); -} else { - this["List"] = require("list.js"); -}})(); \ No newline at end of file diff --git a/apps/workbench/app/assets/javascripts/log_viewer.js b/apps/workbench/app/assets/javascripts/log_viewer.js deleted file mode 100644 index b201ed7f10..0000000000 --- a/apps/workbench/app/assets/javascripts/log_viewer.js +++ /dev/null @@ -1,286 +0,0 @@ -// Copyright (C) The Arvados Authors. All rights reserved. -// -// SPDX-License-Identifier: AGPL-3.0 - -function newTaskState() { - return {"complete_count": 0, - "failure_count": 0, - "task_count": 0, - "incomplete_count": 0, - "nodes": []}; -} - -function addToLogViewer(logViewer, lines, taskState) { - var re = /((\d\d\d\d)-(\d\d)-(\d\d))_((\d\d):(\d\d):(\d\d)) ([a-z0-9]{5}-[a-z0-9]{5}-[a-z0-9]{15}) (\d+) (\d+)? (.*)/; - - var items = []; - var count = logViewer.items.length; - for (var a in lines) { - var v = lines[a].match(re); - if (v != null) { - - var ts = new Date(Date.UTC(v[2], v[3]-1, v[4], v[6], v[7], v[8])); - - v11 = v[11]; - if (typeof v[11] === 'undefined') { - v11 = ""; - } else { - v11 = Number(v11); - } - - var message = v[12]; - var type = ""; - var node = ""; - var slot = ""; - if (v11 !== "") { - if (!taskState.hasOwnProperty(v11)) { - taskState[v11] = {}; - taskState.task_count += 1; - } - - if (/^stderr /.test(message)) { - message = message.substr(7); - if (/^crunchstat: /.test(message)) { - type = "crunchstat"; - message = message.substr(12); - } else if (/^srun: /.test(message) || /^slurmd/.test(message)) { - type = "task-dispatch"; - } else { - type = "task-print"; - } - } else { - var m; - if (m = /^success in (\d+) second/.exec(message)) { - taskState[v11].outcome = "success"; - taskState[v11].runtime = Number(m[1]); - taskState.complete_count += 1; - } - else if (m = /^failure \(\#\d+, (temporary|permanent)\) after (\d+) second/.exec(message)) { - taskState[v11].outcome = "failure"; - taskState[v11].runtime = Number(m[2]); - taskState.failure_count += 1; - if (m[1] == "permanent") { - taskState.incomplete_count += 1; - } - } - else if (m = /^child \d+ started on ([^.]*)\.(\d+)/.exec(message)) { - taskState[v11].node = m[1]; - taskState[v11].slot = m[2]; - if (taskState.nodes.indexOf(m[1], 0) == -1) { - taskState.nodes.push(m[1]); - } - for (var i in items) { - if (i > 0) { - if (items[i].taskid === v11) { - items[i].node = m[1]; - items[i].slot = m[2]; - } - } - } - } - type = "task-dispatch"; - } - node = taskState[v11].node; - slot = taskState[v11].slot; - } else { - type = "crunch"; - } - - items.push({ - id: count, - ts: ts, - timestamp: ts.toLocaleDateString() + " " + ts.toLocaleTimeString(), - taskid: v11, - node: node, - slot: slot, - message: message.replace(/&/g, '&').replace(//g, '>'), - type: type - }); - count += 1; - } else { - console.log("Did not parse line " + a + ": " + lines[a]); - } - } - logViewer.add(items); -} - -function sortById(a, b, opt) { - a = a.values(); - b = b.values(); - - if (a["id"] > b["id"]) { - return 1; - } - if (a["id"] < b["id"]) { - return -1; - } - return 0; -} - -function sortByTask(a, b, opt) { - var aa = a.values(); - var bb = b.values(); - - if (aa["taskid"] === "" && bb["taskid"] !== "") { - return -1; - } - if (aa["taskid"] !== "" && bb["taskid"] === "") { - return 1; - } - - if (aa["taskid"] !== "" && bb["taskid"] !== "") { - if (aa["taskid"] > bb["taskid"]) { - return 1; - } - if (aa["taskid"] < bb["taskid"]) { - return -1; - } - } - - return sortById(a, b, opt); -} - -function sortByNode(a, b, opt) { - var aa = a.values(); - var bb = b.values(); - - if (aa["node"] === "" && bb["node"] !== "") { - return -1; - } - if (aa["node"] !== "" && bb["node"] === "") { - return 1; - } - - if (aa["node"] !== "" && bb["node"] !== "") { - if (aa["node"] > bb["node"]) { - return 1; - } - if (aa["node"] < bb["node"]) { - return -1; - } - } - - if (aa["slot"] !== "" && bb["slot"] !== "") { - if (aa["slot"] > bb["slot"]) { - return 1; - } - if (aa["slot"] < bb["slot"]) { - return -1; - } - } - - return sortById(a, b, opt); -} - - -function dumbPluralize(n, s, p) { - if (typeof p === 'undefined') { - p = "s"; - } - if (n == 0 || n > 1) { - return n + " " + (s + p); - } else { - return n + " " + s; - } -} - -function generateJobOverview(id, logViewer, taskState) { - var html = ""; - - if (logViewer.items.length > 2) { - var first = logViewer.items[1]; - var last = logViewer.items[logViewer.items.length-1]; - var duration = (last.values().ts.getTime() - first.values().ts.getTime()) / 1000; - - var hours = 0; - var minutes = 0; - var seconds; - - if (duration >= 3600) { - hours = Math.floor(duration / 3600); - duration -= (hours * 3600); - } - if (duration >= 60) { - minutes = Math.floor(duration / 60); - duration -= (minutes * 60); - } - seconds = duration; - - var tcount = taskState.task_count; - - html += "

"; - html += "Started at " + first.values().timestamp + ". "; - html += "Ran " + dumbPluralize(tcount, " task") + " over "; - if (hours > 0) { - html += dumbPluralize(hours, " hour"); - } - if (minutes > 0) { - html += " " + dumbPluralize(minutes, " minute"); - } - if (seconds > 0) { - html += " " + dumbPluralize(seconds, " second"); - } - - html += " using " + dumbPluralize(taskState.nodes.length, " node"); - - html += ". " + dumbPluralize(taskState.complete_count, "task") + " completed"; - html += ", " + dumbPluralize(taskState.incomplete_count, "task") + " incomplete"; - html += " (" + dumbPluralize(taskState.failure_count, " failure") + ")"; - - html += ". Finished at " + last.values().timestamp + "."; - html += "

"; - } else { - html = "

Job log is empty or failed to load.

"; - } - - $(id).html(html); -} - -function gotoPage(n, logViewer, page, id) { - if (n < 0) { return; } - if (n*page > logViewer.matchingItems.length) { return; } - logViewer.page_offset = n; - logViewer.show(n*page, page); -} - -function updatePaging(id, logViewer, page) { - var p = ""; - var i = logViewer.matchingItems.length; - var n; - for (n = 0; (n*page) < i; n += 1) { - if (n == logViewer.page_offset) { - p += "" + (n+1) + " "; - } else { - p += "" + (n+1) + " "; - } - } - $(id).html(p); - for (n = 0; (n*page) < i; n += 1) { - (function(n) { - $(".log-viewer-page-" + n).on("click", function() { - gotoPage(n, logViewer, page, id); - return false; - }); - })(n); - } - - if (logViewer.page_offset == 0) { - $(".log-viewer-page-up").addClass("text-muted"); - } else { - $(".log-viewer-page-up").removeClass("text-muted"); - } - - if (logViewer.page_offset == (n-1)) { - $(".log-viewer-page-down").addClass("text-muted"); - } else { - $(".log-viewer-page-down").removeClass("text-muted"); - } -} - -function nextPage(logViewer, page, id) { - gotoPage(logViewer.page_offset+1, logViewer, page, id); -} - -function prevPage(logViewer, page, id) { - gotoPage(logViewer.page_offset-1, logViewer, page, id); -} diff --git a/apps/workbench/app/assets/javascripts/mithril_mount.js b/apps/workbench/app/assets/javascripts/mithril_mount.js deleted file mode 100644 index 7995ffea6a..0000000000 --- a/apps/workbench/app/assets/javascripts/mithril_mount.js +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright (C) The Arvados Authors. All rights reserved. -// -// SPDX-License-Identifier: AGPL-3.0 - -$(document).on('ready arv:pane:loaded', function() { - $('[data-mount-mithril]').each(function() { - var data = $(this).data() - m.mount(this, {view: function () {return m(window[data.mountMithril], data)}}) - }) -}) diff --git a/apps/workbench/app/assets/javascripts/modal_pager.js b/apps/workbench/app/assets/javascripts/modal_pager.js deleted file mode 100644 index ffa45ee172..0000000000 --- a/apps/workbench/app/assets/javascripts/modal_pager.js +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright (C) The Arvados Authors. All rights reserved. -// -// SPDX-License-Identifier: AGPL-3.0 - -// Usage: -// -// 1. Add some buttons to your modal, one with class="pager-next" and -// one with class="pager-prev". -// -// 2. Put multiple .modal-body sections in your modal. -// -// 3. Add a "pager-count" div where page count is shown. -// For ex: "1 of 10" when showing first page of 10 pages. - -$(document).on('click', '.modal .pager-next', function() { - var $modal = $(this).parents('.modal'); - $modal.data('page', ($modal.data('page') || 0) + 1).trigger('pager:render'); - return false; -}).on('click', '.modal .pager-prev', function() { - var $modal = $(this).parents('.modal'); - $modal.data('page', ($modal.data('page') || 1) - 1).trigger('pager:render'); - return false; -}).on('ready ajax:success', function() { - $('.modal').trigger('pager:render'); -}).on('pager:render', '.modal', function() { - var $modal = $(this); - var page = $modal.data('page') || 0; - var $panes = $('.modal-body', $modal); - if (page >= $panes.length) { - // Somehow moved past end - page = $panes.length - 1; - $modal.data('page', page); - } else if (page < 0) { - page = 0; - } - - var $pager_count = $('.pager-count', $modal); - $pager_count.text((page+1) + " of " + $panes.length); - - var selected = $panes.hide().eq(page).show(); - enableButton($('.pager-prev', $modal), page > 0); - enableButton($('.pager-next', $modal), page < $panes.length - 1); - function enableButton(btn, ok) { - btn.prop('disabled', !ok). - toggleClass('btn-primary', ok). - toggleClass('btn-default', !ok); - } -}); diff --git a/apps/workbench/app/assets/javascripts/models/loader.js b/apps/workbench/app/assets/javascripts/models/loader.js deleted file mode 100644 index 0b29de68de..0000000000 --- a/apps/workbench/app/assets/javascripts/models/loader.js +++ /dev/null @@ -1,159 +0,0 @@ -// Copyright (C) The Arvados Authors. All rights reserved. -// -// SPDX-License-Identifier: AGPL-3.0 - -// MultipageLoader retrieves a multi-page result set from the -// server. The constructor initiates the first page load. -// -// config.loadFunc is a function that accepts an array of -// paging-related filters, and returns a promise for the API -// response. loadFunc() must retrieve results in "modified_at desc" -// order. -// -// state is: -// * 'loading' if a network request is in progress; -// * 'done' if there are no more items to load; -// * 'ready' otherwise. -// -// items is a stream that resolves to an array of all items retrieved so far. -// -// loadMore() loads the next page, if any. -window.MultipageLoader = function(config) { - var loader = this - Object.assign(loader, config, { - state: 'ready', - DONE: 'done', - LOADING: 'loading', - READY: 'ready', - - items: m.stream([]), - thresholdItem: null, - loadMore: function() { - if (loader.state == loader.DONE || loader.state == loader.LOADING) - return - var filters = loader.thresholdItem ? [ - ["modified_at", "<=", loader.thresholdItem.modified_at], - ["uuid", "!=", loader.thresholdItem.uuid], - ] : [] - loader.state = loader.LOADING - loader.loadFunc(filters).then(function(resp) { - var items = loader.items() - Array.prototype.push.apply(items, resp.items) - if (resp.items.length == 0) { - loader.state = loader.DONE - } else { - loader.thresholdItem = resp.items[resp.items.length-1] - loader.state = loader.READY - } - loader.items(items) - }).catch(function(err) { - loader.err = err - loader.state = loader.READY - }) - }, - }) - loader.loadMore() -} - -// MergingLoader merges results from multiple loaders (given in the -// config.children array) into a single result set. -// -// new MergingLoader({children: [loader, loader, ...]}) -// -// The children must retrieve results in "modified_at desc" order. -window.MergingLoader = function(config) { - var loader = this - Object.assign(loader, config, { - // Sorted items ready to display, merged from all children. - items: m.stream([]), - state: 'ready', - DONE: 'done', - LOADING: 'loading', - READY: 'ready', - loadable: function() { - // Return an array of children that we could call - // loadMore() on. Update loader.state. - loader.state = loader.DONE - return loader.children.filter(function(child) { - if (child.state == child.DONE) - return false - if (child.state == child.LOADING) { - loader.state = loader.LOADING - return false - } - if (loader.state == loader.DONE) - loader.state = loader.READY - return true - }) - }, - loadMore: function() { - // Call loadMore() on children that have reached - // lowWaterMark. - loader.loadable().map(function(child) { - if (child.items().length - child.itemsDisplayed < loader.lowWaterMark) { - loader.state = loader.LOADING - child.loadMore() - } - }) - }, - mergeItems: function() { - // We want to avoid moving items around on the screen once - // they're displayed. - // - // To this end, here we find the last safely displayable - // item ("cutoff") by getting the last item from each - // unfinished child, and taking the topmost (most recent) - // one of those. - // - // (If we were to display an item below that cutoff, the - // next page of results from an unfinished child could - // include items that get inserted above the cutoff, - // causing the cutoff item to move down.) - var cutoff - var cutoffUnknown = false - loader.children.forEach(function(child) { - if (child.state == child.DONE) - return - var items = child.items() - if (items.length == 0) { - // No idea what's coming in the next page. - cutoffUnknown = true - return - } - var last = items[items.length-1].modified_at - if (!cutoff || cutoff < last) - cutoff = last - }) - if (cutoffUnknown) - return - var combined = [] - loader.children.forEach(function(child) { - child.itemsDisplayed = 0 - child.items().every(function(item) { - if (cutoff && item.modified_at < cutoff) - // Don't display this item or anything after - // it (see "cutoff" comment above). - return false - combined.push(item) - child.itemsDisplayed++ - return true // continue - }) - }) - loader.items(combined.sort(function(a, b) { - return a.modified_at < b.modified_at ? 1 : -1 - })) - }, - // Number of undisplayed items to keep on hand for each result - // set. When hitting "load more", if a result set already has - // this many additional results available, we don't bother - // fetching a new page. This is the _minimum_ number of rows - // that will be added to loader.items in each "load more" - // event (except for the case where all items are displayed). - lowWaterMark: 23, - }) - var childrenReady = m.stream.merge(loader.children.map(function(child) { - return child.items - })) - childrenReady.map(loader.loadable) - childrenReady.map(loader.mergeItems) -} diff --git a/apps/workbench/app/assets/javascripts/models/session_db.js b/apps/workbench/app/assets/javascripts/models/session_db.js deleted file mode 100644 index 70bd0a4ba5..0000000000 --- a/apps/workbench/app/assets/javascripts/models/session_db.js +++ /dev/null @@ -1,357 +0,0 @@ -// Copyright (C) The Arvados Authors. All rights reserved. -// -// SPDX-License-Identifier: AGPL-3.0 - -window.SessionDB = function() { - var db = this; - Object.assign(db, { - discoveryCache: {}, - tokenUUIDCache: null, - loadFromLocalStorage: function() { - try { - return JSON.parse(window.localStorage.getItem('sessions')) || {}; - } catch(e) {} - return {}; - }, - loadAll: function() { - var all = db.loadFromLocalStorage(); - if (window.defaultSession) { - window.defaultSession.isFromRails = true; - all[window.defaultSession.user.uuid.slice(0, 5)] = window.defaultSession; - } - return all; - }, - loadActive: function() { - var sessions = db.loadAll(); - Object.keys(sessions).forEach(function(key) { - if (!sessions[key].token || (sessions[key].user && !sessions[key].user.is_active)) { - delete sessions[key]; - } - }); - return sessions; - }, - loadLocal: function() { - var sessions = db.loadActive(); - var s = false; - Object.keys(sessions).forEach(function(key) { - if (sessions[key].isFromRails) { - s = sessions[key]; - return; - } - }); - return s; - }, - save: function(k, v) { - var sessions = db.loadAll(); - sessions[k] = v; - Object.keys(sessions).forEach(function(key) { - if (sessions[key].isFromRails) { - delete sessions[key]; - } - }); - window.localStorage.setItem('sessions', JSON.stringify(sessions)); - }, - trash: function(k) { - var sessions = db.loadAll(); - delete sessions[k]; - window.localStorage.setItem('sessions', JSON.stringify(sessions)); - }, - findAPI: function(url) { - // Given a Workbench or API host or URL, return a promise - // for the corresponding API server's base URL. Typical - // use: - // sessionDB.findAPI('https://workbench.example/foo').then(sessionDB.login) - if (url.length === 5 && url.indexOf('.') < 0) { - url += '.arvadosapi.com'; - } - if (url.indexOf('://') < 0) { - url = 'https://' + url; - } - url = new URL(url); - return m.request(url.origin + '/discovery/v1/apis/arvados/v1/rest').then(function() { - return url.origin + '/'; - }).catch(function(err) { - // If url is a Workbench site (and isn't too old), - // /status.json will tell us its API host. - return m.request(url.origin + '/status.json').then(function(resp) { - if (!resp.apiBaseURL) { - throw 'no apiBaseURL in status response'; - } - return resp.apiBaseURL; - }); - }); - }, - login: function(baseURL, fallbackLogin) { - // Initiate login procedure with given API base URL (e.g., - // "http://api.example/"). - // - // Any page that has a button that invokes login() must - // also call checkForNewToken() on (at least) its first - // render. Otherwise, the login procedure can't be - // completed. - if (fallbackLogin === undefined) { - fallbackLogin = true; - } - var session = db.loadLocal(); - var apiHostname = new URL(session.baseURL).hostname; - db.discoveryDoc(session).map(function(localDD) { - var uuidPrefix = localDD.uuidPrefix; - db.discoveryDoc({baseURL: baseURL}).map(function(dd) { - if (uuidPrefix in dd.remoteHosts || - (dd.remoteHostsViaDNS && apiHostname.endsWith('.arvadosapi.com'))) { - // Federated identity login via salted token - db.saltedToken(dd.uuidPrefix).then(function(token) { - m.request(baseURL+'arvados/v1/users/current', { - headers: { - authorization: 'Bearer '+token - } - }).then(function(user) { - // Federated login successful. - var remoteSession = { - user: user, - baseURL: baseURL, - token: token, - listedHost: (dd.uuidPrefix in localDD.remoteHosts) - }; - db.save(dd.uuidPrefix, remoteSession); - }).catch(function(e) { - if (dd.uuidPrefix in localDD.remoteHosts) { - // If the remote system is configured to allow federated - // logins from this cluster, but rejected the salted - // token, save as a logged out session anyways. - var remoteSession = { - baseURL: baseURL, - listedHost: true - }; - db.save(dd.uuidPrefix, remoteSession); - } else if (fallbackLogin) { - // Remote cluster not listed as a remote host and rejecting - // the salted token, try classic login. - db.loginClassic(baseURL); - } - }); - }); - } else if (fallbackLogin) { - // Classic login will be used when the remote system doesn't list this - // cluster as part of the federation. - db.loginClassic(baseURL); - } - }); - }); - return false; - }, - loginClassic: function(baseURL) { - document.location = baseURL + 'login?return_to=' + encodeURIComponent(document.location.href.replace(/\?.*/, '')+'?baseURL='+encodeURIComponent(baseURL)); - }, - logout: function(k) { - // Forget the token, but leave the other info in the db so - // the user can log in again without providing the login - // host again. - var sessions = db.loadAll(); - delete sessions[k].token; - db.save(k, sessions[k]); - }, - saltedToken: function(uuid_prefix) { - // Takes a cluster UUID prefix and returns a salted token to allow - // log into said cluster using federated identity. - var session = db.loadLocal(); - return db.tokenUUID().then(function(token_uuid) { - var shaObj = new jsSHA("SHA-1", "TEXT"); - var secret = session.token; - if (session.token.startsWith("v2/")) { - secret = session.token.split("/")[2]; - } - shaObj.setHMACKey(secret, "TEXT"); - shaObj.update(uuid_prefix); - var hmac = shaObj.getHMAC("HEX"); - return 'v2/' + token_uuid + '/' + hmac; - }); - }, - checkForNewToken: function() { - // If there's a token and baseURL in the location bar (i.e., - // we just landed here after a successful login), save it and - // scrub the location bar. - if (document.location.search[0] != '?') { return; } - var params = {}; - document.location.search.slice(1).split('&').forEach(function(kv) { - var e = kv.indexOf('='); - if (e < 0) { - return; - } - params[decodeURIComponent(kv.slice(0, e))] = decodeURIComponent(kv.slice(e+1)); - }); - if (!params.baseURL || !params.api_token) { - // Have a query string, but it's not a login callback. - return; - } - params.token = params.api_token; - delete params.api_token; - db.save(params.baseURL, params); - history.replaceState({}, '', document.location.origin + document.location.pathname); - }, - fillMissingUUIDs: function() { - var sessions = db.loadAll(); - Object.keys(sessions).forEach(function(key) { - if (key.indexOf('://') < 0) { - return; - } - // key is the baseURL placeholder. We need to get our user - // record to find out the cluster's real uuid prefix. - var session = sessions[key]; - m.request(session.baseURL+'arvados/v1/users/current', { - headers: { - authorization: 'OAuth2 '+session.token - } - }).then(function(user) { - session.user = user; - db.save(user.owner_uuid.slice(0, 5), session); - db.trash(key); - }); - }); - }, - // Return the Workbench base URL advertised by the session's - // API server, or a reasonable guess, or (if neither strategy - // works out) null. - workbenchBaseURL: function(session) { - var dd = db.discoveryDoc(session)(); - if (!dd) { - // Don't fall back to guessing until we receive the discovery doc - return null; - } - if (dd.workbenchUrl) { - return dd.workbenchUrl; - } - // Guess workbench.{apihostport} is a Workbench... unless - // the host part of apihostport is an IPv4 or [IPv6] - // address. - if (!session.baseURL.match('://(\\[|\\d+\\.\\d+\\.\\d+\\.\\d+[:/])')) { - var wbUrl = session.baseURL.replace('://', '://workbench.'); - // Remove the trailing slash, if it's there. - return wbUrl.slice(-1) === '/' ? wbUrl.slice(0, -1) : wbUrl; - } - return null; - }, - // Return a m.stream that will get fulfilled with the - // discovery doc from a session's API server. - discoveryDoc: function(session) { - var cache = db.discoveryCache[session.baseURL]; - if (!cache && session) { - db.discoveryCache[session.baseURL] = cache = m.stream(); - var baseURL = session.baseURL; - if (baseURL[baseURL.length - 1] !== '/') { - baseURL += '/'; - } - m.request(baseURL+'discovery/v1/apis/arvados/v1/rest') - .then(function (dd) { - // Just in case we're talking with an old API server. - dd.remoteHosts = dd.remoteHosts || {}; - if (dd.remoteHostsViaDNS === undefined) { - dd.remoteHostsViaDNS = false; - } - return dd; - }) - .then(cache); - } - return cache; - }, - // Return a promise with the local session token's UUID from the API server. - tokenUUID: function() { - var cache = db.tokenUUIDCache; - if (!cache) { - var session = db.loadLocal(); - if (session.token.startsWith("v2/")) { - var uuid = session.token.split("/")[1] - db.tokenUUIDCache = uuid; - return new Promise(function(resolve, reject) { - resolve(uuid); - }); - } - return db.request(session, 'arvados/v1/api_client_authorizations', { - data: { - filters: JSON.stringify([['api_token', '=', session.token]]) - } - }).then(function(resp) { - var uuid = resp.items[0].uuid; - db.tokenUUIDCache = uuid; - return uuid; - }); - } else { - return new Promise(function(resolve, reject) { - resolve(cache); - }); - } - }, - request: function(session, path, opts) { - opts = opts || {}; - opts.headers = opts.headers || {}; - opts.headers.authorization = 'OAuth2 '+ session.token; - return m.request(session.baseURL + path, opts); - }, - // Check non-federated remote active sessions if they should be migrated to - // a salted token. - migrateNonFederatedSessions: function() { - var sessions = db.loadActive(); - Object.keys(sessions).forEach(function(uuidPrefix) { - session = sessions[uuidPrefix]; - if (!session.isFromRails && session.token) { - db.saltedToken(uuidPrefix).then(function(saltedToken) { - if (session.token != saltedToken) { - // Only try the federated login - db.login(session.baseURL, false); - } - }); - } - }); - }, - // If remoteHosts is populated on the local API discovery doc, try to - // add any listed missing session. - autoLoadRemoteHosts: function() { - var sessions = db.loadAll(); - var doc = db.discoveryDoc(db.loadLocal()); - if (doc === undefined) { return; } - doc.map(function(d) { - Object.keys(d.remoteHosts).forEach(function(uuidPrefix) { - if (!(sessions[uuidPrefix])) { - db.findAPI(d.remoteHosts[uuidPrefix]).then(function(baseURL) { - db.login(baseURL, false); - }); - } - }); - }); - }, - // If the current logged in account is from a remote federated cluster, - // redirect the user to their home cluster's workbench. - // This is meant to avoid confusion when the user clicks through a search - // result on the home cluster's multi site search page, landing on the - // remote workbench and later trying to do another search by just clicking - // on the multi site search button instead of going back with the browser. - autoRedirectToHomeCluster: function(path) { - path = path || '/'; - var session = db.loadLocal(); - var userUUIDPrefix = session.user.uuid.slice(0, 5); - // If the current user is local to the cluster, do nothing. - if (userUUIDPrefix === session.user.owner_uuid.slice(0, 5)) { - return; - } - db.discoveryDoc(session).map(function (d) { - // Guess the remote host from the local discovery doc settings - var rHost = null; - if (d.remoteHosts[userUUIDPrefix]) { - rHost = d.remoteHosts[userUUIDPrefix]; - } else if (d.remoteHostsViaDNS) { - rHost = userUUIDPrefix + '.arvadosapi.com'; - } else { - // This should not happen: having remote user whose uuid prefix - // isn't listed on remoteHosts and dns mechanism is deactivated - return; - } - // Get the remote cluster workbench url & redirect there. - db.findAPI(rHost).then(function (apiUrl) { - db.discoveryDoc({baseURL: apiUrl}).map(function (d) { - document.location = d.workbenchUrl + path; - }); - }); - }); - } - }); -}; diff --git a/apps/workbench/app/assets/javascripts/permission_toggle.js b/apps/workbench/app/assets/javascripts/permission_toggle.js deleted file mode 100644 index 007a25baf1..0000000000 --- a/apps/workbench/app/assets/javascripts/permission_toggle.js +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright (C) The Arvados Authors. All rights reserved. -// -// SPDX-License-Identifier: AGPL-3.0 - -$(document). - on('click', '[data-toggle-permission] input[type=checkbox]', function() { - var data = {}; - var keys = ['data-permission-uuid', - 'data-permission-name', - 'data-permission-head', - 'data-permission-tail']; - var attr; - for(var i in keys) { - attr = keys[i]; - data[attr] = $(this).closest('[' + attr + ']').attr(attr); - if (data[attr] === undefined) { - console.log(["Error: no " + attr + " established here.", this]); - return; - } - } - var is_checked = $(this).prop('checked'); - - if (is_checked) { - $.ajax('/links', - {dataType: 'json', - type: 'POST', - data: {'link[tail_uuid]': data['data-permission-tail'], - 'link[head_uuid]': data['data-permission-head'], - 'link[link_class]': 'permission', - 'link[name]': data['data-permission-name']}, - context: this}). - fail(function(jqxhr, status, error) { - $(this).prop('checked', false); - }). - done(function(data, status, jqxhr) { - $(this).attr('data-permission-uuid', data['uuid']); - }). - always(function() { - $(this).prop('disabled', false); - }); - } - else { - $.ajax('/links/' + data['data-permission-uuid'], - {dataType: 'json', - type: 'POST', - data: {'_method': 'DELETE'}, - context: this}). - fail(function(jqxhr, status, error) { - $(this).prop('checked', true); - }). - done(function(data, status, jqxhr) { - $(this).attr('data-permission-uuid', 'x'); - }). - always(function() { - $(this).prop('disabled', false); - }); - } - $(this).prop('disabled', true); - }); diff --git a/apps/workbench/app/assets/javascripts/pipeline_instances.js b/apps/workbench/app/assets/javascripts/pipeline_instances.js deleted file mode 100644 index 7570b2f8a5..0000000000 --- a/apps/workbench/app/assets/javascripts/pipeline_instances.js +++ /dev/null @@ -1,124 +0,0 @@ -// Copyright (C) The Arvados Authors. All rights reserved. -// -// SPDX-License-Identifier: AGPL-3.0 - -function run_pipeline_button_state() { - var a = $('a.editable.required.editable-empty,input.form-control.required[value=""]'); - if ((a.length > 0) || ($('.unreadable-inputs-present').length)) { - $(".run-pipeline-button").addClass("disabled"); - } - else { - $(".run-pipeline-button").removeClass("disabled"); - } -} - -$(document).on('editable:success', function(event, tag, response, newValue) { - var $tag = $(tag); - if ($('.run-pipeline-button').length == 0) - return; - if ($tag.hasClass("required")) { - if (newValue && newValue.trim() != "") { - $tag.removeClass("editable-empty"); - $tag.parent().css("background-color", ""); - $tag.parent().prev().css("background-color", ""); - } - else { - $tag.addClass("editable-empty"); - $tag.parent().css("background-color", "#ffdddd"); - $tag.parent().prev().css("background-color", "#ffdddd"); - } - } - if ($tag.attr('data-name')) { - // Update other inputs representing the same piece of data - $('.editable[data-name="' + $tag.attr('data-name') + '"]'). - editable('setValue', newValue); - } - run_pipeline_button_state(); -}); - -$(document).on('ready ajax:complete', function() { - $('a.editable.required').each(function() { - var $tag = $(this); - if ($tag.hasClass("editable-empty")) { - $tag.parent().css("background-color", "#ffdddd"); - $tag.parent().prev().css("background-color", "#ffdddd"); - } - else { - $tag.parent().css("background-color", ""); - $tag.parent().prev().css("background-color", ""); - } - }); - $('input.required').each(function() { - var $tag = $(this); - if ($tag.hasClass("unreadable-input")) { - $tag.parent().parent().css("background-color", "#ffdddd"); - $tag.parent().parent().prev().css("background-color", "#ffdddd"); - } - else { - $tag.parent().parent().css("background-color", ""); - $tag.parent().parent().prev().css("background-color", ""); - } - }); - run_pipeline_button_state(); -}); - -$(document).on('arv-log-event', '.arv-refresh-on-state-change', function(event, eventData) { - if (this != event.target) { - // Not interested in events sent to child nodes. - return; - } - if (eventData.event_type == "update" && - eventData.properties.old_attributes.state != eventData.properties.new_attributes.state) - { - $(event.target).trigger('arv:pane:reload'); - } -}); - -$(document).on('arv-log-event', '.arv-log-event-subscribe-to-pipeline-job-uuids', function(event, eventData){ - if (this != event.target) { - // Not interested in events sent to child nodes. - return; - } - if (!((eventData.object_kind == 'arvados#pipelineInstance') && - (eventData.event_type == "create" || - eventData.event_type == "update") && - eventData.properties && - eventData.properties.new_attributes && - eventData.properties.new_attributes.components)) { - return; - } - var objs = ""; - var components = eventData.properties.new_attributes.components; - for (a in components) { - if (components[a].job && components[a].job.uuid) { - objs += " " + components[a].job.uuid; - } - } - $(event.target).attr("data-object-uuids", eventData.object_uuid + objs); -}); - -$(document).on('ready ajax:success', function() { - $('.arv-log-refresh-control').each(function() { - var uuids = $(this).attr('data-object-uuids'); - var $pane = $(this).closest('[data-pane-content-url]'); - $pane.attr('data-object-uuids', uuids); - }); -}); - -// Set up all events for the pipeline instances compare button. -(function() { - var compare_form = '#compare'; - var compare_inputs = '#comparedInstances :checkbox[name="uuids[]"]'; - var update_button = function(event) { - var $form = $(compare_form); - var $checked_inputs = $(compare_inputs).filter(':checked'); - $(':submit', $form).prop('disabled', (($checked_inputs.length < 2) || - ($checked_inputs.length > 3))); - $('input[name="uuids[]"]', $form).remove(); - $form.append($checked_inputs.clone() - .removeAttr('id').attr('type', 'hidden')); - }; - $(document) - .on('ready ajax:success', compare_form, update_button) - .on('change', compare_inputs, update_button); -})(); diff --git a/apps/workbench/app/assets/javascripts/report_issue.js b/apps/workbench/app/assets/javascripts/report_issue.js deleted file mode 100644 index 0285693e88..0000000000 --- a/apps/workbench/app/assets/javascripts/report_issue.js +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright (C) The Arvados Authors. All rights reserved. -// -// SPDX-License-Identifier: AGPL-3.0 - -$(document). - on('click', "#report-issue-submit", function(e){ - $(this).html('Sending'); - $(this).prop('disabled', true); - var $cancelButton = $('#report-issue-cancel'); - if ($cancelButton) { - $cancelButton.html('Close'); - } - $('div').remove('.modal-footer-status'); - - $.ajax('/report_issue', { - type: 'POST', - data: $(this).parents('form').serialize() - }).success(function(data, status, jqxhr) { - var $sendButton = $('#report-issue-submit'); - $sendButton.html('Report sent'); - $('div').remove('.modal-footer-status'); - $('.modal-footer').append('

'); - }).fail(function(jqxhr, status, error) { - var $sendButton = $('#report-issue-submit'); - if ($sendButton && $sendButton.prop('disabled')) { - $('div').remove('.modal-footer-status'); - $('.modal-footer').append('

'); - $sendButton.html('Send problem report'); - $sendButton.prop('disabled', false); - } - var $cancelButton = $('#report-issue-cancel'); - $cancelButton.html('Cancel'); - }); - return false; - }); diff --git a/apps/workbench/app/assets/javascripts/request_shell_access.js b/apps/workbench/app/assets/javascripts/request_shell_access.js deleted file mode 100644 index eb4fbc3901..0000000000 --- a/apps/workbench/app/assets/javascripts/request_shell_access.js +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright (C) The Arvados Authors. All rights reserved. -// -// SPDX-License-Identifier: AGPL-3.0 - -$(document).on('ready ajax:success storage', function() { - // Update the "shell access requested" info box according to the - // current state of localStorage. - var msg = localStorage.getItem('request_shell_access'); - var $noShellAccessDiv = $('#no_shell_access'); - if ($noShellAccessDiv.length > 0) { - $('.alert-success p', $noShellAccessDiv).text(msg); - $('.alert-success', $noShellAccessDiv).toggle(!!msg); - } -}); diff --git a/apps/workbench/app/assets/javascripts/select_modal.js b/apps/workbench/app/assets/javascripts/select_modal.js deleted file mode 100644 index 19cf3cd927..0000000000 --- a/apps/workbench/app/assets/javascripts/select_modal.js +++ /dev/null @@ -1,185 +0,0 @@ -// Copyright (C) The Arvados Authors. All rights reserved. -// -// SPDX-License-Identifier: AGPL-3.0 - -$(document).on('click', '.selectable', function() { - var any; - var $this = $(this); - var $container = $(this).closest('.selectable-container'); - if (!$container.hasClass('multiple')) { - $container. - find('.selectable'). - removeClass('active'); - } - $this.toggleClass('active'); - - if (!$this.hasClass('use-preview-selection')) { - any = ($container. - find('.selectable.active').length > 0) - } - - if (!$container.hasClass('preview-selectable-container')) { - $this. - closest('.modal'). - find('[data-enable-if-selection]'). - prop('disabled', !any); - - if ($this.hasClass('active')) { - var no_preview_available = '
(No preview available)
'; - if (!$this.attr('data-preview-href')) { - $(".modal-dialog-preview-pane").html(no_preview_available); - return; - } - $(".modal-dialog-preview-pane").html('
'); - $.ajax($this.attr('data-preview-href'), - {dataType: "html"}). - done(function(data, status, jqxhr) { - $(".modal-dialog-preview-pane").html(data); - }). - fail(function(data, status, jqxhr) { - $(".modal-dialog-preview-pane").html(no_preview_available); - }); - } - } else { - any = ($container. - find('.preview-selectable.active').length > 0) - $(this). - closest('.modal'). - find('[data-enable-if-selection]'). - prop('disabled', !any); - } - -}).on('click', '.modal button[data-action-href]', function() { - var selection = []; - var data = []; - var $modal = $(this).closest('.modal'); - var http_method = $(this).attr('data-method').toUpperCase(); - var action_data = $(this).data('action-data'); - var action_data_from_params = $(this).data('action-data-from-params'); - var selection_param = action_data.selection_param; - $modal.find('.modal-error').removeClass('hide').hide(); - - var $preview_selections = $modal.find('.preview-selectable.active'); - if ($preview_selections.length > 0) { - data.push({name: selection_param, value: $preview_selections.first().attr('href')}); - } - - if (data.length == 0) { // not using preview selection option - $modal.find('.selectable.active[data-object-uuid]').each(function() { - var val = $(this).attr('data-object-uuid'); - data.push({name: selection_param, value: val}); - }); - } - $.each($.extend({}, action_data, action_data_from_params), - function(key, value) { - if (value instanceof Array && key[-1] != ']') { - for (var i in value) { - data.push({name: key + '[]', value: value[i]}); - } - } else { - data.push({name: key, value: value}); - } - }); - if (http_method === 'PATCH') { - // Some user agents do not support HTTP PATCH (notably, - // phantomjs silently ignores our "data" and sends an empty - // request body) so we use POST instead, and supply a - // _method=PATCH param to tell Rails what we really want. - data.push({name: '_method', value: http_method}); - http_method = 'POST'; - } - $.ajax($(this).attr('data-action-href'), - {dataType: 'json', - type: http_method, - data: data, - traditional: false, - context: {modal: $modal, action_data: action_data}}). - fail(function(jqxhr, status, error) { - if (jqxhr.readyState == 0 || jqxhr.status == 0) { - message = "Cancelled." - } else if (jqxhr.responseJSON && jqxhr.responseJSON.errors) { - message = jqxhr.responseJSON.errors.join("; "); - } else { - message = "Request failed."; - } - this.modal.find('.modal-error'). - html('
'). - show(). - children().text(message); - }). - done(function(data, status, jqxhr) { - var event_name = this.action_data.success; - this.modal.find('.modal-error').hide(); - $(document).trigger(event_name!=null ? event_name : 'page-refresh', - [data, status, jqxhr, this.action_data]); - }); -}).on('click', '.chooser-show-project', function() { - var params = {}; - var project_uuid = $(this).attr('data-project-uuid'); - $(this).attr('href', '#'); // Skip normal click handler - if (project_uuid) { - params = {'filters': [['owner_uuid', - '=', - project_uuid]], - 'project_uuid': project_uuid - }; - } - $(".modal-dialog-preview-pane").html(""); - // Use current selection as dropdown button label - $(this). - closest('.dropdown-menu'). - prev('button'). - html($(this).text() + ' '); - // Set (or unset) filter params and refresh filterable rows - $($(this).closest('[data-filterable-target]').attr('data-filterable-target')). - data('infinite-content-params-from-project-dropdown', params). - trigger('refresh-content'); -}).on('ready', function() { - $('form[data-search-modal] a').on('click', function() { - $(this).closest('form').submit(); - return false; - }); - $('form[data-search-modal]').on('submit', function() { - // Ask the server for a Search modal. When it arrives, copy - // the search string from the top nav input into the modal's - // search query field. - var $form = $(this); - var searchq = $form.find('input').val(); - var is_a_uuid = /^([0-9a-f]{32}(\+\S+)?|[0-9a-z]{5}-[0-9a-z]{5}-[0-9a-z]{15})$/; - if (searchq.trim().match(is_a_uuid)) { - window.location = '/actions?uuid=' + encodeURIComponent(searchq.trim()); - // Show the "loading" indicator. TODO: better page transition hook - $(document).trigger('ajax:send'); - return false; - } - if ($form.find('a[data-remote]').length > 0) { - // A search dialog is already loading. - return false; - } - $(''). - attr('data-remote-href', $form.attr('data-search-modal')). - attr('data-remote', 'true'). - attr('data-method', 'GET'). - hide(). - appendTo($form). - on('ajax:success', function(data, status, xhr) { - $('body > .modal-container input[type=text]'). - val($form.find('input').val()). - focus(); - $form.find('input').val(''); - }).on('ajax:complete', function() { - $(this).detach(); - }). - click(); - return false; - }); -}).on('page-refresh', function(event, data, status, jqxhr, action_data) { - window.location.reload(); -}).on('tab-refresh', function(event, data, status, jqxhr, action_data) { - $(document).trigger('arv:pane:reload:all'); - $('body > .modal-container .modal').modal('hide'); -}).on('redirect-to-created-object', function(event, data, status, jqxhr, action_data) { - window.location.href = data.href.replace(/^[^\/]*\/\/[^\/]*/, ''); -}).on('shown.bs.modal', 'body > .modal-container .modal', function() { - $('.focus-on-display', this).focus(); -}); diff --git a/apps/workbench/app/assets/javascripts/selection.js.erb b/apps/workbench/app/assets/javascripts/selection.js.erb deleted file mode 100644 index e8f21eefd5..0000000000 --- a/apps/workbench/app/assets/javascripts/selection.js.erb +++ /dev/null @@ -1,111 +0,0 @@ -<%# Copyright (C) The Arvados Authors. All rights reserved. - -SPDX-License-Identifier: AGPL-3.0 %> - -//= require jquery -//= require jquery_ujs - -/** Javascript for selection. */ - -jQuery(function($){ - $(document). - on('change', '.persistent-selection:checkbox', function(e) { - $(document).trigger('selections-updated'); - }); -}); - -function dispatch_selection_action() { - /* When the user clicks a selection action link, build a form to perform - the action on the selected data, and submit it. - This is based on handleMethod from rails-ujs, extended to add the - selections to the submitted form. - Copyright (c) 2007-2010 Contributors at http://github.com/rails/jquery-ujs/contributors - */ - var $container = $(this); - if ($container.closest('.disabled').length) { - return false; - } - $container.closest('.dropdown-menu').dropdown('toggle'); - - var href = $container.data('href'), - method = $container.data('method') || 'GET', - paramName = $container.data('selection-param-name'), - csrfToken = $('meta[name=csrf-token]').attr('content'), - csrfParam = $('meta[name=csrf-param]').attr('content'), - form = $('
'), - metadataInput = (''); - - if (csrfParam !== undefined && csrfToken !== undefined) { - metadataInput += (''); - } - $container. - closest('.selection-action-container'). - find(':checkbox:checked:visible'). - each(function(index, elem) { - metadataInput += (''); - }); - - form.data('remote', $container.data('remote')); - form.hide().append(metadataInput).appendTo('body'); - form.submit(); - return false; -} - -function enable_disable_selection_actions() { - var $container = $(this); - var $checked = $('.persistent-selection:checkbox:checked', $container); - var collection_lock_classes = $('.lock-collection-btn').attr('class') - - $('[data-selection-action]', $container). - closest('div.btn-group-sm'). - find('ul li'). - toggleClass('disabled', ($checked.length == 0)); - $('[data-selection-action=compare]', $container). - closest('li'). - toggleClass('disabled', - ($checked.filter('[value*=-d1hrv-]').length < 2) || - ($checked.not('[value*=-d1hrv-]').length > 0)); - <% unless Group.copies_to_projects? %> - $('[data-selection-action=copy]', $container). - closest('li'). - toggleClass('disabled', - ($checked.filter('[value*=-j7d0g-]').length > 0) || - ($checked.length < 1)); - <% end %> - $('[data-selection-action=combine-project-contents]', $container). - closest('li'). - toggleClass('disabled', - ($checked.filter('[value*=-4zz18-]').length < 1) || - ($checked.length != $checked.filter('[value*=-4zz18-]').length)); - $('[data-selection-action=remove-selected-files]', $container). - closest('li'). - toggleClass('disabled', - ($checked.length < 0) || - !($checked.length > 0 && collection_lock_classes && collection_lock_classes.indexOf("fa-unlock") !=-1)); - $('[data-selection-action=untrash-selected-items]', $container). - closest('li'). - toggleClass('disabled', - ($checked.length < 1)); -} - -$(document). - on('selections-updated', function() { - $('.selection-action-container').each(enable_disable_selection_actions); - }). - on('ready ajax:complete', function() { - $('[data-selection-action]'). - off('click', dispatch_selection_action). - on('click', dispatch_selection_action); - $(this).trigger('selections-updated'); - }); - -function select_all_items() { - $(".arv-selectable-items :checkbox").filter(":visible").prop("checked", true).trigger("change"); -} - -function unselect_all_items() { - $(".arv-selectable-items :checkbox").filter(":visible").prop("checked", false).trigger("change"); -} diff --git a/apps/workbench/app/assets/javascripts/sizing.js b/apps/workbench/app/assets/javascripts/sizing.js deleted file mode 100644 index 569956fd3a..0000000000 --- a/apps/workbench/app/assets/javascripts/sizing.js +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright (C) The Arvados Authors. All rights reserved. -// -// SPDX-License-Identifier: AGPL-3.0 - -function graph_zoom(divId, svgId, scale) { - var pg = document.getElementById(divId); - vcenter = (pg.scrollTop + (pg.scrollHeight - pg.scrollTopMax)/2.0) / pg.scrollHeight; - hcenter = (pg.scrollLeft + (pg.scrollWidth - pg.scrollLeftMax)/2.0) / pg.scrollWidth; - var g = document.getElementById(svgId); - g.setAttribute("height", parseFloat(g.getAttribute("height")) * scale); - g.setAttribute("width", parseFloat(g.getAttribute("width")) * scale); - pg.scrollTop = (vcenter * pg.scrollHeight) - (pg.scrollHeight - pg.scrollTopMax)/2.0; - pg.scrollLeft = (hcenter * pg.scrollWidth) - (pg.scrollWidth - pg.scrollLeftMax)/2.0; - smart_scroll_fixup(); -} - -function smart_scroll_fixup(s) { - - if (s != null && s.type == 'shown.bs.tab') { - s = [s.target]; - } - else { - s = $(".smart-scroll"); - } - - s.each(function(i, a) { - a = $(a); - var h = window.innerHeight - a.offset().top - a.attr("data-smart-scroll-padding-bottom"); - height = String(h) + "px"; - a.css('max-height', height); - }); -} - -$(window).on('load ready resize scroll ajax:complete', smart_scroll_fixup); -$(document).on('shown.bs.tab', 'ul.nav-tabs > li > a', smart_scroll_fixup); diff --git a/apps/workbench/app/assets/javascripts/tab_panes.js b/apps/workbench/app/assets/javascripts/tab_panes.js deleted file mode 100644 index b19a277ef7..0000000000 --- a/apps/workbench/app/assets/javascripts/tab_panes.js +++ /dev/null @@ -1,217 +0,0 @@ -// Copyright (C) The Arvados Authors. All rights reserved. -// -// SPDX-License-Identifier: AGPL-3.0 - -// Load tab panes on demand. See app/views/application/_content.html.erb - -// Fire when a tab is selected/clicked. -$(document).on('shown.bs.tab', '[data-toggle="tab"]', function(event) { - // reload the pane (unless it's already loaded) - $($(event.target).attr('href')). - not('.pane-loaded'). - trigger('arv:pane:reload'); -}); - -// Ask a refreshable pane to reload via ajax. -// -// Target of this event is the DOM element to be updated. A reload -// consists of an AJAX call to load the "data-pane-content-url" and -// replace the content of the target element with the retrieved HTML. -// -// There are four CSS classes set on the element to indicate its state: -// pane-loading, pane-stale, pane-loaded, pane-reload-pending -// -// There are five states based on the presence or absence of css classes: -// -// 1. Absence of any pane-* states means the pane is empty, and should -// be loaded as soon as it becomes visible. -// -// 2. "pane-loading" means an AJAX call has been made to reload the -// pane and we are waiting on a result. -// -// 3. "pane-loading pane-stale" means the pane is loading, but has -// already been invalidated and should schedule a reload as soon as -// possible after the current load completes. (This happens when there -// is a cluster of events, where the reload is triggered by the first -// event, but we want ensure that we eventually load the final -// quiescent state). -// -// 4. "pane-loaded" means the pane is up to date. -// -// 5. "pane-loaded pane-reload-pending" means a reload is needed, and -// has been scheduled, but has not started because the pane's -// minimum-time-between-reloads throttle has not yet been reached. -// -$(document).on('arv:pane:reload', '[data-pane-content-url]', function(e) { - if (this != e.target) { - // An arv:pane:reload event was sent to an element (e.target) - // which happens to have an ancestor (this) matching the above - // '[data-pane-content-url]' selector. This happens because - // events bubble up the DOM on their way to document. However, - // here we only care about events delivered directly to _this_ - // selected element (i.e., this==e.target), not ones delivered - // to its children. The event "e" is uninteresting here. - return; - } - - // $pane, the event target, is an element whose content is to be - // replaced. Pseudoclasses on $pane (pane-loading, etc) encode the - // current loading state. - var $pane = $(this); - - if ($pane.hasClass('pane-loading')) { - // Already loading, mark stale to schedule a reload after this one. - $pane.addClass('pane-stale'); - return; - } - - // The default throttle (mininum milliseconds between refreshes) - // can be overridden by an .arv-log-refresh-control element inside - // the pane -- or, failing that, the pane element itself -- with a - // data-load-throttle attribute. This allows the server to adjust - // the throttle depending on the pane content. - var throttle = - $pane.find('.arv-log-refresh-control').attr('data-load-throttle') || - $pane.attr('data-load-throttle') || - 15000; - var now = (new Date()).getTime(); - var loaded_at = $pane.attr('data-loaded-at'); - var since_last_load = now - loaded_at; - if (loaded_at && (since_last_load < throttle)) { - if (!$pane.hasClass('pane-reload-pending')) { - $pane.addClass('pane-reload-pending'); - setTimeout((function() { - $pane.trigger('arv:pane:reload'); - }), throttle - since_last_load); - } - return; - } - - // We know this doesn't have 'pane-loading' because we tested for it above - $pane.removeClass('pane-reload-pending'); - $pane.removeClass('pane-loaded'); - $pane.removeClass('pane-stale'); - - if (!$pane.hasClass('active') && - $pane.parent().hasClass('tab-content')) { - // $pane is one of the content areas in a bootstrap tabs - // widget, and it isn't the currently selected tab. If and - // when the user does select the corresponding tab, it will - // get a shown.bs.tab event, which will invoke this reload - // function again (see handler above). For now, we just insert - // a spinner, which will be displayed while the new content is - // loading. - $pane.html('
'); - return; - } - - $pane.addClass('pane-loading'); - - var content_url = $pane.attr('data-pane-content-url'); - $.ajax(content_url, {dataType: 'html', type: 'GET', context: $pane}). - done(function(data, status, jqxhr) { - var $pane = this; - // Preserve collapsed state - var collapsable = {}; - $(".collapse", this).each(function(i, c) { - collapsable[c.id] = $(c).hasClass('in'); - }); - var tmp = $(data); - $(".collapse", tmp).each(function(i, c) { - if (collapsable[c.id]) { - $(c).addClass('in'); - } else { - $(c).removeClass('in'); - } - }); - $pane.html(tmp); - $pane.removeClass('pane-loading'); - $pane.addClass('pane-loaded'); - $pane.attr('data-loaded-at', (new Date()).getTime()); - $pane.trigger('arv:pane:loaded', [$pane]); - - if ($pane.hasClass('pane-stale')) { - $pane.trigger('arv:pane:reload'); - } - }).fail(function(jqxhr, status, error) { - var $pane = this; - var errhtml; - var contentType = jqxhr.getResponseHeader('Content-Type'); - if (jqxhr.readyState == 0 || jqxhr.status == 0) { - if ($pane.attr('data-loaded-at') > 0) { - // Stale content is already present. Leave it - // there while loading the next page. - $pane.removeClass('pane-loading'); - $pane.addClass('pane-loaded'); - // ...but schedule another refresh (after a - // throttle delay) in case the act of navigating - // away gets cancelled itself, leaving this page - // with content that we know is stale. - $pane.addClass('pane-stale'); - $pane.attr('data-loaded-at', (new Date()).getTime()); - $pane.trigger('arv:pane:reload'); - return; - } - errhtml = "Cancelled."; - } else if (contentType && contentType.match(/\btext\/html\b/)) { - var $response = $(jqxhr.responseText); - var $wrapper = $('div#page-wrapper', $response); - if ($wrapper.length) { - errhtml = $wrapper.html(); - } else { - errhtml = jqxhr.responseText; - } - } else { - errhtml = ("An error occurred: " + - (jqxhr.responseText || status)). - replace(/&/g, '&'). - replace(//g, '>'); - } - $pane.html('
'); - $('.tab_reload', $pane).click(function() { - $(this). - html('
'). - closest('.pane-loaded'). - attr('data-loaded-at', 0). - trigger('arv:pane:reload'); - }); - // We want to render the error in an iframe, in order to - // avoid conflicts with the main page's element ids, etc. - // In order to do that dynamically, we have to set a - // timeout on the iframe window to load our HTML *after* - // the default source (e.g., about:blank) has loaded. - var iframe = $('iframe', $pane)[0]; - iframe.contentWindow.setTimeout(function() { - $('body', iframe.contentDocument).html(errhtml); - iframe.height = iframe.contentDocument.body.scrollHeight + "px"; - }, 1); - $pane.removeClass('pane-loading'); - $pane.addClass('pane-loaded'); - }); -}); - -// Mark all panes as stale/dirty. Refresh any 'active' panes. -$(document).on('arv:pane:reload:all', function() { - $('[data-pane-content-url]').trigger('arv:pane:reload'); -}); - -$(document).on('arv-log-event', '.arv-refresh-on-log-event', function(event) { - if (this != event.target) { - // Not interested in events sent to child nodes. - return; - } - // Panes marked arv-refresh-on-log-event should be refreshed - $(event.target).trigger('arv:pane:reload'); -}); - -// If there is a 'tab counts url' in the nav-tabs element then use it to get some javascript that will update them -$(document).on('ready count-change', function() { - var tabCountsUrl = $('ul.nav-tabs').data('tab-counts-url'); - if( tabCountsUrl && tabCountsUrl.length ) { - $.get( tabCountsUrl ); - } -}); diff --git a/apps/workbench/app/assets/javascripts/upload_to_collection.js b/apps/workbench/app/assets/javascripts/upload_to_collection.js deleted file mode 100644 index d66be63853..0000000000 --- a/apps/workbench/app/assets/javascripts/upload_to_collection.js +++ /dev/null @@ -1,494 +0,0 @@ -// Copyright (C) The Arvados Authors. All rights reserved. -// -// SPDX-License-Identifier: AGPL-3.0 - -var app = angular.module('Workbench', ['Arvados']); -app.controller('UploadToCollection', UploadToCollection); -app.directive('arvUuid', arvUuid); - -function arvUuid() { - // Copy the given uuid into the current $scope. - return { - restrict: 'A', - link: function(scope, element, attributes) { - scope.uuid = attributes.arvUuid; - } - }; -} - -UploadToCollection.$inject = ['$scope', '$filter', '$q', '$timeout', - 'ArvadosClient', 'arvadosApiToken']; -function UploadToCollection($scope, $filter, $q, $timeout, - ArvadosClient, arvadosApiToken) { - $.extend($scope, { - uploadQueue: [], - uploader: new QueueUploader(), - addFilesToQueue: function(files) { - // Angular binding doesn't work its usual magic for file - // inputs, so we need to $scope.$apply() this update. - $scope.$apply(function(){ - var i, nItemsTodo; - // Add these new files after the items already waiting - // in the queue -- but before the items that are - // 'Done' and have therefore been pushed to the - // bottom. - for (nItemsTodo = 0; - (nItemsTodo < $scope.uploadQueue.length && - $scope.uploadQueue[nItemsTodo].state !== 'Done'); ) { - nItemsTodo++; - } - for (i=0; i= 0) { - ++found; - } - }); - return found; - } - }); - //////////////////////////////// - - var keepProxy; - var defaultErrorMessage = 'A network error occurred: either the server was unreachable, or there is a server configuration problem. Please check your browser debug console for a more specific error message (browser security features prevent us from showing the details here).'; - - function SliceReader(_slice) { - var that = this; - $.extend(this, { - go: go - }); - //////////////////////////////// - var _deferred; - var _reader; - function go() { - // Return a promise, which will be resolved with the - // requested slice data. - _deferred = $.Deferred(); - _reader = new FileReader(); - _reader.onload = resolve; - _reader.onerror = _deferred.reject; - _reader.onprogress = _deferred.notify; - _reader.readAsArrayBuffer(_slice.blob); - return _deferred.promise(); - } - function resolve() { - if (that._reader.result.length !== that._slice.size) { - // Sometimes we get an onload event even if the read - // did not return the desired number of bytes. We - // treat that as a fail. - _deferred.reject( - null, "Read error", - "Short read: wanted " + _slice.size + - ", received " + _reader.result.length); - return; - } - return _deferred.resolve(_reader.result); - } - } - - function SliceUploader(_label, _data, _dataSize) { - $.extend(this, { - go: go, - stop: stop - }); - //////////////////////////////// - var that = this; - var _deferred; - var _failCount = 0; - var _failMax = 3; - var _jqxhr; - function go() { - // Send data to the Keep proxy. Retry a few times on - // fail. Return a promise that will get resolved with - // resolve(locator) when the block is accepted by the - // proxy. - _deferred = $.Deferred(); - if (proxyUriBase().match(/^http:/) && - window.location.origin.match(/^https:/)) { - // In this case, requests will fail, and no ajax - // success/fail handlers will be called (!), which - // will leave our status saying "uploading" and the - // user waiting for something to happen. Better to - // give up now. - _deferred.reject({ - textStatus: 'error', - err: 'There is a server configuration problem. Proxy ' + proxyUriBase() + ' cannot be used from origin ' + window.location.origin + ' due to the browser\'s mixed-content (https/http) policy.' - }); - } else { - goSend(); - } - return _deferred.promise(); - } - function stop() { - _failMax = 0; - _jqxhr.abort(); - _deferred.reject({ - textStatus: 'stopped', - err: 'interrupted at slice '+_label - }); - } - function goSend() { - _jqxhr = $.ajax({ - url: proxyUriBase(), - type: 'POST', - crossDomain: true, - headers: { - 'Authorization': 'OAuth2 '+arvadosApiToken, - 'Content-Type': 'application/octet-stream', - 'X-Keep-Desired-Replicas': '2' - }, - xhr: function() { - // Make an xhr that reports upload progress - var xhr = $.ajaxSettings.xhr(); - if (xhr.upload) { - xhr.upload.onprogress = onSendProgress; - } - return xhr; - }, - processData: false, - data: _data - }); - _jqxhr.then(onSendResolve, onSendReject); - } - function onSendProgress(xhrProgressEvent) { - _deferred.notify(xhrProgressEvent.loaded, _dataSize); - } - function onSendResolve(data, textStatus, jqxhr) { - _deferred.resolve(data, _dataSize); - } - function onSendReject(xhr, textStatus, err) { - if (++_failCount < _failMax) { - // TODO: nice to tell the user that retry is happening. - console.log('slice ' + _label + ': ' + - textStatus + ', retry ' + _failCount); - goSend(); - } else { - _deferred.reject( - {xhr: xhr, textStatus: textStatus, err: err}); - } - } - function proxyUriBase() { - return ((keepProxy.service_ssl_flag ? 'https' : 'http') + - '://' + keepProxy.service_host + ':' + - keepProxy.service_port + '/'); - } - } - - function FileUploader(file) { - $.extend(this, { - file: file, - locators: [], - progress: 0.0, - state: 'Queued', // Queued, Uploading, Paused, Uploaded, Done - statistics: null, - go: go, - stop: stop // User wants to stop. - }); - //////////////////////////////// - var that = this; - var _currentUploader; - var _currentSlice; - var _deferred; - var _maxBlobSize = Math.pow(2,26); - var _bytesDone = 0; - var _queueTime = Date.now(); - var _startTime; - var _startByte; - var _finishTime; - var _readPos = 0; // number of bytes confirmed uploaded - function go() { - if (_deferred) - _deferred.reject({textStatus: 'restarted'}); - _deferred = $.Deferred(); - that.state = 'Uploading'; - _startTime = Date.now(); - _startByte = _readPos; - setProgress(); - goSlice(); - return _deferred.promise().always(function() { _deferred = null; }); - } - function stop() { - if (_deferred) { - that.state = 'Paused'; - _deferred.reject({textStatus: 'stopped', err: 'interrupted'}); - } - if (_currentUploader) { - _currentUploader.stop(); - _currentUploader = null; - } - } - function goSlice() { - // Ensure this._deferred gets resolved or rejected -- - // either right here, or when a new promise arranged right - // here is fulfilled. - _currentSlice = nextSlice(); - if (!_currentSlice) { - // All slices have been uploaded, but the work won't - // be truly Done until the target collection has been - // updated by the QueueUploader. This state is called: - that.state = 'Uploaded'; - setProgress(_readPos); - _currentUploader = null; - _deferred.resolve([that]); - return; - } - _currentUploader = new SliceUploader( - _readPos.toString(), - _currentSlice.blob, - _currentSlice.size); - _currentUploader.go().then( - onUploaderResolve, - onUploaderReject, - onUploaderProgress); - } - function onUploaderResolve(locator, dataSize) { - var sizeHint = (''+locator).split('+')[1]; - if (!locator || parseInt(sizeHint) !== dataSize) { - console.log("onUploaderResolve, but locator '" + locator + - "' with size hint '" + sizeHint + - "' does not look right for dataSize=" + dataSize); - return onUploaderReject({ - textStatus: "error", - err: "Bad response from slice upload" - }); - } - that.locators.push(locator); - _readPos += dataSize; - _currentUploader = null; - goSlice(); - } - function onUploaderReject(reason) { - that.state = 'Paused'; - setProgress(_readPos); - _currentUploader = null; - if (_deferred) - _deferred.reject(reason); - } - function onUploaderProgress(sliceDone, sliceSize) { - setProgress(_readPos + sliceDone); - } - function nextSlice() { - var size = Math.min( - _maxBlobSize, - that.file.size - _readPos); - setProgress(_readPos); - if (size === 0) { - return false; - } - var blob = that.file.slice( - _readPos, _readPos+size, - 'application/octet-stream; charset=x-user-defined'); - return {blob: blob, size: size}; - } - function setProgress(bytesDone) { - var kBps; - if (that.file.size == 0) - that.progress = 100; - else - that.progress = Math.min(100, 100 * bytesDone / that.file.size); - if (bytesDone > _startByte) { - kBps = (bytesDone - _startByte) / - (Date.now() - _startTime); - that.statistics = ( - '' + $filter('number')(bytesDone/1024, '0') + ' KiB ' + - 'at ~' + $filter('number')(kBps, '0') + ' KiB/s') - if (that.state === 'Paused') { - that.statistics += ', paused'; - } else if (that.state === 'Uploading') { - that.statistics += ', ETA ' + - $filter('date')( - new Date( - Date.now() + (that.file.size - bytesDone) / kBps), - 'shortTime') - } - } else { - that.statistics = that.state; - } - if (that.state === 'Uploaded') { - // 'Uploaded' gets reported as 'finished', which is a - // little misleading because the collection hasn't - // been updated yet. But FileUploader's portion of the - // work (and the time when it makes sense to show - // speed and ETA) is finished. - that.statistics += ', finished ' + - $filter('date')(Date.now(), 'shortTime'); - _finishTime = Date.now(); - } - if (_deferred) - _deferred.notify(); - } - } - - function QueueUploader() { - $.extend(this, { - state: 'Idle', // Idle, Running, Stopped, Failed - stateReason: null, - statusSuccess: null, - go: go, - stop: stop - }); - //////////////////////////////// - var that = this; - var _deferred; // the one we promise to go()'s caller - var _deferredAppend; // tracks current appendToCollection - function go() { - if (_deferred) return _deferred.promise(); - if (_deferredAppend) return _deferredAppend.promise(); - _deferred = $.Deferred(); - that.state = 'Running'; - ArvadosClient.apiPromise( - 'keep_services', 'list', - {filters: [['service_type','=','proxy']]}). - then(doQueueWithProxy); - onQueueProgress(); - return _deferred.promise().always(function() { _deferred = null; }); - } - function stop() { - that.state = 'Stopped'; - if (_deferred) { - _deferred.reject({}); - } - for (var i=0; i<$scope.uploadQueue.length; i++) - $scope.uploadQueue[i].stop(); - onQueueProgress(); - } - function doQueueWithProxy(data) { - keepProxy = data.items[0]; - if (!keepProxy) { - that.state = 'Failed'; - that.stateReason = - 'There seems to be no Keep proxy service available.'; - _deferred.reject(null, 'error', that.stateReason); - return; - } - return doQueueWork(); - } - function doQueueWork() { - // If anything is not Done, do it. - if ($scope.uploadQueue.length > 0 && - $scope.uploadQueue[0].state !== 'Done') { - if (_deferred) { - that.stateReason = null; - return $scope.uploadQueue[0].go(). - then(appendToCollection, null, onQueueProgress). - then(doQueueWork, onQueueReject); - } else { - // Queue work has been stopped. Just update the - // view. - onQueueProgress(); - return; - } - } - // If everything is Done, resolve the promise and clean - // up. Note this can happen even after the _deferred - // promise has been rejected: specifically, when stop() is - // called too late to prevent completion of the last - // upload. In that case we want to update state to "Idle", - // rather than leave it at "Stopped". - onQueueResolve(); - } - function onQueueReject(reason) { - if (!_deferred) { - // Outcome has already been decided (by stop()). - return; - } - - that.state = 'Failed'; - that.stateReason = ( - (reason.textStatus || 'Error') + - (reason.xhr && reason.xhr.options - ? (' (from ' + reason.xhr.options.url + ')') - : '') + - ': ' + - (reason.err || defaultErrorMessage)); - if (reason.xhr && reason.xhr.responseText) - that.stateReason += ' -- ' + reason.xhr.responseText; - _deferred.reject(reason); - onQueueProgress(); - } - function onQueueResolve() { - that.state = 'Idle'; - that.stateReason = 'Done!'; - if (_deferred) - _deferred.resolve(); - onQueueProgress(); - } - function onQueueProgress() { - // Ensure updates happen after FileUpload promise callbacks. - $timeout(function(){$scope.$apply();}); - } - function appendToCollection(uploads) { - _deferredAppend = $.Deferred(); - ArvadosClient.apiPromise( - 'collections', 'get', - { uuid: $scope.uuid }). - then(function(collection) { - var manifestText = ''; - $.each(uploads, function(_, upload) { - var locators = upload.locators; - if (locators.length === 0) { - // Every stream must have at least one - // data locator, even if it is zero bytes - // long: - locators = ['d41d8cd98f00b204e9800998ecf8427e+0']; - } - filename = ArvadosClient.uniqueNameForManifest( - collection.manifest_text, - '.', upload.file.name); - collection.manifest_text += '. ' + - locators.join(' ') + - ' 0:' + upload.file.size.toString() + ':' + - filename + - '\n'; - }); - return ArvadosClient.apiPromise( - 'collections', 'update', - { uuid: $scope.uuid, - collection: - { manifest_text: - collection.manifest_text } - }); - }). - then(function() { - // Mark the completed upload(s) as Done and push - // them to the bottom of the queue. - var i, qLen = $scope.uploadQueue.length; - for (i=0; i= 0) { - $scope.uploadQueue[i].state = 'Done'; - $scope.uploadQueue.push.apply( - $scope.uploadQueue, - $scope.uploadQueue.splice(i, 1)); - --i; - --qLen; - } - } - }). - then(_deferredAppend.resolve, - _deferredAppend.reject); - return _deferredAppend.promise(). - always(function() { - _deferredAppend = null; - }); - } - } -} diff --git a/apps/workbench/app/assets/javascripts/user_agreements.js b/apps/workbench/app/assets/javascripts/user_agreements.js deleted file mode 100644 index 7ce534239b..0000000000 --- a/apps/workbench/app/assets/javascripts/user_agreements.js +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright (C) The Arvados Authors. All rights reserved. -// -// SPDX-License-Identifier: AGPL-3.0 - -function enable_okbutton() { - var $div = $('#open_user_agreement'); - var allchecked = $('input[name="checked[]"]', $div).not(':checked').length == 0; - $('input[type=submit]', $div).prop('disabled', !allchecked); -} -$(document).on('click keyup input', '#open_user_agreement input', enable_okbutton); -$(document).on('ready ajax:complete', enable_okbutton); diff --git a/apps/workbench/app/assets/javascripts/users.js b/apps/workbench/app/assets/javascripts/users.js deleted file mode 100644 index 565ea9cbd6..0000000000 --- a/apps/workbench/app/assets/javascripts/users.js +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright (C) The Arvados Authors. All rights reserved. -// -// SPDX-License-Identifier: AGPL-3.0 - -$(document). - on('notifications:recount', - function() { - var menu = $('.notification-menu'); - n = $('.notification', menu).not('.empty').length; - $('.notification-count', menu).html(n>0 ? n : ''); - }). - on('ajax:success', 'form.new_authorized_key', - function(e, data, status, xhr) { - $(e.target).parents('.notification').eq(0).fadeOut('slow', function() { - $('
  • SSH key added.
  • ').hide().replaceAll(this).fadeIn('slow'); - $(document).trigger('notifications:recount'); - }); - }). - on('ajax:complete', 'form.new_authorized_key', - function(e, data, status, xhr) { - $($('input[name=disable_element]', e.target).val()). - fadeTo(200, 1.0); - }). - on('ajax:error', 'form.new_authorized_key', - function(e, xhr, status, error) { - var error_div; - response = $.parseJSON(xhr.responseText); - error_div = $(e.target).parent().find('div.ajax-errors'); - if (error_div.length == 0) { - $(e.target).parent().append('
    '); - error_div = $(e.target).parent().find('div.ajax-errors'); - } - if (response.errors) { - error_div.html($('

    ').text(response.errors).html()); - } else { - error_div.html('

    Sorry, request failed.

    '); - } - error_div.show(); - $($('input[name=disable_element]', e.target).val()). - fadeTo(200, 1.0); - }). - on('click', 'form[data-remote] input[type=submit]', - function(e) { - $(e.target).parents('form').eq(0).parent().find('div.ajax-errors').html('').hide(); - $($(e.target). - parents('form'). - find('input[name=disable_element]'). - val()). - fadeTo(200, 0.3); - return true; - }); diff --git a/apps/workbench/app/assets/javascripts/work_unit_component.js b/apps/workbench/app/assets/javascripts/work_unit_component.js deleted file mode 100644 index a84a2e71b8..0000000000 --- a/apps/workbench/app/assets/javascripts/work_unit_component.js +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (C) The Arvados Authors. All rights reserved. -// -// SPDX-License-Identifier: AGPL-3.0 - -$(document). - on('click', '.component-detail-panel', function(event) { - var href = $($(event.target).attr('href')); - if ($(href).hasClass("in")) { - var content_div = href.find('.work-unit-component-detail-body'); - content_div.html('
    '); - var content_url = href.attr('content-url'); - var action_data = href.attr('action-data'); - $.ajax(content_url, {dataType: 'html', type: 'POST', data: {action_data: action_data}}). - done(function(data, status, jqxhr) { - content_div.html(data); - }).fail(function(jqxhr, status, error) { - content_div.html(error); - }); - } - }); diff --git a/apps/workbench/app/assets/javascripts/work_unit_log.js b/apps/workbench/app/assets/javascripts/work_unit_log.js deleted file mode 100644 index c43bae0e3c..0000000000 --- a/apps/workbench/app/assets/javascripts/work_unit_log.js +++ /dev/null @@ -1,69 +0,0 @@ -// Copyright (C) The Arvados Authors. All rights reserved. -// -// SPDX-License-Identifier: AGPL-3.0 - -$(document).on('arv-log-event', '.arv-log-event-handler-append-logs', function(event, eventData){ - var wasatbottom, txt; - if (this != event.target) { - // Not interested in events sent to child nodes. - return; - } - - if (!('properties' in eventData)) { - return; - } - - txt = ''; - if ('text' in eventData.properties && - eventData.properties.text.length > 0) { - txt += eventData.properties.text; - if (txt.slice(txt.length-1) != "\n") { - txt += "\n"; - } - } - if (eventData.event_type == 'update' && - eventData.object_uuid.indexOf("-dz642-") == 5 && - 'old_attributes' in eventData.properties && - 'new_attributes' in eventData.properties) { - // Container update - if (eventData.properties.old_attributes.state != eventData.properties.new_attributes.state) { - var stamp = eventData.event_at + " "; - switch(eventData.properties.new_attributes.state) { - case "Queued": - txt += stamp + "Container "+eventData.object_uuid+" was returned to the queue\n"; - break; - case "Locked": - txt += stamp + "Container "+eventData.object_uuid+" was taken from the queue by a dispatch process\n"; - break; - case "Running": - txt += stamp + "Container "+eventData.object_uuid+" started\n"; - break; - case "Complete": - txt += stamp + "Container "+eventData.object_uuid+" finished\n"; - break; - case "Cancelled": - txt += stamp + "Container "+eventData.object_uuid+" was cancelled\n"; - break; - default: - // Unknown state -- unexpected, might as well log it. - txt += stamp + "Container "+eventData.object_uuid+" changed state to " + - eventData.properties.new_attributes.state + "\n"; - break; - } - } - } - - if (txt == '') { - return; - } - - wasatbottom = (this.scrollTop + this.clientHeight >= this.scrollHeight); - if (eventData.prepend) { - $(this).prepend(txt); - } else { - $(this).append(txt); - } - if (wasatbottom) { - this.scrollTop = this.scrollHeight; - } -}); diff --git a/apps/workbench/app/assets/stylesheets/api_client_authorizations.css.scss b/apps/workbench/app/assets/stylesheets/api_client_authorizations.css.scss deleted file mode 100644 index ec87eb255f..0000000000 --- a/apps/workbench/app/assets/stylesheets/api_client_authorizations.css.scss +++ /dev/null @@ -1,7 +0,0 @@ -// Copyright (C) The Arvados Authors. All rights reserved. -// -// SPDX-License-Identifier: AGPL-3.0 - -// Place all the styles related to the ApiClientAuthorizations controller here. -// They will automatically be included in application.css. -// You can use Sass (SCSS) here: http://sass-lang.com/ diff --git a/apps/workbench/app/assets/stylesheets/application.css.scss b/apps/workbench/app/assets/stylesheets/application.css.scss deleted file mode 100644 index 1f21c39729..0000000000 --- a/apps/workbench/app/assets/stylesheets/application.css.scss +++ /dev/null @@ -1,355 +0,0 @@ -// Copyright (C) The Arvados Authors. All rights reserved. -// -// SPDX-License-Identifier: AGPL-3.0 - -/* - * This is a manifest file that'll be compiled into application.css, which will include all the files - * listed below. - * - * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets, - * or vendor/assets/stylesheets of plugins, if any, can be referenced here using a relative path. - * - * You're free to add application-wide styles to this file and they'll appear at the top of the - * compiled file, but it's generally better to create a new file per style scope. - * - *= require_self - *= require bootstrap3-editable/bootstrap-editable - *= require morris - *= require awesomplete - *= require_tree . - */ - -@import "bootstrap-sprockets"; -@import "bootstrap"; - -.contain-align-left { - text-align: left; -} -table.topalign>tbody>tr>td { - vertical-align: top; -} -table.topalign>thead>tr>td { - vertical-align: bottom; -} -tr.cell-valign-center>td { - vertical-align: middle; -} -tr.cell-noborder>td,tr.cell-noborder>th { - border: none; -} -table.table-justforlayout>tr>td, -table.table-justforlayout>tr>th, -table.table-justforlayout>thead>tr>td, -table.table-justforlayout>thead>tr>th, -table.table-justforlayout>tbody>tr>td, -table.table-justforlayout>tbody>tr>th{ - border: none; -} -table.table-justforlayout { - margin-bottom: 0; -} -.smaller-text { - font-size: .8em; -} -.deemphasize { - font-size: .8em; - color: #888; -} -.lighten { - color: #888; -} -.arvados-filename, -.arvados-uuid { - font-size: .8em; - font-family: monospace; -} -table .data-size, .table .data-size { - text-align: right; -} -body .editable-empty { - color: #999; -} -body .editable-empty:hover { - color: #0088cc; -} -table.arv-index tbody td.arv-object-AuthorizedKey.arv-attr-public_key { - overflow-x: hidden; - max-width: 120px; -} -table.arv-index > thead > tr > th { - border-top: none; -} -table.table-fixedlayout { - white-space: nowrap; - table-layout: fixed; -} -table.table-fixedlayout td { - overflow: hidden; - overflow-x: hidden; - text-overflow: ellipsis; -} -table.table-smallcontent td { - font-size: 85%; -} -form input.search-mini { - padding: 0 6px; -} -form.small-form-margin { - margin-bottom: 2px; -} -.nowrap { - white-space: nowrap; -} -input.select-on-focus { - font-family: monospace; - background: inherit; - border: thin #ccc solid; - border-radius: .2em; - padding: .15em .5em; -} -input.select-on-focus:focus { - border-color: #9bf; -} - -/* top nav */ -$top-nav-bg: #3c163d; -$top-nav-bg-bottom: #260027; -nav.navbar-fixed-top .navbar-brand { - color: #79537a; - letter-spacing: 0.4em; -} -nav.navbar-fixed-top { - background: $top-nav-bg; - background: linear-gradient(to bottom, $top-nav-bg 0%,$top-nav-bg-bottom 100%); -} -.navbar.breadcrumbs { - line-height: 50px; - border-radius: 0; - margin-bottom: 0; - border-right: 0; - border-left: 0; -} -.navbar.breadcrumbs .nav > li > a, -.navbar.breadcrumbs .nav > li { - color: #000; -} -.navbar.breadcrumbs .nav > li.nav-separator > i { - color: #bbb; -} -.navbar.breadcrumbs .navbar-form { - margin-top: 0px; - margin-bottom: 0px; -} -.navbar.breadcrumbs .navbar-text { - margin-top: 0px; - margin-bottom: 0px; -} - -nav.navbar-fixed-top .navbar-nav.navbar-right > li.open > a, -nav.navbar-fixed-top .navbar-nav.navbar-right > li.open > a:focus, -nav.navbar-fixed-top .navbar-nav.navbar-right > li.open > a:hover { - background: lighten($top-nav-bg, 5%); -} -nav.navbar-fixed-top .navbar-nav.navbar-right > li > a, -nav.navbar-fixed-top .navbar-nav.navbar-right > li > a:focus, -nav.navbar-fixed-top .navbar-nav.navbar-right > li > a:hover { - color: #fff; -} - -.dax { - max-width: 10%; - margin-right: 1em; - float: left -} - -.smart-scroll { - overflow: auto; - margin-bottom: -15px; -} - -.infinite-scroller .fa-warning { - color: #800; -} - -th[data-sort-order] { - cursor: pointer; -} - -.inline-progress-container div.progress { - margin-bottom: 0; -} - -.inline-progress-container { - width: 100%; - display:inline-block; -} - -td.add-tag-button { - white-space: normal; -} -td.add-tag-button .add-tag-button { - margin-right: 4px; - opacity: 0.2; -} -td.add-tag-button .add-tag-button:hover { - opacity: 1; -} -span.removable-tag-container { - line-height: 1.6; -} -.label.removable-tag a { - color: #fff; - cursor: pointer; -} - -li.notification { - padding: 10px; -} - -td.trash-project-msg { - white-space: normal; -} - -// See HeaderRowFixer in application.js -table.table-fixed-header-row { - width: 100%; - border-spacing: 0px; - margin:0; -} -table.table-fixed-header-row thead { - position:fixed; - background: #fff; -} -table.table-fixed-header-row tbody { - position:relative; - top:1.5em; -} - -.dropdown-menu { - max-height: 30em; - overflow-y: auto; -} - -.dropdown-menu a { - cursor: pointer; -} - -.row-fill-height, .row-fill-height>div[class*='col-'] { - display: flex; -} -.row-fill-height>div[class*='col-']>div { - width: 100%; -} - -/* Show editable popover above side-nav */ -.editable-popup.popover { - z-index:1055; -} - -/* Do not leave space for left-nav */ -div#wrapper { - padding-left: 0; -} - -.arv-description-as-subtitle { - padding-bottom: 1em; -} -.arv-description-in-table { - height: 4em; - overflow-x: hidden; - overflow-y: hidden; -} -.arv-description-in-table:hover { - overflow-y: auto; -} - -.btn.btn-nodecorate { - border: none; -} -svg text { - font-size: 6pt; -} - -div.pane-content iframe { - width: 100%; - border: none; -} -span.editable-textile { - display: inline-block; -} -.text-overflow-ellipsis { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} -.time-label-divider { - font-size: 80%; - min-width: 1em; - padding: 0px 2px 0px 0px; -} -.task-summary-status { - font-size: 80%; -} -#page-wrapper > div > h2 { - margin-top: 0px; -} - -.compute-summary-numbers td { - font-size: 150%; -} - -.arv-log-refresh-control { - display: none; -} - -/* Hide Angular content until Angular is ready */ -[ng\:cloak], [ng-cloak], .ng-cloak { - display: none !important; -} - -/* tabs */ -ul.nav.nav-tabs { - font-size: 90% -} - -.hover-dropdown:hover .dropdown-menu { - display: block; -} - -.arv-description-as-subtitle .editable-inline, -.arv-description-as-subtitle .editable-inline .form-group, -.arv-description-as-subtitle .editable-inline .form-group .editable-input, -.arv-description-as-subtitle .editable-inline .form-group .editable-input textarea, -{ - width: 98%!important; -} - -/* Needed for awesomplete to play nice with bootstrap */ -div.awesomplete { - display: block; -} -/* Makes awesomplete listings to be scrollable */ -.awesomplete > ul { - max-height: 410px; - overflow-y: auto; -} - -.dropdown-menu > li > form > button { - display: block; - padding: 3px 20px; - clear: both; - font-weight: normal; - line-height: 1.428571429; - color: #333333; - white-space: nowrap; - cursor: pointer; - text-decoration: none; - background: transparent; - border-style: none; -} - -.dropdown-menu > li > form > button:hover { - text-decoration: none; - color: #262626; - background-color: #f5f5f5; -} diff --git a/apps/workbench/app/assets/stylesheets/authorized_keys.css.scss b/apps/workbench/app/assets/stylesheets/authorized_keys.css.scss deleted file mode 100644 index 73cfd5bf8a..0000000000 --- a/apps/workbench/app/assets/stylesheets/authorized_keys.css.scss +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright (C) The Arvados Authors. All rights reserved. -// -// SPDX-License-Identifier: AGPL-3.0 - -// Place all the styles related to the AuthorizedKeys controller here. -// They will automatically be included in application.css. -// You can use Sass (SCSS) here: http://sass-lang.com/ -form .table input[type=text] { - width: 600px; -} -form .table textarea { - width: 600px; - height: 10em; -} diff --git a/apps/workbench/app/assets/stylesheets/badges.css.scss b/apps/workbench/app/assets/stylesheets/badges.css.scss deleted file mode 100644 index ddaf5b9ea5..0000000000 --- a/apps/workbench/app/assets/stylesheets/badges.css.scss +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright (C) The Arvados Authors. All rights reserved. -// -// SPDX-License-Identifier: AGPL-3.0 - -/* Colors - * Contextual variations of badges - * Bootstrap 3.0 removed contexts for badges, we re-introduce them, based on what is done for labels - */ - -.badge.badge-error { - background-color: #b94a48; -} - -.badge.badge-warning { - background-color: #f89406; -} - -.badge.badge-success { - background-color: #468847; -} - -.badge.badge-info { - background-color: #3a87ad; -} - -.badge.badge-inverse { - background-color: #333333; -} - -.badge.badge-alert { - background: red; -} diff --git a/apps/workbench/app/assets/stylesheets/cards.css.scss b/apps/workbench/app/assets/stylesheets/cards.css.scss deleted file mode 100644 index 3cf29c56fb..0000000000 --- a/apps/workbench/app/assets/stylesheets/cards.css.scss +++ /dev/null @@ -1,89 +0,0 @@ -// Copyright (C) The Arvados Authors. All rights reserved. -// -// SPDX-License-Identifier: AGPL-3.0 - -.card { - padding-top: 20px; - margin: 10px 0 20px 0; - background-color: #ffffff; - border: 1px solid #d8d8d8; - border-top-width: 0; - border-bottom-width: 2px; - -webkit-border-radius: 3px; - -moz-border-radius: 3px; - border-radius: 3px; - -webkit-box-shadow: none; - -moz-box-shadow: none; - box-shadow: none; - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; -} -.card.arvados-object { - position: relative; - display: inline-block; - width: 170px; - height: 175px; - padding-top: 0; - margin-left: 20px; - overflow: hidden; - vertical-align: top; -} -.card.arvados-object .card-top.green { - background-color: #53a93f; -} -.card.arvados-object .card-top.blue { - background-color: #427fed; -} -.card.arvados-object .card-top { - position: absolute; - top: 0; - left: 0; - display: inline-block; - width: 170px; - height: 25px; - background-color: #ffffff; -} -.card.arvados-object .card-info { - position: absolute; - top: 25px; - display: inline-block; - width: 100%; - height: 101px; - overflow: hidden; - background: #ffffff; - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; -} -.card.arvados-object .card-info .title { - display: block; - margin: 8px 14px 0 14px; - overflow: hidden; - font-size: 16px; - font-weight: bold; - line-height: 18px; - color: #404040; -} -.card.arvados-object .card-info .desc { - display: block; - margin: 8px 14px 0 14px; - overflow: hidden; - font-size: 12px; - line-height: 16px; - color: #737373; - text-overflow: ellipsis; -} -.card.arvados-object .card-bottom { - position: absolute; - bottom: 0; - left: 0; - display: inline-block; - width: 100%; - padding: 10px 20px; - line-height: 29px; - text-align: center; - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; -} diff --git a/apps/workbench/app/assets/stylesheets/collections.css.scss b/apps/workbench/app/assets/stylesheets/collections.css.scss deleted file mode 100644 index c5cc699b33..0000000000 --- a/apps/workbench/app/assets/stylesheets/collections.css.scss +++ /dev/null @@ -1,80 +0,0 @@ -// Copyright (C) The Arvados Authors. All rights reserved. -// -// SPDX-License-Identifier: AGPL-3.0 - -/* Style for _show_files tree view. */ - -ul#collection_files { - padding: 0 .5em; -} - -ul.collection_files { - line-height: 2.5em; - list-style-type: none; - padding-left: 2.3em; -} - -ul.collection_files li { - clear: both; -} - -.collection_files_row { - padding: 1px; /* Replaced by border for :hover */ -} - -.collection_files_row:hover { - background-color: #D9EDF7; - padding: 0px; - border: 1px solid #BCE8F1; - border-radius: 3px; -} - -.collection_files_inline { - clear: both; - width: 80%; - margin: 0 3em; -} - -.collection_files_inline img { - max-height: 15em; -} - -.collection_files_name { - padding-left: .5em; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.collection_files_name i.fa-fw:first-child { - width: 1.6em; -} - -/* - "active" and "inactive" colors are too similar for a toggle switch - in the default bootstrap theme. - */ - -$inactive-bg: #5bc0de; -$active-bg: #39b3d7; - -.btn-group.toggle-persist .btn { - width: 6em; -} -.btn-group.toggle-persist .btn-info { - background-color: lighten($inactive-bg, 15%); -} - -.btn-group.toggle-persist .btn-info.active { - background-color: $active-bg; -} - -.lock-collection-btn { - display: inline-block; - padding: .5em 2em; - margin: 0 1em; -} - -.collection-tag-field * { - display: inline-block; -} diff --git a/apps/workbench/app/assets/stylesheets/groups.css.scss b/apps/workbench/app/assets/stylesheets/groups.css.scss deleted file mode 100644 index 905e72add9..0000000000 --- a/apps/workbench/app/assets/stylesheets/groups.css.scss +++ /dev/null @@ -1,7 +0,0 @@ -// Copyright (C) The Arvados Authors. All rights reserved. -// -// SPDX-License-Identifier: AGPL-3.0 - -// Place all the styles related to the Groups controller here. -// They will automatically be included in application.css. -// You can use Sass (SCSS) here: http://sass-lang.com/ diff --git a/apps/workbench/app/assets/stylesheets/humans.css.scss b/apps/workbench/app/assets/stylesheets/humans.css.scss deleted file mode 100644 index 29668c2737..0000000000 --- a/apps/workbench/app/assets/stylesheets/humans.css.scss +++ /dev/null @@ -1,7 +0,0 @@ -// Copyright (C) The Arvados Authors. All rights reserved. -// -// SPDX-License-Identifier: AGPL-3.0 - -// Place all the styles related to the Humans controller here. -// They will automatically be included in application.css. -// You can use Sass (SCSS) here: http://sass-lang.com/ diff --git a/apps/workbench/app/assets/stylesheets/job_tasks.css.scss b/apps/workbench/app/assets/stylesheets/job_tasks.css.scss deleted file mode 100644 index 0d4d2607bb..0000000000 --- a/apps/workbench/app/assets/stylesheets/job_tasks.css.scss +++ /dev/null @@ -1,7 +0,0 @@ -// Copyright (C) The Arvados Authors. All rights reserved. -// -// SPDX-License-Identifier: AGPL-3.0 - -// Place all the styles related to the JobTasks controller here. -// They will automatically be included in application.css. -// You can use Sass (SCSS) here: http://sass-lang.com/ diff --git a/apps/workbench/app/assets/stylesheets/jobs.css.scss b/apps/workbench/app/assets/stylesheets/jobs.css.scss deleted file mode 100644 index 9b1ea659a1..0000000000 --- a/apps/workbench/app/assets/stylesheets/jobs.css.scss +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (C) The Arvados Authors. All rights reserved. -// -// SPDX-License-Identifier: AGPL-3.0 - -.arv-job-log-window { - height: 40em; - white-space: pre; - overflow: scroll; - background: black; - color: white; - font-family: monospace; - font-size: .8em; - border: 2px solid black; -} - -.morris-hover-point { - text-align: left; - width: 100%; -} \ No newline at end of file diff --git a/apps/workbench/app/assets/stylesheets/keep_disks.css.scss b/apps/workbench/app/assets/stylesheets/keep_disks.css.scss deleted file mode 100644 index 0985d8c8d4..0000000000 --- a/apps/workbench/app/assets/stylesheets/keep_disks.css.scss +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (C) The Arvados Authors. All rights reserved. -// -// SPDX-License-Identifier: AGPL-3.0 - -// Place all the styles related to the KeepDisks controller here. -// They will automatically be included in application.css. -// You can use Sass (SCSS) here: http://sass-lang.com/ - -/* Margin allows us some space between the table above. */ -div.graph { - margin-top: 20px; -} -div.graph h3, div.graph h4 { - text-align: center; -} diff --git a/apps/workbench/app/assets/stylesheets/links.css.scss b/apps/workbench/app/assets/stylesheets/links.css.scss deleted file mode 100644 index cf4c4e70cf..0000000000 --- a/apps/workbench/app/assets/stylesheets/links.css.scss +++ /dev/null @@ -1,7 +0,0 @@ -// Copyright (C) The Arvados Authors. All rights reserved. -// -// SPDX-License-Identifier: AGPL-3.0 - -// Place all the styles related to the Links controller here. -// They will automatically be included in application.css. -// You can use Sass (SCSS) here: http://sass-lang.com/ diff --git a/apps/workbench/app/assets/stylesheets/loading.css.scss.erb b/apps/workbench/app/assets/stylesheets/loading.css.scss.erb deleted file mode 100644 index ee6ca34308..0000000000 --- a/apps/workbench/app/assets/stylesheets/loading.css.scss.erb +++ /dev/null @@ -1,72 +0,0 @@ -<%# Copyright (C) The Arvados Authors. All rights reserved. - -SPDX-License-Identifier: AGPL-3.0 %> - -.loading { - opacity: 0; -} - -.spinner { - /* placeholder for stuff like $.find('.spinner').detach() */ -} - -.spinner-32px { - background-image: url('<%= asset_path('spinner_32px.gif') %>'); - background-repeat: no-repeat; - width: 32px; - height: 32px; -} - -.spinner-h-center { - margin-left: auto; - margin-right: auto; -} - -.spinner-v-center { - position: relative; - top: 45%; -} - -.rotating { - color: #f00; - /* Chrome and Firefox, at least in Linux, render a horrible shaky - mess -- better not to bother. - - animation-name: rotateThis; - animation-duration: 2s; - animation-iteration-count: infinite; - animation-timing-function: linear; - -moz-animation-name: rotateThis; - -moz-animation-duration: 2s; - -moz-animation-iteration-count: infinite; - -moz-animation-timing-function: linear; - -ms-animation-name: rotateThis; - -ms-animation-duration: 2s; - -ms-animation-iteration-count: infinite; - -ms-animation-timing-function: linear; - -webkit-animation-name: rotateThis; - -webkit-animation-duration: 2s; - -webkit-animation-iteration-count: infinite; - -webkit-animation-timing-function: linear; - */ -} - -@keyframes rotateThis { - from { transform: rotate( 0deg ); } - to { transform: rotate( 360deg ); } -} - -@-webkit-keyframes rotateThis { - from { -webkit-transform: rotate( 0deg ); } - to { -webkit-transform: rotate( 360deg ); } -} - -@-moz-keyframes rotateThis { - from { -moz-transform: rotate( 0deg ); } - to { -moz-transform: rotate( 360deg ); } -} - -@-ms-keyframes rotateThis { - from { -ms-transform: rotate( 0deg ); } - to { -ms-transform: rotate( 360deg ); } -} diff --git a/apps/workbench/app/assets/stylesheets/log_viewer.scss b/apps/workbench/app/assets/stylesheets/log_viewer.scss deleted file mode 100644 index c3fa8b96c6..0000000000 --- a/apps/workbench/app/assets/stylesheets/log_viewer.scss +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright (C) The Arvados Authors. All rights reserved. -// -// SPDX-License-Identifier: AGPL-3.0 - -.log-viewer-table { - width: 100%; - font-family: "Lucida Console", Monaco, monospace; - font-size: 11px; - table-layout: fixed; - thead tr { - th { - padding-right: 1em; - } - th.id { - display: none; - } - th.timestamp { - width: 15em; - } - th.type { - width: 8em; - } - th.taskid { - width: 4em; - } - th.node { - width: 8em; - } - th.slot { - width: 3em; - } - th.message { - width: auto; - } - } - tbody tr { - vertical-align: top; - td { - padding-right: 1em; - } - td.id { - display: none; - } - td.taskid { - text-align: right; - } - td.slot { - text-align: right; - } - td.message { - word-wrap: break-word; - } - } -} - -.log-viewer-button { - width: 12em; -} - -.log-viewer-paging-div { - font-size: 18px; - text-align: center; -} - -.log-viewer-page-num { - padding-left: .3em; - padding-right: .3em; -} \ No newline at end of file diff --git a/apps/workbench/app/assets/stylesheets/logs.css.scss b/apps/workbench/app/assets/stylesheets/logs.css.scss deleted file mode 100644 index c8b22f9f5f..0000000000 --- a/apps/workbench/app/assets/stylesheets/logs.css.scss +++ /dev/null @@ -1,7 +0,0 @@ -// Copyright (C) The Arvados Authors. All rights reserved. -// -// SPDX-License-Identifier: AGPL-3.0 - -// Place all the styles related to the Logs controller here. -// They will automatically be included in application.css. -// You can use Sass (SCSS) here: http://sass-lang.com/ diff --git a/apps/workbench/app/assets/stylesheets/nodes.css.scss b/apps/workbench/app/assets/stylesheets/nodes.css.scss deleted file mode 100644 index a7b08612d7..0000000000 --- a/apps/workbench/app/assets/stylesheets/nodes.css.scss +++ /dev/null @@ -1,7 +0,0 @@ -// Copyright (C) The Arvados Authors. All rights reserved. -// -// SPDX-License-Identifier: AGPL-3.0 - -// Place all the styles related to the Nodes controller here. -// They will automatically be included in application.css. -// You can use Sass (SCSS) here: http://sass-lang.com/ diff --git a/apps/workbench/app/assets/stylesheets/pipeline_instances.css.scss b/apps/workbench/app/assets/stylesheets/pipeline_instances.css.scss deleted file mode 100644 index 135685c27c..0000000000 --- a/apps/workbench/app/assets/stylesheets/pipeline_instances.css.scss +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright (C) The Arvados Authors. All rights reserved. -// -// SPDX-License-Identifier: AGPL-3.0 - -// Place all the styles related to the PipelineInstances controller here. -// They will automatically be included in application.css. -// You can use Sass (SCSS) here: http://sass-lang.com/ - -.pipeline-compare-headrow div { - padding-top: .5em; - padding-bottom: .5em; -} -.pipeline-compare-headrow:first-child { - border-bottom: 1px solid black; -} -.pipeline-compare-row .notnormal { - background: #ffffaa; -} - -.pipeline_color_legend { - margin-top: 0.2em; - padding: 0.2em 1em; - border: 1px solid #000; -} -.pipeline_color_legend a { - color: #000; -} - -.col-md-1.pipeline-instance-spacing { - padding: 0px; - margin: 0px; -} - -.col-md-3.pipeline-instance-spacing > .progress { - padding: 0px; - margin: 0px; -} \ No newline at end of file diff --git a/apps/workbench/app/assets/stylesheets/pipeline_templates.css.scss b/apps/workbench/app/assets/stylesheets/pipeline_templates.css.scss deleted file mode 100644 index 329f0ed813..0000000000 --- a/apps/workbench/app/assets/stylesheets/pipeline_templates.css.scss +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright (C) The Arvados Authors. All rights reserved. -// -// SPDX-License-Identifier: AGPL-3.0 - -// Place all the styles related to the PipelineTemplates controller here. -// They will automatically be included in application.css. -// You can use Sass (SCSS) here: http://sass-lang.com/ - -.pipeline_color_legend { - padding-left: 1em; - padding-right: 1em; -} - -table.pipeline-components-table { - width: 100%; - table-layout: fixed; - overflow: hidden; -} - -table.pipeline-components-table thead th { - text-align: bottom; -} -table.pipeline-components-table div.progress { - margin-bottom: 0; -} - -table.pipeline-components-table td { - overflow: hidden; - text-overflow: ellipsis; -} - -td.required { - background: #ffdddd; -} diff --git a/apps/workbench/app/assets/stylesheets/projects.css.scss b/apps/workbench/app/assets/stylesheets/projects.css.scss deleted file mode 100644 index 10c2ed0591..0000000000 --- a/apps/workbench/app/assets/stylesheets/projects.css.scss +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright (C) The Arvados Authors. All rights reserved. -// -// SPDX-License-Identifier: AGPL-3.0 - -.arv-project-list > .row { - padding-top: 5px; - padding-bottom: 5px; - padding-right: 1em; -} -.arv-project-list > .row.project:hover { - background: #d9edf7; -} -div.scroll-20em { - height: 20em; - overflow-y: scroll; -} - -.compute-summary { - margin: 0.15em 0em 0.15em 0em; - display: inline-block; -} - -.compute-summary-head { - margin-left: 0.3em; -} - -.compute-detail { - border: 1px solid; - border-color: #DDD; - border-radius: 3px; - padding: 0.2em; - position: absolute; - z-index: 1; - background: white; -} - -.compute-detail:hover { - cursor: pointer; -} - -.compute-node-summary:hover { - cursor: pointer; -} - -.compute-summary-numbers .panel { - margin-bottom: 0px; -} - -.compute-summary-numbers table { - width: 100%; - td,th { - text-align: center; - } -} - -.compute-summary-nodelist { - margin-bottom: 10px -} - -.dashboard-panel-info-row { - padding: .5em; - border-radius: .3em; -} - -.dashboard-panel-info-row:hover { - background-color: #D9EDF7; -} - -.progress-bar.progress-bar-default { - background-color: #999; -} \ No newline at end of file diff --git a/apps/workbench/app/assets/stylesheets/repositories.css.scss b/apps/workbench/app/assets/stylesheets/repositories.css.scss deleted file mode 100644 index 1dd9a16603..0000000000 --- a/apps/workbench/app/assets/stylesheets/repositories.css.scss +++ /dev/null @@ -1,7 +0,0 @@ -// Copyright (C) The Arvados Authors. All rights reserved. -// -// SPDX-License-Identifier: AGPL-3.0 - -// Place all the styles related to the Repositories controller here. -// They will automatically be included in application.css. -// You can use Sass (SCSS) here: http://sass-lang.com/ diff --git a/apps/workbench/app/assets/stylesheets/sb-admin.css.scss b/apps/workbench/app/assets/stylesheets/sb-admin.css.scss deleted file mode 100644 index 9bae214882..0000000000 --- a/apps/workbench/app/assets/stylesheets/sb-admin.css.scss +++ /dev/null @@ -1,164 +0,0 @@ -/* -Author: Start Bootstrap - http://startbootstrap.com -'SB Admin' HTML Template by Start Bootstrap - -All Start Bootstrap themes are licensed under Apache 2.0. -For more info and more free Bootstrap 3 HTML themes, visit http://startbootstrap.com! -*/ - -/* ATTN: This is mobile first CSS - to update 786px and up screen width use the media query near the bottom of the document! */ - -/* Global Styles */ - -body { - margin-top: 50px; -} - -#wrapper { - padding-left: 0; -} - -#page-wrapper { - width: 100%; - padding: 5px 15px; -} - -/* Nav Messages */ - -.messages-dropdown .dropdown-menu .message-preview .avatar, -.messages-dropdown .dropdown-menu .message-preview .name, -.messages-dropdown .dropdown-menu .message-preview .message, -.messages-dropdown .dropdown-menu .message-preview .time { - display: block; -} - -.messages-dropdown .dropdown-menu .message-preview .avatar { - float: left; - margin-right: 15px; -} - -.messages-dropdown .dropdown-menu .message-preview .name { - font-weight: bold; -} - -.messages-dropdown .dropdown-menu .message-preview .message { - font-size: 12px; -} - -.messages-dropdown .dropdown-menu .message-preview .time { - font-size: 12px; -} - - -/* Nav Announcements */ - -.announcement-heading { - font-size: 50px; - margin: 0; -} - -.announcement-text { - margin: 0; -} - -/* Table Headers */ - -table.tablesorter thead { - cursor: pointer; -} - -table.tablesorter thead tr th:hover { - background-color: #f5f5f5; -} - -/* Flot Chart Containers */ - -.flot-chart { - display: block; - height: 400px; -} - -.flot-chart-content { - width: 100%; - height: 100%; -} - -/* Edit Below to Customize Widths > 768px */ -@media (min-width:768px) { - - /* Wrappers */ - - #wrapper { - padding-left: 225px; - } - - #page-wrapper { - padding: 15px 25px; - } - - /* Side Nav */ - - .side-nav { - margin-left: -225px; - left: 225px; - width: 225px; - position: fixed; - top: 50px; - height: calc(100% - 50px); - border-radius: 0; - border: none; - background-color: #f8f8f8; - overflow-y: auto; - overflow-x: hidden; /* no left nav scroll bar */ - } - - /* Bootstrap Default Overrides - Customized Dropdowns for the Side Nav */ - - .side-nav>li.dropdown>ul.dropdown-menu { - position: relative; - min-width: 225px; - margin: 0; - padding: 0; - border: none; - border-radius: 0; - background-color: transparent; - box-shadow: none; - -webkit-box-shadow: none; - } - - .side-nav>li.dropdown>ul.dropdown-menu>li>a { - color: #777777; - padding: 15px 15px 15px 25px; - } - - .side-nav>li.dropdown>ul.dropdown-menu>li>a:hover, - .side-nav>li.dropdown>ul.dropdown-menu>li>a.active, - .side-nav>li.dropdown>ul.dropdown-menu>li>a:focus { - background-color: #ffffff; - } - - .side-nav>li>a { - width: 225px; - } - - .navbar-default .navbar-nav.side-nav>li>a:hover, - .navbar-default .navbar-nav.side-nav>li>a:focus { - background-color: #ffffff; - } - - /* Nav Messages */ - - .messages-dropdown .dropdown-menu { - min-width: 300px; - } - - .messages-dropdown .dropdown-menu li a { - white-space: normal; - } - - .navbar-collapse { - padding-left: 15px !important; - padding-right: 15px !important; - } - -} diff --git a/apps/workbench/app/assets/stylesheets/scaffolds.css.scss b/apps/workbench/app/assets/stylesheets/scaffolds.css.scss deleted file mode 100644 index 23e0f76ca3..0000000000 --- a/apps/workbench/app/assets/stylesheets/scaffolds.css.scss +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright (C) The Arvados Authors. All rights reserved. -// -// SPDX-License-Identifier: AGPL-3.0 - -/* - We don't want the default Rails CSS, so the rules are deleted. This - empty file is left here so Rails doesn't re-add it next time it - generates a scaffold. - */ diff --git a/apps/workbench/app/assets/stylesheets/select_modal.css.scss b/apps/workbench/app/assets/stylesheets/select_modal.css.scss deleted file mode 100644 index bd7ff92a41..0000000000 --- a/apps/workbench/app/assets/stylesheets/select_modal.css.scss +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright (C) The Arvados Authors. All rights reserved. -// -// SPDX-License-Identifier: AGPL-3.0 - -.selectable-container > .row { - padding-top: 5px; - padding-bottom: 5px; - padding-right: 1em; - color: #888; -} -.selectable-container > .row.selectable { - color: #000; -} -.selectable.active, .selectable:hover { - background: #d9edf7; - cursor: pointer; -} -.selectable.active, -.selectable.active *, -.selectable.active:hover, -.selectable.active:hover * { - background: #428bca; - color: #fff; -} -.selectable-container > .row.class-separator { - background: #ddd; -} diff --git a/apps/workbench/app/assets/stylesheets/sessions.css.scss b/apps/workbench/app/assets/stylesheets/sessions.css.scss deleted file mode 100644 index e08b086209..0000000000 --- a/apps/workbench/app/assets/stylesheets/sessions.css.scss +++ /dev/null @@ -1,7 +0,0 @@ -// Copyright (C) The Arvados Authors. All rights reserved. -// -// SPDX-License-Identifier: AGPL-3.0 - -// Place all the styles related to the Sessions controller here. -// They will automatically be included in application.css. -// You can use Sass (SCSS) here: http://sass-lang.com/ diff --git a/apps/workbench/app/assets/stylesheets/specimens.css.scss b/apps/workbench/app/assets/stylesheets/specimens.css.scss deleted file mode 100644 index 60d630c8ab..0000000000 --- a/apps/workbench/app/assets/stylesheets/specimens.css.scss +++ /dev/null @@ -1,7 +0,0 @@ -// Copyright (C) The Arvados Authors. All rights reserved. -// -// SPDX-License-Identifier: AGPL-3.0 - -// Place all the styles related to the Specimens controller here. -// They will automatically be included in application.css. -// You can use Sass (SCSS) here: http://sass-lang.com/ diff --git a/apps/workbench/app/assets/stylesheets/traits.css.scss b/apps/workbench/app/assets/stylesheets/traits.css.scss deleted file mode 100644 index 7d2f7133e1..0000000000 --- a/apps/workbench/app/assets/stylesheets/traits.css.scss +++ /dev/null @@ -1,7 +0,0 @@ -// Copyright (C) The Arvados Authors. All rights reserved. -// -// SPDX-License-Identifier: AGPL-3.0 - -// Place all the styles related to the Traits controller here. -// They will automatically be included in application.css. -// You can use Sass (SCSS) here: http://sass-lang.com/ diff --git a/apps/workbench/app/assets/stylesheets/user_agreements.css.scss b/apps/workbench/app/assets/stylesheets/user_agreements.css.scss deleted file mode 100644 index d9eb5eb48e..0000000000 --- a/apps/workbench/app/assets/stylesheets/user_agreements.css.scss +++ /dev/null @@ -1,7 +0,0 @@ -// Copyright (C) The Arvados Authors. All rights reserved. -// -// SPDX-License-Identifier: AGPL-3.0 - -// Place all the styles related to the user_agreements controller here. -// They will automatically be included in application.css. -// You can use Sass (SCSS) here: http://sass-lang.com/ diff --git a/apps/workbench/app/assets/stylesheets/users.css.scss b/apps/workbench/app/assets/stylesheets/users.css.scss deleted file mode 100644 index a087ca3bb6..0000000000 --- a/apps/workbench/app/assets/stylesheets/users.css.scss +++ /dev/null @@ -1,7 +0,0 @@ -// Copyright (C) The Arvados Authors. All rights reserved. -// -// SPDX-License-Identifier: AGPL-3.0 - -// Place all the styles related to the Users controller here. -// They will automatically be included in application.css. -// You can use Sass (SCSS) here: http://sass-lang.com/ diff --git a/apps/workbench/app/assets/stylesheets/virtual_machines.css.scss b/apps/workbench/app/assets/stylesheets/virtual_machines.css.scss deleted file mode 100644 index 4a94d45111..0000000000 --- a/apps/workbench/app/assets/stylesheets/virtual_machines.css.scss +++ /dev/null @@ -1,7 +0,0 @@ -// Copyright (C) The Arvados Authors. All rights reserved. -// -// SPDX-License-Identifier: AGPL-3.0 - -// Place all the styles related to the VirtualMachines controller here. -// They will automatically be included in application.css. -// You can use Sass (SCSS) here: http://sass-lang.com/ diff --git a/apps/workbench/app/controllers/actions_controller.rb b/apps/workbench/app/controllers/actions_controller.rb deleted file mode 100644 index 7b8c8eafc8..0000000000 --- a/apps/workbench/app/controllers/actions_controller.rb +++ /dev/null @@ -1,257 +0,0 @@ -# Copyright (C) The Arvados Authors. All rights reserved. -# -# SPDX-License-Identifier: AGPL-3.0 - -require "arvados/collection" -require "app_version" - -class ActionsController < ApplicationController - - # Skip require_thread_api_token if this is a show action - # for an object uuid that supports anonymous access. - skip_around_action :require_thread_api_token, if: proc { |ctrl| - !Rails.configuration.Users.AnonymousUserToken.empty? and - 'show' == ctrl.action_name and - params['uuid'] and - model_class.in?([Collection, Group, Job, PipelineInstance, PipelineTemplate]) - } - skip_around_action :require_thread_api_token, only: [:report_issue_popup, :report_issue] - skip_before_action :check_user_agreements, only: [:report_issue_popup, :report_issue] - - @@exposed_actions = {} - def self.expose_action method, &block - @@exposed_actions[method] = true - define_method method, block - end - - def model_class - ArvadosBase::resource_class_for_uuid(params[:uuid]) - end - - def show - @object = model_class.andand.find(params[:uuid]) - if @object.is_a? Link and - @object.link_class == 'name' and - ArvadosBase::resource_class_for_uuid(@object.head_uuid) == Collection - redirect_to collection_path(id: @object.uuid) - elsif @object.is_a?(Group) and (@object.group_class == 'project' or @object.group_class == 'filter') - redirect_to project_path(id: @object.uuid) - elsif @object - redirect_to @object - else - raise ActiveRecord::RecordNotFound - end - end - - def post - params.keys.collect(&:to_sym).each do |param| - if @@exposed_actions[param] - return self.send(param) - end - end - redirect_back(fallback_location: root_path) - end - - expose_action :copy_selections_into_project do - move_or_copy :copy - end - - expose_action :move_selections_into_project do - move_or_copy :move - end - - def move_or_copy action - uuids_to_add = params["selection"] - uuids_to_add = [ uuids_to_add ] unless uuids_to_add.is_a? Array - resource_classes = uuids_to_add. - collect { |x| ArvadosBase::resource_class_for_uuid(x) }. - uniq - resource_classes.each do |resource_class| - resource_class.filter([['uuid','in',uuids_to_add]]).each do |src| - if resource_class == Collection and not Collection.attribute_info.include?(:name) - dst = Link.new(owner_uuid: @object.uuid, - tail_uuid: @object.uuid, - head_uuid: src.uuid, - link_class: 'name', - name: src.uuid) - else - case action - when :copy - dst = src.dup - if dst.respond_to? :'name=' - if dst.name - dst.name = "Copy of #{dst.name}" - else - dst.name = "Copy of unnamed #{dst.class_for_display.downcase}" - end - end - if resource_class == Collection - dst.manifest_text = Collection.select([:manifest_text]).where(uuid: src.uuid).with_count("none").first.manifest_text - # Fixes bug 19144: nullify some fields that are managed by keep-balance. - dst.storage_classes_confirmed = [] - dst.storage_classes_confirmed_at = nil - end - when :move - dst = src - else - raise ArgumentError.new "Unsupported action #{action}" - end - dst.owner_uuid = @object.uuid - dst.tail_uuid = @object.uuid if dst.class == Link - end - begin - dst.save! - rescue - dst.name += " (#{Time.now.localtime})" if dst.respond_to? :name= - dst.save! - end - end - end - if (resource_classes == [Collection] and - @object.is_a? Group and - @object.group_class == 'project') or - @object.is_a? User - # In the common case where only collections are copied/moved - # into a project, it's polite to land on the collections tab on - # the destination project. - redirect_to project_url(@object.uuid, anchor: 'Data_collections') - else - # Otherwise just land on the default (Description) tab. - redirect_to @object - end - end - - expose_action :combine_selected_files_into_collection do - uuids, source_paths = selected_collection_files params - - new_coll = Arv::Collection.new - Collection.where(uuid: uuids.uniq).with_count("none"). - select([:uuid, :manifest_text]).each do |coll| - src_coll = Arv::Collection.new(coll.manifest_text) - src_pathlist = source_paths[coll.uuid] - if src_pathlist.any?(&:blank?) - src_pathlist = src_coll.each_file_path - destdir = nil - else - destdir = "." - end - src_pathlist.each do |src_path| - src_path = src_path.sub(/^(\.\/|\/|)/, "./") - src_stream, _, basename = src_path.rpartition("/") - dst_stream = destdir || src_stream - # Generate a unique name by adding (1), (2), etc. to it. - # If the filename has a dot that's not at the beginning, insert the - # number just before that. Otherwise, append the number to the name. - if match = basename.match(/[^\.]\./) - suffix_start = match.begin(0) + 1 - else - suffix_start = basename.size - end - suffix_size = 0 - dst_path = nil - loop.each_with_index do |_, try_count| - dst_path = "#{dst_stream}/#{basename}" - break unless new_coll.exist?(dst_path) - uniq_suffix = "(#{try_count + 1})" - basename[suffix_start, suffix_size] = uniq_suffix - suffix_size = uniq_suffix.size - end - new_coll.cp_r(src_path, dst_path, src_coll) - end - end - - coll_attrs = { - manifest_text: new_coll.manifest_text, - name: "Collection created at #{Time.now.localtime}", - } - flash = {} - - # set owner_uuid to current project, provided it is writable - action_data = Oj.safe_load(params['action_data'] || "{}") - if action_data['current_project_uuid'] and - current_project = Group.find?(action_data['current_project_uuid']) and - current_project.writable_by.andand.include?(current_user.uuid) - coll_attrs[:owner_uuid] = current_project.uuid - flash[:message] = - "Created new collection in the project #{current_project.name}." - else - flash[:message] = "Created new collection in your Home project." - end - - newc = Collection.create!(coll_attrs) - source_paths.each_key do |src_uuid| - unless Link.create({ - tail_uuid: src_uuid, - head_uuid: newc.uuid, - link_class: "provenance", - name: "provided", - }) - flash[:error] = " -An error occurred when saving provenance information for this collection. -You can try recreating the collection to get a copy with full provenance data." - break - end - end - redirect_to(newc, flash: flash) - end - - def report_issue_popup - respond_to do |format| - format.js - format.html - end - end - - def report_issue - logger.warn "report_issue: #{params.inspect}" - - respond_to do |format| - IssueReporter.send_report(current_user, params).deliver - format.js {render body: nil} - end - end - - # star / unstar the current project - def star - links = Link.where(owner_uuid: current_user.uuid, - head_uuid: @object.uuid, - link_class: 'star') - - if params['status'] == 'create' - # create 'star' link if one does not already exist - if !links.andand.any? - dst = Link.new(owner_uuid: current_user.uuid, - tail_uuid: current_user.uuid, - head_uuid: @object.uuid, - link_class: 'star', - name: @object.uuid) - dst.save! - end - else # delete any existing 'star' links - if links.andand.any? - links.each do |link| - link.destroy - end - end - end - - respond_to do |format| - format.js - end - end - - protected - - def derive_unique_filename filename, manifest_files - filename_parts = filename.split('.') - filename_part = filename_parts[0] - counter = 1 - while true - return filename if !manifest_files.include? filename - filename_parts[0] = filename_part + "(" + counter.to_s + ")" - filename = filename_parts.join('.') - counter += 1 - end - end - -end diff --git a/apps/workbench/app/controllers/api_client_authorizations_controller.rb b/apps/workbench/app/controllers/api_client_authorizations_controller.rb deleted file mode 100644 index c7ff560773..0000000000 --- a/apps/workbench/app/controllers/api_client_authorizations_controller.rb +++ /dev/null @@ -1,11 +0,0 @@ -# Copyright (C) The Arvados Authors. All rights reserved. -# -# SPDX-License-Identifier: AGPL-3.0 - -class ApiClientAuthorizationsController < ApplicationController - - def index_pane_list - %w(Recent Help) - end - -end diff --git a/apps/workbench/app/controllers/application_controller.rb b/apps/workbench/app/controllers/application_controller.rb deleted file mode 100644 index c2636bf5d7..0000000000 --- a/apps/workbench/app/controllers/application_controller.rb +++ /dev/null @@ -1,1341 +0,0 @@ -# Copyright (C) The Arvados Authors. All rights reserved. -# -# SPDX-License-Identifier: AGPL-3.0 - -class ApplicationController < ActionController::Base - include ArvadosApiClientHelper - include ApplicationHelper - - respond_to :html, :json, :js - protect_from_forgery - - ERROR_ACTIONS = [:render_error, :render_not_found] - - around_action :thread_clear - around_action :set_current_request_id - around_action :set_thread_api_token - # Methods that don't require login should - # skip_around_action :require_thread_api_token - around_action :require_thread_api_token, except: ERROR_ACTIONS - before_action :ensure_arvados_api_exists, only: [:index, :show] - before_action :set_cache_buster - before_action :accept_uuid_as_id_param, except: ERROR_ACTIONS - before_action :check_user_agreements, except: ERROR_ACTIONS - before_action :check_user_profile, except: ERROR_ACTIONS - before_action :load_filters_and_paging_params, except: ERROR_ACTIONS - before_action :find_object_by_uuid, except: [:create, :index, :choose] + ERROR_ACTIONS - theme :select_theme - - begin - rescue_from(ActiveRecord::RecordNotFound, - ActionController::RoutingError, - AbstractController::ActionNotFound, - with: :render_not_found) - rescue_from(Exception, - ActionController::UrlGenerationError, - with: :render_exception) - end - - def set_cache_buster - response.headers["Cache-Control"] = "no-cache, no-store, max-age=0, must-revalidate" - response.headers["Pragma"] = "no-cache" - response.headers["Expires"] = "Fri, 01 Jan 1990 00:00:00 GMT" - end - - def unprocessable(message=nil) - @errors ||= [] - - @errors << message if message - render_error status: 422 - end - - def render_error(opts={}) - # Helpers can rely on the presence of @errors to know they're - # being used in an error page. - @errors ||= [] - opts[:status] ||= 500 - respond_to do |f| - # json must come before html here, so it gets used as the - # default format when js is requested by the client. This lets - # ajax:error callback parse the response correctly, even though - # the browser can't. - f.json { render opts.merge(json: {success: false, errors: @errors}) } - f.html { render({action: 'error'}.merge(opts)) } - f.all { render({action: 'error', formats: 'text'}.merge(opts)) } - end - end - - def render_exception(e) - logger.error e.inspect - logger.error e.backtrace.collect { |x| x + "\n" }.join('') if e.backtrace - err_opts = {status: 422} - if e.is_a?(ArvadosApiClient::ApiError) - err_opts.merge!(action: 'api_error', locals: {api_error: e}) - @errors = e.api_response[:errors] - elsif @object.andand.errors.andand.full_messages.andand.any? - @errors = @object.errors.full_messages - else - @errors = [e.to_s] - end - # Make user information available on the error page, falling back to the - # session cache if the API server is unavailable. - begin - load_api_token(session[:arvados_api_token]) - rescue ArvadosApiClient::ApiError - unless session[:user].nil? - begin - Thread.current[:user] = User.new(session[:user]) - rescue ArvadosApiClient::ApiError - # This can happen if User's columns are unavailable. Nothing to do. - end - end - end - # Preload projects trees for the template. If that's not doable, set empty - # trees so error page rendering can proceed. (It's easier to rescue the - # exception here than in a template.) - unless current_user.nil? - begin - my_starred_projects current_user, 'project' - build_my_wanted_projects_tree current_user - rescue ArvadosApiClient::ApiError - # Fall back to the default-setting code later. - end - end - @starred_projects ||= [] - @my_wanted_projects_tree ||= [] - render_error(err_opts) - end - - def render_not_found(e=ActionController::RoutingError.new("Path not found")) - logger.error e.inspect - @errors = ["Path not found"] - set_thread_api_token do - self.render_error(action: '404', status: 404) - end - end - - # params[:order]: - # - # The order can be left empty to allow it to default. - # Or it can be a comma separated list of real database column names, one per model. - # Column names should always be qualified by a table name and a direction is optional, defaulting to asc - # (e.g. "collections.name" or "collections.name desc"). - # If a column name is specified, that table will be sorted by that column. - # If there are objects from different models that will be shown (such as in Pipelines and processes tab), - # then a sort column name can optionally be specified for each model, passed as an comma-separated list (e.g. "jobs.script, pipeline_instances.name") - # Currently only one sort column name and direction can be specified for each model. - def load_filters_and_paging_params - if params[:order].blank? - @order = 'created_at desc' - elsif params[:order].is_a? Array - @order = params[:order] - else - begin - @order = JSON.load(params[:order]) - rescue - @order = params[:order].split(',') - end - end - @order = [@order] unless @order.is_a? Array - - @limit ||= 200 - if params[:limit] - @limit = params[:limit].to_i - end - - @offset ||= 0 - if params[:offset] - @offset = params[:offset].to_i - end - - @filters ||= [] - if params[:filters] - filters = params[:filters] - if filters.is_a? String - filters = Oj.safe_load filters - elsif filters.is_a? Array - filters = filters.collect do |filter| - if filter.is_a? String - # Accept filters[]=["foo","=","bar"] - Oj.safe_load filter - else - # Accept filters=[["foo","=","bar"]] - filter - end - end - end - # After this, params[:filters] can be trusted to be an array of arrays: - params[:filters] = filters - @filters += filters - end - end - - def find_objects_for_index - @objects ||= model_class - @objects = @objects.filter(@filters).limit(@limit).offset(@offset).order(@order) - @objects.fetch_multiple_pages(false) - end - - def render_index - respond_to do |f| - f.json { - if params[:partial] - @next_page_href = next_page_href(partial: params[:partial], filters: @filters.to_json) - render json: { - content: render_to_string(partial: "show_#{params[:partial]}", - formats: [:html]), - next_page_href: @next_page_href - } - else - render json: @objects - end - } - f.html { - if params[:tab_pane] - render_pane params[:tab_pane] - else - render - end - } - f.js { render } - end - end - - helper_method :render_pane - def render_pane tab_pane, opts={} - render_opts = { - partial: 'show_' + tab_pane.downcase, - locals: { - comparable: self.respond_to?(:compare), - objects: @objects, - tab_pane: tab_pane - }.merge(opts[:locals] || {}) - } - if opts[:to_string] - render_to_string render_opts - else - render render_opts - end - end - - def ensure_arvados_api_exists - if model_class.is_a?(Class) && model_class < ArvadosBase && !model_class.api_exists?(params['action'].to_sym) - @errors = ["#{params['action']} method is not supported for #{params['controller']}"] - return render_error(status: 404) - end - end - - def index - @objects = nil if !defined?(@objects) - find_objects_for_index if !@objects - render_index - end - - helper_method :next_page_offset - def next_page_offset objects=nil - if !objects - objects = @objects - end - if objects.respond_to?(:result_offset) and - objects.respond_to?(:result_limit) - next_offset = objects.result_offset + objects.result_limit - if objects.respond_to?(:items_available) and (objects.items_available != nil) and (next_offset < objects.items_available) - next_offset - elsif @objects.results.size > 0 and (params[:count] == 'none' or - (params[:controller] == 'search' and params[:action] == 'choose')) - last_object_class = @objects.last.class - if params['last_object_class'].nil? or params['last_object_class'] == last_object_class.to_s - next_offset - else - @objects.select{|obj| obj.class == last_object_class}.size - end - else - nil - end - end - end - - helper_method :next_page_href - def next_page_href with_params={} - if next_page_offset - url_for with_params.merge(offset: next_page_offset) - end - end - - helper_method :next_page_filters - def next_page_filters nextpage_operator - next_page_filters = @filters.reject do |attr, op, val| - (attr == 'created_at' and op == nextpage_operator) or - (attr == 'uuid' and op == 'not in') - end - - if @objects.any? - last_created_at = @objects.last.created_at - - last_uuids = [] - @objects.each do |obj| - last_uuids << obj.uuid if obj.created_at.eql?(last_created_at) - end - - next_page_filters += [['created_at', nextpage_operator, last_created_at]] - next_page_filters += [['uuid', 'not in', last_uuids]] - end - - next_page_filters - end - - def show - if !@object - return render_not_found("object not found") - end - respond_to do |f| - f.json do - extra_attrs = { href: url_for(action: :show, id: @object) } - @object.textile_attributes.each do |textile_attr| - extra_attrs.merge!({ "#{textile_attr}Textile" => view_context.render_markup(@object.attributes[textile_attr]) }) - end - render json: @object.attributes.merge(extra_attrs) - end - f.html { - if params['tab_pane'] - render_pane(if params['tab_pane'].is_a? Hash then params['tab_pane']["name"] else params['tab_pane'] end) - elsif request.request_method.in? ['GET', 'HEAD'] - render - else - redirect_to (params[:return_to] || - polymorphic_url(@object, - anchor: params[:redirect_to_anchor])) - end - } - f.js { render } - end - end - - def redirect_to uri, *args - if request.xhr? - if not uri.is_a? String - uri = polymorphic_url(uri) - end - render json: {href: uri} - else - super - end - end - - def choose - @objects = nil if !defined?(@objects) - params[:limit] ||= 40 - respond_to do |f| - if params[:partial] - f.json { - find_objects_for_index if !@objects - render json: { - content: render_to_string(partial: "choose_rows.html", - formats: [:html]), - next_page_href: next_page_href(partial: params[:partial]) - } - } - end - f.js { - find_objects_for_index if !@objects - render partial: 'choose', locals: {multiple: params[:multiple]} - } - end - end - - def render_content - if !@object - return render_not_found("object not found") - end - end - - def new - @object = model_class.new - end - - def update - @updates ||= params[@object.resource_param_name.to_sym] - if @updates.is_a? ActionController::Parameters - @updates = @updates.to_unsafe_hash - end - @updates.keys.each do |attr| - if @object.send(attr).is_a? Hash - if @updates[attr].is_a? String - @updates[attr] = Oj.safe_load @updates[attr] - end - if params[:merge] || params["merge_#{attr}".to_sym] - # Merge provided Hash with current Hash, instead of - # replacing. - if @updates[attr].is_a? ActionController::Parameters - @updates[attr] = @updates[attr].to_unsafe_hash - end - @updates[attr] = @object.send(attr).with_indifferent_access. - deep_merge(@updates[attr].with_indifferent_access) - end - end - end - if @object.update_attributes @updates - show - else - self.render_error status: 422 - end - end - - def create - @new_resource_attrs ||= params[model_class.to_s.underscore.singularize] - @new_resource_attrs ||= {} - @new_resource_attrs.reject! { |k,v| k.to_s == 'uuid' } - @object ||= model_class.new @new_resource_attrs, params["options"] - - if @object.save - show - else - render_error status: 422 - end - end - - # Clone the given object, merging any attribute values supplied as - # with a create action. - def copy - @new_resource_attrs ||= params[model_class.to_s.underscore.singularize] - @new_resource_attrs ||= {} - @object = @object.dup - @object.update_attributes @new_resource_attrs - if not @new_resource_attrs[:name] and @object.respond_to? :name - if @object.name and @object.name != '' - @object.name = "Copy of #{@object.name}" - else - @object.name = "" - end - end - @object.save! - show - end - - def destroy - if @object.destroy - respond_to do |f| - f.json { render json: @object } - f.html { - if params[:return_to] - redirect_to(params[:return_to]) - else - redirect_back(fallback_location: root_path) - end - } - f.js { render } - end - else - self.render_error status: 422 - end - end - - def current_user - Thread.current[:user] - end - - def model_class - controller_name.classify.constantize - end - - def breadcrumb_page_name - (@breadcrumb_page_name || - (@object.friendly_link_name if @object.respond_to? :friendly_link_name) || - action_name) - end - - def index_pane_list - %w(Recent) - end - - def show_pane_list - %w(Attributes Advanced) - end - - def set_share_links - @user_is_manager = false - @share_links = [] - - if @object.uuid != current_user.andand.uuid - begin - @share_links = Link.permissions_for(@object) - @user_is_manager = true - rescue ArvadosApiClient::AccessForbiddenException, - ArvadosApiClient::NotFoundException - end - end - end - - def share_with - if not params[:uuids].andand.any? - @errors = ["No user/group UUIDs specified to share with."] - return render_error(status: 422) - end - results = {"success" => [], "errors" => []} - params[:uuids].each do |shared_uuid| - begin - Link.create(tail_uuid: shared_uuid, link_class: "permission", - name: "can_read", head_uuid: @object.uuid) - rescue ArvadosApiClient::ApiError => error - error_list = error.api_response.andand[:errors] - if error_list.andand.any? - results["errors"] += error_list.map { |e| "#{shared_uuid}: #{e}" } - else - error_code = error.api_status || "Bad status" - results["errors"] << "#{shared_uuid}: #{error_code} response" - end - else - results["success"] << shared_uuid - end - end - if results["errors"].empty? - results.delete("errors") - status = 200 - else - status = 422 - end - respond_to do |f| - f.json { render(json: results, status: status) } - end - end - - helper_method :is_starred - def is_starred - links = Link.where(tail_uuid: current_user.uuid, - head_uuid: @object.uuid, - link_class: 'star').with_count("none") - - return links.andand.any? - end - - protected - - helper_method :strip_token_from_path - def strip_token_from_path(path) - path.sub(/([\?&;])api_token=[^&;]*[&;]?/, '\1') - end - - def redirect_to_login - if request.xhr? or request.format.json? - @errors = ['You are not logged in. Most likely your session has timed out and you need to log in again.'] - render_error status: 401 - elsif request.method.in? ['GET', 'HEAD'] - redirect_to arvados_api_client.arvados_login_url(return_to: strip_token_from_path(request.url)) - else - flash[:error] = "Either you are not logged in, or your session has timed out. I can't automatically log you in and re-attempt this request." - redirect_back(fallback_location: root_path) - end - false # For convenience to return from callbacks - end - - def using_specific_api_token(api_token, opts={}) - start_values = {} - [:arvados_api_token, :user].each do |key| - start_values[key] = Thread.current[key] - end - if opts.fetch(:load_user, true) - load_api_token(api_token) - else - Thread.current[:arvados_api_token] = api_token - Thread.current[:user] = nil - end - begin - yield - ensure - start_values.each_key { |key| Thread.current[key] = start_values[key] } - end - end - - - def accept_uuid_as_id_param - if params[:id] and params[:id].match(/\D/) - params[:uuid] = params.delete :id - end - end - - def find_object_by_uuid - begin - if not model_class - @object = nil - elsif params[:uuid].nil? or params[:uuid].empty? - @object = nil - elsif not params[:uuid].is_a?(String) - @object = model_class.where(uuid: params[:uuid]).first - elsif (model_class != Link and - resource_class_for_uuid(params[:uuid]) == Link) - @name_link = Link.find(params[:uuid]) - @object = model_class.find(@name_link.head_uuid) - else - @object = model_class.find(params[:uuid]) - load_preloaded_objects [@object] - end - rescue ArvadosApiClient::NotFoundException, ArvadosApiClient::NotLoggedInException, RuntimeError => error - if error.is_a?(RuntimeError) and (error.message !~ /^argument to find\(/) - raise - end - render_not_found(error) - return false - end - end - - def thread_clear - load_api_token(nil) - Rails.cache.delete_matched(/^request_#{Thread.current.object_id}_/) - yield - Rails.cache.delete_matched(/^request_#{Thread.current.object_id}_/) - end - - def set_current_request_id - response.headers['X-Request-Id'] = - Thread.current[:request_id] = - "req-" + Random::DEFAULT.rand(2**128).to_s(36)[0..19] - yield - Thread.current[:request_id] = nil - end - - def append_info_to_payload(payload) - super - payload[:request_id] = response.headers['X-Request-Id'] - end - - # Set up the thread with the given API token and associated user object. - def load_api_token(new_token) - Thread.current[:arvados_api_token] = new_token - if new_token.nil? - Thread.current[:user] = nil - else - Thread.current[:user] = User.current - end - end - - # If there's a valid api_token parameter, set up the session with that - # user's information. Return true if the method redirects the request - # (usually a post-login redirect); false otherwise. - def setup_user_session - return false unless params[:api_token] - Thread.current[:arvados_api_token] = params[:api_token] - begin - user = User.current - rescue ArvadosApiClient::NotLoggedInException - false # We may redirect to login, or not, based on the current action. - else - session[:arvados_api_token] = params[:api_token] - # If we later have trouble contacting the API server, we still want - # to be able to render basic user information in the UI--see - # render_exception above. We store that in the session here. This is - # not intended to be used as a general-purpose cache. See #2891. - session[:user] = { - uuid: user.uuid, - email: user.email, - first_name: user.first_name, - last_name: user.last_name, - is_active: user.is_active, - is_admin: user.is_admin, - prefs: user.prefs - } - - if !request.format.json? and request.method.in? ['GET', 'HEAD'] - # Repeat this request with api_token in the (new) session - # cookie instead of the query string. This prevents API - # tokens from appearing in (and being inadvisedly copied - # and pasted from) browser Location bars. - redirect_to strip_token_from_path(request.fullpath) - true - else - false - end - ensure - Thread.current[:arvados_api_token] = nil - end - end - - # Save the session API token in thread-local storage, and yield. - # This method also takes care of session setup if the request - # provides a valid api_token parameter. - # If a token is unavailable or expired, the block is still run, with - # a nil token. - def set_thread_api_token - if Thread.current[:arvados_api_token] - yield # An API token has already been found - pass it through. - return - elsif setup_user_session - return # A new session was set up and received a response. - end - - begin - load_api_token(session[:arvados_api_token]) - yield - rescue ArvadosApiClient::NotLoggedInException - # If we got this error with a token, it must've expired. - # Retry the request without a token. - unless Thread.current[:arvados_api_token].nil? - load_api_token(nil) - yield - end - ensure - # Remove token in case this Thread is used for anything else. - load_api_token(nil) - end - end - - # Redirect to login/welcome if client provided expired API token (or - # none at all) - def require_thread_api_token - if Thread.current[:arvados_api_token] - yield - elsif session[:arvados_api_token] - # Expired session. Clear it before refreshing login so that, - # if this login procedure fails, we end up showing the "please - # log in" page instead of getting stuck in a redirect loop. - session.delete :arvados_api_token - redirect_to_login - elsif request.xhr? - # If we redirect to the welcome page, the browser will handle - # the 302 by itself and the client code will end up rendering - # the "welcome" page in some content area where it doesn't make - # sense. Instead, we send 401 ("authenticate and try again" or - # "display error", depending on how smart the client side is). - @errors = ['You are not logged in.'] - render_error status: 401 - else - redirect_to welcome_users_path(return_to: request.fullpath) - end - end - - def ensure_current_user_is_admin - if not current_user - @errors = ['Not logged in'] - render_error status: 401 - elsif not current_user.is_admin - @errors = ['Permission denied'] - render_error status: 403 - end - end - - helper_method :unsigned_user_agreements - def unsigned_user_agreements - @signed_ua_uuids ||= UserAgreement.signatures.map &:head_uuid - @unsigned_user_agreements ||= UserAgreement.all.map do |ua| - if not @signed_ua_uuids.index ua.uuid - Collection.find(ua.uuid) - end - end.compact - end - - def check_user_agreements - if current_user && !current_user.is_active - if not current_user.is_invited - return redirect_to inactive_users_path(return_to: request.fullpath) - end - if unsigned_user_agreements.empty? - # No agreements to sign. Perhaps we just need to ask? - current_user.activate - if !current_user.is_active - logger.warn "#{current_user.uuid.inspect}: " + - "No user agreements to sign, but activate failed!" - end - end - if !current_user.is_active - redirect_to user_agreements_path(return_to: request.fullpath) - end - end - true - end - - def check_user_profile - return true if !current_user - if request.method.downcase != 'get' || params[:partial] || - params[:tab_pane] || params[:action_method] || - params[:action] == 'setup_popup' - return true - end - - if missing_required_profile? - redirect_to profile_user_path(current_user.uuid, return_to: request.fullpath) - end - true - end - - helper_method :missing_required_profile? - def missing_required_profile? - missing_required = false - - profile_config = Rails.configuration.Workbench.UserProfileFormFields - if current_user && !profile_config.empty? - current_user_profile = current_user.prefs[:profile] - profile_config.each do |k, entry| - if entry[:Required] - if !current_user_profile || - !current_user_profile[k] || - current_user_profile[k].empty? - missing_required = true - break - end - end - end - end - - missing_required - end - - def select_theme - return Rails.configuration.Workbench.Theme - end - - @@notification_tests = [] - - @@notification_tests.push lambda { |controller, current_user| - return nil if Rails.configuration.Services.WebShell.ExternalURL != URI("") - AuthorizedKey.limit(1).with_count('none').where(authorized_user_uuid: current_user.uuid).each do - return nil - end - return lambda { |view| - view.render partial: 'notifications/ssh_key_notification' - } - } - - @@notification_tests.push lambda { |controller, current_user| - Collection.limit(1).with_count('none').where(created_by: current_user.uuid).each do - return nil - end - return lambda { |view| - view.render partial: 'notifications/collections_notification' - } - } - - @@notification_tests.push lambda { |controller, current_user| - if PipelineInstance.api_exists?(:index) - PipelineInstance.limit(1).with_count('none').where(created_by: current_user.uuid).each do - return nil - end - else - return nil - end - return lambda { |view| - view.render partial: 'notifications/pipelines_notification' - } - } - - helper_method :user_notifications - def user_notifications - @errors = nil if !defined?(@errors) - return [] if @errors or not current_user.andand.is_active or not Rails.configuration.Workbench.ShowUserNotifications - @notifications ||= @@notification_tests.map do |t| - t.call(self, current_user) - end.compact - end - - helper_method :all_projects - def all_projects - @all_projects ||= Group. - filter([['group_class','IN',['project','filter']]]).order('name') - end - - helper_method :my_projects - def my_projects - return @my_projects if @my_projects - @my_projects = [] - root_of = {} - all_projects.each do |g| - root_of[g.uuid] = g.owner_uuid - @my_projects << g - end - done = false - while not done - done = true - root_of = root_of.each_with_object({}) do |(child, parent), h| - if root_of[parent] - h[child] = root_of[parent] - done = false - else - h[child] = parent - end - end - end - @my_projects = @my_projects.select do |g| - root_of[g.uuid] == current_user.uuid - end - end - - helper_method :projects_shared_with_me - def projects_shared_with_me - my_project_uuids = my_projects.collect &:uuid - all_projects.reject { |x| x.uuid.in? my_project_uuids } - end - - helper_method :recent_jobs_and_pipelines - def recent_jobs_and_pipelines - (Job.limit(10) | - PipelineInstance.limit(10).with_count("none")). - sort_by do |x| - (x.finished_at || x.started_at rescue nil) || x.modified_at || x.created_at - end.reverse - end - - helper_method :running_pipelines - def running_pipelines - pi = PipelineInstance.order(["started_at asc", "created_at asc"]).with_count("none").filter([["state", "in", ["RunningOnServer", "RunningOnClient"]]]) - jobs = {} - pi.each do |pl| - pl.components.each do |k,v| - if v.is_a? Hash and v[:job] - jobs[v[:job][:uuid]] = {} - end - end - end - - if jobs.keys.any? - Job.filter([["uuid", "in", jobs.keys]]).with_count("none").each do |j| - jobs[j[:uuid]] = j - end - - pi.each do |pl| - pl.components.each do |k,v| - if v.is_a? Hash and v[:job] - v[:job] = jobs[v[:job][:uuid]] - end - end - end - end - - pi - end - - helper_method :recent_processes - def recent_processes lim - lim = 12 if lim.nil? - - procs = {} - if PipelineInstance.api_exists?(:index) - cols = %w(uuid owner_uuid created_at modified_at pipeline_template_uuid name state started_at finished_at) - pipelines = PipelineInstance.select(cols).limit(lim).order(["created_at desc"]).with_count("none") - pipelines.results.each { |pi| procs[pi] = pi.created_at } - end - - crs = ContainerRequest.limit(lim).with_count("none").order(["created_at desc"]).filter([["requesting_container_uuid", "=", nil]]).select( - ["uuid", "name", "container_uuid", "output_uuid", "state", "created_at", "modified_at"]) - crs.results.each { |c| procs[c] = c.created_at } - - Hash[procs.sort_by {|key, value| value}].keys.reverse.first(lim) - end - - helper_method :recent_collections - def recent_collections lim - c = Collection.limit(lim).with_count("none").order(["modified_at desc"]).results - own = {} - Group.filter([["uuid", "in", c.map(&:owner_uuid)]]).with_count("none").each do |g| - own[g[:uuid]] = g - end - {collections: c, owners: own} - end - - helper_method :my_starred_projects - def my_starred_projects user, group_class - return if defined?(@starred_projects) && @starred_projects - links = Link.filter([['owner_uuid', 'in', ["#{Rails.configuration.ClusterID}-j7d0g-publicfavorites", user.uuid]], - ['link_class', '=', 'star'], - ['head_uuid', 'is_a', 'arvados#group']]).with_count("none").select(%w(head_uuid)) - uuids = links.collect { |x| x.head_uuid } - if group_class == "" - starred_projects = Group.filter([['uuid', 'in', uuids]]).order('name').with_count("none") - else - starred_projects = Group.filter([['uuid', 'in', uuids],['group_class', '=', group_class]]).order('name').with_count("none") - end - @starred_projects = starred_projects.results - end - - # If there are more than 200 projects that are readable by the user, - # build the tree using only the top 200+ projects owned by the user, - # from the top three levels. - # That is: get toplevel projects under home, get subprojects of - # these projects, and so on until we hit the limit. - def my_wanted_projects(user, page_size=100) - return @my_wanted_projects if defined?(@my_wanted_projects) && @my_wanted_projects - - from_top = [] - uuids = [user.uuid] - depth = 0 - @too_many_projects = false - @reached_level_limit = false - while from_top.size <= page_size*2 - current_level = Group.filter([['group_class','IN',['project','filter']], - ['owner_uuid', 'in', uuids]]) - .order('name').limit(page_size*2) - break if current_level.results.size == 0 - @too_many_projects = true if current_level.items_available > current_level.results.size - from_top.concat current_level.results - uuids = current_level.results.collect(&:uuid) - depth += 1 - if depth >= 3 - @reached_level_limit = true - break - end - end - @my_wanted_projects = from_top - end - - helper_method :my_wanted_projects_tree - def my_wanted_projects_tree(user, page_size=100) - build_my_wanted_projects_tree(user, page_size) - [@my_wanted_projects_tree, @too_many_projects, @reached_level_limit] - end - - def build_my_wanted_projects_tree(user, page_size=100) - return @my_wanted_projects_tree if defined?(@my_wanted_projects_tree) && @my_wanted_projects_tree - - parent_of = {user.uuid => 'me'} - my_wanted_projects(user, page_size).each do |ob| - parent_of[ob.uuid] = ob.owner_uuid - end - children_of = {false => [], 'me' => [user]} - my_wanted_projects(user, page_size).each do |ob| - if ob.owner_uuid != user.uuid and - not parent_of.has_key? ob.owner_uuid - parent_of[ob.uuid] = false - end - children_of[parent_of[ob.uuid]] ||= [] - children_of[parent_of[ob.uuid]] << ob - end - buildtree = lambda do |chldrn_of, root_uuid=false| - tree = {} - chldrn_of[root_uuid].andand.each do |ob| - tree[ob] = buildtree.call(chldrn_of, ob.uuid) - end - tree - end - sorted_paths = lambda do |tree, depth=0| - paths = [] - tree.keys.sort_by { |ob| - ob.is_a?(String) ? ob : ob.friendly_link_name - }.each do |ob| - paths << {object: ob, depth: depth} - paths += sorted_paths.call tree[ob], depth+1 - end - paths - end - @my_wanted_projects_tree = - sorted_paths.call buildtree.call(children_of, 'me') - end - - helper_method :get_object - def get_object uuid - if @get_object.nil? and @objects - @get_object = @objects.each_with_object({}) do |object, h| - h[object.uuid] = object - end - end - @get_object ||= {} - @get_object[uuid] - end - - helper_method :project_breadcrumbs - def project_breadcrumbs - crumbs = [] - current = @name_link || @object - while current - # Halt if a group ownership loop is detected. API should refuse - # to produce this state, but it could still arise from a race - # condition when group ownership changes between our find() - # queries. - break if crumbs.collect(&:uuid).include? current.uuid - - if current.is_a?(Group) and current.group_class == 'project' - crumbs.prepend current - end - if current.is_a? Link - current = Group.find?(current.tail_uuid) - else - current = Group.find?(current.owner_uuid) - end - end - crumbs - end - - helper_method :current_project_uuid - def current_project_uuid - if @object.is_a? Group and @object.group_class == 'project' - @object.uuid - elsif @name_link.andand.tail_uuid - @name_link.tail_uuid - elsif @object and resource_class_for_uuid(@object.owner_uuid) == Group - @object.owner_uuid - else - nil - end - end - - # helper method to get links for given object or uuid - helper_method :links_for_object - def links_for_object object_or_uuid - raise ArgumentError, 'No input argument' unless object_or_uuid - preload_links_for_objects([object_or_uuid]) - uuid = object_or_uuid.is_a?(String) ? object_or_uuid : object_or_uuid.uuid - @all_links_for[uuid] ||= [] - end - - # helper method to preload links for given objects and uuids - helper_method :preload_links_for_objects - def preload_links_for_objects objects_and_uuids - @all_links_for ||= {} - - raise ArgumentError, 'Argument is not an array' unless objects_and_uuids.is_a? Array - return @all_links_for if objects_and_uuids.empty? - - uuids = objects_and_uuids.collect { |x| x.is_a?(String) ? x : x.uuid } - - # if already preloaded for all of these uuids, return - if not uuids.select { |x| @all_links_for[x].nil? }.any? - return @all_links_for - end - - uuids.each do |x| - @all_links_for[x] = [] - end - - # TODO: make sure we get every page of results from API server - Link.filter([['head_uuid', 'in', uuids]]).with_count("none").each do |link| - @all_links_for[link.head_uuid] << link - end - @all_links_for - end - - # helper method to get a certain number of objects of a specific type - # this can be used to replace any uses of: "dataclass.limit(n)" - helper_method :get_n_objects_of_class - def get_n_objects_of_class dataclass, size - @objects_map_for ||= {} - - raise ArgumentError, 'Argument is not a data class' unless dataclass.is_a? Class and dataclass < ArvadosBase - raise ArgumentError, 'Argument is not a valid limit size' unless (size && size>0) - - # if the objects_map_for has a value for this dataclass, and the - # size used to retrieve those objects is equal, return it - size_key = "#{dataclass.name}_size" - if @objects_map_for[dataclass.name] && @objects_map_for[size_key] && - (@objects_map_for[size_key] == size) - return @objects_map_for[dataclass.name] - end - - @objects_map_for[size_key] = size - @objects_map_for[dataclass.name] = dataclass.limit(size) - end - - # helper method to get collections for the given uuid - helper_method :collections_for_object - def collections_for_object uuid - raise ArgumentError, 'No input argument' unless uuid - preload_collections_for_objects([uuid]) - @all_collections_for[uuid] ||= [] - end - - # helper method to preload collections for the given uuids - helper_method :preload_collections_for_objects - def preload_collections_for_objects uuids - @all_collections_for ||= {} - - raise ArgumentError, 'Argument is not an array' unless uuids.is_a? Array - return @all_collections_for if uuids.empty? - - # if already preloaded for all of these uuids, return - if not uuids.select { |x| @all_collections_for[x].nil? }.any? - return @all_collections_for - end - - uuids.each do |x| - @all_collections_for[x] = [] - end - - # TODO: make sure we get every page of results from API server - Collection.where(uuid: uuids).with_count("none").each do |collection| - @all_collections_for[collection.uuid] << collection - end - @all_collections_for - end - - # helper method to get log collections for the given log - helper_method :log_collections_for_object - def log_collections_for_object log - raise ArgumentError, 'No input argument' unless log - - preload_log_collections_for_objects([log]) - - uuid = log - fixup = /([a-f0-9]{32}\+\d+)(\+?.*)/.match(log) - if fixup && fixup.size>1 - uuid = fixup[1] - end - - @all_log_collections_for[uuid] ||= [] - end - - # helper method to preload collections for the given uuids - helper_method :preload_log_collections_for_objects - def preload_log_collections_for_objects logs - @all_log_collections_for ||= {} - - raise ArgumentError, 'Argument is not an array' unless logs.is_a? Array - return @all_log_collections_for if logs.empty? - - uuids = [] - logs.each do |log| - fixup = /([a-f0-9]{32}\+\d+)(\+?.*)/.match(log) - if fixup && fixup.size>1 - uuids << fixup[1] - else - uuids << log - end - end - - # if already preloaded for all of these uuids, return - if not uuids.select { |x| @all_log_collections_for[x].nil? }.any? - return @all_log_collections_for - end - - uuids.each do |x| - @all_log_collections_for[x] = [] - end - - # TODO: make sure we get every page of results from API server - Collection.where(uuid: uuids).with_count("none").each do |collection| - @all_log_collections_for[collection.uuid] << collection - end - @all_log_collections_for - end - - # Helper method to get one collection for the given portable_data_hash - # This is used to determine if a pdh is readable by the current_user - helper_method :collection_for_pdh - def collection_for_pdh pdh - raise ArgumentError, 'No input argument' unless pdh - preload_for_pdhs([pdh]) - @all_pdhs_for[pdh] ||= [] - end - - # Helper method to preload one collection each for the given pdhs - # This is used to determine if a pdh is readable by the current_user - helper_method :preload_for_pdhs - def preload_for_pdhs pdhs - @all_pdhs_for ||= {} - - raise ArgumentError, 'Argument is not an array' unless pdhs.is_a? Array - return @all_pdhs_for if pdhs.empty? - - # if already preloaded for all of these pdhs, return - if not pdhs.select { |x| @all_pdhs_for[x].nil? }.any? - return @all_pdhs_for - end - - pdhs.each do |x| - @all_pdhs_for[x] = [] - end - - Collection.select(%w(portable_data_hash)).where(portable_data_hash: pdhs).distinct().with_count("none").each do |collection| - @all_pdhs_for[collection.portable_data_hash] << collection - end - @all_pdhs_for - end - - # helper method to get object of a given dataclass and uuid - helper_method :object_for_dataclass - def object_for_dataclass dataclass, uuid, by_attr=nil - raise ArgumentError, 'No input argument dataclass' unless (dataclass && uuid) - preload_objects_for_dataclass(dataclass, [uuid], by_attr) - @objects_for[uuid] - end - - # helper method to preload objects for given dataclass and uuids - helper_method :preload_objects_for_dataclass - def preload_objects_for_dataclass dataclass, uuids, by_attr=nil, select_fields=nil - @objects_for ||= {} - - raise ArgumentError, 'Argument is not a data class' unless dataclass.is_a? Class - raise ArgumentError, 'Argument is not an array' unless uuids.is_a? Array - - return @objects_for if uuids.empty? - - # if already preloaded for all of these uuids, return - if not uuids.select { |x| !@objects_for.include?(x) }.any? - return @objects_for - end - - # preset all uuids to nil - uuids.each do |x| - @objects_for[x] = nil - end - if by_attr and ![:uuid, :name].include?(by_attr) - raise ArgumentError, "Preloading only using lookups by uuid or name are supported: #{by_attr}" - elsif by_attr and by_attr == :name - dataclass.where(name: uuids).each do |obj| - @objects_for[obj.name] = obj - end - else - key_prefix = "request_#{Thread.current.object_id}_#{dataclass.to_s}_" - dataclass.where(uuid: uuids).select(select_fields).each do |obj| - @objects_for[obj.uuid] = obj - if dataclass == Collection - # The collecions#index defaults to "all attributes except manifest_text" - # Hence, this object is not suitable for preloading the find() cache. - else - Rails.cache.write(key_prefix + obj.uuid, obj.as_json) - end - end - end - @objects_for - end - - # helper method to load objects that are already preloaded - helper_method :load_preloaded_objects - def load_preloaded_objects objs - @objects_for ||= {} - objs.each do |obj| - @objects_for[obj.uuid] = obj - end - end - - # helper method to get the names of collection files selected - helper_method :selected_collection_files - def selected_collection_files params - link_uuids, coll_ids = params["selection"].partition do |sel_s| - ArvadosBase::resource_class_for_uuid(sel_s) == Link - end - - unless link_uuids.empty? - Link.select([:head_uuid]).where(uuid: link_uuids).with_count("none").each do |link| - if ArvadosBase::resource_class_for_uuid(link.head_uuid) == Collection - coll_ids << link.head_uuid - end - end - end - - uuids = [] - pdhs = [] - source_paths = Hash.new { |hash, key| hash[key] = [] } - coll_ids.each do |coll_id| - if m = CollectionsHelper.match(coll_id) - key = m[1] + m[2] - pdhs << key - source_paths[key] << m[4] - elsif m = CollectionsHelper.match_uuid_with_optional_filepath(coll_id) - key = m[1] - uuids << key - source_paths[key] << m[4] - end - end - - unless pdhs.empty? - Collection.where(portable_data_hash: pdhs.uniq).with_count("none"). - select([:uuid, :portable_data_hash]).each do |coll| - unless source_paths[coll.portable_data_hash].empty? - uuids << coll.uuid - source_paths[coll.uuid] = source_paths.delete(coll.portable_data_hash) - end - end - end - - [uuids, source_paths] - end - - def wiselinks_layout - 'body' - end -end diff --git a/apps/workbench/app/controllers/authorized_keys_controller.rb b/apps/workbench/app/controllers/authorized_keys_controller.rb deleted file mode 100644 index ac47ce7592..0000000000 --- a/apps/workbench/app/controllers/authorized_keys_controller.rb +++ /dev/null @@ -1,21 +0,0 @@ -# Copyright (C) The Arvados Authors. All rights reserved. -# -# SPDX-License-Identifier: AGPL-3.0 - -class AuthorizedKeysController < ApplicationController - def index_pane_list - %w(Recent Help) - end - - def new - super - @object.authorized_user_uuid = current_user.uuid if current_user - @object.key_type = 'SSH' - end - - def create - defaults = { authorized_user_uuid: current_user.uuid, key_type: 'SSH' } - @object = AuthorizedKey.new defaults.merge(params[:authorized_key] || {}) - super - end -end diff --git a/apps/workbench/app/controllers/collections_controller.rb b/apps/workbench/app/controllers/collections_controller.rb deleted file mode 100644 index 9073d06c17..0000000000 --- a/apps/workbench/app/controllers/collections_controller.rb +++ /dev/null @@ -1,394 +0,0 @@ -# Copyright (C) The Arvados Authors. All rights reserved. -# -# SPDX-License-Identifier: AGPL-3.0 - -require "arvados/keep" -require "arvados/collection" -require "uri" - -class CollectionsController < ApplicationController - include ActionController::Live - - skip_around_action :require_thread_api_token, if: proc { |ctrl| - !Rails.configuration.Users.AnonymousUserToken.empty? and - 'show' == ctrl.action_name - } - skip_around_action(:require_thread_api_token, - only: [:show_file, :show_file_links]) - skip_before_action(:find_object_by_uuid, - only: [:provenance, :show_file, :show_file_links]) - # We depend on show_file to display the user agreement: - skip_before_action :check_user_agreements, only: :show_file - skip_before_action :check_user_profile, only: :show_file - - RELATION_LIMIT = 5 - - def show_pane_list - panes = %w(Files Upload Tags Provenance_graph Used_by Advanced) - panes = panes - %w(Upload) unless (@object.editable? rescue false) - panes - end - - def set_persistent - case params[:value] - when 'persistent', 'cache' - persist_links = Link.filter([['owner_uuid', '=', current_user.uuid], - ['link_class', '=', 'resources'], - ['name', '=', 'wants'], - ['tail_uuid', '=', current_user.uuid], - ['head_uuid', '=', @object.uuid]]).with_count("none") - logger.debug persist_links.inspect - else - return unprocessable "Invalid value #{value.inspect}" - end - if params[:value] == 'persistent' - if not persist_links.any? - Link.create(link_class: 'resources', - name: 'wants', - tail_uuid: current_user.uuid, - head_uuid: @object.uuid) - end - else - persist_links.each do |link| - link.destroy || raise - end - end - - respond_to do |f| - f.json { render json: @object } - end - end - - def index - # API server index doesn't return manifest_text by default, but our - # callers want it unless otherwise specified. - @select ||= Collection.columns.map(&:name) - base_search = Collection.select(@select) - if params[:search].andand.length.andand > 0 - tags = Link.where(any: ['contains', params[:search]]).with_count("none") - @objects = (base_search.where(uuid: tags.collect(&:head_uuid)) | - base_search.where(any: ['contains', params[:search]])). - uniq { |c| c.uuid } - else - if params[:limit] - limit = params[:limit].to_i - else - limit = 100 - end - - if params[:offset] - offset = params[:offset].to_i - else - offset = 0 - end - - @objects = base_search.limit(limit).offset(offset) - end - @links = Link.where(head_uuid: @objects.collect(&:uuid)).with_count("none") - @collection_info = {} - @objects.each do |c| - @collection_info[c.uuid] = { - tag_links: [], - wanted: false, - wanted_by_me: false, - provenance: [], - links: [] - } - end - @links.each do |link| - @collection_info[link.head_uuid] ||= {} - info = @collection_info[link.head_uuid] - case link.link_class - when 'tag' - info[:tag_links] << link - when 'resources' - info[:wanted] = true - info[:wanted_by_me] ||= link.tail_uuid == current_user.uuid - when 'provenance' - info[:provenance] << link.name - end - info[:links] << link - end - @request_url = request.url - - render_index - end - - def show_file_links - return show_file - end - - def show_file - # The order of searched tokens is important: because the anonymous user - # token is passed along with every API request, we have to check it first. - # Otherwise, it's impossible to know whether any other request succeeded - # because of the reader token. - coll = nil - tokens = [(if !Rails.configuration.Users.AnonymousUserToken.empty? then - Rails.configuration.Users.AnonymousUserToken else nil end), - params[:reader_token], - Thread.current[:arvados_api_token]].compact - usable_token = find_usable_token(tokens) do - coll = Collection.find(params[:uuid]) - end - if usable_token.nil? - # Response already rendered. - return - end - - opts = {} - if usable_token == params[:reader_token] - opts[:path_token] = usable_token - elsif usable_token == Rails.configuration.Users.AnonymousUserToken - # Don't pass a token at all - else - # We pass the current user's real token only if it's necessary - # to read the collection. - opts[:query_token] = usable_token - end - opts[:disposition] = params[:disposition] if params[:disposition] - return redirect_to keep_web_url(params[:uuid], params[:file], opts) - end - - def sharing_scopes - ["GET /arvados/v1/collections/#{@object.uuid}", "GET /arvados/v1/collections/#{@object.uuid}/", "GET /arvados/v1/keep_services/accessible"] - end - - def search_scopes - begin - ApiClientAuthorization.filter([['scopes', '=', sharing_scopes]]).results - rescue ArvadosApiClient::AccessForbiddenException - nil - end - end - - def find_object_by_uuid - if not Keep::Locator.parse params[:id] - super - end - end - - def show - return super if !@object - - @logs = [] - - if params["tab_pane"] == "Provenance_graph" - @prov_svg = ProvenanceHelper::create_provenance_graph(@object.provenance, "provenance_svg", - {:request => request, - :direction => "RL", - :combine_jobs => :script_only}) rescue nil - end - - if current_user - if Keep::Locator.parse params["uuid"] - @same_pdh = Collection.filter([["portable_data_hash", "=", @object.portable_data_hash]]).limit(20) - if @same_pdh.results.size == 1 - redirect_to collection_path(@same_pdh[0]["uuid"]) - return - end - owners = @same_pdh.map(&:owner_uuid).to_a.uniq - preload_objects_for_dataclass Group, owners - preload_objects_for_dataclass User, owners - uuids = @same_pdh.map(&:uuid).to_a.uniq - preload_links_for_objects uuids - render 'hash_matches' - return - else - if Job.api_exists?(:index) - jobs_with = lambda do |conds| - Job.limit(RELATION_LIMIT).with_count("none").where(conds) - .results.sort_by { |j| j.finished_at || j.created_at } - end - @output_of = jobs_with.call(output: @object.portable_data_hash) - @log_of = jobs_with.call(log: @object.portable_data_hash) - end - - @project_links = Link.limit(RELATION_LIMIT).with_count("none").order("modified_at DESC") - .where(head_uuid: @object.uuid, link_class: 'name').results - project_hash = Group.where(uuid: @project_links.map(&:tail_uuid)).with_count("none").to_hash - @projects = project_hash.values - - @permissions = Link.limit(RELATION_LIMIT).with_count("none").order("modified_at DESC") - .where(head_uuid: @object.uuid, link_class: 'permission', - name: 'can_read').results - @search_sharing = search_scopes - - if params["tab_pane"] == "Used_by" - @used_by_svg = ProvenanceHelper::create_provenance_graph(@object.used_by, "used_by_svg", - {:request => request, - :direction => "LR", - :combine_jobs => :script_only, - :pdata_only => true}) rescue nil - end - end - end - super - end - - def sharing_popup - @search_sharing = search_scopes - render("sharing_popup.js", content_type: "text/javascript") - end - - helper_method :download_link - - def download_link - token = @search_sharing.first.api_token - keep_web_url(@object.uuid, nil, {path_token: token}) - end - - def share - ApiClientAuthorization.create(scopes: sharing_scopes) - sharing_popup - end - - def unshare - search_scopes.each do |s| - s.destroy - end - sharing_popup - end - - def remove_selected_files - uuids, source_paths = selected_collection_files params - - arv_coll = Arv::Collection.new(@object.manifest_text) - source_paths[uuids[0]].each do |p| - arv_coll.rm "."+p - end - - if @object.update_attributes manifest_text: arv_coll.manifest_text - show - else - self.render_error status: 422 - end - end - - def update - updated_attr = params[:collection].to_unsafe_hash.each.select {|a| a[0].andand.start_with? 'rename-file-path:'} - - if updated_attr.size > 0 - # Is it file rename? - file_path = updated_attr[0][0].split('rename-file-path:')[-1] - - new_file_path = updated_attr[0][1] - if new_file_path.start_with?('./') - # looks good - elsif new_file_path.start_with?('/') - new_file_path = '.' + new_file_path - else - new_file_path = './' + new_file_path - end - - arv_coll = Arv::Collection.new(@object.manifest_text) - - if arv_coll.exist?(new_file_path) - @errors = 'Duplicate file path. Please use a different name.' - self.render_error status: 422 - else - arv_coll.rename "./"+file_path, new_file_path - - if @object.update_attributes manifest_text: arv_coll.manifest_text - show - else - self.render_error status: 422 - end - end - else - # Not a file rename; use default - super - end - end - - protected - - def find_usable_token(token_list) - # Iterate over every given token to make it the current token and - # yield the given block. - # If the block succeeds, return the token it used. - # Otherwise, render an error response based on the most specific - # error we encounter, and return nil. - most_specific_error = [401] - token_list.each do |api_token| - begin - # We can't load the corresponding user, because the token may not - # be scoped for that. - using_specific_api_token(api_token, load_user: false) do - yield - return api_token - end - rescue ArvadosApiClient::ApiError => error - if error.api_status >= most_specific_error.first - most_specific_error = [error.api_status, error] - end - end - end - case most_specific_error.shift - when 401, 403 - redirect_to_login - when 404 - render_not_found(*most_specific_error) - end - return nil - end - - def keep_web_url(uuid_or_pdh, file, opts) - munged_id = uuid_or_pdh.sub('+', '-') - - tmpl = Rails.configuration.Services.WebDAV.ExternalURL.to_s - - if Rails.configuration.Services.WebDAVDownload.ExternalURL != URI("") and - (tmpl.empty? or opts[:disposition] == 'attachment') - # Prefer the attachment-only-host when we want an attachment - # (and when there is no preview link configured) - tmpl = Rails.configuration.Services.WebDAVDownload.ExternalURL.to_s - elsif not Rails.configuration.Collections.TrustAllContent - check_uri = URI.parse(tmpl.sub("*", munged_id)) - if opts[:query_token] and - (check_uri.host.nil? or ( - not check_uri.host.start_with?(munged_id + "--") and - not check_uri.host.start_with?(munged_id + "."))) - # We're about to pass a token in the query string, but - # keep-web can't accept that safely at a single-origin URL - # template (unless it's -attachment-only-host). - tmpl = Rails.configuration.Services.WebDAVDownload.ExternalURL.to_s - if tmpl.empty? - raise ArgumentError, "Download precluded by site configuration" - end - logger.warn("Using download link, even though inline content " \ - "was requested: #{check_uri.to_s}") - end - end - - if tmpl == Rails.configuration.Services.WebDAVDownload.ExternalURL.to_s - # This takes us to keep-web's -attachment-only-host so there is - # no need to add ?disposition=attachment. - opts.delete :disposition - end - - uri = URI.parse(tmpl.sub("*", munged_id)) - if tmpl.index("*").nil? - uri.path = "/c=#{munged_id}" - end - uri.path += '/' unless uri.path.end_with? '/' - if opts[:path_token] - uri.path += 't=' + opts[:path_token] + '/' - end - uri.path += '_/' - uri.path += URI.escape(file) if file - - query = Hash[URI.decode_www_form(uri.query || '')] - { query_token: 'api_token', - disposition: 'disposition' }.each do |opt, param| - if opts.include? opt - query[param] = opts[opt] - end - end - unless query.empty? - uri.query = URI.encode_www_form(query) - end - - uri.to_s - end -end diff --git a/apps/workbench/app/controllers/container_requests_controller.rb b/apps/workbench/app/controllers/container_requests_controller.rb deleted file mode 100644 index be463b022c..0000000000 --- a/apps/workbench/app/controllers/container_requests_controller.rb +++ /dev/null @@ -1,219 +0,0 @@ -# Copyright (C) The Arvados Authors. All rights reserved. -# -# SPDX-License-Identifier: AGPL-3.0 - -class ContainerRequestsController < ApplicationController - skip_around_action :require_thread_api_token, if: proc { |ctrl| - !Rails.configuration.Users.AnonymousUserToken.empty? and - 'show' == ctrl.action_name - } - - def generate_provenance(cr) - return if params['tab_pane'] != "Provenance" - - nodes = {} - child_crs = [] - col_uuids = [] - col_pdhs = [] - col_uuids << cr[:output_uuid] if cr[:output_uuid] - col_pdhs += ProvenanceHelper::cr_input_pdhs(cr) - - # Search for child CRs - if cr[:container_uuid] - child_crs = ContainerRequest.where(requesting_container_uuid: cr[:container_uuid]).with_count("none") - - child_crs.each do |child| - nodes[child[:uuid]] = child - col_uuids << child[:output_uuid] if child[:output_uuid] - col_pdhs += ProvenanceHelper::cr_input_pdhs(child) - end - end - - if nodes.length == 0 - nodes[cr[:uuid]] = cr - end - - pdh_to_col = {} # Indexed by PDH - output_pdhs = [] - - # Batch requests to get all related collections - # First fetch output collections by UUID. - Collection.filter([['uuid', 'in', col_uuids.uniq]]).with_count("none").each do |c| - output_pdhs << c[:portable_data_hash] - pdh_to_col[c[:portable_data_hash]] = c - nodes[c[:uuid]] = c - end - # Next, get input collections by PDH. - Collection.filter( - [['portable_data_hash', 'in', col_pdhs - output_pdhs]]).with_count("none").each do |c| - nodes[c[:portable_data_hash]] = c - end - - @svg = ProvenanceHelper::create_provenance_graph( - nodes, "provenance_svg", - { - :request => request, - :pdh_to_uuid => pdh_to_col, - } - ) - end - - def show_pane_list - panes = %w(Status Log Provenance Advanced) - if @object.andand.state == 'Uncommitted' - panes = %w(Inputs) + panes - %w(Log Provenance) - end - panes - end - - def show - generate_provenance(@object) - super - end - - def cancel - if @object.container_uuid - c = Container.select(['state']).where(uuid: @object.container_uuid).with_count("none").first - if c && c.state != 'Running' - # If the container hasn't started yet, setting priority=0 - # leaves our request in "Committed" state and doesn't cancel - # the container (even if no other requests are giving it - # priority). To avoid showing this container request as "on - # hold" after hitting the Cancel button, set state=Final too. - @object.state = 'Final' - end - end - @object.update_attributes! priority: 0 - if params[:return_to] - redirect_to params[:return_to] - else - redirect_to @object - end - end - - def update - @updates ||= params[@object.class.to_s.underscore.singularize.to_sym] - input_obj = @updates[:mounts].andand[:"/var/lib/cwl/cwl.input.json"].andand[:content] - if input_obj - workflow = @object.mounts[:"/var/lib/cwl/workflow.json"][:content] - get_cwl_inputs(workflow).each do |input_schema| - if not input_obj.include? cwl_shortname(input_schema[:id]) - next - end - required, primary_type, param_id = cwl_input_info(input_schema) - if input_obj[param_id] == "" - input_obj[param_id] = nil - elsif primary_type == "boolean" - input_obj[param_id] = input_obj[param_id] == "true" - elsif ["int", "long"].include? primary_type - input_obj[param_id] = input_obj[param_id].to_i - elsif ["float", "double"].include? primary_type - input_obj[param_id] = input_obj[param_id].to_f - elsif ["File", "Directory"].include? primary_type - re = CollectionsHelper.match_uuid_with_optional_filepath(input_obj[param_id]) - if re - c = Collection.find(re[1]) - input_obj[param_id] = {"class" => primary_type, - "location" => "keep:#{c.portable_data_hash}#{re[4]}", - "http://arvados.org/cwl#collectionUUID" => re[1]} - end - end - end - end - params[:merge] = true - - if !@updates[:reuse_steps].nil? - if @updates[:reuse_steps] == "false" - @updates[:reuse_steps] = false - end - @updates[:command] ||= @object.command - @updates[:command] -= ["--disable-reuse", "--enable-reuse"] - if @updates[:reuse_steps] - @updates[:command].insert(1, "--enable-reuse") - else - @updates[:command].insert(1, "--disable-reuse") - end - @updates.delete(:reuse_steps) - end - - begin - super - rescue => e - flash[:error] = e.to_s - show - end - end - - def copy - src = @object - - @object = ContainerRequest.new - - # set owner_uuid to that of source, provided it is a project and writable by current user - if params[:work_unit].andand[:owner_uuid] - @object.owner_uuid = src.owner_uuid = params[:work_unit][:owner_uuid] - else - current_project = Group.find(src.owner_uuid) rescue nil - if (current_project && current_project.writable_by.andand.include?(current_user.uuid)) - @object.owner_uuid = src.owner_uuid - end - end - - command = src.command - if command[0] == 'arvados-cwl-runner' - command.each_with_index do |arg, i| - if arg.start_with? "--project-uuid=" - command[i] = "--project-uuid=#{@object.owner_uuid}" - end - end - command -= ["--disable-reuse", "--enable-reuse"] - command.insert(1, '--enable-reuse') - end - - if params[:use_existing] == "false" - params[:use_existing] = false - elsif params[:use_existing] == "true" - params[:use_existing] = true - end - - if params[:use_existing] || params[:use_existing].nil? - # If nil, reuse workflow steps but not the workflow runner. - @object.use_existing = !!params[:use_existing] - - # Pass the correct argument to arvados-cwl-runner command. - if command[0] == 'arvados-cwl-runner' - command -= ["--disable-reuse", "--enable-reuse"] - command.insert(1, '--enable-reuse') - end - else - @object.use_existing = false - # Pass the correct argument to arvados-cwl-runner command. - if command[0] == 'arvados-cwl-runner' - command -= ["--disable-reuse", "--enable-reuse"] - command.insert(1, '--disable-reuse') - end - end - - @object.command = command - @object.container_image = src.container_image - @object.cwd = src.cwd - @object.description = src.description - @object.environment = src.environment - @object.mounts = src.mounts - @object.name = src.name - @object.output_path = src.output_path - @object.priority = 1 - @object.properties[:template_uuid] = src.properties[:template_uuid] - @object.runtime_constraints = src.runtime_constraints - @object.scheduling_parameters = src.scheduling_parameters - @object.state = 'Uncommitted' - - super - end - - def index - @limit = 20 - super - end - -end diff --git a/apps/workbench/app/controllers/containers_controller.rb b/apps/workbench/app/controllers/containers_controller.rb deleted file mode 100644 index 4b5606778f..0000000000 --- a/apps/workbench/app/controllers/containers_controller.rb +++ /dev/null @@ -1,14 +0,0 @@ -# Copyright (C) The Arvados Authors. All rights reserved. -# -# SPDX-License-Identifier: AGPL-3.0 - -class ContainersController < ApplicationController - skip_around_action :require_thread_api_token, if: proc { |ctrl| - !Rails.configuration.Users.AnonymousUserToken.empty? and - 'show' == ctrl.action_name - } - - def show_pane_list - %w(Status Log Advanced) - end -end diff --git a/apps/workbench/app/controllers/groups_controller.rb b/apps/workbench/app/controllers/groups_controller.rb deleted file mode 100644 index 6abd2ff11d..0000000000 --- a/apps/workbench/app/controllers/groups_controller.rb +++ /dev/null @@ -1,21 +0,0 @@ -# Copyright (C) The Arvados Authors. All rights reserved. -# -# SPDX-License-Identifier: AGPL-3.0 - -class GroupsController < ApplicationController - def index - @groups = Group.filter [['group_class', '!=', 'project'], ['group_class', '!=', 'filter']] - @group_uuids = @groups.collect &:uuid - @links_from = Link.where(link_class: 'permission', tail_uuid: @group_uuids).with_count("none") - @links_to = Link.where(link_class: 'permission', head_uuid: @group_uuids).with_count("none") - render_index - end - - def show - if @object.group_class == 'project' or @object.group_class == 'filter' - redirect_to(project_path(@object)) - else - super - end - end -end diff --git a/apps/workbench/app/controllers/humans_controller.rb b/apps/workbench/app/controllers/humans_controller.rb deleted file mode 100644 index dd08b3064a..0000000000 --- a/apps/workbench/app/controllers/humans_controller.rb +++ /dev/null @@ -1,6 +0,0 @@ -# Copyright (C) The Arvados Authors. All rights reserved. -# -# SPDX-License-Identifier: AGPL-3.0 - -class HumansController < ApplicationController -end diff --git a/apps/workbench/app/controllers/job_tasks_controller.rb b/apps/workbench/app/controllers/job_tasks_controller.rb deleted file mode 100644 index 67b31adde7..0000000000 --- a/apps/workbench/app/controllers/job_tasks_controller.rb +++ /dev/null @@ -1,6 +0,0 @@ -# Copyright (C) The Arvados Authors. All rights reserved. -# -# SPDX-License-Identifier: AGPL-3.0 - -class JobTasksController < ApplicationController -end diff --git a/apps/workbench/app/controllers/jobs_controller.rb b/apps/workbench/app/controllers/jobs_controller.rb deleted file mode 100644 index e5c71cb275..0000000000 --- a/apps/workbench/app/controllers/jobs_controller.rb +++ /dev/null @@ -1,87 +0,0 @@ -# Copyright (C) The Arvados Authors. All rights reserved. -# -# SPDX-License-Identifier: AGPL-3.0 - -class JobsController < ApplicationController - skip_around_action :require_thread_api_token, if: proc { |ctrl| - !Rails.configuration.Users.AnonymousUserToken.empty? and - 'show' == ctrl.action_name - } - - def generate_provenance(jobs) - return if params['tab_pane'] != "Provenance" - - nodes = {} - collections = [] - hashes = [] - jobs.each do |j| - nodes[j[:uuid]] = j - hashes << j[:output] - ProvenanceHelper::find_collections(j[:script_parameters]) do |hash, uuid| - collections << uuid if uuid - hashes << hash if hash - end - nodes[j[:script_version]] = {:uuid => j[:script_version]} - end - - Collection.where(uuid: collections).with_count("none").each do |c| - nodes[c[:portable_data_hash]] = c - end - - Collection.where(portable_data_hash: hashes).with_count("none").each do |c| - nodes[c[:portable_data_hash]] = c - end - - @svg = ProvenanceHelper::create_provenance_graph nodes, "provenance_svg", { - :request => request, - :all_script_parameters => true, - :script_version_nodes => true} - end - - def index - @svg = "" - if params[:uuid] - @objects = Job.where(uuid: params[:uuid]) - generate_provenance(@objects) - render_index - else - @limit = 20 - super - end - end - - def cancel - @object.cancel - if params[:return_to] - redirect_to params[:return_to] - else - redirect_to @object - end - end - - def show - generate_provenance([@object]) - super - end - - def logs - @logs = @object. - stderr_log_query(Rails.configuration.Workbench.RunningJobLogRecordsToFetch). - map { |e| e.serializable_hash.merge({ 'prepend' => true }) } - respond_to do |format| - format.json { render json: @logs } - end - end - - def index_pane_list - if params[:uuid] - %w(Recent Provenance) - else - %w(Recent) - end - end - - def show_pane_list - %w(Status Log Details Provenance Advanced) - end -end diff --git a/apps/workbench/app/controllers/keep_disks_controller.rb b/apps/workbench/app/controllers/keep_disks_controller.rb deleted file mode 100644 index c95ebdc255..0000000000 --- a/apps/workbench/app/controllers/keep_disks_controller.rb +++ /dev/null @@ -1,59 +0,0 @@ -# Copyright (C) The Arvados Authors. All rights reserved. -# -# SPDX-License-Identifier: AGPL-3.0 - -class KeepDisksController < ApplicationController - def create - defaults = { is_readable: true, is_writable: true } - @object = KeepDisk.new defaults.merge(params[:keep_disk] || {}) - super - end - - def index - # Retrieve cache age histogram info from logs. - - # In the logs we expect to find it in an ordered list with entries - # of the form (mtime, disk proportion free). - - # An entry of the form (1388747781, 0.52) means that if we deleted - # the oldest non-presisted blocks until we had 52% of the disk - # free, then all blocks with an mtime greater than 1388747781 - # would be preserved. - - # The chart we want to produce, will tell us how much of the disk - # will be free if we use a cache age of x days. Therefore we will - # produce output specifying the age, cache and persisted. age is - # specified in milliseconds. cache is the size of the cache if we - # delete all blocks older than age. persistent is the size of the - # persisted blocks. It is constant regardless of age, but it lets - # us show a stacked graph. - - # Finally each entry in cache_age_histogram is a dictionary, - # because that's what our charting package wats. - - @cache_age_histogram = [] - @histogram_pretty_date = nil - histogram_log = Log. - filter([[:event_type, '=', 'block-age-free-space-histogram']]). - order(:created_at => :desc). - with_count('none'). - limit(1) - histogram_log.each do |log_entry| - # We expect this block to only execute at most once since we - # specified limit(1) - @cache_age_histogram = log_entry['properties'][:histogram] - # Javascript wants dates in milliseconds. - histogram_date_ms = log_entry['event_at'].to_i * 1000 - @histogram_pretty_date = log_entry['event_at'].strftime('%b %-d, %Y') - - total_free_cache = @cache_age_histogram[-1][1] - persisted_storage = 1 - total_free_cache - @cache_age_histogram.map! { |x| {:age => histogram_date_ms - x[0]*1000, - :cache => total_free_cache - x[1], - :persisted => persisted_storage} } - end - - # Do the regular control work needed. - super - end -end diff --git a/apps/workbench/app/controllers/keep_services_controller.rb b/apps/workbench/app/controllers/keep_services_controller.rb deleted file mode 100644 index 361d4002c6..0000000000 --- a/apps/workbench/app/controllers/keep_services_controller.rb +++ /dev/null @@ -1,6 +0,0 @@ -# Copyright (C) The Arvados Authors. All rights reserved. -# -# SPDX-License-Identifier: AGPL-3.0 - -class KeepServicesController < ApplicationController -end diff --git a/apps/workbench/app/controllers/links_controller.rb b/apps/workbench/app/controllers/links_controller.rb deleted file mode 100644 index b79fad4a6c..0000000000 --- a/apps/workbench/app/controllers/links_controller.rb +++ /dev/null @@ -1,13 +0,0 @@ -# Copyright (C) The Arvados Authors. All rights reserved. -# -# SPDX-License-Identifier: AGPL-3.0 - -class LinksController < ApplicationController - def show - if @object.link_class == 'name' and - Collection == ArvadosBase::resource_class_for_uuid(@object.head_uuid) - return redirect_to collection_path(@object.uuid) - end - super - end -end diff --git a/apps/workbench/app/controllers/logs_controller.rb b/apps/workbench/app/controllers/logs_controller.rb deleted file mode 100644 index 7e413284bb..0000000000 --- a/apps/workbench/app/controllers/logs_controller.rb +++ /dev/null @@ -1,7 +0,0 @@ -# Copyright (C) The Arvados Authors. All rights reserved. -# -# SPDX-License-Identifier: AGPL-3.0 - -class LogsController < ApplicationController - before_action :ensure_current_user_is_admin -end diff --git a/apps/workbench/app/controllers/management_controller.rb b/apps/workbench/app/controllers/management_controller.rb deleted file mode 100644 index 4c8b52f661..0000000000 --- a/apps/workbench/app/controllers/management_controller.rb +++ /dev/null @@ -1,51 +0,0 @@ -# Copyright (C) The Arvados Authors. All rights reserved. -# -# SPDX-License-Identifier: AGPL-3.0 - -require 'app_version' - -class ManagementController < ApplicationController - skip_around_action :thread_clear - skip_around_action :set_thread_api_token - skip_around_action :require_thread_api_token - skip_before_action :ensure_arvados_api_exists - skip_before_action :accept_uuid_as_id_param - skip_before_action :check_user_agreements - skip_before_action :check_user_profile - skip_before_action :load_filters_and_paging_params - skip_before_action :find_object_by_uuid - - before_action :check_auth_header - - def check_auth_header - mgmt_token = Rails.configuration.ManagementToken - auth_header = request.headers['Authorization'] - - if mgmt_token.empty? - render :json => {:errors => "disabled"}, :status => 404 - elsif !auth_header - render :json => {:errors => "authorization required"}, :status => 401 - elsif auth_header != 'Bearer '+mgmt_token - render :json => {:errors => "authorization error"}, :status => 403 - end - end - - def metrics - render content_type: 'text/plain', plain: <<~EOF -# HELP arvados_config_load_timestamp_seconds Time when config file was loaded. -# TYPE arvados_config_load_timestamp_seconds gauge -arvados_config_load_timestamp_seconds{sha256="#{Rails.configuration.SourceSHA256}"} #{Rails.configuration.LoadTimestamp.to_f} -# HELP arvados_config_source_timestamp_seconds Timestamp of config file when it was loaded. -# TYPE arvados_config_source_timestamp_seconds gauge -arvados_config_source_timestamp_seconds{sha256="#{Rails.configuration.SourceSHA256}"} #{Rails.configuration.SourceTimestamp.to_f} -# HELP arvados_version_running Indicated version is running. -# TYPE arvados_version_running gauge -arvados_version_running{version="#{AppVersion.package_version}"} 1 -EOF - end - - def health - resp = {"health" => "OK"} - render json: resp - end -end diff --git a/apps/workbench/app/controllers/nodes_controller.rb b/apps/workbench/app/controllers/nodes_controller.rb deleted file mode 100644 index 72bde698e4..0000000000 --- a/apps/workbench/app/controllers/nodes_controller.rb +++ /dev/null @@ -1,6 +0,0 @@ -# Copyright (C) The Arvados Authors. All rights reserved. -# -# SPDX-License-Identifier: AGPL-3.0 - -class NodesController < ApplicationController -end diff --git a/apps/workbench/app/controllers/pipeline_instances_controller.rb b/apps/workbench/app/controllers/pipeline_instances_controller.rb deleted file mode 100644 index 81417ff165..0000000000 --- a/apps/workbench/app/controllers/pipeline_instances_controller.rb +++ /dev/null @@ -1,373 +0,0 @@ -# Copyright (C) The Arvados Authors. All rights reserved. -# -# SPDX-License-Identifier: AGPL-3.0 - -class PipelineInstancesController < ApplicationController - skip_before_action :find_object_by_uuid, only: :compare - before_action :find_objects_by_uuid, only: :compare - skip_around_action :require_thread_api_token, if: proc { |ctrl| - !Rails.configuration.Users.AnonymousUserToken.empty? and - 'show' == ctrl.action_name - } - - include PipelineInstancesHelper - include PipelineComponentsHelper - - def copy - template = PipelineTemplate.find?(@object.pipeline_template_uuid) - - source = @object - @object = PipelineInstance.new - @object.pipeline_template_uuid = source.pipeline_template_uuid - - if params['components'] == 'use_latest' and template - @object.components = template.components.deep_dup - @object.components.each do |cname, component| - # Go through the script parameters of each component - # that are marked as user input and copy them over. - # Skip any components that are not present in the - # source instance (there's nothing to copy) - if source.components.include? cname - component[:script_parameters].each do |pname, val| - if val.is_a? Hash and val[:dataclass] - # this is user-inputtable, so check the value from the source pipeline - srcvalue = source.components[cname][:script_parameters][pname] - if not srcvalue.nil? - component[:script_parameters][pname] = srcvalue - end - end - end - end - end - else - @object.components = source.components.deep_dup - end - - if params['script'] == 'use_same' - # Go through each component and copy the script_version from each job. - @object.components.each do |cname, component| - if source.components.include? cname and source.components[cname][:job] - component[:script_version] = source.components[cname][:job][:script_version] - end - end - end - - @object.components.each do |cname, component| - component.delete :job - end - @object.state = 'New' - - # set owner_uuid to that of source, provided it is a project and writable by current user - current_project = Group.find(source.owner_uuid) rescue nil - if (current_project && current_project.writable_by.andand.include?(current_user.uuid)) - @object.owner_uuid = source.owner_uuid - end - - super - end - - def update - @updates ||= params.to_unsafe_hash[@object.class.to_s.underscore.singularize.to_sym] - if (components = @updates[:components]) - components.each do |cname, component| - if component[:script_parameters] - component[:script_parameters].each do |param, value_info| - if value_info.is_a? Hash - value_info_partitioned = value_info[:value].partition('/') if value_info[:value].andand.class.eql?(String) - value_info_value = value_info_partitioned ? value_info_partitioned[0] : value_info[:value] - value_info_class = resource_class_for_uuid value_info_value - if value_info_class == Link - # Use the link target, not the link itself, as script - # parameter; but keep the link info around as well. - link = Link.find value_info[:value] - value_info[:value] = link.head_uuid - value_info[:link_uuid] = link.uuid - value_info[:link_name] = link.name - else - # Delete stale link_uuid and link_name data. - value_info[:link_uuid] = nil - value_info[:link_name] = nil - end - if value_info_class == Collection - # to ensure reproducibility, the script_parameter for a - # collection should be the portable_data_hash - # keep the collection name and uuid for human-readability - obj = Collection.find value_info_value - if value_info_partitioned - value_info[:value] = obj.portable_data_hash + value_info_partitioned[1] + value_info_partitioned[2] - value_info[:selection_name] = obj.name ? obj.name + value_info_partitioned[1] + value_info_partitioned[2] : obj.name - else - value_info[:value] = obj.portable_data_hash - value_info[:selection_name] = obj.name - end - value_info[:selection_uuid] = obj.uuid - end - end - end - end - end - end - super - end - - def graph(pipelines) - return nil, nil if params['tab_pane'] != "Graph" - - provenance = {} - pips = {} - n = 1 - - # When comparing more than one pipeline, "pips" stores bit fields that - # indicates which objects are part of which pipelines. - - pipelines.each do |p| - collections = [] - hashes = [] - jobs = [] - - p[:components].each do |k, v| - provenance["component_#{p[:uuid]}_#{k}"] = v - - collections << v[:output_uuid] if v[:output_uuid] - jobs << v[:job][:uuid] if v[:job] - end - - jobs = jobs.compact.uniq - if jobs.any? - Job.where(uuid: jobs).with_count("none").each do |j| - job_uuid = j.uuid - - provenance[job_uuid] = j - pips[job_uuid] = 0 unless pips[job_uuid] != nil - pips[job_uuid] |= n - - hashes << j[:output] if j[:output] - ProvenanceHelper::find_collections(j) do |hash, uuid| - collections << uuid if uuid - hashes << hash if hash - end - - if j[:script_version] - script_uuid = j[:script_version] - provenance[script_uuid] = {:uuid => script_uuid} - pips[script_uuid] = 0 unless pips[script_uuid] != nil - pips[script_uuid] |= n - end - end - end - - hashes = hashes.compact.uniq - if hashes.any? - Collection.where(portable_data_hash: hashes).with_count("none").each do |c| - hash_uuid = c.portable_data_hash - provenance[hash_uuid] = c - pips[hash_uuid] = 0 unless pips[hash_uuid] != nil - pips[hash_uuid] |= n - end - end - - collections = collections.compact.uniq - if collections.any? - Collection.where(uuid: collections).with_count("none").each do |c| - collection_uuid = c.uuid - provenance[collection_uuid] = c - pips[collection_uuid] = 0 unless pips[collection_uuid] != nil - pips[collection_uuid] |= n - end - end - - n = n << 1 - end - - return provenance, pips - end - - def show - # the #show action can also be called by #compare, which does its own work to set up @pipelines - unless defined? @pipelines - @pipelines = [@object] - end - - provenance, pips = graph(@pipelines) - if provenance - @prov_svg = ProvenanceHelper::create_provenance_graph provenance, "provenance_svg", { - :request => request, - :all_script_parameters => true, - :combine_jobs => :script_and_version, - :pips => pips, - :only_components => true, - :no_docker => true, - :no_log => true} - end - - super - end - - def compare - @breadcrumb_page_name = 'compare' - - @rows = [] # each is {name: S, components: [...]} - - if params['tab_pane'] == "Compare" or params['tab_pane'].nil? - # Build a table: x=pipeline y=component - @objects.each_with_index do |pi, pi_index| - pipeline_jobs(pi).each do |component| - # Find a cell with the same name as this component but no - # entry for this pipeline - target_row = nil - @rows.each_with_index do |row, row_index| - if row[:name] == component[:name] and !row[:components][pi_index] - target_row = row - end - end - if !target_row - target_row = {name: component[:name], components: []} - @rows << target_row - end - target_row[:components][pi_index] = component - end - end - - @rows.each do |row| - # Build a "normal" pseudo-component for this row by picking the - # most common value for each attribute. If all values are - # equally common, there is no "normal". - normal = {} # attr => most common value - highscore = {} # attr => how common "normal" is - score = {} # attr => { value => how common } - row[:components].each do |pj| - next if pj.nil? - pj.each do |k,v| - vstr = for_comparison v - score[k] ||= {} - score[k][vstr] = (score[k][vstr] || 0) + 1 - highscore[k] ||= 0 - if score[k][vstr] == highscore[k] - # tie for first place = no "normal" - normal.delete k - elsif score[k][vstr] == highscore[k] + 1 - # more pipelines have v than anything else - highscore[k] = score[k][vstr] - normal[k] = vstr - end - end - end - - # Add a hash in component[:is_normal]: { attr => is_the_value_normal? } - row[:components].each do |pj| - next if pj.nil? - pj[:is_normal] = {} - pj.each do |k,v| - pj[:is_normal][k] = (normal.has_key?(k) && normal[k] == for_comparison(v)) - end - end - end - end - - if params['tab_pane'] == "Graph" - @pipelines = @objects - end - - @object = @objects.first - - show - end - - def show_pane_list - panes = %w(Components Log Graph Advanced) - if @object and @object.state.in? ['New', 'Ready'] - panes = %w(Inputs) + panes - %w(Log) - end - if not @object.components.values.any? { |x| x[:job] rescue false } - panes -= ['Graph'] - end - panes - end - - def compare_pane_list - %w(Compare Graph) - end - - helper_method :unreadable_inputs_present? - def unreadable_inputs_present? - unless @unreadable_inputs_present.nil? - return @unreadable_inputs_present - end - - input_uuids = [] - input_pdhs = [] - @object.components.each do |k, component| - next if !component - component[:script_parameters].andand.each do |p, tv| - if (tv.is_a? Hash) and ((tv[:dataclass] == "Collection") || (tv[:dataclass] == "File")) - if tv[:value] - value = tv[:value] - elsif tv[:default] - value = tv[:default] - else - value = '' - end - if value.present? - split = value.split '/' - if CollectionsHelper.match(split[0]) - input_pdhs << split[0] - else - input_uuids << split[0] - end - end - end - end - end - - input_pdhs = input_pdhs.uniq - input_uuids = input_uuids.uniq - - preload_collections_for_objects input_uuids if input_uuids.any? - preload_for_pdhs input_pdhs if input_pdhs.any? - - @unreadable_inputs_present = false - input_uuids.each do |uuid| - if !collections_for_object(uuid).any? - @unreadable_inputs_present = true - break - end - end - if !@unreadable_inputs_present - input_pdhs.each do |pdh| - if !collection_for_pdh(pdh).any? - @unreadable_inputs_present = true - break - end - end - end - - @unreadable_inputs_present - end - - def cancel - @object.cancel - if params[:return_to] - redirect_to params[:return_to] - else - redirect_to @object - end - end - - protected - def for_comparison v - if v.is_a? Hash or v.is_a? Array - v.to_json - else - v.to_s - end - end - - def load_filters_and_paging_params - params[:limit] = 20 - super - end - - def find_objects_by_uuid - @objects = model_class.where(uuid: params[:uuids]) - end -end diff --git a/apps/workbench/app/controllers/pipeline_templates_controller.rb b/apps/workbench/app/controllers/pipeline_templates_controller.rb deleted file mode 100644 index aa444c153b..0000000000 --- a/apps/workbench/app/controllers/pipeline_templates_controller.rb +++ /dev/null @@ -1,21 +0,0 @@ -# Copyright (C) The Arvados Authors. All rights reserved. -# -# SPDX-License-Identifier: AGPL-3.0 - -class PipelineTemplatesController < ApplicationController - skip_around_action :require_thread_api_token, if: proc { |ctrl| - !Rails.configuration.Users.AnonymousUserToken.empty? and - 'show' == ctrl.action_name - } - - include PipelineComponentsHelper - - def show - @objects = PipelineInstance.where(pipeline_template_uuid: @object.uuid) - super - end - - def show_pane_list - %w(Components Pipelines Advanced) - end -end diff --git a/apps/workbench/app/controllers/projects_controller.rb b/apps/workbench/app/controllers/projects_controller.rb deleted file mode 100644 index e448e1b453..0000000000 --- a/apps/workbench/app/controllers/projects_controller.rb +++ /dev/null @@ -1,323 +0,0 @@ -# Copyright (C) The Arvados Authors. All rights reserved. -# -# SPDX-License-Identifier: AGPL-3.0 - -class ProjectsController < ApplicationController - before_action :set_share_links, if: -> { defined? @object and @object} - skip_around_action :require_thread_api_token, if: proc { |ctrl| - !Rails.configuration.Users.AnonymousUserToken.empty? and - %w(show tab_counts public).include? ctrl.action_name - } - - def model_class - Group - end - - def find_object_by_uuid - if (current_user and params[:uuid] == current_user.uuid) or - (resource_class_for_uuid(params[:uuid]) == User) - if params[:uuid] != current_user.uuid - @object = User.find(params[:uuid]) - else - @object = current_user.dup - @object.uuid = current_user.uuid - end - - class << @object - def name - if current_user.uuid == self.uuid - 'Home' - else - "Home for #{self.email}" - end - end - def description - '' - end - def attribute_editable? attr, *args - case attr - when 'description', 'name' - false - else - super - end - end - end - else - super - end - end - - def index_pane_list - %w(Projects) - end - - # Returning an array of hashes instead of an array of strings will allow - # us to tell the interface to get counts for each pane (using :filters). - # It also seems to me that something like these could be used to configure the contents of the panes. - def show_pane_list - pane_list = [] - - procs = ["arvados#containerRequest"] - procs_pane_name = 'Processes' - if PipelineInstance.api_exists?(:index) - procs << "arvados#pipelineInstance" - procs_pane_name = 'Pipelines_and_processes' - end - - workflows = ["arvados#workflow"] - workflows_pane_name = 'Workflows' - if PipelineTemplate.api_exists?(:index) - workflows << "arvados#pipelineTemplate" - workflows_pane_name = 'Pipeline_templates' - end - - if @object.uuid != current_user.andand.uuid - pane_list << 'Description' - end - pane_list << - { - :name => 'Data_collections', - :filters => [%w(uuid is_a arvados#collection)] - } - pane_list << - { - :name => procs_pane_name, - :filters => [%w(uuid is_a) + [procs]] - } - pane_list << - { - :name => workflows_pane_name, - :filters => [%w(uuid is_a) + [workflows]] - } - pane_list << - { - :name => 'Subprojects', - :filters => [%w(uuid is_a arvados#group)] - } - pane_list << - { - :name => 'Other_objects', - :filters => [%w(uuid is_a) + [%w(arvados#human arvados#specimen arvados#trait)]] - } if current_user - pane_list << { :name => 'Sharing', - :count => @share_links.count } if @user_is_manager - pane_list << { :name => 'Advanced' } - end - - # Called via AJAX and returns Javascript that populates tab counts into tab titles. - # References #show_pane_list action which should return an array of hashes each with :name - # and then optionally a :filters to run or a straight up :count - # - # This action could easily be moved to the ApplicationController to genericize the tab_counts behaviour, - # but one or more new routes would have to be created, the js.erb would also have to be moved - def tab_counts - @tab_counts = {} - show_pane_list.each do |pane| - if pane.is_a?(Hash) - if pane[:count] - @tab_counts[pane[:name]] = pane[:count] - elsif pane[:filters] - @tab_counts[pane[:name]] = @object.contents(filters: pane[:filters]).items_available - end - end - end - end - - def remove_item - params[:item_uuids] = [params[:item_uuid]] - remove_items - render template: 'projects/remove_items' - end - - def remove_items - @removed_uuids = [] - params[:item_uuids].collect { |uuid| ArvadosBase.find uuid }.each do |item| - if item.class == Collection or item.class == Group or item.class == Workflow or item.class == ContainerRequest - # Use delete API on collections and projects/groups - item.destroy - @removed_uuids << item.uuid - elsif item.owner_uuid == @object.uuid - # Object is owned by this project. Remove it from the project by - # changing owner to the current user. - begin - item.update_attributes owner_uuid: current_user.uuid - @removed_uuids << item.uuid - rescue ArvadosApiClient::ApiErrorResponseException => e - if e.message.include? '_owner_uuid_' - rename_to = item.name + ' removed from ' + - (@object.name ? @object.name : @object.uuid) + - ' at ' + Time.now.to_s - updates = {} - updates[:name] = rename_to - updates[:owner_uuid] = current_user.uuid - item.update_attributes updates - @removed_uuids << item.uuid - else - raise - end - end - end - end - end - - def destroy - while (objects = Link.filter([['owner_uuid','=',@object.uuid], - ['tail_uuid','=',@object.uuid]]).with_count("none")).any? - objects.each do |object| - object.destroy - end - end - while (objects = @object.contents).any? - objects.each do |object| - object.update_attributes! owner_uuid: current_user.uuid - end - end - if ArvadosBase::resource_class_for_uuid(@object.owner_uuid) == Group - params[:return_to] ||= group_path(@object.owner_uuid) - else - params[:return_to] ||= projects_path - end - super - end - - def find_objects_for_index - # We can use the all_projects helper, but we have to dup the - # result -- otherwise, when we apply our per-request filters and - # limits, they will infect the @all_projects cache too (see - # #6640). - @objects = all_projects.dup - super - end - - def load_contents_objects kinds=[] - kind_filters = @filters.select do |attr,op,val| - op == 'is_a' and val.is_a? Array and val.count > 1 - end - if /^created_at\b/ =~ @order[0] and kind_filters.count == 1 - # If filtering on multiple types and sorting by date: Get the - # first page of each type, sort the entire set, truncate to one - # page, and use the last item on this page as a filter for - # retrieving the next page. Ideally the API would do this for - # us, but it doesn't (yet). - - # To avoid losing items that have the same created_at as the - # last item on this page, we retrieve an overlapping page with a - # "created_at <= last_created_at" filter, then remove duplicates - # with a "uuid not in [...]" filter (see below). - nextpage_operator = /\bdesc$/i =~ @order[0] ? '<=' : '>=' - - @objects = [] - @name_link_for = {} - kind_filters.each do |attr,op,val| - (val.is_a?(Array) ? val : [val]).each do |type| - klass = type.split('#')[-1] - klass[0] = klass[0].capitalize - next if(!Object.const_get(klass).api_exists?(:index)) - - filters = @filters - kind_filters + [['uuid', 'is_a', type]] - if type == 'arvados#containerRequest' - filters = filters + [['container_requests.requesting_container_uuid', '=', nil]] - end - objects = @object.contents(order: @order, - limit: @limit, - filters: filters, - ) - objects.each do |object| - @name_link_for[object.andand.uuid] = objects.links_for(object, 'name').first - end - @objects += objects - end - end - @objects = @objects.to_a.sort_by(&:created_at) - @objects.reverse! if nextpage_operator == '<=' - @objects = @objects[0..@limit-1] - - if @objects.any? - @next_page_filters = next_page_filters(nextpage_operator) - @next_page_href = url_for(partial: :contents_rows, - limit: @limit, - filters: @next_page_filters.to_json) - else - @next_page_href = nil - end - else - @objects = @object.contents(order: @order, - limit: @limit, - filters: @filters, - offset: @offset) - @next_page_href = next_page_href(partial: :contents_rows, - filters: @filters.to_json, - order: @order.to_json) - end - - preload_links_for_objects(@objects.to_a) - end - - def show - if !@object - return render_not_found("object not found") - end - - if params[:partial] - load_contents_objects - respond_to do |f| - f.json { - render json: { - content: render_to_string(partial: 'show_contents_rows.html', - formats: [:html]), - next_page_href: @next_page_href - } - } - end - else - @objects = [] - super - end - end - - def create - @new_resource_attrs = (params['project'] || {}).merge(group_class: 'project') - @new_resource_attrs[:name] ||= 'New project' - super - end - - def update - @updates = params['project'] - super - end - - helper_method :get_objects_and_names - def get_objects_and_names(objects=nil) - objects = @objects if objects.nil? - objects_and_names = [] - objects.each do |object| - if objects.respond_to? :links_for and - !(name_links = objects.links_for(object, 'name')).empty? - name_links.each do |name_link| - objects_and_names << [object, name_link] - end - elsif @name_link_for.andand[object.uuid] - objects_and_names << [object, @name_link_for[object.uuid]] - elsif object.respond_to? :name - objects_and_names << [object, object] - else - objects_and_names << [object, - Link.new(owner_uuid: @object.uuid, - tail_uuid: @object.uuid, - head_uuid: object.uuid, - link_class: "name", - name: "")] - - end - end - objects_and_names - end - - def public # Yes 'public' is the name of the action for public projects - return render_not_found if Rails.configuration.Users.AnonymousUserToken.empty? or not Rails.configuration.Workbench.EnablePublicProjectsPage - @objects = using_specific_api_token Rails.configuration.Users.AnonymousUserToken do - Group.where(group_class: 'project').order("modified_at DESC") - end - end -end diff --git a/apps/workbench/app/controllers/repositories_controller.rb b/apps/workbench/app/controllers/repositories_controller.rb deleted file mode 100644 index 953ee1dd21..0000000000 --- a/apps/workbench/app/controllers/repositories_controller.rb +++ /dev/null @@ -1,107 +0,0 @@ -# Copyright (C) The Arvados Authors. All rights reserved. -# -# SPDX-License-Identifier: AGPL-3.0 - -class RepositoriesController < ApplicationController - before_action :set_share_links, if: -> { defined? @object } - - def index_pane_list - %w(repositories help) - end - - def show_pane_list - if @user_is_manager - panes = super | %w(Sharing) - panes.insert(panes.length-1, panes.delete_at(panes.index('Advanced'))) if panes.index('Advanced') - panes - else - panes = super - end - panes.delete('Attributes') if !current_user.is_admin - panes - end - - def show_tree - @commit = params[:commit] - @path = params[:path] || '' - @subtree = @object.ls_subtree @commit, @path.chomp('/') - end - - def show_blob - @commit = params[:commit] - @path = params[:path] - @blobdata = @object.cat_file @commit, @path - end - - def show_commit - @commit = params[:commit] - end - - def all_repos - limit = params[:limit].andand.to_i || 100 - offset = params[:offset].andand.to_i || 0 - @filters = params[:filters] || [] - - if @filters.any? - owner_filter = @filters.select do |attr, op, val| - (attr == 'owner_uuid') - end - end - - if !owner_filter.andand.any? - filters = @filters + [["owner_uuid", "=", current_user.uuid]] - my_repos = Repository.all.order("name ASC").limit(limit).with_count("none").offset(offset).filter(filters).results - else # done fetching all owned repositories - my_repos = [] - end - - if !owner_filter.andand.any? # if this is next page request, the first page was still fetching "own" repos - @filters = @filters.reject do |attr, op, val| - (attr == 'owner_uuid') or - (attr == 'name') or - (attr == 'uuid') - end - end - - filters = @filters + [["owner_uuid", "!=", current_user.uuid]] - other_repos = Repository.all.order("name ASC").limit(limit).with_count("none").offset(offset).filter(filters).results - - @objects = (my_repos + other_repos).first(limit) - end - - def find_objects_for_index - return if !params[:partial] - - all_repos - - if @objects.any? - @next_page_filters = next_page_filters('>=') - @next_page_href = url_for(partial: :repositories_rows, - filters: @next_page_filters.to_json) - else - @next_page_href = nil - end - end - - def next_page_href with_params={} - @next_page_href - end - - def next_page_filters nextpage_operator - next_page_filters = @filters.reject do |attr, op, val| - (attr == 'owner_uuid') or - (attr == 'name' and op == nextpage_operator) or - (attr == 'uuid' and op == 'not in') - end - - if @objects.any? - last_obj = @objects.last - next_page_filters += [['name', nextpage_operator, last_obj.name]] - next_page_filters += [['uuid', 'not in', [last_obj.uuid]]] - # if not-owned, it means we are done with owned repos and fetching other repos - next_page_filters += [['owner_uuid', '!=', last_obj.uuid]] if last_obj.owner_uuid != current_user.uuid - end - - next_page_filters - end -end diff --git a/apps/workbench/app/controllers/search_controller.rb b/apps/workbench/app/controllers/search_controller.rb deleted file mode 100644 index 80f3ff117a..0000000000 --- a/apps/workbench/app/controllers/search_controller.rb +++ /dev/null @@ -1,38 +0,0 @@ -# Copyright (C) The Arvados Authors. All rights reserved. -# -# SPDX-License-Identifier: AGPL-3.0 - -class SearchController < ApplicationController - skip_before_action :ensure_arvados_api_exists - - def find_objects_for_index - search_what = Group - if params[:project_uuid] - # Special case for "search all things in project": - @filters = @filters.select do |attr, operator, operand| - not (attr == 'owner_uuid' and operator == '=') - end - # Special case for project_uuid is a user uuid: - if ArvadosBase::resource_class_for_uuid(params[:project_uuid]) == User - search_what = User.find params[:project_uuid] - else - search_what = Group.find params[:project_uuid] - end - end - @objects = search_what.contents(limit: @limit, - offset: @offset, - recursive: true, - count: 'none', - last_object_class: params["last_object_class"], - filters: @filters) - super - end - - def next_page_href with_params={} - super with_params.merge(last_object_class: @objects.last.class.to_s, - project_uuid: params[:project_uuid], - recursive: true, - count: 'none', - filters: @filters.to_json) - end -end diff --git a/apps/workbench/app/controllers/sessions_controller.rb b/apps/workbench/app/controllers/sessions_controller.rb deleted file mode 100644 index 6557fc0626..0000000000 --- a/apps/workbench/app/controllers/sessions_controller.rb +++ /dev/null @@ -1,25 +0,0 @@ -# Copyright (C) The Arvados Authors. All rights reserved. -# -# SPDX-License-Identifier: AGPL-3.0 - -class SessionsController < ApplicationController - skip_around_action :require_thread_api_token, :only => [:destroy, :logged_out] - skip_around_action :set_thread_api_token, :only => [:destroy, :logged_out] - skip_before_action :find_object_by_uuid - skip_before_action :find_objects_for_index, raise: false - skip_before_action :ensure_arvados_api_exists - - def destroy - token = session[:arvados_api_token] - session.clear - redirect_to arvados_api_client.arvados_logout_url(return_to: root_url, api_token: token) - end - - def logged_out - redirect_to root_url if session[:arvados_api_token] - render_index - end - - def index - end -end diff --git a/apps/workbench/app/controllers/specimens_controller.rb b/apps/workbench/app/controllers/specimens_controller.rb deleted file mode 100644 index 76a12715a7..0000000000 --- a/apps/workbench/app/controllers/specimens_controller.rb +++ /dev/null @@ -1,6 +0,0 @@ -# Copyright (C) The Arvados Authors. All rights reserved. -# -# SPDX-License-Identifier: AGPL-3.0 - -class SpecimensController < ApplicationController -end diff --git a/apps/workbench/app/controllers/status_controller.rb b/apps/workbench/app/controllers/status_controller.rb deleted file mode 100644 index 0e45daa1df..0000000000 --- a/apps/workbench/app/controllers/status_controller.rb +++ /dev/null @@ -1,19 +0,0 @@ -# Copyright (C) The Arvados Authors. All rights reserved. -# -# SPDX-License-Identifier: AGPL-3.0 - -require "app_version" - -class StatusController < ApplicationController - skip_around_action :require_thread_api_token - skip_before_action :find_object_by_uuid - def status - # Allow non-credentialed cross-origin requests - headers['Access-Control-Allow-Origin'] = '*' - resp = { - apiBaseURL: arvados_api_client.arvados_v1_base.sub(%r{/arvados/v\d+.*}, '/'), - version: AppVersion.hash, - } - render json: resp - end -end diff --git a/apps/workbench/app/controllers/tests_controller.rb b/apps/workbench/app/controllers/tests_controller.rb deleted file mode 100644 index 73c1f4f34a..0000000000 --- a/apps/workbench/app/controllers/tests_controller.rb +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (C) The Arvados Authors. All rights reserved. -# -# SPDX-License-Identifier: AGPL-3.0 - -class TestsController < ApplicationController - skip_before_action :find_object_by_uuid - def mithril - end -end diff --git a/apps/workbench/app/controllers/traits_controller.rb b/apps/workbench/app/controllers/traits_controller.rb deleted file mode 100644 index 81bded480f..0000000000 --- a/apps/workbench/app/controllers/traits_controller.rb +++ /dev/null @@ -1,6 +0,0 @@ -# Copyright (C) The Arvados Authors. All rights reserved. -# -# SPDX-License-Identifier: AGPL-3.0 - -class TraitsController < ApplicationController -end diff --git a/apps/workbench/app/controllers/trash_items_controller.rb b/apps/workbench/app/controllers/trash_items_controller.rb deleted file mode 100644 index d8f7ae62c8..0000000000 --- a/apps/workbench/app/controllers/trash_items_controller.rb +++ /dev/null @@ -1,147 +0,0 @@ -# Copyright (C) The Arvados Authors. All rights reserved. -# -# SPDX-License-Identifier: AGPL-3.0 - -class TrashItemsController < ApplicationController - def model_class - Collection - end - - def index_pane_list - %w(Trashed_collections Trashed_projects) - end - - def find_objects_for_index - # If it's not the index rows partial display, just return - # The /index request will again be invoked to display the - # partial at which time, we will be using the objects found. - return if !params[:partial] - - trashed_items - - if @objects.any? - @objects = @objects.sort_by { |obj| obj.modified_at }.reverse - @next_page_filters = next_page_filters('<=') - @next_page_href = url_for(partial: params[:partial], - filters: @next_page_filters.to_json) - else - @next_page_href = nil - end - end - - def next_page_href with_params={} - @next_page_href - end - - def next_page_filters nextpage_operator - next_page_filters = @filters.reject do |attr, op, val| - (attr == 'modified_at' and op == nextpage_operator) or - (attr == 'uuid' and op == 'not in') - end - - if @objects.any? - last_trash_at = @objects.last.modified_at - - last_uuids = [] - @objects.each do |obj| - last_uuids << obj.uuid if obj.trash_at.eql?(last_trash_at) - end - - next_page_filters += [['modified_at', nextpage_operator, last_trash_at]] - next_page_filters += [['uuid', 'not in', last_uuids]] - end - - next_page_filters - end - - def trashed_items - if params[:partial] == "trashed_collection_rows" - query_on = Collection - elsif params[:partial] == "trashed_project_rows" - query_on = Group - end - - last_mod_at = nil - last_uuids = [] - - # API server index doesn't return manifest_text by default, but our - # callers want it unless otherwise specified. - #@select ||= query_on.columns.map(&:name) - %w(id updated_at) - limit = if params[:limit] then params[:limit].to_i else 100 end - offset = if params[:offset] then params[:offset].to_i else 0 end - - @objects = [] - while !@objects.any? - base_search = query_on - - if !last_mod_at.nil? - base_search = base_search.filter([["modified_at", "<=", last_mod_at], ["uuid", "not in", last_uuids]]) - end - - base_search = base_search.include_trash(true).limit(limit).with_count("none").offset(offset) - - if params[:filters].andand.length.andand > 0 - tags = Link.filter(params[:filters]).with_count("none") - tagged = [] - if tags.results.length > 0 - tagged = query_on.include_trash(true).where(uuid: tags.collect(&:head_uuid)) - end - @objects = (tagged | base_search.filter(params[:filters])).uniq(&:uuid) - else - @objects = base_search.where(is_trashed: true) - end - - if @objects.any? - owner_uuids = @objects.collect(&:owner_uuid).uniq - @owners = {} - @not_trashed = {} - [Group, User].each do |owner_class| - owner_class.filter([["uuid", "in", owner_uuids]]).with_count("none") - .include_trash(true).fetch_multiple_pages(false) - .each do |owner| - @owners[owner.uuid] = owner - end - end - Group.filter([["uuid", "in", owner_uuids]]).with_count("none").select([:uuid]).each do |grp| - @not_trashed[grp.uuid] = true - end - else - return - end - - last_mod_at = @objects.last.modified_at - last_uuids = [] - @objects.each do |obj| - last_uuids << obj.uuid if obj.modified_at.eql?(last_mod_at) - end - - @objects = @objects.select {|item| item.is_trashed || @not_trashed[item.owner_uuid].nil? } - end - end - - def untrash_items - @untrashed_uuids = [] - - updates = {trash_at: nil} - - if params[:selection].is_a? Array - klass = resource_class_for_uuid(params[:selection][0]) - else - klass = resource_class_for_uuid(params[:selection]) - end - - first = nil - klass.include_trash(1).where(uuid: params[:selection]).each do |c| - first = c - c.untrash - @untrashed_uuids << c.uuid - end - - respond_to do |format| - format.js - format.html do - redirect_to first - end - end - end -end diff --git a/apps/workbench/app/controllers/user_agreements_controller.rb b/apps/workbench/app/controllers/user_agreements_controller.rb deleted file mode 100644 index 5e530a657e..0000000000 --- a/apps/workbench/app/controllers/user_agreements_controller.rb +++ /dev/null @@ -1,37 +0,0 @@ -# Copyright (C) The Arvados Authors. All rights reserved. -# -# SPDX-License-Identifier: AGPL-3.0 - -class UserAgreementsController < ApplicationController - skip_before_action :check_user_agreements - skip_before_action :find_object_by_uuid - skip_before_action :check_user_profile - - def index - if unsigned_user_agreements.empty? - if params[:return_to] - redirect_to(params[:return_to]) - else - redirect_back(fallback_location: root_path) - end - end - end - - def model_class - Collection - end - - def sign - params[:checked].each do |checked| - if (r = CollectionsHelper.match_uuid_with_optional_filepath(checked)) - UserAgreement.sign uuid: r[1] - end - end - current_user.activate - if params[:return_to] - redirect_to(params[:return_to]) - else - redirect_back(fallback_location: root_path) - end - end -end diff --git a/apps/workbench/app/controllers/users_controller.rb b/apps/workbench/app/controllers/users_controller.rb deleted file mode 100644 index 21ea7a8e69..0000000000 --- a/apps/workbench/app/controllers/users_controller.rb +++ /dev/null @@ -1,389 +0,0 @@ -# Copyright (C) The Arvados Authors. All rights reserved. -# -# SPDX-License-Identifier: AGPL-3.0 - -class UsersController < ApplicationController - skip_around_action :require_thread_api_token, only: :welcome - skip_before_action :check_user_agreements, only: [:welcome, :inactive, :link_account, :merge] - skip_before_action :check_user_profile, only: [:welcome, :inactive, :profile, :link_account, :merge] - skip_before_action :find_object_by_uuid, only: [:welcome, :activity, :storage] - before_action :ensure_current_user_is_admin, only: [:sudo, :unsetup, :setup] - - def show - if params[:uuid] == current_user.uuid - respond_to do |f| - f.html do - if request.url.include?("/users/#{current_user.uuid}") - super - else - redirect_to(params[:return_to] || project_path(params[:uuid])) - end - end - end - else - super - end - end - - def welcome - if current_user - redirect_to (params[:return_to] || '/') - end - end - - def inactive - if current_user.andand.is_invited - redirect_to (params[:return_to] || '/') - end - end - - def profile - params[:offer_return_to] ||= params[:return_to] - - # In a federation situation, when you get a user record using - # "current user of token" it can fetch a stale user record from - # the local cluster. So even if profile settings were just written - # to the user record on the login cluster (because the user just - # filled out the profile), those profile settings may not appear - # in the "current user" response because it is returning a cached - # record from the local cluster. - # - # In this case, explicitly fetching user record forces it to get a - # fresh record from the login cluster. - Thread.current[:user] = User.find(current_user.uuid) - end - - def activity - @breadcrumb_page_name = nil - @users = User.limit(params[:limit]).with_count("none") - @user_activity = {} - @activity = { - logins: {}, - jobs: {}, - pipeline_instances: {} - } - @total_activity = {} - @spans = [['This week', Time.now.beginning_of_week, Time.now], - ['Last week', - Time.now.beginning_of_week.advance(weeks:-1), - Time.now.beginning_of_week], - ['This month', Time.now.beginning_of_month, Time.now], - ['Last month', - 1.month.ago.beginning_of_month, - Time.now.beginning_of_month]] - @spans.each do |span, threshold_start, threshold_end| - @activity[:logins][span] = Log.select(%w(uuid modified_by_user_uuid)). - filter([[:event_type, '=', 'login'], - [:object_kind, '=', 'arvados#user'], - [:created_at, '>=', threshold_start], - [:created_at, '<', threshold_end]]).with_count("none") - @activity[:jobs][span] = Job.select(%w(uuid modified_by_user_uuid)). - filter([[:created_at, '>=', threshold_start], - [:created_at, '<', threshold_end]]).with_count("none") - @activity[:pipeline_instances][span] = PipelineInstance.select(%w(uuid modified_by_user_uuid)). - filter([[:created_at, '>=', threshold_start], - [:created_at, '<', threshold_end]]).with_count("none") - @activity.each do |type, act| - records = act[span] - @users.each do |u| - @user_activity[u.uuid] ||= {} - @user_activity[u.uuid][span + ' ' + type.to_s] ||= 0 - end - records.each do |record| - @user_activity[record.modified_by_user_uuid] ||= {} - @user_activity[record.modified_by_user_uuid][span + ' ' + type.to_s] ||= 0 - @user_activity[record.modified_by_user_uuid][span + ' ' + type.to_s] += 1 - @total_activity[span + ' ' + type.to_s] ||= 0 - @total_activity[span + ' ' + type.to_s] += 1 - end - end - end - @users = @users.sort_by do |a| - [-@user_activity[a.uuid].values.inject(:+), a.full_name] - end - # Prepend a "Total" pseudo-user to the sorted list - @user_activity[nil] = @total_activity - @users = [OpenStruct.new(uuid: nil)] + @users - end - - def storage - @breadcrumb_page_name = nil - @users = User.limit(params[:limit]).with_count("none") - @user_storage = {} - total_storage = {} - @log_date = {} - @users.each do |u| - @user_storage[u.uuid] ||= {} - storage_log = Log. - filter([[:object_uuid, '=', u.uuid], - [:event_type, '=', 'user-storage-report']]). - order(:created_at => :desc). - with_count('none'). - limit(1) - storage_log.each do |log_entry| - # We expect this block to only execute once since we specified limit(1) - @user_storage[u.uuid] = log_entry['properties'] - @log_date[u.uuid] = log_entry['event_at'] - end - total_storage.merge!(@user_storage[u.uuid]) { |k,v1,v2| v1 + v2 } - end - @users = @users.sort_by { |u| - [-@user_storage[u.uuid].values.push(0).inject(:+), u.full_name]} - # Prepend a "Total" pseudo-user to the sorted list - @users = [OpenStruct.new(uuid: nil)] + @users - @user_storage[nil] = total_storage - end - - def show_pane_list - if current_user.andand.is_admin - %w(Admin) | super - else - super - end - end - - def index_pane_list - if current_user.andand.is_admin - super | %w(Activity) - else - super - end - end - - def sudo - resp = arvados_api_client.api(ApiClientAuthorization, '', { - api_client_authorization: { - owner_uuid: @object.uuid - } - }) - redirect_to root_url(api_token: "v2/#{resp[:uuid]}/#{resp[:api_token]}") - end - - def home - @my_ssh_keys = AuthorizedKey.where(authorized_user_uuid: current_user.uuid) - @my_tag_links = {} - - @my_jobs = Job. - limit(10). - order('created_at desc'). - with_count('none'). - where(created_by: current_user.uuid) - - @my_collections = Collection. - limit(10). - order('created_at desc'). - with_count('none'). - where(created_by: current_user.uuid) - collection_uuids = @my_collections.collect &:uuid - - @persist_state = {} - collection_uuids.each do |uuid| - @persist_state[uuid] = 'cache' - end - - Link.filter([['head_uuid', 'in', collection_uuids], - ['link_class', 'in', ['tag', 'resources']]]).with_count("none") - each do |link| - case link.link_class - when 'tag' - (@my_tag_links[link.head_uuid] ||= []) << link - when 'resources' - if link.name == 'wants' - @persist_state[link.head_uuid] = 'persistent' - end - end - end - - @my_pipelines = PipelineInstance. - limit(10). - order('created_at desc'). - with_count('none'). - where(created_by: current_user.uuid) - - respond_to do |f| - f.js { render template: 'users/home.js' } - f.html { render template: 'users/home' } - end - end - - def unsetup - if current_user.andand.is_admin - @object.unsetup - end - show - end - - def setup - respond_to do |format| - if current_user.andand.is_admin - setup_params = {} - setup_params[:send_notification_email] = "#{Rails.configuration.Mail.SendUserSetupNotificationEmail}" - if params['user_uuid'] && params['user_uuid'].size>0 - setup_params[:uuid] = params['user_uuid'] - end - if params['email'] && params['email'].size>0 - user = {email: params['email']} - setup_params[:user] = user - end - if params['openid_prefix'] && params['openid_prefix'].size>0 - setup_params[:openid_prefix] = params['openid_prefix'] - end - if params['vm_uuid'] && params['vm_uuid'].size>0 - setup_params[:vm_uuid] = params['vm_uuid'] - end - - setup_resp = User.setup setup_params - if setup_resp - vm_link = nil - setup_resp[:items].each do |item| - if item[:head_kind] == "arvados#virtualMachine" - vm_link = item - break - end - end - if params[:groups] - new_groups = params[:groups].split(',').map(&:strip).select{|i| !i.empty?} - if vm_link and new_groups != vm_link[:properties][:groups] - vm_login_link = Link.where(uuid: vm_link[:uuid]) - if vm_login_link.items_available > 0 - link = vm_login_link.results.first - props = link.properties - props[:groups] = new_groups - link.save! - end - end - end - - format.js - else - self.render_error status: 422 - end - else - self.render_error status: 422 - end - end - end - - def setup_popup - @vms = VirtualMachine.all.results - - @current_selections = find_current_links @object - - respond_to do |format| - format.html - format.js - end - end - - def virtual_machines - @my_vm_logins = {} - Link.where(tail_uuid: @object.uuid, - link_class: 'permission', - name: 'can_login').with_count("none"). - each do |perm_link| - if perm_link.properties.andand[:username] - @my_vm_logins[perm_link.head_uuid] ||= [] - @my_vm_logins[perm_link.head_uuid] << perm_link.properties[:username] - end - end - @my_virtual_machines = VirtualMachine.where(uuid: @my_vm_logins.keys).with_count("none") - end - - def ssh_keys - @my_ssh_keys = AuthorizedKey.where(key_type: 'SSH', owner_uuid: @object.uuid) - end - - def add_ssh_key_popup - respond_to do |format| - format.html - format.js - end - end - - def add_ssh_key - respond_to do |format| - key_params = {'key_type' => 'SSH'} - key_params['authorized_user_uuid'] = current_user.uuid - - if params['name'] && params['name'].size>0 - key_params['name'] = params['name'].strip - end - if params['public_key'] && params['public_key'].size>0 - key_params['public_key'] = params['public_key'].strip - end - - if !key_params['name'] && params['public_key'].andand.size>0 - split_key = key_params['public_key'].split - key_params['name'] = split_key[-1] if (split_key.size == 3) - end - - new_key = AuthorizedKey.create! key_params - if new_key - format.js - else - self.render_error status: 422 - end - end - end - - def request_shell_access - logger.warn "request_access: #{params.inspect}" - params['request_url'] = request.url - RequestShellAccessReporter.send_request(current_user, params).deliver - end - - def merge - User.merge params[:new_user_token], params[:direction] - redirect_to "/" - end - - protected - - def find_current_links user - current_selections = {} - - if !user - return current_selections - end - - # oid login perm - oid_login_perms = Link.where(tail_uuid: user.email, - head_kind: 'arvados#user', - link_class: 'permission', - name: 'can_login').with_count("none") - - if oid_login_perms.any? - prefix_properties = oid_login_perms.first.properties - current_selections[:identity_url_prefix] = prefix_properties[:identity_url_prefix] - end - - # repo perm - repo_perms = Link.where(tail_uuid: user.uuid, - head_kind: 'arvados#repository', - link_class: 'permission', - name: 'can_write').with_count("none") - if repo_perms.any? - repo_uuid = repo_perms.first.head_uuid - repos = Repository.where(head_uuid: repo_uuid).with_count("none") - if repos.any? - repo_name = repos.first.name - current_selections[:repo_name] = repo_name - end - end - - # vm login perm - vm_login_perms = Link.where(tail_uuid: user.uuid, - head_kind: 'arvados#virtualMachine', - link_class: 'permission', - name: 'can_login').with_count("none") - if vm_login_perms.any? - vm_perm = vm_login_perms.first - vm_uuid = vm_perm.head_uuid - current_selections[:vm_uuid] = vm_uuid - current_selections[:groups] = vm_perm.properties[:groups].andand.join(', ') - end - - return current_selections - end - -end diff --git a/apps/workbench/app/controllers/virtual_machines_controller.rb b/apps/workbench/app/controllers/virtual_machines_controller.rb deleted file mode 100644 index c743773141..0000000000 --- a/apps/workbench/app/controllers/virtual_machines_controller.rb +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright (C) The Arvados Authors. All rights reserved. -# -# SPDX-License-Identifier: AGPL-3.0 - -class VirtualMachinesController < ApplicationController - def index - @objects ||= model_class.all - @vm_logins = {} - if @objects.andand.first - Link.where(tail_uuid: current_user.uuid, - head_uuid: @objects.collect(&:uuid), - link_class: 'permission', - name: 'can_login').with_count("none"). - each do |perm_link| - if perm_link.properties.andand[:username] - @vm_logins[perm_link.head_uuid] ||= [] - @vm_logins[perm_link.head_uuid] << perm_link.properties[:username] - end - end - @objects.each do |vm| - vm.current_user_logins = @vm_logins[vm.uuid].andand.compact || [] - end - end - super - end - - def webshell - return render_not_found if Rails.configuration.Services.WebShell.ExternalURL == URI("") - webshell_url = URI(Rails.configuration.Services.WebShell.ExternalURL) - if webshell_url.host.index("*") != nil - webshell_url.host = webshell_url.host.sub("*", @object.hostname) - else - webshell_url.path = "/#{@object.hostname}" - end - @webshell_url = webshell_url.to_s - render layout: false - end - -end diff --git a/apps/workbench/app/controllers/websocket_controller.rb b/apps/workbench/app/controllers/websocket_controller.rb deleted file mode 100644 index 35993dc20e..0000000000 --- a/apps/workbench/app/controllers/websocket_controller.rb +++ /dev/null @@ -1,14 +0,0 @@ -# Copyright (C) The Arvados Authors. All rights reserved. -# -# SPDX-License-Identifier: AGPL-3.0 - -class WebsocketController < ApplicationController - skip_before_action :find_objects_for_index, raise: false - - def index - end - - def model_class - "Websocket" - end -end diff --git a/apps/workbench/app/controllers/work_unit_templates_controller.rb b/apps/workbench/app/controllers/work_unit_templates_controller.rb deleted file mode 100644 index 0376590a55..0000000000 --- a/apps/workbench/app/controllers/work_unit_templates_controller.rb +++ /dev/null @@ -1,36 +0,0 @@ -# Copyright (C) The Arvados Authors. All rights reserved. -# -# SPDX-License-Identifier: AGPL-3.0 - -class WorkUnitTemplatesController < ApplicationController - def find_objects_for_index - return if !params[:partial] - - @limit = 40 - @filters = @filters || [] - - # get next page of pipeline_templates - if PipelineTemplate.api_exists?(:index) - filters = @filters + [["uuid", "is_a", ["arvados#pipelineTemplate"]]] - pipelines = PipelineTemplate.limit(@limit).with_count("none").order(["created_at desc"]).filter(filters) - end - - # get next page of workflows - filters = @filters + [["uuid", "is_a", ["arvados#workflow"]]] - workflows = Workflow.limit(@limit).order(["created_at desc"]).with_count("none").filter(filters) - - @objects = (pipelines.to_a + workflows.to_a).sort_by(&:created_at).reverse.first(@limit) - - if @objects.any? - @next_page_filters = next_page_filters('<=') - @next_page_href = url_for(partial: :choose_rows, - filters: @next_page_filters.to_json) - else - @next_page_href = nil - end - end - - def next_page_href with_params={} - @next_page_href - end -end diff --git a/apps/workbench/app/controllers/work_units_controller.rb b/apps/workbench/app/controllers/work_units_controller.rb deleted file mode 100644 index 86e3cdd91d..0000000000 --- a/apps/workbench/app/controllers/work_units_controller.rb +++ /dev/null @@ -1,224 +0,0 @@ -# Copyright (C) The Arvados Authors. All rights reserved. -# -# SPDX-License-Identifier: AGPL-3.0 - -class WorkUnitsController < ApplicationController - skip_around_action :require_thread_api_token, if: proc { |ctrl| - !Rails.configuration.Users.AnonymousUserToken.empty? and - 'show_child_component' == ctrl.action_name - } - - def find_objects_for_index - # If it's not the index rows partial display, just return - # The /index request will again be invoked to display the - # partial at which time, we will be using the objects found. - return if !params[:partial] - - @limit = 20 - @filters = @filters || [] - - pipelines = [] - jobs = [] - - # get next page of pipeline_instances - if PipelineInstance.api_exists?(:index) - filters = @filters + [["uuid", "is_a", ["arvados#pipelineInstance"]]] - pipelines = PipelineInstance.limit(@limit).order(["created_at desc"]).filter(filters).with_count("none") - end - - if params[:show_children] - # get next page of jobs - if Job.api_exists?(:index) - filters = @filters + [["uuid", "is_a", ["arvados#job"]]] - jobs = Job.limit(@limit).order(["created_at desc"]).filter(filters).with_count("none") - end - end - - # get next page of container_requests - filters = @filters + [["uuid", "is_a", ["arvados#containerRequest"]]] - if !params[:show_children] - filters << ["requesting_container_uuid", "=", nil] - end - crs = ContainerRequest.limit(@limit).order(["created_at desc"]).filter(filters).with_count("none") - @objects = (jobs.to_a + pipelines.to_a + crs.to_a).sort_by(&:created_at).reverse.first(@limit) - - if @objects.any? - @next_page_filters = next_page_filters('<=') - @next_page_href = url_for(partial: :all_processes_rows, - filters: @next_page_filters.to_json, - show_children: params[:show_children]) - preload_links_for_objects(@objects.to_a) - else - @next_page_href = nil - end - end - - def next_page_href with_params={} - @next_page_href - end - - def create - template_uuid = params['work_unit']['template_uuid'] - - attrs = {} - rc = resource_class_for_uuid(template_uuid) - if rc == PipelineTemplate - model_class = PipelineInstance - attrs['pipeline_template_uuid'] = template_uuid - elsif rc == Workflow - # workflow json - workflow = Workflow.find? template_uuid - if workflow.definition - begin - wf_json = ActiveSupport::HashWithIndifferentAccess.new YAML::load(workflow.definition) - rescue => e - logger.error "Error converting definition yaml to json: #{e.message}" - raise ArgumentError, "Error converting definition yaml to json: #{e.message}" - end - end - - model_class = ContainerRequest - - attrs['name'] = "#{workflow['name']} container" if workflow['name'].present? - attrs['properties'] = {'template_uuid' => template_uuid} - attrs['priority'] = 1 - attrs['state'] = "Uncommitted" - attrs['use_existing'] = false - - # required - attrs['container_image'] = "arvados/jobs" - attrs['cwd'] = "/var/spool/cwl" - attrs['output_path'] = "/var/spool/cwl" - - # runtime constriants - runtime_constraints = { - "vcpus" => 1, - "ram" => 1024 * 1024 * 1024, - "API" => true - } - - keep_cache = 256 - input_defaults = {} - if wf_json - main = get_cwl_main(wf_json) - main[:inputs].each do |input| - if input[:default] - input_defaults[cwl_shortname(input[:id])] = input[:default] - end - end - if main[:hints] - main[:hints].each do |hint| - if hint[:class] == "http://arvados.org/cwl#WorkflowRunnerResources" - if hint[:coresMin] - runtime_constraints["vcpus"] = hint[:coresMin] - end - if hint[:ramMin] - runtime_constraints["ram"] = hint[:ramMin] * 1024 * 1024 - end - if hint[:keep_cache] - keep_cache = hint[:keep_cache] - end - if hint[:acrContainerImage] - attrs['container_image'] = hint[:acrContainerImage] - end - end - end - end - end - - attrs['command'] = ["arvados-cwl-runner", - "--enable-reuse", - "--local", - "--api=containers", - "--project-uuid=#{params['work_unit']['owner_uuid']}", - "--collection-cache-size=#{keep_cache}", - "/var/lib/cwl/workflow.json#main", - "/var/lib/cwl/cwl.input.json"] - - # mounts - mounts = { - "/var/lib/cwl/cwl.input.json" => { - "kind" => "json", - "content" => input_defaults - }, - "stdout" => { - "kind" => "file", - "path" => "/var/spool/cwl/cwl.output.json" - }, - "/var/spool/cwl" => { - "kind" => "collection", - "writable" => true - } - } - if wf_json - mounts["/var/lib/cwl/workflow.json"] = { - "kind" => "json", - "content" => wf_json - } - end - attrs['mounts'] = mounts - - attrs['runtime_constraints'] = runtime_constraints - else - raise ArgumentError, "Unsupported template uuid: #{template_uuid}" - end - - attrs['owner_uuid'] = params['work_unit']['owner_uuid'] - @object ||= model_class.new attrs - - if @object.save - redirect_to @object - else - render_error status: 422 - end - end - - def find_object_by_uuid - if params['object_type'] - @object = params['object_type'].constantize.find(params['uuid']) - else - super - end - end - - def show_child_component - data = JSON.load(params[:action_data]) - - current_obj = {} - current_obj_uuid = data['current_obj_uuid'] - current_obj_name = data['current_obj_name'] - current_obj_type = data['current_obj_type'] - current_obj_parent = data['current_obj_parent'] - if current_obj_uuid - resource_class = resource_class_for_uuid current_obj_uuid - obj = object_for_dataclass(resource_class, current_obj_uuid) - current_obj = obj if obj - end - - if current_obj.is_a?(Hash) and !current_obj.any? - if current_obj_parent - resource_class = resource_class_for_uuid current_obj_parent - parent = object_for_dataclass(resource_class, current_obj_parent) - parent_wu = parent.work_unit - children = parent_wu.children - if current_obj_uuid - wu = children.select {|c| c.uuid == current_obj_uuid}.first - else current_obj_name - wu = children.select {|c| c.label.to_s == current_obj_name}.first - end - end - else - if current_obj_type == JobWorkUnit.to_s - wu = JobWorkUnit.new(current_obj, current_obj_name, current_obj_parent) - elsif current_obj_type == PipelineInstanceWorkUnit.to_s - wu = PipelineInstanceWorkUnit.new(current_obj, current_obj_name, current_obj_parent) - elsif current_obj_type == ContainerWorkUnit.to_s - wu = ContainerWorkUnit.new(current_obj, current_obj_name, current_obj_parent) - end - end - - respond_to do |f| - f.html { render(partial: "show_component", locals: {wu: wu}) } - end - end -end diff --git a/apps/workbench/app/controllers/workflows_controller.rb b/apps/workbench/app/controllers/workflows_controller.rb deleted file mode 100644 index 4d78ca7ed9..0000000000 --- a/apps/workbench/app/controllers/workflows_controller.rb +++ /dev/null @@ -1,14 +0,0 @@ -# Copyright (C) The Arvados Authors. All rights reserved. -# -# SPDX-License-Identifier: AGPL-3.0 - -class WorkflowsController < ApplicationController - skip_around_action :require_thread_api_token, if: proc { |ctrl| - !Rails.configuration.Users.AnonymousUserToken.empty? and - 'show' == ctrl.action_name - } - - def show_pane_list - %w(Definition Advanced) - end -end diff --git a/apps/workbench/app/helpers/application_helper.rb b/apps/workbench/app/helpers/application_helper.rb deleted file mode 100644 index 697c469b56..0000000000 --- a/apps/workbench/app/helpers/application_helper.rb +++ /dev/null @@ -1,701 +0,0 @@ -# Copyright (C) The Arvados Authors. All rights reserved. -# -# SPDX-License-Identifier: AGPL-3.0 - -module ApplicationHelper - def current_user - controller.current_user - end - - def self.match_uuid(uuid) - /^([0-9a-z]{5})-([0-9a-z]{5})-([0-9a-z]{15})$/.match(uuid.to_s) - end - - def current_api_host - if Rails.configuration.Services.Controller.ExternalURL.port == 443 - "#{Rails.configuration.Services.Controller.ExternalURL.hostname}" - else - "#{Rails.configuration.Services.Controller.ExternalURL.hostname}:#{Rails.configuration.Services.Controller.ExternalURL.port}" - end - end - - def current_uuid_prefix - Rails.configuration.ClusterID - end - - def render_markup(markup) - allowed_tags = Rails::Html::Sanitizer.white_list_sanitizer.allowed_tags + %w(table tbody th tr td col colgroup caption thead tfoot) - sanitize(raw(RedCloth.new(markup.to_s).to_html(:refs_arvados, :textile)), tags: allowed_tags) if markup - end - - def human_readable_bytes_html(n) - return h(n) unless n.is_a? Integer - return "0 bytes" if (n == 0) - - orders = { - 1 => "bytes", - 1024 => "KiB", - (1024*1024) => "MiB", - (1024*1024*1024) => "GiB", - (1024*1024*1024*1024) => "TiB" - } - - orders.each do |k, v| - sig = (n.to_f/k) - if sig >=1 and sig < 1024 - if v == 'bytes' - return "%i #{v}" % sig - else - return "%0.1f #{v}" % sig - end - end - end - - return h(n) - end - - def resource_class_for_uuid(attrvalue, opts={}) - ArvadosBase::resource_class_for_uuid(attrvalue, opts) - end - - # When using {remote:true}, or using {method:...} to use an HTTP - # method other than GET, move the target URI from href to - # data-remote-href. Otherwise, browsers offer features like "open in - # new window" and "copy link address" which bypass Rails' click - # handler and therefore end up at incorrect/nonexistent routes (by - # ignoring data-method) and expect to receive pages rather than - # javascript responses. - # - # See assets/javascripts/link_to_remote.js for supporting code. - def link_to *args, &block - if (args.last and args.last.is_a? Hash and - (args.last[:remote] or - (args.last[:method] and - args.last[:method].to_s.upcase != 'GET'))) - if Rails.env.test? - # Capybara/phantomjs can't click_link without an href, even if - # the click handler means it never gets used. - raw super.gsub(' href="', ' href="#" data-remote-href="') - else - # Regular browsers work as desired: users can click A elements - # without hrefs, and click handlers fire; but there's no "copy - # link address" option in the right-click menu. - raw super.gsub(' href="', ' data-remote-href="') - end - else - super - end - end - - ## - # Returns HTML that links to the Arvados object specified in +attrvalue+ - # Provides various output control and styling options. - # - # +attrvalue+ an Arvados model object or uuid - # - # +opts+ a set of flags to control output: - # - # [:link_text] the link text to use (may include HTML), overrides everything else - # - # [:friendly_name] whether to use the "friendly" name in the link text (by - # calling #friendly_link_name on the object), otherwise use the uuid - # - # [:with_class_name] prefix the link text with the class name of the model - # - # [:no_tags] disable tags in the link text (default is to show tags). - # Currently tags are only shown for Collections. - # - # [:thumbnail] if the object is a collection, show an image thumbnail if the - # collection consists of a single image file. - # - # [:no_link] don't create a link, just return the link text - # - # +style_opts+ additional HTML properties for the anchor tag, passed to link_to - # - def link_to_if_arvados_object(attrvalue, opts={}, style_opts={}) - if (resource_class = resource_class_for_uuid(attrvalue, opts)) - if attrvalue.is_a? ArvadosBase - object = attrvalue - link_uuid = attrvalue.uuid - else - object = nil - link_uuid = attrvalue - end - link_name = opts[:link_text] - tags = "" - if !link_name - link_name = object.andand.default_name || resource_class.default_name - - if opts[:friendly_name] - if attrvalue.respond_to? :friendly_link_name - link_name = attrvalue.friendly_link_name opts[:lookup] - else - begin - if resource_class.name == 'Collection' - if CollectionsHelper.match(link_uuid) - link_name = collection_for_pdh(link_uuid).andand.first.andand.portable_data_hash - else - link_name = collections_for_object(link_uuid).andand.first.andand.friendly_link_name - end - else - link_name = object_for_dataclass(resource_class, link_uuid).andand.friendly_link_name - end - rescue ArvadosApiClient::NotFoundException - # If that lookup failed, the link will too. So don't make one. - return attrvalue - end - end - end - if link_name.nil? or link_name.empty? - link_name = attrvalue - end - if opts[:with_class_name] - link_name = "#{resource_class.to_s}: #{link_name}" - end - if !opts[:no_tags] and resource_class == Collection - links_for_object(link_uuid).each do |tag| - if tag.link_class.in? ["tag", "identifier"] - tags += ' ' - tags += link_to tag.name, controller: "links", filters: [["link_class", "=", "tag"], ["name", "=", tag.name]].to_json - tags += '' - end - end - end - if opts[:thumbnail] and resource_class == Collection - # add an image thumbnail if the collection consists of a single image file. - collections_for_object(link_uuid).each do |c| - if c.files.length == 1 and CollectionsHelper::is_image c.files.first[1] - link_name += " " - link_name += image_tag "#{url_for c}/#{CollectionsHelper::file_path c.files.first}", style: "height: 4em; width: auto" - end - end - end - end - style_opts[:class] = (style_opts[:class] || '') + ' nowrap' - if opts[:no_link] or (resource_class == User && !current_user) - raw(link_name) - else - controller_class = resource_class.to_s.tableize - if controller_class.eql?('groups') and (object.andand.group_class.eql?('project') or object.andand.group_class.eql?('filter')) - controller_class = 'projects' - end - (link_to raw(link_name), { controller: controller_class, action: 'show', id: ((opts[:name_link].andand.uuid) || link_uuid) }, style_opts) + raw(tags) - end - else - # just return attrvalue if it is not recognizable as an Arvados object or uuid. - if attrvalue.nil? or (attrvalue.is_a? String and attrvalue.empty?) - "(none)" - else - attrvalue - end - end - end - - def link_to_arvados_object_if_readable(attrvalue, link_text_if_not_readable, opts={}) - resource_class = resource_class_for_uuid(attrvalue.split('/')[0]) if attrvalue.is_a?(String) - if !resource_class - return link_to_if_arvados_object attrvalue, opts - end - - readable = object_readable attrvalue, resource_class - if readable - link_to_if_arvados_object attrvalue, opts - elsif opts[:required] and current_user # no need to show this for anonymous user - raw('
    ') - else - link_text_if_not_readable - end - end - - # This method takes advantage of preloaded collections and objects. - # Hence you can improve performance by first preloading objects - # related to the page context before using this method. - def object_readable attrvalue, resource_class=nil - # if it is a collection filename, check readable for the locator - attrvalue = attrvalue.split('/')[0] if attrvalue - - resource_class = resource_class_for_uuid(attrvalue) if resource_class.nil? - return if resource_class.nil? - - return_value = nil - if resource_class.to_s == 'Collection' - if CollectionsHelper.match(attrvalue) - found = collection_for_pdh(attrvalue) - return_value = found.first if found.any? - else - found = collections_for_object(attrvalue) - return_value = found.first if found.any? - end - else - return_value = object_for_dataclass(resource_class, attrvalue) - end - return_value - end - - # Render an editable attribute with the attrvalue of the attr. - # The htmloptions are added to the editable element's list of attributes. - # The nonhtml_options are only used to customize the display of the element. - def render_editable_attribute(object, attr, attrvalue=nil, htmloptions={}, nonhtml_options={}) - attrvalue = object.send(attr) if attrvalue.nil? - if not object.attribute_editable?(attr) - if attrvalue && attrvalue.length > 0 - return render_attribute_as_textile( object, attr, attrvalue, false ) - else - return (attr == 'name' and object.andand.default_name) || - '(none)' - end - end - - input_type = 'text' - opt_selection = nil - attrtype = object.class.attribute_info[attr.to_sym].andand[:type] - if attrtype == 'text' or attr == 'description' - input_type = 'textarea' - elsif attrtype == 'datetime' - input_type = 'date' - elsif attrtype == 'boolean' - input_type = 'select' - opt_selection = ([{value: "true", text: "true"}, {value: "false", text: "false"}]).to_json - else - input_type = 'text' - end - - attrvalue = attrvalue.to_json if attrvalue.is_a? Hash or attrvalue.is_a? Array - rendervalue = render_attribute_as_textile( object, attr, attrvalue, false ) - - ajax_options = { - "data-pk" => { - id: object.uuid, - key: object.class.to_s.underscore - } - } - if object.uuid - ajax_options['data-url'] = url_for(action: "update", id: object.uuid, controller: object.class.to_s.pluralize.underscore) - else - ajax_options['data-url'] = url_for(action: "create", controller: object.class.to_s.pluralize.underscore) - ajax_options['data-pk'][:defaults] = object.attributes - end - ajax_options['data-pk'] = ajax_options['data-pk'].to_json - @unique_id ||= (Time.now.to_f*1000000).to_i - span_id = object.uuid.to_s + '-' + attr.to_s + '-' + (@unique_id += 1).to_s - - span_tag = content_tag 'span', rendervalue, { - "data-emptytext" => '(none)', - "data-placement" => "bottom", - "data-type" => input_type, - "data-source" => opt_selection, - "data-title" => "Edit #{attr.to_s.gsub '_', ' '}", - "data-name" => htmloptions['selection_name'] || attr, - "data-object-uuid" => object.uuid, - "data-toggle" => "manual", - "data-value" => htmloptions['data-value'] || attrvalue, - "id" => span_id, - :class => "editable #{is_textile?( object, attr ) ? 'editable-textile' : ''}" - }.merge(htmloptions).merge(ajax_options) - - edit_tiptitle = 'edit' - edit_tiptitle = 'Warning: do not use hyphens in the repository name as they will be stripped' if (object.class.to_s == 'Repository' and attr == 'name') - - edit_button = raw('' + (nonhtml_options[:btntext] || '') + '') - - if nonhtml_options[:btnplacement] == :left - edit_button + ' ' + span_tag - elsif nonhtml_options[:btnplacement] == :top - edit_button + raw('
    ') + span_tag - else - span_tag + ' ' + edit_button - end - end - - def render_pipeline_component_attribute(object, attr, subattr, value_info, htmloptions={}) - datatype = nil - required = true - attrvalue = value_info - - if value_info.is_a? Hash - if value_info[:output_of] - return raw("#{value_info[:output_of]}") - end - if value_info[:dataclass] - dataclass = value_info[:dataclass] - end - if value_info[:optional] != nil - required = (value_info[:optional] != "true") - end - if value_info[:required] != nil - required = value_info[:required] - end - - # Pick a suitable attrvalue to show as the current value (i.e., - # the one that would be used if we ran the pipeline right now). - if value_info[:value] - attrvalue = value_info[:value] - elsif value_info[:default] - attrvalue = value_info[:default] - else - attrvalue = '' - end - preconfigured_search_str = value_info[:search_for] - end - - if not object.andand.attribute_editable?(attr) - return link_to_arvados_object_if_readable(attrvalue, attrvalue, {friendly_name: true, required: required}) - end - - if dataclass - begin - dataclass = dataclass.constantize - rescue NameError - end - else - dataclass = ArvadosBase.resource_class_for_uuid(attrvalue) - end - - id = "#{object.uuid}-#{subattr.join('-')}" - dn = "[#{attr}]" - subattr.each do |a| - dn += "[#{a}]" - end - if value_info.is_a? Hash - dn += '[value]' - end - - if (dataclass == Collection) or (dataclass == File) - selection_param = object.class.to_s.underscore + dn - display_value = attrvalue - if value_info.is_a?(Hash) - if (link = Link.find? value_info[:link_uuid]) - display_value = link.name - elsif value_info[:link_name] - display_value = value_info[:link_name] - elsif (sn = value_info[:selection_name]) && sn != "" - display_value = sn - end - end - if (attr == :components) and (subattr.size > 2) - chooser_title = "Choose a #{dataclass == Collection ? 'dataset' : 'file'} for #{object.component_input_title(subattr[0], subattr[2])}:" - else - chooser_title = "Choose a #{dataclass == Collection ? 'dataset' : 'file'}:" - end - modal_path = choose_collections_path \ - ({ title: chooser_title, - filters: [['owner_uuid', '=', object.owner_uuid]].to_json, - action_name: 'OK', - action_href: pipeline_instance_path(id: object.uuid), - action_method: 'patch', - preconfigured_search_str: (preconfigured_search_str || ""), - action_data: { - merge: true, - use_preview_selection: dataclass == File ? true : nil, - selection_param: selection_param, - success: 'page-refresh' - }.to_json, - }) - - return content_tag('div', :class => 'input-group') do - html = text_field_tag(dn, display_value, - :class => - "form-control #{'required' if required} #{'unreadable-input' if attrvalue.present? and !object_readable(attrvalue, Collection)}") - html + content_tag('span', :class => 'input-group-btn') do - link_to('Choose', - modal_path, - { :class => "btn btn-primary", - :remote => true, - :method => 'get', - }) - end - end - end - - if attrvalue.is_a? String - datatype = 'text' - elsif attrvalue.is_a?(Array) or dataclass.andand.is_a?(Class) - # TODO: find a way to edit with x-editable - return attrvalue - end - - # When datatype is a String or Fixnum, link_to the attrvalue - lt = link_to attrvalue, '#', { - "data-emptytext" => "none", - "data-placement" => "bottom", - "data-type" => datatype, - "data-url" => url_for(action: "update", id: object.uuid, controller: object.class.to_s.pluralize.underscore, merge: true), - "data-title" => "Set value for #{subattr[-1].to_s}", - "data-name" => dn, - "data-pk" => "{id: \"#{object.uuid}\", key: \"#{object.class.to_s.underscore}\"}", - "data-value" => attrvalue, - # "clear" button interferes with form-control's up/down arrows - "data-clear" => false, - :class => "editable #{'required' if required} form-control", - :id => id - }.merge(htmloptions) - - lt - end - - def get_cwl_main(workflow) - if workflow[:"$graph"].nil? - return workflow - else - workflow[:"$graph"].each do |tool| - if tool[:id] == "#main" - return tool - end - end - end - end - - def get_cwl_inputs(workflow) - get_cwl_main(workflow)[:inputs] - end - - - def cwl_shortname(id) - if id[0] == "#" - id = id[1..-1] - end - return id.split("/")[-1] - end - - def cwl_input_info(input_schema) - required = !(input_schema[:type].include? "null") - if input_schema[:type].is_a? Array - primary_type = input_schema[:type].select { |n| n != "null" }[0] - elsif input_schema[:type].is_a? String - primary_type = input_schema[:type] - elsif input_schema[:type].is_a? Hash - primary_type = input_schema[:type] - end - param_id = cwl_shortname(input_schema[:id]) - return required, primary_type, param_id - end - - def cwl_input_value(object, input_schema, set_attr_path) - dn = "" - attrvalue = object - set_attr_path.each do |a| - dn += "[#{a}]" - attrvalue = attrvalue[a.to_sym] - end - return dn, attrvalue - end - - def cwl_inputs_required(object, inputs_schema, set_attr_path) - r = 0 - inputs_schema.each do |input| - required, _, param_id = cwl_input_info(input) - _, attrvalue = cwl_input_value(object, input, set_attr_path + [param_id]) - r += 1 if required and attrvalue.nil? - end - r - end - - def render_cwl_input(object, input_schema, set_attr_path, htmloptions={}) - required, primary_type, param_id = cwl_input_info(input_schema) - - dn, attrvalue = cwl_input_value(object, input_schema, set_attr_path + [param_id]) - attrvalue = if attrvalue.nil? then "" else attrvalue end - - id = "#{object.uuid}-#{param_id}" - - opt_empty_selection = if required then [] else [{value: "", text: ""}] end - - if ["Directory", "File"].include? primary_type - chooser_title = "Choose a #{primary_type == 'Directory' ? 'dataset' : 'file'}:" - selection_param = object.class.to_s.underscore + dn - if attrvalue.is_a? Hash - display_value = attrvalue[:"http://arvados.org/cwl#collectionUUID"] || attrvalue[:"arv:collection"] || attrvalue[:location] - re = CollectionsHelper.match_uuid_with_optional_filepath(display_value) - locationre = CollectionsHelper.match(attrvalue[:location][5..-1]) - if re - if locationre and locationre[4] - display_value = "#{Collection.find(re[1]).name} / #{locationre[4][1..-1]}" - else - display_value = Collection.find(re[1]).name - end - end - end - modal_path = choose_collections_path \ - ({ title: chooser_title, - filters: [['owner_uuid', '=', object.owner_uuid]].to_json, - action_name: 'OK', - action_href: container_request_path(id: object.uuid), - action_method: 'patch', - preconfigured_search_str: "", - action_data: { - merge: true, - use_preview_selection: primary_type == 'File' ? true : nil, - selection_param: selection_param, - success: 'page-refresh' - }.to_json, - }) - - return content_tag('div', :class => 'input-group') do - html = text_field_tag(dn, display_value, - :class => - "form-control #{'required' if required}") - html + content_tag('span', :class => 'input-group-btn') do - link_to('Choose', - modal_path, - { :class => "btn btn-primary", - :remote => true, - :method => 'get', - }) - end - end - elsif "boolean" == primary_type - return link_to attrvalue.to_s, '#', { - "data-emptytext" => "none", - "data-placement" => "bottom", - "data-type" => "select", - "data-source" => (opt_empty_selection + [{value: "true", text: "true"}, {value: "false", text: "false"}]).to_json, - "data-url" => url_for(action: "update", id: object.uuid, controller: object.class.to_s.pluralize.underscore, merge: true), - "data-title" => "Set value for #{cwl_shortname(input_schema[:id])}", - "data-name" => dn, - "data-pk" => "{id: \"#{object.uuid}\", key: \"#{object.class.to_s.underscore}\"}", - "data-value" => attrvalue.to_s, - # "clear" button interferes with form-control's up/down arrows - "data-clear" => false, - :class => "editable #{'required' if required} form-control", - :id => id - }.merge(htmloptions) - elsif primary_type.is_a? Hash and primary_type[:type] == "enum" - return link_to attrvalue, '#', { - "data-emptytext" => "none", - "data-placement" => "bottom", - "data-type" => "select", - "data-source" => (opt_empty_selection + primary_type[:symbols].map {|i| {:value => cwl_shortname(i), :text => cwl_shortname(i)} }).to_json, - "data-url" => url_for(action: "update", id: object.uuid, controller: object.class.to_s.pluralize.underscore, merge: true), - "data-title" => "Set value for #{cwl_shortname(input_schema[:id])}", - "data-name" => dn, - "data-pk" => "{id: \"#{object.uuid}\", key: \"#{object.class.to_s.underscore}\"}", - "data-value" => attrvalue, - # "clear" button interferes with form-control's up/down arrows - "data-clear" => false, - :class => "editable #{'required' if required} form-control", - :id => id - }.merge(htmloptions) - elsif primary_type.is_a? String - if ["int", "long"].include? primary_type - datatype = "number" - else - datatype = "text" - end - - return link_to attrvalue, '#', { - "data-emptytext" => "none", - "data-placement" => "bottom", - "data-type" => datatype, - "data-url" => url_for(action: "update", id: object.uuid, controller: object.class.to_s.pluralize.underscore, merge: true), - "data-title" => "Set value for #{cwl_shortname(input_schema[:id])}", - "data-name" => dn, - "data-pk" => "{id: \"#{object.uuid}\", key: \"#{object.class.to_s.underscore}\"}", - "data-value" => attrvalue, - # "clear" button interferes with form-control's up/down arrows - "data-clear" => false, - :class => "editable #{'required' if required} form-control", - :id => id - }.merge(htmloptions) - else - return "Unable to render editing control for parameter type #{primary_type}" - end - end - - def render_arvados_object_list_start(list, button_text, button_href, - params={}, *rest, &block) - show_max = params.delete(:show_max) || 3 - params[:class] ||= 'btn btn-xs btn-default' - list[0...show_max].each { |item| yield item } - unless list[show_max].nil? - link_to(h(button_text) + - raw('   '), - button_href, params, *rest) - end - end - - def render_controller_partial partial, opts - cname = opts.delete :controller_name - begin - render opts.merge(partial: "#{cname}/#{partial}") - rescue ActionView::MissingTemplate - render opts.merge(partial: "application/#{partial}") - end - end - - RESOURCE_CLASS_ICONS = { - "Collection" => "fa-archive", - "ContainerRequest" => "fa-gears", - "Group" => "fa-users", - "Human" => "fa-male", # FIXME: Use a more inclusive icon. - "Job" => "fa-gears", - "KeepDisk" => "fa-hdd-o", - "KeepService" => "fa-exchange", - "Link" => "fa-arrows-h", - "Node" => "fa-cloud", - "PipelineInstance" => "fa-gears", - "PipelineTemplate" => "fa-gears", - "Repository" => "fa-code-fork", - "Specimen" => "fa-flask", - "Trait" => "fa-clipboard", - "User" => "fa-user", - "VirtualMachine" => "fa-terminal", - "Workflow" => "fa-gears", - } - DEFAULT_ICON_CLASS = "fa-cube" - - def fa_icon_class_for_class(resource_class, default=DEFAULT_ICON_CLASS) - RESOURCE_CLASS_ICONS.fetch(resource_class.to_s, default) - end - - def fa_icon_class_for_uuid(uuid, default=DEFAULT_ICON_CLASS) - fa_icon_class_for_class(resource_class_for_uuid(uuid), default) - end - - def fa_icon_class_for_object(object, default=DEFAULT_ICON_CLASS) - case class_name = object.class.to_s - when "Group" - object.group_class ? 'fa-folder' : 'fa-users' - else - RESOURCE_CLASS_ICONS.fetch(class_name, default) - end - end - - def chooser_preview_url_for object, use_preview_selection=false - case object.class.to_s - when 'Collection' - polymorphic_path(object, tab_pane: 'chooser_preview', use_preview_selection: use_preview_selection) - else - nil - end - end - - def render_attribute_as_textile( object, attr, attrvalue, truncate ) - if attrvalue && (is_textile? object, attr) - markup = render_markup attrvalue - markup = markup[0,markup.index('

    ')+4] if (truncate && markup.index('

    ')) - return markup - else - return attrvalue - end - end - - def render_localized_date(date, opts="") - raw("#{date}") - end - - def render_time duration, use_words, round_to_min=true - render_runtime duration, use_words, round_to_min - end - - # Keep locators are expected to be of the form \"...\" or \"...\" - JSON_KEEP_LOCATOR_REGEXP = /([0-9a-f]{32}\+\d+[^'"]*|[a-z0-9]{5}-4zz18-[a-z0-9]{15}[^'"]*)(?=['"]|\z|$)/ - def keep_locator_in_json str - # Return a list of all matches - str.scan(JSON_KEEP_LOCATOR_REGEXP).flatten - end - -private - def is_textile?( object, attr ) - object.textile_attributes.andand.include?(attr) - end -end diff --git a/apps/workbench/app/helpers/arvados_api_client_helper.rb b/apps/workbench/app/helpers/arvados_api_client_helper.rb deleted file mode 100644 index 929b64923e..0000000000 --- a/apps/workbench/app/helpers/arvados_api_client_helper.rb +++ /dev/null @@ -1,17 +0,0 @@ -# Copyright (C) The Arvados Authors. All rights reserved. -# -# SPDX-License-Identifier: AGPL-3.0 - -module ArvadosApiClientHelper - def arvados_api_client - ArvadosApiClient.new_or_current - end -end - -# For the benefit of themes that still expect $arvados_api_client to work: -class ArvadosClientProxyHack - def method_missing *args - ArvadosApiClient.new_or_current.send(*args) - end -end -$arvados_api_client = ArvadosClientProxyHack.new diff --git a/apps/workbench/app/helpers/collections_helper.rb b/apps/workbench/app/helpers/collections_helper.rb deleted file mode 100644 index 0c89ca8783..0000000000 --- a/apps/workbench/app/helpers/collections_helper.rb +++ /dev/null @@ -1,81 +0,0 @@ -# Copyright (C) The Arvados Authors. All rights reserved. -# -# SPDX-License-Identifier: AGPL-3.0 - -module CollectionsHelper - def d3ify_links(links) - links.collect do |x| - {source: x.tail_uuid, target: x.head_uuid, type: x.name} - end - end - - ## - # Regex match for collection portable data hash, returns a regex match object with the - # hash in group 1, (optional) size in group 2, (optional) subsequent uuid - # fields in group 3, and (optional) file path within the collection as group - # 4 - # returns nil for no match. - # - # +pdh+ the portable data hash string to match - # - def self.match(pdh) - /^([a-f0-9]{32})(\+\d+)(\+[^+]+)*?(\/.*)?$/.match(pdh.to_s) - end - - ## - # Regex match for collection UUIDs, returns a regex match object with the - # uuid in group 1, empty groups 2 and 3 (for consistency with the match - # method above), and (optional) file path within the collection as group - # 4. - # returns nil for no match. - # - def self.match_uuid_with_optional_filepath(uuid_with_optional_file) - /^([0-9a-z]{5}-4zz18-[0-9a-z]{15})()()(\/.*)?$/.match(uuid_with_optional_file.to_s) - end - - ## - # Regex match for common image file extensions, returns a regex match object - # with the matched extension in group 1; or nil for no match. - # - # +file+ the file string to match - # - def self.is_image file - /\.(jpg|jpeg|gif|png|svg)$/i.match(file) - end - - ## - # Generates a relative file path than can be appended to the URL of a - # collection to get a file download link without adding a spurious ./ at the - # beginning for files in the default stream. - # - # +file+ an entry in the Collection.files list in the form [stream, name, size] - # - def self.file_path file - f0 = file[0] - f0 = '' if f0 == '.' - f0 = f0[2..-1] if f0[0..1] == './' - f0 += '/' if not f0.empty? - "#{f0}#{file[1]}" - end - - ## - # Check if collection preview is allowed for the given filename with extension - # - def preview_allowed_for file_name - file_type = MIME::Types.type_for(file_name).first - if file_type.nil? - if file_name.downcase.end_with?('.cwl') # unknown mime type, but we support preview - true - else - false - end - elsif (file_type.raw_media_type == "text") || (file_type.raw_media_type == "image") - true - elsif (file_type.raw_media_type == "application") && - Rails.configuration.Workbench.ApplicationMimetypesWithViewIcon[file_type.sub_type] - true - else - false - end - end -end diff --git a/apps/workbench/app/helpers/pipeline_components_helper.rb b/apps/workbench/app/helpers/pipeline_components_helper.rb deleted file mode 100644 index 702772ce98..0000000000 --- a/apps/workbench/app/helpers/pipeline_components_helper.rb +++ /dev/null @@ -1,20 +0,0 @@ -# Copyright (C) The Arvados Authors. All rights reserved. -# -# SPDX-License-Identifier: AGPL-3.0 - -module PipelineComponentsHelper - def render_pipeline_components(template_suffix, fallback=nil, locals={}) - begin - render(partial: "pipeline_instances/show_components_#{template_suffix}", - locals: locals) - rescue => e - logger.error "#{e.inspect}" - logger.error "#{e.backtrace.join("\n\t")}" - case fallback - when :json - render(partial: "pipeline_instances/show_components_json", - locals: {error_name: e.inspect, backtrace: e.backtrace.join("\n\t")}) - end - end - end -end diff --git a/apps/workbench/app/helpers/pipeline_instances_helper.rb b/apps/workbench/app/helpers/pipeline_instances_helper.rb deleted file mode 100644 index 8e89331cb6..0000000000 --- a/apps/workbench/app/helpers/pipeline_instances_helper.rb +++ /dev/null @@ -1,319 +0,0 @@ -# Copyright (C) The Arvados Authors. All rights reserved. -# -# SPDX-License-Identifier: AGPL-3.0 - -module PipelineInstancesHelper - - def pipeline_jobs object=nil - object ||= @object - if object.components[:steps].is_a? Array - pipeline_jobs_oldschool object - elsif object.components.is_a? Hash - pipeline_jobs_newschool object - end - end - - def render_pipeline_jobs - pipeline_jobs.collect do |pj| - render_pipeline_job pj - end - end - - def render_pipeline_job pj - pj[:progress_bar] = render partial: 'job_progress', locals: {:j => pj[:job]} - pj[:output_link] = link_to_if_arvados_object pj[:output] - pj[:job_link] = link_to_if_arvados_object pj[:job][:uuid] if pj[:job] - pj - end - - # Merge (started_at, finished_at) time range into the list of time ranges in - # timestamps (timestamps must be sorted and non-overlapping). - # return the updated timestamps list. - def merge_range timestamps, started_at, finished_at - # in the comments below, 'i' is the entry in the timestamps array and 'j' - # is the started_at, finished_at range which is passed in. - timestamps.each_index do |i| - if started_at - if started_at >= timestamps[i][0] and finished_at <= timestamps[i][1] - # 'j' started and ended during 'i' - return timestamps - end - - if started_at < timestamps[i][0] and finished_at >= timestamps[i][0] and finished_at <= timestamps[i][1] - # 'j' started before 'i' and finished during 'i' - # re-merge range between when 'j' started and 'i' finished - finished_at = timestamps[i][1] - timestamps.delete_at i - return merge_range timestamps, started_at, finished_at - end - - if started_at >= timestamps[i][0] and started_at <= timestamps[i][1] - # 'j' started during 'i' and finished sometime after - # move end time of 'i' back - # re-merge range between when 'i' started and 'j' finished - started_at = timestamps[i][0] - timestamps.delete_at i - return merge_range timestamps, started_at, finished_at - end - - if finished_at < timestamps[i][0] - # 'j' finished before 'i' started, so insert before 'i' - timestamps.insert i, [started_at, finished_at] - return timestamps - end - end - end - - timestamps << [started_at, finished_at] - end - - # Accept a list of objects with [:started_at] and [:finished_at] keys and - # merge overlapping ranges to compute the time spent running after periods of - # overlapping execution are factored out. - def determine_wallclock_runtime jobs - timestamps = [] - jobs.each do |j| - started_at = (j.started_at if j.respond_to?(:started_at)) || (j[:started_at] if j.is_a?(Hash)) - finished_at = (j.finished_at if j.respond_to?(:finished_at)) || (j[:finished_at] if j.is_a?(Hash)) || Time.now - if started_at - timestamps = merge_range timestamps, started_at, finished_at - end - end - timestamps.map { |t| t[1] - t[0] }.reduce(:+) || 0 - end - - protected - - def pipeline_jobs_newschool object - ret = [] - i = -1 - - jobuuids = object.components.values.map { |c| - c[:job][:uuid] if c.is_a?(Hash) and c[:job].is_a?(Hash) - }.compact - job = {} - Job.where(uuid: jobuuids).with_count("none").each do |j| - job[j[:uuid]] = j - end - - object.components.each do |cname, c| - i += 1 - pj = {index: i, name: cname} - if not c.is_a?(Hash) - ret << pj - next - end - if c[:job] and c[:job][:uuid] and job[c[:job][:uuid]] - pj[:job] = job[c[:job][:uuid]] - elsif c[:job].is_a?(Hash) - pj[:job] = c[:job] - if pj[:job][:started_at].is_a? String - pj[:job][:started_at] = Time.parse(pj[:job][:started_at]) - end - if pj[:job][:finished_at].is_a? String - pj[:job][:finished_at] = Time.parse(pj[:job][:finished_at]) - end - # If necessary, figure out the state based on the other fields. - pj[:job][:state] ||= if pj[:job][:cancelled_at] - "Cancelled" - elsif pj[:job][:success] == false - "Failed" - elsif pj[:job][:success] == true - "Complete" - elsif pj[:job][:running] == true - "Running" - else - "Queued" - end - else - pj[:job] = {} - end - pj[:percent_done] = 0 - pj[:percent_running] = 0 - if pj[:job][:success] - if pj[:job][:output] - pj[:progress] = 1.0 - pj[:percent_done] = 100 - else - pj[:progress] = 0.0 - end - else - if pj[:job][:tasks_summary] - begin - ts = pj[:job][:tasks_summary] - denom = ts[:done].to_f + ts[:running].to_f + ts[:todo].to_f - pj[:progress] = (ts[:done].to_f + ts[:running].to_f/2) / denom - pj[:percent_done] = 100.0 * ts[:done].to_f / denom - pj[:percent_running] = 100.0 * ts[:running].to_f / denom - pj[:progress_detail] = "#{ts[:done]} done #{ts[:running]} run #{ts[:todo]} todo" - rescue - pj[:progress] = 0.5 - pj[:percent_done] = 0.0 - pj[:percent_running] = 100.0 - end - else - pj[:progress] = 0.0 - end - end - - case pj[:job][:state] - when 'Complete' - pj[:result] = 'complete' - pj[:labeltype] = 'success' - pj[:complete] = true - pj[:progress] = 1.0 - when 'Failed' - pj[:result] = 'failed' - pj[:labeltype] = 'danger' - pj[:failed] = true - when 'Cancelled' - pj[:result] = 'cancelled' - pj[:labeltype] = 'danger' - pj[:failed] = true - when 'Running' - pj[:result] = 'running' - pj[:labeltype] = 'primary' - when 'Queued' - pj[:result] = 'queued' - pj[:labeltype] = 'default' - else - pj[:result] = 'none' - pj[:labeltype] = 'default' - end - - pj[:job_id] = pj[:job][:uuid] - pj[:script] = pj[:job][:script] || c[:script] - pj[:repository] = pj[:job][:script] || c[:repository] - pj[:script_parameters] = pj[:job][:script_parameters] || c[:script_parameters] - pj[:script_version] = pj[:job][:script_version] || c[:script_version] - pj[:nondeterministic] = pj[:job][:nondeterministic] || c[:nondeterministic] - pj[:output] = pj[:job][:output] - pj[:output_uuid] = c[:output_uuid] - pj[:finished_at] = pj[:job][:finished_at] - ret << pj - end - ret - end - - def pipeline_jobs_oldschool object - ret = [] - object.components[:steps].each_with_index do |step, i| - pj = {index: i, name: step[:name]} - if step[:complete] and step[:complete] != 0 - if step[:output_data_locator] - pj[:progress] = 1.0 - else - pj[:progress] = 0.0 - end - else - if step[:progress] and - (re = step[:progress].match(/^(\d+)\+(\d+)\/(\d+)$/)) - pj[:progress] = (((re[1].to_f + re[2].to_f/2) / re[3].to_f) rescue 0.5) - else - pj[:progress] = 0.0 - end - if step[:failed] - pj[:result] = 'failed' - pj[:failed] = true - end - end - if step[:warehousejob] - if step[:complete] - pj[:result] = 'complete' - pj[:complete] = true - pj[:progress] = 1.0 - elsif step[:warehousejob][:finishtime] - pj[:result] = 'failed' - pj[:failed] = true - elsif step[:warehousejob][:starttime] - pj[:result] = 'running' - else - pj[:result] = 'queued' - end - end - pj[:progress_detail] = (step[:progress] rescue nil) - pj[:job_id] = (step[:warehousejob][:id] rescue nil) - pj[:job_link] = pj[:job_id] - pj[:script] = step[:function] - pj[:script_version] = (step[:warehousejob][:revision] rescue nil) - pj[:output] = step[:output_data_locator] - pj[:finished_at] = (Time.parse(step[:warehousejob][:finishtime]) rescue nil) - ret << pj - end - ret - end - - MINUTE = 60 - HOUR = 60 * MINUTE - DAY = 24 * HOUR - - def render_runtime duration, use_words, round_to_min=true - days = 0 - hours = 0 - minutes = 0 - seconds = 0 - - if duration >= DAY - days = (duration / DAY).floor - duration -= days * DAY - end - - if duration >= HOUR - hours = (duration / HOUR).floor - duration -= hours * HOUR - end - - if duration >= MINUTE - minutes = (duration / MINUTE).floor - duration -= minutes * MINUTE - end - - seconds = duration.floor - - if round_to_min and seconds >= 30 - minutes += 1 - end - - if use_words - s = [] - if days > 0 then - s << "#{days} day#{'s' if days != 1}" - end - if hours > 0 then - s << "#{hours} hour#{'s' if hours != 1}" - end - if minutes > 0 then - s << "#{minutes} minute#{'s' if minutes != 1}" - end - if not round_to_min or s.size == 0 - s << "#{seconds} second#{'s' if seconds != 1}" - end - s = s * " " - else - s = "" - if days > 0 - s += "#{days}d" - end - - if (hours > 0) - s += "#{hours}h" - end - - s += "#{minutes}m" - - if not round_to_min or (days == 0 and hours == 0 and minutes == 0) - s += "#{seconds}s" - end - end - - raw(s) - end - - def render_unreadable_inputs_present - if current_user and controller.class.name.eql?('PipelineInstancesController') and unreadable_inputs_present? - raw('
    ' + - '

    One or more inputs provided are not readable by you. ' + - 'Please correct these before you can run the pipeline.

    ') - end - end -end diff --git a/apps/workbench/app/helpers/provenance_helper.rb b/apps/workbench/app/helpers/provenance_helper.rb deleted file mode 100644 index cef5cc7ee8..0000000000 --- a/apps/workbench/app/helpers/provenance_helper.rb +++ /dev/null @@ -1,437 +0,0 @@ -# Copyright (C) The Arvados Authors. All rights reserved. -# -# SPDX-License-Identifier: AGPL-3.0 - -module ProvenanceHelper - - class GenerateGraph - def initialize(pdata, opts) - @pdata = pdata - @opts = opts - @visited = {} - @jobs = {} - @node_extra = {} - end - - def self.collection_uuid(uuid) - Keep::Locator.parse(uuid).andand.strip_hints.andand.to_s - end - - def url_for u - p = { :host => @opts[:request].host, - :port => @opts[:request].port, - :protocol => @opts[:request].protocol } - p.merge! u - Rails.application.routes.url_helpers.url_for (p) - end - - def determine_fillcolor(n) - fillcolor = %w(666666 669966 666699 666666 996666)[n || 0] || '666666' - "style=\"filled\",color=\"#ffffff\",fillcolor=\"##{fillcolor}\",fontcolor=\"#ffffff\"" - end - - def describe_node(uuid, describe_opts={}) - bgcolor = determine_fillcolor (describe_opts[:pip] || @opts[:pips].andand[uuid]) - - rsc = ArvadosBase::resource_class_for_uuid uuid - - if GenerateGraph::collection_uuid(uuid) || rsc == Collection - if Collection.is_empty_blob_locator? uuid.to_s - # special case - return "\"#{uuid}\" [label=\"(empty collection)\"];\n" - end - - if describe_opts[:col_uuid] - href = url_for ({:controller => Collection.to_s.tableize, - :action => :show, - :id => describe_opts[:col_uuid].to_s }) - else - href = url_for ({:controller => Collection.to_s.tableize, - :action => :show, - :id => uuid.to_s }) - end - - return "\"#{uuid}\" [label=\"#{encode_quotes(describe_opts[:label] || (@pdata[uuid] and @pdata[uuid][:name]) || uuid)}\",shape=box,href=\"#{href}\",#{bgcolor}];\n" - else - href = "" - if describe_opts[:href] - href = ",href=\"#{url_for ({:controller => describe_opts[:href][:controller], - :action => :show, - :id => describe_opts[:href][:id] })}\"" - end - return "\"#{uuid}\" [label=\"#{encode_quotes(describe_opts[:label] || uuid)}\",#{bgcolor},shape=#{describe_opts[:shape] || 'box'}#{href}];\n" - end - end - - def job_uuid(job) - d = Digest::MD5.hexdigest(job[:script_parameters].to_json) - if @opts[:combine_jobs] == :script_only - uuid = "#{job[:script]}_#{d}" - elsif @opts[:combine_jobs] == :script_and_version - uuid = "#{job[:script]}_#{job[:script_version]}_#{d}" - else - uuid = "#{job[:uuid]}" - end - - @jobs[uuid] = [] unless @jobs[uuid] - @jobs[uuid] << job unless @jobs[uuid].include? job - - uuid - end - - def edge(tail, head, extra) - if @opts[:direction] == :bottom_up - gr = "\"#{encode_quotes head}\" -> \"#{encode_quotes tail}\"" - else - gr = "\"#{encode_quotes tail}\" -> \"#{encode_quotes head}\"" - end - - if extra.length > 0 - gr += " [" - extra.each do |k, v| - gr += "#{k}=\"#{encode_quotes v}\"," - end - gr += "]" - end - gr += ";\n" - gr - end - - def script_param_edges(uuid, sp) - gr = "" - - sp.each do |k, v| - if @opts[:all_script_parameters] - if v.is_a? Array or v.is_a? Hash - encv = JSON.pretty_generate(v).gsub("\n", "\\l") + "\\l" - else - encv = v.to_json - end - gr += "\"#{encode_quotes encv}\" [shape=box];\n" - gr += edge(encv, uuid, {:label => k}) - end - end - gr - end - - def job_edges job, edge_opts={} - uuid = job_uuid(job) - gr = "" - - ProvenanceHelper::find_collections job[:script_parameters] do |collection_hash, collection_uuid, key| - if collection_uuid - gr += describe_node(collection_uuid) - gr += edge(collection_uuid, uuid, {:label => key}) - else - gr += describe_node(collection_hash) - gr += edge(collection_hash, uuid, {:label => key}) - end - end - - if job[:docker_image_locator] and !@opts[:no_docker] - gr += describe_node(job[:docker_image_locator], {label: (job[:runtime_constraints].andand[:docker_image] || job[:docker_image_locator])}) - gr += edge(job[:docker_image_locator], uuid, {label: "docker_image"}) - end - - if @opts[:script_version_nodes] - gr += describe_node(job[:script_version], {:label => "git:#{job[:script_version]}"}) - gr += edge(job[:script_version], uuid, {:label => "script_version"}) - end - - if job[:output] and !edge_opts[:no_output] - gr += describe_node(job[:output]) - gr += edge(uuid, job[:output], {label: "output" }) - end - - if job[:log] and !edge_opts[:no_log] - gr += describe_node(job[:log]) - gr += edge(uuid, job[:log], {label: "log"}) - end - - gr - end - - def cr_edges cont, edge_opts={} - uuid = cont[:uuid] - gr = "" - - gr += describe_node(cont[:uuid], {href: {controller: 'container_requests', - id: cont[:uuid]}, - shape: 'oval', - label: cont[:name]}) - - ProvenanceHelper::find_collections cont[:mounts] do |collection_hash, collection_uuid, key| - if @opts[:pdh_to_uuid] and @opts[:pdh_to_uuid][collection_hash] - collection_uuid = @opts[:pdh_to_uuid][collection_hash].uuid - collection_hash = nil - end - if collection_uuid and @pdata[collection_uuid] - gr += describe_node(collection_uuid) - gr += edge(collection_uuid, uuid, {:label => key}) - elsif collection_hash and @pdata[collection_hash] - gr += describe_node(collection_hash) - gr += edge(collection_hash, uuid, {:label => key}) - end - end - - if cont[:container_image] and !@opts[:no_docker] and @pdata[cont[:container_image]] - gr += describe_node(cont[:container_image], {label: cont[:container_image]}) - gr += edge(cont[:container_image], uuid, {label: "docker_image"}) - end - - if cont[:output_uuid] and !edge_opts[:no_output] and @pdata[cont[:output_uuid]] - gr += describe_node(cont[:output_uuid]) - gr += edge(uuid, cont[:output_uuid], {label: "output" }) - end - - if cont[:log_uuid] and !edge_opts[:no_log] and @pdata[cont[:log_uuid]] - gr += describe_node(cont[:log_uuid]) - gr += edge(uuid, cont[:log_uuid], {label: "log"}) - end - - gr - end - - def container_edges cont, edge_opts={} - uuid = cont[:uuid] - gr = "" - - gr += describe_node(cont[:uuid], {href: {controller: 'containers', - id: cont[:uuid]}, - shape: 'oval'}) - - ProvenanceHelper::find_collections cont[:mounts] do |collection_hash, collection_uuid, key| - if collection_uuid and @pdata[collection_uuid] - gr += describe_node(collection_uuid) - gr += edge(collection_uuid, uuid, {:label => key}) - elsif collection_hash and @pdata[collection_hash] - gr += describe_node(collection_hash) - gr += edge(collection_hash, uuid, {:label => key}) - end - end - - if cont[:container_image] and !@opts[:no_docker] and @pdata[cont[:container_image]] - gr += describe_node(cont[:container_image], {label: cont[:container_image]}) - gr += edge(cont[:container_image], uuid, {label: "docker_image"}) - end - - if cont[:output] and !edge_opts[:no_output] and @pdata[cont[:output]] - gr += describe_node(cont[:output]) - gr += edge(uuid, cont[:output], {label: "output" }) - end - - if cont[:log] and !edge_opts[:no_log] and @pdata[cont[:log]] - gr += describe_node(cont[:log]) - gr += edge(uuid, cont[:log], {label: "log"}) - end - - gr - end - - def generate_provenance_edges(uuid) - gr = "" - m = GenerateGraph::collection_uuid(uuid) - uuid = m if m - - if uuid.nil? or uuid.empty? or @visited[uuid] - return "" - end - - if @pdata[uuid].nil? - return "" - else - @visited[uuid] = true - end - - if uuid.start_with? "component_" - # Pipeline component inputs - job = @pdata[@pdata[uuid][:job].andand[:uuid]] - - if job - gr += describe_node(job_uuid(job), {label: uuid[38..-1], pip: @opts[:pips].andand[job[:uuid]], shape: "oval", - href: {controller: 'jobs', id: job[:uuid]}}) - gr += job_edges job, {no_output: true, no_log: true} - end - - # Pipeline component output - outuuid = @pdata[uuid][:output_uuid] - if outuuid - outcollection = @pdata[outuuid] - if outcollection - gr += edge(job_uuid(job), outcollection[:portable_data_hash], {label: "output"}) - gr += describe_node(outcollection[:portable_data_hash], {label: outcollection[:name]}) - end - elsif job and job[:output] - gr += describe_node(job[:output]) - gr += edge(job_uuid(job), job[:output], {label: "output" }) - end - else - rsc = ArvadosBase::resource_class_for_uuid uuid - - if rsc == Job - job = @pdata[uuid] - gr += job_edges job if job - elsif rsc == ContainerRequest - cr = @pdata[uuid] - gr += cr_edges cr if cr - elsif rsc == Container - cr = @pdata[uuid] - gr += container_edges cr if cr - end - end - - @pdata.each do |k, link| - if link[:head_uuid] == uuid.to_s and link[:link_class] == "provenance" - href = url_for ({:controller => Link.to_s.tableize, - :action => :show, - :id => link[:uuid] }) - - gr += describe_node(link[:tail_uuid]) - gr += edge(link[:head_uuid], link[:tail_uuid], {:label => link[:name], :href => href}) - gr += generate_provenance_edges(link[:tail_uuid]) - end - end - - gr - end - - def describe_jobs - gr = "" - @jobs.each do |k, v| - href = url_for ({:controller => Job.to_s.tableize, - :action => :index }) - - gr += "\"#{k}\" [href=\"#{href}?" - - n = 0 - v.each do |u| - gr += ";" unless gr.end_with? "?" - gr += "uuid%5b%5d=#{u[:uuid]}" - n |= @opts[:pips][u[:uuid]] if @opts[:pips] and @opts[:pips][u[:uuid]] - end - - gr += "\",label=\"" - - label = "#{v[0][:script]}" - - if label == "run-command" and v[0][:script_parameters][:command].is_a? Array - label = v[0][:script_parameters][:command].join(' ') - end - - if not @opts[:combine_jobs] - label += "\\n#{v[0][:finished_at]}" - end - - gr += encode_quotes label - - gr += "\",#{determine_fillcolor n}];\n" - end - gr - end - - def encode_quotes value - value.to_s.gsub("\"", "\\\"").gsub("\n", "\\n") - end - end - - def self.create_provenance_graph(pdata, svgId, opts={}) - if pdata.is_a? Array or pdata.is_a? ArvadosResourceList - p2 = {} - pdata.each do |k| - p2[k[:uuid]] = k if k[:uuid] - end - pdata = p2 - end - - unless pdata.is_a? Hash - raise "create_provenance_graph accepts Array or Hash for pdata only, pdata is #{pdata.class}" - end - - gr = """strict digraph { -node [fontsize=10,fontname=\"Helvetica,Arial,sans-serif\"]; -edge [fontsize=10,fontname=\"Helvetica,Arial,sans-serif\"]; -""" - if ["LR", "RL"].include? opts[:direction] - gr += "rankdir=#{opts[:direction]};" - end - - begin - pdata = pdata.stringify_keys - - g = GenerateGraph.new(pdata, opts) - - pdata.each do |k, v| - if !opts[:only_components] or k.start_with? "component_" - gr += g.generate_provenance_edges(k) - else - #gr += describe_node(k) - end - end - - if !opts[:only_components] - gr += g.describe_jobs - end - - rescue => e - Rails.logger.warn "#{e.inspect}" - Rails.logger.warn "#{e.backtrace.join("\n\t")}" - raise - end - - gr += "}" - svg = "" - - require 'open3' - - Open3.popen2("dot", "-Tsvg") do |stdin, stdout, wait_thr| - stdin.print(gr) - stdin.close - svg = stdout.read() - wait_thr.value - stdout.close() - end - - svg = svg.sub(/<\?xml.*?\?>/m, "") - svg = svg.sub(//m, "") - svg = svg.sub(/ true) - errors = @api_response[:errors] - if errors.respond_to?(:join) - errors = errors.join("\n\n") - else - errors = errors.to_s - end - super(request_url, "#{errors} [API: #{@api_status}]") - end - end - - class AccessForbiddenException < ApiErrorResponseException; end - class NotFoundException < ApiErrorResponseException; end - class NotLoggedInException < ApiErrorResponseException; end - - ERROR_CODE_CLASSES = { - 401 => NotLoggedInException, - 403 => AccessForbiddenException, - 404 => NotFoundException, - } - - @@profiling_enabled = Rails.configuration.Workbench.ProfilingEnabled - @@discovery = nil - - # An API client object suitable for handling API requests on behalf - # of the current thread. - def self.new_or_current - # If this thread doesn't have an API client yet, *or* this model - # has been reloaded since the existing client was created, create - # a new client. Otherwise, keep using the latest client created in - # the current thread. - unless Thread.current[:arvados_api_client].andand.class == self - Thread.current[:arvados_api_client] = new - end - Thread.current[:arvados_api_client] - end - - def initialize *args - @api_client = nil - @client_mtx = Mutex.new - end - - def api(resources_kind, action, data=nil, tokens={}, include_anon_token=true) - - profile_checkpoint - - if not @api_client - @client_mtx.synchronize do - @api_client = HTTPClient.new - @api_client.ssl_config.timeout = Rails.configuration.Workbench.APIClientConnectTimeout - @api_client.connect_timeout = Rails.configuration.Workbench.APIClientConnectTimeout - @api_client.receive_timeout = Rails.configuration.Workbench.APIClientReceiveTimeout - if Rails.configuration.TLS.Insecure - @api_client.ssl_config.verify_mode = OpenSSL::SSL::VERIFY_NONE - else - # Use system CA certificates - ["/etc/ssl/certs/ca-certificates.crt", - "/etc/pki/tls/certs/ca-bundle.crt"] - .select { |ca_path| File.readable?(ca_path) } - .each { |ca_path| @api_client.ssl_config.add_trust_ca(ca_path) } - end - if Rails.configuration.Workbench.APIResponseCompression - @api_client.transparent_gzip_decompression = true - end - end - end - - resources_kind = class_kind(resources_kind).pluralize if resources_kind.is_a? Class - url = "#{self.arvados_v1_base}/#{resources_kind}#{action}" - - # Clean up /arvados/v1/../../discovery/v1 to /discovery/v1 - url.sub! '/arvados/v1/../../', '/' - - anon_tokens = [Rails.configuration.Users.AnonymousUserToken].select { |x| !x.empty? && include_anon_token } - - query = { - 'reader_tokens' => ((tokens[:reader_tokens] || - Thread.current[:reader_tokens] || - []) + - anon_tokens).to_json, - } - if !data.nil? - data.each do |k,v| - if v.is_a? String or v.nil? - query[k] = v - elsif v == true - query[k] = 1 - elsif v == false - query[k] = 0 - else - query[k] = Oj.dump(v, mode: :compat) - end - end - else - query["_method"] = "GET" - end - - if @@profiling_enabled - query["_profile"] = "true" - end - - headers = { - "Accept" => "application/json", - "Authorization" => "OAuth2 " + - (tokens[:arvados_api_token] || - Thread.current[:arvados_api_token] || - ''), - "X-Request-Id" => Thread.current[:request_id] || '', - } - - profile_checkpoint { "Prepare request #{query["_method"] or "POST"} #{url} #{query[:uuid]} #{query.inspect[0,256]}" } - msg = @client_mtx.synchronize do - begin - @api_client.post(url, query, headers) - rescue => exception - raise NoApiResponseException.new(url, exception) - end - end - profile_checkpoint 'API transaction' - if @@profiling_enabled - if msg.headers['X-Runtime'] - Rails.logger.info "API server: #{msg.headers['X-Runtime']} runtime reported" - end - Rails.logger.info "Content-Encoding #{msg.headers['Content-Encoding'].inspect}, Content-Length #{msg.headers['Content-Length'].inspect}, actual content size #{msg.content.size}" - end - - begin - resp = Oj.strict_load(msg.content, :symbol_keys => true) - rescue Oj::ParseError - resp = nil - end - - if not resp.is_a? Hash - raise InvalidApiResponseException.new(url, msg) - elsif msg.status_code != 200 - error_class = ERROR_CODE_CLASSES.fetch(msg.status_code, - ApiErrorResponseException) - raise error_class.new(url, msg) - end - - if resp[:_profile] - Rails.logger.info "API client: " \ - "#{resp.delete(:_profile)[:request_time]} request_time" - end - profile_checkpoint 'Parse response' - resp - end - - def self.patch_paging_vars(ary, items_available, offset, limit, links=nil) - if items_available - (class << ary; self; end).class_eval { attr_accessor :items_available } - ary.items_available = items_available - end - if offset - (class << ary; self; end).class_eval { attr_accessor :offset } - ary.offset = offset - end - if limit - (class << ary; self; end).class_eval { attr_accessor :limit } - ary.limit = limit - end - if links - (class << ary; self; end).class_eval { attr_accessor :links } - ary.links = links - end - ary - end - - def unpack_api_response(j, kind=nil) - if j.is_a? Hash and j[:items].is_a? Array and j[:kind].match(/(_list|List)$/) - ary = j[:items].collect { |x| unpack_api_response x, x[:kind] } - links = ArvadosResourceList.new Link - links.results = (j[:links] || []).collect do |x| - unpack_api_response x, x[:kind] - end - self.class.patch_paging_vars(ary, j[:items_available], j[:offset], j[:limit], links) - elsif j.is_a? Hash and (kind || j[:kind]) - oclass = self.kind_class(kind || j[:kind]) - if oclass - j.keys.each do |k| - childkind = j["#{k.to_s}_kind".to_sym] - if childkind - j[k] = self.unpack_api_response(j[k], childkind) - end - end - oclass.new.private_reload(j) - else - j - end - else - j - end - end - - def arvados_login_url(params={}) - if Rails.configuration.testing_override_login_url - uri = URI(Rails.configuration.testing_override_login_url) - uri.path = "/login" - uri.query = URI.encode_www_form(params) - return uri.to_s - end - - case - when Rails.configuration.Login.PAM.Enable, - Rails.configuration.Login.LDAP.Enable, - Rails.configuration.Login.Test.Enable - - uri = URI.parse(Rails.configuration.Services.Workbench1.ExternalURL.to_s) - uri.path = "/users/welcome" - uri.query = URI.encode_www_form(params) - else - uri = URI.parse(Rails.configuration.Services.Controller.ExternalURL.to_s) - uri.path = "/login" - uri.query = URI.encode_www_form(params) - end - uri.to_s - end - - def arvados_logout_url(params={}) - uri = URI.parse(Rails.configuration.Services.Controller.ExternalURL.to_s) - if Rails.configuration.testing_override_login_url - uri = URI(Rails.configuration.testing_override_login_url) - end - uri.path = "/logout" - uri.query = URI.encode_www_form(params) - uri.to_s - end - - def arvados_v1_base - # workaround Ruby 2.3 bug, can't duplicate URI objects - # https://github.com/httprb/http/issues/388 - u = URI.parse(Rails.configuration.Services.Controller.ExternalURL.to_s) - u.path = "/arvados/v1" - u.to_s - end - - def discovery - @@discovery ||= api '../../discovery/v1/apis/arvados/v1/rest', '' - end - - def kind_class(kind) - kind.match(/^arvados\#(.+?)(_list|List)?$/)[1].pluralize.classify.constantize rescue nil - end - - def class_kind(resource_class) - resource_class.to_s.underscore - end - - def self.class_kind(resource_class) - resource_class.to_s.underscore - end - - protected - def profile_checkpoint label=nil - return if !@@profiling_enabled - label = yield if block_given? - t = Time.now - if label and @profile_t0 - Rails.logger.info "API client: #{t - @profile_t0} #{label}" - end - @profile_t0 = t - end -end diff --git a/apps/workbench/app/models/arvados_base.rb b/apps/workbench/app/models/arvados_base.rb deleted file mode 100644 index c5e1a4ed22..0000000000 --- a/apps/workbench/app/models/arvados_base.rb +++ /dev/null @@ -1,623 +0,0 @@ -# Copyright (C) The Arvados Authors. All rights reserved. -# -# SPDX-License-Identifier: AGPL-3.0 - -class ArvadosBase - include ActiveModel::Validations - include ActiveModel::Conversion - include ActiveModel::Serialization - include ActiveModel::Dirty - include ActiveModel::AttributeAssignment - extend ActiveModel::Naming - - Column = Struct.new("Column", :name) - - attr_accessor :attribute_sortkey - attr_accessor :create_params - - class Error < StandardError; end - - module Type - class Hash < ActiveModel::Type::Value - def type - :hash - end - - def default_value - {} - end - - private - def cast_value(value) - (value.class == String) ? ::JSON.parse(value) : value - end - end - - class Array < ActiveModel::Type::Value - def type - :array - end - - def default_value - [] - end - - private - def cast_value(value) - (value.class == String) ? ::JSON.parse(value) : value - end - end - end - - def self.arvados_api_client - ArvadosApiClient.new_or_current - end - - def arvados_api_client - ArvadosApiClient.new_or_current - end - - def self.uuid_infix_object_kind - @@uuid_infix_object_kind ||= - begin - infix_kind = {} - arvados_api_client.discovery[:schemas].each do |name, schema| - if schema[:uuidPrefix] - infix_kind[schema[:uuidPrefix]] = - 'arvados#' + name.to_s.camelcase(:lower) - end - end - - # Recognize obsolete types. - infix_kind. - merge('mxsvm' => 'arvados#pipelineTemplate', # Pipeline - 'uo14g' => 'arvados#pipelineInstance', # PipelineInvocation - 'ldvyl' => 'arvados#group') # Project - end - end - - def initialize raw_params={}, create_params={} - self.class.permit_attribute_params(raw_params) - @create_params = create_params - @attribute_sortkey ||= { - 'id' => nil, - 'name' => '000', - 'owner_uuid' => '002', - 'event_type' => '100', - 'link_class' => '100', - 'group_class' => '100', - 'tail_uuid' => '101', - 'head_uuid' => '102', - 'object_uuid' => '102', - 'summary' => '104', - 'description' => '104', - 'properties' => '150', - 'info' => '150', - 'created_at' => '200', - 'modified_at' => '201', - 'modified_by_user_uuid' => '202', - 'modified_by_client_uuid' => '203', - 'uuid' => '999', - } - @loaded_attributes = {} - attributes = self.class.columns.map { |c| [c.name.to_sym, nil] }.to_h.merge(raw_params) - attributes.symbolize_keys.each do |name, value| - send("#{name}=", value) - end - end - - # The ActiveModel::Dirty API was changed on Rails 5.2 - # See: https://github.com/rails/rails/commit/c3675f50d2e59b7fc173d7b332860c4b1a24a726#diff-aaddd42c7feb0834b1b5c66af69814d3 - def mutations_from_database - @mutations_from_database ||= ActiveModel::NullMutationTracker.instance - end - - def self.columns - @discovered_columns = [] if !defined?(@discovered_columns) - return @discovered_columns if @discovered_columns.andand.any? - @attribute_info ||= {} - schema = arvados_api_client.discovery[:schemas][self.to_s.to_sym] - return @discovered_columns if schema.nil? - schema[:properties].each do |k, coldef| - case k - when :etag, :kind - attr_reader k - else - if coldef[:type] == coldef[:type].downcase - # boolean, integer, etc. - @discovered_columns << column(k, coldef[:type]) - else - # Hash, Array - @discovered_columns << column(k, coldef[:type], coldef[:type].constantize.new) - end - attr_reader k - @attribute_info[k] = coldef - end - end - @discovered_columns - end - - def new_record? - # dup method doesn't reset the uuid attr - @uuid.nil? || @new_record || false - end - - def initialize_dup(other) - super - @new_record = true - @created_at = nil - end - - def self.column(name, sql_type = nil, default = nil, null = true) - caster = case sql_type - when 'integer' - ActiveModel::Type::Integer - when 'string', 'text' - ActiveModel::Type::String - when 'float' - ActiveModel::Type::Float - when 'datetime' - ActiveModel::Type::DateTime - when 'boolean' - ActiveModel::Type::Boolean - when 'Hash' - ArvadosBase::Type::Hash - when 'Array' - ArvadosBase::Type::Array - when 'jsonb' - ArvadosBase::Type::Hash - else - raise ArvadosBase::Error.new("Type unknown: #{sql_type}") - end - define_method "#{name}=" do |val| - val = default if val.nil? - casted_value = caster.new.cast(val) - attribute_will_change!(name) if send(name) != casted_value - set_attribute_after_cast(name, casted_value) - end - Column.new(name.to_s) - end - - def set_attribute_after_cast(name, casted_value) - instance_variable_set("@#{name}", casted_value) - end - - def [](attr_name) - begin - send(attr_name) - rescue - Rails.logger.debug "BUG: access non-loaded attribute #{attr_name}" - nil - end - end - - def []=(attr_name, attr_val) - send("#{attr_name}=", attr_val) - end - - def self.attribute_info - self.columns - @attribute_info - end - - def self.find(uuid, opts={}) - if uuid.class != String or uuid.length < 27 then - raise 'argument to find() must be a uuid string. Acceptable formats: warehouse locator or string with format xxxxx-xxxxx-xxxxxxxxxxxxxxx' - end - - if self == ArvadosBase - # Determine type from uuid and defer to the appropriate subclass. - return resource_class_for_uuid(uuid).find(uuid, opts) - end - - # Only do one lookup on the API side per {class, uuid, workbench - # request} unless {cache: false} is given via opts. - cache_key = "request_#{Thread.current.object_id}_#{self.to_s}_#{uuid}" - if opts[:cache] == false - Rails.cache.write cache_key, arvados_api_client.api(self, '/' + uuid) - end - hash = Rails.cache.fetch cache_key do - arvados_api_client.api(self, '/' + uuid) - end - new.private_reload(hash) - end - - def self.find?(*args) - find(*args) rescue nil - end - - def self.order(*args) - ArvadosResourceList.new(self).order(*args) - end - - def self.filter(*args) - ArvadosResourceList.new(self).filter(*args) - end - - def self.where(*args) - ArvadosResourceList.new(self).where(*args) - end - - def self.limit(*args) - ArvadosResourceList.new(self).limit(*args) - end - - def self.select(*args) - ArvadosResourceList.new(self).select(*args) - end - - def self.with_count(*args) - ArvadosResourceList.new(self).with_count(*args) - end - - def self.distinct(*args) - ArvadosResourceList.new(self).distinct(*args) - end - - def self.include_trash(*args) - ArvadosResourceList.new(self).include_trash(*args) - end - - def self.recursive(*args) - ArvadosResourceList.new(self).recursive(*args) - end - - def self.eager(*args) - ArvadosResourceList.new(self).eager(*args) - end - - def self.all - ArvadosResourceList.new(self) - end - - def self.permit_attribute_params raw_params - # strong_parameters does not provide security in Workbench: anyone - # who can get this far can just as well do a call directly to our - # database (Arvados) with the same credentials we use. - # - # The following permit! is necessary even with - # "ActionController::Parameters.permit_all_parameters = true", - # because permit_all does not permit nested attributes. - if !raw_params.is_a? ActionController::Parameters - raw_params = ActionController::Parameters.new(raw_params) - end - raw_params.permit! - end - - def self.create raw_params={}, create_params={} - x = new(permit_attribute_params(raw_params), create_params) - x.save - x - end - - def self.create! raw_params={}, create_params={} - x = new(permit_attribute_params(raw_params), create_params) - x.save! - x - end - - def self.table_name - self.name.underscore.pluralize.downcase - end - - def update_attributes raw_params={} - assign_attributes(self.class.permit_attribute_params(raw_params)) - save - end - - def update_attributes! raw_params={} - assign_attributes(self.class.permit_attribute_params(raw_params)) - save! - end - - def save - obdata = {} - self.class.columns.each do |col| - # Non-nil serialized values must be sent because we can't tell - # whether they've changed. Other than that, any given attribute - # is either unchanged (in which case there's no need to send its - # old value in the update/create command) or has been added to - # #changed by ActiveRecord's #attr= method. - if changed.include? col.name or - ([Hash, Array].include?(attributes[col.name].class) and - @loaded_attributes[col.name]) - obdata[col.name.to_sym] = self.send col.name - end - end - obdata.delete :id - postdata = { self.class.to_s.underscore => obdata } - if etag - postdata['_method'] = 'PUT' - obdata.delete :uuid - resp = arvados_api_client.api(self.class, '/' + uuid, postdata) - else - if @create_params - @create_params = @create_params.to_unsafe_hash if @create_params.is_a? ActionController::Parameters - postdata.merge!(@create_params) - end - resp = arvados_api_client.api(self.class, '', postdata) - end - return false if !resp[:etag] || !resp[:uuid] - - # set read-only non-database attributes - @etag = resp[:etag] - @kind = resp[:kind] - - # attributes can be modified during "save" -- we should update our copies - resp.keys.each do |attr| - if self.respond_to? "#{attr}=".to_sym - self.send(attr.to_s + '=', resp[attr.to_sym]) - end - end - - changes_applied - @new_record = false - - self - end - - def save! - self.save or raise Exception.new("Save failed") - end - - def persisted? - (!new_record? && !destroyed?) ? true : false - end - - def destroyed? - !(new_record? || etag || uuid) - end - - def destroy - if etag || uuid - postdata = { '_method' => 'DELETE' } - resp = arvados_api_client.api(self.class, '/' + uuid, postdata) - resp[:etag] && resp[:uuid] && resp - else - true - end - end - - def links(*args) - o = {} - o.merge!(args.pop) if args[-1].is_a? Hash - o[:link_class] ||= args.shift - o[:name] ||= args.shift - o[:tail_uuid] = self.uuid - if all_links - return all_links.select do |m| - ok = true - o.each do |k,v| - if !v.nil? - test_v = m.send(k) - if (v.respond_to?(:uuid) ? v.uuid : v.to_s) != (test_v.respond_to?(:uuid) ? test_v.uuid : test_v.to_s) - ok = false - end - end - end - ok - end - end - @links = arvados_api_client.api Link, '', { _method: 'GET', where: o, eager: true } - @links = arvados_api_client.unpack_api_response(@links) - end - - def all_links - return @all_links if @all_links - res = arvados_api_client.api Link, '', { - _method: 'GET', - where: { - tail_kind: self.kind, - tail_uuid: self.uuid - }, - eager: true - } - @all_links = arvados_api_client.unpack_api_response(res) - end - - def reload - private_reload(self.uuid) - end - - def private_reload(uuid_or_hash) - raise "No such object" if !uuid_or_hash - if uuid_or_hash.is_a? Hash - hash = uuid_or_hash - else - hash = arvados_api_client.api(self.class, '/' + uuid_or_hash) - end - hash.each do |k,v| - @loaded_attributes[k.to_s] = true - if self.respond_to?(k.to_s + '=') - self.send(k.to_s + '=', v) - else - # When ArvadosApiClient#schema starts telling us what to expect - # in API responses (not just the server side database - # columns), this sort of awfulness can be avoided: - self.instance_variable_set('@' + k.to_s, v) - if !self.respond_to? k - singleton = class << self; self end - singleton.send :define_method, k, lambda { instance_variable_get('@' + k.to_s) } - end - end - end - @all_links = nil - changes_applied - @new_record = false - self - end - - def to_param - uuid - end - - def initialize_copy orig - super - forget_uuid! - end - - def attributes - kv = self.class.columns.collect {|c| c.name}.map {|key| [key, send(key)]} - kv.to_h - end - - def attributes_for_display - self.attributes.reject { |k,v| - attribute_sortkey.has_key?(k) and !attribute_sortkey[k] - }.sort_by { |k,v| - attribute_sortkey[k] or k - } - end - - def class_for_display - self.class.to_s.underscore.humanize - end - - def self.class_for_display - self.to_s.underscore.humanize - end - - # Array of strings that are names of attributes that should be rendered as textile. - def textile_attributes - [] - end - - def self.creatable? - current_user.andand.is_active && api_exists?(:create) - end - - def self.goes_in_projects? - false - end - - # can this class of object be copied into a project? - # override to false on indivudal model classes for which this should not be true - def self.copies_to_projects? - self.goes_in_projects? - end - - def editable? - (current_user and current_user.is_active and - (current_user.is_admin or - current_user.uuid == self.owner_uuid or - new_record? or - (respond_to?(:writable_by) ? - writable_by.include?(current_user.uuid) : - (ArvadosBase.find(owner_uuid).writable_by.include? current_user.uuid rescue false)))) or false - end - - def deletable? - editable? - end - - def self.api_exists?(method) - arvados_api_client.discovery[:resources][self.to_s.underscore.pluralize.to_sym].andand[:methods].andand[method] - end - - # Array of strings that are the names of attributes that can be edited - # with X-Editable. - def editable_attributes - self.class.columns.map(&:name) - - %w(created_at modified_at modified_by_user_uuid modified_by_client_uuid updated_at) - end - - def attribute_editable?(attr, ever=nil) - if not editable_attributes.include?(attr.to_s) - false - elsif not (current_user.andand.is_active) - false - elsif attr == 'uuid' - current_user.is_admin - elsif ever - true - else - editable? - end - end - - def self.resource_class_for_uuid(uuid, opts={}) - if uuid.is_a? ArvadosBase - return uuid.class - end - unless uuid.is_a? String - return nil - end - if opts[:class].is_a? Class - return opts[:class] - end - if uuid.match(/^[0-9a-f]{32}(\+[^,]+)*(,[0-9a-f]{32}(\+[^,]+)*)*$/) - return Collection - end - resource_class = nil - uuid.match(/^[0-9a-z]{5}-([0-9a-z]{5})-[0-9a-z]{15}$/) do |re| - resource_class ||= arvados_api_client. - kind_class(self.uuid_infix_object_kind[re[1]]) - end - if opts[:referring_object] and - opts[:referring_attr] and - opts[:referring_attr].match(/_uuid$/) - resource_class ||= arvados_api_client. - kind_class(opts[:referring_object]. - attributes[opts[:referring_attr]. - sub(/_uuid$/, '_kind')]) - end - resource_class - end - - def resource_param_name - self.class.to_s.underscore - end - - def friendly_link_name lookup=nil - (name if self.respond_to? :name) || default_name - end - - def content_summary - self.class_for_display - end - - def selection_label - friendly_link_name - end - - def self.default_name - self.to_s.underscore.humanize - end - - def controller - (self.class.to_s.pluralize + 'Controller').constantize - end - - def controller_name - self.class.to_s.tableize - end - - # Placeholder for name when name is missing or empty - def default_name - if self.respond_to? :name - "New #{class_for_display.downcase}" - else - uuid - end - end - - def owner - ArvadosBase.find(owner_uuid) rescue nil - end - - protected - - def forget_uuid! - self.uuid = nil - @etag = nil - self - end - - def self.current_user - Thread.current[:user] ||= User.current if Thread.current[:arvados_api_token] - Thread.current[:user] - end - def current_user - self.class.current_user - end -end diff --git a/apps/workbench/app/models/arvados_resource_list.rb b/apps/workbench/app/models/arvados_resource_list.rb deleted file mode 100644 index 75a9429a43..0000000000 --- a/apps/workbench/app/models/arvados_resource_list.rb +++ /dev/null @@ -1,267 +0,0 @@ -# Copyright (C) The Arvados Authors. All rights reserved. -# -# SPDX-License-Identifier: AGPL-3.0 - -class ArvadosResourceList - include ArvadosApiClientHelper - include Enumerable - - attr_reader :resource_class - - def initialize resource_class=nil - @resource_class = resource_class - @fetch_multiple_pages = true - @arvados_api_token = Thread.current[:arvados_api_token] - @reader_tokens = Thread.current[:reader_tokens] - @results = nil - @count = nil - @offset = 0 - @cond = nil - @eager = nil - @select = nil - @orderby_spec = nil - @filters = nil - @distinct = nil - @include_trash = nil - @limit = nil - end - - def eager(bool=true) - @eager = bool - self - end - - def distinct(bool=true) - @distinct = bool - self - end - - def include_trash(option=nil) - @include_trash = option - self - end - - def recursive(option=nil) - @recursive = option - self - end - - def limit(max_results) - if not max_results.nil? and not max_results.is_a? Integer - raise ArgumentError("argument to limit() must be an Integer or nil") - end - @limit = max_results - self - end - - def offset(skip) - @offset = skip - self - end - - def order(orderby_spec) - @orderby_spec = orderby_spec - self - end - - def select(columns=nil) - # If no column arguments were given, invoke Enumerable#select. - if columns.nil? - super() - else - @select ||= [] - @select += columns - self - end - end - - def filter _filters - @filters ||= [] - @filters += _filters - self - end - - def where(cond) - @cond = cond.dup - @cond.keys.each do |uuid_key| - if @cond[uuid_key] and (@cond[uuid_key].is_a? Array or - @cond[uuid_key].is_a? ArvadosBase) - # Coerce cond[uuid_key] to an array of uuid strings. This - # allows caller the convenience of passing an array of real - # objects and uuids in cond[uuid_key]. - if !@cond[uuid_key].is_a? Array - @cond[uuid_key] = [@cond[uuid_key]] - end - @cond[uuid_key] = @cond[uuid_key].collect do |item| - if item.is_a? ArvadosBase - item.uuid - else - item - end - end - end - end - @cond.keys.select { |x| x.match(/_kind$/) }.each do |kind_key| - if @cond[kind_key].is_a? Class - @cond = @cond.merge({ kind_key => 'arvados#' + arvados_api_client.class_kind(@cond[kind_key]) }) - end - end - self - end - - # with_count sets the 'count' parameter to 'exact' or 'none' -- see - # https://doc.arvados.org/api/methods.html#index - def with_count(count_param='exact') - @count = count_param - self - end - - def fetch_multiple_pages(f) - @fetch_multiple_pages = f - self - end - - def results - if !@results - @results = [] - self.each_page do |r| - @results.concat r - end - end - @results - end - - def results=(r) - @results = r - @items_available = r.items_available if r.respond_to? :items_available - @result_limit = r.limit if r.respond_to? :limit - @result_offset = r.offset if r.respond_to? :offset - @results - end - - def to_ary - results - end - - def each(&block) - if not @results.nil? - @results.each(&block) - else - results = [] - self.each_page do |items| - items.each do |i| - results << i - block.call i - end - end - # Cache results only if all were retrieved (block didn't raise - # an exception). - @results = results - end - self - end - - def first - results.first - end - - def last - results.last - end - - def [](*x) - results.send('[]', *x) - end - - def |(x) - if x.is_a? Hash - self.to_hash | x - else - results | x.to_ary - end - end - - def to_hash - Hash[self.collect { |x| [x.uuid, x] }] - end - - def empty? - self.first.nil? - end - - def items_available - results - @items_available - end - - def result_limit - results - @result_limit - end - - def result_offset - results - @result_offset - end - - # Obsolete method retained during api transition. - def links_for item_or_uuid, link_class=false - [] - end - - protected - - def each_page - api_params = { - _method: 'GET' - } - api_params[:count] = @count if @count - api_params[:where] = @cond if @cond - api_params[:eager] = '1' if @eager - api_params[:select] = @select if @select - api_params[:order] = @orderby_spec if @orderby_spec - api_params[:filters] = @filters if @filters - api_params[:distinct] = @distinct if @distinct - api_params[:include_trash] = @include_trash if @include_trash - api_params[:cluster_id] = Rails.configuration.ClusterID - if @fetch_multiple_pages - # Default limit to (effectively) api server's MAX_LIMIT - api_params[:limit] = 2**(0.size*8 - 1) - 1 - end - - item_count = 0 - offset = @offset || 0 - @result_limit = nil - @result_offset = nil - - begin - api_params[:offset] = offset - api_params[:limit] = (@limit - item_count) if @limit - - res = arvados_api_client.api(@resource_class, '', api_params, - arvados_api_token: @arvados_api_token, - reader_tokens: @reader_tokens) - items = arvados_api_client.unpack_api_response res - - @items_available = items.items_available if items.respond_to?(:items_available) - @result_limit = items.limit if (@fetch_multiple_pages == false) and items.respond_to?(:limit) - @result_offset = items.offset if (@fetch_multiple_pages == false) and items.respond_to?(:offset) - - break if items.nil? or not items.any? - - item_count += items.size - if items.respond_to?(:offset) - offset = items.offset + items.size - else - offset = item_count - end - - yield items - - break if @limit and item_count >= @limit - break if items.respond_to? :items_available and offset >= items.items_available - end while @fetch_multiple_pages - self - end - -end diff --git a/apps/workbench/app/models/authorized_key.rb b/apps/workbench/app/models/authorized_key.rb deleted file mode 100644 index 9809eef0e3..0000000000 --- a/apps/workbench/app/models/authorized_key.rb +++ /dev/null @@ -1,17 +0,0 @@ -# Copyright (C) The Arvados Authors. All rights reserved. -# -# SPDX-License-Identifier: AGPL-3.0 - -class AuthorizedKey < ArvadosBase - def attribute_editable?(attr, ever=nil) - if (attr.to_s == 'authorized_user_uuid') and (not ever) - current_user.andand.is_admin - else - super - end - end - - def self.creatable? - false - end -end diff --git a/apps/workbench/app/models/collection.rb b/apps/workbench/app/models/collection.rb deleted file mode 100644 index ead2c951c3..0000000000 --- a/apps/workbench/app/models/collection.rb +++ /dev/null @@ -1,100 +0,0 @@ -# Copyright (C) The Arvados Authors. All rights reserved. -# -# SPDX-License-Identifier: AGPL-3.0 - -require "arvados/keep" - -class Collection < ArvadosBase - MD5_EMPTY = 'd41d8cd98f00b204e9800998ecf8427e' - - def default_name - if Collection.is_empty_blob_locator? self.uuid - "Empty Collection" - else - super - end - end - - # Return true if the given string is the locator of a zero-length blob - def self.is_empty_blob_locator? locator - !!locator.to_s.match("^#{MD5_EMPTY}(\\+.*)?\$") - end - - def self.goes_in_projects? - true - end - - def manifest - if @manifest.nil? or manifest_text_changed? - @manifest = Keep::Manifest.new(manifest_text || "") - end - @manifest - end - - def files - # This method provides backwards compatibility for code that relied on - # the old files field in API results. New code should use manifest - # methods directly. - manifest.files - end - - def content_summary - if total_bytes > 0 - ApplicationController.helpers.human_readable_bytes_html(total_bytes) + " " + super - else - super + " modified at " + modified_at.to_s - end - end - - def total_bytes - manifest.files.inject(0) { |sum, filespec| sum + filespec.last } - end - - def files_tree - tree = manifest.files.group_by do |file_spec| - File.split(file_spec.first) - end - return [] if tree.empty? - # Fill in entries for empty directories. - tree.keys.map { |basedir, _| File.split(basedir) }.each do |splitdir| - until tree.include?(splitdir) - tree[splitdir] = [] - splitdir = File.split(splitdir.first) - end - end - dir_to_tree = lambda do |dirname| - # First list subdirectories, with their files inside. - subnodes = tree.keys.select { |bd, td| (bd == dirname) and (td != '.') } - .sort.flat_map do |parts| - [parts + [nil]] + dir_to_tree.call(File.join(parts)) - end - # Then extend that list with files in this directory, except the empty dir placeholders (0:0:. files). - subnodes + tree[File.split(dirname)].reject { |_, basename, size| (basename == '.') and (size == 0) } - end - dir_to_tree.call('.') - end - - def editable_attributes - %w(name description manifest_text filename) - end - - def provenance - arvados_api_client.api "collections/#{self.uuid}/", "provenance" - end - - def used_by - arvados_api_client.api "collections/#{self.uuid}/", "used_by" - end - - def friendly_link_name lookup=nil - name || portable_data_hash - end - - def textile_attributes - [ 'description' ] - end - - def untrash - arvados_api_client.api(self.class, "/#{self.uuid}/untrash", {"ensure_unique_name" => true}) - end -end diff --git a/apps/workbench/app/models/container.rb b/apps/workbench/app/models/container.rb deleted file mode 100644 index 8de28ae41f..0000000000 --- a/apps/workbench/app/models/container.rb +++ /dev/null @@ -1,13 +0,0 @@ -# Copyright (C) The Arvados Authors. All rights reserved. -# -# SPDX-License-Identifier: AGPL-3.0 - -class Container < ArvadosBase - def self.creatable? - false - end - - def work_unit(label=nil, child_objects=nil) - ContainerWorkUnit.new(self, label, self.uuid, child_objects=child_objects) - end -end diff --git a/apps/workbench/app/models/container_request.rb b/apps/workbench/app/models/container_request.rb deleted file mode 100644 index be97a6cfb5..0000000000 --- a/apps/workbench/app/models/container_request.rb +++ /dev/null @@ -1,45 +0,0 @@ -# Copyright (C) The Arvados Authors. All rights reserved. -# -# SPDX-License-Identifier: AGPL-3.0 - -class ContainerRequest < ArvadosBase - def self.creatable? - false - end - - def textile_attributes - [ 'description' ] - end - - def self.goes_in_projects? - true - end - - def self.copies_to_projects? - false - end - - def work_unit(label=nil, child_objects=nil) - ContainerWorkUnit.new(self, label, self.uuid, child_objects=child_objects) - end - - def editable_attributes - super + ["reuse_steps"] - end - - def reuse_steps - command.each do |arg| - if arg == "--enable-reuse" - return true - end - end - false - end - - def self.attribute_info - self.columns - @attribute_info[:reuse_steps] = {:type => "boolean"} - @attribute_info - end - -end diff --git a/apps/workbench/app/models/container_work_unit.rb b/apps/workbench/app/models/container_work_unit.rb deleted file mode 100644 index 292bc3679b..0000000000 --- a/apps/workbench/app/models/container_work_unit.rb +++ /dev/null @@ -1,236 +0,0 @@ -# Copyright (C) The Arvados Authors. All rights reserved. -# -# SPDX-License-Identifier: AGPL-3.0 - -class ContainerWorkUnit < ProxyWorkUnit - attr_accessor :container - attr_accessor :child_proxies - - def initialize proxied, label, parent, child_objects=nil - super proxied, label, parent - if @proxied.is_a?(ContainerRequest) - container_uuid = get(:container_uuid) - if container_uuid - @container = Container.find(container_uuid) - end - end - @container = nil if !defined?(@container) - @child_proxies = child_objects - end - - def children - return @my_children if @my_children - - items = [] - container_uuid = if @proxied.is_a?(Container) then uuid else get(:container_uuid) end - if container_uuid - cols = ContainerRequest.columns.map(&:name) - %w(id updated_at mounts secret_mounts runtime_token) - my_children = @child_proxies || ContainerRequest.select(cols).where(requesting_container_uuid: container_uuid).with_count("none").results if !my_children - my_child_containers = my_children.map(&:container_uuid).compact.uniq - grandchildren = {} - my_child_containers.each { |c| grandchildren[c] = []} if my_child_containers.any? - reqs = ContainerRequest.select(cols).where(requesting_container_uuid: my_child_containers).order(["requesting_container_uuid", "uuid"]).with_count("none").results if my_child_containers.any? - reqs.each {|cr| grandchildren[cr.requesting_container_uuid] << cr} if reqs - - my_children.each do |cr| - items << cr.work_unit(cr.name || 'this container', child_objects=grandchildren[cr.container_uuid]) - end - end - - @child_proxies = nil #no need of this any longer - @my_children = items - end - - def title - "container" - end - - def uri - uuid = get(:uuid) - - return nil unless uuid - - if @proxied.class.respond_to? :table_name - "/#{@proxied.class.table_name}/#{uuid}" - else - resource_class = ArvadosBase.resource_class_for_uuid(uuid) - "#{resource_class.table_name}/#{uuid}" if resource_class - end - end - - def can_cancel? - @proxied.is_a?(ContainerRequest) && - @proxied.state == "Committed" && - (@proxied.priority > 0 || get(:state, @container) != 'Running') && - @proxied.editable? - end - - def container_uuid - get(:container_uuid) - end - - def requesting_container_uuid - get(:requesting_container_uuid) - end - - def priority - @proxied.priority - end - - # For the following properties, use value from the @container if exists - # This applies to a ContainerRequest with container_uuid - - def started_at - t = get_combined(:started_at) - t = Time.parse(t) if (t.is_a? String) - t - end - - def modified_at - t = get_combined(:modified_at) - t = Time.parse(t) if (t.is_a? String) - t - end - - def finished_at - t = get_combined(:finished_at) - t = Time.parse(t) if (t.is_a? String) - t - end - - def state_label - if get(:state) == 'Final' && get(:state, @container) != 'Complete' - # Request was finalized before its container started (or the - # container was cancelled) - return 'Cancelled' - end - state = get(:state, @container) || get(:state, @proxied) - case state - when 'Locked', 'Queued' - if priority == 0 - 'On hold' - else - 'Queued' - end - when 'Complete' - if exit_code == 0 - state - else - 'Failed' - end - when 'Running' - if runtime_status[:error] - 'Failing' - elsif runtime_status[:warning] - 'Warning' - else - state - end - else - # Cancelled, or Uncommitted (no container assigned) - state - end - end - - def runtime_status - return get(:runtime_status, @container) || get(:runtime_status, @proxied) - end - - def state_bootstrap_class - case state_label - when 'Failing' - 'danger' - when 'Warning' - 'warning' - else - super - end - end - - def exit_code - get_combined(:exit_code) - end - - def docker_image - get_combined(:container_image) - end - - def runtime_constraints - get_combined(:runtime_constraints) - end - - def log_collection - if @proxied.is_a?(ContainerRequest) - get(:log_uuid) - else - get(:log) - end - end - - def outputs - items = [] - if @proxied.is_a?(ContainerRequest) - out = get(:output_uuid) - else - out = get(:output) - end - items << out if out - items - end - - def command - get_combined(:command) - end - - def cwd - get_combined(:cwd) - end - - def environment - env = get_combined(:environment) - env = nil if env.andand.empty? - env - end - - def mounts - mnt = get_combined(:mounts) - mnt = nil if mnt.andand.empty? - mnt - end - - def output_path - get_combined(:output_path) - end - - def log_object_uuids - [get(:uuid, @container), get(:uuid, @proxied)].compact - end - - def render_log - collection = Collection.find(log_collection) rescue nil - if collection - return {log: collection, partial: 'collections/show_files', locals: {object: collection, no_checkboxes: true}} - end - end - - def template_uuid - properties = get(:properties) - if properties - properties[:template_uuid] - end - end - - # End combined properties - - protected - def get_combined key - from_container = get(key, @container) - from_proxied = get(key, @proxied) - - if from_container.is_a? Hash or from_container.is_a? Array - if from_container.any? then from_container else from_proxied end - else - from_container || from_proxied - end - end -end diff --git a/apps/workbench/app/models/group.rb b/apps/workbench/app/models/group.rb deleted file mode 100644 index ea3da2db5e..0000000000 --- a/apps/workbench/app/models/group.rb +++ /dev/null @@ -1,54 +0,0 @@ -# Copyright (C) The Arvados Authors. All rights reserved. -# -# SPDX-License-Identifier: AGPL-3.0 - -class Group < ArvadosBase - def self.goes_in_projects? - true - end - - def self.copies_to_projects? - false - end - - def self.contents params={} - res = arvados_api_client.api self, "/contents", { - _method: 'GET' - }.merge(params) - ret = ArvadosResourceList.new - ret.results = arvados_api_client.unpack_api_response(res) - ret - end - - def editable? - if group_class == 'filter' - return false - end - super - end - - def contents params={} - res = arvados_api_client.api self.class, "/#{self.uuid}/contents", { - _method: 'GET' - }.merge(params) - ret = ArvadosResourceList.new - ret.results = arvados_api_client.unpack_api_response(res) - ret - end - - def class_for_display - (group_class == 'project' or group_class == 'filter') ? 'Project' : super - end - - def textile_attributes - [ 'description' ] - end - - def self.creatable? - false - end - - def untrash - arvados_api_client.api(self.class, "/#{self.uuid}/untrash", {"ensure_unique_name" => true}) - end -end diff --git a/apps/workbench/app/models/human.rb b/apps/workbench/app/models/human.rb deleted file mode 100644 index c1acef5645..0000000000 --- a/apps/workbench/app/models/human.rb +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (C) The Arvados Authors. All rights reserved. -# -# SPDX-License-Identifier: AGPL-3.0 - -class Human < ArvadosBase - def self.goes_in_projects? - true - end -end diff --git a/apps/workbench/app/models/job.rb b/apps/workbench/app/models/job.rb deleted file mode 100644 index 7c55d9e857..0000000000 --- a/apps/workbench/app/models/job.rb +++ /dev/null @@ -1,63 +0,0 @@ -# Copyright (C) The Arvados Authors. All rights reserved. -# -# SPDX-License-Identifier: AGPL-3.0 - -class Job < ArvadosBase - def self.goes_in_projects? - true - end - - def content_summary - "#{script} job" - end - - def editable_attributes - %w(description) - end - - def default_name - if script - x = "\"#{script}\" job" - else - x = super - end - if finished_at - x += " finished #{finished_at.strftime('%b %-d')}" - elsif started_at - x += " started #{started_at.strftime('%b %-d')}" - elsif created_at - x += " submitted #{created_at.strftime('%b %-d')}" - end - end - - def cancel - arvados_api_client.api "jobs/#{self.uuid}/", "cancel", {"cascade" => true} - end - - def self.queue_size - arvados_api_client.api("jobs/", "queue_size", {"_method"=> "GET"})[:queue_size] rescue 0 - end - - def self.queue - arvados_api_client.unpack_api_response arvados_api_client.api("jobs/", "queue", {"_method"=> "GET"}) - end - - def textile_attributes - [ 'description' ] - end - - def stderr_log_query(limit=nil) - query = Log.where(object_uuid: self.uuid).order("created_at DESC").with_count('none') - query = query.limit(limit) if limit - query - end - - def stderr_log_lines(limit=2000) - stderr_log_query(limit).results.reverse. - flat_map { |log| log.properties[:text].split("\n") rescue [] } - end - - def work_unit(label=nil) - JobWorkUnit.new(self, label, self.uuid) - end -end diff --git a/apps/workbench/app/models/job_task.rb b/apps/workbench/app/models/job_task.rb deleted file mode 100644 index b10a2b0fd6..0000000000 --- a/apps/workbench/app/models/job_task.rb +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (C) The Arvados Authors. All rights reserved. -# -# SPDX-License-Identifier: AGPL-3.0 - -class JobTask < ArvadosBase - def work_unit(label=nil) - JobTaskWorkUnit.new(self, label, self.uuid) - end -end diff --git a/apps/workbench/app/models/job_task_work_unit.rb b/apps/workbench/app/models/job_task_work_unit.rb deleted file mode 100644 index f5cd526c55..0000000000 --- a/apps/workbench/app/models/job_task_work_unit.rb +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (C) The Arvados Authors. All rights reserved. -# -# SPDX-License-Identifier: AGPL-3.0 - -class JobTaskWorkUnit < ProxyWorkUnit - def title - "job task" - end -end diff --git a/apps/workbench/app/models/job_work_unit.rb b/apps/workbench/app/models/job_work_unit.rb deleted file mode 100644 index 83825a5338..0000000000 --- a/apps/workbench/app/models/job_work_unit.rb +++ /dev/null @@ -1,100 +0,0 @@ -# Copyright (C) The Arvados Authors. All rights reserved. -# -# SPDX-License-Identifier: AGPL-3.0 - -class JobWorkUnit < ProxyWorkUnit - def children - return @my_children if @my_children - - # Jobs components - items = [] - components = get(:components) - uuids = components.andand.collect {|_, v| v} - return items if (!uuids or uuids.empty?) - - rcs = {} - uuids.each do |u| - r = ArvadosBase::resource_class_for_uuid(u) - rcs[r] = [] unless rcs[r] - rcs[r] << u - end - rcs.each do |rc, ids| - rc.where(uuid: ids).each do |obj| - items << obj.work_unit(components.key(obj.uuid)) - end - end - - @my_children = items - end - - def child_summary - if children.any? - super - else - get(:tasks_summary) - end - end - - def parameters - get(:script_parameters) - end - - def repository - get(:repository) - end - - def script - get(:script) - end - - def script_version - get(:script_version) - end - - def supplied_script_version - get(:supplied_script_version) - end - - def docker_image - get(:docker_image_locator) - end - - def nondeterministic - get(:nondeterministic) - end - - def runtime_constraints - get(:runtime_constraints) - end - - def priority - get(:priority) - end - - def log_collection - get(:log) - end - - def outputs - items = [] - items << get(:output) if get(:output) - items - end - - def can_cancel? - state_label.in? ["Queued", "Running"] - end - - def confirm_cancellation - "All unfinished child jobs and pipelines will also be canceled, even if they are being used in another job or pipeline. Are you sure you want to cancel this job?" - end - - def uri - uuid = get(:uuid) - "/jobs/#{uuid}" - end - - def title - "job" - end -end diff --git a/apps/workbench/app/models/keep_disk.rb b/apps/workbench/app/models/keep_disk.rb deleted file mode 100644 index f4fea2ce9b..0000000000 --- a/apps/workbench/app/models/keep_disk.rb +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (C) The Arvados Authors. All rights reserved. -# -# SPDX-License-Identifier: AGPL-3.0 - -class KeepDisk < ArvadosBase - def self.creatable? - false - end -end diff --git a/apps/workbench/app/models/keep_service.rb b/apps/workbench/app/models/keep_service.rb deleted file mode 100644 index 2fea18aecf..0000000000 --- a/apps/workbench/app/models/keep_service.rb +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (C) The Arvados Authors. All rights reserved. -# -# SPDX-License-Identifier: AGPL-3.0 - -class KeepService < ArvadosBase - def self.creatable? - false - end -end diff --git a/apps/workbench/app/models/link.rb b/apps/workbench/app/models/link.rb deleted file mode 100644 index 920b4bdcc5..0000000000 --- a/apps/workbench/app/models/link.rb +++ /dev/null @@ -1,29 +0,0 @@ -# Copyright (C) The Arvados Authors. All rights reserved. -# -# SPDX-License-Identifier: AGPL-3.0 - -class Link < ArvadosBase - attr_accessor :head - attr_accessor :tail - def self.by_tail(t, opts={}) - where(opts.merge :tail_uuid => t.uuid) - end - - def default_name - self.class.resource_class_for_uuid(head_uuid).default_name rescue super - end - - def self.permissions_for(thing) - if thing.respond_to? :uuid - uuid = thing.uuid - else - uuid = thing - end - result = arvados_api_client.api("permissions", "/#{uuid}") - arvados_api_client.unpack_api_response(result) - end - - def self.creatable? - false - end -end diff --git a/apps/workbench/app/models/log.rb b/apps/workbench/app/models/log.rb deleted file mode 100644 index 6bbefa19d8..0000000000 --- a/apps/workbench/app/models/log.rb +++ /dev/null @@ -1,12 +0,0 @@ -# Copyright (C) The Arvados Authors. All rights reserved. -# -# SPDX-License-Identifier: AGPL-3.0 - -class Log < ArvadosBase - attr_accessor :object - def self.creatable? - # Technically yes, but not worth offering: it will be empty, and - # you won't be able to edit it. - false - end -end diff --git a/apps/workbench/app/models/node.rb b/apps/workbench/app/models/node.rb deleted file mode 100644 index 785cc4fcfe..0000000000 --- a/apps/workbench/app/models/node.rb +++ /dev/null @@ -1,12 +0,0 @@ -# Copyright (C) The Arvados Authors. All rights reserved. -# -# SPDX-License-Identifier: AGPL-3.0 - -class Node < ArvadosBase - def self.creatable? - false - end - def friendly_link_name lookup=nil - (hostname && !hostname.empty?) ? hostname : uuid - end -end diff --git a/apps/workbench/app/models/pipeline_instance.rb b/apps/workbench/app/models/pipeline_instance.rb deleted file mode 100644 index d481f41c7e..0000000000 --- a/apps/workbench/app/models/pipeline_instance.rb +++ /dev/null @@ -1,153 +0,0 @@ -# Copyright (C) The Arvados Authors. All rights reserved. -# -# SPDX-License-Identifier: AGPL-3.0 - -require "arvados/keep" - -class PipelineInstance < ArvadosBase - attr_accessor :pipeline_template - - def self.goes_in_projects? - true - end - - def friendly_link_name lookup=nil - pipeline_name = self.name - if pipeline_name.nil? or pipeline_name.empty? - template = if lookup and lookup[self.pipeline_template_uuid] - lookup[self.pipeline_template_uuid] - else - PipelineTemplate.find?(self.pipeline_template_uuid) if self.pipeline_template_uuid - end - if template - template.name - else - self.uuid - end - else - pipeline_name - end - end - - def content_summary - begin - PipelineTemplate.find(pipeline_template_uuid).name - rescue - super - end - end - - def update_job_parameters(new_params) - self.components[:steps].each_with_index do |step, i| - step[:params].each do |param| - if new_params.has_key?(new_param_name = "#{i}/#{param[:name]}") or - new_params.has_key?(new_param_name = "#{step[:name]}/#{param[:name]}") or - new_params.has_key?(new_param_name = param[:name]) - param_type = :value - %w(hash data_locator).collect(&:to_sym).each do |ptype| - param_type = ptype if param.has_key? ptype - end - param[param_type] = new_params[new_param_name] - end - end - end - end - - def editable_attributes - %w(name description components) - end - - def attribute_editable?(name, ever=nil) - if name.to_s == "components" - (ever or %w(New Ready).include?(state)) and super - else - super - end - end - - def attributes_for_display - super.reject { |k,v| k == 'components' } - end - - def self.creatable? - false - end - - def component_input_title(component_name, input_name) - component = components[component_name] - return nil if component.nil? - param_info = component[:script_parameters].andand[input_name.to_sym] - if param_info.is_a?(Hash) and param_info[:title] - param_info[:title] - else - "\"#{input_name.to_s}\" parameter for #{component[:script]} script in #{component_name} component" - end - end - - def textile_attributes - [ 'description' ] - end - - def job_uuids - components_map { |cspec| cspec[:job][:uuid] rescue nil } - end - - def job_log_ids - components_map { |cspec| cspec[:job][:log] rescue nil } - end - - def job_ids - components_map { |cspec| cspec[:job][:uuid] rescue nil } - end - - def stderr_log_object_uuids - result = job_uuids.values.compact - result << uuid - end - - def stderr_log_query(limit=nil) - query = Log. - with_count('none'). - where(event_type: "stderr", - object_uuid: stderr_log_object_uuids). - order("created_at DESC") - unless limit.nil? - query = query.limit(limit) - end - query - end - - def stderr_log_lines(limit=2000) - stderr_log_query(limit).results.reverse. - flat_map { |log| log.properties[:text].split("\n") rescue [] } - end - - def has_readable_logs? - log_pdhs, log_uuids = job_log_ids.values.compact.partition do |loc_s| - Keep::Locator.parse(loc_s) - end - if log_pdhs.any? and - Collection.where(portable_data_hash: log_pdhs).limit(1).with_count("none").results.any? - true - elsif log_uuids.any? and - Collection.where(uuid: log_uuids).limit(1).with_count("none").results.any? - true - else - stderr_log_query(1).results.any? - end - end - - def work_unit(label=nil) - PipelineInstanceWorkUnit.new(self, label || self.name, self.uuid) - end - - def cancel - arvados_api_client.api "pipeline_instances/#{self.uuid}/", "cancel", {"cascade" => true} - end - - private - - def components_map - Hash[components.map { |cname, cspec| [cname, yield(cspec)] }] - end -end diff --git a/apps/workbench/app/models/pipeline_instance_work_unit.rb b/apps/workbench/app/models/pipeline_instance_work_unit.rb deleted file mode 100644 index 1d75f58433..0000000000 --- a/apps/workbench/app/models/pipeline_instance_work_unit.rb +++ /dev/null @@ -1,80 +0,0 @@ -# Copyright (C) The Arvados Authors. All rights reserved. -# -# SPDX-License-Identifier: AGPL-3.0 - -class PipelineInstanceWorkUnit < ProxyWorkUnit - def children - return @my_children if @my_children - - items = [] - - jobs = {} - results = Job.where(uuid: @proxied.job_ids.values).with_count("none").results - results.each do |j| - jobs[j.uuid] = j - end - - components = get(:components) - components.each do |name, c| - if c.is_a?(Hash) - job = c[:job] - if job - if job[:uuid] and jobs[job[:uuid]] - items << jobs[job[:uuid]].work_unit(name) - else - items << JobWorkUnit.new(job, name, uuid) - end - else - items << JobWorkUnit.new(c, name, uuid) - end - else - @unreadable_children = true - break - end - end - - @my_children = items - end - - def outputs - items = [] - components = get(:components) - components.each do |name, c| - if c.is_a?(Hash) - items << c[:output_uuid] if c[:output_uuid] - end - end - items - end - - def uri - uuid = get(:uuid) - "/pipeline_instances/#{uuid}" - end - - def title - "pipeline" - end - - def template_uuid - get(:pipeline_template_uuid) - end - - def state_label - if get(:state) != "Failed" - return super - end - if get(:components_summary).andand[:failed].andand > 0 - return super - end - # Show "Cancelled" instead of "Failed" if there are no failed - # components. #12840 - get(:components).each do |_, c| - jstate = c[:job][:state] rescue nil - if jstate == "Failed" - return "Failed" - end - end - "Cancelled" - end -end diff --git a/apps/workbench/app/models/pipeline_template.rb b/apps/workbench/app/models/pipeline_template.rb deleted file mode 100644 index bce0f08d6b..0000000000 --- a/apps/workbench/app/models/pipeline_template.rb +++ /dev/null @@ -1,17 +0,0 @@ -# Copyright (C) The Arvados Authors. All rights reserved. -# -# SPDX-License-Identifier: AGPL-3.0 - -class PipelineTemplate < ArvadosBase - def self.goes_in_projects? - true - end - - def self.creatable? - false - end - - def textile_attributes - [ 'description' ] - end -end diff --git a/apps/workbench/app/models/proxy_work_unit.rb b/apps/workbench/app/models/proxy_work_unit.rb deleted file mode 100644 index adf0bd7d67..0000000000 --- a/apps/workbench/app/models/proxy_work_unit.rb +++ /dev/null @@ -1,339 +0,0 @@ -# Copyright (C) The Arvados Authors. All rights reserved. -# -# SPDX-License-Identifier: AGPL-3.0 - -class ProxyWorkUnit < WorkUnit - require 'time' - - attr_accessor :lbl - attr_accessor :proxied - attr_accessor :my_children - attr_accessor :unreadable_children - - def initialize proxied, label, parent - @lbl = label - @proxied = proxied - @parent = parent - end - - def label - @lbl - end - - def uuid - get(:uuid) - end - - def parent - @parent - end - - def modified_by_user_uuid - get(:modified_by_user_uuid) - end - - def owner_uuid - get(:owner_uuid) - end - - def created_at - t = get(:created_at) - t = Time.parse(t) if (t.is_a? String) - t - end - - def started_at - t = get(:started_at) - t = Time.parse(t) if (t.is_a? String) - t - end - - def modified_at - t = get(:modified_at) - t = Time.parse(t) if (t.is_a? String) - t - end - - def finished_at - t = get(:finished_at) - t = Time.parse(t) if (t.is_a? String) - t - end - - def state_label - state = get(:state) - if ["Running", "RunningOnServer", "RunningOnClient"].include? state - "Running" - elsif state == 'New' - "Not started" - else - state - end - end - - def state_bootstrap_class - state = state_label - case state - when 'Complete' - 'success' - when 'Failed', 'Cancelled' - 'danger' - when 'Running', 'RunningOnServer', 'RunningOnClient' - 'info' - else - 'default' - end - end - - def success? - state = state_label - if state == 'Complete' - true - elsif state == 'Failed' or state == 'Cancelled' - false - else - nil - end - end - - def child_summary - done = 0 - failed = 0 - todo = 0 - running = 0 - children.each do |c| - case c.state_label - when 'Complete' - done = done+1 - when 'Failed', 'Cancelled' - failed = failed+1 - when 'Running' - running = running+1 - else - todo = todo+1 - end - end - - summary = {} - summary[:done] = done - summary[:failed] = failed - summary[:todo] = todo - summary[:running] = running - summary - end - - def child_summary_str - summary = child_summary - summary_txt = '' - - if state_label == 'Running' - done = summary[:done] || 0 - running = summary[:running] || 0 - failed = summary[:failed] || 0 - todo = summary[:todo] || 0 - total = done + running + failed + todo - - if total > 0 - summary_txt += "#{summary[:done]} #{'child'.pluralize(summary[:done])} done," - summary_txt += "#{summary[:failed]} failed," - summary_txt += "#{summary[:running]} running," - summary_txt += "#{summary[:todo]} pending" - end - end - summary_txt - end - - def progress - state = state_label - if state == 'Complete' - return 1.0 - elsif state == 'Failed' or state == 'Cancelled' - return 0.0 - end - - summary = child_summary - return 0.0 if summary.nil? - - done = summary[:done] || 0 - running = summary[:running] || 0 - failed = summary[:failed] || 0 - todo = summary[:todo] || 0 - total = done + running + failed + todo - if total > 0 - (done+failed).to_f / total - else - 0.0 - end - end - - def children - [] - end - - def outputs - [] - end - - def title - "process" - end - - def has_unreadable_children - @unreadable_children - end - - def walltime - if state_label != "Queued" - if started_at - ((if finished_at then finished_at else Time.now() end) - started_at) - end - end - end - - def cputime - if children.any? - children.map { |c| - c.cputime - }.reduce(:+) || 0 - else - if started_at - (runtime_constraints.andand[:min_nodes] || 1).to_i * ((finished_at || Time.now()) - started_at) - else - 0 - end - end - end - - def queuedtime - if state_label == "Queued" - Time.now - Time.parse(created_at.to_s) - end - end - - def is_running? - state_label == 'Running' - end - - def is_paused? - state_label == 'Paused' - end - - def is_finished? - state_label.in? ["Complete", "Failed", "Cancelled"] - end - - def is_failed? - state_label == 'Failed' - end - - def runtime_contributors - contributors = [] - if children.any? - children.each{|c| contributors << c.runtime_contributors} - else - contributors << self - end - contributors.flatten - end - - def runningtime - ApplicationController.helpers.determine_wallclock_runtime runtime_contributors - end - - def show_runtime - walltime = 0 - running_time = runningtime - if started_at - walltime = if finished_at then (finished_at - started_at) else (Time.now - started_at) end - end - resp = '

    ' - - if started_at - resp << "This #{title} started at " - resp << ApplicationController.helpers.render_localized_date(started_at) - resp << ". It " - if state_label == 'Complete' - resp << "completed in " - elsif state_label == 'Failed' - resp << "failed after " - elsif state_label == 'Cancelled' - resp << "was cancelled after " - else - resp << "has been active for " - end - - resp << ApplicationController.helpers.render_time(walltime, false) - - if finished_at - resp << " at " - resp << ApplicationController.helpers.render_localized_date(finished_at) - end - resp << "." - else - if state_label - resp << "This #{title} is " - resp << if state_label == 'Running' then 'active' else state_label.downcase end - resp << "." - end - end - - if is_failed? - if runtime_status.andand[:error] - resp << " Check the error information below." - else - resp << " Check the Log tab for more detail about why it failed." - end - end - resp << "

    " - - resp << "

    " - if state_label - resp << "It has runtime of " - - cpu_time = cputime - - resp << ApplicationController.helpers.render_time(running_time, false) - if (walltime - running_time) > 0 - resp << "(" - resp << ApplicationController.helpers.render_time(walltime - running_time, false) - resp << "queued)" - end - if cpu_time == 0 - resp << "." - else - resp << " and used " - resp << ApplicationController.helpers.render_time(cpu_time, false) - resp << " of node allocation time (" - resp << (cpu_time/running_time).round(1).to_s - resp << "⨯ scaling)." - end - end - resp << "

    " - - resp - end - - def log_object_uuids - [uuid] - end - - def live_log_lines(limit) - Log.where(object_uuid: log_object_uuids). - order("created_at DESC"). - limit(limit). - with_count('none'). - select { |log| log.properties[:text].is_a? String }. - reverse. - flat_map { |log| log.properties[:text].split("\n") } - end - - protected - - def get key, obj=@proxied - if obj.respond_to? key - obj.send(key) - elsif obj.is_a?(Hash) - obj[key] || obj[key.to_s] - end - end -end diff --git a/apps/workbench/app/models/repository.rb b/apps/workbench/app/models/repository.rb deleted file mode 100644 index fd30be9462..0000000000 --- a/apps/workbench/app/models/repository.rb +++ /dev/null @@ -1,119 +0,0 @@ -# Copyright (C) The Arvados Authors. All rights reserved. -# -# SPDX-License-Identifier: AGPL-3.0 - -class Repository < ArvadosBase - def self.creatable? - false - end - def attributes_for_display - super.reject { |x| x[0] == 'fetch_url' } - end - def editable_attributes - if current_user.is_admin - super - else - [] - end - end - - def show commit_sha1 - refresh - run_git 'show', commit_sha1 - end - - def cat_file commit_sha1, path - refresh - run_git 'cat-file', 'blob', commit_sha1 + ':' + path - end - - def ls_tree_lr commit_sha1 - refresh - run_git 'ls-tree', '-l', '-r', commit_sha1 - end - - # subtree returns a list of files under the given path at the - # specified commit. Results are returned as an array of file nodes, - # where each file node is an array [file mode, blob sha1, file size - # in bytes, path relative to the given directory]. If the path is - # not found, [] is returned. - def ls_subtree commit, path - path = path.chomp '/' - subtree = [] - ls_tree_lr(commit).each_line do |line| - mode, type, sha1, size, filepath = line.split - next if type != 'blob' - if filepath[0,path.length] == path and - (path == '' or filepath[path.length] == '/') - subtree << [mode.to_i(8), sha1, size.to_i, - filepath[path.length,filepath.length]] - end - end - subtree - end - - # http_fetch_url returns the first http:// or https:// url (if any) - # in the api response's clone_urls attribute. - def http_fetch_url - clone_urls.andand.select { |u| /^http/ =~ u }.first - end - - protected - - # refresh fetches the latest repository content into the local - # cache. It is a no-op if it has already been run on this object: - # this (pretty much) avoids doing more than one remote git operation - # per Workbench request. - def refresh - run_git 'fetch', http_fetch_url, '+*:*' unless @fresh - @fresh = true - end - - # run_git sets up the ARVADOS_API_TOKEN environment variable, - # creates a local git directory for this repository if necessary, - # executes "git --git-dir localgitdir {args to run_git}", and - # returns the output. It raises GitCommandError if git exits - # non-zero. - def run_git *gitcmd - if not @workdir - workdir = File.expand_path uuid+'.git', Rails.configuration.Workbench.RepositoryCache - if not File.exists? workdir - FileUtils.mkdir_p Rails.configuration.Workbench.RepositoryCache - [['git', 'init', '--bare', workdir], - ].each do |cmd| - system(*cmd, in: "/dev/null") - raise GitCommandError.new($?.to_s) unless $?.exitstatus == 0 - end - end - @workdir = workdir - end - [['git', '--git-dir', @workdir, 'config', '--local', - "credential.#{http_fetch_url}.username", 'none'], - ['git', '--git-dir', @workdir, 'config', '--local', - "credential.#{http_fetch_url}.helper", - '!cred(){ cat >/dev/null; if [ "$1" = get ]; then echo password=$ARVADOS_API_TOKEN; fi; };cred'], - ['git', '--git-dir', @workdir, 'config', '--local', - 'http.sslVerify', - Rails.configuration.TLS.Insecure ? 'false' : 'true'], - ].each do |cmd| - system(*cmd, in: "/dev/null") - raise GitCommandError.new($?.to_s) unless $?.exitstatus == 0 - end - env = {}. - merge(ENV). - merge('ARVADOS_API_TOKEN' => Thread.current[:arvados_api_token], - 'GIT_TERMINAL_PROMPT' => '0') - cmd = ['git', '--git-dir', @workdir] + gitcmd - io = IO.popen(env, cmd, err: [:child, :out], in: "/dev/null") - output = io.read - io.close - # "If [io] is opened by IO.popen, close sets $?." --ruby 2.2.1 docs - unless $?.exitstatus == 0 - raise GitCommandError.new("`git #{gitcmd.join ' '}` #{$?}: #{output}") - end - output - end - - class GitCommandError < StandardError - end -end diff --git a/apps/workbench/app/models/specimen.rb b/apps/workbench/app/models/specimen.rb deleted file mode 100644 index 4418f7c947..0000000000 --- a/apps/workbench/app/models/specimen.rb +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (C) The Arvados Authors. All rights reserved. -# -# SPDX-License-Identifier: AGPL-3.0 - -class Specimen < ArvadosBase - def self.goes_in_projects? - true - end -end diff --git a/apps/workbench/app/models/trait.rb b/apps/workbench/app/models/trait.rb deleted file mode 100644 index 421a107665..0000000000 --- a/apps/workbench/app/models/trait.rb +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (C) The Arvados Authors. All rights reserved. -# -# SPDX-License-Identifier: AGPL-3.0 - -class Trait < ArvadosBase - def self.goes_in_projects? - true - end -end diff --git a/apps/workbench/app/models/user.rb b/apps/workbench/app/models/user.rb deleted file mode 100644 index c4b273c6b8..0000000000 --- a/apps/workbench/app/models/user.rb +++ /dev/null @@ -1,115 +0,0 @@ -# Copyright (C) The Arvados Authors. All rights reserved. -# -# SPDX-License-Identifier: AGPL-3.0 - -class User < ArvadosBase - def initialize(*args) - super(*args) - @attribute_sortkey['first_name'] = '050' - @attribute_sortkey['last_name'] = '051' - end - - def self.current - res = arvados_api_client.api self, '/current', nil, {}, false - arvados_api_client.unpack_api_response(res) - end - - def self.merge new_user_token, direction - # Merge user accounts. - # - # If the direction is "in", the current user is merged into the - # user represented by new_user_token - # - # If the direction is "out", the user represented by new_user_token - # is merged into the current user. - - if direction == "in" - user_a = new_user_token - user_b = Thread.current[:arvados_api_token] - new_group_name = "Migrated from #{Thread.current[:user].email} (#{Thread.current[:user].uuid})" - elsif direction == "out" - user_a = Thread.current[:arvados_api_token] - user_b = new_user_token - res = arvados_api_client.api self, '/current', nil, {:arvados_api_token => user_b}, false - user_b_info = arvados_api_client.unpack_api_response(res) - new_group_name = "Migrated from #{user_b_info.email} (#{user_b_info.uuid})" - else - raise "Invalid merge direction, expected 'in' or 'out'" - end - - # Create a project owned by user_a to accept everything owned by user_b - res = arvados_api_client.api Group, nil, {:group => { - :name => new_group_name, - :group_class => "project"}, - :ensure_unique_name => true}, - {:arvados_api_token => user_a}, false - target = arvados_api_client.unpack_api_response(res) - - # The merge API merges the "current" user (user_b) into the user - # represented by "new_user_token" (user_a). - # After merging, the user_b redirects to user_a. - res = arvados_api_client.api self, '/merge', {:new_user_token => user_a, - :new_owner_uuid => target[:uuid], - :redirect_to_new_user => true}, - {:arvados_api_token => user_b}, false - arvados_api_client.unpack_api_response(res) - end - - def self.system - @@arvados_system_user ||= begin - res = arvados_api_client.api self, '/system' - arvados_api_client.unpack_api_response(res) - end - end - - def full_name - (self.first_name || "") + " " + (self.last_name || "") - end - - def activate - self.private_reload(arvados_api_client.api(self.class, - "/#{self.uuid}/activate", - {})) - end - - def contents params={} - Group.contents params.merge(uuid: self.uuid) - end - - def attributes_for_display - super.reject { |k,v| %w(owner_uuid default_owner_uuid identity_url prefs).index k } - end - - def attribute_editable?(attr, ever=nil) - (ever or not (self.uuid.andand.match(/000000000000000$/) and - self.is_admin)) and super - end - - def friendly_link_name lookup=nil - [self.first_name, self.last_name].compact.join ' ' - end - - def unsetup - self.private_reload(arvados_api_client.api(self.class, - "/#{self.uuid}/unsetup", - {})) - end - - def self.setup params - arvados_api_client.api(self, "/setup", params) - end - - def update_profile params - self.private_reload(arvados_api_client.api(self.class, - "/#{self.uuid}/profile", - params)) - end - - def deletable? - false - end - - def self.creatable? - current_user.andand.is_admin - end -end diff --git a/apps/workbench/app/models/user_agreement.rb b/apps/workbench/app/models/user_agreement.rb deleted file mode 100644 index fbba426899..0000000000 --- a/apps/workbench/app/models/user_agreement.rb +++ /dev/null @@ -1,14 +0,0 @@ -# Copyright (C) The Arvados Authors. All rights reserved. -# -# SPDX-License-Identifier: AGPL-3.0 - -class UserAgreement < ArvadosBase - def self.signatures - res = arvados_api_client.api self, '/signatures' - arvados_api_client.unpack_api_response(res) - end - def self.sign(params) - res = arvados_api_client.api self, '/sign', params - arvados_api_client.unpack_api_response(res) - end -end diff --git a/apps/workbench/app/models/virtual_machine.rb b/apps/workbench/app/models/virtual_machine.rb deleted file mode 100644 index a81d76fd79..0000000000 --- a/apps/workbench/app/models/virtual_machine.rb +++ /dev/null @@ -1,30 +0,0 @@ -# Copyright (C) The Arvados Authors. All rights reserved. -# -# SPDX-License-Identifier: AGPL-3.0 - -class VirtualMachine < ArvadosBase - attr_accessor :current_user_logins - - def self.creatable? - false - end - - def attributes_for_display - super.append ['current_user_logins', @current_user_logins] - end - - def editable_attributes - super - %w(current_user_logins) - end - - def self.attribute_info - merger = ->(k,a,b) { a.merge(b, &merger) } - merger [nil, - {current_user_logins: {column_heading: "logins", type: 'array'}}, - super] - end - - def friendly_link_name lookup=nil - (hostname && !hostname.empty?) ? hostname : uuid - end -end diff --git a/apps/workbench/app/models/work_unit.rb b/apps/workbench/app/models/work_unit.rb deleted file mode 100644 index 493dd2f578..0000000000 --- a/apps/workbench/app/models/work_unit.rb +++ /dev/null @@ -1,218 +0,0 @@ -# Copyright (C) The Arvados Authors. All rights reserved. -# -# SPDX-License-Identifier: AGPL-3.0 - -class WorkUnit - # This is an abstract class that documents the WorkUnit interface - - def label - # returns the label that was assigned when creating the work unit - end - - def uuid - # returns the arvados UUID of the underlying object - end - - def parent - # returns the parent uuid of this work unit - end - - def children - # returns an array of child work units - end - - def modified_by_user_uuid - # returns uuid of the user who modified this work unit most recently - end - - def owner_uuid - # returns uuid of the owner of this work unit - end - - def created_at - # returns created_at timestamp - end - - def modified_at - # returns modified_at timestamp - end - - def started_at - # returns started_at timestamp for this work unit - end - - def finished_at - # returns finished_at timestamp - end - - def state_label - # returns a string representing state of the work unit - end - - def exit_code - # returns the work unit's execution exit code - end - - def state_bootstrap_class - # returns a class like "danger", "success", or "warning" that a view can use directly to make a display class - end - - def success? - # returns true if the work unit finished successfully, - # false if it has a permanent failure, - # and nil if the final state is not determined. - end - - def progress - # returns a number between 0 and 1 - end - - def log_collection - # returns uuid or pdh with saved log data, if any - end - - def parameters - # returns work unit parameters, if any - end - - def script - # returns script for this work unit, if any - end - - def repository - # returns this work unit's script repository, if any - end - - def script_version - # returns this work unit's script_version, if any - end - - def supplied_script_version - # returns this work unit's supplied_script_version, if any - end - - def docker_image - # returns this work unit's docker_image, if any - end - - def runtime_constraints - # returns this work unit's runtime_constraints, if any - end - - def priority - # returns this work unit's priority, if any - end - - def nondeterministic - # returns if this is nondeterministic - end - - def outputs - # returns array containing uuid or pdh of output data - end - - def child_summary - # summary status of any children of this work unit - end - - def child_summary_str - # textual representation of child summary - end - - def can_cancel? - # returns true if this work unit can be canceled - end - - def confirm_cancellation - # returns true if this work unit wants to use a confirmation for cancellation - end - - def uri - # returns the uri for this work unit - end - - def title - # title for the work unit - end - - def has_unreadable_children - # accept it if you can't understand your own children - end - - # view helper methods - def walltime - # return walltime for a running or completed work unit - end - - def cputime - # return cputime for a running or completed work unit - end - - def queuedtime - # return queued time if the work unit is queued - end - - def is_running? - # is the work unit in running state? - end - - def is_paused? - # is the work unit in paused state? - end - - def is_finished? - # is the work unit in finished state? - end - - def is_failed? - # is this work unit in failed state? - end - - def command - # command to execute - end - - def cwd - # initial workind directory - end - - def environment - # environment variables - end - - def mounts - # mounts - end - - def output_path - # path to a directory or file to save output - end - - def container_uuid - # container_uuid of a container_request - end - - def requesting_container_uuid - # requesting_container_uuid of a container_request - end - - def log_object_uuids - # object uuids for live log - end - - def live_log_lines(limit) - # fetch log entries from logs table for @proxied - end - - def render_log - # return partial and locals to be rendered - end - - def template_uuid - # return the uuid of this work unit's template, if one exists - end - - def runtime_status - # Returns this work unit's runtime_status, if any - end -end diff --git a/apps/workbench/app/models/workflow.rb b/apps/workbench/app/models/workflow.rb deleted file mode 100644 index 31d433e912..0000000000 --- a/apps/workbench/app/models/workflow.rb +++ /dev/null @@ -1,17 +0,0 @@ -# Copyright (C) The Arvados Authors. All rights reserved. -# -# SPDX-License-Identifier: AGPL-3.0 - -class Workflow < ArvadosBase - def self.goes_in_projects? - true - end - - def self.creatable? - false - end - - def textile_attributes - [ 'description' ] - end -end diff --git a/apps/workbench/app/views/api_client_authorizations/_show_help.html.erb b/apps/workbench/app/views/api_client_authorizations/_show_help.html.erb deleted file mode 100644 index 18907ed71b..0000000000 --- a/apps/workbench/app/views/api_client_authorizations/_show_help.html.erb +++ /dev/null @@ -1,18 +0,0 @@ -<%# Copyright (C) The Arvados Authors. All rights reserved. - -SPDX-License-Identifier: AGPL-3.0 %> - -
    -### Pasting the following lines at a shell prompt will allow Arvados SDKs
    -### to authenticate to your account, <%= current_user.email %>
    -
    -read ARVADOS_API_TOKEN <<EOF
    -<%= Thread.current[:arvados_api_token] %>
    -EOF
    -export ARVADOS_API_TOKEN ARVADOS_API_HOST=<%= current_api_host %>
    -<% if Rails.configuration.TLS.Insecure %>
    -export ARVADOS_API_HOST_INSECURE=true
    -<% else %>
    -unset ARVADOS_API_HOST_INSECURE
    -<% end %>
    -
    diff --git a/apps/workbench/app/views/application/404.html.erb b/apps/workbench/app/views/application/404.html.erb deleted file mode 100644 index 61cbd67005..0000000000 --- a/apps/workbench/app/views/application/404.html.erb +++ /dev/null @@ -1,107 +0,0 @@ -<%# Copyright (C) The Arvados Authors. All rights reserved. - -SPDX-License-Identifier: AGPL-3.0 %> - -<% - if (controller.andand.action_name == 'show') and params[:uuid] - check_trash = controller.model_class.include_trash(true).where(uuid: params[:uuid]) - class_name = controller.model_class.to_s.underscore - class_name_h = class_name.humanize(capitalize: false) - req_item = safe_join([class_name_h, " with UUID ", - raw(""), params[:uuid], raw("")], "") - req_item_plain_text = safe_join([class_name_h, " with UUID ", params[:uuid]]) - else - req_item = "page you requested" - req_item_plain_text = "page you requested" - end -%> - - <% untrash_object = nil %> - - <% if check_trash.andand.any? %> - <% object = check_trash.first %> - <% if object.respond_to?(:is_trashed) && object.is_trashed %> - <% untrash_object = object %> - <% else %> - <% owner = object %> - <% while true %> - <% owner = Group.where(uuid: owner.owner_uuid).include_trash(true).first %> - <% if owner.nil? %> - <% break %> - <% end %> - <% if owner.is_trashed %> - <% untrash_object = owner %> - <% break %> - <% end %> - <% end %> - <% end %> - <% end %> - - <% if !untrash_object.nil? %> -

    Trashed

    - - <% untrash_name = if !untrash_object.name.blank? then - "'#{untrash_object.name}'" - else - untrash_object.uuid - end %> - -

    The <%= req_item %> is - <% if untrash_object == object %> - in the trash. - <% else %> - owned by trashed project <%= untrash_name %> (<%= untrash_object.uuid %>). - <% end %> -

    - -

    - It will be permanently deleted at <%= render_localized_date(untrash_object.delete_at) %>. -

    - -

    - <% if untrash_object != object %> - You must untrash the owner project to access this <%= class_name_h %>. - <% end %> - <% if untrash_object.is_trashed and untrash_object.editable? %> - <% msg = "Untrash '#{untrash_name}'?" %> - <%= link_to({action: 'untrash_items', selection: [untrash_object.uuid], controller: :trash_items}, remote: true, method: :post, - title: "Untrash", style: 'cursor: pointer;') do %> - - <% end %> - - <%= form_tag url_for({action: 'untrash_items', controller: :trash_items}), {method: :post} %> - <%= hidden_field_tag :selection, [untrash_object.uuid] %> - - <% end %> -

    - - <% else %> - -

    Not Found

    - -

    The <%= req_item %> was not found.

    - -<% if !current_user %> - -

    - <%= link_to(arvados_api_client.arvados_login_url(return_to: strip_token_from_path(request.url)), - {class: "btn btn-primary report-issue-modal-window"}) do %> - Log in - <% end %> - to view private data. -

    - -<% elsif class_name %> - -

    - Perhaps you'd like to <%= link_to("browse all - #{class_name_h.pluralize}", action: :index, controller: - class_name.tableize) %>? -

    - -<% end %> - -<% end %> - -<% error_message = "The #{req_item_plain_text} was not found." %> -<%= render :partial => "report_error", :locals => {error_message: error_message, error_type: '404'} %> diff --git a/apps/workbench/app/views/application/404.json.erb b/apps/workbench/app/views/application/404.json.erb deleted file mode 100644 index a69749050d..0000000000 --- a/apps/workbench/app/views/application/404.json.erb +++ /dev/null @@ -1,5 +0,0 @@ -<%# Copyright (C) The Arvados Authors. All rights reserved. - -SPDX-License-Identifier: AGPL-3.0 %> - -{"errors":<%= raw @errors.to_json %>} \ No newline at end of file diff --git a/apps/workbench/app/views/application/_arvados_attr_value.html.erb b/apps/workbench/app/views/application/_arvados_attr_value.html.erb deleted file mode 100644 index 98732dc3bf..0000000000 --- a/apps/workbench/app/views/application/_arvados_attr_value.html.erb +++ /dev/null @@ -1,26 +0,0 @@ -<%# Copyright (C) The Arvados Authors. All rights reserved. - -SPDX-License-Identifier: AGPL-3.0 %> - -<% if attrvalue.is_a? Array and attrvalue.collect(&:class).uniq.compact == [String] %> - <% attrvalue.each do |message| %> - <%= message %>
    - <% end %> -<% else %> - <% if attr and obj.attribute_editable?(attr) and (!defined?(editable) || editable) %> - <% if resource_class_for_uuid(attrvalue, {referring_object: obj, referring_attr: attr}) %> - <%= link_to_if_arvados_object attrvalue, {referring_attr: attr, referring_object: obj, with_class_name: true, friendly_name: true} %> -
    - <% end %> - <%= render_editable_attribute obj, attr %> - <% elsif attr == 'uuid' %> - <%= link_to_if_arvados_object attrvalue, {referring_attr: attr, referring_object: obj, with_class_name: false, friendly_name: false} %> - <% else %> - <%= link_to_if_arvados_object attrvalue, {referring_attr: attr, referring_object: obj, with_class_name: true, friendly_name: true, thumbnail: true} %> - <% end %> - -<% end %> diff --git a/apps/workbench/app/views/application/_arvados_object.html.erb b/apps/workbench/app/views/application/_arvados_object.html.erb deleted file mode 100644 index 6d59e0eb59..0000000000 --- a/apps/workbench/app/views/application/_arvados_object.html.erb +++ /dev/null @@ -1,40 +0,0 @@ -<%# Copyright (C) The Arvados Authors. All rights reserved. - -SPDX-License-Identifier: AGPL-3.0 %> - -<% content_for :arvados_object_table do %> - -<% end %> - -<% if content_for? :page_content %> -<%= yield :page_content %> -<% else %> -<%= yield :arvados_object_table %> -<% end %> - -
    - - -
    - <% if content_for? :page_content %> -
    - <%= yield :arvados_object_table %> -
    - <% end %> -
    - -
    - - -
    -
    diff --git a/apps/workbench/app/views/application/_arvados_object_attr.html.erb b/apps/workbench/app/views/application/_arvados_object_attr.html.erb deleted file mode 100644 index 9b9c39f809..0000000000 --- a/apps/workbench/app/views/application/_arvados_object_attr.html.erb +++ /dev/null @@ -1,21 +0,0 @@ -<%# Copyright (C) The Arvados Authors. All rights reserved. - -SPDX-License-Identifier: AGPL-3.0 %> - -<% object ||= @object %> -<% if attrvalue.is_a? Hash then attrvalue.each do |infokey, infocontent| %> - - <%= attr %>[<%= infokey %>] - - <%= render partial: 'application/arvados_attr_value', locals: { obj: object, attr: nil, attrvalue: infocontent } %> - - -<% end %> -<% elsif attrvalue.is_a? String or attrvalue.respond_to? :to_s %> - - <%= attr %> - - <%= render partial: 'application/arvados_attr_value', locals: { obj: object, attr: attr, attrvalue: attrvalue } %> - - -<% end %> diff --git a/apps/workbench/app/views/application/_breadcrumb_page_name.html.erb b/apps/workbench/app/views/application/_breadcrumb_page_name.html.erb deleted file mode 100644 index 0ff635b44c..0000000000 --- a/apps/workbench/app/views/application/_breadcrumb_page_name.html.erb +++ /dev/null @@ -1,5 +0,0 @@ -<%# Copyright (C) The Arvados Authors. All rights reserved. - -SPDX-License-Identifier: AGPL-3.0 %> - - diff --git a/apps/workbench/app/views/application/_breadcrumbs.html.erb b/apps/workbench/app/views/application/_breadcrumbs.html.erb deleted file mode 100644 index c3c2e07da7..0000000000 --- a/apps/workbench/app/views/application/_breadcrumbs.html.erb +++ /dev/null @@ -1,80 +0,0 @@ -<%# Copyright (C) The Arvados Authors. All rights reserved. - -SPDX-License-Identifier: AGPL-3.0 %> - - diff --git a/apps/workbench/app/views/application/_browser_unsupported.html b/apps/workbench/app/views/application/_browser_unsupported.html deleted file mode 100644 index 5424aba269..0000000000 --- a/apps/workbench/app/views/application/_browser_unsupported.html +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - diff --git a/apps/workbench/app/views/application/_choose.html.erb b/apps/workbench/app/views/application/_choose.html.erb deleted file mode 100644 index e3e270853d..0000000000 --- a/apps/workbench/app/views/application/_choose.html.erb +++ /dev/null @@ -1,93 +0,0 @@ -<%# Copyright (C) The Arvados Authors. All rights reserved. - -SPDX-License-Identifier: AGPL-3.0 %> - -