diff --git a/.ci/flake8_wrapper.sh b/.ci/flake8_wrapper.sh index b6fb1a2e0f6..522a7b2abb2 100755 --- a/.ci/flake8_wrapper.sh +++ b/.ci/flake8_wrapper.sh @@ -5,4 +5,4 @@ set -e flake8 --exclude $(paste -sd, .ci/flake8_ignorelist.txt) . # Apply stricter rules for the directories shared with Pulsar -flake8 --ignore=D --max-line-length=150 lib/galaxy/jobs/runners/util/ +flake8 --ignore=E203,D --max-line-length=150 lib/galaxy/jobs/runners/util/ diff --git a/.circleci/config.yml b/.circleci/config.yml index aeb23948fd6..4e82f32b0b7 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -104,7 +104,7 @@ jobs: - run: tox -e test_galaxy_packages js_lint: docker: - - image: cimg/node:14.15 + - image: cimg/node:16.13.2 <<: *set_workdir steps: - *restore_yarn_cache diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 00000000000..8b485445d4f --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,7 @@ +# Migrate code style to Black and Apply isort +623a5e165ad79e15321b19b2a2afbf2f9224cc90 +# Application of black styling to /tools/ +03351d2a9f81b938d650e2a955c65c18526ee7f0 +# Migrate code style to Prettier +5b2928f851bd5ea3b9c2a04abf2cee9ff0bc54cc +87873c5e2f4e6b97fe0f2084bfca0295fcd471de diff --git a/.github/codecov.yml b/.github/codecov.yml new file mode 100644 index 00000000000..69cb76019a4 --- /dev/null +++ b/.github/codecov.yml @@ -0,0 +1 @@ +comment: false diff --git a/.github/workflows/api.yaml b/.github/workflows/api.yaml index d4eb52b1e7a..6f8e17ddc1a 100644 --- a/.github/workflows/api.yaml +++ b/.github/workflows/api.yaml @@ -10,10 +10,13 @@ on: - 'client/**' - 'doc/**' - 'lib/galaxy_test/selenium/**' + schedule: + # Run at midnight UTC every Tuesday + - cron: '0 0 * * 2' env: + GALAXY_DEPENDENCIES_INSTALL_WEASYPRINT: '1' GALAXY_TEST_DBURI: 'postgresql://postgres:postgres@localhost:5432/galaxy?client_encoding=utf8' GALAXY_TEST_RAISE_EXCEPTION_ON_HISTORYLESS_HDA: '1' - GALAXY_DEPENDENCIES_INSTALL_WEASYPRINT: '1' GALAXY_CONFIG_SQLALCHEMY_WARN_20: '1' concurrency: group: api-${{ github.ref }} @@ -35,6 +38,10 @@ jobs: ports: - 5432:5432 steps: + - if: github.event_name == 'schedule' + run: | + echo "GALAXY_CONFIG_OVERRIDE_METADATA_STRATEGY=extended" >> $GITHUB_ENV + echo "GALAXY_CONFIG_OVERRIDE_OUTPUTS_TO_WORKING_DIRECTORY=true" >> $GITHUB_ENV - uses: actions/checkout@v2 with: path: 'galaxy root' @@ -53,11 +60,15 @@ jobs: - name: Cache galaxy venv uses: actions/cache@v2 with: - path: .venv + path: 'galaxy root/.venv' key: gxy-venv-${{ runner.os }}-${{ steps.full-python-version.outputs.version }}-${{ hashFiles('galaxy root/requirements.txt') }}-api - name: Run tests - run: ./run_tests.sh --skip_flakey_fails -api + run: ./run_tests.sh --coverage --skip_flakey_fails -api working-directory: 'galaxy root' + - uses: codecov/codecov-action@v2 + with: + flags: api + working-directory: 'galaxy root' - uses: actions/upload-artifact@v2 if: failure() with: diff --git a/.github/workflows/converter_tests.yaml b/.github/workflows/converter_tests.yaml index db123cd8f48..4625cbe3b7e 100644 --- a/.github/workflows/converter_tests.yaml +++ b/.github/workflows/converter_tests.yaml @@ -8,6 +8,9 @@ on: paths-ignore: - 'client/**' - 'doc/**' + schedule: + # Run at midnight UTC every Tuesday + - cron: '0 0 * * 2' env: GALAXY_TEST_RAISE_EXCEPTION_ON_HISTORYLESS_HDA: '1' concurrency: @@ -21,6 +24,10 @@ jobs: matrix: python-version: ['3.7'] steps: + - if: github.event_name == 'schedule' + run: | + echo "GALAXY_CONFIG_OVERRIDE_METADATA_STRATEGY=extended" >> $GITHUB_ENV + echo "GALAXY_CONFIG_OVERRIDE_OUTPUTS_TO_WORKING_DIRECTORY=true" >> $GITHUB_ENV - uses: actions/checkout@v2 with: path: 'galaxy root' diff --git a/.github/workflows/cwl_conformance.yaml b/.github/workflows/cwl_conformance.yaml index 9e5cf7bdbb7..18c9596452c 100644 --- a/.github/workflows/cwl_conformance.yaml +++ b/.github/workflows/cwl_conformance.yaml @@ -53,11 +53,15 @@ jobs: - name: Cache galaxy venv uses: actions/cache@v2 with: - path: .venv + path: 'galaxy root/.venv' key: gxy-venv-${{ runner.os }}-${{ steps.full-python-version.outputs.version }}-${{ hashFiles('galaxy root/requirements.txt') }} - name: Run tests - run: ./run_tests.sh --skip_flakey_fails -cwl lib/galaxy_test/api/cwl -- -m "${{ matrix.marker }} and ${{ matrix.conformance-version }}" + run: ./run_tests.sh --coverage --skip_flakey_fails -cwl lib/galaxy_test/api/cwl -- -m "${{ matrix.marker }} and ${{ matrix.conformance-version }}" working-directory: 'galaxy root' + - uses: codecov/codecov-action@v2 + with: + flags: cwl-conformance + working-directory: 'galaxy root' - uses: actions/upload-artifact@v2 if: failure() with: diff --git a/.github/workflows/first_startup.yaml b/.github/workflows/first_startup.yaml index 62f4b2647e5..94d78bf2c9e 100644 --- a/.github/workflows/first_startup.yaml +++ b/.github/workflows/first_startup.yaml @@ -22,10 +22,6 @@ jobs: fail-fast: false matrix: python-version: ['3.7', '3.10'] - webserver: ['uwsgi'] - include: - - python-version: '3.10' - webserver: 'dev' defaults: run: shell: bash -l {0} @@ -57,11 +53,6 @@ jobs: yarn-lock-file: 'galaxy root/client/yarn.lock' - name: Install tox run: pip install tox - # Use this job to test the latest migrations - - run: wget -q https://github.com/jmchilton/galaxy-downloads/raw/master/db_gx_rev_0141.sqlite - - run: mv db_gx_rev_0141.sqlite 'galaxy root'/database/universe.sqlite - name: run tests run: tox -e first_startup working-directory: 'galaxy root' - env: - APP_WEBSERVER: ${{ matrix.webserver }} diff --git a/.github/workflows/framework.yaml b/.github/workflows/framework.yaml index c0c3b997428..58a55bbc1e2 100644 --- a/.github/workflows/framework.yaml +++ b/.github/workflows/framework.yaml @@ -10,6 +10,9 @@ on: - 'client/**' - 'doc/**' - 'lib/galaxy_test/selenium/**' + schedule: + # Run at midnight UTC every Tuesday + - cron: '0 0 * * 2' env: GALAXY_TEST_DBURI: 'postgresql://postgres:postgres@localhost:5432/galaxy?client_encoding=utf8' GALAXY_TEST_RAISE_EXCEPTION_ON_HISTORYLESS_HDA: '1' @@ -33,6 +36,10 @@ jobs: ports: - 5432:5432 steps: + - if: github.event_name == 'schedule' + run: | + echo "GALAXY_CONFIG_OVERRIDE_METADATA_STRATEGY=extended" >> $GITHUB_ENV + echo "GALAXY_CONFIG_OVERRIDE_OUTPUTS_TO_WORKING_DIRECTORY=true" >> $GITHUB_ENV - uses: actions/checkout@v2 with: path: 'galaxy root' @@ -51,11 +58,15 @@ jobs: - name: Cache galaxy venv uses: actions/cache@v2 with: - path: .venv + path: 'galaxy root/.venv' key: gxy-venv-${{ runner.os }}-${{ steps.full-python-version.outputs.version }}-${{ hashFiles('galaxy root/requirements.txt') }}-framework - name: Run tests - run: ./run_tests.sh --framework + run: ./run_tests.sh --coverage --framework working-directory: 'galaxy root' + - uses: codecov/codecov-action@v2 + with: + flags: framework + working-directory: 'galaxy root' - uses: actions/upload-artifact@v2 if: failure() with: diff --git a/.github/workflows/integration.yaml b/.github/workflows/integration.yaml index 25bb0d3783a..c7662bd5858 100644 --- a/.github/workflows/integration.yaml +++ b/.github/workflows/integration.yaml @@ -10,11 +10,15 @@ on: - 'client/**' - 'doc/**' - 'lib/galaxy_test/selenium/**' + schedule: + # Run at midnight UTC every Tuesday + - cron: '0 0 * * 2' env: GALAXY_TEST_AMQP_URL: 'amqp://localhost:5672//' GALAXY_TEST_DBURI: 'postgresql://postgres:postgres@localhost:5432/galaxy?client_encoding=utf8' GALAXY_TEST_RAISE_EXCEPTION_ON_HISTORYLESS_HDA: '1' GALAXY_CONFIG_SQLALCHEMY_WARN_20: '1' + GALAXY_DEPENDENCIES_INSTALL_WEASYPRINT: '1' concurrency: group: integration-${{ github.ref }} cancel-in-progress: true @@ -41,6 +45,11 @@ jobs: ports: - 5672:5672 steps: + - if: github.event_name == 'schedule' + run: | + echo "GALAXY_CONFIG_OVERRIDE_METADATA_STRATEGY=extended" >> $GITHUB_ENV + # Skip outputs_to_working_directory: true in integration tests, doesn't work with pulsar + # echo "GALAXY_CONFIG_OVERRIDE_OUTPUTS_TO_WORKING_DIRECTORY=true" >> $GITHUB_ENV - name: Prune unused docker image, volumes and containers run: docker system prune -a -f - name: Clean dotnet folder for space @@ -74,15 +83,19 @@ jobs: - name: Cache galaxy venv uses: actions/cache@v2 with: - path: .venv + path: 'galaxy root/.venv' key: gxy-venv-${{ runner.os }}-${{ steps.full-python-version.outputs.version }}-${{ hashFiles('galaxy root/requirements.txt') }}-integration - name: Install ffmpeg run: sudo apt-get update && sudo apt-get -y install ffmpeg - name: Run tests run: | . .ci/minikube-test-setup/start_services.sh - ./run_tests.sh -integration test/integration -- --num-shards=4 --shard-id=${{ matrix.chunk }} + ./run_tests.sh --coverage -integration test/integration -- --num-shards=4 --shard-id=${{ matrix.chunk }} working-directory: 'galaxy root' + - uses: codecov/codecov-action@v2 + with: + flags: integration + working-directory: 'galaxy root' - uses: actions/upload-artifact@v2 if: failure() with: diff --git a/.github/workflows/integration_selenium.yaml b/.github/workflows/integration_selenium.yaml index 6ee668d9517..e3a45b93f87 100644 --- a/.github/workflows/integration_selenium.yaml +++ b/.github/workflows/integration_selenium.yaml @@ -6,6 +6,9 @@ on: pull_request: paths-ignore: - 'doc/**' + schedule: + # Run at midnight UTC every Tuesday + - cron: '0 0 * * 2' env: GALAXY_SKIP_CLIENT_BUILD: '0' GALAXY_TEST_DBURI: 'postgresql://postgres:postgres@localhost:5432/galaxy?client_encoding=utf8' @@ -13,6 +16,7 @@ env: GALAXY_TEST_SELENIUM_RETRIES: 1 YARN_INSTALL_OPTS: --frozen-lockfile GALAXY_CONFIG_SQLALCHEMY_WARN_20: '1' + GALAXY_DEPENDENCIES_INSTALL_WEASYPRINT: '1' concurrency: group: integration-selenium-${{ github.ref }} cancel-in-progress: true @@ -33,6 +37,10 @@ jobs: ports: - 5432:5432 steps: + - if: github.event_name == 'schedule' + run: | + echo "GALAXY_CONFIG_OVERRIDE_METADATA_STRATEGY=extended" >> $GITHUB_ENV + echo "GALAXY_CONFIG_OVERRIDE_OUTPUTS_TO_WORKING_DIRECTORY=true" >> $GITHUB_ENV - name: Prune unused docker image, volumes and containers run: docker system prune -a -f - uses: actions/checkout@v2 @@ -53,15 +61,19 @@ jobs: - name: Cache galaxy venv uses: actions/cache@v2 with: - path: .venv + path: 'galaxy root/.venv' key: gxy-venv-${{ runner.os }}-${{ steps.full-python-version.outputs.version }}-${{ hashFiles('galaxy root/requirements.txt') }}-integration-selenium - uses: mvdbeek/gha-yarn-cache@master with: yarn-lock-file: 'galaxy root/client/yarn.lock' - uses: nanasess/setup-chromedriver@master - name: Run tests - run: ./run_tests.sh -integration test/integration_selenium + run: ./run_tests.sh --coverage -integration test/integration_selenium working-directory: 'galaxy root' + - uses: codecov/codecov-action@v2 + with: + flags: integration-selenium + working-directory: 'galaxy root' - uses: actions/upload-artifact@v2 if: failure() with: diff --git a/.github/workflows/jest.yaml b/.github/workflows/jest.yaml index 65bfd466843..fd9992f558d 100644 --- a/.github/workflows/jest.yaml +++ b/.github/workflows/jest.yaml @@ -3,9 +3,11 @@ on: push: paths: - 'client/**' + - '.github/workflows/jest.yaml' pull_request: paths: - 'client/**' + - '.github/workflows/jest.yaml' concurrency: group: client-${{ github.ref }} cancel-in-progress: true @@ -14,7 +16,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node: [14] + node: [16] steps: - uses: actions/checkout@v2 - name: Setup node diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index 9dfd554cf3c..f0d45352cb1 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -3,9 +3,13 @@ on: push: paths: - '**.py' + - '.github/workflows/lint.yaml' + - lib/galaxy/dependencies/** pull_request: paths: - '**.py' + - '.github/workflows/lint.yaml' + - lib/galaxy/dependencies/** concurrency: group: py-lint-${{ github.ref }} cancel-in-progress: true @@ -43,3 +47,5 @@ jobs: run: tox -e lint_docstring_include_list - name: Run mypy checks run: tox -e mypy + - uses: psf/black@stable + - uses: jamescurtin/isort-action@master diff --git a/.github/workflows/performance.yaml b/.github/workflows/performance.yaml index 596ee09871e..7b797e6360d 100644 --- a/.github/workflows/performance.yaml +++ b/.github/workflows/performance.yaml @@ -10,6 +10,9 @@ on: - 'client/**' - 'doc/**' - 'lib/galaxy_test/selenium/**' + schedule: + # Run at midnight UTC every Tuesday + - cron: '0 0 * * 2' env: GALAXY_TEST_DBURI: 'postgresql://postgres:postgres@localhost:5432/galaxy?client_encoding=utf8' concurrency: @@ -32,6 +35,10 @@ jobs: ports: - 5432:5432 steps: + - if: github.event_name == 'schedule' + run: | + echo "GALAXY_CONFIG_OVERRIDE_METADATA_STRATEGY=extended" >> $GITHUB_ENV + echo "GALAXY_CONFIG_OVERRIDE_OUTPUTS_TO_WORKING_DIRECTORY=true" >> $GITHUB_ENV - uses: actions/checkout@v2 with: path: 'galaxy root' @@ -50,7 +57,7 @@ jobs: - name: Cache galaxy venv uses: actions/cache@v2 with: - path: .venv + path: 'galaxy root/.venv' key: gxy-venv-${{ runner.os }}-${{ steps.full-python-version.outputs.version }}-${{ hashFiles('galaxy root/requirements.txt') }}-performance - name: Run tests run: ./run_tests.sh --ci_test_metrics --structured_data_html --structured_data_report_file "test.json" --skip_flakey_fails -api lib/galaxy_test/performance diff --git a/.github/workflows/reports_startup.yaml b/.github/workflows/reports_startup.yaml index 59a42974dd3..afc25b4b56d 100644 --- a/.github/workflows/reports_startup.yaml +++ b/.github/workflows/reports_startup.yaml @@ -12,7 +12,6 @@ concurrency: group: reports-startup-${{ github.ref }} cancel-in-progress: true jobs: - test: name: Reports startup test runs-on: ubuntu-latest @@ -41,16 +40,16 @@ jobs: with: path: ~/.cache/pip key: pip-cache-${{ matrix.python-version }}-${{ hashFiles('galaxy root/requirements.txt') }} - - name: Cache tox env + - name: Cache galaxy venv uses: actions/cache@v2 with: - path: .tox - key: tox-cache-${{ runner.os }}-${{ steps.full-python-version.outputs.version }}-${{ hashFiles('galaxy root/requirements.txt') }}-reports-startup + path: 'galaxy root/.venv' + key: gxy-venv-${{ runner.os }}-${{ steps.full-python-version.outputs.version }}-${{ hashFiles('galaxy root/requirements.txt') }}-reports-startup - uses: mvdbeek/gha-yarn-cache@master with: yarn-lock-file: 'galaxy root/client/yarn.lock' - name: Install tox run: pip install tox - - name: run tests + - name: Run tests run: tox -e reports_startup working-directory: 'galaxy root' diff --git a/.github/workflows/selenium.yaml b/.github/workflows/selenium.yaml index 844f4cc0495..fad8b7dd169 100644 --- a/.github/workflows/selenium.yaml +++ b/.github/workflows/selenium.yaml @@ -6,6 +6,9 @@ on: pull_request: paths-ignore: - 'doc/**' + schedule: + # Run at midnight UTC every Tuesday + - cron: '0 0 * * 2' env: GALAXY_CONFIG_GALAXY_URL_PREFIX: '/galaxypf' GALAXY_TEST_DBURI: 'postgresql://postgres:postgres@localhost:5432/galaxy?client_encoding=utf8' @@ -36,6 +39,10 @@ jobs: ports: - 5432:5432 steps: + - if: github.event_name == 'schedule' + run: | + echo "GALAXY_CONFIG_OVERRIDE_METADATA_STRATEGY=extended" >> $GITHUB_ENV + echo "GALAXY_CONFIG_OVERRIDE_OUTPUTS_TO_WORKING_DIRECTORY=true" >> $GITHUB_ENV - uses: actions/checkout@v2 with: path: 'galaxy root' @@ -54,15 +61,19 @@ jobs: - name: Cache galaxy venv uses: actions/cache@v2 with: - path: .venv + path: 'galaxy root/.venv' key: gxy-venv-${{ runner.os }}-${{ steps.full-python-version.outputs.version }}-${{ hashFiles('galaxy root/requirements.txt') }}-selenium - uses: mvdbeek/gha-yarn-cache@master with: yarn-lock-file: 'galaxy root/client/yarn.lock' - uses: nanasess/setup-chromedriver@master - name: Run tests - run: ./run_tests.sh -selenium lib/galaxy_test/selenium -- --num-shards=3 --shard-id=${{ matrix.chunk }} + run: ./run_tests.sh --coverage -selenium lib/galaxy_test/selenium -- --num-shards=3 --shard-id=${{ matrix.chunk }} working-directory: 'galaxy root' + - uses: codecov/codecov-action@v2 + with: + flags: selenium + working-directory: 'galaxy root' - uses: actions/upload-artifact@v2 if: failure() with: diff --git a/.github/workflows/selenium_beta.yaml b/.github/workflows/selenium_beta.yaml index 5eeb2a8dab3..c0ce81f1bf7 100644 --- a/.github/workflows/selenium_beta.yaml +++ b/.github/workflows/selenium_beta.yaml @@ -6,8 +6,12 @@ on: pull_request: paths-ignore: - 'doc/**' + schedule: + # Run at midnight UTC every Tuesday + - cron: '0 0 * * 2' env: GALAXY_CONFIG_GALAXY_URL_PREFIX: '/galaxypf' + GALAXY_CONFIG_USE_LEGACY_HISTORY: false GALAXY_TEST_DBURI: 'postgresql://postgres:postgres@localhost:5432/galaxy?client_encoding=utf8' GALAXY_TEST_RAISE_EXCEPTION_ON_HISTORYLESS_HDA: '1' GALAXY_TEST_SELENIUM_RETRIES: 1 @@ -37,6 +41,10 @@ jobs: ports: - 5432:5432 steps: + - if: github.event_name == 'schedule' + run: | + echo "GALAXY_CONFIG_OVERRIDE_METADATA_STRATEGY=extended" >> $GITHUB_ENV + echo "GALAXY_CONFIG_OVERRIDE_OUTPUTS_TO_WORKING_DIRECTORY=true" >> $GITHUB_ENV - uses: actions/checkout@v2 with: path: 'galaxy root' @@ -55,15 +63,19 @@ jobs: - name: Cache galaxy venv uses: actions/cache@v2 with: - path: .venv + path: 'galaxy root/.venv' key: gxy-venv-${{ runner.os }}-${{ steps.full-python-version.outputs.version }}-${{ hashFiles('galaxy root/requirements.txt') }}-selenium-beta - uses: mvdbeek/gha-yarn-cache@master with: yarn-lock-file: 'galaxy root/client/yarn.lock' - uses: nanasess/setup-chromedriver@master - name: Run tests - run: ./run_tests.sh -selenium lib/galaxy_test/selenium -- --num-shards=3 --shard-id=${{ matrix.chunk }} + run: ./run_tests.sh --coverage -selenium lib/galaxy_test/selenium -- --num-shards=3 --shard-id=${{ matrix.chunk }} working-directory: 'galaxy root' + - uses: codecov/codecov-action@v2 + with: + flags: selenium-beta-history + working-directory: 'galaxy root' - uses: actions/upload-artifact@v2 if: failure() with: diff --git a/.github/workflows/test_galaxy_release.yaml b/.github/workflows/test_galaxy_release.yaml index 84dc6f4f1a8..32f2065a329 100644 --- a/.github/workflows/test_galaxy_release.yaml +++ b/.github/workflows/test_galaxy_release.yaml @@ -2,11 +2,15 @@ name: Test Galaxy release script on: push: paths: + - '.github/workflows/test_galaxy_release.yaml' + - lib/galaxy/dependencies/** - lib/galaxy/version.py - scripts/release.sh - test/release.sh pull_request: paths: + - '.github/workflows/test_galaxy_release.yaml' + - lib/galaxy/dependencies/** - lib/galaxy/version.py - scripts/release.sh - test/release.sh diff --git a/.github/workflows/toolshed.yaml b/.github/workflows/toolshed.yaml index b82f40963cd..e29fb389455 100644 --- a/.github/workflows/toolshed.yaml +++ b/.github/workflows/toolshed.yaml @@ -50,7 +50,7 @@ jobs: - name: Cache galaxy venv uses: actions/cache@v2 with: - path: .venv + path: 'galaxy root/.venv' key: gxy-venv-${{ runner.os }}-${{ steps.full-python-version.outputs.version }}-${{ hashFiles('galaxy root/requirements.txt') }}-toolshed - name: Run tests run: './run_tests.sh -toolshed' diff --git a/.github/workflows/api_paste.yaml b/.github/workflows/unit-postgres.yaml similarity index 69% rename from .github/workflows/api_paste.yaml rename to .github/workflows/unit-postgres.yaml index 989cf1c0df4..122273c1310 100644 --- a/.github/workflows/api_paste.yaml +++ b/.github/workflows/unit-postgres.yaml @@ -1,4 +1,4 @@ -name: API (legacy paste) tests +name: Unit w/postgres tests on: push: paths-ignore: @@ -11,24 +11,21 @@ on: - 'doc/**' - 'lib/galaxy_test/selenium/**' env: - GALAXY_TEST_DBURI: 'postgresql://postgres:postgres@localhost:5432/galaxy?client_encoding=utf8' - GALAXY_TEST_USE_UVICORN: false - GALAXY_TEST_RAISE_EXCEPTION_ON_HISTORYLESS_HDA: '1' - GALAXY_CONFIG_SQLALCHEMY_WARN_20: '1' + GALAXY_TEST_DBURI: 'postgresql://postgres:postgres@localhost:5432/postgres?client_encoding=utf8' # using postgres as the db concurrency: - group: api-legacy-${{ github.ref }} + group: py-unit-postgres-${{ github.ref }} cancel-in-progress: true jobs: test: name: Test runs-on: ubuntu-latest strategy: + fail-fast: false matrix: python-version: ['3.7'] services: postgres: - # Run tests on the oldest Galaxy-supported version - image: postgres:9.6 + image: postgres:13 env: POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres @@ -54,13 +51,8 @@ jobs: - name: Cache galaxy venv uses: actions/cache@v2 with: - path: .venv - key: gxy-venv-${{ runner.os }}-${{ steps.full-python-version.outputs.version }}-${{ hashFiles('galaxy root/requirements.txt') }}-api-paste - - name: Run tests - run: ./run_tests.sh --skip_flakey_fails -api + path: 'galaxy root/.venv' + key: gxy-venv-${{ runner.os }}-${{ steps.full-python-version.outputs.version }}-${{ hashFiles('galaxy root/requirements.txt') }}-unit-postgres + - name: Run migration tests + run: ./run_tests.sh -unit test/unit/data/model/migrations/test_migrations.py working-directory: 'galaxy root' - - uses: actions/upload-artifact@v2 - if: failure() - with: - name: API (legacy paste) test results (${{ matrix.python-version }}) - path: 'galaxy root/run_api_tests.html' diff --git a/.github/workflows/unit.yaml b/.github/workflows/unit.yaml index 510d09c0469..7cf9bdb11b8 100644 --- a/.github/workflows/unit.yaml +++ b/.github/workflows/unit.yaml @@ -37,18 +37,20 @@ jobs: with: path: ~/.cache/pip key: pip-cache-${{ matrix.python-version }}-${{ hashFiles('galaxy root/requirements.txt') }} - - name: Cache tox env + - name: Cache galaxy venv uses: actions/cache@v2 with: - path: .tox - key: tox-cache-${{ runner.os }}-${{ steps.full-python-version.outputs.version }}-${{ hashFiles('galaxy root/requirements.txt') }}-unit + path: 'galaxy root/.venv' + key: gxy-venv-${{ runner.os }}-${{ steps.full-python-version.outputs.version }}-${{ hashFiles('galaxy root/requirements.txt') }}-unit - name: Install ffmpeg run: sudo apt-get update && sudo apt-get -y install ffmpeg - - name: Install tox - run: pip install tox - name: Run tests - run: tox -e unit + run: ./run_tests.sh --coverage -u working-directory: 'galaxy root' + - uses: codecov/codecov-action@v2 + with: + flags: py-unit + working-directory: 'galaxy root' - uses: actions/upload-artifact@v2 if: failure() with: diff --git a/.gitignore b/.gitignore index 34e09958e33..f956b65bd5c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .hg* + # Downloaded and locally built eggs eggs scripts/scramble/build @@ -21,27 +22,7 @@ lib/galaxy/web/framework/static/scripts lib/galaxy/web/framework/static/style # Database stuff -database/*.sqlite -database/beaker_sessions -database/cache -database/citations -database/community_files -database/compiled_templates -database/dependencies -database/files -database/job_working_directory -database/jobs_directory -database/mulled -database/object_store_cache -database/objects -database/openid_consumer_cache -database/pbs -database/shed_tools -database/test_errors -database/tmp -database/tool_cache -database/tool_search_index -database/tool_recommendation_model.hdf5 +/database/ # Python bytecode @@ -53,6 +34,9 @@ database/tool_recommendation_model.hdf5 *.pid celerybeat-schedule +# Celery state files +celerybeat-schedule.* + # Tool Shed Runtime Files tool_shed_webapp.lock tool_shed_webapp.log @@ -145,6 +129,7 @@ test/unit/**.log .vscode client/**/jsconfig.json vetur.config.js +.pre-commit-config.yaml # Chrom len files *.len diff --git a/.isort.cfg b/.isort.cfg new file mode 100644 index 00000000000..cc4bdd803fe --- /dev/null +++ b/.isort.cfg @@ -0,0 +1,14 @@ +[settings] +extend_skip=doc/source/conf.py,lib/galaxy/util/jstree.py +force_alphabetical_sort_within_sections=true +# Override force_grid_wrap value from profile=black, but black is still happy +force_grid_wrap=2 +# Same line length as for black +line_length=120 +no_lines_before=LOCALFOLDER +profile=black +reverse_relative=true +skip_gitignore=true +# Make isort run faster by skipping database +skip_glob=database/* +src_paths=lib diff --git a/.k8s_ci.Dockerfile b/.k8s_ci.Dockerfile index 06e5bbcaca8..4042f77ea1d 100644 --- a/.k8s_ci.Dockerfile +++ b/.k8s_ci.Dockerfile @@ -22,6 +22,7 @@ ARG STAGE1_BASE=python:3.7-slim ARG FINAL_STAGE_BASE=$STAGE1_BASE ARG GALAXY_USER=galaxy ARG GALAXY_PLAYBOOK_REPO=https://github.com/galaxyproject/galaxy-docker-k8s +ARG GALAXY_PLAYBOOK_BRANCH=v2.0.0 ARG GIT_COMMIT=unspecified ARG BUILD_DATE=unspecified @@ -34,6 +35,7 @@ FROM $STAGE1_BASE AS stage1 ARG DEBIAN_FRONTEND=noninteractive ARG SERVER_DIR ARG GALAXY_PLAYBOOK_REPO +ARG GALAXY_PLAYBOOK_BRANCH # Init Env ENV LC_ALL=en_US.UTF-8 @@ -57,7 +59,7 @@ RUN set -xe; \ WORKDIR /tmp/ansible RUN rm -rf * ENV LC_ALL en_US.UTF-8 -RUN git clone --depth 1 $GALAXY_PLAYBOOK_REPO galaxy-docker +RUN git clone --depth 1 --branch $GALAXY_PLAYBOOK_BRANCH $GALAXY_PLAYBOOK_REPO galaxy-docker WORKDIR /tmp/ansible/galaxy-docker RUN ansible-galaxy install -r requirements.yml -p roles --force-with-deps @@ -176,6 +178,10 @@ COPY --chown=$GALAXY_USER:$GALAXY_USER --from=server_build $ROOT_DIR . COPY --chown=$GALAXY_USER:$GALAXY_USER --from=client_build $SERVER_DIR/static ./server/static WORKDIR $SERVER_DIR + +# The data in version.json will be displayed in Galaxy's /api/version endpoint +RUN printf "{\n \"git_commit\": \"$(cat GITREVISION)\",\n \"build_date\": \"$BUILD_DATE\",\n \"image_tag\": \"$IMAGE_TAG\"\n}\n" > version.json + EXPOSE 8080 USER $GALAXY_USER @@ -185,4 +191,4 @@ ENV GALAXY_CONFIG_CONDA_AUTO_INIT=False ENTRYPOINT ["tini", "--"] # [optional] to run: -CMD uwsgi --yaml config/galaxy.yml +CMD galaxy diff --git a/.pre-commit-config.yaml.sample b/.pre-commit-config.yaml.sample new file mode 100644 index 00000000000..241ce9e1b2a --- /dev/null +++ b/.pre-commit-config.yaml.sample @@ -0,0 +1,48 @@ +repos: + - repo: https://github.com/psf/black + rev: 22.1.0 + hooks: + - id: black + language_version: python3.7 + - repo: https://gitlab.com/pycqa/flake8 + rev: 4.0.1 + hooks: + - id: flake8 + - repo: https://github.com/pre-commit/mirrors-prettier + rev: v2.6.0 # Use the sha or tag you want to point at + hooks: + - id: prettier + types: [file] + types_or: [javascript, jsx, ts, tsx, vue] + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.1.0 # Use the ref you want to point at + hooks: + - id: trailing-whitespace + - id: check-merge-conflict + - id: check-symlinks + - id: destroyed-symlinks + - id: end-of-file-fixer + - repo: https://github.com/detailyang/pre-commit-shell + rev: v1.0.6 + hooks: + - id: shell-lint + args: [--format=json] + - repo: https://github.com/python-jsonschema/check-jsonschema + rev: 0.14.0 + hooks: + - id: check-github-workflows + - repo: local + hooks: + - id: eslint + name: client eslint + language: system + files: ^client/ + entry: make client-eslint-precommit + types: [file] + types_or: [javascript, jsx, ts, tsx, vue] + pass_filenames: false +# - repo: https://github.com/pycqa/isort +# rev: 5.10.1 +# hooks: +# - id: isort +# name: isort (python) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 50676c0f24d..edbea18c1cd 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -130,11 +130,13 @@ https://help.github.com/en/github/getting-started-with-github/git-and-github-lea - Galaxy follows [PEP-8](https://www.python.org/dev/peps/pep-0008/), with particular emphasis on readability being the ultimate goal: - 4 spaces (not tabs!) per indentation level - - divergences from PEP-8 are listed in the `[flake8]` section of the `setup.cfg` - file. -- Python imports should be ordered following the - [smarkets](https://github.com/PyCQA/flake8-import-order/blob/master/tests/test_cases/complete_smarkets.py) - style. + - divergences from PEP-8 are listed in the `[flake8]` section of the + `setup.cfg` file + - The Python code base is automatically formatted using + [isort](https://pycqa.github.io/isort/) (for imports) and + [black](https://black.readthedocs.io). To easily format your Python code + before submitting your contribution, please either use `make diff-format` + or run `isort FILE; black FILE` for each FILE you modify. - Python [docstrings](http://www.python.org/dev/peps/pep-0257/) need to be in [reStructured Text (RST)](https://docutils.sourceforge.io/rst.html) format and compatible with [Sphinx](https://www.sphinx-doc.org). diff --git a/Makefile b/Makefile index 93e632030d2..9b5fd1df9b1 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ VENV?=.venv # Source virtualenv to execute command (flake8, sphinx, twine, etc...) IN_VENV=if [ -f "$(VENV)/bin/activate" ]; then . "$(VENV)/bin/activate"; fi; -RELEASE_CURR:=22.01 +RELEASE_CURR:=22.05 RELEASE_UPSTREAM:=upstream TARGET_BRANCH=$(RELEASE_UPSTREAM)/dev CONFIG_MANAGE=$(IN_VENV) python lib/galaxy/config/config_manage.py @@ -38,8 +38,12 @@ docs-develop: ## Fast doc generation and more warnings (for development) setup-venv: if [ ! -f $(VENV)/bin/activate ]; then bash scripts/common_startup.sh --dev-wheels; fi -diff-format: - $(IN_VENV) darker -r $(TARGET_BRANCH) +diff-format: ## Format Python code changes since last commit + $(IN_VENV) darker . + +format: ## Format Python code base + $(IN_VENV) isort . + $(IN_VENV) black . list-dependency-updates: setup-venv $(IN_VENV) pip list --outdated --format=columns @@ -60,9 +64,6 @@ open-docs: docs _open-docs ## generate Sphinx HTML documentation and open in bro open-project: ## open project on github $(OPEN_RESOURCE) $(PROJECT_URL) -uwsgi-rebuild-validation: ## rebuild uwsgi_config.yml kwalify schema against latest uwsgi master. - $(CONFIG_MANAGE) build_uwsgi_yaml - tool-shed-config-validate: ## validate tool shed YAML configuration file $(CONFIG_MANAGE) validate tool_shed @@ -188,6 +189,9 @@ client-dev-server: node-deps ## Starts a webpack dev server for client developme client-test: node-deps ## Run JS unit tests cd client && yarn run test +client-eslint-precommit: node-deps # Client linting for pre-commit hook; skips glob input and takes specific paths + cd client && yarn run eslint-precommit + client-eslint: node-deps # Run client linting cd client && yarn run eslint diff --git a/README.rst b/README.rst index f260d040869..34d382dc68b 100644 --- a/README.rst +++ b/README.rst @@ -24,12 +24,12 @@ Community support is available at `Galaxy Help Galaxy Quickstart ================= -Galaxy requires Python 3.6 . To check your Python version, run: +Galaxy requires Python 3.7 . To check your Python version, run: .. code:: console $ python -V - Python 3.6.10 + Python 3.7.6 Start Galaxy: diff --git a/client/.eslintignore b/client/.eslintignore deleted file mode 100644 index bacad2ec6e2..00000000000 --- a/client/.eslintignore +++ /dev/null @@ -1,5 +0,0 @@ -src/qunit -src/mocha -src/libs -src/nls -src/legacy diff --git a/client/.eslintrc.js b/client/.eslintrc.json similarity index 53% rename from client/.eslintrc.js rename to client/.eslintrc.json index b061f993130..aed1aa619f1 100644 --- a/client/.eslintrc.js +++ b/client/.eslintrc.json @@ -1,38 +1,37 @@ -module.exports = { - extends: [ +{ + "root": true, + "extends": [ "eslint:recommended", - "plugin:vue/strongly-recommended", + "plugin:vue/strongly-recommended" //"airbnb-base", eventually ], - env: { - browser: true, - commonjs: true, - es6: true, - node: true, - jest: true, + "env": { + "browser": true, + "commonjs": true, + "es6": true, + "node": true, + "jest": true }, - parserOptions: { - parser: "babel-eslint", - sourceType: "module", + "parserOptions": { + "parser": "@babel/eslint-parser", + "babelOptions": { "configFile": "./babel.config.json" } }, - rules: { + "rules": { // Standard rules "no-console": "off", - "no-unused-vars": ["error", { args: "none" }], + "no-unused-vars": ["error", { "args": "none" }], "prefer-const": "error", "one-var": ["error", "never"], "curly": "error", "vue/valid-v-slot": "error", - "vue/v-slot-style": ["error", { atComponent: "v-slot", default: "v-slot", named: "longform" }], - - // Now in strongly-recommended, enforce instead of warn. - "vue/attribute-hyphenation": "error", + "vue/v-slot-style": ["error", { "atComponent": "v-slot", "default": "v-slot", "named": "longform" }], // Vue TODO (enable these) "vue/require-default-prop": "warn", "vue/require-prop-types": "warn", "vue/prop-name-casing": "warn", + "vue/multi-word-component-names": "warn", // Prettier compromises/workarounds -- mostly #wontfix? "vue/html-indent": "off", @@ -41,6 +40,7 @@ module.exports = { "vue/singleline-html-element-content-newline": "off", "vue/multiline-html-element-content-newline": "off", "vue/html-closing-bracket-newline": "off", - "vue/html-closing-bracket-spacing": "off", + "vue/html-closing-bracket-spacing": "off" }, -}; + "ignorePatterns": ["src/qunit", "src/mocha", "src/libs", "src/nls", "src/legacy"] +} diff --git a/client/.node_version b/client/.node_version index c1074247761..23d9c36a118 100644 --- a/client/.node_version +++ b/client/.node_version @@ -1 +1 @@ -14.15.0 +16.13.2 diff --git a/client/README.md b/client/README.md index c9eb83c2bdb..4ef83850246 100644 --- a/client/README.md +++ b/client/README.md @@ -75,8 +75,7 @@ This will start up an extra client development server running on port 8081. Open your browser to http://localhost:8081 (instead of the default 8080 that Galaxy would run on), and you should see Galaxy like normal. Except now, when you change client code it'll automatically rebuild *and* reload the relevant -portion of the application for you. Note that unlike previous versions of this -functionality, it is no longer required to use uWSGI for this. Lastly, if you +portion of the application for you. Lastly, if you are running Galaxy at a location other than the default, you can specify a different proxy target (in this example, port 8000) using the GALAXY_URL environment variable: @@ -91,7 +90,7 @@ Sometimes you want to run your local UI against a remote Galaxy server. This is Galaxy uses Sass for its styling, which is a superset of CSS that compiles down to regular CSS. Most Galaxy styling source (.scss) files are kept in -`client/src/style/scss. There are additionally style blocks alongside some Vue +`client/src/style/scss`. There are additionally style blocks alongside some Vue components -- styles that are particular to that individual component and do not apply site-wide. diff --git a/client/docs/README.md b/client/docs/README.md deleted file mode 100644 index b57412ac27e..00000000000 --- a/client/docs/README.md +++ /dev/null @@ -1,5 +0,0 @@ -The \*.md files in this directory are rendered as a style guide generated when -running ```yarn run styleguide```. - -For more details see [Vue -Styleguidist](https://github.com/vue-styleguidist/vue-styleguidist). diff --git a/client/docs/src/component-design/avoid-using-global-Galaxy.md b/client/docs/avoid-using-global-Galaxy.md similarity index 100% rename from client/docs/src/component-design/avoid-using-global-Galaxy.md rename to client/docs/avoid-using-global-Galaxy.md diff --git a/client/docs/src/component-design/never-use-jquery.md b/client/docs/never-use-jquery.md similarity index 100% rename from client/docs/src/component-design/never-use-jquery.md rename to client/docs/never-use-jquery.md diff --git a/client/docs/src/component-design/providers-and-renderers.md b/client/docs/providers-and-renderers.md similarity index 100% rename from client/docs/src/component-design/providers-and-renderers.md rename to client/docs/providers-and-renderers.md diff --git a/client/docs/src/component-design/readme.md b/client/docs/readme.md similarity index 100% rename from client/docs/src/component-design/readme.md rename to client/docs/readme.md diff --git a/client/docs/root.js b/client/docs/root.js deleted file mode 100644 index 7e02f3f2ecc..00000000000 --- a/client/docs/root.js +++ /dev/null @@ -1,15 +0,0 @@ -/** - * Component root for rendering inside the styleguide, injects vuex store and other common elements - * into each component example section. - */ - -import store from "../src/store"; - -export default (previewComponent) => { - return { - store, - render(h) { - return h(previewComponent); - }, - }; -}; diff --git a/client/docs/sections.js b/client/docs/sections.js deleted file mode 100644 index 0b86475259a..00000000000 --- a/client/docs/sections.js +++ /dev/null @@ -1,142 +0,0 @@ -/** - * Functions for generating the sections for the style guide. Has the old style doc glob and a - * recursive tree-walker that looks through the components folder and tries to arrange found - * markdown docs in a tree. - */ - -const path = require("path"); -const fs = require("fs"); -const glob = require("glob"); -const { humanize, titleize } = require("underscore.string"); - -/** - * Gets the table of contents sections for documentation in the components folder. - * - * @param {string} rootPath absolute path to components folder - * @return {Object} Rootnode, section map, childMap - */ -function getDocSections(rootPath, options = {}) { - const { ignore = [], docSelector = "*.@(vue|md)" } = options; - - const sections = new Map(); // absolute section directory path -> section object - const childToParent = new Map(); // absolute child path -> absolute parent path - - // create root node - const rootNode = newSection(rootPath, ignore); - sections.set(rootPath, rootNode); - childToParent.set(rootPath, null); - - // get all matching doc files - const selector = path.join(rootPath, "**", docSelector); - const allFiles = glob.sync(selector, { ignore }); - - allFiles.forEach((file) => { - const p = path.parse(file); - - // intermediate dir names between this dir and root - const relPath = path.relative(rootPath, p.dir); - const midFolders = relPath.split(path.sep); - - // build intermediate parent sections in the event that this is deeply-nested - while (midFolders.length) { - const sectionPath = path.join(rootPath, ...midFolders); - if (!sections.has(sectionPath)) { - // create new section - const section = newSection(sectionPath, ignore); - sections.set(sectionPath, section); - // register relationship for tree-build later - const parentPath = path.join(sectionPath, ".."); - childToParent.set(sectionPath, parentPath); - } - midFolders.pop(); - } - - // if it's a MD file, add to parent as a subsection - const section = sections.get(p.dir); - if (section && isSubsection(section, file)) { - section.children.add({ - name: titleize(humanize(p.name)), - content: file, - }); - } - }); - - // assemble recursive section tree under rootNode - buildSectionTree(sections, childToParent); - - // rootNode is the main result, returning the maps so the user has the option to manipulate the - // tree before handing it over to the styleguide configs - return { sections, childToParent, rootNode }; -} - -function isSubsection(section, file) { - const p = path.parse(file); - - // it's not a subsection if it's not a markdown file - if (p.ext !== ".md") return false; - - // it's not a subsection if it's the same file as the section content - if (file === section.content) return false; - - // it's not a subsection if it's an example for an existing component - const matchingComponentPath = path.join(p.dir, `${p.name}.vue`); - const componentExists = fs.existsSync(matchingComponentPath); - if (componentExists) return false; - - return true; -} - -/** - * Creates a new section in the TOC from passed path. - * - * @param {string} dir absolute directory path - * @return {Object} section object - */ -function newSection(dir, ignore = []) { - const section = { - name: titleize(humanize(path.basename(dir))), - components: () => glob.sync(path.join(dir, "*.vue"), { ignore }), - children: new Set(), - sectionDepth: 1, - }; - - // summary doc is readme/docs/index.md - const summarySelector = path.join(dir, "@(readme|docs|index).md"); - const summaryDocs = glob.sync(summarySelector, { ignore, nocase: true }); - if (summaryDocs.length) { - section.content = summaryDocs[0]; - } - - return section; -} - -/** - * Turns the pair of maps into a nested object for use in the styleguide config script. Operates on - * sections array by reference. - * - * @param {Map} sections Map of path -> section object - * @param {Map} childToParent Map of childPath -> parentPath - */ -function buildSectionTree(sections, childToParent) { - // add each child to its parent - for (const [childPath, parentPath] of childToParent) { - const child = sections.get(childPath); - const parent = sections.get(parentPath); - if (child && parent && parent.children) { - parent.children.add(child); - } - } - // convert child Sets to arrays now that de-dupe not required - for (const section of sections.values()) { - section.sections = Array.from(section.children).sort((a, b) => a.name.localeCompare(b.name)); - delete section.children; - } -} - -module.exports = { - // public function - getDocSections, - // exporting these in case user wants to tweak the result tree and rebuild it - buildSectionTree, - newSection, -}; diff --git a/client/docs/src/styles/bootstrap.md b/client/docs/src/styles/bootstrap.md deleted file mode 100644 index 30ad491c74d..00000000000 --- a/client/docs/src/styles/bootstrap.md +++ /dev/null @@ -1,379 +0,0 @@ -## Buttons - -Regular buttons - -```vue -
- - - - - - - -
-``` - -Disabled - -```vue -
- - - - - - - -
-``` - -```vue -
- - - - - - -
-``` - -```vue -
- - -
-``` - -```vue -
- - -
-``` - -```vue -
- - -
-``` - -```vue -
- - -
-``` - -```vue -
- - - -
-``` - -## Alerts - -```vue -
- -

Warning!

-

Best check yo self, you're not looking too good. Nulla vitae elit libero, a pharetra augue. Praesent commodo cursus magna, vel scelerisque nisl consectetur et.

-
-``` - -```vue -
- - Oh snap! Change a few things up and try submitting again. -
-``` - -```vue -
- - Well done! You successfully read this important alert message. -
-``` - -```vue -
- - Heads up! This alert needs your attention, but it's not super important. -
-``` - -## Badges - -```vue -
- Primary - Secondary - Success - Warning - Danger - Info -
-``` - -```vue -
- -
-``` - -## Tables - -```vue -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
#Column headingColumn headingColumn heading
1Column contentColumn contentColumn content
2Column contentColumn contentColumn content
3Column contentColumn contentColumn content
4Column contentColumn contentColumn content
5Column contentColumn contentColumn content
6Column contentColumn contentColumn content
7Column contentColumn contentColumn content
-
-``` - -## Cards - -```vue -
-
-
-
-
Header
-
-

Primary card title

-

Some quick example text to build on the card title and make up the bulk of the card's content.

-
-
-
-
Header
-
-

Secondary card title

-

Some quick example text to build on the card title and make up the bulk of the card's content.

-
-
-
-
Header
-
-

Success card title

-

Some quick example text to build on the card title and make up the bulk of the card's content.

-
-
-
-
Header
-
-

Danger card title

-

Some quick example text to build on the card title and make up the bulk of the card's content.

-
-
-
-
Header
-
-

Warning card title

-

Some quick example text to build on the card title and make up the bulk of the card's content.

-
-
-
-
Header
-
-

Info card title

-

Some quick example text to build on the card title and make up the bulk of the card's content.

-
-
-
-
Header
-
-

Light card title

-

Some quick example text to build on the card title and make up the bulk of the card's content.

-
-
-
-
Header
-
-

Dark card title

-

Some quick example text to build on the card title and make up the bulk of the card's content.

-
-
-
-
-
-
-
-
Header
-
-

Primary card title

-

Some quick example text to build on the card title and make up the bulk of the card's content.

-
-
-
-
Header
-
-

Secondary card title

-

Some quick example text to build on the card title and make up the bulk of the card's content.

-
-
-
-
Header
-
-

Success card title

-

Some quick example text to build on the card title and make up the bulk of the card's content.

-
-
-
-
Header
-
-

Danger card title

-

Some quick example text to build on the card title and make up the bulk of the card's content.

-
-
-
-
Header
-
-

Warning card title

-

Some quick example text to build on the card title and make up the bulk of the card's content.

-
-
-
-
Header
-
-

Info card title

-

Some quick example text to build on the card title and make up the bulk of the card's content.

-
-
-
-
Header
-
-

Light card title

-

Some quick example text to build on the card title and make up the bulk of the card's content.

-
-
-
-
Header
-
-

Dark card title

-

Some quick example text to build on the card title and make up the bulk of the card's content.

-
-
-
-
- -
-
-
-

Card header

-
-
Special title treatment
-
Support card subtitle
-
- Card image -
-

Some quick example text to build on the card title and make up the bulk of the card's content.

-
-
    -
  • Cras justo odio
  • -
  • Dapibus ac facilisis in
  • -
  • Vestibulum at eros
  • -
- - -
-
-
-

Card title

-
Card subtitle
-

Some quick example text to build on the card title and make up the bulk of the card's content.

- Card link - Another link -
-
-
-
-
-``` diff --git a/client/docs/src/styles/galaxy-styles/buttons.md b/client/docs/src/styles/galaxy-styles/buttons.md deleted file mode 100644 index 30675603809..00000000000 --- a/client/docs/src/styles/galaxy-styles/buttons.md +++ /dev/null @@ -1,49 +0,0 @@ -## Buttons - -```vue - -``` - -```vue -An anchor with .action-button -``` - -All the crazy permutations of menu button... - -```vue -An anchor with .menu-button -``` - -```vue -An anchor with .menu-button.popup -``` - -```vue - - An anchor with .menu-button.popup.split - -``` - -## Radio Buttons - -As generated by mvc.ui.ui-options - -```vue -
- - - -
-``` - -## Pagination - -```vue - -``` diff --git a/client/docs/src/styles/galaxy-styles/forms.md b/client/docs/src/styles/galaxy-styles/forms.md deleted file mode 100644 index 662f096689b..00000000000 --- a/client/docs/src/styles/galaxy-styles/forms.md +++ /dev/null @@ -1,55 +0,0 @@ -## Forms - -Manually crafted "tool" form class, this is broken in the BS4 branch I think. - -```vue -
-
-
- - -
-
- -
-
-
-``` - -Portlet variant generated by mvc.form.form-view - -```vue -
-
-
-
- - Portlet Title
-
-
-
-
-
-
-
-
- - Text Field 1 -
-
- - - -
- -
-
- -
Password
-
-
-
-``` diff --git a/client/docs/src/styles/galaxy-styles/masthead.md b/client/docs/src/styles/galaxy-styles/masthead.md deleted file mode 100644 index 164ed6e5060..00000000000 --- a/client/docs/src/styles/galaxy-styles/masthead.md +++ /dev/null @@ -1,69 +0,0 @@ -## Masthead - -As generated by layout.masthead.js - -Default height: - -```vue - -``` - -Override height with "height: 80px;" - -```vue - -``` - -Override height with "height: 4rem;" - -```vue - -``` diff --git a/client/docs/src/styles/galaxy-styles/menus.md b/client/docs/src/styles/galaxy-styles/menus.md deleted file mode 100644 index 12763f91d02..00000000000 --- a/client/docs/src/styles/galaxy-styles/menus.md +++ /dev/null @@ -1,19 +0,0 @@ -## Popup menus - -These are the classes for the menu itself, normally floating, so placed inside -a relative element. - -```vue -
- -
-``` diff --git a/client/docs/src/styles/galaxy-styles/messages.md b/client/docs/src/styles/galaxy-styles/messages.md deleted file mode 100644 index 51397a8403c..00000000000 --- a/client/docs/src/styles/galaxy-styles/messages.md +++ /dev/null @@ -1,33 +0,0 @@ -### Panel Messages - -Messages that appear across the top of the panel view below the masthead. - -```vue -
-
-
I'm a panel-{{type}}-message
-
-
-``` - -### Large Messages - -Used for providing feedback inline. - -```vue -
-
-
I'm a {{type}}messagelarge
-
-
-``` - -### Small Messages - -```vue -
-
-
I'm a {{type}}message
-
-
-``` diff --git a/client/docs/src/styles/galaxy-styles/panels.md b/client/docs/src/styles/galaxy-styles/panels.md deleted file mode 100644 index 373f457bcdb..00000000000 --- a/client/docs/src/styles/galaxy-styles/panels.md +++ /dev/null @@ -1,20 +0,0 @@ -## Panel (toolMenuContainer) - -```vue -
-
-
-
- Header -
-
-
-
- one
- two
- three
-
-
-
-
-``` diff --git a/client/docs/src/styles/galaxy-styles/tables.md b/client/docs/src/styles/galaxy-styles/tables.md deleted file mode 100644 index 66a2ffd3f94..00000000000 --- a/client/docs/src/styles/galaxy-styles/tables.md +++ /dev/null @@ -1,14 +0,0 @@ -## Tables - -Tables using the .grid class - -```vue - - - - - - - -
OneTwo
Value 1Value 2
-``` diff --git a/client/docs/src/styles/galaxy-styles/tabs.md b/client/docs/src/styles/galaxy-styles/tabs.md deleted file mode 100644 index d8f19e05504..00000000000 --- a/client/docs/src/styles/galaxy-styles/tabs.md +++ /dev/null @@ -1,32 +0,0 @@ -## Tabs - -Tabs as generated by mvc.ui.ui-tabs - -```vue - -``` diff --git a/client/docs/test/sample-docs/a-file.md b/client/docs/test/sample-docs/a-file.md deleted file mode 100644 index bd8d6998af4..00000000000 --- a/client/docs/test/sample-docs/a-file.md +++ /dev/null @@ -1 +0,0 @@ -# testdocs/a-file.md \ No newline at end of file diff --git a/client/docs/test/sample-docs/another-file.md b/client/docs/test/sample-docs/another-file.md deleted file mode 100644 index c8d2be4519d..00000000000 --- a/client/docs/test/sample-docs/another-file.md +++ /dev/null @@ -1 +0,0 @@ -# testdocs/another-file.md \ No newline at end of file diff --git a/client/docs/test/sample-docs/component-example/SomeComponent.md b/client/docs/test/sample-docs/component-example/SomeComponent.md deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/client/docs/test/sample-docs/component-example/SomeComponent.vue b/client/docs/test/sample-docs/component-example/SomeComponent.vue deleted file mode 100644 index ef519b76833..00000000000 --- a/client/docs/test/sample-docs/component-example/SomeComponent.vue +++ /dev/null @@ -1,3 +0,0 @@ - \ No newline at end of file diff --git a/client/docs/test/sample-docs/contains-ommitted-file/ignored-file.md b/client/docs/test/sample-docs/contains-ommitted-file/ignored-file.md deleted file mode 100644 index 18c16545f98..00000000000 --- a/client/docs/test/sample-docs/contains-ommitted-file/ignored-file.md +++ /dev/null @@ -1 +0,0 @@ -# ignored-file.md \ No newline at end of file diff --git a/client/docs/test/sample-docs/contains-ommitted-file/some-loose-file.md b/client/docs/test/sample-docs/contains-ommitted-file/some-loose-file.md deleted file mode 100644 index 7cc5029a826..00000000000 --- a/client/docs/test/sample-docs/contains-ommitted-file/some-loose-file.md +++ /dev/null @@ -1 +0,0 @@ -# some-loose-file.md \ No newline at end of file diff --git a/client/docs/test/sample-docs/has-readme/loose.md b/client/docs/test/sample-docs/has-readme/loose.md deleted file mode 100644 index 70ef94ec877..00000000000 --- a/client/docs/test/sample-docs/has-readme/loose.md +++ /dev/null @@ -1 +0,0 @@ -# loose file \ No newline at end of file diff --git a/client/docs/test/sample-docs/has-readme/readme.md b/client/docs/test/sample-docs/has-readme/readme.md deleted file mode 100644 index eb8b03babff..00000000000 --- a/client/docs/test/sample-docs/has-readme/readme.md +++ /dev/null @@ -1 +0,0 @@ -# testdocs/abc/readme.md \ No newline at end of file diff --git a/client/docs/test/sample-docs/ignored/foo.md b/client/docs/test/sample-docs/ignored/foo.md deleted file mode 100644 index ad96c729e02..00000000000 --- a/client/docs/test/sample-docs/ignored/foo.md +++ /dev/null @@ -1 +0,0 @@ -# foo \ No newline at end of file diff --git a/client/docs/test/sample-docs/nested-folders/another-nested-folder/should-exist.md b/client/docs/test/sample-docs/nested-folders/another-nested-folder/should-exist.md deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/client/docs/test/sample-docs/no-summary-file/another-loose-file.md b/client/docs/test/sample-docs/no-summary-file/another-loose-file.md deleted file mode 100644 index 5be0105b76e..00000000000 --- a/client/docs/test/sample-docs/no-summary-file/another-loose-file.md +++ /dev/null @@ -1 +0,0 @@ -# another loose file \ No newline at end of file diff --git a/client/docs/test/sample-docs/no-summary-file/loose.md b/client/docs/test/sample-docs/no-summary-file/loose.md deleted file mode 100644 index 8306ab6295b..00000000000 --- a/client/docs/test/sample-docs/no-summary-file/loose.md +++ /dev/null @@ -1 +0,0 @@ -# Loose file \ No newline at end of file diff --git a/client/docs/test/sections.test.js b/client/docs/test/sections.test.js deleted file mode 100644 index d079ffe3162..00000000000 --- a/client/docs/test/sections.test.js +++ /dev/null @@ -1,62 +0,0 @@ -/** - * Tests the functions which build the sections for the styleguide. - */ - -const path = require("path"); -const { getDocSections } = require("../sections"); - -const getSectionByName = (sections, name) => sections.find((o) => o.name == name); - -describe("getDocSections", () => { - const ignore = [ - // ignoring a whole directory - "**/ignored/*", - // ignoring a file - "**/ignore*", - ]; - const testDocRoot = path.join(__dirname, "sample-docs"); - const { rootNode } = getDocSections(testDocRoot, { ignore }); - - test("section generation", () => { - expect(rootNode.name).toEqual("Sample Docs"); - expect(rootNode.sections.length).toEqual(7); - expect(rootNode.content).toBeUndefined(); - }); - - test("nested subsection should appear even if no docs in intermediate folders", () => { - const deepSection = getSectionByName(rootNode.sections, "Nested Folders"); - expect(deepSection.sections.length).toEqual(1); - }); - - test("subdirectory with readme should register as summary", () => { - const subsection = getSectionByName(rootNode.sections, "Has Readme"); - - // readme interpreted as content file and not as loose section - // one other loose file - expect(subsection.content).toContain("readme.md"); - expect(subsection.sections.length).toEqual(1); - }); - - test("subdirectory with no readme file", () => { - const subsection = getSectionByName(rootNode.sections, "No Summary File"); - - // should just see 2 folders no summary - expect(subsection.content).toBeUndefined(); - expect(subsection.sections.length).toEqual(2); - }); - - test("subdirectory with ignored file", () => { - const subsection = getSectionByName(rootNode.sections, "Contains Ommitted File"); - expect(subsection.sections.length).toEqual(1); - }); - - test("ignored subdirectory", () => { - const ignoredSection = getSectionByName(rootNode.sections, "Ignored"); - expect(ignoredSection).toBeUndefined(); - }); - - test("should not create a section for a component example file", () => { - const section = getSectionByName(rootNode.sections, "Component Example"); - expect(section.sections.length).toEqual(0); - }); -}); diff --git a/client/docs/src/component-design/unit-testing/debugging-unit-tests.md b/client/docs/unit-testing/debugging-unit-tests.md similarity index 100% rename from client/docs/src/component-design/unit-testing/debugging-unit-tests.md rename to client/docs/unit-testing/debugging-unit-tests.md diff --git a/client/docs/src/component-design/unit-testing/readme.md b/client/docs/unit-testing/readme.md similarity index 100% rename from client/docs/src/component-design/unit-testing/readme.md rename to client/docs/unit-testing/readme.md diff --git a/client/docs/src/component-design/unit-testing/strategies.md b/client/docs/unit-testing/strategies.md similarity index 100% rename from client/docs/src/component-design/unit-testing/strategies.md rename to client/docs/unit-testing/strategies.md diff --git a/client/docs/src/component-design/unit-testing/writing-tests.md b/client/docs/unit-testing/writing-tests.md similarity index 100% rename from client/docs/src/component-design/unit-testing/writing-tests.md rename to client/docs/unit-testing/writing-tests.md diff --git a/client/gulpfile.js b/client/gulpfile.js index c71fa66555c..04d9c0cc149 100644 --- a/client/gulpfile.js +++ b/client/gulpfile.js @@ -30,21 +30,12 @@ const STATIC_PLUGIN_BUILD_IDS = [ "nora", "venn", ]; - const DIST_PLUGIN_BUILD_IDS = ["new_user"]; - const PLUGIN_BUILD_IDS = Array.prototype.concat(DIST_PLUGIN_BUILD_IDS, STATIC_PLUGIN_BUILD_IDS); -const paths = { - node_modules: "./node_modules", - plugin_dirs: [ - "../config/plugins/{visualizations,interactive_environments,welcome_page}/*/static/**/*", - "../config/plugins/{visualizations,interactive_environments,welcome_page}/*/*/static/**/*", - ], - plugin_build_modules: [ - `../config/plugins/{visualizations,welcome_page}/{${PLUGIN_BUILD_IDS.join(",")}}/package.json`, - ], - lib_locs: { +const PATHS = { + nodeModules: "./node_modules", + stagedLibraries: { // This is a stepping stone towards having all this staged // automatically. Eventually, this dictionary and staging step will // not be necessary. @@ -57,13 +48,26 @@ const paths = { requirejs: ["require.js", "require.js"], underscore: ["underscore.js", "underscore.js"], }, - libs: ["src/libs/**/*.js"], }; +PATHS.pluginBaseDir = + (process.env.GALAXY_PLUGIN_PATH && process.env.GALAXY_PLUGIN_PATH !== "None" + ? process.env.GALAXY_PLUGIN_PATH + : undefined) || "../config/plugins/"; + +PATHS.pluginDirs = [ + path.join(PATHS.pluginBaseDir, "{visualizations,welcome_page}/*/static/**/*"), + path.join(PATHS.pluginBaseDir, "{visualizations,welcome_page}/*/*/static/**/*"), +]; + +PATHS.pluginBuildModules = [ + path.join(PATHS.pluginBaseDir, `{visualizations,welcome_page}/{${PLUGIN_BUILD_IDS.join(",")}}/package.json`), +]; + function stageLibs(callback) { - Object.keys(paths.lib_locs).forEach((lib) => { - var p1 = path.resolve(path.join(paths.node_modules, lib, paths.lib_locs[lib][0])); - var p2 = path.resolve(path.join("src", "libs", paths.lib_locs[lib][1])); + Object.keys(PATHS.stagedLibraries).forEach((lib) => { + var p1 = path.resolve(path.join(PATHS.nodeModules, lib, PATHS.stagedLibraries[lib][0])); + var p2 = path.resolve(path.join("src", "libs", PATHS.stagedLibraries[lib][1])); if (fs.existsSync(p1)) { del.sync(p2); fs.createReadStream(p1).pipe(fs.createWriteStream(p2)); @@ -78,57 +82,75 @@ function stageLibs(callback) { } function fonts() { - return src(path.resolve(path.join(paths.node_modules, "font-awesome/fonts/**/*"))).pipe( + return src(path.resolve(path.join(PATHS.nodeModules, "font-awesome/fonts/**/*"))).pipe( dest("../static/images/fonts") ); } function stagePlugins() { - return src(paths.plugin_dirs).pipe(dest("../static/plugins/")); + return src(PATHS.pluginDirs).pipe(dest("../static/plugins/")); } -function buildPlugins(callback) { +function buildPlugins(callback, forceRebuild) { /* - * Walk plugin_build_modules glob and attempt to build modules. + * Walk pluginBuildModules glob and attempt to build modules. * */ - paths.plugin_build_modules.map((build_module) => { - glob(build_module, {}, (er, files) => { + PATHS.pluginBuildModules.map((buildModule) => { + glob(buildModule, {}, (er, files) => { files.map((file) => { - let skip_build = false; - const f = path.join(process.cwd(), file).slice(0, -12); - const plugin_name = path.dirname(file).split(path.sep).pop(); - const hash_file_path = path.join( - f, - DIST_PLUGIN_BUILD_IDS.indexOf(plugin_name) > -1 ? "dist" : "static", + let skipBuild = false; + const pluginDir = path.dirname(file); + const pluginName = pluginDir.split(path.sep).pop(); + + const hashFilePath = path.join( + pluginDir, + DIST_PLUGIN_BUILD_IDS.indexOf(pluginName) > -1 ? "dist" : "static", "plugin_build_hash.txt" ); - if (fs.existsSync(hash_file_path)) { - skip_build = - child_process.spawnSync("git", ["diff", "--quiet", `$(cat ${hash_file_path})`, "--", f], { - stdio: "inherit", - shell: true, - }).status === 0; + if (forceRebuild) { + skipBuild = false; } else { - console.log(`No build hashfile detected for ${plugin_name}, generating now.`); + if (fs.existsSync(hashFilePath)) { + skipBuild = + child_process.spawnSync( + "git", + ["diff", "--quiet", `$(cat ${hashFilePath})`, "--", pluginDir], + { + stdio: "inherit", + shell: true, + } + ).status === 0; + } else { + console.log(`No build hashfile detected for ${pluginName}, generating now.`); + } } - if (skip_build) { - console.log(`No changes detected for ${plugin_name}`); + if (skipBuild) { + console.log(`No changes detected for ${pluginName}`); } else { - console.log(`Installing Dependencies for ${plugin_name}`); + console.log(`Installing Dependencies for ${pluginName}`); child_process.spawnSync( "yarn", ["install", "--production=false", "--network-timeout=300000", "--check-files"], { - cwd: f, + cwd: pluginDir, stdio: "inherit", shell: true, } ); - console.log(`Building ${plugin_name}`); - child_process.spawnSync("yarn", ["build"], { cwd: f, stdio: "inherit", shell: true }); - child_process.exec(`(git rev-parse HEAD 2>/dev/null || echo \`\`) > ${hash_file_path}`); + console.log(`Building ${pluginName}`); + if ( + child_process.spawnSync("yarn", ["build"], { cwd: pluginDir, stdio: "inherit", shell: true }) + .status === 0 + ) { + console.log(`Successfully built, saving build state to ${hashFilePath}`); + child_process.exec(`(git rev-parse HEAD 2>/dev/null || echo \`\`) > ${hashFilePath}`); + } else { + console.error( + `Error building ${pluginName}, not saving build state. Please report this issue to the Galaxy Team.` + ); + } } }); }); @@ -136,21 +158,27 @@ function buildPlugins(callback) { return callback(); } +function forceBuildPlugins(callback) { + return buildPlugins(callback, true); +} + function cleanPlugins() { - return del(["../static/plugins/{visualizations,interactive_environments,welcome_page}/*"], { force: true }); + return del(["../static/plugins/{visualizations,welcome_page}/*"], { force: true }); } const client = parallel(fonts, stageLibs); const plugins = series(buildPlugins, cleanPlugins, stagePlugins); +const pluginsRebuild = series(forceBuildPlugins, cleanPlugins, stagePlugins); function watchPlugins() { const BUILD_PLUGIN_WATCH_GLOB = [ - `../config/plugins/{visualizations,welcome_page}/{${PLUGIN_BUILD_IDS.join(",")}}/**/*`, + path.join(PATHS.pluginBaseDir, `{visualizations,welcome_page}/{${PLUGIN_BUILD_IDS.join(",")}}/**/*`), ]; watch(BUILD_PLUGIN_WATCH_GLOB, { queue: false }, plugins); } module.exports.client = client; module.exports.plugins = plugins; +module.exports.pluginsRebuild = pluginsRebuild; module.exports.watchPlugins = watchPlugins; module.exports.default = parallel(client, plugins); diff --git a/client/package.json b/client/package.json index faedd886b87..7be12213b52 100644 --- a/client/package.json +++ b/client/package.json @@ -11,8 +11,7 @@ }, "license": "AFL-3.0", "resolutions": { - "**/chokidar": "3.5.2", - "**/ua-parser-js": "0.7.30" + "**/chokidar": "3.5.3" }, "dependencies": { "@fortawesome/fontawesome-free": "^5.15.4", @@ -20,21 +19,22 @@ "@fortawesome/free-brands-svg-icons": "^5.15.4", "@fortawesome/free-regular-svg-icons": "^5.15.4", "@fortawesome/free-solid-svg-icons": "^5.15.4", - "@fortawesome/vue-fontawesome": "^2.0.2", - "@galaxyproject/bootstrap-tour": "^0.12.1", + "@fortawesome/vue-fontawesome": "^2.0.6", + "@galaxyproject/bootstrap-tour": "0.12.2", "@handsontable/vue": "^2.0.0-beta1", - "@hirez_io/observer-spy": "^2.1.0", + "@hirez_io/observer-spy": "^2.1.2", "@johmun/vue-tags-input": "^2.1.0", - "@sentry/browser": "^5.20.1", - "axios": "^0.21.1", + "@sentry/browser": "^6.17.4", + "assert": "^2.0.0", + "axios": "^0.25.0", "babel-runtime": "^6.26.0", "backbone": "1.4.0", - "bootstrap": "4.5.0", + "bootstrap": "4.6", "bootstrap-vue": "^2.21.2", - "citation-js": "^0.5.1", - "core-js": "^3.19.1", + "citation-js": "^0.5.5", + "core-js": "^3.21.0", "d3": "3", - "date-fns": "^2.24.0", + "date-fns": "^2.28.0", "decode-uri-component": "^0.2.0", "deep-diff": "^1.0.2", "deep-equal": "^2.0.5", @@ -44,62 +44,60 @@ "flush-promises": "^1.0.2", "glob": "^7.2.0", "handsontable": "^2.0.0", - "imask": "^6.2.2", + "imask": "^6.4.0", "in-viewport": "^3.6.0", "is-promise": "^4.0.0", "isotope-layout": "^3.0.6", - "iter-tools": "7.1.4", + "iter-tools": "7.2.0", "jquery": "2", "jquery-migrate": "~1.4", "jquery-mousewheel": "^3.1.13", - "jquery-ui": "^1.12.1", + "jquery-ui": "^1.13.1", "jquery.cookie": "^1.4.1", - "jspdf": "^2.4.0", - "linkifyjs": "^2.1.9", + "jspdf": "^2.5.1", + "linkify-html": "^3.0.5", + "linkifyjs": "^3.0.5", + "localforage": "^1.10.0", "markdown-it": "^12.3.2", "markdown-it-regexp": "^0.4.0", - "moment": "2.29.1", + "moment": "2.29.2", "object-hash": "^2.2.0", "popper.js": "^1.16.1", - "pouchdb": "^7.2.2", - "pouchdb-adapter-memory": "^7.2.2", - "pouchdb-debug": "^7.2.1", - "pouchdb-erase": "^1.0.2", - "pouchdb-find": "^7.2.2", - "pouchdb-upsert": "^2.2.0", "pretty-bytes": "^5.6.0", - "proper-skip-list": "^4.0.2", + "proper-skip-list": "^4.1.0", "pyre-to-regexp": "^0.0.5", + "querystring-es3": "^0.2.1", "regenerator-runtime": "^0.13.9", "regression": "^2.0.1", "requirejs": "2.3.6", - "rxjs": "^7.4.0", - "rxjs-spy": "^8.0.0", + "rxjs": "^7.5.2", + "rxjs-spy": "^8.0.2", "rxjs-spy-devtools-plugin": "^0.0.4", - "slugify": "^1.6.0", + "slugify": "^1.6.5", "snake-case": "^3.0.4", "splitpanes": "2.3.8", "stream-browserify": "^3.0.0", - "threads": "^1.6.5", + "threads": "^1.7.0", "timers-browserify": "^2.0.12", "toastr": "^2.1.4", "tus-js-client": "^2.3.0", - "underscore": "^1.10.2", - "underscore.string": "^3.3.5", + "underscore": "^1.13.2", + "underscore.string": "^3.3.6", + "util": "^0.12.4", "vue": "^2.6.14", "vue-infinite-scroll": "^2.0.2", - "vue-multiselect": "^2.1.0", + "vue-multiselect": "^2.1.6", "vue-observe-visibility": "^1.0.0", "vue-prismjs": "^1.2.0", - "vue-router": "^3.5.2", + "vue-router": "^3.5.3", "vue-rx": "^6.2.0", "vue-scrollto": "^2.20.0", + "vue-virtual-scroll-list": "^2.3.3", "vuedraggable": "2.24.3", "vueisotope": "^3.1.2", - "vuex": "^3.4.0", - "vuex-cache": "^3.2.0", - "vuex-persist": "^3.1.3", - "vuex-persistedstate": "^4.1.0", + "vuex": "^3.6.2", + "vuex-cache": "^3.4.0", + "vuex-persist": "2.3", "xml-beautifier": "^0.5.0" }, "scripts": { @@ -117,76 +115,74 @@ "save-build-hash": "(git rev-parse HEAD 2>/dev/null || echo '') >../static/client_build_hash.txt", "prettier": "prettier --write 'src/style/scss/**/*.scss' 'src/**/{*.js,*.vue}' '!src/libs/**'", "prettier-check": "prettier --check 'src/style/scss/**/*.scss' 'src/**/{*.js,*.vue}' '!src/libs/**'", - "styleguide": "vue-styleguidist server", - "styleguide:build": "vue-styleguidist build", "test": "yarn run qunit && yarn run jest", "jest": "jest --config tests/jest/jest.config.js", "jest-watch": "jest --config tests/jest/jest.config.js --watch", "qunit": "karma start tests/karma/karma.config.qunit.js", - "eslint": "eslint -c .eslintrc.js src --ext .js,.vue" + "eslint-precommit": "eslint -c .eslintrc.json --quiet --ext .js,.vue src", + "eslint": "eslint -c .eslintrc.json --ext .js,.vue src" }, "devDependencies": { - "@babel/core": "^7.15.8", - "@babel/helper-validator-identifier": "^7.15.7", + "@babel/core": "^7.17.0", + "@babel/eslint-parser": "^7.17.0", + "@babel/helper-validator-identifier": "^7.16.7", "@babel/plugin-syntax-dynamic-import": "^7.8.3", - "@babel/plugin-transform-runtime": "^7.15.8", - "@babel/preset-env": "^7.15.8", + "@babel/plugin-transform-runtime": "^7.17.0", + "@babel/preset-env": "^7.16.11", "@cerner/duplicate-package-checker-webpack-plugin": "^2.1.0", - "@testing-library/jest-dom": "^5.14.1", - "@vue/test-utils": "^1.2.2", - "@vue/vue2-jest": "^27.0.0-alpha.2", + "@testing-library/jest-dom": "^5.16.2", + "@vue/test-utils": "^1.3.0", + "@vue/vue2-jest": "^27.0.0-alpha.4", "amdi18n-loader": "^0.9.3", - "autoprefixer": "^10.3.5", + "autoprefixer": "^10.4.2", "axios-mock-adapter": "^1.20.0", "babel-core": "^7.0.0-bridge.0", - "babel-eslint": "^10.1.0", - "babel-jest": "^27.2.5", - "babel-loader": "^8.2.2", + "babel-jest": "^27.4.6", + "babel-loader": "^8.2.3", "babel-plugin-transform-inline-environment-variables": "^0.4.3", "babel-plugin-transform-vue-template": "^0.4.2", "buffer": "^6.0.3", - "chai": "^4.2.0", - "chokidar": "^3.5.2", - "css-loader": "^6.3.0", - "css-minimizer-webpack-plugin": "^3.0.2", + "chai": "^4.3.6", + "css-loader": "^6.6.0", + "css-minimizer-webpack-plugin": "^3.4.1", "del": "^6.0.0", - "eslint": "^7.32.0", - "eslint-plugin-vue": "^7.18.0", - "expose-loader": "^3.0.0", + "eslint": "^8.8.0", + "eslint-plugin-vue": "^8.4.1", + "expose-loader": "^3.1.0", "gulp": "^4.0.2", "ignore-loader": "^0.1.2", - "imports-loader": "^3.0.0", - "jest": "^27.2.5", + "imports-loader": "^3.1.1", + "jest": "^27.4.7", + "jest-location-mock": "^1.0.9", "jest-raw-loader": "^1.0.1", - "jest-transform-yaml": "0.1.2", + "jest-transform-yaml": "1.0.0", "json-loader": "^0.5.7", - "karma": "^6.3.4", + "karma": "^6.3.16", "karma-chrome-launcher": "^3.1.0", "karma-polyfill": "^1.1.0", - "karma-qunit": "^4.1.1", + "karma-qunit": "^4.1.2", "karma-webpack": "^5.0.0", - "mini-css-extract-plugin": "^2.4.2", - "postcss-loader": "^6.1.1", - "prettier": "^2.4.1", + "mini-css-extract-plugin": "^2.5.3", + "postcss-loader": "^6.2.1", + "prettier": "^2.5.1", "process": "^0.11.10", "qunit": "^2.17.2", "raw-loader": "^4.0.2", - "sass": "^1.42.1", - "sass-loader": "^12.1.0", - "sinon": "^11.1.2", + "sass": "^1.49.7", + "sass-loader": "^12.4.0", + "sinon": "^13.0.1", "store": "^2.0.12", - "style-loader": "^3.3.0", + "style-loader": "^3.3.1", "uuid": "^8.3.2", "vue-loader": "^15.9.8", - "vue-styleguidist": "^4.41.2", "vue-template-compiler": "^2.6.14", - "webpack": "^5.58.0", - "webpack-cli": "^4.9.0", - "webpack-dev-server": "^4.2.1", + "webpack": "^5.68.0", + "webpack-cli": "^4.9.2", + "webpack-dev-server": "^4.7.4", "webpack-merge": "^5.8.0", "yaml-loader": "^0.6.0" }, "peerDependencies": { - "postcss": "^8.2.8" + "postcss": "^8.4.6" } } diff --git a/client/src/bundleEntries.js b/client/src/bundleEntries.js index 26240b1a5ee..9780e2d917f 100644 --- a/client/src/bundleEntries.js +++ b/client/src/bundleEntries.js @@ -30,7 +30,6 @@ export { default as HDAModel } from "mvc/history/hda-model"; export { default as LegacyGridView } from "legacy/grid/grid-view"; export { create_chart, create_histogram } from "reports/run_stats"; export { default as ToolshedGroups } from "toolshed/toolshed.groups"; -export { default as IES } from "galaxy.interactive_environments"; export { openGlobalUploadModal } from "components/Upload"; export { Toast } from "ui/toast"; // TODO: remove when external consumers are updated/gone (IES right now) diff --git a/client/src/components/Collections/PairedListCollectionCreator.test.js b/client/src/components/Collections/PairedListCollectionCreator.test.js index ce54a281bd0..393dd33884f 100644 --- a/client/src/components/Collections/PairedListCollectionCreator.test.js +++ b/client/src/components/Collections/PairedListCollectionCreator.test.js @@ -45,12 +45,11 @@ describe("PairedListCollectionCreator", () => { }); await wrapper.vm.$nextTick(); //change filter to .1.fastq/.2.fastq - wrapper.find("div.forward-unpaired-filter > div.input-group-append > button").trigger("click"); - wrapper + await wrapper.find("div.forward-unpaired-filter > div.input-group-append > button").trigger("click"); + await wrapper .findAll("div.dropdown-menu > a.dropdown-item") .wrappers.find((e) => e.text() == ".1.fastq") .trigger("click"); - await wrapper.vm.$nextTick(); //assert forward filter const forwardFilter = wrapper.find("div.forward-unpaired-filter > input").element.value; expect(forwardFilter).toBe(".1.fastq"); @@ -58,8 +57,7 @@ describe("PairedListCollectionCreator", () => { const reverseFilter = wrapper.find("div.reverse-unpaired-filter > input").element.value; expect(reverseFilter).toBe(".2.fastq"); // click Autopair - wrapper.find("a.autopair-link").trigger("click"); - await wrapper.vm.$nextTick(); + await wrapper.find("a.autopair-link").trigger("click"); //assert pair-name longer name const pairname = wrapper.find("span.pair-name"); expect(pairname.text()).toBe("DP134_1_FS_PSII_FSB_42C_A10"); @@ -83,12 +81,11 @@ describe("PairedListCollectionCreator", () => { }); await wrapper.vm.$nextTick(); //change filter to .1.fastq/.2.fastq - wrapper.find("div.forward-unpaired-filter > div.input-group-append > button").trigger("click"); - wrapper + await wrapper.find("div.forward-unpaired-filter > div.input-group-append > button").trigger("click"); + await wrapper .findAll("div.dropdown-menu > a.dropdown-item") .wrappers.find((e) => e.text() == ".1.fastq") .trigger("click"); - await wrapper.vm.$nextTick(); //assert forward filter const forwardFilter = wrapper.find("div.forward-unpaired-filter > input").element.value; expect(forwardFilter).toBe(".1.fastq"); @@ -96,8 +93,7 @@ describe("PairedListCollectionCreator", () => { const reverseFilter = wrapper.find("div.reverse-unpaired-filter > input").element.value; expect(reverseFilter).toBe(".2.fastq"); // click Autopair - wrapper.find("a.autopair-link").trigger("click"); - await wrapper.vm.$nextTick(); + await wrapper.find("a.autopair-link").trigger("click"); //assert pair-name longer name const pairname = wrapper.find("span.pair-name"); expect(pairname.text()).toBe("UII_moo_1"); @@ -121,10 +117,8 @@ describe("PairedListCollectionCreator", () => { }); await wrapper.vm.$nextTick(); //change filter to _R1/_R2 - wrapper.find("div.forward-unpaired-filter > input").setValue("_R1"); - await wrapper.vm.$nextTick(); - wrapper.find("div.reverse-unpaired-filter > input").setValue("_R2"); - await wrapper.vm.$nextTick(); + await wrapper.find("div.forward-unpaired-filter > input").setValue("_R1"); + await wrapper.find("div.reverse-unpaired-filter > input").setValue("_R2"); //assert forward filter const forwardFilter = wrapper.find("div.forward-unpaired-filter > input").element.value; expect(forwardFilter).toBe("_R1"); @@ -132,8 +126,7 @@ describe("PairedListCollectionCreator", () => { const reverseFilter = wrapper.find("div.reverse-unpaired-filter > input").element.value; expect(reverseFilter).toBe("_R2"); // click Autopair - wrapper.find("a.autopair-link").trigger("click"); - await wrapper.vm.$nextTick(); + await wrapper.find("a.autopair-link").trigger("click"); //assert all pairs matched expect(wrapper.findAll("li.dataset unpaired").length == 0).toBeTruthy(); }); diff --git a/client/src/components/Collections/PairedListCollectionCreator.vue b/client/src/components/Collections/PairedListCollectionCreator.vue index 5bd7d66d3af..36238198529 100644 --- a/client/src/components/Collections/PairedListCollectionCreator.vue +++ b/client/src/components/Collections/PairedListCollectionCreator.vue @@ -248,13 +248,7 @@
+ class="unpaired-filter forward-unpaired-filter float-left search-input search-query input-group">
+ class="unpaired-filter reverse-unpaired-filter float-left search-input search-query input-group"> -
+ - - -
+ @keydown.esc="setQuery('')" /> + + + + + + + diff --git a/client/src/components/DataDialog/DataDialog.vue b/client/src/components/DataDialog/DataDialog.vue index eab7aeb96ef..77c8a203e37 100644 --- a/client/src/components/DataDialog/DataDialog.vue +++ b/client/src/components/DataDialog/DataDialog.vue @@ -14,7 +14,7 @@ :multiple="multiple" :filter="filter" @clicked="onClick" - @load="load" /> + @open="onLoad" /> diff --git a/client/src/components/JobInformation/JobOutputs.vue b/client/src/components/JobInformation/JobOutputs.vue index a9f822fa877..feba2eecd7b 100644 --- a/client/src/components/JobInformation/JobOutputs.vue +++ b/client/src/components/JobInformation/JobOutputs.vue @@ -14,7 +14,11 @@ {{ value[0].label || name }} - + @@ -23,11 +27,11 @@ + + diff --git a/client/src/components/JobParameters/JobParametersArrayValue.vue b/client/src/components/JobParameters/JobParametersArrayValue.vue index d969aad9e04..1878f3efce9 100644 --- a/client/src/components/JobParameters/JobParametersArrayValue.vue +++ b/client/src/components/JobParameters/JobParametersArrayValue.vue @@ -1,22 +1,21 @@ diff --git a/client/src/components/Masthead/Masthead.test.js b/client/src/components/Masthead/Masthead.test.js index b395516e313..d01bd835a0e 100644 --- a/client/src/components/Masthead/Masthead.test.js +++ b/client/src/components/Masthead/Masthead.test.js @@ -93,11 +93,9 @@ describe("Masthead.vue", () => { it("should disable brand when displayGalaxyBrand is true", async () => { expect(wrapper.find(".navbar-brand-title").text()).toBe("Galaxy"); - wrapper.setProps({ brand: "Foo " }); - await localVue.nextTick(); + await wrapper.setProps({ brand: "Foo " }); expect(wrapper.find(".navbar-brand-title").text()).toBe("Galaxy Foo"); - wrapper.setProps({ displayGalaxyBrand: false }); - await localVue.nextTick(); + await wrapper.setProps({ displayGalaxyBrand: false }); expect(wrapper.find(".navbar-brand-title").text()).toBe("Foo"); }); @@ -137,8 +135,7 @@ describe("Masthead.vue", () => { it("should display scratchbook button", async () => { expect(wrapper.find("#enable-scratchbook a span").classes("fa-th")).toBe(true); expect(scratchbook.active).toBe(false); - wrapper.find("#enable-scratchbook a").trigger("click"); - await localVue.nextTick(); + await wrapper.find("#enable-scratchbook a").trigger("click"); expect(scratchbook.active).toBe(true); }); diff --git a/client/src/components/PageDisplay/PageDisplay.vue b/client/src/components/PageDisplay/PageDisplay.vue index 5140342633e..a74982baa89 100644 --- a/client/src/components/PageDisplay/PageDisplay.vue +++ b/client/src/components/PageDisplay/PageDisplay.vue @@ -1,14 +1,24 @@ diff --git a/client/src/components/RuleCollectionBuilder.vue b/client/src/components/RuleCollectionBuilder.vue index b100b6304fd..f837e262573 100644 --- a/client/src/components/RuleCollectionBuilder.vue +++ b/client/src/components/RuleCollectionBuilder.vue @@ -339,7 +339,8 @@ + :saved-rules="this.savedRules" + :ruleColHeaders="colHeadersPerRule" />
Rows contain differing numbers of columns, there was likely a problem parsing your data. @@ -1252,7 +1253,7 @@ export default { } else { const Galaxy = getGalaxyInstance(); const history = Galaxy && Galaxy.currHistoryPanel && Galaxy.currHistoryPanel.model; - history.refresh(); + history.refresh && history.refresh(); this.oncreate(); } }; diff --git a/client/src/components/SelectionDialog/DataDialogTable.vue b/client/src/components/SelectionDialog/DataDialogTable.vue index 9bf04ba11f7..8b951b3166b 100644 --- a/client/src/components/SelectionDialog/DataDialogTable.vue +++ b/client/src/components/SelectionDialog/DataDialogTable.vue @@ -1,5 +1,7 @@ diff --git a/client/src/components/User/DiskUsage/Management/Cleanup/CleanupOperationSummary.test.js b/client/src/components/User/DiskUsage/Management/Cleanup/CleanupOperationSummary.test.js new file mode 100644 index 00000000000..ddbd588f893 --- /dev/null +++ b/client/src/components/User/DiskUsage/Management/Cleanup/CleanupOperationSummary.test.js @@ -0,0 +1,95 @@ +import { mount } from "@vue/test-utils"; +import flushPromises from "flush-promises"; +import { getLocalVue } from "jest/helpers"; +import CleanupOperationSummary from "./CleanupOperationSummary"; +import { CleanableSummary, CleanupOperation, CleanupResult } from "./model"; + +const localVue = getLocalVue(); + +const REVIEW_ITEMS_LINK = '[data-test-id="review-link"]'; +const NO_ITEMS_INDICATOR = '[data-test-id="no-items-indicator"]'; + +/** Operation that can clean some items */ +const CLEANUP_OPERATION = new CleanupOperation({ + id: "operation-id", + name: "operation name", + description: "operation description", + fetchSummary: () => + new CleanableSummary({ + totalSize: 1024, + totalItems: 2, + }), + fetchItems: () => [], + cleanupItems: (items) => + new CleanupResult({ + totalItemCount: 2, + totalFreeBytes: 1024, + }), +}); +/** Operation without items to clean*/ +const EMPTY_CLEANUP_OPERATION = new CleanupOperation({ + id: "operation-id-empty", + name: "empty operation", + description: "operation that has no items to clean", + fetchSummary: () => + new CleanableSummary({ + totalSize: 0, + totalItems: 0, + }), + fetchItems: () => [], + cleanupItems: (items) => null, +}); +/** Operation that fails on every action */ +const ERROR_CLEANUP_OPERATION = new CleanupOperation({ + id: "operation-id-error", + name: "operation with error", + description: "operation causing errors", + fetchSummary: () => { + throw new Error("Cannot fetch summary"); + }, + fetchItems: () => { + throw new Error("Cannot fetch items"); + }, + cleanupItems: (items) => { + throw new Error("Cannot cleanup items"); + }, +}); + +async function mountCleanupOperationSummaryWith(operation, refreshOperationId = null, refreshDelay = 0) { + const wrapper = mount( + CleanupOperationSummary, + { propsData: { operation, refreshOperationId, refreshDelay } }, + localVue + ); + await flushPromises(); + return wrapper; +} + +describe("CleanupOperationSummary.vue", () => { + it("should display the operation information", async () => { + const wrapper = await mountCleanupOperationSummaryWith(CLEANUP_OPERATION); + + expect(wrapper.html()).toContain(CLEANUP_OPERATION.name); + expect(wrapper.html()).toContain(CLEANUP_OPERATION.description); + }); + + it("should display a link to review items if there are items to be cleaned", async () => { + const wrapper = await mountCleanupOperationSummaryWith(CLEANUP_OPERATION); + + expect(wrapper.find(REVIEW_ITEMS_LINK).exists()).toBe(true); + }); + + it("should display an indicator when there are no items to review", async () => { + const wrapper = await mountCleanupOperationSummaryWith(EMPTY_CLEANUP_OPERATION); + + expect(wrapper.find(NO_ITEMS_INDICATOR).exists()).toBe(true); + expect(wrapper.find(REVIEW_ITEMS_LINK).exists()).toBe(false); + }); + + it("should display an error if the summary information cannot be retrieved", async () => { + const wrapper = await mountCleanupOperationSummaryWith(ERROR_CLEANUP_OPERATION); + + expect(wrapper.find(REVIEW_ITEMS_LINK).exists()).toBe(false); + expect(wrapper.html()).toContain("Cannot fetch summary"); + }); +}); diff --git a/client/src/components/User/DiskUsage/Management/Cleanup/CleanupOperationSummary.vue b/client/src/components/User/DiskUsage/Management/Cleanup/CleanupOperationSummary.vue new file mode 100644 index 00000000000..8b62994f8d9 --- /dev/null +++ b/client/src/components/User/DiskUsage/Management/Cleanup/CleanupOperationSummary.vue @@ -0,0 +1,110 @@ + + + + + diff --git a/client/src/components/User/DiskUsage/Management/Cleanup/CleanupResultDialog.test.js b/client/src/components/User/DiskUsage/Management/Cleanup/CleanupResultDialog.test.js new file mode 100644 index 00000000000..fb7bc0ba64d --- /dev/null +++ b/client/src/components/User/DiskUsage/Management/Cleanup/CleanupResultDialog.test.js @@ -0,0 +1,78 @@ +import { mount } from "@vue/test-utils"; +import flushPromises from "flush-promises"; +import { getLocalVue } from "jest/helpers"; +import CleanupResultDialog from "./CleanupResultDialog"; +import { CleanupResult } from "./model"; + +const localVue = getLocalVue(); + +const LOADING_SPINNER = '[data-test-id="loading-spinner"]'; +const ERROR_ALERT = '[data-test-id="error-alert"]'; +const SUCCESS_INFO = '[data-test-id="success-info"]'; +const PARTIAL_SUCCESS_INFO = '[data-test-id="partial-success-info"]'; +const ERRORS_TABLE = '[data-test-id="errors-table"]'; + +const NO_RESULT_YET = null; +const FAILED_RESULT = () => { + return new CleanupResult({ errorMessage: "The operation failed" }); +}; +const PARTIAL_SUCCESS_RESULT = () => { + return new CleanupResult({ + totalItemCount: 3, + errors: [ + { name: "Dataset X", reason: "Failed because of X" }, + { name: "Dataset Y", reason: "Failed because of Y" }, + ], + }); +}; +const SUCCESS_RESULT = () => { + return new CleanupResult({ totalItemCount: 2 }); +}; +async function mountCleanupResultDialogWith(result) { + const wrapper = mount(CleanupResultDialog, { propsData: { result, show: true } }, localVue); + await flushPromises(); + return wrapper; +} + +describe("CleanupResultDialog.vue", () => { + it("should display a loading indicator when there is no result yet", async () => { + const wrapper = await mountCleanupResultDialogWith(NO_RESULT_YET); + + expect(wrapper.find(LOADING_SPINNER).exists()).toBe(true); + + await wrapper.setProps({ result: SUCCESS_RESULT() }); + expect(wrapper.find(LOADING_SPINNER).exists()).toBe(false); + }); + + it("should display an error message when the operation completely failed", async () => { + const failedResult = FAILED_RESULT(); + const wrapper = await mountCleanupResultDialogWith(failedResult); + + const errorAlert = wrapper.find(ERROR_ALERT); + expect(errorAlert.exists()).toBe(true); + expect(errorAlert.html()).toContain(failedResult.errorMessage); + expect(wrapper.find(SUCCESS_INFO).exists()).toBe(false); + expect(wrapper.find(PARTIAL_SUCCESS_INFO).exists()).toBe(false); + expect(wrapper.find(ERRORS_TABLE).exists()).toBe(false); + }); + + it("should display the amount freed and a table with errors for each errored item when partial success", async () => { + const partialSuccessResult = PARTIAL_SUCCESS_RESULT(); + const wrapper = await mountCleanupResultDialogWith(partialSuccessResult); + + expect(wrapper.find(ERROR_ALERT).exists()).toBe(false); + expect(wrapper.find(SUCCESS_INFO).exists()).toBe(false); + expect(wrapper.find(PARTIAL_SUCCESS_INFO).exists()).toBe(true); + expect(wrapper.find(ERRORS_TABLE).exists()).toBe(true); + expect(wrapper.findAll("tbody > tr").wrappers.length).toBe(partialSuccessResult.errors.length); + }); + + it("should display a success message when everything went OK", async () => { + const wrapper = await mountCleanupResultDialogWith(SUCCESS_RESULT()); + + expect(wrapper.find(SUCCESS_INFO).exists()).toBe(true); + expect(wrapper.find(ERROR_ALERT).exists()).toBe(false); + expect(wrapper.find(PARTIAL_SUCCESS_INFO).exists()).toBe(false); + expect(wrapper.find(ERRORS_TABLE).exists()).toBe(false); + }); +}); diff --git a/client/src/components/User/DiskUsage/Management/Cleanup/CleanupResultDialog.vue b/client/src/components/User/DiskUsage/Management/Cleanup/CleanupResultDialog.vue new file mode 100644 index 00000000000..ac47be77e10 --- /dev/null +++ b/client/src/components/User/DiskUsage/Management/Cleanup/CleanupResultDialog.vue @@ -0,0 +1,81 @@ + + + diff --git a/client/src/components/User/DiskUsage/Management/Cleanup/ReviewCleanupDialog.test.js b/client/src/components/User/DiskUsage/Management/Cleanup/ReviewCleanupDialog.test.js new file mode 100644 index 00000000000..b003960edaa --- /dev/null +++ b/client/src/components/User/DiskUsage/Management/Cleanup/ReviewCleanupDialog.test.js @@ -0,0 +1,104 @@ +import { mount } from "@vue/test-utils"; +import flushPromises from "flush-promises"; +import { getLocalVue } from "jest/helpers"; +import { CleanableSummary, CleanupOperation, CleanupResult } from "./model"; +import ReviewCleanupDialog from "./ReviewCleanupDialog"; + +const localVue = getLocalVue(); + +const REVIEW_TABLE = '[data-test-id="review-table"]'; +const DELETE_BUTTON = '[data-test-id="delete-button"]'; +const SELECT_ALL_CHECKBOX = '[data-test-id="select-all-checkbox"]'; +const AGREEMENT_CHECKBOX = '[data-test-id="agreement-checkbox"]'; +const CONFIRMATION_MODAL = "#confirmation-modal"; + +const EXPECTED_TOTAL_ITEMS = 2; +const FAKE_OPERATION = () => { + return new CleanupOperation({ + id: "operation-id", + name: "operation name", + description: "operation description", + fetchSummary: () => + new CleanableSummary({ + totalSize: 1024, + totalItems: EXPECTED_TOTAL_ITEMS, + }), + fetchItems: () => [ + { id: "1", name: "Item 1", size: 512, update_time: new Date().toISOString(), hda_ldda: "hda" }, + { id: "2", name: "Item 2", size: 512, update_time: new Date().toISOString(), hda_ldda: "hda" }, + ], + cleanupItems: (items) => + new CleanupResult({ + totalItemCount: EXPECTED_TOTAL_ITEMS, + totalFreeBytes: 1024, + }), + }); +}; + +async function mountReviewCleanupDialogWith(operation, totalItems = EXPECTED_TOTAL_ITEMS) { + const wrapper = mount(ReviewCleanupDialog, { propsData: { operation, totalItems, show: true } }, localVue); + await flushPromises(); + return wrapper; +} + +describe("ReviewCleanupDialog.vue", () => { + it("should display a table with items to review", async () => { + const wrapper = await mountReviewCleanupDialogWith(FAKE_OPERATION()); + + expect(wrapper.find(REVIEW_TABLE).exists()).toBe(true); + expect(wrapper.findAll("tbody > tr").wrappers.length).toBe(EXPECTED_TOTAL_ITEMS); + }); + + it("should disable the delete button if no items are selected", async () => { + const wrapper = await mountReviewCleanupDialogWith(FAKE_OPERATION()); + const deleteButton = wrapper.find(DELETE_BUTTON); + const selectAllCheckbox = wrapper.find(SELECT_ALL_CHECKBOX); + + expect(deleteButton.element.disabled).toBeTruthy(); + expect(wrapper.vm.selectedItems.length).toBe(0); + await selectAllCheckbox.setChecked(); + expect(wrapper.vm.selectedItems.length).toBe(EXPECTED_TOTAL_ITEMS); + expect(deleteButton.element.disabled).toBeFalsy(); + }); + + it("should show a confirmation message when deleting items", async () => { + const wrapper = await mountReviewCleanupDialogWith(FAKE_OPERATION()); + await wrapper.find(SELECT_ALL_CHECKBOX).setChecked(); + const confirmationModal = wrapper.find(CONFIRMATION_MODAL); + + expect(confirmationModal.attributes("aria-hidden")).toBeTruthy(); + await wrapper.find(DELETE_BUTTON).trigger("click"); + expect(confirmationModal.attributes("aria-hidden")).toBeFalsy(); + }); + + it("should disable the confirmation button until the agreement has been accepted", async () => { + const wrapper = await mountReviewCleanupDialogWith(FAKE_OPERATION()); + await wrapper.find(SELECT_ALL_CHECKBOX).setChecked(); + await wrapper.find(DELETE_BUTTON).trigger("click"); + const permanentlyDeleteBtn = findByText(wrapper, "button", /Permanently delete/); + + expect(permanentlyDeleteBtn.element.disabled).toBeTruthy(); + await wrapper.find(AGREEMENT_CHECKBOX).setChecked(); + expect(permanentlyDeleteBtn.element.disabled).toBeFalsy(); + }); + + it("should emit the confirmation event when the agreement and deletion has been confirmed", async () => { + const wrapper = await mountReviewCleanupDialogWith(FAKE_OPERATION()); + await wrapper.find(SELECT_ALL_CHECKBOX).setChecked(); + await wrapper.find(DELETE_BUTTON).trigger("click"); + const permanentlyDeleteBtn = findByText(wrapper, "button", /Permanently delete/); + await wrapper.find(AGREEMENT_CHECKBOX).setChecked(); + + expect(wrapper.emitted().onConfirmCleanupSelectedItems).toBeFalsy(); + await permanentlyDeleteBtn.trigger("click"); + expect(wrapper.emitted().onConfirmCleanupSelectedItems).toBeTruthy(); + expect(wrapper.emitted().onConfirmCleanupSelectedItems.length).toBe(1); + }); + + function findByText(wrap, selector, text) { + return wrap + .findAll(selector) + .filter((n) => n.text().match(text)) + .at(0); + } +}); diff --git a/client/src/components/User/DiskUsage/Management/Cleanup/ReviewCleanupDialog.vue b/client/src/components/User/DiskUsage/Management/Cleanup/ReviewCleanupDialog.vue new file mode 100644 index 00000000000..17e6a517162 --- /dev/null +++ b/client/src/components/User/DiskUsage/Management/Cleanup/ReviewCleanupDialog.vue @@ -0,0 +1,264 @@ + + + diff --git a/client/src/components/User/DiskUsage/Management/Cleanup/categories.js b/client/src/components/User/DiskUsage/Management/Cleanup/categories.js new file mode 100644 index 00000000000..66c5dd9a27b --- /dev/null +++ b/client/src/components/User/DiskUsage/Management/Cleanup/categories.js @@ -0,0 +1,22 @@ +import _l from "utils/localization"; +import { cleanupDatasets, fetchDiscardedDatasets, fetchDiscardedDatasetsSummary } from "../services"; + +export const cleanupCategories = [ + { + name: _l("Discarded Items"), + operations: [ + { + id: "deleted_datasets", + name: _l("Deleted datasets"), + description: _l( + "When you delete a dataset it's not immediately removed from the disk (so you can recover it later)." + + " But this means it's still taking space until you permanently delete it." + + " Here you can quickly find and remove those datasets to free up some space" + ), + fetchSummary: fetchDiscardedDatasetsSummary, + fetchItems: fetchDiscardedDatasets, + cleanupItems: cleanupDatasets, + }, + ], + }, +]; diff --git a/client/src/components/User/DiskUsage/Management/Cleanup/cleanupCategoriesProvider.js b/client/src/components/User/DiskUsage/Management/Cleanup/cleanupCategoriesProvider.js new file mode 100644 index 00000000000..d53bbc56e13 --- /dev/null +++ b/client/src/components/User/DiskUsage/Management/Cleanup/cleanupCategoriesProvider.js @@ -0,0 +1,23 @@ +import { cleanupCategories } from "./categories"; +import { CleanupCategory } from "./model"; + +export default { + created() { + this.categories = this.loadCategories(); + }, + data() { + return { + categories: [], + }; + }, + methods: { + loadCategories() { + return cleanupCategories.map((category) => new CleanupCategory(category)); + }, + }, + render() { + return this.$scopedSlots.default({ + categories: this.categories, + }); + }, +}; diff --git a/client/src/components/User/DiskUsage/Management/Cleanup/index.js b/client/src/components/User/DiskUsage/Management/Cleanup/index.js new file mode 100644 index 00000000000..15496b37911 --- /dev/null +++ b/client/src/components/User/DiskUsage/Management/Cleanup/index.js @@ -0,0 +1,4 @@ +export { CleanupCategoriesProvider } from "./cleanupCategoriesProvider"; +export { CleanupOperationSummary } from "./CleanupOperationSummary"; +export { CleanupResultDialog } from "./CleanupResultDialog"; +export { ReviewCleanupDialog } from "./ReviewCleanupDialog"; diff --git a/client/src/components/User/DiskUsage/Management/Cleanup/model/CleanableSummary.js b/client/src/components/User/DiskUsage/Management/Cleanup/model/CleanableSummary.js new file mode 100644 index 00000000000..7e81e0d0645 --- /dev/null +++ b/client/src/components/User/DiskUsage/Management/Cleanup/model/CleanableSummary.js @@ -0,0 +1,35 @@ +import { bytesToString } from "utils/utils"; + +/** + * Contains summary information about how much storage space can be recovered by removing + * a collection of items from it. + */ +export class CleanableSummary { + constructor(data) { + this._totalSize = data.totalSize; + this._totalItems = data.totalItems; + } + + /** + * The total size in bytes that can be recovered by removing all the items. + * @returns {Number} + */ + get totalSize() { + return this._totalSize; + } + + /** + * The human readable total amount of disk space that can be recovered. + * @returns {String} + */ + get niceTotalSize() { + return bytesToString(this.totalSize, true); + } + + /** + * The total number of items that could be removed. + */ + get totalItems() { + return this._totalItems; + } +} diff --git a/client/src/components/User/DiskUsage/Management/Cleanup/model/CleanupCategory.js b/client/src/components/User/DiskUsage/Management/Cleanup/model/CleanupCategory.js new file mode 100644 index 00000000000..281791fa3f4 --- /dev/null +++ b/client/src/components/User/DiskUsage/Management/Cleanup/model/CleanupCategory.js @@ -0,0 +1,33 @@ +import { CleanupOperation } from "."; + +export class CleanupCategory { + constructor(data) { + this._id = data.id; + this._name = data.name; + this._operations = data.operations.map((op) => new CleanupOperation(op)); + } + + /** + * The ID of this category. + * @returns {String} + */ + get id() { + return this._id; + } + + /** + * The name of this category. + * @returns {String} + */ + get name() { + return this._name; + } + + /** + * The collection of cleanup operations associated with + * this category. + */ + get operations() { + return this._operations; + } +} diff --git a/client/src/components/User/DiskUsage/Management/Cleanup/model/CleanupOperation.js b/client/src/components/User/DiskUsage/Management/Cleanup/model/CleanupOperation.js new file mode 100644 index 00000000000..ce40dfa3cfd --- /dev/null +++ b/client/src/components/User/DiskUsage/Management/Cleanup/model/CleanupOperation.js @@ -0,0 +1,67 @@ +/** + * Represents an operation that can potentially `clean` the user storage. + * The concept of `cleaning` here refers to any action that can free up + * some space in the user storage. + */ +export class CleanupOperation { + constructor(data) { + this._id = data.id; + this._name = data.name; + this._description = data.description; + this._fetchSummary = data.fetchSummary; + this._fetchItems = data.fetchItems; + this._cleanupItems = data.cleanupItems; + } + + /** + * The ID of this cleanup operation. + * @returns {String} + */ + get id() { + return this._id; + } + + /** + * The name of this cleanup operation. + * @returns {String} + */ + get name() { + return this._name; + } + + /** + * The description of this cleanup operation. + * @returns {String} + */ + get description() { + return this._description; + } + + /** + * Fetches summary information about the total amount of + * space that can be cleaned/recovered using this operation. + * @returns {Promise} + */ + async fetchSummary() { + return await this._fetchSummary(); + } + + /** + * Fetches an array of items that can be potentially `cleaned` by this operation. + * @param {Object} filterOptions The filter options for sorting and pagination of the items. + * @returns {Promise>} An array of items that can be potentially `cleaned` and match the filtering params. + */ + async fetchItems(filterOptions) { + return await this._fetchItems(filterOptions); + } + + /** + * Processes the given items to free up some user storage space and provides a result + * indicating how much was space was recovered or what errors may have ocurred. + * @param {Array} items An array of items to be `cleaned` + * @returns {Promise} The result of the cleanup operation. + */ + async cleanupItems(items) { + return await this._cleanupItems(items); + } +} diff --git a/client/src/components/User/DiskUsage/Management/Cleanup/model/CleanupResult.js b/client/src/components/User/DiskUsage/Management/Cleanup/model/CleanupResult.js new file mode 100644 index 00000000000..baf3aa6514c --- /dev/null +++ b/client/src/components/User/DiskUsage/Management/Cleanup/model/CleanupResult.js @@ -0,0 +1,64 @@ +import { bytesToString } from "utils/utils"; + +/** + * Contains information about the result of the cleaning operation. + */ +export class CleanupResult { + constructor(props = {}) { + this.totalItemCount = props.totalItemCount || 0; + this.totalFreeBytes = props.totalFreeBytes || 0; + this.errors = props.errors || []; + this.errorMessage = props.errorMessage || null; + } + + /** + * Whether the cleanup operation yielded some errors. + * It doesn't mean the operation completely failed. + * @returns {boolean} + */ + get hasSomeErrors() { + return this.errors.length > 0; + } + + /** + * Whether the cleanup operation completely failed. + * This means not even partial cleaning was made. + * @returns {boolean} + */ + get hasFailed() { + return this.errorMessage !== null; + } + + /** + * Whether the cleanup operation was executed without errors. + * @returns {boolean} + */ + get success() { + return !this.hasSomeErrors && !this.errorMessage; + } + + /** + * The number of items successfully cleaned. + * @returns {number} + */ + get totalCleaned() { + return this.totalItemCount - this.errors.length; + } + + /** + * Whether the cleanup operation managed to + * free some items but not all of them. + * @returns {boolean} + */ + get isPartialSuccess() { + return this.errors.length > 0 && this.totalCleaned > 0; + } + + /** + * The total amount of disk space freed by the cleanup operation. + * @returns {String} + */ + get niceTotalFreeBytes() { + return bytesToString(this.totalFreeBytes, true); + } +} diff --git a/client/src/components/User/DiskUsage/Management/Cleanup/model/index.js b/client/src/components/User/DiskUsage/Management/Cleanup/model/index.js new file mode 100644 index 00000000000..e4a6554677d --- /dev/null +++ b/client/src/components/User/DiskUsage/Management/Cleanup/model/index.js @@ -0,0 +1,4 @@ +export { CleanableSummary } from "./CleanableSummary"; +export { CleanupCategory } from "./CleanupCategory"; +export { CleanupOperation } from "./CleanupOperation"; +export { CleanupResult } from "./CleanupResult"; diff --git a/client/src/components/User/DiskUsage/Management/StorageManager.vue b/client/src/components/User/DiskUsage/Management/StorageManager.vue new file mode 100644 index 00000000000..68c5f943e50 --- /dev/null +++ b/client/src/components/User/DiskUsage/Management/StorageManager.vue @@ -0,0 +1,104 @@ + + + diff --git a/client/src/components/User/DiskUsage/Management/services.js b/client/src/components/User/DiskUsage/Management/services.js new file mode 100644 index 00000000000..c940d37441f --- /dev/null +++ b/client/src/components/User/DiskUsage/Management/services.js @@ -0,0 +1,119 @@ +import axios from "axios"; +import { getAppRoot } from "onload/loadConfig"; +import { rethrowSimple } from "utils/simple-error"; +import { CleanableSummary, CleanupResult } from "./Cleanup/model"; + +const datasetKeys = "id,name,size,update_time,hda_ldda"; +const isDataset = "q=history_content_type-eq&qv=dataset"; +const isDeleted = "q=deleted-eq&qv=True"; +const isNotPurged = "q=purged-eq&qv=False"; +const maxItemsToFetch = 500; +const discardedDatasetsQueryParams = `${isDataset}&${isDeleted}&${isNotPurged}&limit=${maxItemsToFetch}`; + +/** + * Calculates the total amount of bytes that can be cleaned by permanently removing + * deleted datasets. + * @returns {CleanableSummary} Object containing information about how much can be cleaned. + */ +export async function fetchDiscardedDatasetsSummary() { + //TODO: possible optimization -> moving this to specific API endpoint so we don't have to parse + // potentially a huge number of items + const summaryKeys = "size"; + const url = `${getAppRoot()}api/datasets?keys=${summaryKeys}&${discardedDatasetsQueryParams}`; + try { + const { data } = await axios.get(url); + const totalSizeInBytes = data.reduce((partial_sum, item) => partial_sum + item.size, 0); + return new CleanableSummary({ + totalSize: totalSizeInBytes, + totalItems: data.length, + }); + } catch (e) { + rethrowSimple(e); + } +} + +/** + * Retrieves all deleted datasets of the current user that haven't been purged yet using pagination. + * @param {Object} options Filtering options for pagination and sorting. + * @returns {Array} Array of dataset objects with the fields defined in `datasetKeys` constant. + */ +export async function fetchDiscardedDatasets(options = {}) { + let params = ""; + if (options.sortBy) { + const sortPostfix = options.sortDesc ? "-dsc" : "-asc"; + params += `order=${options.sortBy}${sortPostfix}&`; + } + if (options.limit) { + params += `limit=${options.limit}&`; + } + if (options.offset) { + params += `offset=${options.offset}&`; + } + const url = `${getAppRoot()}api/datasets?keys=${datasetKeys}&${discardedDatasetsQueryParams}&${params}`; + try { + const { data } = await axios.get(url); + return data; + } catch (e) { + rethrowSimple(e); + } +} + +/** + * Purges a collection of datasets. + * @param {Array} datasetSourceIds Array of objects with datasets {id, src} to be purged. + * @returns {Object} Result object with `success_count` and `errors`. + */ +export async function purgeDatasets(datasetSourceIds) { + const payload = { + purge: true, + datasets: datasetSourceIds, + }; + const url = `${getAppRoot()}api/datasets`; + try { + const { data } = await axios.delete(url, { data: payload }); + return data; + } catch (e) { + rethrowSimple(e); + } +} + +/** + * Purges a set of datasets instances (HDA, LDDA, ...) from disk and returns the total space freed in bytes + * taking into account possible datasets that couldn't be deleted. + * @param {Array} datasets Array of datasets to be removed from disk. + * Each dataset must contain `id` and `size`. + * @returns {CleanupResult} + */ +export async function cleanupDatasets(datasets) { + const result = new CleanupResult(); + try { + const datasetsTable = datasets.reduce((acc, item) => ((acc[item.id] = item), acc), {}); + const datasetSourceIds = datasets.map((dataset) => { + return { id: dataset.id, src: dataset.hda_ldda }; + }); + const requestResult = await purgeDatasets(datasetSourceIds); + result.totalItemCount = datasets.length; + result.errors = mapErrors(datasetsTable, requestResult.errors); + const erroredIds = requestResult.errors.reduce((acc, error) => [...acc, error.dataset.id], []); + result.totalFreeBytes = datasetSourceIds.reduce( + (partial_sum, item) => partial_sum + (erroredIds.includes(item.id) ? 0 : datasetsTable[item.id].size), + 0 + ); + } catch (error) { + result.errorMessage = error; + } + return result; +} + +/** + * Maps the error messages with the dataset name for user display. + * @param datasetsTable Datasets dictionary indexed by ID + * @param errors List of errors associated with each dataset ID + * @returns A list with the name of the dataset and the associated error message. + */ +function mapErrors(datasetsTable, errors) { + return errors.map((error) => { + const name = datasetsTable[error.dataset.id].name; + return { name: name, reason: error.error_message }; + }); +} diff --git a/client/src/components/User/DiskUsage/Quota/QuotaUsageBar.test.js b/client/src/components/User/DiskUsage/Quota/QuotaUsageBar.test.js new file mode 100644 index 00000000000..f78309e51cf --- /dev/null +++ b/client/src/components/User/DiskUsage/Quota/QuotaUsageBar.test.js @@ -0,0 +1,55 @@ +import { mount } from "@vue/test-utils"; +import { getLocalVue } from "jest/helpers"; +import QuotaUsageBar from "./QuotaUsageBar"; + +const localVue = getLocalVue(); + +const NON_DEFAULT_QUOTA_USAGE = { + sourceLabel: "The label", + quotaInBytes: 68468436, + totalDiskUsageInBytes: 4546654, + quotaPercent: 20, + niceQuota: "X MB", + niceTotalDiskUsage: "Y MB", + isUnlimited: false, +}; + +const UNLIMITED_USAGE = { + sourceLabel: "Unlimited", + quotaInBytes: null, + totalDiskUsageInBytes: 4546654, + quotaPercent: null, + niceQuota: "unlimited", + niceTotalDiskUsage: "Y MB", + isUnlimited: true, +}; + +function mountQuotaUsageBarWith(quotaUsage) { + const wrapper = mount(QuotaUsageBar, { propsData: { quotaUsage } }, localVue); + return wrapper; +} + +describe("QuotaUsageBar.vue", () => { + it("should display the quota source label when the quota source is not the default one", async () => { + const wrapper = mountQuotaUsageBarWith(NON_DEFAULT_QUOTA_USAGE); + const storageSourceLabel = wrapper.find(".storage-source-label"); + expect(wrapper.vm.isDefaultQuota).toBe(false); + expect(storageSourceLabel.exists()).toBe(true); + expect(storageSourceLabel.text()).toBe(NON_DEFAULT_QUOTA_USAGE.sourceLabel); + }); + + it("should show the usage percent when there is a quota limit", async () => { + const wrapper = mountQuotaUsageBarWith(NON_DEFAULT_QUOTA_USAGE); + const percentText = wrapper.find(".quota-percent-text"); + expect(wrapper.vm.quotaHasLimit).toBe(true); + expect(percentText.exists()).toBe(true); + expect(percentText.text()).toContain(NON_DEFAULT_QUOTA_USAGE.quotaPercent.toString()); + }); + + it("should not show percentage when there is no quota limit", async () => { + const wrapper = mountQuotaUsageBarWith(UNLIMITED_USAGE); + const percentText = wrapper.find(".quota-percent-text"); + expect(wrapper.vm.quotaHasLimit).toBe(false); + expect(percentText.exists()).toBe(false); + }); +}); diff --git a/client/src/components/User/DiskUsage/Quota/QuotaUsageBar.vue b/client/src/components/User/DiskUsage/Quota/QuotaUsageBar.vue new file mode 100644 index 00000000000..a2a0ad623c8 --- /dev/null +++ b/client/src/components/User/DiskUsage/Quota/QuotaUsageBar.vue @@ -0,0 +1,59 @@ + + + diff --git a/client/src/components/User/DiskUsage/Quota/QuotaUsageProvider.js b/client/src/components/User/DiskUsage/Quota/QuotaUsageProvider.js new file mode 100644 index 00000000000..a794b00291a --- /dev/null +++ b/client/src/components/User/DiskUsage/Quota/QuotaUsageProvider.js @@ -0,0 +1,24 @@ +import axios from "axios"; +import { SingleQueryProvider } from "components/providers/SingleQueryProvider"; +import { getAppRoot } from "onload/loadConfig"; +import { rethrowSimple } from "utils/simple-error"; +import { QuotaUsage } from "./model"; + +// TODO: replace this with the proper provider and API call after +// https://github.com/galaxyproject/galaxy/pull/10977 is available + +/** + * Fetches the disk usage by the user across all ObjectStores. + * @returns {Array} + */ +async function fetchQuotaUsage() { + const url = `${getAppRoot()}api/users/current`; + try { + const { data } = await axios.get(url); + return [new QuotaUsage(data)]; + } catch (e) { + rethrowSimple(e); + } +} + +export const QuotaUsageProvider = SingleQueryProvider(fetchQuotaUsage); diff --git a/client/src/components/User/DiskUsage/Quota/QuotaUsageSummary.test.js b/client/src/components/User/DiskUsage/Quota/QuotaUsageSummary.test.js new file mode 100644 index 00000000000..e4df21e7c4f --- /dev/null +++ b/client/src/components/User/DiskUsage/Quota/QuotaUsageSummary.test.js @@ -0,0 +1,44 @@ +import { shallowMount } from "@vue/test-utils"; +import { getLocalVue } from "jest/helpers"; +import QuotaUsageSummary from "./QuotaUsageSummary"; + +const localVue = getLocalVue(); + +const QUOTA_1_BYTES = 654846535; +const QUOTA_2_BYTES = 68468436; + +const FAKE_QUOTA_USAGES_LIST = [ + { + sourceLabel: "source 1", + quotaInBytes: QUOTA_1_BYTES, + }, + { + sourceLabel: "source 2", + quotaInBytes: QUOTA_2_BYTES, + }, + { + sourceLabel: "Unlimited source", + quotaInBytes: null, + }, +]; + +function mountQuotaUsageSummaryWith(quotaUsages) { + const wrapper = shallowMount(QuotaUsageSummary, { propsData: { quotaUsages } }, localVue); + return wrapper; +} + +describe("QuotaUsageSummary.vue", () => { + it("should calculate the total amount of quotas without the unlimited", () => { + const wrapper = mountQuotaUsageSummaryWith(FAKE_QUOTA_USAGES_LIST); + const expectedTotalBytes = QUOTA_1_BYTES + QUOTA_2_BYTES; + + expect(wrapper.vm.totalQuota).toBe(expectedTotalBytes); + }); + + it("should display a quota bar for each quota", () => { + const wrapper = mountQuotaUsageSummaryWith(FAKE_QUOTA_USAGES_LIST); + const expectedNumberOfBars = FAKE_QUOTA_USAGES_LIST.length; + + expect(wrapper.findAll("quotausagebar-stub").length).toBe(expectedNumberOfBars); + }); +}); diff --git a/client/src/components/User/DiskUsage/Quota/QuotaUsageSummary.vue b/client/src/components/User/DiskUsage/Quota/QuotaUsageSummary.vue new file mode 100644 index 00000000000..2ddce96ecec --- /dev/null +++ b/client/src/components/User/DiskUsage/Quota/QuotaUsageSummary.vue @@ -0,0 +1,53 @@ + + + diff --git a/client/src/components/User/DiskUsage/Quota/model/QuotaUsage.js b/client/src/components/User/DiskUsage/Quota/model/QuotaUsage.js new file mode 100644 index 00000000000..f4bcc2a4d0e --- /dev/null +++ b/client/src/components/User/DiskUsage/Quota/model/QuotaUsage.js @@ -0,0 +1,71 @@ +import { bytesToString } from "utils/utils"; + +export const DEFAULT_QUOTA_SOURCE_LABEL = "Default"; + +/** + * Contains information about quota usage for a particular ObjectStore. + */ +export class QuotaUsage { + constructor(data) { + this._data = data; + } + + /** + * The name of the ObjectStore associated with the quota. + * @returns {String} + */ + get sourceLabel() { + return this._data.quota_source_label || DEFAULT_QUOTA_SOURCE_LABEL; + } + + /** + * The maximum allowed disk usage in bytes. + * @returns {Number} + */ + get quotaInBytes() { + return this._data.quota_bytes; + } + + /** + * The total amount of bytes used in this ObjectStore. + * @returns {Number} + */ + get totalDiskUsageInBytes() { + return this._data.total_disk_usage; + } + + /** + * The percentage of used quota. + * @returns {Number} + */ + get quotaPercent() { + return this._data.quota_percent; + } + + /** + * The maximum allowed disk usage as human readable size. + * @returns {String} + */ + get niceQuota() { + if (this.isUnlimited) { + return "unlimited"; + } + return bytesToString(this.quotaInBytes, true); + } + + /** + * The total amount of disk used in this ObjectStore as human readable size. + * @returns {String} + */ + get niceTotalDiskUsage() { + return bytesToString(this.totalDiskUsageInBytes, true); + } + + /** + * Whether this ObjectStore has unlimited quota + * @returns {Boolean} + */ + get isUnlimited() { + return !this.quotaInBytes; + } +} diff --git a/client/src/components/User/DiskUsage/Quota/model/QuotaUsage.test.js b/client/src/components/User/DiskUsage/Quota/model/QuotaUsage.test.js new file mode 100644 index 00000000000..0c647689685 --- /dev/null +++ b/client/src/components/User/DiskUsage/Quota/model/QuotaUsage.test.js @@ -0,0 +1,46 @@ +import { DEFAULT_QUOTA_SOURCE_LABEL, QuotaUsage } from "./QuotaUsage"; + +const RAW_DEFAULT_QUOTA_USAGE = { + quota_source_label: null, + quota_bytes: 68468436, + total_disk_usage: 4546654, + quota_percent: 20, +}; + +const RAW_LIMITED_SOURCE_QUOTA_USAGE = { + quota_source_label: "The source", + quota_bytes: 68468436, + total_disk_usage: 4546654, + quota_percent: 20, +}; + +const RAW_UNLIMITED_SOURCE_QUOTA_USAGE = { + quota_source_label: "The unlimited source", + quota_bytes: null, + total_disk_usage: 4546654, + quota_percent: null, +}; + +describe("QuotaUsage", () => { + const defaultQuotaUsage = new QuotaUsage(RAW_DEFAULT_QUOTA_USAGE); + const limitedQuotaUsage = new QuotaUsage(RAW_LIMITED_SOURCE_QUOTA_USAGE); + const unlimitedQuotaUsage = new QuotaUsage(RAW_UNLIMITED_SOURCE_QUOTA_USAGE); + + test("default quota source label should have the default value", () => { + expect(defaultQuotaUsage.sourceLabel).toBe(DEFAULT_QUOTA_SOURCE_LABEL); + }); + + test("non default quota source label should have the correct value", () => { + expect(limitedQuotaUsage.sourceLabel).toBe(RAW_LIMITED_SOURCE_QUOTA_USAGE.quota_source_label); + }); + + test("nice quota should be 'unlimited' when the source is unlimited", () => { + expect(unlimitedQuotaUsage.isUnlimited).toBe(true); + expect(unlimitedQuotaUsage.niceQuota).toBe("unlimited"); + }); + + test("is unlimited should indicate the correct status", () => { + expect(unlimitedQuotaUsage.isUnlimited).toBe(true); + expect(limitedQuotaUsage.isUnlimited).toBe(false); + }); +}); diff --git a/client/src/components/User/DiskUsage/Quota/model/index.js b/client/src/components/User/DiskUsage/Quota/model/index.js new file mode 100644 index 00000000000..43c68fd60b0 --- /dev/null +++ b/client/src/components/User/DiskUsage/Quota/model/index.js @@ -0,0 +1 @@ +export { QuotaUsage } from "./QuotaUsage"; diff --git a/client/src/components/User/DiskUsage/StorageDashboard.vue b/client/src/components/User/DiskUsage/StorageDashboard.vue new file mode 100644 index 00000000000..164ac0187f7 --- /dev/null +++ b/client/src/components/User/DiskUsage/StorageDashboard.vue @@ -0,0 +1,52 @@ + + + diff --git a/client/src/components/User/DiskUsage/StorageDashboardRouter.js b/client/src/components/User/DiskUsage/StorageDashboardRouter.js new file mode 100644 index 00000000000..a53345cc389 --- /dev/null +++ b/client/src/components/User/DiskUsage/StorageDashboardRouter.js @@ -0,0 +1,29 @@ +import { getAppRoot } from "onload/loadConfig"; +import Vue from "vue"; +import VueRouter from "vue-router"; +import StorageManager from "./Management/StorageManager"; +import StorageDashboard from "./StorageDashboard"; + +Vue.use(VueRouter); + +export default new VueRouter({ + mode: "history", + base: `${getAppRoot()}storage`, + routes: [ + { + path: "/", + name: "StorageDashboard", + component: StorageDashboard, + }, + { + path: "/management", + name: "StorageManager", + component: StorageManager, + props: true, + }, + { + path: "*", + redirect: { name: "StorageDashboard" }, + }, + ], +}); diff --git a/client/src/components/User/DiskUsage/index.js b/client/src/components/User/DiskUsage/index.js new file mode 100644 index 00000000000..2374b3b8c75 --- /dev/null +++ b/client/src/components/User/DiskUsage/index.js @@ -0,0 +1,8 @@ +import router from "./StorageDashboardRouter.js"; + +export const StorageDashboardRouter = { + router, + render: (h) => h("router-view"), +}; + +export default StorageDashboardRouter; diff --git a/client/src/components/User/UserPreferences.vue b/client/src/components/User/UserPreferences.vue index 6066a5bf348..c3a5e7b2a6a 100644 --- a/client/src/components/User/UserPreferences.vue +++ b/client/src/components/User/UserPreferences.vue @@ -177,16 +177,20 @@ export default { }, methods: { toggleNotifications() { - Notification.requestPermission().then(function (permission) { - //If the user accepts, let's create a notification - if (permission === "granted") { - new Notification("Notifications enabled", { - icon: "static/favicon.ico", - }); - } else { - alert("Notifications disabled, please re-enable through browser settings."); - } - }); + if (window.Notification) { + Notification.requestPermission().then(function (permission) { + //If the user accepts, let's create a notification + if (permission === "granted") { + new Notification("Notifications enabled", { + icon: "static/favicon.ico", + }); + } else { + alert("Notifications disabled, please re-enable through browser settings."); + } + }); + } else { + alert("Notifications are not supported by this browser."); + } }, openManageCustomBuilds() { const Galaxy = getGalaxyInstance(); diff --git a/client/src/components/Visualizations/DisplayApplications.test.js b/client/src/components/Visualizations/DisplayApplications.test.js index 045a9dd67a4..1cafa13934a 100644 --- a/client/src/components/Visualizations/DisplayApplications.test.js +++ b/client/src/components/Visualizations/DisplayApplications.test.js @@ -44,7 +44,6 @@ describe("DisplayApplications", () => { }, ], }, - resultLabel: "item", }), }, localVue, diff --git a/client/src/components/Visualizations/DisplayApplications.vue b/client/src/components/Visualizations/DisplayApplications.vue index f70946adc77..23131a19ee0 100644 --- a/client/src/components/Visualizations/DisplayApplications.vue +++ b/client/src/components/Visualizations/DisplayApplications.vue @@ -1,6 +1,6 @@ diff --git a/client/src/components/WorkflowInvocationState/WorkflowInvocationState.vue b/client/src/components/WorkflowInvocationState/WorkflowInvocationState.vue index 928e69e9dd4..dc28a0bc158 100644 --- a/client/src/components/WorkflowInvocationState/WorkflowInvocationState.vue +++ b/client/src/components/WorkflowInvocationState/WorkflowInvocationState.vue @@ -133,7 +133,7 @@ export default { return stepStates; }, invocationAndJobTerminal: function () { - return this.invocationSchedulingTerminal && this.jobStatesTerminal; + return !!(this.invocationSchedulingTerminal && this.jobStatesTerminal); }, invocationLink: function () { return getUrl(`workflows/invocations/report?id=${this.invocationId}`); diff --git a/client/src/components/WorkflowInvocationState/WorkflowInvocationStep.vue b/client/src/components/WorkflowInvocationState/WorkflowInvocationStep.vue index 92a493d86d0..c7b35ab9c66 100644 --- a/client/src/components/WorkflowInvocationState/WorkflowInvocationStep.vue +++ b/client/src/components/WorkflowInvocationState/WorkflowInvocationStep.vue @@ -11,7 +11,8 @@ + auto-refresh + v-slot="{ result: stepDetails, loading }">
@@ -21,7 +22,7 @@ Output Datasets
{{ name }} - +
Output Dataset Collections
{{ name }} - +
@@ -40,16 +41,20 @@ - + :parameters="[invocation.input_step_parameters[stepDetails.workflow_step_label]]" /> +
Workflow invocation for this step is not yet scheduled. -

+
This step consumes outputs from these steps:
  • import("components/WorkflowInvocationState/WorkflowInvocationState"), }, props: { diff --git a/client/src/components/admin/Jobs.vue b/client/src/components/admin/Jobs.vue index 0c320f1bb58..8faf8385ae3 100644 --- a/client/src/components/admin/Jobs.vue +++ b/client/src/components/admin/Jobs.vue @@ -1,7 +1,7 @@