Merge branch 'dev' of https://github.com/galaxyproject/galaxy into menu-cleanup

This commit is contained in:
Assunta DeSanto
2022-03-21 09:12:41 -04:00
959 changed files with 12824 additions and 41202 deletions
+1
View File
@@ -0,0 +1 @@
comment: false
+14 -3
View File
@@ -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:
+7
View File
@@ -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'
+6 -2
View File
@@ -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:
-9
View File
@@ -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 }}
+13 -2
View File
@@ -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:
+14 -2
View File
@@ -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_AMQP_URL: 'amqp://localhost:5672//'
GALAXY_TEST_DBURI: 'postgresql://postgres:postgres@localhost:5432/galaxy?client_encoding=utf8'
@@ -41,6 +44,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 +82,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:
+14 -2
View File
@@ -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:
+3 -1
View File
@@ -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
+8 -1
View File
@@ -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
+4 -5
View File
@@ -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'
+13 -2
View File
@@ -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:
+13 -2
View File
@@ -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'
@@ -37,6 +40,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 +62,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:
+1 -1
View File
@@ -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'
@@ -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'
+8 -6
View File
@@ -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:
+7 -1
View File
@@ -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
+2 -5
View File
@@ -38,8 +38,8 @@ 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: ## Format Python code changes
$(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 .
@@ -64,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
+2 -2
View File
@@ -24,12 +24,12 @@ Community support is available at `Galaxy Help <https://help.galaxyproject.org/>
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:
+50 -31
View File
@@ -35,16 +35,16 @@ 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: [
const PATHS = {
nodeModules: "./node_modules",
pluginDirs: [
"../config/plugins/{visualizations,interactive_environments,welcome_page}/*/static/**/*",
"../config/plugins/{visualizations,interactive_environments,welcome_page}/*/*/static/**/*",
],
plugin_build_modules: [
pluginBuildModules: [
`../config/plugins/{visualizations,welcome_page}/{${PLUGIN_BUILD_IDS.join(",")}}/package.json`,
],
lib_locs: {
stagedLibraries: {
// This is a stepping stone towards having all this staged
// automatically. Eventually, this dictionary and staging step will
// not be necessary.
@@ -57,13 +57,12 @@ const paths = {
requirejs: ["require.js", "require.js"],
underscore: ["underscore.js", "underscore.js"],
},
libs: ["src/libs/**/*.js"],
};
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,45 +77,49 @@ 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) => {
PATHS.pluginBuildModules.map((build_module) => {
glob(build_module, {}, (er, files) => {
files.map((file) => {
let skip_build = false;
let skipBuild = 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(
const pluginName = path.dirname(file).split(path.sep).pop();
const hashFilePath = path.join(
f,
DIST_PLUGIN_BUILD_IDS.indexOf(plugin_name) > -1 ? "dist" : "static",
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})`, "--", f], {
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"],
@@ -126,9 +129,19 @@ function buildPlugins(callback) {
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: f, 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,12 +149,17 @@ 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 });
}
const client = parallel(fonts, stageLibs);
const plugins = series(buildPlugins, cleanPlugins, stagePlugins);
const pluginsRebuild = series(forceBuildPlugins, cleanPlugins, stagePlugins);
function watchPlugins() {
const BUILD_PLUGIN_WATCH_GLOB = [
@@ -152,5 +170,6 @@ function watchPlugins() {
module.exports.client = client;
module.exports.plugins = plugins;
module.exports.pluginsRebuild = pluginsRebuild;
module.exports.watchPlugins = watchPlugins;
module.exports.default = parallel(client, plugins);
+5 -10
View File
@@ -20,7 +20,7 @@
"@fortawesome/free-regular-svg-icons": "^5.15.4",
"@fortawesome/free-solid-svg-icons": "^5.15.4",
"@fortawesome/vue-fontawesome": "^2.0.6",
"@galaxyproject/bootstrap-tour": "^0.12.1",
"@galaxyproject/bootstrap-tour": "0.12.2",
"@handsontable/vue": "^2.0.0-beta1",
"@hirez_io/observer-spy": "^2.1.2",
"@johmun/vue-tags-input": "^2.1.0",
@@ -57,17 +57,12 @@
"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",
"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.1.0",
"pyre-to-regexp": "^0.0.5",
@@ -97,12 +92,12 @@
"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.6.2",
"vuex-cache": "^3.4.0",
"vuex-persist": "^3.1.3",
"vuex-persistedstate": "^4.1.0",
"vuex-persist": "2.3",
"xml-beautifier": "^0.5.0"
},
"scripts": {
@@ -160,7 +155,7 @@
"jest-raw-loader": "^1.0.1",
"jest-transform-yaml": "1.0.0",
"json-loader": "^0.5.7",
"karma": "^6.3.13",
"karma": "^6.3.16",
"karma-chrome-launcher": "^3.1.0",
"karma-polyfill": "^1.1.0",
"karma-qunit": "^4.1.2",
@@ -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();
});
+1 -11
View File
@@ -2,13 +2,7 @@
<b-button v-if="wait" variant="info" disabled>
<font-awesome-icon icon="spinner" class="mr-2" spin />Please wait...
</b-button>
<b-button
v-else
variant="primary"
v-b-tooltip.hover.bottom
:title="tooltip"
:disabled="disabled"
@click="$emit('onClick')">
<b-button v-else variant="primary" v-b-tooltip.hover.bottom :title="tooltip" @click="$emit('onClick')">
<font-awesome-icon icon="check" class="mr-2" />{{ title }}
</b-button>
</template>
@@ -37,10 +31,6 @@ export default {
type: String,
default: null,
},
disabled: {
type: Boolean,
default: false,
},
},
};
</script>
@@ -1,4 +1,3 @@
import Vue from "vue";
import axios from "axios";
import { mount } from "@vue/test-utils";
import { getLocalVue } from "jest/helpers";
@@ -9,8 +8,8 @@ import flushPromises from "flush-promises";
const localVue = getLocalVue();
function buildWrapper(conversion_disable = false) {
return mount(DatasetAttributes, {
async function buildWrapper(conversion_disable = false) {
const wrapper = mount(DatasetAttributes, {
propsData: {
datasetId: "dataset_id",
},
@@ -29,14 +28,15 @@ function buildWrapper(conversion_disable = false) {
FormElement: false,
},
});
await flushPromises();
return wrapper;
}
describe("DatasetAttributes", () => {
it("check rendering", async () => {
const axiosMock = new MockAdapter(axios);
axiosMock.onPut(`/dataset/set_edit`).reply(200, { message: "success", status: "success" });
const wrapper = buildWrapper();
await Vue.nextTick();
const wrapper = await buildWrapper();
expect(wrapper.findAll("button").length).toBe(6);
expect(wrapper.findAll("#attribute_text").length).toBe(1);
expect(wrapper.findAll("#conversion_text").length).toBe(1);
@@ -50,8 +50,7 @@ describe("DatasetAttributes", () => {
});
it("check rendering without conversion option", async () => {
const wrapper = buildWrapper(true);
await Vue.nextTick();
const wrapper = await buildWrapper(true);
expect(wrapper.findAll("button").length).toBe(5);
expect(wrapper.findAll("#attribute_text").length).toBe(1);
expect(wrapper.findAll("#conversion_text").length).toBe(0);
@@ -4,7 +4,7 @@
<b-alert v-if="messageText" :variant="messageVariant" show>
{{ messageText | l }}
</b-alert>
<DatasetAttributesProvider :id="datasetId" :use-cache="false" v-slot="{ result, loading }" @error="onError">
<DatasetAttributesProvider :id="datasetId" v-slot="{ result, loading }" @error="onError">
<div v-if="!loading" class="mt-3">
<b-tabs>
<b-tab v-if="!result['attribute_disable']">
@@ -2,7 +2,7 @@
<ConfigProvider v-slot="{ config }">
<DatasetProvider
:id="datasetId"
v-slot="{ item: dataset, loading: isDatasetLoading, error: datasetLoadingError }">
v-slot="{ result: dataset, loading: isDatasetLoading, error: datasetLoadingError }">
<div>
<LoadingSpan v-if="isDatasetLoading" />
<Alert v-else-if="datasetLoadingError" :message="datasetLoadingError" variant="error" />
@@ -10,8 +10,8 @@
<JobDetailsProvider
v-if="!isDatasetLoading && dataset.creating_job !== null"
:jobid="dataset.creating_job"
v-slot="{ result: job, loading: isJobLoading }"
:use-cache="false">
auto-refresh
v-slot="{ result: job, loading: isJobLoading }">
<div v-if="!isJobLoading">
<dataset-information class="detail" :hda_id="datasetId" />
<job-parameters class="detail" dataset_type="hda" :dataset-id="datasetId" />
@@ -29,7 +29,6 @@ function buildWrapper(has_duplicate_inputs = true, has_empty_inputs = true, user
result: { has_duplicate_inputs: has_duplicate_inputs, has_empty_inputs: has_empty_inputs },
}),
DatasetProvider: MockProvider({
resultLabel: "item",
result: { id: "dataset_id", creating_job: "creating_job" },
}),
FontAwesomeIcon: false,
@@ -4,7 +4,7 @@
<h4 class="alert-heading">Failed to access Dataset details.</h4>
{{ errorMessage }}
</b-alert>
<DatasetProvider :id="datasetId" v-slot="{ item: dataset, loading: datasetLoading }">
<DatasetProvider :id="datasetId" v-slot="{ result: dataset, loading: datasetLoading }">
<JobDetailsProvider
v-if="!datasetLoading"
:jobid="dataset.creating_job"
@@ -13,7 +13,7 @@ const mockDatasetProvider = {
render() {
return this.$scopedSlots.default({
loading: false,
item: datasetResponse,
result: datasetResponse,
});
},
};
@@ -1,5 +1,5 @@
<template>
<DatasetProvider :id="hda_id" v-slot="{ item: dataset, loading }">
<DatasetProvider :id="hda_id" v-slot="{ result: dataset, loading }">
<div v-if="!loading">
<h3>Dataset Information</h3>
<table id="dataset-details" class="tabletip info_data_table">
@@ -1,7 +1,6 @@
import { mount } from "@vue/test-utils";
import { getLocalVue } from "jest/helpers";
import FormBoolean from "./FormBoolean";
import Vue from "vue";
const localVue = getLocalVue();
@@ -20,12 +19,11 @@ describe("FormBoolean", () => {
it("check initial value and value change", async () => {
const input = wrapper.find("input");
expect(wrapper.vm.currentValue).toBe(false);
wrapper.setProps({ value: true });
await Vue.nextTick();
await wrapper.setProps({ value: true });
expect(wrapper.vm.currentValue).toBe(true);
input.trigger("click");
await input.trigger("click");
expect(input.element.checked).toBe(false);
input.trigger("click");
await input.trigger("click");
expect(input.element.checked).toBe(true);
expect(wrapper.emitted().input[0][0]).toBe(true);
});
@@ -52,9 +52,8 @@ describe("FormInput", () => {
const props = { value: 50, type: "float", min: 10, max: 100 };
const wrapper = await mountFormNumber(props);
const input = await getInput(wrapper);
input.setValue(number);
input.trigger("change");
await flushPromises();
await input.setValue(number);
await input.trigger("change");
const alert = await getAlert(wrapper);
expect(alert.exists()).toBeTruthy();
expect(alert.text().includes(`${number} is out`)).toBeTruthy();
@@ -1,6 +1,5 @@
import { mount } from "@vue/test-utils";
import { getLocalVue } from "jest/helpers";
import Vue from "vue";
import FormElement from "./FormElement";
const localVue = getLocalVue();
@@ -46,19 +45,17 @@ describe("FormElement", () => {
expect(wrapper.find(".ui-form-title-text").text()).toEqual("title_text");
expect(wrapper.findAll("span[title='Disable']").length).toEqual(1);
expect(wrapper.emitted().input[0][0]).toEqual("initial_value");
wrapper.find(".ui-form-collapsible-icon").trigger("click");
await wrapper.find(".ui-form-collapsible-icon").trigger("click");
expect(wrapper.emitted().input[1][0]).toEqual("collapsible_value");
expect(wrapper.emitted().input[1][1]).toEqual("input");
await Vue.nextTick();
await wrapper.setProps({
collapsedEnableText: "Enable Collapsible",
collapsedDisableText: "Disable Collapsible",
});
expect(wrapper.findAll("span[title='Enable Collapsible']").length).toEqual(1);
expect(wrapper.findAll("span[title='Disable Collapsible']").length).toEqual(0);
wrapper.find(".ui-form-collapsible-icon").trigger("click");
await wrapper.find(".ui-form-collapsible-icon").trigger("click");
expect(wrapper.emitted().input[2][0]).toEqual("default_value");
await Vue.nextTick();
expect(wrapper.findAll("span[title='Disable Collapsible']").length).toEqual(1);
expect(wrapper.findAll("span[title='Enable Collapsible']").length).toEqual(0);
});
@@ -0,0 +1,35 @@
<template>
<h6 v-if="collectionType" class="description py-1">
<span>a {{ collectionLabel | localize }}</span>
<span v-if="elementCount == 1">with 1 item</span>
<span v-else-if="elementCount > 1">with {{ elementCount }} items</span>
</h6>
</template>
<script>
export default {
props: {
item: { type: Object, required: true },
},
data() {
return {
labels: {
list: "list",
paired: "dataset pair",
"list:paired": "list of pairs",
},
};
},
computed: {
collectionLabel() {
return this.labels[this.collectionType] || "nested list";
},
collectionType() {
return this.item.collection_type;
},
elementCount() {
return this.item.element_count;
},
},
};
</script>
@@ -0,0 +1,218 @@
<template>
<div
:id="contentId"
:class="['content-item m-1 p-0 rounded', clsStatus]"
draggable
:data-hid="id"
:data-state="state"
@dragstart="onDragStart">
<div class="p-1 cursor-pointer" @click.stop="onClick">
<div class="clearfix overflow-hidden">
<div class="btn-group float-right">
<b-button
v-if="isDataset"
:disabled="displayDisabled"
:title="displayButtonTitle"
class="px-1"
size="sm"
variant="link"
@click.stop="onDisplay">
<icon icon="eye" />
</b-button>
<b-button
v-if="isHistoryItem"
:disabled="editDisabled"
class="px-1"
title="Edit attributes"
size="sm"
variant="link"
@click.stop="onEdit">
<icon icon="pen" />
</b-button>
<b-button
v-if="isHistoryItem && !item.deleted"
class="px-1"
title="Delete"
size="sm"
variant="link"
:disabled="item.purged"
@click.stop="$emit('delete', item)">
<icon icon="trash" />
</b-button>
<b-button
v-if="isHistoryItem && item.deleted"
class="px-1"
title="Undelete"
size="sm"
variant="link"
:disabled="item.purged"
@click.stop="$emit('undelete', item)">
<icon icon="trash-restore" />
</b-button>
<b-button
v-if="isHistoryItem && !item.visible"
class="px-1"
title="Unhide"
size="sm"
variant="link"
@click.stop="$emit('unhide', item)">
<icon icon="unlock" />
</b-button>
</div>
<h5 class="float-left p-1 w-75 font-weight-bold">
<div v-if="selectable" class="selector float-left mr-2">
<span
v-if="selected"
class="fa fa-lg fa-check-square-o"
@click.stop="$emit('update:selected', false)" />
<span v-else class="fa fa-lg fa-square-o" @click.stop="$emit('update:selected', true)" />
</div>
<icon v-if="stateIcon" :icon="stateIcon" />
<span class="id hid">{{ id }}</span>
<span>:</span>
<span class="content-title name">{{ name }}</span>
<CollectionDescription v-if="!isDataset" :item="item" />
<div v-if="!expandDataset && item.tags && item.tags.length > 0" class="nametags">
<Nametag v-for="tag in item.tags" :key="tag" :tag="tag" />
</div>
</h5>
</div>
</div>
<!-- collections are not expandable, so we only need the DatasetDetails component here -->
<div class="detail-animation-wrapper" :class="expandDataset ? '' : 'collapsed'">
<DatasetDetails v-if="expandDataset" @edit="onEdit" :item="item" />
</div>
</div>
</template>
<script>
import { backboneRoute, useGalaxy, iframeRedirect } from "components/plugins/legacyNavigation";
import { Nametag } from "components/Nametags";
import CollectionDescription from "./Collection/CollectionDescription";
import DatasetDetails from "./Dataset/DatasetDetails";
import CONTENTSTATE from "./contentState";
export default {
components: {
CollectionDescription,
DatasetDetails,
Nametag,
},
props: {
expandDataset: { type: Boolean, required: true },
item: { type: Object, required: true },
id: { type: Number, required: true },
isDataset: { type: Boolean, default: true },
isHistoryItem: { type: Boolean, default: true },
name: { type: String, required: true },
selected: { type: Boolean, default: false },
selectable: { type: Boolean, default: false },
},
computed: {
contentId() {
return `dataset-${this.item.id}`;
},
state() {
if (this.item.job_state_summary) {
for (const key of ["error", "failed", "paused", "upload", "running"]) {
if (this.item.job_state_summary[key] > 0) {
return key;
}
}
return "ok";
} else {
return this.item.state;
}
},
clsStatus() {
const status = CONTENTSTATE[this.state] && CONTENTSTATE[this.state].status;
if (this.selected) {
return "alert-info";
} else if (!status) {
return `alert-success`;
} else {
return `alert-${status}`;
}
},
displayButtonTitle() {
if (this.item.purged) {
return "Cannot display datasets removed from disk.";
}
if (this.displayDisabled) {
return "This dataset is not yet viewable.";
}
return "Display";
},
displayDisabled() {
return this.item.purged || ["discarded", "new", "upload"].includes(this.state);
},
editButtonTitle() {
if (this.item.purged) {
return "Cannot edit attributes of datasets removed from disk.";
}
if (this.editDisabled) {
return "This dataset is not yet editable.";
}
return "Edit Attributes";
},
editDisabled() {
return (
this.item.purged || ["discarded", "new", "upload", "queued", "running", "waiting"].includes(this.state)
);
},
stateIcon() {
return CONTENTSTATE[this.state] && CONTENTSTATE[this.state].icon;
},
},
methods: {
onDragStart(evt) {
evt.dataTransfer.dropEffect = "move";
evt.dataTransfer.effectAllowed = "move";
evt.dataTransfer.setData("text", JSON.stringify([this.item]));
},
onDisplay() {
const id = this.item.id;
useGalaxy((Galaxy) => {
if (Galaxy.frame && Galaxy.frame.active) {
Galaxy.frame.addDataset(id);
} else {
iframeRedirect(`datasets/${id}/display/?preview=True`);
}
});
},
onEdit() {
if (this.item.collection_type) {
backboneRoute(`collection/edit/${this.item.id}`);
} else {
backboneRoute("datasets/edit", { dataset_id: this.item.id });
}
},
onClick() {
if (this.isDataset) {
this.$emit("update:expand-dataset", !this.expandDataset);
} else {
this.$emit("view-collection", this.item);
}
},
},
};
</script>
<style>
.content-item:hover {
filter: brightness(105%);
}
.content-item {
.name {
word-wrap: break-word;
}
}
.detail-animation-wrapper {
overflow: hidden;
transition: max-height 0.5s ease-out;
height: auto;
max-height: 400px;
}
.detail-animation-wrapper.collapsed {
max-height: 0;
}
</style>
@@ -0,0 +1,159 @@
<template>
<div class="dataset-actions mb-1">
<div class="clearfix">
<div class="btn-group float-left">
<b-button v-if="showError" class="px-1" title="Error" size="sm" variant="link" @click.stop="onError">
<span class="fa fa-bug" />
</b-button>
<b-button
v-if="showDownloads"
class="download-btn px-1"
title="Download"
size="sm"
variant="link"
@click.stop="onDownload">
<span class="fa fa-save" />
</b-button>
<b-button
v-if="showDownloads"
class="px-1"
title="Copy Link"
size="sm"
variant="link"
@click.stop="onCopyLink">
<span class="fa fa-link" />
</b-button>
<b-button
v-if="showInfo"
class="params-btn px-1"
title="Dataset Details"
size="sm"
variant="link"
@click.stop="onInfo">
<span class="fa fa-info-circle" />
</b-button>
<b-button
v-if="showRerun"
class="px-1"
title="Run Job Again"
size="sm"
variant="link"
@click.stop="onRerun">
<span class="fa fa-redo" />
</b-button>
<b-button
v-if="showVisualizations"
class="px-1"
title="Visualize"
size="sm"
variant="link"
@click.stop="onVisualize">
<span class="fa fa-bar-chart-o" />
</b-button>
<b-button v-if="showRerun" class="px-1" title="Help" size="sm" variant="link" @click.stop="onRerun">
<span class="fa fa-question" />
</b-button>
</div>
<div class="btn-group float-right">
<b-button
class="px-1"
title="Edit Tags"
size="sm"
variant="link"
:pressed="showTags"
@click="onToggleTags">
<span class="fa fa-tags" />
</b-button>
</div>
</div>
<div v-if="showTags" class="mb-2">
<StatelessTags class="tags" v-model="tags" @input="onTags" />
</div>
</div>
</template>
<script>
import { StatelessTags } from "components/Tags";
import { legacyNavigationMixin } from "components/plugins/legacyNavigation";
import { prependPath } from "utils/redirect";
import { copy as sendToClipboard } from "utils/clipboard";
import { absPath } from "utils/redirect";
import { updateContentFields } from "components/History/model/queries";
export default {
mixins: [legacyNavigationMixin],
components: {
StatelessTags,
},
props: {
item: { type: Object, required: true },
},
data() {
return {
showTags: false,
tags: this.item.tags,
};
},
computed: {
downloadUrl() {
return prependPath(`datasets/${this.item.id}/display?to_ext=${this.item.extension}`);
},
showDownloads() {
return !this.item.purged && ["ok", "failed_metadata", "error"].includes(this.item.state);
},
showError() {
return this.item.state == "error";
},
showInfo() {
return this.item.state != "noPermission";
},
showRerun() {
return (
this.item.rerunnable &&
this.item.creating_job &&
this.item.state != "upload" &&
this.item.state != "noPermission"
);
},
showVisualizations() {
// TODO: Check hasViz, if visualizations are activated in the config
return !this.item.purged && ["ok", "failed_metadata", "error"].includes(this.item.state);
},
},
methods: {
onCopyLink() {
const msg = this.localize("Link is copied to your clipboard");
sendToClipboard(absPath(this.downloadUrl), msg);
},
onDownload() {
window.location.href = this.downloadUrl;
},
onError() {
this.backboneRoute("datasets/error", { dataset_id: this.item.id });
},
onInfo() {
this.backboneRoute(`datasets/${this.item.id}/details`);
},
onRerun() {
this.backboneRoute(`root?job_id=${this.item.creating_job}`);
},
onTags() {
updateContentFields(this.item, { tags: this.tags });
},
onToggleTags() {
this.showTags = !this.showTags;
},
onVisualize() {
const path = `visualizations?dataset_id=${this.item.id}`;
const redirectParams = {
path: path,
title: "Dataset details",
tryIframe: false,
};
if (!this.iframeAdd(redirectParams)) {
this.backboneRoute(path);
}
},
},
};
</script>
@@ -0,0 +1,52 @@
<template>
<UrlDataProvider :url="dataUrl" auto-refresh v-slot="{ loading, result }">
<div v-if="!loading" class="dataset">
<div class="p-2 details">
<div class="summary">
<div v-if="result.misc_blurb" class="blurb">
<span class="value">{{ result.misc_blurb }}</span>
</div>
<span v-if="result.file_ext" class="datatype">
<label class="prompt" v-localize>format</label>
<span class="value">{{ result.file_ext }}</span>
</span>
<span v-if="result.genome_build" class="dbkey">
<label class="prompt" v-localize>database</label>
<b-link class="value" @click.stop="$emit('edit', dataset)" data-label="Database/Build">{{
result.genome_build
}}</b-link>
</span>
<div v-if="result.misc_info" class="info">
<span class="value">{{ result.misc_info }}</span>
</div>
</div>
<DatasetActions :item="result" />
<pre v-if="result.peek" class="dataset-peek p-1" v-html="result.peek" />
</div>
</div>
</UrlDataProvider>
</template>
<script>
import { UrlDataProvider } from "components/providers/UrlDataProvider";
import DatasetActions from "./DatasetActions";
export default {
components: {
DatasetActions,
UrlDataProvider,
},
props: {
item: { type: Object, required: true },
},
computed: {
dataset() {
return this.item.object || this.item;
},
dataUrl() {
const datasetId = this.dataset.id;
return `api/datasets/${datasetId}`;
},
},
};
</script>
@@ -0,0 +1,37 @@
<template>
<div>
<ContentItem
:item="item.object"
:id="item.element_index"
:name="item.element_identifier"
:is-dataset="item.element_type == 'hda'"
:expand-dataset="expandDataset"
:is-history-item="false"
@update:expand-dataset="expandDataset = $event"
@view-collection="viewCollection = !viewCollection" />
<GenericItem v-if="viewCollection" :itemId="item.object.id" :itemSrc="item.object.history_content_type" />
</div>
</template>
<script>
import ContentItem from "./ContentItem";
export default {
components: {
ContentItem,
GenericItem: () => import("components/History/Content/GenericItem"),
},
props: {
item: {
type: Object,
required: true,
},
},
data() {
return {
viewCollection: false,
expandDataset: false,
};
},
};
</script>
@@ -0,0 +1,76 @@
<template>
<component :is="providerComponent" :id="itemId" auto-refresh v-slot="{ result: item, loading }">
<loading-span v-if="loading" message="Loading dataset" />
<div v-else>
<ContentItem
:item="item"
:id="item.hid"
:name="item.name"
:expand-dataset="expandDataset"
:is-dataset="item.history_content_type == 'dataset'"
@update:expand-dataset="expandDataset = $event"
@view-collection="viewCollection = !viewCollection"
@delete="onDelete"
@undelete="onUndelete"
@unhide="onUnhide" />
<div v-if="viewCollection">
<div v-for="(collectionItem, collectionIndex) in item.elements" :key="collectionIndex">
<GenericElement :item="collectionItem" />
</div>
</div>
</div>
</component>
</template>
<script>
import LoadingSpan from "components/LoadingSpan";
import { DatasetProvider, DatasetCollectionProvider } from "components/providers";
import { deleteContent, updateContentFields } from "components/History/model/queries";
import ContentItem from "./ContentItem";
import GenericElement from "./GenericElement";
export default {
components: {
ContentItem,
GenericElement,
DatasetProvider,
DatasetCollectionProvider,
LoadingSpan,
},
props: {
itemId: {
type: String,
required: true,
},
itemSrc: {
type: String,
required: true,
},
},
data() {
return {
viewCollection: false,
expandDataset: false,
};
},
computed: {
providerComponent() {
return this.itemSrc == "hda" ? "DatasetProvider" : "DatasetCollectionProvider";
},
},
methods: {
onDelete(item) {
deleteContent(item);
},
onUndelete(item) {
updateContentFields(item, { deleted: false });
},
onHide(item) {
updateContentFields(item, { visible: false });
},
onUnhide(item) {
updateContentFields(item, { visible: true });
},
},
};
</script>
@@ -1,5 +1,5 @@
/**
* This is simple list management for the monent, but if I remember right we have plans for a more
* This is simple list management for the moment, but if I remember right we have plans for a more
* abstracted selection scheme in the future for purposes of handling large bulk operations on the
* server, this is a good place to implement that logic.
*/
@@ -28,10 +28,10 @@ export default {
const key = this.getItemKey(item);
return this.items.has(key);
},
setSelected(item, bSelected) {
setSelected(item, selected) {
const key = this.getItemKey(item);
const newSelected = new Map(this.items);
bSelected ? newSelected.set(key, item) : newSelected.delete(key);
selected ? newSelected.set(key, item) : newSelected.delete(key);
this.items = newSelected;
},
selectItems(items = []) {
@@ -42,14 +42,6 @@ export default {
});
this.items = new Map(newEntries);
},
// unselectItems(items = []) {
// const doomedKeys = items.map((item) => this.getItemKey(item));
// const existing = this.items.entries();
// const newEntries = Array.from(existing).filter(([key]) => {
// return !doomedKeys.includes(key);
// });
// this.items = new Map(newEntries);
// },
reset() {
this.items = new Map();
},
@@ -77,7 +69,6 @@ export default {
showSelection: this.showSelection,
setShowSelection: this.setShowSelection,
selectItems: this.selectItems,
// unselectItems: this.unselectItems,
isSelected: this.isSelected,
setSelected: this.setSelected,
resetSelection: this.reset,
@@ -0,0 +1,67 @@
/*
Client representation of state and state messages. See: https://github.com/galaxyproject/galaxy/blob/dev/lib/galaxy/model/__init__.py#L3292
for a list of available states.
*/
export default {
/** deleted while uploading */
discarded: {
status: "danger",
text: "The job creating this dataset was cancelled before completion.",
icon: "exclamation-triangle",
},
/** has no data */
empty: {
status: "success",
text: "No data",
},
/** the tool producing this dataset failed */
error: {
status: "danger",
text: "An error occurred with this dataset",
icon: "exclamation-triangle",
},
/** metadata discovery/setting failed or errored (but otherwise ok) */
failed_metadata: {
status: "danger",
},
/** was created without a tool */
new: {
status: "warning",
text: "This is a new dataset and not all of its data are available yet.",
icon: "clock",
},
/** has successfully completed running */
ok: {
status: "success",
},
/** the job that will produce the dataset paused */
paused: {
status: "info",
text: "This job is paused. Use the 'Resume Paused Jobs' in the history menu to resume.",
icon: "pause",
},
/** the job that will produce the dataset queued in the runner */
queued: {
status: "warning",
text: "This job is waiting to run.",
icon: "clock",
},
/** the job that will produce the dataset is running */
running: {
status: "warning",
text: "This job is currently running.",
icon: "clock",
},
/** metadata for the dataset is being discovered/set */
setting_metadata: {
status: "warning",
text: "Metadata is being auto-detected.",
icon: "clock",
},
/** is uploading and not ready */
upload: {
status: "warning",
text: "This dataset is currently uploading.",
icon: "clock",
},
};
@@ -1,19 +0,0 @@
import ContentItem from "./ContentItem";
export default {
mixins: [ContentItem],
computed: {
contentItemComponent() {
const { history_content_type } = this.item;
switch (history_content_type) {
case "dataset":
return "Subdataset";
case "dataset_collection":
return "Subcollection";
default:
return "Placeholder";
}
},
},
};
@@ -1,83 +0,0 @@
/**
* This is a single item in the list. The main job of the contentItem is to
* pick a specific selector for the :is attribute of the generic component
* placeholder. This should be a dataset, collection, or other component
* depending on the specific list implementation.
*
* The mixin also pulls out "selected", and "expanded" properties which are
* derived from the current component state rather than the data itself.
*/
import Placeholder from "./Placeholder";
import Dataset from "./Dataset";
import DatasetCollection from "./DatasetCollection";
import Subdataset from "./Subdataset";
import Subcollection from "./Subcollection";
export default {
template: `
<component :is="contentItemComponent"
:data-ci-type="contentItemComponent"
class="content-item"
:class="{ loading }"
:tabindex="index"
:writable="writable"
v-on="$listeners"
v-bind="$attrs"
:item="item"
:index="index"
:row-key="rowKey"
:writable="writable"
@mouseover.native.stop="setFocus(index)"
@keydown.native.arrow-up.self.stop="setFocus(index - 1)"
@keydown.native.arrow-down.self.stop="setFocus(index + 1)"
/>
`,
components: {
Placeholder,
Dataset,
DatasetCollection,
Subdataset,
Subcollection,
},
props: {
item: { type: Object, required: true },
index: { type: Number, required: false, default: null },
rowKey: { type: [Number, String], required: false, default: "" },
writable: { type: Boolean, required: false, default: true },
},
data: () => ({
suppressFocus: false,
}),
methods: {
setFocus(index) {
if (this.suppressFocus) {
return;
}
const ul = this.$el.closest(".scroller");
const el = ul.querySelector(`[tabindex="${index}"]`);
if (el) {
el.focus();
}
},
},
computed: {
loading() {
return !this.item;
},
contentItemComponent() {
// override me
return "Placeholder";
},
},
created() {
this.$root.$on("bv::dropdown::show", () => (this.suppressFocus = true));
this.$root.$on("bv::dropdown::hide", () => (this.suppressFocus = false));
},
};
@@ -1,56 +0,0 @@
<template>
<DatasetUI
v-if="dataset"
v-bind="$attrs"
v-on="$listeners"
:dataset="dataset"
@update:dataset="onUpdate"
@delete="onDelete"
@undelete="onUndelete"
@unhide="onUnhide"
@copy-link="onCopyLink" />
</template>
<script>
import DatasetUI from "./DatasetUI";
import { Dataset } from "../../model";
import { deleteContent, updateContentFields } from "../../model/queries";
import { cacheContent } from "components/providers/History/caching";
import { copy as sendToClipboard } from "utils/clipboard";
import { absPath } from "utils/redirect";
export default {
components: {
DatasetUI,
},
props: {
item: { type: Object, required: true },
},
computed: {
dataset() {
return new Dataset(this.item);
},
},
methods: {
async onDelete(opts = {}) {
const ajaxResult = await deleteContent(this.item, opts);
await cacheContent(ajaxResult);
},
async onUnhide() {
await this.onUpdate({ visible: true });
},
async onUndelete() {
await this.onUpdate({ deleted: false });
},
async onUpdate(changes) {
const newContent = await updateContentFields(this.item, changes);
await cacheContent(newContent);
},
onCopyLink() {
const relPath = this.dataset.getUrl("download");
const msg = this.localize("Link is copied to your clipboard");
sendToClipboard(absPath(relPath), msg);
},
},
};
</script>
@@ -1,294 +0,0 @@
<template>
<div>
<b-button-group>
<IconButton
v-if="notIn(STATES.NOT_VIEWABLE, STATES.DISCARDED)"
icon="eye"
:title="displayButtonTitle"
:disabled="dataset.purged || isIn(STATES.UPLOAD, STATES.NEW)"
@click.stop="viewData"
variant="link"
class="px-1 display-btn" />
<IconButton
v-if="writable && notIn(STATES.DISCARDED)"
icon="pen"
:title="editButtonTitle"
:disabled="dataset.deleted || isIn(STATES.UPLOAD, STATES.NEW)"
@click.stop="$emit('edit')"
variant="link"
class="px-1 edit-btn" />
<IconButton
v-if="writable && dataset.accessible"
:icon="dataset.deleted ? 'trash-restore' : 'trash'"
:title="deleteButtonTitle"
:disabled="dataset.purged"
v-b-modal="bsId('delete-modal')"
variant="link"
class="px-1 delete-btn"
@click.stop />
<b-dropdown
right
size="sm"
variant="link"
:text="'Dataset Operations' | l"
toggle-class="p-1 pl-2"
class="flex-grow-0 dataset-operations-dropdown"
v-if="expanded"
boundary="window">
<template v-slot:button-content>
<Icon icon="ellipsis-v" variant="link" />
<span class="sr-only">Dataset Operations</span>
</template>
<b-dropdown-item
v-if="isIn(STATES.ERROR)"
key="show-error"
title="Error"
@click.stop="
backboneRoute('datasets/error', {
dataset_id: dataset.id,
})
">
<Icon icon="exclamation-triangle" class="mr-1" />
<span v-localize>Show Error</span>
</b-dropdown-item>
<b-dropdown-item
v-if="showDownloads && dataset.getUrl('download')"
title="Copy Link"
@click.stop="$emit('copy-link')">
<Icon icon="link" class="mr-1" />
<span v-localize>Copy Link</span>
</b-dropdown-item>
<b-dropdown-group v-if="showDownloads && dataset.hasMetaData">
<b-dropdown-item
v-for="mf in dataset.meta_files"
:key="'download-' + mf.download_url"
:title="'Download ' + mf.file_type"
:href="prependPath(dataset.getUrl('meta_download') + mf.file_type)"
target="_blank"
download>
<Icon icon="download" class="mr-1" />
<span v-localize>{{ "Download " + mf.file_type }}</span>
</b-dropdown-item>
</b-dropdown-group>
<b-dropdown-item
v-else-if="showDownloads"
title="Download"
:href="prependPath(dataset.getUrl('download'))"
target="_blank"
class="download-btn"
download>
<Icon icon="download" class="mr-1" />
<span v-localize>Download</span>
</b-dropdown-item>
<b-dropdown-item
v-if="dataset.rerunnable && dataset.creating_job && notIn(STATES.UPLOAD, STATES.NOT_VIEWABLE)"
title="Run job again"
:href="prependPath(dataset.getUrl('rerun'))"
@click.stop.prevent="
backboneRoute('/', {
job_id: dataset.creating_job,
})
">
<Icon icon="play" class="mr-1" />
<span v-localize>Run job again</span>
</b-dropdown-item>
<b-dropdown-item
v-if="showViz && hasViz && isIn(STATES.OK, STATES.FAILED_METADATA)"
title="Visualize Data"
@click.stop.prevent="visualize">
<Icon icon="chart-area" class="mr-1" />
<span v-localize>Visualize Data</span>
</b-dropdown-item>
<b-dropdown-item
v-if="currentUser && currentUser.id && dataset.creating_job"
title="Tool Help"
@click.stop="showToolHelp(dataset.creating_job)">
<Icon icon="question" class="mr-1" />
<span v-localize>Tool Help</span>
</b-dropdown-item>
<b-dropdown-item
v-if="notIn(STATES.NOT_VIEWABLE)"
key="dataset-details"
title="View Dataset Details"
class="params-btn"
@click.stop.prevent="showDetails">
<Icon icon="info-circle" class="mr-1" />
<span v-localize>View Dataset Details</span>
</b-dropdown-item>
</b-dropdown>
</b-button-group>
<!-- modals -->
<b-modal :id="bsId('delete-modal')" title="Delete Dataset?" title-tag="h2" @ok="onDeleteClick">
<p v-localize>Really delete this dataset?</p>
</b-modal>
</div>
</template>
<script>
import { mapGetters } from "vuex";
import { Dataset } from "../../model";
import { legacyNavigationMixin } from "components/plugins/legacyNavigation";
import IconButton from "components/IconButton";
export default {
mixins: [legacyNavigationMixin],
inject: ["STATES"],
components: {
IconButton,
},
props: {
dataset: { type: Dataset, required: true },
expanded: { type: Boolean, required: true },
writable: { type: Boolean, default: true },
},
data() {
return {
toolHelp: null,
testVal: false,
};
},
computed: {
...mapGetters("user", ["currentUser"]),
...mapGetters("config", ["config"]),
showViz() {
return this.config.visualizations_visible;
},
displayButtonTitle() {
if (this.dataset.purged) {
return "Cannot display datasets removed from disk";
}
if (this.dataset.state == this.STATES.UPLOAD) {
return "This dataset must finish uploading before it can be viewed";
}
if (this.dataset.state == this.STATES.NEW) {
return "This dataset is not yet viewable";
}
return "View data";
},
editButtonTitle() {
if (this.dataset.deleted) {
return "Undelete dataset to edit attributes";
}
if (this.dataset.purged) {
return "Cannot edit attributes of datasets removed from disk";
}
const unreadyStates = new Set([this.STATES.UPLOAD, this.STATES.NEW]);
if (unreadyStates.has(this.dataset.state)) {
return "This dataset is not yet editable";
}
return "Edit attributes";
},
deleteButtonTitle() {
return this.dataset.purged
? "Dataset has been permanently deleted"
: this.dataset.deleted
? "Undelete"
: "Delete";
},
showDownloads() {
if (this.dataset.purged) {
return false;
}
if (!this.dataset.hasData) {
return false;
}
const okStates = new Set([this.STATES.OK, this.STATES.FAILED_METADATA, this.STATES.ERROR]);
return okStates.has(this.dataset.state);
},
hasViz() {
const viz_count = this.dataset.viz_count || 0;
return viz_count && this.dataset.hasData && !this.dataset.deleted;
},
},
methods: {
notIn(...states) {
const badStates = new Set(states);
return !badStates.has(this.dataset.state);
},
isIn(...states) {
const goodStates = new Set(states);
return goodStates.has(this.dataset.state);
},
viewData() {
const id = this.dataset.id;
if (!id) {
return;
}
this.useGalaxy((Galaxy) => {
if (Galaxy.frame && Galaxy.frame.active) {
Galaxy.frame.addDataset(id);
} else {
const path = this.dataset.getUrl("display");
if (path) {
this.iframeRedirect(path);
}
}
});
},
// wierd iframe navigation
visualize() {
const showDetailsUrl = `/datasets/${this.dataset.id}/details`;
const redirectParams = {
path: showDetailsUrl,
title: "Dataset details",
tryIframe: false,
};
if (!this.iframeAdd(redirectParams)) {
this.backboneRoute("visualizations", {
dataset_id: this.dataset.id,
});
}
},
showDetails() {
const showDetailsUrl = `/datasets/${this.dataset.id}/details`;
const redirectParams = {
path: showDetailsUrl,
title: "Dataset details",
tryIframe: false,
};
if (!this.iframeAdd(redirectParams)) {
this.backboneRoute(showDetailsUrl);
}
},
showToolHelp(job_id) {
this.eventHub.$emit("toggleToolHelp", job_id);
},
onDeleteClick() {
const eventName = this.dataset.deleted ? "undelete" : "delete";
this.$emit(eventName);
},
// bootstrap demands ids instead of simply using a ref so I guess we're doing this
bsId(suffix) {
return `dataset-${this.dataset.id}-${suffix}`;
},
},
};
</script>
@@ -1,249 +0,0 @@
<!--
The guts and markup of the dataset item separated from the
data assignment so we can re-use it on the collection listing
DO NOT put data retrieval into this component, access the dataset
either through the props, and make updates through the events -->
<template>
<div
class="dataset history-content"
:id="typeId"
:class="{ expanded, collapsed, selected }"
:data-state="dataset.state"
@keydown.arrow-left.self.stop="$emit('update:expanded', false)"
@keydown.arrow-right.self.stop="$emit('update:expanded', true)"
@keydown.space.self.stop.prevent="$emit('update:selected', !selected)">
<!-- name, state buttons, menus -->
<nav class="content-top-menu p-1 d-flex cursor-pointer" @click.stop="$emit('update:expanded', !expanded)">
<div class="d-flex flex-grow-1 overflow-hidden">
<div class="pl-1" v-if="showSelectionBox">
<b-check class="selector" :checked="selected" @change="$emit('update:selected', $event)"></b-check>
</div>
<StatusIcon
v-if="!collapsed"
class="status-icon px-1"
:state="dataset.state"
@click.stop="onStatusClick" />
<IconButton
v-if="!collapsed && !dataset.visible"
class="px-1"
state="hidden"
title="Unhide"
icon="eye-slash"
@click.stop="$emit('unhide')" />
<IconButton
v-if="!collapsed && dataset.isDeleted && !dataset.purged"
class="px-1"
state="deleted"
title="Undelete"
icon="trash-restore"
@click.stop="$emit('undelete')" />
<div class="content-title title p-1 overflow-hidden">
<h5 class="text-truncate" v-if="collapsed">
<span class="hid" data-description="dataset hid">{{ dataset.hid }}</span>
<span class="name" data-description="dataset name">{{ dataset.title }}</span>
</h5>
</div>
</div>
<div class="d-flex">
<slot name="menu">
<DatasetMenu
:dataset="dataset"
:expanded="expanded"
:writable="writable"
@edit="edit"
v-on="$listeners" />
</slot>
</div>
</nav>
<!--- read-only tags with name: prefix -->
<div v-if="collapsed && dataset.nameTags.length" class="nametags px-2 pb-2">
<Nametag v-for="tag in dataset.nameTags" :key="tag" :tag="tag" />
</div>
<div v-if="expanded" class="p-3 details">
<div class="d-flex">
<div class="flex-grow-1">
<h4 data-description="dataset name">{{ name || "(Dataset Name)" }}</h4>
<template v-if="!editing">
<p v-if="annotation">{{ annotation }}</p>
<div v-if="dataset.nameTags.length" class="nametags mt-2">
<Nametag v-for="tag in dataset.nameTags" :key="tag" :tag="tag" />
</div>
</template>
<div v-else class="my-3">
<b-textarea
class="mb-3"
v-model="name"
placeholder="Dataset Name"
trim
max-rows="4"></b-textarea>
<b-textarea
class="mb-3"
v-model="annotation"
placeholder="Annotation (optional)"
trim
max-rows="4"></b-textarea>
<StatelessTags class="mt-2" v-model="tags" />
</div>
</div>
<EditorMenu
class="ml-3 flex-grow-0 d-flex flex-column"
v-if="writable"
model-name="Dataset"
:editing.sync="editing"
:writable="writable"
:valid="valid"
:dirty="dirty"
@save="save"
@revert="revert" />
</div>
<div class="details">
<DatasetSummary :dataset="dataset" class="summary" @edit="edit" />
<div class="display-applications" v-if="dataset.displayLinks.length">
<div class="display-application" v-for="app in dataset.displayLinks" :key="app.label">
<span class="display-application-location">
{{ app.label }}
</span>
<span class="display-application-links">
<a v-for="l in app.links" :key="l.href" :href="l.href" :target="l.target">
{{ l.text }}
</a>
</span>
</div>
</div>
<pre v-if="dataset.peek" class="dataset-peek p-1" v-html="dataset.peek"></pre>
</div>
</div>
</div>
</template>
<script>
import { Dataset, STATES } from "../../model";
import { Nametag } from "components/Nametags";
import StatusIcon from "../../StatusIcon";
import DatasetMenu from "./DatasetMenu";
import DatasetSummary from "./Summary";
import { legacyNavigationMixin } from "components/plugins/legacyNavigation";
import IconButton from "components/IconButton";
import EditorMenu from "../../EditorMenu";
import { StatelessTags } from "components/Tags";
export default {
mixins: [legacyNavigationMixin],
components: {
StatusIcon,
DatasetMenu,
DatasetSummary,
Nametag,
IconButton,
EditorMenu,
StatelessTags,
},
props: {
dataset: { type: Dataset, required: true },
expanded: { type: Boolean, required: true },
selected: { type: Boolean, required: false, default: false },
showSelection: { type: Boolean, required: false, default: false },
writable: { type: Boolean, required: false, default: true },
selectable: { type: Boolean, required: false, default: true },
},
data() {
return {
editing: false,
tempContent: this.dataset.clone(),
};
},
computed: {
collapsed() {
return !this.expanded;
},
typeId() {
return this.dataset.type_id;
},
isEditing() {
return this.writable && this.editing;
},
valid() {
return true;
},
dirty() {
return !this.tempContent.equals(this.dataset);
},
name: {
get() {
return this.tempContent?.name || "";
},
set(newVal) {
this.patchTempContent({ name: newVal });
},
},
annotation: {
get() {
return this.tempContent?.annotation || "";
},
set(newVal) {
this.patchTempContent({ annotation: newVal });
},
},
tags: {
get() {
return this.tempContent?.tags || [];
},
set(newTags) {
if (Array.isArray(newTags)) {
this.patchTempContent({ tags: newTags });
}
},
},
showSelectionBox() {
return this.selectable && this.showSelection;
},
},
methods: {
onStatusClick(evt) {
switch (this.dataset.state) {
case STATES.ERROR:
this.backboneRoute("datasets/error", { dataset_id: this.dataset.id });
break;
default:
console.log("unhandled status icon click", this.dataset.state);
}
},
patchTempContent(props = {}) {
this.tempContent = this.tempContent.patch(props);
},
save() {
if (this.valid) {
this.$emit("update:dataset", this.tempContent);
this.editing = false;
}
},
reset() {
this.tempContent = this.dataset.clone();
},
revert() {
if (this.dirty) {
this.reset();
}
},
edit() {
this.backboneRoute("datasets/edit", { dataset_id: this.dataset.id });
},
},
};
</script>
@@ -1,5 +0,0 @@
<template functional>
<div>
<p v-localize>The job creating this dataset was cancelled before completion.</p>
</div>
</template>
@@ -1,5 +0,0 @@
<template functional>
<div v-if="props.dataset">
<span v-localize>No data</span>: <i>{{ props.dataset.misc_blurb }}</i>
</div>
</template>
@@ -1,11 +0,0 @@
<template functional>
<div v-if="props.dataset">
<div v-if="!props.dataset.purged && props.dataset.misc_info">
<span>{{ props.dataset.misc_info }}</span>
</div>
<span class="help-text" v-localize>An error occurred with this dataset</span>
<div v-if="props.dataset.misc_info" class="job-error-text">
<span>{{ props.dataset.misc_info }}</span>
</div>
</div>
</template>
@@ -1,5 +0,0 @@
<template functional>
<div>
<p v-localize>This is a new dataset and not all of its data are available yet.</p>
</div>
</template>
@@ -1,5 +0,0 @@
<template functional>
<div>
<p v-localize>You do not have permission to view this dataset.</p>
</div>
</template>
@@ -1,18 +0,0 @@
<template functional>
<div v-if="props.dataset">
<div v-if="props.dataset.misc_blurb" class="blurb">
<span class="value">{{ props.dataset.misc_blurb }}</span>
</div>
<div v-if="props.dataset.file_ext" class="datatype">
<label class="prompt" v-localize>format</label>
<span class="value">{{ props.dataset.file_ext }}</span>
</div>
<div v-if="props.dataset.genome_build" class="dbkey">
<label class="prompt" v-localize>database</label>
<b-link class="value" @click.stop="listeners['edit']()">{{ props.dataset.genome_build }}</b-link>
</div>
<div v-if="props.dataset.misc_info" class="info">
<span class="value">{{ props.dataset.misc_info }}</span>
</div>
</div>
</template>
@@ -1,8 +0,0 @@
<template functional>
<div v-if="dataset">
<p v-localize>This job is paused. Use the "Resume Paused Jobs" in the history menu to resume.</p>
<div v-if="dataset.misc_info">
<span>{{ props.dataset.misc_info }}</span>
</div>
</div>
</template>
@@ -1,5 +0,0 @@
<template functional>
<div>
<p v-localize>This job is waiting to run.</p>
</div>
</template>
@@ -1,5 +0,0 @@
<template functional>
<div>
<p v-localize>This job is currently running.</p>
</div>
</template>
@@ -1,5 +0,0 @@
<template functional>
<div>
<p v-localize>Metadata is being auto-detected.</p>
</div>
</template>
@@ -1,3 +0,0 @@
<template functional>
<div v-if="props.dataset">Error: unknown dataset state "{{ props.dataset.state }}"</div>
</template>
@@ -1,5 +0,0 @@
<template functional>
<div>
<p v-localize>This dataset is currently uploading.</p>
</div>
</template>
@@ -1,53 +0,0 @@
<template>
<component :is="summaryComponent" :dataset="dataset" @edit="edit" />
</template>
<script>
import { capitalize, camelize } from "underscore.string";
import Discarded from "./Discarded";
import Empty from "./Empty";
import Error from "./Error";
import New from "./New";
import NotViewable from "./NotViewable";
import Ok from "./Ok";
import Paused from "./Paused";
import Queued from "./Queued";
import Running from "./Running";
import SettingMetadata from "./SettingMetadata";
import Upload from "./Upload";
export default {
inject: ["STATES"],
components: {
Discarded,
Empty,
Error,
New,
NotViewable,
Ok,
Paused,
Queued,
Running,
SettingMetadata,
Upload,
},
props: {
dataset: { type: Object, required: true },
},
computed: {
summaryComponent() {
let state = this.dataset.state;
if (state == this.STATES.FAILED_METADATA) {
state = this.STATES.OK;
}
return capitalize(camelize(state));
},
},
methods: {
edit() {
this.$emit("edit");
},
},
};
</script>
@@ -1,5 +0,0 @@
import Dataset from "./Dataset";
export { default as DatasetUI } from "./DatasetUI";
export { default as DatasetMenu } from "./DatasetMenu";
export default Dataset;
@@ -1,56 +0,0 @@
<template>
<DscUI
v-if="dsc"
v-bind="$attrs"
v-on="$listeners"
class="dataset-collection history-content"
:dsc="dsc"
@update:dsc="onUpdate"
@delete="onDelete"
@undelete="onUndelete"
@unhide="onUnhide" />
</template>
<script>
import DscUI from "./DscUI";
import { DatasetCollection } from "../../model";
import { deleteDatasetCollection, updateContentFields } from "../../model/queries";
import { cacheContent } from "components/providers/History/caching";
export default {
components: {
DscUI,
},
props: {
item: { type: Object, required: true },
},
computed: {
dsc() {
return new DatasetCollection(this.item);
},
},
methods: {
async onDelete(flags = {}) {
const { recursive = false, purge = false } = flags;
const collection = this.item;
const result = await deleteDatasetCollection(collection, recursive, purge);
if (result.deleted) {
const newFields = Object.assign(collection, {
isDeleted: result.deleted,
});
await cacheContent(newFields);
}
},
async onUnhide() {
await this.onUpdate({ visible: true });
},
async onUndelete() {
await this.onUpdate({ deleted: false });
},
async onUpdate(changes) {
const newContent = await updateContentFields(this.item, changes);
await cacheContent(newContent);
},
},
};
</script>
@@ -1,26 +0,0 @@
<template>
<div class="description">
{{ dsc.collectionType | localize }} of {{ dsc.totalElements }}
<b v-if="isHomogeneous">{{ homogeneousDatatype }} </b>datasets
</div>
</template>
<script>
import { DatasetCollection } from "../../model/DatasetCollection";
export default {
props: {
dsc: { type: DatasetCollection, required: true },
},
computed: {
/** @return {Boolean} */
isHomogeneous() {
return this.dsc.isHomogeneous;
},
/** @return {String} */
homogeneousDatatype() {
return this.dsc.homogeneousDatatype;
},
},
};
</script>
@@ -1,108 +0,0 @@
<template>
<div class="collection-menu">
<b-button-group>
<IconButton
icon="info-circle"
v-if="hasJob"
key="dataset-details"
title="View Dataset Collection Details"
@click.stop.prevent="backboneRoute(collectionDetailsPath)"
variant="link"
class="px-1">
</IconButton>
<IconButton
v-if="notIn(STATES.DISCARDED)"
icon="pen"
:title="editButtonTitle"
:disabled="dsc.deleted || isIn(STATES.UPLOAD, STATES.NEW)"
@click.stop="backboneRoute('collection/edit/' + dsc.hdca_id)"
variant="link"
class="px-1 dsc-edit-view" />
<b-dropdown v-if="notIn(STATES.DISCARDED)" no-caret right variant="link" size="sm" boundary="window">
<template v-slot:button-content>
<Icon icon="trash" variant="link" />
<span class="sr-only">Delete Collection</span>
</template>
<b-dropdown-item @click.stop="$emit('delete')">
<span v-localize>Delete Collection Only</span>
</b-dropdown-item>
<b-dropdown-item @click.stop="$emit('delete', { recursive: true })">
<span v-localize>Delete Contained Datasets</span>
</b-dropdown-item>
<b-dropdown-item @click.stop="$emit('delete', { recursive: true, purge: true })">
<span v-localize>Purge Contained Datasets</span>
</b-dropdown-item>
</b-dropdown>
</b-button-group>
</div>
</template>
<script>
import { DatasetCollection } from "../../model";
import { legacyNavigationMixin } from "components/plugins/legacyNavigation";
import Vue from "vue";
import BootstrapVue from "bootstrap-vue";
import IconButton from "components/IconButton";
Vue.use(BootstrapVue);
export default {
data() {
return {
deleteSelected: null,
deleteCollectionModalShow: false,
};
},
components: {
IconButton,
},
mixins: [legacyNavigationMixin],
inject: ["STATES"],
props: {
dsc: { type: DatasetCollection, required: true },
},
computed: {
editButtonTitle() {
if (this.dsc.deleted) {
return "Collection must not be deleted to edit";
}
if (this.dsc.purged) {
return "Cannot edit attributes of collections removed from disk";
}
const unreadyStates = new Set([this.STATES.UPLOAD, this.STATES.NEW]);
if (unreadyStates.has(this.dsc.state)) {
return "This collection is not yet editable";
}
return "Edit attributes";
},
deleteCollectionButtonTitle() {
return "Delete Collection";
},
hasJob() {
return this.dsc?.job_source_type == "Job";
},
collectionDetailsPath() {
return `jobs/${this.dsc.job_source_id}/view`;
},
},
methods: {
runDelete: function (kind) {
if (kind) {
this.$emit("delete", kind);
} else {
this.$emit("delete");
}
},
notIn(...states) {
const badStates = new Set(states);
return !badStates.has(this.dsc.state);
},
isIn(...states) {
const goodStates = new Set(states);
return goodStates.has(this.dsc.state);
},
},
};
</script>
@@ -1,114 +0,0 @@
<!-- UI only, updates/changes to data should be handled outside
this component via events emissions -->
<template>
<div
class="dataset dataset-collection collapsed"
:class="{ selected }"
:id="typedId"
:data-state="dsc.state"
@keydown.arrow-right.self.stop="$emit('viewCollection')"
@keydown.space.self.stop.prevent="$emit('update:selected', !selected)">
<nav class="content-top-menu p-1 d-flex cursor-pointer">
<div class="d-flex flex-grow-1 overflow-hidden">
<div class="pl-1" v-if="showSelection">
<b-check
class="selector"
:checked="selected"
@click.stop
@change="$emit('update:selected', $event)" />
</div>
<StatusIcon
v-if="dsc.state != 'ok'"
class="status-icon px-1"
:state="dsc.state"
@click.stop="onStatusClick" />
<IconButton
v-if="!dsc.visible"
class="px-1"
state="hidden"
title="Unhide"
icon="eye-slash"
@click.stop="$emit('unhide')" />
<IconButton
v-if="dsc.deleted"
class="px-1"
state="deleted"
title="Undelete"
icon="trash-restore"
@click.stop="$emit('undelete')" />
<div class="content-title title flex-grow-1 overflow-hidden" @click.stop="$emit('viewCollection')">
<h5 class="text-truncate">
<span class="hid">{{ dsc.hid }}</span>
<span class="name">{{ dsc.name }}</span>
</h5>
<DscDescription :dsc="dsc" />
</div>
</div>
<div class="d-flex" v-if="writable">
<slot name="menu">
<DscMenu v-if="!dsc.deleted" v-on="$listeners" :dsc="dsc" />
</slot>
</div>
</nav>
<!--- read-only tags with name: prefix -->
<div v-if="collapsed && dsc.nameTags.length" class="nametags px-2 pb-2">
<Nametag v-for="tag in dsc.nameTags" :key="tag" :tag="tag" />
</div>
<JobStateProgress class="m-2" v-if="dsc.jobSummary" :summary="dsc.jobSummary" />
</div>
</template>
<script>
import { DatasetCollection } from "../../model/DatasetCollection";
import StatusIcon from "../../StatusIcon";
import JobStateProgress from "./JobStateProgress";
import DscDescription from "./DscDescription";
import DscMenu from "./DscMenu";
import { Nametag } from "components/Nametags";
import IconButton from "components/IconButton";
export default {
components: {
StatusIcon,
JobStateProgress,
DscMenu,
Nametag,
IconButton,
DscDescription,
},
props: {
dsc: { type: DatasetCollection, required: true },
selected: { type: Boolean, required: false, default: false },
showSelection: { type: Boolean, required: false, default: false },
writable: { type: Boolean, required: false, default: true },
},
methods: {
onStatusClick() {
console.log("onStatusClick", ...arguments);
},
},
computed: {
typedId() {
return this.dsc.type_id;
},
collapsed() {
return !this.expanded;
},
},
};
</script>
<style lang="scss" scoped>
header,
header * {
cursor: pointer;
}
</style>
@@ -1,44 +0,0 @@
<!-- Job state progress bar for a collection. There's another similar component
at components/JobStates/CollectionJobStates but it relies on the backbone data
model, so probably has to go eventually.-->
<template>
<b-progress v-if="maxJobs && runningJobs" :max="maxJobs" height="1em">
<b-progress-bar v-if="errorJobs" :value="errorJobs" variant="danger"></b-progress-bar>
<b-progress-bar v-if="okJobs" :value="okJobs" variant="success"></b-progress-bar>
<b-progress-bar v-if="runningJobs" :value="runningJobs" variant="info" animated></b-progress-bar>
<b-progress-bar v-if="waitingJobs" :value="waitingJobs" variant="dark" animated></b-progress-bar>
</b-progress>
</template>
<script>
import { JobStateSummary } from "../../model";
export default {
props: {
summary: { type: JobStateSummary, required: true },
},
computed: {
hasRunningJobs() {
return this.summary && this.summary.running > 0;
},
maxJobs() {
return this.summary.all_jobs;
},
okJobs() {
return this.summary.ok;
},
runningJobs() {
return this.summary.running;
},
errorJobs() {
const { failed = 0, error = 0 } = this.summary;
return failed + error;
},
waitingJobs() {
const { waiting = 0, queued = 0 } = this.summary;
return waiting + queued;
},
},
};
</script>
@@ -1,6 +0,0 @@
import DatasetCollection from "./DatasetCollection";
export { default as DscUI } from "./DscUI";
export { default as DscMenu } from "./DscMenu";
export { default as JobStateProgress } from "./JobStateProgress";
export default DatasetCollection;
@@ -1,38 +0,0 @@
<template>
<div>
<div v-for="contentItem in collectionContents" :index="1" :key="contentItem.id" style="margin-left: 2em">
<DatasetProvider
v-if="contentItem.element_type === 'hda'"
:id="contentItem.object.id"
v-slot="{ item, loading }">
<div>
<loading-span v-if="loading" message="Loading datasets" />
<DatasetUIWrapper v-else :item="item" :element_identifier="contentItem.element_identifier">
</DatasetUIWrapper>
</div>
</DatasetProvider>
<DatasetCollectionUIWrapper
v-if="contentItem.element_type === 'dataset_collection'"
:item="contentItem"
:element_count="collectionContents.length + 1">
</DatasetCollectionUIWrapper>
</div>
</div>
</template>
<script>
import { DatasetProvider } from "components/providers";
import DatasetUIWrapper from "./DatasetUIWrapper";
import LoadingSpan from "components/LoadingSpan";
export default {
components: {
DatasetProvider,
DatasetCollectionUIWrapper: () => import("./DatasetCollectionUIWrapper"),
DatasetUIWrapper,
LoadingSpan,
},
props: {
collectionContents: { type: Array, required: true },
},
};
</script>
@@ -1,61 +0,0 @@
import DatasetCollectionUIWrapper from "./DatasetCollectionUIWrapper";
import DscUI from "components/History/ContentItem/DatasetCollection/DscUI";
import { shallowMount } from "@vue/test-utils";
import flushPromises from "flush-promises";
import datasetCollectionRaw from "components/providers/History/test/json/DatasetCollection.json";
import datasetCollectionContent from "components/providers/History/test/json/DatasetCollection.nested.json";
describe("DatasetUIWrapper.vue with Dataset", () => {
let wrapper;
let propsData;
let onHide;
let onUnhide;
let onDelete;
let onUndelete;
beforeEach(async () => {
propsData = {
item: datasetCollectionRaw,
};
onHide = jest.spyOn(DatasetCollectionUIWrapper.methods, "onHide").mockImplementation();
onUnhide = jest.spyOn(DatasetCollectionUIWrapper.methods, "onUnhide").mockImplementation();
onDelete = jest.spyOn(DatasetCollectionUIWrapper.methods, "onDelete").mockImplementation();
onUndelete = jest.spyOn(DatasetCollectionUIWrapper.methods, "onUndelete").mockImplementation();
wrapper = shallowMount(DatasetCollectionUIWrapper, {
propsData,
});
});
it("loads DscUI component", async () => {
expect(wrapper.findComponent(DscUI).exists()).toBeTruthy();
});
it("reacts to all emittted events", async () => {
expect(onHide).not.toHaveBeenCalled();
expect(onUnhide).not.toHaveBeenCalled();
expect(onDelete).not.toHaveBeenCalled();
expect(onUndelete).not.toHaveBeenCalled();
wrapper.findComponent(DscUI).vm.$emit("hide");
expect(onHide).toHaveBeenCalled();
wrapper.findComponent(DscUI).vm.$emit("unhide");
expect(onUnhide).toHaveBeenCalled();
wrapper.findComponent(DscUI).vm.$emit("delete");
expect(onDelete).toHaveBeenCalled();
wrapper.findComponent(DscUI).vm.$emit("undelete");
expect(onUndelete).toHaveBeenCalled();
});
it("creates DatasetCollection", async () => {
const datasetCollection = wrapper.vm.datasetCollection;
expect(datasetCollection.collectionType).toBe("list of pairs");
});
it("manages collection expansion", async () => {
expect(wrapper.vm.expand).toBeFalsy();
wrapper.findComponent(DscUI).vm.$emit("viewCollection", wrapper.vm.datasetCollection);
expect(wrapper.vm.expand).toBeTruthy();
});
it("build dsc from collection content", async () => {
wrapper.setProps({ item: datasetCollectionContent });
await flushPromises();
expect(wrapper.vm.datasetCollection.name).toBe(datasetCollectionContent.element_identifier);
});
});
@@ -1,98 +0,0 @@
<template>
<div>
<DscUI
v-if="datasetCollection"
v-bind="$attrs"
v-on="$listeners"
:dsc="datasetCollection"
:show-tags="true"
@viewCollection="toggleExpand"
@hide="onHide(datasetCollection)"
@unhide="onUnhide(datasetCollection)"
@delete="onDelete(datasetCollection, $event)"
@undelete="onUndelete(datasetCollection)" />
<DatasetCollectionContentProvider v-if="expand" :id="datasetCollection.contents_url" v-slot="{ item, loading }">
<b-spinner v-if="loading">Loading Dataset Collection...</b-spinner>
<DatasetCollectionContents v-else :collection-contents="item" />
</DatasetCollectionContentProvider>
</div>
</template>
<script>
import { deleteDatasetCollection, updateContentFields } from "../../model/queries";
import { cacheContent } from "components/providers/History/caching";
import { DatasetCollection } from "../../model/DatasetCollection";
import DscUI from "components/History/ContentItem/DatasetCollection/DscUI";
import { DatasetCollectionContentProvider } from "components/providers";
import DatasetCollectionContents from "./DatasetCollectionContents";
export default {
components: {
DatasetCollectionContentProvider,
DatasetCollectionContents,
DscUI,
},
props: {
item: { type: Object, required: true },
element_count: { type: Number, required: false },
},
data() {
return { expand: false };
},
computed: {
datasetCollection() {
var rawObject = this.item;
if (rawObject.model_class === "DatasetCollectionElement") {
rawObject = this.item.object;
rawObject.name = this.item.element_identifier;
rawObject.contents_url = this.item.contents_url || rawObject.contents_url;
rawObject.element_count = this.element_count;
rawObject.populated_state = "ok";
}
return new DatasetCollection(rawObject);
},
},
methods: {
toggleExpand() {
this.expand = !this.expand;
},
async onDelete(collection, flags = {}) {
const { recursive = false, purge = false } = flags;
const result = await deleteDatasetCollection(collection, recursive, purge);
if (result.deleted) {
const newFields = Object.assign(collection, {
isDeleted: result.deleted,
});
await cacheContent(newFields);
}
},
async onUndelete(collection) {
const result = await updateContentFields(collection, { deleted: false });
if (result.deleted === false) {
const newFields = Object.assign(collection, {
isDeleted: result.deleted,
});
await cacheContent(newFields);
}
},
async onHide(collection) {
const result = await updateContentFields(collection, { visible: false });
if (result.visible === false) {
const newFields = Object.assign(collection, {
isVisible: result.visible,
});
await cacheContent(newFields);
}
},
async onUnhide(collection) {
const result = await updateContentFields(collection, { visible: true });
if (result.visible) {
const newFields = Object.assign(collection, {
isVisible: result.visible,
});
await cacheContent(newFields);
}
},
},
};
</script>
@@ -1,51 +0,0 @@
import DatasetUIWrapper from "./DatasetUIWrapper";
import DatasetUI from "components/History/ContentItem/Dataset/DatasetUI";
import { shallowMount } from "@vue/test-utils";
import raw from "components/providers/History/test/json/Dataset.json";
describe("DatasetUIWrapper.vue with Dataset", () => {
let wrapper;
let propsData;
let onDelete;
let onUnhide;
let onUndelete;
let updateDataset;
beforeEach(async () => {
propsData = {
item: raw,
};
onDelete = jest.spyOn(DatasetUIWrapper.methods, "onDelete").mockImplementation();
onUnhide = jest.spyOn(DatasetUIWrapper.methods, "onUnhide").mockImplementation();
onUndelete = jest.spyOn(DatasetUIWrapper.methods, "onUndelete").mockImplementation();
updateDataset = jest.spyOn(DatasetUIWrapper.methods, "onUpdate").mockImplementation();
wrapper = shallowMount(DatasetUIWrapper, {
propsData,
});
});
it("loads DatasetUI component", async () => {
expect(wrapper.findComponent(DatasetUI).exists()).toBeTruthy();
});
it("reacts to all emittted events", async () => {
expect(onDelete).not.toHaveBeenCalled();
expect(onUnhide).not.toHaveBeenCalled();
expect(onUndelete).not.toHaveBeenCalled();
expect(updateDataset).not.toHaveBeenCalled();
wrapper.findComponent(DatasetUI).vm.$emit("delete");
expect(onDelete).toHaveBeenCalled();
wrapper.findComponent(DatasetUI).vm.$emit("undelete");
expect(onUndelete).toHaveBeenCalled();
wrapper.findComponent(DatasetUI).vm.$emit("unhide");
expect(onUnhide).toHaveBeenCalled();
wrapper.findComponent(DatasetUI).vm.$emit("update");
expect(updateDataset).toHaveBeenCalled();
});
it("handles element identifier", async () => {
expect(wrapper.vm.dataset.canEditName).toBeTruthy();
await wrapper.setProps({ element_identifier: "a cool identifier" });
expect(wrapper.vm.dataset.title).toBe("a cool identifier");
expect(wrapper.vm.dataset.canEditName).toBeFalsy();
});
});
@@ -1,62 +0,0 @@
<!-- controls display of DisplayUI and delegates modifications -->
<template>
<DatasetUI
v-if="dataset"
v-bind="$attrs"
v-on="$listeners"
@update:expanded="expand"
:expanded="expanded"
:dataset="dataset"
:show-tags="true"
@delete="onDelete"
@undelete="onUndelete"
@unhide="onUnhide"
@update="onUpdate">
</DatasetUI>
</template>
<script>
import DatasetUI from "components/History/ContentItem/Dataset/DatasetUI";
import { Dataset } from "../../model";
import { deleteContent, updateContentFields } from "../../model/queries";
import { cacheContent } from "components/providers/History/caching";
export default {
components: {
DatasetUI,
},
props: {
item: { type: Object, required: true },
element_identifier: { type: String, required: false, default: "" },
},
computed: {
dataset() {
const element = { ...this.item, element_identifier: this.element_identifier };
return new Dataset(element);
},
},
data() {
return {
expanded: false,
};
},
methods: {
expand(event) {
this.expanded = !this.expanded;
},
async onDelete(opts = {}) {
const ajaxResult = await deleteContent(this.dataset, opts);
await cacheContent(ajaxResult);
},
async onUnhide() {
await this.onUpdate({ visible: true });
},
async onUndelete() {
await this.onUpdate({ deleted: false });
},
async onUpdate(changes = {}) {
const newContent = await updateContentFields(this.dataset, changes);
await cacheContent(newContent);
},
},
};
</script>
@@ -1,42 +0,0 @@
<template>
<component :is="providerComponent" :id="data_item.id" v-slot="{ item, loading }">
<div>
<loading-span v-if="loading" message="Loading dataset" />
<component :is="renderComponent" v-if="item" :item="item"> </component>
</div>
</component>
</template>
<script>
import { DatasetProvider, DatasetCollectionProvider } from "components/providers";
import DatasetCollectionUIWrapper from "./DatasetCollectionUIWrapper";
import DatasetUIWrapper from "./DatasetUIWrapper";
import { STATES } from "components/History/model";
import LoadingSpan from "components/LoadingSpan";
export default {
components: {
DatasetProvider,
DatasetCollectionProvider,
DatasetUIWrapper,
DatasetCollectionUIWrapper,
LoadingSpan,
},
props: {
data_item: {
required: true,
},
},
computed: {
renderComponent() {
return { hda: "DatasetUIWrapper", hdca: "DatasetCollectionUIWrapper" }[this.data_item.src];
},
providerComponent() {
return { hda: "DatasetProvider", hdca: "DatasetCollectionProvider" }[this.data_item.src];
},
},
provide: {
STATES,
},
};
</script>
@@ -1,22 +0,0 @@
import ContentItem from "./ContentItem";
export default {
mixins: [ContentItem],
computed: {
contentItemComponent() {
if (this.item.id === undefined) {
return "Placeholder";
}
const { history_content_type } = this.item;
switch (history_content_type) {
case "dataset":
return "Dataset";
case "dataset_collection":
return "DatasetCollection";
default:
return "Placeholder";
}
},
},
};
@@ -1,18 +0,0 @@
<template>
<div class="collapsed" data-state="waiting">
<nav class="content-top-menu d-flex align-items-center justify-content-between p-1">
<h5 class="flex-grow-1 overflow-hidden mr-auto text-nowrap text-truncate">
<span class="name">Placholder: {{ item.hid }}</span>
</h5>
</nav>
</div>
</template>
<script>
export default {
props: {
item: { type: Object, required: true },
index: { type: Number, required: true },
},
};
</script>
@@ -1,29 +0,0 @@
<!-- a separate collection content item for collections
inside of other collections -->
<template>
<DscUI v-if="dsc" v-bind="$attrs" v-on="$listeners" class="dataset-collection history-content" :dsc="dsc" />
</template>
<script>
import { DatasetCollection } from "../model/DatasetCollection";
import { STATES } from "../model";
import DscUI from "./DatasetCollection/DscUI";
export default {
components: {
DscUI,
},
props: {
item: { type: Object, required: true },
},
computed: {
dsc() {
// TODO: I'm sure this is not the right way to set the state, but the collection api
// doesn't return any kind of state value at because the api is inconsistent
// /api/dataset_collections/d071e794759ab192/contents/e144222fed44799a
return new DatasetCollection({ ...this.item, populated_state: STATES.OK });
},
},
};
</script>
@@ -1,60 +0,0 @@
<!-- a read-only dataset inside a collection. The collection query does not return enough
information to properly render the dataset, so what we do is look it up on expand, checking the
cache first to see if we already have the data -->
<template>
<DatasetUI
v-if="dataset"
v-bind="$attrs"
v-on="$listeners"
:dataset="dataset"
:writable="false"
:selectable="false"
@update:expanded="onExpand" />
</template>
<script>
import { Dataset } from "../model";
import { getContentByTypeId, cacheContent } from "components/providers/History/caching";
import { getContentDetails } from "../model/queries";
import DatasetUI from "./Dataset/DatasetUI";
export default {
components: {
DatasetUI,
},
props: {
item: { type: Object, required: true },
},
data() {
return {
localItem: { ...this.item },
loading: false,
};
},
computed: {
dataset() {
return new Dataset(this.localItem);
},
},
methods: {
onExpand(val) {
if (val) {
this.loadDetails();
}
this.$emit("update:expand", val);
},
async loadDetails() {
const { type_id, history_id } = this.item;
const existingDataset = await getContentByTypeId(history_id, type_id);
if (existingDataset) {
this.localItem = existingDataset;
} else {
const loadedDataset = await getContentDetails(this.item);
await cacheContent(loadedDataset);
this.localItem = loadedDataset;
}
},
},
};
</script>
@@ -1,9 +0,0 @@
/**
* Scrolling components for history contents and collection contents.
*/
import "./styles.scss";
export { default as ContentItem } from "./ContentItem";
export { default as CollectionContentItem } from "./CollectionContentItem";
export { default as HistoryContentItem } from "./HistoryContentItem";
@@ -1,32 +0,0 @@
@import "theme/blue.scss";
@import "scss/mixins.scss";
.content-item {
outline: none;
border-bottom: 2px solid white;
&:focus-within {
box-shadow: inset 0 0 1em 0.25em rgba(255, 255, 255, 0.25);
}
}
// Nav across the top of the content item
.content-item .content-top-menu {
min-height: 2em;
.content-title {
.hid {
color: adjust-color($text-color, $alpha: -0.6);
font-size: 80%;
user-select: none;
display: inline-block;
text-align: right;
min-width: 0.25rem;
&:not(:empty)::after {
content: ":";
}
}
.name {
font-weight: 700;
}
}
}
@@ -0,0 +1,34 @@
<template>
<Details
:name="name"
:tags="dsc.tags"
:writeable="writeable"
:show-annotation="false"
@save="$emit('update:dsc', $event)">
<template v-slot:name>
<h3 data-description="collection name display">{{ name || "Collection" }}</h3>
<CollectionDescription :item="dsc" />
</template>
</Details>
</template>
<script>
import Details from "components/History/Layout/Details";
import CollectionDescription from "components/History/Content/Collection/CollectionDescription";
export default {
components: {
CollectionDescription,
Details,
},
props: {
dsc: { type: Object, required: true },
writeable: { type: Boolean, required: true },
},
computed: {
name() {
return this.dsc.name || this.dsc.element_identifier;
},
},
};
</script>
@@ -0,0 +1,37 @@
<template>
<div>
<b-button size="sm" variant="link" @click="close">
<icon icon="angle-double-left" class="mr-1" data-description="back to history" /><span>History</span>
</b-button>
<b-button size="sm" variant="link" v-if="previousName" @click="back">
<span class="fa fa-angle-left mr-1" /><span>{{ previousName }}</span>
</b-button>
</div>
</template>
<script>
export default {
props: {
selectedCollections: { type: Array, required: true, validate: (val) => val.length > 0 },
},
computed: {
previousName() {
const length = this.selectedCollections.length;
if (length > 1) {
const last = this.selectedCollections[length - 2];
return last.name || last.element_identifier;
}
return null;
},
},
methods: {
back() {
const newList = this.selectedCollections.slice(0, -1);
this.$emit("update:selected-collections", newList);
},
close() {
this.$emit("update:selected-collections", []);
},
},
};
</script>
@@ -1,32 +1,25 @@
<template>
<section>
<nav class="d-flex justify-content-end">
<nav class="content-operations d-flex justify-content-between bg-secondary">
<b-button-group>
<IconButton
v-if="isRoot"
class="mb-2 mx-3"
icon="download"
title="Download Collection"
:href="downloadCollectionUrl"
download />
<b-button title="Download Collection" size="sm" variant="link" download :href="downloadUrl">
<Icon class="mr-1" icon="download" />
<span>Download</span>
</b-button>
</b-button-group>
</nav>
</section>
</template>
<script>
import IconButton from "components/IconButton";
export default {
components: { IconButton },
props: {
isRoot: { type: Boolean, required: true },
collection: { type: Object, required: true },
dsc: { type: Object, required: true },
},
computed: {
/** @return {String} */
downloadCollectionUrl() {
return `${this.collection.url}/download`;
downloadUrl() {
return `${this.dsc.url}/download`;
},
},
};
@@ -0,0 +1,106 @@
<!-- When a dataset collection is being viewed, this panel shows the contents of that collection -->
<template>
<UrlDataProvider v-if="dsc" :key="dsc.id" :url="url" auto-refresh v-slot="{ result: payload }">
<ExpandedItems :scope-key="dsc.id" :get-item-key="(item) => item.id" v-slot="{ isExpanded, setExpanded }">
<Layout class="dataset-collection-panel">
<template v-slot:navigation>
<CollectionNavigation
:history="history"
:selected-collections="selectedCollections"
v-on="$listeners" />
</template>
<template v-slot:listcontrols>
<CollectionOperations v-if="isRoot" :dsc="dsc" />
</template>
<template v-slot:details>
<CollectionDetails :dsc="dsc" :writeable="isRoot" @update:dsc="updateDsc(dsc, $event)" />
</template>
<template v-slot:listing>
<Listing item-key="element_index" :payload="payload" :limit="limit" @scroll="onScroll">
<template v-slot:history-item="{ item }">
<ContentItem
:item="item.object"
:id="item.element_index"
:name="item.element_identifier"
:is-dataset="item.element_type == 'hda'"
:expand-dataset="isExpanded(item)"
:is-history-item="false"
@update:expand-dataset="setExpanded(item, $event)"
@view-collection="$emit('view-collection', item)" />
</template>
</Listing>
</template>
</Layout>
</ExpandedItems>
</UrlDataProvider>
</template>
<script>
import { UrlDataProvider } from "components/providers/UrlDataProvider";
import { History } from "components/History/model";
import { updateContentFields } from "components/History/model/queries";
import ContentItem from "components/History/Content/ContentItem";
import ExpandedItems from "components/History/Content/ExpandedItems";
import Listing from "components/History/Layout/Listing";
import Layout from "components/History/Layout/Layout";
import CollectionNavigation from "./CollectionNavigation";
import CollectionOperations from "./CollectionOperations";
import CollectionDetails from "./CollectionDetails";
export default {
components: {
CollectionDetails,
CollectionNavigation,
CollectionOperations,
ContentItem,
ExpandedItems,
Layout,
Listing,
UrlDataProvider,
},
props: {
history: { type: History, required: true },
selectedCollections: { type: Array, required: true },
},
data() {
return {
offset: 0,
limit: 100,
};
},
computed: {
dsc() {
const arr = this.selectedCollections;
return arr[arr.length - 1];
},
isRoot() {
return this.dsc == this.rootCollection;
},
rootCollection() {
return this.selectedCollections[0];
},
url() {
// either the observered source is a collection element or the initial history item i.e. root collection
const source = this.dsc.object || this.rootCollection;
const contentsUrl = source.contents_url.substring(1);
return `${contentsUrl}?offset=${this.offset}&limit=${this.limit}`;
},
},
methods: {
updateDsc(collection, fields) {
updateContentFields(collection, fields).then((response) => {
Object.keys(response).forEach((key) => {
collection[key] = response[key];
});
});
},
onScroll(offset) {
this.offset = offset;
},
},
};
</script>
@@ -1,128 +0,0 @@
<!-- Edits a collection, if that collection is editable. -->
<template>
<section class="history-details d-flex" data-description="edit details">
<div class="flex-grow-1">
<h3 class="history-title" data-description="collection name display">
{{ dscName || "(Collection Name)" }}
</h3>
<p class="mt-1">
<DscDescription :dsc="dsc" />
</p>
<div v-if="isEditing" class="mt-3" @keydown.esc="revertAndCancel" data-description="edit form">
<b-textarea
v-model="dscName"
placeholder="Collection Name"
trim
max-rows="4"
data-description="name input"></b-textarea>
<StatelessTags v-model="tags" class="mt-3 tags" />
</div>
<div v-else-if="dsc.tags && dsc.tags.length">
<Nametag v-for="tag in dsc.tags" :key="tag" :tag="tag" />
</div>
</div>
<EditorMenu
class="ml-3 flex-grow-0 d-flex flex-column"
v-if="writable"
model-name="Collection"
:editing.sync="editing"
:writable="writable"
:valid="valid"
:dirty="dirty"
@save="save"
@revert="revert" />
</section>
</template>
<script>
import { DatasetCollection } from "../model";
import { Nametag } from "components/Nametags";
import EditorMenu from "../EditorMenu";
import { StatelessTags } from "components/Tags";
import DscDescription from "components/History/ContentItem/DatasetCollection/DscDescription";
export default {
components: {
Nametag,
EditorMenu,
StatelessTags,
DscDescription,
},
props: {
dsc: { type: DatasetCollection, required: true },
writable: { type: Boolean, required: true },
},
data() {
return {
editing: false,
tempContent: this.dsc.clone(),
};
},
computed: {
dscId() {
return this.dsc.id;
},
isEditing() {
return this.writable && this.editing;
},
valid() {
return this.tempContent.name.length > 0;
},
dirty() {
return !this.tempContent.equals(this.dsc);
},
dscName: {
get() {
return this.tempContent.name;
},
set(newName) {
this.patchTempDsc({ name: newName });
},
},
tags: {
get() {
return this.tempContent?.tags || [];
},
set(newTags) {
if (Array.isArray(newTags)) {
this.patchTempDsc({ tags: newTags });
}
},
},
},
watch: {
dscId(newId, oldId) {
if (newId != oldId) {
this.reset();
}
},
},
methods: {
reset() {
this.tempContent = this.dsc.clone();
},
patchTempDsc(props = {}) {
this.tempContent = this.tempContent.patch(props);
},
save() {
if (this.valid) {
this.$emit("update:dsc", this.tempContent);
this.editing = false;
}
},
revert() {
if (this.dirty) {
this.reset();
}
},
revertAndCancel() {
this.revert();
this.editing = false;
},
},
};
</script>
@@ -1,126 +0,0 @@
<!-- When a dataset collection is being viewed, this panel shows the contents of that collection -->
<template>
<DscProvider :is-root="isRoot" :debounce-period="500" :collection="selectedCollection" v-slot="{ dsc }">
<CollectionContentProvider v-if="dsc" :parent="dsc" :debug="false" v-slot="{ loading, payload, setScrollPos }">
<ExpandedItems
:scope-key="selectedCollection.id"
:get-item-key="(item) => item.type_id"
v-slot="{ isExpanded, setExpanded }">
<Layout class="dataset-collection-panel">
<template v-slot:globalNav>
<TopNav :history="history" :selected-collections="selectedCollections" v-on="$listeners" />
</template>
<template v-slot:localNav>
<!-- Empty -->
<div />
</template>
<template v-slot:listcontrols>
<CollectionOperations :collection="selectedCollection" :is-root="isRoot" />
</template>
<template v-slot:details>
<Details :dsc="dsc" :writable="writable" @update:dsc="updateDsc(dsc, $event)" />
</template>
<template v-slot:listing>
<Scroller
key-field="element_index"
:class="{ loadingBackground: loading }"
v-bind="payload"
@scroll="setScrollPos"
:debug="false">
<template v-slot="{ item, index, rowKey }">
<CollectionContentItem
:item="item"
:index="index"
:row-key="rowKey"
:expanded="isExpanded(item)"
:writable="false"
:selectable="false"
@update:expanded="setExpanded(item, $event)"
@viewCollection="$emit('viewCollection', item)"
:data-index="index"
:data-row-key="rowKey" />
</template>
</Scroller>
</template>
</Layout>
</ExpandedItems>
</CollectionContentProvider>
</DscProvider>
</template>
<script>
import { History } from "../model";
import { updateContentFields } from "../model/queries";
import { cacheContent } from "components/providers/History/caching";
import { DscProvider, CollectionContentProvider } from "components/providers/History";
import ExpandedItems from "../ExpandedItems";
import Layout from "../Layout";
import TopNav from "./TopNav";
import CollectionOperations from "./CollectionOperations.vue";
import Details from "./Details";
import Scroller from "../Scroller";
import { CollectionContentItem } from "../ContentItem";
import { reportPayload } from "components/providers/History/ContentProvider/helpers";
export default {
filters: {
reportPayload,
},
components: {
DscProvider,
CollectionContentProvider,
Layout,
TopNav,
Details,
Scroller,
CollectionContentItem,
ExpandedItems,
CollectionOperations,
},
props: {
history: { type: History, required: true },
selectedCollections: { type: Array, required: true },
},
computed: {
selectedCollection() {
const arr = this.selectedCollections;
const selected = arr[arr.length - 1];
return selected;
},
isRoot() {
return this.selectedCollection == this.rootCollection;
},
writable() {
return this.isRoot;
},
rootCollection() {
return this.selectedCollections[0];
},
downloadCollectionUrl() {
let url = "";
if (this.rootCollection) {
url = `${this.rootCollection.url}/download`;
}
return url;
},
},
methods: {
// change the data of the root collection, anything past the root
// collection is part of the dataset collection, which i believe is supposed to
// be immutable, so only edit name, tags, blah of top-level selected collection,
async updateDsc(collection, fields) {
if (this.writable) {
const ajaxResult = await updateContentFields(collection, fields);
await cacheContent({ ...collection, ...ajaxResult });
}
},
},
};
</script>
@@ -1,71 +0,0 @@
<template>
<b-dropdown
v-if="breadCrumbOptions.length"
size="sm"
text="Return to..."
boundary="viewport"
data-description="collection breadcrumbs menu"
no-caret>
<template v-slot:button-content>
<Icon icon="arrow-up" class="mr-1" />
<b class="text-nowrap" v-localize>Return to...</b>
</template>
<b-dropdown-item @click="close" data-description="back to history">
<span>History: {{ history.name }}</span>
</b-dropdown-item>
<b-dropdown-item v-for="option in breadCrumbOptions" :key="option.key" @click="reselect(option.value)">
<span>{{ option.text }}</span>
</b-dropdown-item>
</b-dropdown>
<b-button
v-else
class="back"
size="sm"
title="`Return to: ${history.name}`"
@click="close"
data-description="back to history">
<Icon icon="arrow-up" class="mr-1" />
<b class="text-nowrap">Return to: {{ history.name }}</b>
</b-button>
</template>
<script>
import { History } from "../model";
export default {
props: {
history: { type: History, required: true },
selectedCollections: { type: Array, required: true, validate: (val) => val.length > 0 },
},
computed: {
// List for the dropdown. Should be the history and every parent
// collection excepting the current one.
breadCrumbOptions() {
const parents = this.selectedCollections.slice(0, -1);
const options = parents.map((bc, i) => {
return {
key: bc.type_id,
value: parents.slice(0, i + 1),
text: bc.name,
};
});
return options;
},
},
methods: {
reselect(newList) {
this.$emit("update:selectedCollections", newList);
},
back() {
const newList = this.selectedCollections.slice(0, -1);
this.reselect(newList);
},
close() {
this.reselect([]);
},
},
};
</script>
@@ -0,0 +1,37 @@
<template>
<Details
:name="history.name"
:annotation="history.annotation"
:tags="history.tags"
:writeable="writeable"
@save="$emit('update:currentHistory', $event)">
<template v-slot:name>
<h3 data-description="history name display">{{ history.name || "History" }}</h3>
<h5 class="history-size">
<span v-if="history.size">{{ history.size | niceFileSize }}</span>
<span v-else v-localize>(empty)</span>
</h5>
</template>
</Details>
</template>
<script>
import prettyBytes from "pretty-bytes";
import { History } from "components/History/model";
import Details from "components/History/Layout/Details";
export default {
components: {
Details,
},
filters: {
niceFileSize(rawSize = 0) {
return prettyBytes(rawSize);
},
},
props: {
history: { type: History, required: true },
writeable: { type: Boolean, default: true },
},
};
</script>
@@ -31,7 +31,6 @@
</template>
<script>
import { SearchParams } from "components/providers/History/SearchParams";
import DebouncedInput from "components/DebouncedInput";
export default {
@@ -39,25 +38,15 @@ export default {
DebouncedInput,
},
props: {
params: { type: SearchParams, required: true },
params: { type: Object, required: true },
},
computed: {
skip: {
get() {
return this.params.skip;
},
set(newSkip) {
const newParams = this.params.clone();
newParams.skip = newSkip;
this.updateParams(newParams);
},
},
filterText: {
get() {
return this.params.filterText;
},
set(newVal) {
const newParams = this.params.clone();
const newParams = Object.assign({}, this.params);
newParams.filterText = newVal;
this.updateParams(newParams);
},
@@ -67,7 +56,7 @@ export default {
return this.params.showDeleted;
},
set(newFlag) {
const newParams = this.params.clone();
const newParams = Object.assign({}, this.params);
newParams.showDeleted = newFlag;
this.updateParams(newParams);
},
@@ -77,7 +66,7 @@ export default {
return this.params.showHidden;
},
set(newFlag) {
const newParams = this.params.clone();
const newParams = Object.assign({}, this.params);
newParams.showHidden = newFlag;
this.updateParams(newParams);
},
@@ -106,8 +106,8 @@
</template>
<script>
import { History } from "./model";
import CopyHistoryModal from "./CopyModal";
import { History } from "components/History/model";
import CopyHistoryModal from "components/History/Modals/CopyModal";
import { legacyNavigationMixin } from "components/plugins/legacyNavigation";
export default {
@@ -13,7 +13,7 @@
</template>
<script>
import { History } from "./model";
import { History } from "components/History/model";
export default {
props: {
@@ -61,10 +61,10 @@
</template>
<script>
import { History } from "./model";
import { History } from "components/History/model";
import { legacyNavigationMixin } from "components/plugins/legacyNavigation";
import { switchToLegacyHistoryPanel } from "./adapters/betaToggle";
import HistorySelectorModal from "./HistorySelectorModal";
import { switchToLegacyHistoryPanel } from "components/History/adapters/betaToggle";
import HistorySelectorModal from "components/History/Modals/HistorySelectorModal";
export default {
mixins: [legacyNavigationMixin],
@@ -1,117 +1,103 @@
<template>
<section>
<nav class="content-operations-menu d-flex justify-content-between bg-secondary">
<nav class="content-operations d-flex justify-content-between bg-secondary">
<b-button-group>
<IconButton
:title="hasSelection ? 'Select All Contents' : 'Unselect All Contents'"
icon="check-square"
:disabled="!totalMatches"
:pressed="hasSelection"
@click="toggleSelectAll" />
<IconButton
title="Filter History Content"
icon="filter"
:disabled="!totalMatches"
<b-button
title="Select Items"
class="show-history-content-selectors-btn rounded-0"
size="sm"
variant="link"
:disabled="!hasMatches"
:pressed="showSelection"
@click="toggleSelection">
<Icon icon="check-square" />
</b-button>
<b-button
title="Search Items"
class="rounded-0"
size="sm"
variant="link"
:pressed="showFilter"
@click="showFilter = !showFilter"
data-description="content filter toggle" />
<IconButton
title="Collapse Expanded Items"
icon="compress"
@click="toggleFilter"
data-description="content filter toggle">
<Icon icon="search" />
</b-button>
<b-button
title="Collapse Items"
class="rounded-0"
size="sm"
variant="link"
:disabled="!expandedCount"
@click="$emit('collapseAllContent')" />
@click="$emit('collapse-all')">
<Icon icon="compress" />
</b-button>
</b-button-group>
<b-button-group>
<b-dropdown
class="history-contents-list-action-menu-btn"
size="sm"
text="Selection"
:disabled="!hasSelection"
data-description="selected content menu">
<b-dropdown-text id="history-op-selected-content">
<span v-localize v-if="hasSelection">With {{ numSelected }} selected items...</span>
<span v-localize v-else>You don't have any selected content.</span>
size="sm"
variant="link"
toggle-class="text-decoration-none"
data-description="selected content menu"
:disabled="!hasSelection">
<b-dropdown-text>
<span v-localize>With {{ numSelected }} selected...</span>
</b-dropdown-text>
<b-dropdown-item aria-describedby="history-op-selected-content" v-b-modal:hide-selected-content>
<span v-localize>Hide</span>
</b-dropdown-item>
<b-dropdown-item aria-describedby="history-op-selected-content" v-b-modal:show-selected-content>
<b-dropdown-item v-if="params.showHidden" v-b-modal:show-selected-content>
<span v-localize>Unhide</span>
</b-dropdown-item>
<b-dropdown-item aria-describedby="history-op-selected-content" v-b-modal:delete-selected-content>
<span v-localize>Delete</span>
<b-dropdown-item v-else v-b-modal:hide-selected-content>
<span v-localize>Hide</span>
</b-dropdown-item>
<b-dropdown-item aria-describedby="history-op-selected-content" v-b-modal:restore-selected-content>
<b-dropdown-item v-if="params.showDeleted" v-b-modal:restore-selected-content>
<span v-localize>Undelete</span>
</b-dropdown-item>
<b-dropdown-item
aria-describedby="history-op-selected-content"
v-b-modal:purge-selected-content
:disabled="!hasSelection">
<span v-localize>Permanently Delete</span>
<b-dropdown-item v-else v-b-modal:delete-selected-content>
<span v-localize>Delete</span>
</b-dropdown-item>
<b-dropdown-item
aria-describedby="history-op-selected-content"
@click="buildDatasetList"
data-description="build list">
<b-dropdown-item v-if="!params.showDeleted" v-b-modal:purge-selected-content>
<span v-localize>Delete (permanently)</span>
</b-dropdown-item>
<b-dropdown-item v-if="showBuildOptions" @click="buildDatasetList" data-description="build list">
<span v-localize>Build Dataset List</span>
</b-dropdown-item>
<b-dropdown-item
aria-describedby="history-op-selected-content"
@click="buildDatasetPair"
data-description="build pair">
<b-dropdown-item v-if="showBuildOptions" @click="buildDatasetPair" data-description="build pair">
<span v-localize>Build Dataset Pair</span>
</b-dropdown-item>
<b-dropdown-item
aria-describedby="history-op-selected-content"
v-if="showBuildOptions"
@click="buildListOfPairs"
data-description="build list of pairs">
<span v-localize>Build List of Dataset Pairs</span>
</b-dropdown-item>
<b-dropdown-item
aria-describedby="history-op-selected-content"
v-if="showBuildOptions"
@click="buildCollectionFromRules"
data-description="build collection from rules">
<span v-localize>Build Collection from Rules</span>
</b-dropdown-item>
</b-dropdown>
<b-dropdown
class="history-contents-list-action-menu-btn"
size="sm"
text="History"
:disabled="!totalMatches"
data-description="history action menu">
size="sm"
variant="link"
class="rounded-0"
toggle-class="text-decoration-none"
data-description="history action menu"
:disabled="!hasMatches">
<b-dropdown-text id="history-op-all-content">
<span v-localize>With entire history...</span>
</b-dropdown-text>
<b-dropdown-item
aria-describedby="history-op-all-content"
@click="iframeRedirect('/dataset/copy_datasets')"
data-description="copy datasets">
<b-dropdown-item @click="iframeRedirect('/dataset/copy_datasets')" data-description="copy datasets">
<span v-localize>Copy Datasets</span>
</b-dropdown-item>
<b-dropdown-item v-b-modal:show-all-hidden-content aria-describedby="history-op-all-content">
<b-dropdown-item v-b-modal:show-all-hidden-content>
<span v-localize>Unhide All Hidden Content</span>
</b-dropdown-item>
<b-dropdown-item v-b-modal:delete-all-hidden-content aria-describedby="history-op-all-content">
<b-dropdown-item v-b-modal:delete-all-hidden-content>
<span v-localize>Delete All Hidden Content</span>
</b-dropdown-item>
<b-dropdown-item v-b-modal:purge-all-deleted-content aria-describedby="history-op-all-content">
<b-dropdown-item v-b-modal:purge-all-deleted-content>
<span v-localize>Purge All Hidden Content</span>
</b-dropdown-item>
</b-dropdown>
@@ -119,11 +105,9 @@
</nav>
<transition name="shutterfade">
<content-filters v-if="showContentFilters" class="content-filters p-2" :params.sync="localParams" />
<history-filters v-if="showFilter" class="content-operations-filters p-2" :params.sync="localParams" />
</transition>
<!-- #region Menus and popovers -->
<b-modal id="hide-selected-content" title="Hide Selected Content?" title-tag="h2" @ok="hideSelected">
<p v-localize>Really hide {{ numSelected }} content items?</p>
</b-modal>
@@ -155,14 +139,11 @@
<b-modal id="purge-all-deleted-content" title="Purge Deleted Datasets" title-tag="h2" @ok="purgeAllDeleted">
<p v-localize>"Really delete all deleted datasets permanently? This cannot be undone.</p>
</b-modal>
<!-- #endregion -->
</section>
</template>
<script>
import { SearchParams } from "components/providers/History/SearchParams";
import { History } from "./model/History";
import { History } from "components/History/model/History";
import {
hideSelectedContent,
unhideSelectedContent,
@@ -172,28 +153,24 @@ import {
unhideAllHiddenContent,
deleteAllHiddenContent,
purgeAllDeletedContent,
} from "./model";
import { createDatasetCollection } from "./model/queries";
import { cacheContent } from "components/providers/History/caching";
} from "components/History/model";
import { createDatasetCollection } from "components/History/model/queries";
import { legacyNavigationMixin } from "components/plugins/legacyNavigation";
import { buildCollectionModal } from "./adapters/buildCollectionModal";
import ContentFilters from "./ContentFilters";
import IconButton from "components/IconButton";
import { buildCollectionModal } from "components/History/adapters/buildCollectionModal";
import HistoryFilters from "./HistoryFilters";
export default {
mixins: [legacyNavigationMixin],
components: {
ContentFilters,
IconButton,
HistoryFilters,
},
props: {
history: { type: History, required: true },
params: { type: SearchParams, required: true },
params: { type: Object, required: true },
contentSelection: { type: Map, required: true },
showSelection: { type: Boolean, required: true },
totalMatches: { type: Number, required: true },
hasMatches: { type: Boolean, required: true },
expandedCount: { type: Number, required: false, default: 0 },
debug: { type: Boolean, default: false },
},
data() {
return {
@@ -201,62 +178,55 @@ export default {
};
},
computed: {
showContentFilters() {
showBuildOptions() {
return !this.params.showHidden && !this.params.showDeleted;
},
hasHistoryFilters() {
const { showHidden, showDeleted, filterText } = this.params;
return this.showFilter || showHidden || showDeleted || filterText;
return showHidden || showDeleted || filterText;
},
localParams: {
get() {
return this.params;
},
set(newVal) {
this.$emit("update:params", newVal.clone());
this.$emit("update:params", Object.assign({}, newVal));
},
},
numSelected() {
return this.contentSelection.size || 0;
},
hasSelection() {
return this.numSelected > 0;
},
countHidden() {
return this.history.contents_active.hidden;
},
},
methods: {
toggleSelectAll() {
if (this.hasSelection) {
this.$emit("resetSelection");
this.$emit("update:show-selection", false);
} else {
this.$emit("selectAllContent");
this.$emit("update:show-selection", true);
toggleSelection() {
this.$emit("update:show-selection", !this.showSelection);
},
toggleFilter() {
if (!this.showFilter) {
this.showFilter = true;
} else if (!this.hasContentFilters) {
this.showFilter = false;
}
},
// #region history-wide bulk updates, does server query first to determine "selection"
// History-wide bulk updates, does server query first to determine "selection"
async unhideAll(evt) {
await unhideAllHiddenContent(this.history);
this.$emit("manualReload");
this.$emit("reload");
},
async deleteAllHidden(evt) {
await deleteAllHiddenContent(this.history);
this.$emit("manualReload");
this.$emit("reload");
},
async purgeAllDeleted(evt) {
await purgeAllDeletedContent(this.history);
this.$emit("manualReload");
this.$emit("reload");
},
// #endregion
// #region Selected content manipulation, hide/show/delete/purge
// Selected content manipulation, hide/show/delete/purge
hideSelected() {
this.runOnSelection(hideSelectedContent);
},
@@ -275,18 +245,12 @@ export default {
async runOnSelection(fn) {
const items = Array.from(this.contentSelection.values());
const type_ids = items.map((o) => o.type_id);
const results = await fn(this.history, type_ids);
if (this.debug) {
console.log("operation results", results);
}
this.$emit("resetSelection");
this.$emit("manualReload");
await fn(this.history, type_ids);
this.$emit("hide-selection", items);
this.$emit("reset-selection");
},
// #endregion
// #region collection creation, fires up a modal
// collection creation, fires up a modal
async buildDatasetList() {
await this.buildNewCollection("list");
},
@@ -301,28 +265,19 @@ export default {
},
async buildNewCollection(collectionTypeCode) {
const modalResult = await buildCollectionModal(collectionTypeCode, this.history.id, this.contentSelection);
const newCollection = await createDatasetCollection(this.history, modalResult);
// cache the collection
await cacheContent(newCollection);
await createDatasetCollection(this.history, modalResult);
// have to hide the source items if that was requested
if (modalResult.hide_source_items) {
this.contentSelection.forEach(async (dataset) => {
await cacheContent({ ...dataset, visible: false }, true);
});
this.$emit("resetSelection");
this.$emit("hide-selection", this.contentSelection);
this.$emit("reset-selection");
}
// this.$emit("manualReload");
},
// #endregion
},
watch: {
hasSelection(newVal) {
if (newVal) {
this.$emit("update:showSelection", true);
this.$emit("update:show-selection", true);
}
},
},
@@ -331,7 +286,7 @@ export default {
<style lang="scss">
// remove borders around buttons in menu
.content-operations-menu .btn-group .btn {
.content-operations .btn-group .btn {
border-color: transparent !important;
}
</style>
@@ -0,0 +1,182 @@
<template>
<UrlDataProvider :key="history.id" :url="dataUrl" auto-refresh v-slot="{ loading, result: payload }">
<ExpandedItems
:scope-key="history.id"
:get-item-key="(item) => item.type_id"
v-slot="{ expandedCount, isExpanded, setExpanded, collapseAll }">
<SelectedItems
:scope-key="history.id"
:get-item-key="(item) => item.type_id"
v-slot="{
selectedItems,
showSelection,
setShowSelection,
selectItems,
isSelected,
setSelected,
resetSelection,
}">
<Layout>
<template v-slot:navigation>
<slot name="navigation" :history="history" />
<HistoryMenu :history="history" v-on="$listeners" />
</template>
<template v-slot:details>
<HistoryDetails :history="history" v-on="$listeners" />
</template>
<template v-slot:messages>
<HistoryMessages class="m-2" :history="history" />
</template>
<template v-slot:listcontrols>
<HistoryOperations
:history="history"
:params.sync="params"
:content-selection="selectedItems"
:show-selection="showSelection"
:expanded-count="expandedCount"
:has-matches="hasMatches(payload)"
@update:content-selection="selectItems"
@update:show-selection="setShowSelection"
@reset-selection="resetSelection"
@hide-selection="onHideSelection"
@select-all="selectItems(payload)"
@collapse-all="collapseAll" />
</template>
<template v-slot:listing>
<HistoryEmpty v-if="payload && payload.length == 0" class="m-2" />
<b-alert v-else-if="loading" class="m-2" variant="info" show>
<LoadingSpan message="Loading History" />
</b-alert>
<Listing
v-else
reversed
:query-key="queryKey"
:limit="limit"
:payload="payload"
@scroll="onScroll">
<template v-slot:history-item="{ item }">
<ContentItem
v-if="!hideSelection[item.hid]"
:item="item"
:id="item.hid"
:name="item.name"
:expand-dataset="isExpanded(item)"
:is-dataset="item.history_content_type == 'dataset'"
:selected="isSelected(item)"
:selectable="showSelection"
@update:expand-dataset="setExpanded(item, $event)"
@update:selected="setSelected(item, $event)"
@view-collection="$emit('view-collection', item)"
@delete="onDelete"
@undelete="onUndelete"
@unhide="onUnhide" />
</template>
</Listing>
</template>
<template v-slot:modals>
<ToolHelpModal />
</template>
</Layout>
</SelectedItems>
</ExpandedItems>
</UrlDataProvider>
</template>
<script>
import { UrlDataProvider } from "components/providers/UrlDataProvider";
import LoadingSpan from "components/LoadingSpan";
import ContentItem from "components/History/Content/ContentItem";
import { History } from "components/History/model";
import { deleteContent, updateContentFields } from "components/History/model/queries";
import ExpandedItems from "components/History/Content/ExpandedItems";
import SelectedItems from "components/History/Content/SelectedItems";
import Layout from "components/History/Layout/Layout";
import Listing from "components/History/Layout/Listing";
import ToolHelpModal from "components/History/Modals/ToolHelpModal";
import HistoryOperations from "./HistoryOperations";
import HistoryDetails from "./HistoryDetails";
import HistoryEmpty from "./HistoryEmpty";
import HistoryMenu from "./HistoryMenu";
import HistoryMessages from "./HistoryMessages";
export default {
components: {
LoadingSpan,
UrlDataProvider,
Layout,
Listing,
ContentItem,
HistoryMessages,
HistoryDetails,
HistoryEmpty,
HistoryMenu,
HistoryOperations,
ToolHelpModal,
ExpandedItems,
SelectedItems,
},
props: {
history: { type: History, required: true },
},
data() {
return {
hideSelection: {},
offset: 0,
limit: 500,
params: {},
};
},
watch: {
queryKey() {
this.hideSelection = {};
this.offset = 0;
},
},
computed: {
dataUrl() {
return `api/histories/${this.historyId}/contents?v=dev&order=hid&offset=${this.offset}&limit=${this.limit}&${this.queryString}`;
},
historyId() {
return this.history.id;
},
queryKey() {
return `${this.history.id}&${this.queryString}`;
},
queryString() {
const deleted = this.params.showDeleted ? "True" : "False";
const visible = this.params.showHidden ? "False" : "True";
return `q=deleted&q=visible&qv=${deleted}&qv=${visible}`;
},
},
methods: {
hasMatches(payload) {
return !!payload && payload.length > 0;
},
onScroll(offset) {
this.offset = offset;
},
onHideSelection(selectedItems) {
selectedItems.forEach((item) => {
this.hideSelection[item.hid] = true;
});
},
onDelete(item) {
this.hideSelection[item.hid] = true;
deleteContent(item);
},
onUndelete(item) {
this.hideSelection[item.hid] = true;
updateContentFields(item, { deleted: false });
},
onUnhide(item) {
this.hideSelection[item.hid] = true;
updateContentFields(item, { visible: true });
},
},
};
</script>
@@ -1,35 +0,0 @@
<template>
<CurrentUser class="d-flex flex-column" v-slot="{ user }">
<UserHistories v-if="user" :user="user" v-slot="{ currentHistory, histories, handlers }">
<HistoryPanel v-if="currentHistory" :history="currentHistory" v-on="handlers" id="current-history-panel">
<template v-slot:globalNav>
<HistoriesMenu
v-on="handlers"
:histories="histories"
:current-history="currentHistory"
title="Histories" />
</template>
</HistoryPanel>
<div v-else class="flex-grow-1 loadingBackground h-100">
<span class="sr-only" v-localize>Loading History...</span>
</div>
</UserHistories>
</CurrentUser>
</template>
<script>
import CurrentUser from "components/providers/CurrentUser";
import UserHistories from "components/providers/History/UserHistories";
import HistoryPanel from "./HistoryPanel";
import HistoriesMenu from "./HistoriesMenu";
export default {
components: {
CurrentUser,
UserHistories,
HistoryPanel,
HistoriesMenu,
},
};
</script>
@@ -1,28 +0,0 @@
<template>
<b-modal ref="deleteModal" title="Delete History" v-model="showModal" title-tag="h2">
<p>Delete?</p>
</b-modal>
</template>
<script>
import { History } from "./model";
export default {
props: {
history: { type: History, required: true },
value: { type: Boolean, required: false, default: false },
},
computed: {
showModal: {
get() {
return this.value;
},
set(newVal, oldVal) {
if (newVal !== oldVal) {
this.$emit("input", newVal);
}
},
},
},
};
</script>
@@ -1,73 +0,0 @@
<template>
<nav class="edit-controls">
<IconButton
:title="label('Edit') | l"
class="edit-button mb-1"
:pressed="editing"
icon="pen"
@click="$emit('update:editing', !editing)"
data-description="editor toggle" />
<template v-if="editing">
<!-- save -->
<IconButton
:title="label('Save') | l"
class="save-button mb-1"
:class="{ disabled: !valid }"
icon="save"
@click="$emit('save')"
data-description="editor save button" />
<!-- revert -->
<IconButton
:title="label('Revert') | l"
class="revert-changes-button mb-1"
:class="{ disabled: !dirty }"
icon="undo"
@click="$emit('revert')"
data-description="editor revert button" />
<!-- form status -->
<IconButton
:title="statusLabel | l"
class="status-button mb-1"
:class="{ valid: valid, disabled: !valid }"
:icon="valid ? 'check' : 'exclamation-triangle'"
variant="link"
data-description="editor status" />
</template>
</nav>
</template>
<script>
import IconButton from "components/IconButton";
export default {
components: {
IconButton,
},
props: {
modelName: { type: String, required: true },
editing: { type: Boolean, required: true },
dirty: { type: Boolean, required: false, default: false },
valid: { type: Boolean, required: false, default: false },
},
computed: {
statusLabel() {
const status = this.valid ? "Valid" : "Invalid";
return `${status} ${this.modelName}`;
},
},
methods: {
label(verb) {
return `${verb} ${this.modelName}`;
},
},
};
</script>
<style lang="scss">
.edit-controls .disabled {
opacity: 0.25;
}
</style>
-146
View File
@@ -1,146 +0,0 @@
<template>
<HistoryContentProvider
:parent="history"
:params="params"
:disable-poll="false"
:debug="false"
:debounce-period="500"
v-slot="{ loading, payload, manualReload, setScrollPos }">
<ExpandedItems
:scope-key="history.id"
:get-item-key="(item) => item.type_id"
v-slot="{ expandedCount, isExpanded, setExpanded, collapseAll }">
<SelectedItems
:scope-key="history.id"
:get-item-key="(item) => item.type_id"
v-slot="{
selectedItems,
showSelection,
setShowSelection,
selectItems,
isSelected,
setSelected,
resetSelection,
}">
<Layout>
<template v-slot:globalNav>
<slot name="globalNav" :history="history"></slot>
</template>
<template v-slot:localNav>
<HistoryMenu :history="history" v-on="$listeners" />
</template>
<template v-slot:details>
<HistoryDetails :history="history" v-on="$listeners" />
</template>
<template v-slot:messages>
<HistoryMessages class="m-2" :history="history" />
</template>
<template v-slot:listcontrols>
<ContentOperations
:history="history"
:total-matches="payload.totalMatches"
:loading="loading"
:params.sync="params"
:content-selection="selectedItems"
@update:content-selection="selectItems"
:show-selection="showSelection"
@update:show-selection="setShowSelection"
@resetSelection="resetSelection"
@selectAllContent="selectItems(payload.contents)"
@manualReload="manualReload"
:expanded-count="expandedCount"
@collapseAllContent="collapseAll" />
</template>
<template v-slot:listing>
<HistoryEmpty v-if="history.empty" class="m-2" />
<HistoryEmpty v-else-if="payload && payload.noResults" message="No Results." class="m-2" />
<Scroller
v-else-if="payload"
:class="{ loadingBackground: loading }"
key-field="hid"
v-bind="payload"
:debug="false"
@scroll="setScrollPos">
<template v-slot="{ item, index, rowKey }">
<HistoryContentItem
:item="item"
:index="index"
:row-key="rowKey"
:show-selection="showSelection"
:expanded="isExpanded(item)"
@update:expanded="setExpanded(item, $event)"
:selected="isSelected(item)"
@update:selected="setSelected(item, $event)"
@viewCollection="$emit('viewCollection', item)"
:data-hid="item.hid"
:data-index="index"
:data-row-key="rowKey" />
</template>
</Scroller>
</template>
<template v-slot:modals>
<ToolHelpModal />
</template>
</Layout>
</SelectedItems>
</ExpandedItems>
</HistoryContentProvider>
</template>
<script>
import { History } from "./model";
import { SearchParams } from "components/providers/History/SearchParams";
import { HistoryContentProvider } from "components/providers/History/";
import ExpandedItems from "./ExpandedItems";
import SelectedItems from "./SelectedItems";
import Layout from "./Layout";
import HistoryMessages from "./HistoryMessages";
import HistoryDetails from "./HistoryDetails";
import HistoryEmpty from "./HistoryEmpty";
import ContentOperations from "./ContentOperations";
import ToolHelpModal from "./ToolHelpModal";
import Scroller from "./Scroller";
import { HistoryContentItem } from "./ContentItem";
import { reportPayload } from "components/providers/History/ContentProvider/helpers";
import HistoryMenu from "./HistoryMenu";
export default {
filters: {
reportPayload,
},
components: {
HistoryContentProvider,
Layout,
HistoryMessages,
HistoryDetails,
HistoryEmpty,
ContentOperations,
ToolHelpModal,
Scroller,
HistoryContentItem,
ExpandedItems,
SelectedItems,
HistoryMenu,
},
props: {
history: { type: History, required: true },
},
data() {
return {
params: new SearchParams(),
useItemSelection: false,
};
},
computed: {
historyId() {
return this.history.id;
},
},
};
</script>
@@ -1,151 +0,0 @@
<!--
History object editor. Change name, edit tags, description.
Emit new History model object when done via .sync syntax
-->
<template>
<section class="history-details d-flex" data-description="edit details">
<div class="flex-grow-1 overflow-hidden">
<h3 data-description="history name display">{{ historyName || "(History Name)" }}</h3>
<h5 class="history-size">
<span v-if="history.size">{{ history.size | niceFileSize }}</span>
<span v-else v-localize>(empty)</span>
</h5>
<!-- display title, annotation, tags -->
<div v-if="!editing">
<p data-description="history annotation" v-if="annotation" class="mt-1">{{ annotation }}</p>
<StatelessTags class="mt-2 tags" :value="tags" :disabled="true" />
</div>
<!-- edit form, change title, annotation, or tags -->
<div v-else class="mt-3" @keydown.esc="revertAndCancel" data-description="edit form">
<b-textarea
class="mb-2"
v-model="historyName"
placeholder="History Name"
trim
max-rows="4"
data-description="name input"></b-textarea>
<b-textarea
class="mb-2"
v-model="annotation"
placeholder="Annotation (optional)"
trim
max-rows="4"
data-description="annotation input"></b-textarea>
<StatelessTags class="mt-3 tags" v-model="tags" />
</div>
</div>
<EditorMenu
class="ml-3 flex-grow-0 d-flex flex-column"
v-if="writable"
model-name="History"
:editing.sync="editing"
:writable="writable"
:valid="valid"
:dirty="dirty"
@save="save"
@revert="revert" />
</section>
</template>
<script>
import prettyBytes from "pretty-bytes";
import { History } from "./model";
import EditorMenu from "./EditorMenu";
import { StatelessTags } from "components/Tags";
export default {
components: {
EditorMenu,
StatelessTags,
},
filters: {
niceFileSize(rawSize = 0) {
return prettyBytes(rawSize);
},
},
props: {
history: { type: History, required: true },
writable: { type: Boolean, required: false, default: true },
},
data() {
return {
editing: false,
tempHistory: this.history.clone(),
};
},
computed: {
historyId() {
return this.history.id;
},
isEditing() {
return this.writable && this.editing;
},
valid() {
return this.tempHistory.name.length > 0;
},
dirty() {
return !this.tempHistory.equals(this.history);
},
annotation: {
get() {
return this.tempHistory.annotation;
},
set(annotation) {
this.patchTempHistory({ annotation });
},
},
historyName: {
get() {
return this.tempHistory.name;
},
set(name) {
this.patchTempHistory({ name });
},
},
tags: {
get() {
return this.tempHistory.tags || [];
},
set(newTags) {
this.patchTempHistory({ tags: newTags });
},
},
},
watch: {
historyId(newId, oldId) {
if (newId != oldId) {
this.reset();
}
},
},
methods: {
reset() {
this.tempHistory = this.history.clone();
},
patchTempHistory(props = {}) {
this.tempHistory = this.tempHistory.patch(props);
},
save() {
if (this.valid) {
this.$emit("update:history", this.tempHistory);
this.editing = false;
}
},
revert() {
if (this.dirty) {
this.reset();
}
},
revertAndCancel() {
this.revert();
this.editing = false;
},
},
};
</script>

Some files were not shown because too many files have changed in this diff Show More