mirror of
https://github.com/galaxyproject/galaxy.git
synced 2026-09-24 16:30:27 +08:00
Merge remote-tracking branch 'upstream/dev' into dnd-upload
This commit is contained in:
@@ -0,0 +1 @@
|
||||
hda
|
||||
@@ -0,0 +1,60 @@
|
||||
name: OpenAPI linting
|
||||
on:
|
||||
push:
|
||||
paths-ignore:
|
||||
- 'client/**'
|
||||
- 'doc/**'
|
||||
- 'lib/galaxy_test/selenium/**'
|
||||
pull_request:
|
||||
paths-ignore:
|
||||
- 'client/**'
|
||||
- 'doc/**'
|
||||
- 'lib/galaxy_test/selenium/**'
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
jobs:
|
||||
validate-schema:
|
||||
name: Validate OpenAPI schema
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version: ['3.7']
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
path: 'galaxy root'
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Get full Python version
|
||||
id: full-python-version
|
||||
shell: bash
|
||||
run: echo "version=$(python -c 'import sys; print("-".join(str(v) for v in sys.version_info))')" >> $GITHUB_OUTPUT
|
||||
- name: Cache pip dir
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
key: pip-cache-${{ matrix.python-version }}-${{ hashFiles('galaxy root/requirements.txt') }}
|
||||
- name: Cache galaxy venv
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: 'galaxy root/.venv'
|
||||
key: gxy-venv-${{ runner.os }}-${{ steps.full-python-version.outputs.version }}-${{ hashFiles('galaxy root/requirements.txt') }}-api
|
||||
- name: Install dependencies
|
||||
run: ./scripts/common_startup.sh --skip-client-build
|
||||
working-directory: 'galaxy root'
|
||||
- name: Lint schema
|
||||
run: make lint-api-schema
|
||||
working-directory: 'galaxy root'
|
||||
- name: Build typescript schema
|
||||
run: make update-client-api-schema
|
||||
working-directory: 'galaxy root'
|
||||
- name: Check for changes
|
||||
run: |
|
||||
if [[ `git status --porcelain` ]]; then
|
||||
echo "Rebuilding client/src/schema/schema.ts resulted in changes, run 'make update-client-api-schema' and commit results"
|
||||
exit 1
|
||||
fi
|
||||
working-directory: 'galaxy root'
|
||||
@@ -0,0 +1,16 @@
|
||||
# This file instructs Redocly's linter to ignore the rules contained for specific parts of your API.
|
||||
# See https://redoc.ly/docs/cli/ for more information.
|
||||
_schema.yaml:
|
||||
no-empty-servers:
|
||||
- '#/openapi'
|
||||
no-ambiguous-paths:
|
||||
- >-
|
||||
#/paths/~1api~1histories~1{history_id}~1contents~1{dataset_id}~1permissions
|
||||
- >-
|
||||
#/paths/~1api~1histories~1{history_id}~1contents~1{history_content_id}~1display
|
||||
- >-
|
||||
#/paths/~1api~1histories~1{history_id}~1contents~1{history_content_id}~1extra_files
|
||||
- >-
|
||||
#/paths/~1api~1histories~1{history_id}~1contents~1{history_content_id}~1metadata_file
|
||||
- '#/paths/~1api~1histories~1{history_id}~1contents~1{id}~1validate'
|
||||
- '#/paths/~1api~1histories~1{history_id}~1contents~1{type}s~1{id}'
|
||||
@@ -0,0 +1,5 @@
|
||||
organization: galaxyproject.org
|
||||
extends:
|
||||
- recommended
|
||||
rules:
|
||||
operation-4xx-response: off
|
||||
+1
-1
@@ -11,7 +11,7 @@ The following individuals have contributed code to Galaxy:
|
||||
* Patrick Austin <patrick.austin@stfc.ac.uk>
|
||||
* Raj Ayyampalayam <raj76@uga.edu>
|
||||
* Abdulrahman Azab <eng.azab@gmail.com>
|
||||
* Finn Bacall <finn.bacall@cs.man.ac.uk>
|
||||
* Finn Bacall <finn.bacall@manchester.ac.uk>
|
||||
* Dannon Baker <dannon.baker@gmail.com>
|
||||
* balto <balto_59@hotmail.fr>
|
||||
* Christopher Bare <christopherbare@gmail.com>
|
||||
|
||||
@@ -170,8 +170,20 @@ else
|
||||
endif
|
||||
|
||||
|
||||
update-client-api-schema: node-deps
|
||||
$(IN_VENV) cd client && python ../scripts/dump_openapi_schema.py | yarn run openapi-typescript --output src/schema/schema.ts && npx prettier --write src/schema/schema.ts
|
||||
build-api-schema:
|
||||
$(IN_VENV) python scripts/dump_openapi_schema.py _schema.yaml
|
||||
|
||||
remove-api-schema:
|
||||
rm _schema.yaml
|
||||
|
||||
update-client-api-schema: node-deps build-api-schema
|
||||
$(IN_VENV) cd client && node openapi_to_schema.mjs ../_schema.yaml > src/schema/schema.ts && npx prettier --write src/schema/schema.ts
|
||||
$(MAKE) remove-api-schema
|
||||
|
||||
lint-api-schema: build-api-schema
|
||||
$(IN_VENV) npx --yes @redocly/cli lint _schema.yaml
|
||||
$(IN_VENV) codespell -I .ci/ignore-spelling.txt _schema.yaml
|
||||
$(MAKE) remove-api-schema
|
||||
|
||||
client: node-deps ## Rebuild client-side artifacts for local development.
|
||||
cd client && yarn run build
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
// this is a helper script that fixes const values
|
||||
// upstream fix in https://github.com/drwpow/openapi-typescript/pull/1014
|
||||
import openapiTS from "openapi-typescript";
|
||||
|
||||
const inputFilePath = process.argv[2];
|
||||
|
||||
const localPath = new URL(inputFilePath, import.meta.url);
|
||||
openapiTS(localPath, {
|
||||
transform(schemaObject, metadata) {
|
||||
if ("const" in schemaObject && schemaObject.type === "string") {
|
||||
return `"${schemaObject.const}"`;
|
||||
}
|
||||
},
|
||||
}).then((output) => console.log(output));
|
||||
+4
-3
@@ -29,7 +29,7 @@
|
||||
"@fortawesome/free-regular-svg-icons": "^5.15.4",
|
||||
"@fortawesome/free-solid-svg-icons": "^5.15.4",
|
||||
"@fortawesome/vue-fontawesome": "^2.0.9",
|
||||
"@handsontable/vue": "^2.0.0-beta1",
|
||||
"@handsontable/vue": "^2.0.0",
|
||||
"@hirez_io/observer-spy": "^2.1.2",
|
||||
"@johmun/vue-tags-input": "^2.1.0",
|
||||
"@popperjs/core": "^2.11.5",
|
||||
@@ -54,7 +54,7 @@
|
||||
"file-saver": "^2.0.5",
|
||||
"flush-promises": "^1.0.2",
|
||||
"glob": "^7.2.0",
|
||||
"handsontable": "^2.0.0",
|
||||
"handsontable": "^4.0.0",
|
||||
"hsluv": "^0.1.0",
|
||||
"imask": "^6.4.0",
|
||||
"jquery": "2",
|
||||
@@ -97,7 +97,7 @@
|
||||
"vue-prismjs": "^1.2.0",
|
||||
"vue-router": "^3.6.5",
|
||||
"vue-rx": "^6.2.0",
|
||||
"vue-virtual-scroll-list": "^2.3.3",
|
||||
"vue-virtual-scroll-list": "^2.3.4",
|
||||
"vuedraggable": "2.24.3",
|
||||
"vuex": "^3.6.2",
|
||||
"vuex-cache": "^3.4.0",
|
||||
@@ -180,6 +180,7 @@
|
||||
"style-loader": "^3.3.1",
|
||||
"ts-jest": "^29.0.3",
|
||||
"ts-loader": "^9.4.1",
|
||||
"tsconfig-paths-webpack-plugin": "^4.0.0",
|
||||
"typescript": "^4.9.3",
|
||||
"typescript-eslint": "^0.0.1-alpha.0",
|
||||
"vue-loader": "^15.10.0",
|
||||
|
||||
@@ -5,7 +5,7 @@ import BASE_MVC from "./base-mvc";
|
||||
import userModel from "./user-model";
|
||||
import metricsLogger from "utils/metrics-logger";
|
||||
import addLogging from "utils/add-logging";
|
||||
import localize from "utils/localization";
|
||||
import { localize, _setUserLocale, _getUserLocale } from "utils/localization";
|
||||
import { getGalaxyInstance } from "app";
|
||||
import { create, dialog } from "utils/data";
|
||||
|
||||
@@ -77,8 +77,8 @@ GalaxyApp.prototype._init = function (options, bootstrapped) {
|
||||
this._initUser(options.user || {});
|
||||
this.debug("GalaxyApp.user: ", this.user);
|
||||
|
||||
this.localize._setUserLocale(this.user, this.config);
|
||||
this.localize._getUserLocale();
|
||||
_setUserLocale(this.user, this.config);
|
||||
_getUserLocale();
|
||||
this.debug("currentLocale: ", sessionStorage.getItem("currentLocale"));
|
||||
|
||||
this._setUpListeners();
|
||||
|
||||
@@ -4,11 +4,11 @@
|
||||
|
||||
import { computed } from "vue";
|
||||
|
||||
import { getAppRoot } from "onload/loadConfig";
|
||||
import { useConfig } from "composables/config";
|
||||
import UtcDate from "components/UtcDate.vue";
|
||||
import License from "components/License/License.vue";
|
||||
import ExternalLink from "components/ExternalLink.vue";
|
||||
import { getAppRoot } from "@/onload/loadConfig";
|
||||
import { useConfig } from "@/composables/config";
|
||||
import UtcDate from "@/components/UtcDate.vue";
|
||||
import License from "@/components/License/License.vue";
|
||||
import ExternalLink from "@/components/ExternalLink.vue";
|
||||
|
||||
const { config, isLoaded } = useConfig();
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import { useDetailedDatatypes } from "composables/datatypes";
|
||||
import { useFilterObjectArray } from "composables/utils/filter";
|
||||
import DelayedInput from "components/Common/DelayedInput.vue";
|
||||
import { useDetailedDatatypes, type DetailedDatatypes } from "@/composables/datatypes";
|
||||
import { useFilterObjectArray } from "@/composables/utils/filter";
|
||||
import DelayedInput from "@/components/Common/DelayedInput.vue";
|
||||
|
||||
const filter = ref("");
|
||||
const filterFields = ["extension"];
|
||||
const filterFields: Array<keyof DetailedDatatypes> = ["extension"];
|
||||
|
||||
const { datatypes } = useDetailedDatatypes();
|
||||
const filteredDatatypes = useFilterObjectArray(datatypes, filter, filterFields);
|
||||
|
||||
@@ -35,4 +35,35 @@ describe("ExportForm.vue", () => {
|
||||
it("should localize button text", async () => {
|
||||
expect(wrapper.find(".export-button").text()).toBeLocalizationOf("Export");
|
||||
});
|
||||
|
||||
it("should emit 'export' event with correct inputs on export button click", async () => {
|
||||
await wrapper.setData({
|
||||
name: "export.tar.gz",
|
||||
directory: "gxfiles://",
|
||||
});
|
||||
expect(wrapper.emitted()).not.toHaveProperty("export");
|
||||
|
||||
await wrapper.find(".export-button").trigger("click");
|
||||
|
||||
expect(wrapper.emitted()).toHaveProperty("export");
|
||||
expect(wrapper.emitted()["export"][0][0]).toBe("gxfiles://");
|
||||
expect(wrapper.emitted()["export"][0][1]).toBe("export.tar.gz");
|
||||
});
|
||||
|
||||
it("should clear the inputs after export when clearInputAfterExport is enabled", async () => {
|
||||
await wrapper.setProps({
|
||||
clearInputAfterExport: true,
|
||||
});
|
||||
await wrapper.setData({
|
||||
name: "export.tar.gz",
|
||||
directory: "gxfiles://",
|
||||
});
|
||||
expect(wrapper.vm.directory).toEqual("gxfiles://");
|
||||
expect(wrapper.vm.name).toEqual("export.tar.gz");
|
||||
|
||||
await wrapper.find(".export-button").trigger("click");
|
||||
|
||||
expect(wrapper.vm.directory).toBe(null);
|
||||
expect(wrapper.vm.name).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="export-to-remote-file">
|
||||
<b-form-group
|
||||
id="fieldset-directory"
|
||||
label-for="directory"
|
||||
@@ -32,6 +32,10 @@ export default {
|
||||
type: String,
|
||||
default: "archive",
|
||||
},
|
||||
clearInputAfterExport: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -59,6 +63,10 @@ export default {
|
||||
methods: {
|
||||
doExport() {
|
||||
this.$emit("export", this.directory, this.name);
|
||||
if (this.clearInputAfterExport) {
|
||||
this.directory = null;
|
||||
this.name = null;
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
<script setup>
|
||||
import { computed } from "vue";
|
||||
import { BAlert, BCard, BCardTitle } from "bootstrap-vue";
|
||||
import LoadingSpan from "components/LoadingSpan";
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import { library } from "@fortawesome/fontawesome-svg-core";
|
||||
import { faExclamationCircle, faExclamationTriangle, faCheckCircle, faClock } from "@fortawesome/free-solid-svg-icons";
|
||||
import { ExportRecordModel } from "./models/exportRecordModel";
|
||||
|
||||
library.add(faExclamationCircle, faExclamationTriangle, faCheckCircle, faClock);
|
||||
|
||||
const props = defineProps({
|
||||
record: {
|
||||
type: ExportRecordModel,
|
||||
required: true,
|
||||
},
|
||||
objectType: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
actionMessage: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
actionMessageVariant: {
|
||||
type: String,
|
||||
default: "info",
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["onReimport", "onDownload", "onActionMessageDismissed"]);
|
||||
|
||||
const title = computed(() => (props.record.isReady ? `Exported` : `Export started`));
|
||||
const preparingMessage = computed(
|
||||
() => `Preparing export. This may take some time depending on the size of your ${props.objectType}`
|
||||
);
|
||||
|
||||
async function reimportObject() {
|
||||
emit("onReimport", props.record);
|
||||
}
|
||||
|
||||
function downloadObject() {
|
||||
emit("onDownload", props.record);
|
||||
}
|
||||
|
||||
function onMessageDismissed() {
|
||||
emit("onActionMessageDismissed");
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<b-card class="export-record-details">
|
||||
<b-card-title>
|
||||
<b>{{ title }}</b> {{ props.record.elapsedTime }}
|
||||
</b-card-title>
|
||||
<p v-if="!props.record.isPreparing">
|
||||
Format: <b class="record-archive-format">{{ props.record.modelStoreFormat }}</b>
|
||||
</p>
|
||||
<span v-if="props.record.isPreparing">
|
||||
<loading-span :message="preparingMessage" />
|
||||
</span>
|
||||
<div v-else>
|
||||
<div v-if="props.record.hasFailed">
|
||||
<font-awesome-icon
|
||||
icon="exclamation-circle"
|
||||
class="text-danger record-failed-icon"
|
||||
title="Export failed" />
|
||||
<span>
|
||||
Something failed during this export. Please try again and if the problem persist contact your
|
||||
administrator.
|
||||
</span>
|
||||
<b-alert show variant="danger">{{ props.record.errorMessage }}</b-alert>
|
||||
</div>
|
||||
<div v-else-if="props.record.isUpToDate" title="Up to date">
|
||||
<font-awesome-icon icon="check-circle" class="text-success record-up-to-date-icon" />
|
||||
<span> This export record contains the latest changes of the {{ props.objectType }}. </span>
|
||||
</div>
|
||||
<div v-else>
|
||||
<font-awesome-icon icon="exclamation-triangle" class="text-warning record-outdated-icon" />
|
||||
<span>
|
||||
This export is outdated and contains the changes of this {{ props.objectType }} from
|
||||
{{ props.record.elapsedTime }}.
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p v-if="props.record.canExpire" class="mt-3">
|
||||
<span v-if="props.record.hasExpired">
|
||||
<font-awesome-icon icon="clock" class="text-danger record-expired-icon" /> This download link has
|
||||
expired.
|
||||
</span>
|
||||
<span v-else>
|
||||
<font-awesome-icon icon="clock" class="text-warning record-expiration-warning-icon" /> This download
|
||||
link expires {{ props.record.expirationElapsedTime }}.
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<div v-if="props.record.isReady">
|
||||
<p class="mt-3">You can do the following actions with this {{ props.objectType }} export:</p>
|
||||
<b-alert
|
||||
v-if="props.actionMessage !== null"
|
||||
:variant="props.actionMessageVariant"
|
||||
show
|
||||
fade
|
||||
dismissible
|
||||
@dismissed="onMessageDismissed">
|
||||
{{ props.actionMessage }}
|
||||
</b-alert>
|
||||
<div v-else class="actions">
|
||||
<b-button
|
||||
v-if="props.record.canDownload"
|
||||
class="record-download-btn"
|
||||
variant="primary"
|
||||
@click="downloadObject">
|
||||
Download
|
||||
</b-button>
|
||||
<b-button
|
||||
v-if="props.record.canReimport"
|
||||
class="record-reimport-btn"
|
||||
variant="primary"
|
||||
@click="reimportObject">
|
||||
Reimport
|
||||
</b-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</b-card>
|
||||
</template>
|
||||
@@ -0,0 +1,130 @@
|
||||
<script setup>
|
||||
import { computed, ref } from "vue";
|
||||
import { BCard, BButton, BButtonGroup, BButtonToolbar, BCollapse, BTable, BLink } from "bootstrap-vue";
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import { library } from "@fortawesome/fontawesome-svg-core";
|
||||
import {
|
||||
faExclamationCircle,
|
||||
faCheckCircle,
|
||||
faDownload,
|
||||
faFileImport,
|
||||
faSpinner,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
library.add(faExclamationCircle, faCheckCircle, faDownload, faFileImport, faSpinner);
|
||||
|
||||
const props = defineProps({
|
||||
records: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["onReimport", "onDownload"]);
|
||||
|
||||
const fields = [
|
||||
{ key: "elapsedTime", label: "Exported" },
|
||||
{ key: "format", label: "Format" },
|
||||
{ key: "expires", label: "Expires" },
|
||||
{ key: "isUpToDate", label: "Up to date", class: "text-center" },
|
||||
{ key: "isReady", label: "Ready", class: "text-center" },
|
||||
{ key: "actions", label: "Actions" },
|
||||
];
|
||||
|
||||
const isExpanded = ref(false);
|
||||
const title = computed(() => (isExpanded.value ? `Hide export records` : `Show export records`));
|
||||
|
||||
async function reimportObject(record) {
|
||||
emit("onReimport", record);
|
||||
}
|
||||
|
||||
function downloadObject(record) {
|
||||
emit("onDownload", record);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<b-link
|
||||
:class="isExpanded ? null : 'collapsed'"
|
||||
:aria-expanded="isExpanded ? 'true' : 'false'"
|
||||
aria-controls="collapse-previous"
|
||||
@click="isExpanded = !isExpanded">
|
||||
{{ title }}
|
||||
</b-link>
|
||||
<b-collapse id="collapse-previous" v-model="isExpanded">
|
||||
<b-card>
|
||||
<b-table :items="props.records" :fields="fields">
|
||||
<template v-slot:cell(elapsedTime)="row">
|
||||
<span :title="row.item.date">{{ row.value }}</span>
|
||||
</template>
|
||||
<template v-slot:cell(format)="row">
|
||||
<span>{{ row.item.modelStoreFormat }}</span>
|
||||
</template>
|
||||
<template v-slot:cell(expires)="row">
|
||||
<span v-if="row.item.hasExpired">Expired</span>
|
||||
<span v-else-if="row.item.expirationDate" :title="row.item.expirationDate">{{
|
||||
row.item.expirationElapsedTime
|
||||
}}</span>
|
||||
<span v-else>No</span>
|
||||
</template>
|
||||
<template v-slot:cell(isUpToDate)="row">
|
||||
<font-awesome-icon
|
||||
v-if="row.item.isUpToDate"
|
||||
icon="check-circle"
|
||||
class="text-success"
|
||||
title="This export record contains the latest changes." />
|
||||
<font-awesome-icon
|
||||
v-else
|
||||
icon="exclamation-circle"
|
||||
class="text-danger"
|
||||
title="This export record is outdated. Please consider generating a new export if you need the latest changes." />
|
||||
</template>
|
||||
<template v-slot:cell(isReady)="row">
|
||||
<font-awesome-icon
|
||||
v-if="row.item.isReady"
|
||||
icon="check-circle"
|
||||
class="text-success"
|
||||
title="Ready to download or import." />
|
||||
<font-awesome-icon
|
||||
v-else-if="row.item.isPreparing"
|
||||
icon="spinner"
|
||||
spin
|
||||
class="text-info"
|
||||
title="Exporting in progress..." />
|
||||
<font-awesome-icon
|
||||
v-else-if="row.item.hasExpired"
|
||||
icon="exclamation-circle"
|
||||
class="text-danger"
|
||||
title="The export has expired." />
|
||||
<font-awesome-icon
|
||||
v-else
|
||||
icon="exclamation-circle"
|
||||
class="text-danger"
|
||||
title="The export failed." />
|
||||
</template>
|
||||
<template v-slot:cell(actions)="row">
|
||||
<b-button-toolbar aria-label="Actions">
|
||||
<b-button-group>
|
||||
<b-button
|
||||
v-b-tooltip.hover.bottom
|
||||
:disabled="!row.item.canDownload"
|
||||
title="Download"
|
||||
@click="downloadObject(row.item)">
|
||||
<font-awesome-icon icon="download" />
|
||||
</b-button>
|
||||
<b-button
|
||||
v-b-tooltip.hover.bottom
|
||||
:disabled="!row.item.canReimport"
|
||||
title="Reimport"
|
||||
@click="reimportObject(row.item)">
|
||||
<font-awesome-icon icon="file-import" />
|
||||
</b-button>
|
||||
</b-button-group>
|
||||
</b-button-toolbar>
|
||||
</template>
|
||||
</b-table>
|
||||
</b-card>
|
||||
</b-collapse>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,61 @@
|
||||
import { ExportRecordModel } from "./exportRecordModel";
|
||||
import {
|
||||
EXPECTED_EXPIRATION_DATE,
|
||||
EXPIRED_STS_DOWNLOAD_RESPONSE,
|
||||
FAILED_DOWNLOAD_RESPONSE,
|
||||
FILE_SOURCE_STORE_RESPONSE,
|
||||
RECENT_STS_DOWNLOAD_RESPONSE,
|
||||
} from "./testData/exportData";
|
||||
|
||||
describe("ExportRecordModel", () => {
|
||||
describe("STS Download Record", () => {
|
||||
const stsDownloadRecord = new ExportRecordModel(RECENT_STS_DOWNLOAD_RESPONSE);
|
||||
|
||||
it("should be considered temporal (STS) when it has a short term storage ID defined", () => {
|
||||
expect(stsDownloadRecord.isStsDownload).toBe(true);
|
||||
expect(stsDownloadRecord.stsDownloadId).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should allow download when ready and not yet expired", () => {
|
||||
expect(stsDownloadRecord.isReady).toBe(true);
|
||||
expect(stsDownloadRecord.hasExpired).toBe(false);
|
||||
expect(stsDownloadRecord.canDownload).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Expired STS Download Record", () => {
|
||||
const expiredDownloadRecord = new ExportRecordModel(EXPIRED_STS_DOWNLOAD_RESPONSE);
|
||||
|
||||
it("should calculate the correct expiration date", () => {
|
||||
expect(expiredDownloadRecord.canExpire).toBe(true);
|
||||
expect(expiredDownloadRecord.expirationDate).toStrictEqual(EXPECTED_EXPIRATION_DATE());
|
||||
});
|
||||
|
||||
it("should not allow download when expired", () => {
|
||||
expect(expiredDownloadRecord.hasExpired).toBe(true);
|
||||
expect(expiredDownloadRecord.canDownload).toBe(false);
|
||||
expect(expiredDownloadRecord.isReady).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Failed STS Download Record", () => {
|
||||
const failedDownloadRecord = new ExportRecordModel(FAILED_DOWNLOAD_RESPONSE);
|
||||
|
||||
it("should not be downloadable", () => {
|
||||
expect(failedDownloadRecord.isReady).toBe(false);
|
||||
expect(failedDownloadRecord.isPreparing).toBe(false);
|
||||
expect(failedDownloadRecord.hasExpired).toBe(false);
|
||||
expect(failedDownloadRecord.canDownload).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("File Source Storage Record", () => {
|
||||
const failedDownloadRecord = new ExportRecordModel(FILE_SOURCE_STORE_RESPONSE);
|
||||
|
||||
it("should be importable", () => {
|
||||
expect(failedDownloadRecord.isReady).toBe(true);
|
||||
expect(failedDownloadRecord.canReimport).toBe(true);
|
||||
expect(failedDownloadRecord.importUri).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,148 @@
|
||||
import { formatDistanceToNow, parseISO } from "date-fns";
|
||||
import type { components } from "@/schema";
|
||||
|
||||
type ExportObjectRequestMetadata = components["schemas"]["ExportObjectRequestMetadata"];
|
||||
|
||||
export type StoreExportPayload = components["schemas"]["StoreExportPayload"];
|
||||
export type ObjectExportTaskResponse = components["schemas"]["ObjectExportTaskResponse"];
|
||||
|
||||
export class ExportParamsModel {
|
||||
private _params: StoreExportPayload;
|
||||
constructor(data: StoreExportPayload = {}) {
|
||||
this._params = data;
|
||||
}
|
||||
|
||||
get modelStoreFormat() {
|
||||
return this._params?.model_store_format;
|
||||
}
|
||||
|
||||
get includeFiles() {
|
||||
return this._params?.include_files;
|
||||
}
|
||||
|
||||
get includeDeleted() {
|
||||
return this._params?.include_deleted;
|
||||
}
|
||||
|
||||
get includeHidden() {
|
||||
return this._params?.include_hidden;
|
||||
}
|
||||
|
||||
public equals(otherExportParams?: ExportParamsModel) {
|
||||
if (!otherExportParams) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
this.modelStoreFormat === otherExportParams.modelStoreFormat &&
|
||||
this.includeFiles === otherExportParams.includeFiles &&
|
||||
this.includeDeleted === otherExportParams.includeDeleted &&
|
||||
this.includeHidden === otherExportParams.includeHidden
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class ExportRecordModel {
|
||||
private _data: ObjectExportTaskResponse;
|
||||
private _expirationDate?: Date | null;
|
||||
private _requestMetadata?: ExportObjectRequestMetadata;
|
||||
private _exportParameters?: ExportParamsModel;
|
||||
|
||||
constructor(data: ObjectExportTaskResponse) {
|
||||
this._data = data;
|
||||
this._expirationDate = undefined;
|
||||
this._requestMetadata = data.export_metadata?.request_data;
|
||||
this._exportParameters = this._requestMetadata?.payload
|
||||
? new ExportParamsModel(this._requestMetadata?.payload)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
get isReady() {
|
||||
return (this._data.ready && !this.hasExpired) ?? false;
|
||||
}
|
||||
|
||||
get isPreparing() {
|
||||
return this._data.preparing ?? false;
|
||||
}
|
||||
|
||||
get isUpToDate() {
|
||||
return this._data.up_to_date ?? false;
|
||||
}
|
||||
|
||||
get hasFailed() {
|
||||
return !this.isReady && !this.isPreparing && !this.hasExpired;
|
||||
}
|
||||
|
||||
get date() {
|
||||
return parseISO(`${this._data.create_time}Z`);
|
||||
}
|
||||
|
||||
get elapsedTime() {
|
||||
return formatDistanceToNow(this.date, { addSuffix: true });
|
||||
}
|
||||
|
||||
get taskUUID() {
|
||||
return this._data.task_uuid;
|
||||
}
|
||||
|
||||
get importUri() {
|
||||
const payload = this._requestMetadata?.payload;
|
||||
return payload && "target_uri" in payload ? payload.target_uri : undefined;
|
||||
}
|
||||
|
||||
get canReimport() {
|
||||
return this.isReady && Boolean(this.importUri);
|
||||
}
|
||||
|
||||
get stsDownloadId() {
|
||||
const payload = this._requestMetadata?.payload;
|
||||
return payload && "short_term_storage_request_id" in payload
|
||||
? payload.short_term_storage_request_id
|
||||
: undefined;
|
||||
}
|
||||
|
||||
get isStsDownload() {
|
||||
return Boolean(this.stsDownloadId);
|
||||
}
|
||||
|
||||
get canDownload() {
|
||||
return this.isReady && this.isStsDownload && !this.hasExpired;
|
||||
}
|
||||
|
||||
get modelStoreFormat() {
|
||||
return this.exportParams?.modelStoreFormat;
|
||||
}
|
||||
|
||||
get exportParams() {
|
||||
return this._exportParameters;
|
||||
}
|
||||
|
||||
get duration() {
|
||||
const payload = this._requestMetadata?.payload;
|
||||
return payload && "duration" in payload ? payload.duration : undefined;
|
||||
}
|
||||
|
||||
get canExpire() {
|
||||
return this.isStsDownload && Boolean(this.duration);
|
||||
}
|
||||
|
||||
get expirationDate() {
|
||||
if (this._expirationDate === undefined) {
|
||||
this._expirationDate = this.duration ? new Date(this.date.getTime() + this.duration * 1000) : null;
|
||||
}
|
||||
return this._expirationDate;
|
||||
}
|
||||
|
||||
get expirationElapsedTime() {
|
||||
return this.canExpire && this.expirationDate
|
||||
? formatDistanceToNow(this.expirationDate, { addSuffix: true })
|
||||
: null;
|
||||
}
|
||||
|
||||
get hasExpired() {
|
||||
return this.canExpire && this.expirationDate && Date.now() > this.expirationDate.getTime();
|
||||
}
|
||||
|
||||
get errorMessage() {
|
||||
return this._data?.export_metadata?.result_data?.error;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
import { ExportRecordModel } from "@/components/Common/models/exportRecordModel";
|
||||
import type { components } from "@/schema";
|
||||
|
||||
type ObjectExportTaskResponse = components["schemas"]["ObjectExportTaskResponse"];
|
||||
type ExportObjectRequestMetadata = components["schemas"]["ExportObjectRequestMetadata"];
|
||||
type ExportObjectResultMetadata = components["schemas"]["ExportObjectResultMetadata"];
|
||||
|
||||
const PAST_EXPORT_DATE = new Date("11 November 2022 14:48 UTC").toISOString();
|
||||
const RECENT_EXPORT_DATE = new Date().toISOString();
|
||||
const STS_EXPORT_DURATION_IN_SECONDS = 86400;
|
||||
export const EXPECTED_EXPIRATION_DATE = () => {
|
||||
const expectedDate = new Date(PAST_EXPORT_DATE);
|
||||
expectedDate.setSeconds(expectedDate.getSeconds() + STS_EXPORT_DURATION_IN_SECONDS);
|
||||
return expectedDate;
|
||||
};
|
||||
|
||||
const FAKE_STS_DOWNLOAD_REQUEST_DATA: ExportObjectRequestMetadata = {
|
||||
object_id: "3cc0effd29705aa3",
|
||||
object_type: "history",
|
||||
user_id: "f597429621d6eb2b",
|
||||
payload: {
|
||||
model_store_format: "rocrate.zip",
|
||||
include_files: true,
|
||||
include_deleted: false,
|
||||
include_hidden: false,
|
||||
short_term_storage_request_id: "08bf4cc3-758e-4a9d-9fe4-a89a0d0604c7",
|
||||
duration: STS_EXPORT_DURATION_IN_SECONDS,
|
||||
},
|
||||
};
|
||||
|
||||
const FAKE_FILE_SOURCE_REQUEST_DATA: ExportObjectRequestMetadata = {
|
||||
object_id: "3cc0effd29705aa3",
|
||||
object_type: "history",
|
||||
user_id: "f597429621d6eb2b",
|
||||
payload: {
|
||||
model_store_format: "tar.gz",
|
||||
include_files: true,
|
||||
include_deleted: false,
|
||||
include_hidden: false,
|
||||
target_uri: "gxfiles://fake-target-uri/test.tar.gz",
|
||||
},
|
||||
};
|
||||
|
||||
const SUCCESS_EXPORT_RESULT_DATA: ExportObjectResultMetadata = {
|
||||
success: true,
|
||||
error: undefined,
|
||||
};
|
||||
|
||||
const FAILED_EXPORT_RESULT_DATA: ExportObjectResultMetadata = {
|
||||
success: false,
|
||||
error: "Fake Error Message",
|
||||
};
|
||||
|
||||
export const RECENT_STS_DOWNLOAD_RESPONSE: ObjectExportTaskResponse = {
|
||||
id: "FAKE_RECENT_DOWNLOAD_ID",
|
||||
ready: true,
|
||||
preparing: false,
|
||||
up_to_date: true,
|
||||
task_uuid: "35563335-e275-4520-80e8-885793279095",
|
||||
create_time: RECENT_EXPORT_DATE,
|
||||
export_metadata: {
|
||||
request_data: FAKE_STS_DOWNLOAD_REQUEST_DATA,
|
||||
result_data: SUCCESS_EXPORT_RESULT_DATA,
|
||||
},
|
||||
};
|
||||
|
||||
export const EXPIRED_STS_DOWNLOAD_RESPONSE: ObjectExportTaskResponse = {
|
||||
id: "FAKE_EXPIRED_DOWNLOAD_ID",
|
||||
ready: true,
|
||||
preparing: false,
|
||||
up_to_date: true,
|
||||
task_uuid: "35563335-e275-4520-80e8-885793279095",
|
||||
create_time: PAST_EXPORT_DATE,
|
||||
export_metadata: {
|
||||
request_data: FAKE_STS_DOWNLOAD_REQUEST_DATA,
|
||||
result_data: SUCCESS_EXPORT_RESULT_DATA,
|
||||
},
|
||||
};
|
||||
|
||||
export const FAILED_DOWNLOAD_RESPONSE: ObjectExportTaskResponse = {
|
||||
id: "FAKE_FAILED_DOWNLOAD_ID",
|
||||
ready: false,
|
||||
preparing: false,
|
||||
up_to_date: true,
|
||||
task_uuid: "35563335-e275-4520-80e8-885793279095",
|
||||
create_time: RECENT_EXPORT_DATE,
|
||||
export_metadata: {
|
||||
request_data: FAKE_STS_DOWNLOAD_REQUEST_DATA,
|
||||
result_data: FAILED_EXPORT_RESULT_DATA,
|
||||
},
|
||||
};
|
||||
|
||||
export const FILE_SOURCE_STORE_RESPONSE: ObjectExportTaskResponse = {
|
||||
id: "FAKE_RECENT_DOWNLOAD_ID",
|
||||
ready: true,
|
||||
preparing: false,
|
||||
up_to_date: true,
|
||||
task_uuid: "35563335-e275-4520-80e8-885793279095",
|
||||
create_time: RECENT_EXPORT_DATE,
|
||||
export_metadata: {
|
||||
request_data: FAKE_FILE_SOURCE_REQUEST_DATA,
|
||||
result_data: SUCCESS_EXPORT_RESULT_DATA,
|
||||
},
|
||||
};
|
||||
|
||||
export const EXPIRED_STS_DOWNLOAD_RECORD = new ExportRecordModel(EXPIRED_STS_DOWNLOAD_RESPONSE);
|
||||
export const FILE_SOURCE_STORE_RECORD = new ExportRecordModel(FILE_SOURCE_STORE_RESPONSE);
|
||||
export const RECENT_STS_DOWNLOAD_RECORD = new ExportRecordModel(RECENT_STS_DOWNLOAD_RESPONSE);
|
||||
@@ -1,6 +1,6 @@
|
||||
import { fetcher } from "schema";
|
||||
import { safePath } from "utils/redirect";
|
||||
import type { FetchArgType } from "openapi-typescript-fetch";
|
||||
import { fetcher } from "@/schema";
|
||||
import { safePath } from "@/utils/redirect";
|
||||
|
||||
const _getDatasets = fetcher.path("/api/datasets").method("get").create();
|
||||
type GetDatasetsApiOptions = FetchArgType<typeof _getDatasets>;
|
||||
|
||||
@@ -2,6 +2,7 @@ import { mount } from "@vue/test-utils";
|
||||
import { getLocalVue } from "tests/jest/helpers";
|
||||
import DatasetError from "./DatasetError";
|
||||
import MockProvider from "../providers/MockProvider";
|
||||
import MockCurrentUser from "../providers/MockCurrentUser";
|
||||
|
||||
jest.mock("components/providers", () => {
|
||||
return {}; // stubbed below
|
||||
@@ -33,6 +34,7 @@ function buildWrapper(has_duplicate_inputs = true, has_empty_inputs = true, user
|
||||
}),
|
||||
FontAwesomeIcon: false,
|
||||
FormElement: false,
|
||||
CurrentUser: MockCurrentUser({ email: "email" }),
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -48,7 +50,6 @@ describe("DatasetError", () => {
|
||||
expect(messages.at(1).text()).toBe("message_2");
|
||||
expect(wrapper.find("#dataset-error-has-empty-inputs")).toBeDefined();
|
||||
expect(wrapper.find("#dataset-error-has-duplicate-inputs")).toBeDefined();
|
||||
expect(wrapper.findAll("#dataset-error-email").length).toBe(1);
|
||||
});
|
||||
|
||||
it("check props without common problems", async () => {
|
||||
|
||||
@@ -61,25 +61,25 @@
|
||||
show
|
||||
>{{ resultMessage[0] }}</b-alert
|
||||
>
|
||||
<div v-if="showForm" id="fieldsAndButton">
|
||||
<FormElement
|
||||
v-if="!jobDetails.user_email"
|
||||
id="dataset-error-email"
|
||||
v-model="email"
|
||||
title="Please provide your email:" />
|
||||
<FormElement
|
||||
id="dataset-error-message"
|
||||
v-model="message"
|
||||
:area="true"
|
||||
title="Please provide detailed information on the activities leading to this issue:" />
|
||||
<b-button
|
||||
id="dataset-error-submit"
|
||||
variant="primary"
|
||||
class="mt-3"
|
||||
@click="submit(dataset, jobDetails.user_email)">
|
||||
<font-awesome-icon icon="bug" class="mr-1" />Report
|
||||
</b-button>
|
||||
</div>
|
||||
<CurrentUser v-slot="{ user }">
|
||||
<div v-if="showForm" id="fieldsAndButton">
|
||||
<span class="mr-2 font-weight-bold">{{ emailTitle }}</span>
|
||||
<span v-if="!!user.email">{{ user.email }}</span>
|
||||
<span v-else>{{ "You must be logged in to receive emails" | l }}</span>
|
||||
<FormElement
|
||||
id="dataset-error-message"
|
||||
v-model="message"
|
||||
:area="true"
|
||||
title="Please provide detailed information on the activities leading to this issue:" />
|
||||
<b-button
|
||||
id="dataset-error-submit"
|
||||
variant="primary"
|
||||
class="mt-3"
|
||||
@click="submit(dataset, jobDetails.user_email)">
|
||||
<font-awesome-icon icon="bug" class="mr-1" />Report
|
||||
</b-button>
|
||||
</div>
|
||||
</CurrentUser>
|
||||
</div>
|
||||
</JobDetailsProvider>
|
||||
</DatasetProvider>
|
||||
@@ -95,6 +95,7 @@ import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import { library } from "@fortawesome/fontawesome-svg-core";
|
||||
import { faBug } from "@fortawesome/free-solid-svg-icons";
|
||||
import { sendErrorReport } from "./services";
|
||||
import CurrentUser from "components/providers/CurrentUser";
|
||||
|
||||
library.add(faBug);
|
||||
|
||||
@@ -106,6 +107,7 @@ export default {
|
||||
FormElement,
|
||||
JobDetailsProvider,
|
||||
JobProblemProvider,
|
||||
CurrentUser,
|
||||
},
|
||||
props: {
|
||||
datasetId: {
|
||||
@@ -116,9 +118,9 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
message: null,
|
||||
email: null,
|
||||
errorMessage: null,
|
||||
resultMessages: [],
|
||||
emailTitle: this.l("Your email address"),
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -132,8 +134,8 @@ export default {
|
||||
onError(err) {
|
||||
this.errorMessage = err;
|
||||
},
|
||||
submit(dataset, userEmail) {
|
||||
const email = this.email || userEmail;
|
||||
submit(dataset, userEmailJob) {
|
||||
const email = userEmailJob || this.currentUserEmail;
|
||||
const message = this.message;
|
||||
sendErrorReport(dataset, message, email).then(
|
||||
(resultMessages) => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { components } from "schema";
|
||||
import type { components } from "@/schema";
|
||||
|
||||
export type DatatypesCombinedMap = components["schemas"]["DatatypesCombinedMap"];
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { fetcher } from "schema/fetcher";
|
||||
import { fetcher } from "@/schema/fetcher";
|
||||
|
||||
const getTypesAndMappings = fetcher.path("/api/datatypes/types_and_mapping").method("get").create();
|
||||
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
import { mount } from "@vue/test-utils";
|
||||
import { getLocalVue } from "tests/jest/helpers";
|
||||
import MountTarget from "./FormCheck";
|
||||
|
||||
const localVue = getLocalVue(true);
|
||||
|
||||
describe("FormCheck", () => {
|
||||
let wrapper;
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = mount(MountTarget, {
|
||||
propsData: {
|
||||
value: null,
|
||||
options: [],
|
||||
},
|
||||
localVue,
|
||||
});
|
||||
});
|
||||
|
||||
it("Confirm 'n + 1' checkboxes created (eg. includes the Select-All). Confirm labels and values match. Confirm correct values emitted.", async () => {
|
||||
const noInput = wrapper.find("[type='checkbox']");
|
||||
expect(noInput.exists()).toBe(false);
|
||||
const n = 3;
|
||||
const options = [];
|
||||
for (let i = 0; i < n; i++) {
|
||||
options.push([`label_${i}`, `value_${i}`]);
|
||||
}
|
||||
await wrapper.setProps({ options });
|
||||
const inputs = wrapper.findAll("[type='checkbox']");
|
||||
const labels = wrapper.findAll(".custom-control-label");
|
||||
expect(inputs.length).toBe(n + 1);
|
||||
const expectedValues = [];
|
||||
for (let i = 0; i < n; i++) {
|
||||
await inputs.at(i + 1).setChecked();
|
||||
expect(labels.at(i + 1).text()).toBe(`label_${i}`);
|
||||
expect(inputs.at(i + 1).attributes("value")).toBe(`value_${i}`);
|
||||
expectedValues.push(`value_${i}`);
|
||||
expect(wrapper.emitted()["input"][i][0]).toEqual(expectedValues);
|
||||
}
|
||||
});
|
||||
it("Confirm checkboxes are created when various 'empty values' are passed.", async () => {
|
||||
const emptyValues = [0, null, false, true, undefined];
|
||||
const options = [];
|
||||
for (let i = 0; i < emptyValues.length; i++) {
|
||||
options.push([`label_${i}`, emptyValues[i]]);
|
||||
}
|
||||
await wrapper.setProps({ options });
|
||||
const inputs = wrapper.findAll("[type='checkbox']");
|
||||
expect(inputs.length).toBe(emptyValues.length + 1);
|
||||
const expectedValues = [];
|
||||
for (let i = 0; i < emptyValues; i++) {
|
||||
await inputs.at(i + 1).setChecked();
|
||||
expect(inputs.at(i + 1).attributes("value")).toBe(emptyValues[i]);
|
||||
expectedValues.push(expectedValues[i]);
|
||||
expect(wrapper.emitted()["input"][i][0]).toEqual(expectedValues);
|
||||
}
|
||||
});
|
||||
it("Confirm Select-All checkbox works in various states: select-all, unselect-all, indeterminate/partial-list-selection.", async () => {
|
||||
const n = 3;
|
||||
const options = [];
|
||||
for (let i = 0; i < n; i++) {
|
||||
options.push([`label_${i}`, `value_${i}`]);
|
||||
}
|
||||
await wrapper.setProps({ options });
|
||||
const inputs = wrapper.findAll("[type='checkbox']");
|
||||
/* confirm number of checkboxes requested matches number checkboxes created */
|
||||
expect(inputs.length).toBe(n + 1);
|
||||
/* confirm component loads unchecked */
|
||||
for (let i = 0; i < n + 1; i++) {
|
||||
expect(inputs.at(i).element.checked).toBeFalsy();
|
||||
}
|
||||
/* 1 - confirm select-all option checked */
|
||||
await inputs.at(0).setChecked();
|
||||
expect(inputs.at(0).element.checked).toBeTruthy();
|
||||
/* ...confirm corresponding options checked */
|
||||
const values = options.map((option) => option[1]);
|
||||
expect(wrapper.emitted()["input"][0][0]).toStrictEqual(values);
|
||||
/* 2 - confirm select-all option UNchecked */
|
||||
await inputs.at(0).setChecked(false);
|
||||
expect(inputs.at(0).element.checked).toBeFalsy();
|
||||
/* ...confirm corresponding options UNchecked */
|
||||
for (let i = 0; i < n; i++) {
|
||||
expect(inputs.at(i + 1).element.checked).toBeFalsy();
|
||||
}
|
||||
/* 3 - confirm corresponding options indeterminate-state */
|
||||
await inputs.at(1).setChecked(true);
|
||||
expect(wrapper.emitted()["indeterminate"][0][0]).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,72 @@
|
||||
<script setup>
|
||||
import { computed, ref, watch } from "vue";
|
||||
|
||||
const emit = defineEmits(["input", "indeterminate"]);
|
||||
const props = defineProps({
|
||||
value: {
|
||||
default: null,
|
||||
},
|
||||
options: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
const selectAll = ref(false);
|
||||
const indeterminate = ref(false);
|
||||
|
||||
const currentValue = computed({
|
||||
get: () => {
|
||||
const val = props.value ?? [];
|
||||
return Array.isArray(val) ? val : [val];
|
||||
},
|
||||
set: (val) => {
|
||||
emit("input", val);
|
||||
},
|
||||
});
|
||||
|
||||
const hasOptions = computed(() => {
|
||||
return props.options.length > 0;
|
||||
});
|
||||
|
||||
function onSelectAll() {
|
||||
if (selectAll.value) {
|
||||
const allValues = props.options.map((option) => option[1]);
|
||||
emit("input", allValues);
|
||||
} else {
|
||||
emit("input", []);
|
||||
}
|
||||
}
|
||||
|
||||
watch(currentValue, () => {
|
||||
const valueLength = currentValue.value.length;
|
||||
if (valueLength === 0) {
|
||||
selectAll.value = false;
|
||||
indeterminate.value = false;
|
||||
} else if (valueLength === props.options.length) {
|
||||
selectAll.value = true;
|
||||
indeterminate.value = false;
|
||||
} else {
|
||||
indeterminate.value = true;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="hasOptions">
|
||||
<b-form-checkbox
|
||||
v-model="selectAll"
|
||||
v-localize
|
||||
class="mb-1"
|
||||
:indeterminate="indeterminate"
|
||||
@input="onSelectAll"
|
||||
@change="emit('indeterminate', selectAll)">
|
||||
Select / Deselect all
|
||||
</b-form-checkbox>
|
||||
<b-form-checkbox-group v-model="currentValue" stacked class="pl-3">
|
||||
<b-form-checkbox v-for="(option, index) in options" :key="index" :value="option[1]">
|
||||
{{ option[0] }}
|
||||
</b-form-checkbox>
|
||||
</b-form-checkbox-group>
|
||||
</div>
|
||||
<b-alert v-else v-localize variant="warning" show> No options available. </b-alert>
|
||||
</template>
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup>
|
||||
import { computed } from "vue";
|
||||
|
||||
const $emit = defineEmits(["input"]);
|
||||
const emit = defineEmits(["input"]);
|
||||
const props = defineProps({
|
||||
value: {
|
||||
default: null,
|
||||
@@ -17,15 +17,20 @@ const currentValue = computed({
|
||||
return props.value;
|
||||
},
|
||||
set: (val) => {
|
||||
$emit("input", val);
|
||||
emit("input", val);
|
||||
},
|
||||
});
|
||||
|
||||
const hasOptions = computed(() => {
|
||||
return props.options.length > 0;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<b-form-radio-group v-model="currentValue" stacked>
|
||||
<b-form-radio-group v-if="hasOptions" v-model="currentValue" stacked>
|
||||
<b-form-radio v-for="(option, index) in options" :key="index" :value="option[1]">
|
||||
{{ option[0] }}
|
||||
</b-form-radio>
|
||||
</b-form-radio-group>
|
||||
<b-alert v-else v-localize variant="warning" show> No options available. </b-alert>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
<script setup>
|
||||
import RuleCollectionBuilder from "components/RuleCollectionBuilder";
|
||||
import RulesDisplay from "components/RulesDisplay/RulesDisplay";
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import { ref, computed } from "vue";
|
||||
import { getAppRoot } from "onload/loadConfig";
|
||||
import axios from "axios";
|
||||
|
||||
const props = defineProps({
|
||||
value: {
|
||||
type: Object,
|
||||
},
|
||||
target: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
const modal = ref(null);
|
||||
const elements = ref(null);
|
||||
|
||||
const initialRules = {
|
||||
rules: [],
|
||||
mapping: [],
|
||||
};
|
||||
|
||||
const displayRules = computed(() => props.value ?? initialRules);
|
||||
|
||||
async function onEdit() {
|
||||
if (props.target) {
|
||||
const url = `${getAppRoot()}api/dataset_collections/${props.target.id}?instance_type=history`;
|
||||
|
||||
try {
|
||||
const response = await axios.get(url);
|
||||
elements.value = response.data;
|
||||
modal.value.show();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
console.log("problem fetching collection");
|
||||
}
|
||||
} else {
|
||||
modal.value.show();
|
||||
}
|
||||
}
|
||||
|
||||
const emit = defineEmits(["input"]);
|
||||
|
||||
function onSaveRules(rules) {
|
||||
modal.value.hide();
|
||||
emit("input", rules);
|
||||
}
|
||||
|
||||
function onCancel() {
|
||||
modal.value.hide();
|
||||
}
|
||||
</script>
|
||||
|
||||
<script>
|
||||
import { library } from "@fortawesome/fontawesome-svg-core";
|
||||
import { faEdit } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
library.add(faEdit);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="form-rules-edit">
|
||||
<RulesDisplay :input-rules="displayRules" />
|
||||
<b-button title="Edit Rules" @click="onEdit">
|
||||
<FontAwesomeIcon icon="fa-edit" />
|
||||
<span>Edit</span>
|
||||
</b-button>
|
||||
|
||||
<b-modal ref="modal" modal-class="ui-form-rules-edit-modal" hide-footer>
|
||||
<template v-slot:modal-title>
|
||||
<h2 class="mb-0">Build Rules for Applying to Existing Collection</h2>
|
||||
</template>
|
||||
<RuleCollectionBuilder
|
||||
elements-type="collection_contents"
|
||||
import-type="collections"
|
||||
:initial-elements="elements"
|
||||
:initial-rules="props.value"
|
||||
:save-rules-fn="onSaveRules"
|
||||
:oncancel="onCancel"
|
||||
:oncreate="() => {}" />
|
||||
</b-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.ui-form-rules-edit-modal {
|
||||
.modal-dialog {
|
||||
width: 100%;
|
||||
max-width: 85%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup>
|
||||
import { computed } from "vue";
|
||||
import FormCheck from "./FormCheck";
|
||||
import FormRadio from "./FormRadio";
|
||||
|
||||
const $emit = defineEmits(["input"]);
|
||||
@@ -50,5 +51,6 @@ const currentOptions = computed(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<form-radio v-if="display == 'radio'" v-model="currentValue" :options="currentOptions" />
|
||||
<form-check v-if="display === 'checkboxes'" v-model="currentValue" :options="currentOptions" />
|
||||
<form-radio v-else-if="display === 'radio'" v-model="currentValue" :options="currentOptions" />
|
||||
</template>
|
||||
|
||||
@@ -8,7 +8,6 @@ import Ui from "mvc/ui/ui-misc";
|
||||
import SelectContent from "mvc/ui/ui-select-content";
|
||||
import SelectLibrary from "mvc/ui/ui-select-library";
|
||||
import SelectFtp from "mvc/ui/ui-select-ftp";
|
||||
import RulesEdit from "mvc/ui/ui-rules-edit";
|
||||
import DataPicker from "mvc/ui/ui-data-picker";
|
||||
|
||||
// create form view
|
||||
@@ -197,14 +196,6 @@ export default Backbone.View.extend({
|
||||
});
|
||||
},
|
||||
|
||||
_fieldRulesEdit: function (input_def) {
|
||||
return new RulesEdit.View({
|
||||
id: input_def.id,
|
||||
onchange: input_def.onchange,
|
||||
target: input_def.target,
|
||||
});
|
||||
},
|
||||
|
||||
/** Upload file field */
|
||||
_fieldUpload: function (input_def) {
|
||||
return new Ui.Upload({
|
||||
|
||||
@@ -1,94 +1,67 @@
|
||||
<script setup>
|
||||
import FormBoolean from "./Elements/FormBoolean";
|
||||
import FormHidden from "./Elements/FormHidden";
|
||||
import FormInput from "./Elements/FormInput";
|
||||
import FormParameter from "./Elements/FormParameter";
|
||||
import FormSelection from "./Elements/FormSelection";
|
||||
import FormColor from "./Elements/FormColor";
|
||||
import FormDirectory from "./Elements/FormDirectory";
|
||||
import FormNumber from "./Elements/FormNumber";
|
||||
<script setup lang="ts">
|
||||
import FormBoolean from "./Elements/FormBoolean.vue";
|
||||
import FormHidden from "./Elements/FormHidden.vue";
|
||||
import FormInput from "./Elements/FormInput.vue";
|
||||
import FormParameter from "./Elements/FormParameter.vue";
|
||||
import FormSelection from "./Elements/FormSelection.vue";
|
||||
import FormColor from "./Elements/FormColor.vue";
|
||||
import FormDirectory from "./Elements/FormDirectory.vue";
|
||||
import FormNumber from "./Elements/FormNumber.vue";
|
||||
import FormRulesEdit from "./Elements/FormRulesEdit.vue";
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import { ref, computed, useAttrs } from "vue";
|
||||
import { library } from "@fortawesome/fontawesome-svg-core";
|
||||
import { faExclamation, faTimes, faArrowsAltH } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faCaretSquareDown, faCaretSquareUp } from "@fortawesome/free-regular-svg-icons";
|
||||
|
||||
const props = defineProps({
|
||||
id: {
|
||||
type: String,
|
||||
default: "identifier",
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
value: {
|
||||
default: null,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
refreshOnChange: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
help: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
error: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
backbonejs: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
attributes: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
collapsedEnableText: {
|
||||
type: String,
|
||||
default: "Enable",
|
||||
},
|
||||
collapsedDisableText: {
|
||||
type: String,
|
||||
default: "Disable",
|
||||
},
|
||||
collapsedEnableIcon: {
|
||||
type: String,
|
||||
default: "far fa-caret-square-down",
|
||||
},
|
||||
collapsedDisableIcon: {
|
||||
type: String,
|
||||
default: "far fa-caret-square-up",
|
||||
},
|
||||
connectedEnableText: {
|
||||
type: String,
|
||||
default: "Remove connection from module.",
|
||||
},
|
||||
connectedDisableText: {
|
||||
type: String,
|
||||
default: "Add connection to module.",
|
||||
},
|
||||
connectedEnableIcon: {
|
||||
type: String,
|
||||
default: "fa fa-times",
|
||||
},
|
||||
connectedDisableIcon: {
|
||||
type: String,
|
||||
default: "fa fa-arrows-alt-h",
|
||||
},
|
||||
workflowBuildingMode: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
import type { ComputedRef } from "vue";
|
||||
import type { FormParameterTypes, FormParameterAttributes, FormParameterValue } from "./parameterTypes";
|
||||
|
||||
export interface FormElementProps {
|
||||
id?: string;
|
||||
type?: FormParameterTypes;
|
||||
value?: FormParameterValue;
|
||||
title?: string;
|
||||
refreshOnChange?: boolean;
|
||||
help?: string;
|
||||
error?: string;
|
||||
backbonejs?: boolean;
|
||||
disabled?: boolean;
|
||||
attributes?: FormParameterAttributes;
|
||||
collapsedEnableText?: string;
|
||||
collapsedDisableText?: string;
|
||||
collapsedEnableIcon?: string;
|
||||
collapsedDisableIcon?: string;
|
||||
connectedEnableText?: string;
|
||||
connectedDisableText?: string;
|
||||
connectedEnableIcon?: string;
|
||||
connectedDisableIcon?: string;
|
||||
workflowBuildingMode?: boolean;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<FormElementProps>(), {
|
||||
id: "identifier",
|
||||
refreshOnChange: false,
|
||||
backbonejs: false,
|
||||
disabled: false,
|
||||
collapsedEnableText: "Enable",
|
||||
collapsedDisableText: "Disable",
|
||||
collapsedEnableIcon: "far fa-caret-square-down",
|
||||
collapsedDisableIcon: "far fa-caret-square-up",
|
||||
connectedEnableText: "Remove connection from module.",
|
||||
connectedDisableText: "Add connection to module.",
|
||||
connectedEnableIcon: "fa fa-times",
|
||||
connectedDisableIcon: "fa fa-arrows-alt-h",
|
||||
workflowBuildingMode: false,
|
||||
});
|
||||
|
||||
const emit = defineEmits(["input", "change"]);
|
||||
const emit = defineEmits<{
|
||||
(e: "input", value: FormParameterValue, id: string): void;
|
||||
(e: "change", shouldRefresh: boolean): void;
|
||||
}>();
|
||||
|
||||
//@ts-ignore bad library types
|
||||
library.add(faExclamation, faTimes, faArrowsAltH, faCaretSquareDown, faCaretSquareUp);
|
||||
|
||||
/** TODO: remove attrs computed.
|
||||
useAttrs is *not* reactive, and does not play nice with type safety.
|
||||
@@ -96,15 +69,15 @@ const emit = defineEmits(["input", "change"]);
|
||||
but should be removed as soon as that component is removed.
|
||||
*/
|
||||
const attrs = computed(() => props.attributes || useAttrs());
|
||||
const collapsibleValue = computed(() => attrs.value["collapsible_value"]);
|
||||
const defaultValue = computed(() => attrs.value["default_value"]);
|
||||
const connectedValue = { __class__: "ConnectedValue" };
|
||||
const collapsibleValue: ComputedRef<FormParameterValue> = computed(() => attrs.value["collapsible_value"]);
|
||||
const defaultValue: ComputedRef<FormParameterValue> = computed(() => attrs.value["default_value"]);
|
||||
const connectedValue: FormParameterValue = { __class__: "ConnectedValue" };
|
||||
|
||||
const connected = ref(false);
|
||||
const collapsed = ref(false);
|
||||
|
||||
const collapsible = computed(() => !props.disabled && collapsibleValue.value !== undefined);
|
||||
const connectable = computed(() => collapsible.value && attrs.value["connectable"]);
|
||||
const connectable = computed(() => collapsible.value && Boolean(attrs.value["connectable"]));
|
||||
|
||||
// Determines to wether expand or collapse the input
|
||||
{
|
||||
@@ -117,7 +90,7 @@ const connectable = computed(() => collapsible.value && attrs.value["connectable
|
||||
}
|
||||
|
||||
/** Submits a changed value. */
|
||||
function setValue(value) {
|
||||
function setValue(value: FormParameterValue) {
|
||||
emit("input", value, props.id);
|
||||
emit("change", props.refreshOnChange);
|
||||
}
|
||||
@@ -153,7 +126,7 @@ const showField = computed(() => !collapsed.value && !props.disabled);
|
||||
const previewText = computed(() => attrs.value["text_value"]);
|
||||
const helpText = computed(() => {
|
||||
const helpArgument = attrs.value["argument"];
|
||||
if (helpArgument && !props.help.includes(`(${helpArgument})`)) {
|
||||
if (helpArgument && !props.help?.includes(`(${helpArgument})`)) {
|
||||
return `${props.help} (${helpArgument})`;
|
||||
} else {
|
||||
return props.help;
|
||||
@@ -170,7 +143,9 @@ const currentValue = computed({
|
||||
});
|
||||
|
||||
const isHiddenType = computed(
|
||||
() => ["hidden", "hidden_data", "baseurl"].includes(props.type) || (props.attributes && props.attributes.titleonly)
|
||||
() =>
|
||||
["hidden", "hidden_data", "baseurl"].includes(props.type ?? "") ||
|
||||
(props.attributes && props.attributes.titleonly)
|
||||
);
|
||||
|
||||
const collapseText = computed(() => (collapsed.value ? props.collapsedEnableText : props.collapsedDisableText));
|
||||
@@ -181,7 +156,7 @@ const isEmpty = computed(() => {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (["text", "integer", "float", "password"].includes(props.type) && currentValue.value === "") {
|
||||
if (["text", "integer", "float", "password"].includes(props.type ?? "") && currentValue.value === "") {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -193,14 +168,6 @@ const isRequiredType = computed(() => props.type !== "boolean");
|
||||
const isOptional = computed(() => !isRequired.value && attrs.value["optional"] !== undefined);
|
||||
</script>
|
||||
|
||||
<script>
|
||||
import { library } from "@fortawesome/fontawesome-svg-core";
|
||||
import { faExclamation, faTimes, faArrowsAltH } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faCaretSquareDown, faCaretSquareUp } from "@fortawesome/free-regular-svg-icons";
|
||||
|
||||
library.add(faExclamation, faTimes, faArrowsAltH, faCaretSquareDown, faCaretSquareUp);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-show="!isHidden"
|
||||
@@ -257,10 +224,10 @@ library.add(faExclamation, faTimes, faArrowsAltH, faCaretSquareDown, faCaretSqua
|
||||
v-model="currentValue"
|
||||
:max="attrs.max"
|
||||
:min="attrs.min"
|
||||
:type="type"
|
||||
:type="props.type ?? 'float'"
|
||||
:workflow-building-mode="workflowBuildingMode" />
|
||||
<FormSelection
|
||||
v-else-if="props.type == 'select' && attrs.display == 'radio'"
|
||||
v-else-if="props.type === 'select' && ['radio', 'checkboxes'].includes(attrs.display)"
|
||||
:id="id"
|
||||
v-model="currentValue"
|
||||
:data="attrs.data"
|
||||
@@ -270,12 +237,13 @@ library.add(faExclamation, faTimes, faArrowsAltH, faCaretSquareDown, faCaretSqua
|
||||
:multiple="attrs.multiple" />
|
||||
<FormColor v-else-if="props.type === 'color'" :id="props.id" v-model="currentValue" />
|
||||
<FormDirectory v-else-if="props.type === 'directory_uri'" v-model="currentValue" />
|
||||
<FormRulesEdit v-else-if="type == 'rules'" v-model="currentValue" :target="attrs.target" />
|
||||
<FormParameter
|
||||
v-else-if="backbonejs"
|
||||
:id="props.id"
|
||||
v-model="currentValue"
|
||||
:data-label="props.title"
|
||||
:type="props.type"
|
||||
:type="props.type ?? 'text'"
|
||||
:attributes="attrs" />
|
||||
<FormInput v-else :id="props.id" v-model="currentValue" :area="attrs['area']" />
|
||||
</div>
|
||||
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
// TODO: stricter types
|
||||
export type FormParameterValue = any;
|
||||
export type FormParameterAttributes = {
|
||||
[attribute: string]: any;
|
||||
};
|
||||
|
||||
export type FormParameterTypes =
|
||||
| "boolean"
|
||||
| "hidden"
|
||||
| "hidden_data"
|
||||
| "baseurl"
|
||||
| "integer"
|
||||
| "float"
|
||||
| "radio"
|
||||
| "color"
|
||||
| "directory_uri"
|
||||
| "text"
|
||||
| "password"
|
||||
| "select"
|
||||
| "data_column"
|
||||
| "genomebuild"
|
||||
| "data"
|
||||
| "data_collection"
|
||||
| "drill_down"
|
||||
| "group_tag"
|
||||
| "library_data"
|
||||
| "ftpfile"
|
||||
| "upload"
|
||||
| "rules"
|
||||
| "data_dialog";
|
||||
@@ -77,7 +77,7 @@
|
||||
:value="tags"
|
||||
:disabled="tagsDisabled"
|
||||
:clickable="filterable"
|
||||
:useToggleLink="false"
|
||||
:use-toggle-link="false"
|
||||
@input="onTags"
|
||||
@tag-click="onTagClick" />
|
||||
<!-- collections are not expandable, so we only need the DatasetDetails component here -->
|
||||
|
||||
@@ -1,153 +1,146 @@
|
||||
<template>
|
||||
<HistoryItemsProvider
|
||||
:key="historyId"
|
||||
v-slot="{ loading, result: itemsLoaded, count: totalItemsInQuery }"
|
||||
:history-id="historyId"
|
||||
:offset="offset"
|
||||
:update-time="history.update_time"
|
||||
:filter-text="filterText">
|
||||
<ExpandedItems
|
||||
v-slot="{ expandedCount, isExpanded, setExpanded, collapseAll }"
|
||||
:scope-key="historyId"
|
||||
:get-item-key="(item) => item.type_id">
|
||||
<SelectedItems
|
||||
v-slot="{
|
||||
selectedItems,
|
||||
showSelection,
|
||||
isQuerySelection,
|
||||
selectionSize,
|
||||
setShowSelection,
|
||||
selectAllInCurrentQuery,
|
||||
isSelected,
|
||||
setSelected,
|
||||
resetSelection,
|
||||
}"
|
||||
:scope-key="queryKey"
|
||||
:get-item-key="(item) => item.type_id"
|
||||
:filter-text="filterText"
|
||||
:total-items-in-query="totalItemsInQuery"
|
||||
@query-selection-break="querySelectionBreak = true">
|
||||
<section
|
||||
class="history-layout d-flex flex-column w-100"
|
||||
@drop.prevent="onDrop"
|
||||
@dragenter.prevent="onDragEnter"
|
||||
@dragover.prevent
|
||||
@dragleave.prevent="onDragLeave">
|
||||
<slot name="navigation" :history="history" />
|
||||
<HistoryFilters
|
||||
<ExpandedItems
|
||||
v-slot="{ expandedCount, isExpanded, setExpanded, collapseAll }"
|
||||
:scope-key="historyId"
|
||||
:get-item-key="(item) => item.type_id">
|
||||
<SelectedItems
|
||||
v-slot="{
|
||||
selectedItems,
|
||||
showSelection,
|
||||
isQuerySelection,
|
||||
selectionSize,
|
||||
setShowSelection,
|
||||
selectAllInCurrentQuery,
|
||||
isSelected,
|
||||
setSelected,
|
||||
resetSelection,
|
||||
}"
|
||||
:scope-key="queryKey"
|
||||
:get-item-key="(item) => item.type_id"
|
||||
:filter-text="filterText"
|
||||
:total-items-in-query="totalItemsInQuery"
|
||||
@query-selection-break="querySelectionBreak = true">
|
||||
<section
|
||||
class="history-layout d-flex flex-column w-100"
|
||||
@drop.prevent="onDrop"
|
||||
@dragenter.prevent="onDragEnter"
|
||||
@dragover.prevent
|
||||
@dragleave.prevent="onDragLeave">
|
||||
<slot name="navigation" :history="history" />
|
||||
<HistoryFilters
|
||||
v-if="showControls"
|
||||
class="content-operations-filters mx-3"
|
||||
:filter-text.sync="filterText"
|
||||
:show-advanced.sync="showAdvanced" />
|
||||
<section v-if="!showAdvanced">
|
||||
<HistoryDetails
|
||||
:history="history"
|
||||
:writeable="writable"
|
||||
@update:history="$emit('updateHistory', $event)" />
|
||||
<HistoryMessages :history="history" />
|
||||
<HistoryCounter
|
||||
v-if="showControls"
|
||||
class="content-operations-filters mx-3"
|
||||
:history="history"
|
||||
:is-watching="isWatching"
|
||||
:last-checked="lastChecked"
|
||||
:filter-text.sync="filterText"
|
||||
:show-advanced.sync="showAdvanced" />
|
||||
<section v-if="!showAdvanced">
|
||||
<HistoryDetails
|
||||
:history="history"
|
||||
:writeable="writable"
|
||||
@update:history="$emit('updateHistory', $event)" />
|
||||
<HistoryMessages :history="history" />
|
||||
<HistoryCounter
|
||||
v-if="showControls"
|
||||
:history="history"
|
||||
:is-watching="isWatching"
|
||||
:last-checked="lastChecked"
|
||||
:filter-text.sync="filterText"
|
||||
@reloadContents="reloadContents" />
|
||||
<HistoryOperations
|
||||
v-if="showControls"
|
||||
:history="history"
|
||||
:show-selection="showSelection"
|
||||
:expanded-count="expandedCount"
|
||||
:has-matches="hasMatches(itemsLoaded)"
|
||||
:operation-running.sync="operationRunning"
|
||||
@update:show-selection="setShowSelection"
|
||||
@collapse-all="collapseAll">
|
||||
<template v-slot:selection-operations>
|
||||
<HistorySelectionOperations
|
||||
:history="history"
|
||||
:filter-text="filterText"
|
||||
:content-selection="selectedItems"
|
||||
:selection-size="selectionSize"
|
||||
:is-query-selection="isQuerySelection"
|
||||
:total-items-in-query="totalItemsInQuery"
|
||||
:operation-running.sync="operationRunning"
|
||||
@update:show-selection="setShowSelection"
|
||||
@operation-error="onOperationError"
|
||||
@hide-selection="onHideSelection"
|
||||
@reset-selection="resetSelection" />
|
||||
<HistorySelectionStatus
|
||||
v-if="showSelection"
|
||||
:selection-size="selectionSize"
|
||||
@select-all="selectAllInCurrentQuery(itemsLoaded)"
|
||||
@reset-selection="resetSelection" />
|
||||
</template>
|
||||
</HistoryOperations>
|
||||
<SelectionChangeWarning :query-selection-break="querySelectionBreak" />
|
||||
<OperationErrorDialog
|
||||
v-if="operationError"
|
||||
:operation-error="operationError"
|
||||
@hide="operationError = null" />
|
||||
</section>
|
||||
<section v-if="!showAdvanced" class="position-relative flex-grow-1 scroller">
|
||||
<history-drop-zone v-if="showDropZone" />
|
||||
<div>
|
||||
<div v-if="loading && itemsLoaded && itemsLoaded.length === 0">
|
||||
<b-alert class="m-2" variant="info" show>
|
||||
<LoadingSpan message="Loading History" />
|
||||
</b-alert>
|
||||
</div>
|
||||
<b-alert v-else-if="isProcessing" class="m-2" variant="info" show>
|
||||
<LoadingSpan message="Processing operation" />
|
||||
</b-alert>
|
||||
<div v-else-if="itemsLoaded.length === 0">
|
||||
<HistoryEmpty v-if="queryDefault" class="m-2" />
|
||||
<b-alert v-else class="m-2" variant="info" show>
|
||||
No data found for selected filter.
|
||||
</b-alert>
|
||||
</div>
|
||||
<ListingLayout
|
||||
v-else
|
||||
:offset="listOffset"
|
||||
:items="itemsLoaded"
|
||||
:query-key="queryKey"
|
||||
@scroll="onScroll">
|
||||
<template v-slot:item="{ item, currentOffset }">
|
||||
<ContentItem
|
||||
v-if="!invisible[item.hid]"
|
||||
:id="item.hid"
|
||||
is-history-item
|
||||
:item="item"
|
||||
:name="item.name"
|
||||
:writable="writable"
|
||||
:expand-dataset="isExpanded(item)"
|
||||
:is-dataset="isDataset(item)"
|
||||
:highlight="getHighlight(item)"
|
||||
:selected="isSelected(item)"
|
||||
:selectable="showSelection"
|
||||
:filterable="filterable"
|
||||
@tag-click="onTagClick"
|
||||
@tag-change="onTagChange"
|
||||
@toggleHighlights="toggleHighlights"
|
||||
@update:expand-dataset="setExpanded(item, $event)"
|
||||
@update:selected="setSelected(item, $event)"
|
||||
@view-collection="$emit('view-collection', item, currentOffset)"
|
||||
@delete="onDelete(item)"
|
||||
@undelete="onUndelete(item)"
|
||||
@unhide="onUnhide(item)" />
|
||||
</template>
|
||||
</ListingLayout>
|
||||
</div>
|
||||
</section>
|
||||
@reloadContents="reloadContents" />
|
||||
<HistoryOperations
|
||||
v-if="showControls"
|
||||
:history="history"
|
||||
:show-selection="showSelection"
|
||||
:expanded-count="expandedCount"
|
||||
:has-matches="hasMatches(itemsLoaded)"
|
||||
:operation-running.sync="operationRunning"
|
||||
@update:show-selection="setShowSelection"
|
||||
@collapse-all="collapseAll">
|
||||
<template v-slot:selection-operations>
|
||||
<HistorySelectionOperations
|
||||
:history="history"
|
||||
:filter-text="filterText"
|
||||
:content-selection="selectedItems"
|
||||
:selection-size="selectionSize"
|
||||
:is-query-selection="isQuerySelection"
|
||||
:total-items-in-query="totalItemsInQuery"
|
||||
:operation-running.sync="operationRunning"
|
||||
@update:show-selection="setShowSelection"
|
||||
@operation-error="onOperationError"
|
||||
@hide-selection="onHideSelection"
|
||||
@reset-selection="resetSelection" />
|
||||
<HistorySelectionStatus
|
||||
v-if="showSelection"
|
||||
:selection-size="selectionSize"
|
||||
@select-all="selectAllInCurrentQuery(itemsLoaded)"
|
||||
@reset-selection="resetSelection" />
|
||||
</template>
|
||||
</HistoryOperations>
|
||||
<SelectionChangeWarning :query-selection-break="querySelectionBreak" />
|
||||
<OperationErrorDialog
|
||||
v-if="operationError"
|
||||
:operation-error="operationError"
|
||||
@hide="operationError = null" />
|
||||
</section>
|
||||
</SelectedItems>
|
||||
</ExpandedItems>
|
||||
</HistoryItemsProvider>
|
||||
<section v-if="!showAdvanced" class="position-relative flex-grow-1 scroller">
|
||||
<history-drop-zone v-if="showDropZone" />
|
||||
<div>
|
||||
<div v-if="loading && itemsLoaded && itemsLoaded.length === 0">
|
||||
<b-alert class="m-2" variant="info" show>
|
||||
<LoadingSpan message="Loading History" />
|
||||
</b-alert>
|
||||
</div>
|
||||
<b-alert v-else-if="isProcessing" class="m-2" variant="info" show>
|
||||
<LoadingSpan message="Processing operation" />
|
||||
</b-alert>
|
||||
<div v-else-if="itemsLoaded.length === 0">
|
||||
<HistoryEmpty v-if="queryDefault" class="m-2" />
|
||||
<b-alert v-else class="m-2" variant="info" show>
|
||||
No data found for selected filter.
|
||||
</b-alert>
|
||||
</div>
|
||||
<ListingLayout
|
||||
v-else
|
||||
:offset="listOffset"
|
||||
:items="itemsLoaded"
|
||||
:query-key="queryKey"
|
||||
@scroll="onScroll">
|
||||
<template v-slot:item="{ item, currentOffset }">
|
||||
<ContentItem
|
||||
v-if="!invisible[item.hid]"
|
||||
:id="item.hid"
|
||||
is-history-item
|
||||
:item="item"
|
||||
:name="item.name"
|
||||
:writable="writable"
|
||||
:expand-dataset="isExpanded(item)"
|
||||
:is-dataset="isDataset(item)"
|
||||
:highlight="getHighlight(item)"
|
||||
:selected="isSelected(item)"
|
||||
:selectable="showSelection"
|
||||
:filterable="filterable"
|
||||
@tag-click="onTagClick"
|
||||
@tag-change="onTagChange"
|
||||
@toggleHighlights="toggleHighlights"
|
||||
@update:expand-dataset="setExpanded(item, $event)"
|
||||
@update:selected="setSelected(item, $event)"
|
||||
@view-collection="$emit('view-collection', item, currentOffset)"
|
||||
@delete="onDelete(item)"
|
||||
@undelete="onUndelete(item)"
|
||||
@unhide="onUnhide(item)" />
|
||||
</template>
|
||||
</ListingLayout>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
</SelectedItems>
|
||||
</ExpandedItems>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Vue from "vue";
|
||||
import { Toast } from "composables/toast";
|
||||
import { mapActions } from "vuex";
|
||||
import { HistoryItemsProvider } from "components/providers/storeProviders";
|
||||
import { mapActions as vuexMapActions } from "vuex";
|
||||
import { mapActions, mapState, storeToRefs } from "pinia";
|
||||
import { useHistoryItemsStore } from "stores/history/historyItemsStore";
|
||||
import LoadingSpan from "components/LoadingSpan";
|
||||
import ContentItem from "components/History/Content/ContentItem";
|
||||
import { deleteContent, updateContentFields } from "components/History/model/queries";
|
||||
@@ -179,7 +172,6 @@ export default {
|
||||
HistoryDropZone,
|
||||
HistoryEmpty,
|
||||
HistoryFilters,
|
||||
HistoryItemsProvider,
|
||||
HistoryOperations,
|
||||
HistorySelectionOperations,
|
||||
HistorySelectionStatus,
|
||||
@@ -199,10 +191,12 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
error: null,
|
||||
filterText: "",
|
||||
highlights: {},
|
||||
highlightsKey: null,
|
||||
invisible: {},
|
||||
loading: false,
|
||||
offset: 0,
|
||||
showAdvanced: false,
|
||||
showDropZone: false,
|
||||
@@ -212,10 +206,15 @@ export default {
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState(useHistoryItemsStore, ["getHistoryItems"]),
|
||||
/** @returns {String} */
|
||||
historyId() {
|
||||
return this.history.id;
|
||||
},
|
||||
/** @returns {Date} */
|
||||
historyUpdateTime() {
|
||||
return this.history.update_time;
|
||||
},
|
||||
/** @returns {String} */
|
||||
queryKey() {
|
||||
return `${this.historyId}-${this.filterText}`;
|
||||
@@ -232,12 +231,24 @@ export default {
|
||||
isProcessing() {
|
||||
return this.operationRunning >= this.history.update_time;
|
||||
},
|
||||
/** @returns {Array} */
|
||||
itemsLoaded() {
|
||||
return this.getHistoryItems(this.historyId, this.filterText);
|
||||
},
|
||||
/** @returns {Date} */
|
||||
lastChecked() {
|
||||
return this.$store.getters.getLastCheckedTime();
|
||||
const { getLastCheckedTime } = storeToRefs(useHistoryItemsStore());
|
||||
return getLastCheckedTime.value;
|
||||
},
|
||||
/** @returns {Number} */
|
||||
totalItemsInQuery() {
|
||||
const { getTotalMatchesCount } = storeToRefs(useHistoryItemsStore());
|
||||
return getTotalMatchesCount.value;
|
||||
},
|
||||
/** @returns {Boolean} */
|
||||
isWatching() {
|
||||
return this.$store.getters.getWatchingVisibility();
|
||||
const { getWatchingVisibility } = storeToRefs(useHistoryItemsStore());
|
||||
return getWatchingVisibility.value;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
@@ -245,6 +256,7 @@ export default {
|
||||
this.invisible = {};
|
||||
this.offset = 0;
|
||||
this.resetHighlights();
|
||||
this.loadHistoryItems();
|
||||
},
|
||||
historyId(newVal, oldVal) {
|
||||
if (newVal !== oldVal) {
|
||||
@@ -255,9 +267,19 @@ export default {
|
||||
filter(newVal) {
|
||||
this.filterText = newVal;
|
||||
},
|
||||
offset() {
|
||||
this.loadHistoryItems();
|
||||
},
|
||||
historyUpdateTime() {
|
||||
this.loadHistoryItems();
|
||||
},
|
||||
},
|
||||
async mounted() {
|
||||
await this.loadHistoryItems();
|
||||
},
|
||||
methods: {
|
||||
...mapActions("history", ["loadHistoryById"]),
|
||||
...vuexMapActions("history", ["loadHistoryById"]),
|
||||
...mapActions(useHistoryItemsStore, ["fetchHistoryItems"]),
|
||||
getHighlight(item) {
|
||||
return this.highlights[this.getItemKey(item)];
|
||||
},
|
||||
@@ -270,6 +292,18 @@ export default {
|
||||
isDataset(item) {
|
||||
return item.history_content_type == "dataset";
|
||||
},
|
||||
async loadHistoryItems() {
|
||||
this.loading = true;
|
||||
try {
|
||||
await this.fetchHistoryItems(this.historyId, this.filterText, this.offset);
|
||||
this.error = null;
|
||||
this.loading = false;
|
||||
} catch (error) {
|
||||
console.debug("HistoryPanel - Load error.", error);
|
||||
this.error = error;
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
onDelete(item) {
|
||||
this.setInvisible(item);
|
||||
deleteContent(item);
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
<script setup>
|
||||
import { computed, reactive, ref } from "vue";
|
||||
import { BCard, BFormSelect, BFormCheckbox, BFormGroup, BCollapse, BLink } from "bootstrap-vue";
|
||||
import { ExportParamsModel } from "components/Common/models/exportRecordModel";
|
||||
import { AVAILABLE_EXPORT_FORMATS } from "./services";
|
||||
|
||||
const props = defineProps({
|
||||
exportParams: {
|
||||
type: ExportParamsModel,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["onValueChanged"]);
|
||||
|
||||
const isExpanded = ref(false);
|
||||
const title = computed(() => (isExpanded.value ? `Hide advanced export options` : `Show advanced export options`));
|
||||
const localOptions = reactive({
|
||||
modelStoreFormat: props.exportParams.modelStoreFormat,
|
||||
includeFiles: props.exportParams.includeFiles,
|
||||
includeDeleted: props.exportParams.includeDeleted,
|
||||
includeHidden: props.exportParams.includeHidden,
|
||||
});
|
||||
|
||||
function onValueChanged() {
|
||||
emit("onValueChanged", localOptions);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<b-link
|
||||
id="toggle-options-link"
|
||||
:class="isExpanded ? null : 'collapsed'"
|
||||
:aria-expanded="isExpanded ? 'true' : 'false'"
|
||||
aria-controls="collapse-options"
|
||||
@click="isExpanded = !isExpanded">
|
||||
{{ title }}
|
||||
</b-link>
|
||||
<b-collapse id="collapse-options" v-model="isExpanded">
|
||||
<b-card>
|
||||
<b-form-group label="Export Format:" label-for="format">
|
||||
<b-form-select
|
||||
id="format-selector"
|
||||
v-model="localOptions.modelStoreFormat"
|
||||
:options="AVAILABLE_EXPORT_FORMATS"
|
||||
value-field="id"
|
||||
text-field="name"
|
||||
@change="onValueChanged" />
|
||||
</b-form-group>
|
||||
|
||||
<b-form-group label="Dataset files included in the package:">
|
||||
<b-form-checkbox
|
||||
id="include-files-check"
|
||||
v-model="localOptions.includeFiles"
|
||||
switch
|
||||
@change="onValueChanged">
|
||||
Include Active
|
||||
</b-form-checkbox>
|
||||
|
||||
<b-form-checkbox
|
||||
id="include-deleted-check"
|
||||
v-model="localOptions.includeDeleted"
|
||||
switch
|
||||
@change="onValueChanged">
|
||||
Include Deleted (not purged)
|
||||
</b-form-checkbox>
|
||||
|
||||
<b-form-checkbox
|
||||
id="include-hidden-check"
|
||||
v-model="localOptions.includeHidden"
|
||||
switch
|
||||
@change="onValueChanged">
|
||||
Include Hidden
|
||||
</b-form-checkbox>
|
||||
</b-form-group>
|
||||
</b-card>
|
||||
</b-collapse>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,103 @@
|
||||
import { shallowMount } from "@vue/test-utils";
|
||||
import axios from "axios";
|
||||
import MockAdapter from "axios-mock-adapter";
|
||||
import {
|
||||
EXPIRED_STS_DOWNLOAD_RECORD,
|
||||
FILE_SOURCE_STORE_RECORD,
|
||||
RECENT_STS_DOWNLOAD_RECORD,
|
||||
} from "@/components/Common/models/testData/exportData";
|
||||
import flushPromises from "flush-promises";
|
||||
import type { components } from "@/schema";
|
||||
import { getLocalVue } from "../../../../tests/jest/helpers";
|
||||
import HistoryExport from "./HistoryExport.vue";
|
||||
import { getExportRecords } from "./services";
|
||||
|
||||
const localVue = getLocalVue(true);
|
||||
|
||||
jest.mock("./services");
|
||||
const mockGetExportRecords = getExportRecords as jest.MockedFunction<typeof getExportRecords>;
|
||||
mockGetExportRecords.mockResolvedValue([]);
|
||||
|
||||
const FAKE_HISTORY_ID = "fake-history-id";
|
||||
const REMOTE_FILES_API_ENDPOINT = new RegExp("/api/remote_files/plugins");
|
||||
|
||||
type FilesSourcePluginList = components["schemas"]["FilesSourcePlugin"][];
|
||||
const REMOTE_FILES_API_RESPONSE: FilesSourcePluginList = [
|
||||
{
|
||||
id: "test-posix-source",
|
||||
type: "posix",
|
||||
uri_root: "gxfiles://test-posix-source",
|
||||
label: "TestSource",
|
||||
doc: "For testing",
|
||||
writable: true,
|
||||
requires_roles: undefined,
|
||||
requires_groups: undefined,
|
||||
},
|
||||
];
|
||||
|
||||
async function mountHistoryExport() {
|
||||
const wrapper = shallowMount(HistoryExport, {
|
||||
propsData: { historyId: FAKE_HISTORY_ID },
|
||||
localVue,
|
||||
});
|
||||
await flushPromises();
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
describe("HistoryExport.vue", () => {
|
||||
let axiosMock: MockAdapter;
|
||||
|
||||
beforeEach(async () => {
|
||||
axiosMock = new MockAdapter(axios);
|
||||
axiosMock.onGet(REMOTE_FILES_API_ENDPOINT).reply(200, []);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
axiosMock.restore();
|
||||
});
|
||||
|
||||
it("should render export options", async () => {
|
||||
const wrapper = await mountHistoryExport();
|
||||
|
||||
expect(wrapper.find("#history-export-options").exists()).toBe(true);
|
||||
});
|
||||
|
||||
it("should display a message indicating there are no exports where there are none", async () => {
|
||||
const wrapper = await mountHistoryExport();
|
||||
|
||||
expect(wrapper.find("#no-export-records-alert").exists()).toBe(true);
|
||||
});
|
||||
|
||||
it("should render previous records when there is more than one record", async () => {
|
||||
mockGetExportRecords.mockResolvedValue([
|
||||
RECENT_STS_DOWNLOAD_RECORD,
|
||||
FILE_SOURCE_STORE_RECORD,
|
||||
EXPIRED_STS_DOWNLOAD_RECORD,
|
||||
]);
|
||||
const wrapper = await mountHistoryExport();
|
||||
|
||||
expect(wrapper.find("#previous-export-records").exists()).toBe(true);
|
||||
});
|
||||
|
||||
it("should not render previous records when there is one or less records", async () => {
|
||||
mockGetExportRecords.mockResolvedValue([RECENT_STS_DOWNLOAD_RECORD]);
|
||||
const wrapper = await mountHistoryExport();
|
||||
|
||||
expect(wrapper.find("#previous-export-records").exists()).toBe(false);
|
||||
});
|
||||
|
||||
it("should display file sources tab if there are available", async () => {
|
||||
axiosMock.onGet(REMOTE_FILES_API_ENDPOINT).reply(200, REMOTE_FILES_API_RESPONSE);
|
||||
const wrapper = await mountHistoryExport();
|
||||
|
||||
expect(wrapper.find("#direct-download-tab").exists()).toBe(true);
|
||||
expect(wrapper.find("#file-source-tab").exists()).toBe(true);
|
||||
});
|
||||
|
||||
it("should not display file sources tab if there are no file sources available", async () => {
|
||||
const wrapper = await mountHistoryExport();
|
||||
|
||||
expect(wrapper.find("#direct-download-tab").exists()).toBe(true);
|
||||
expect(wrapper.find("#file-source-tab").exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,202 @@
|
||||
<script setup>
|
||||
import { computed, ref, reactive, onMounted, watch } from "vue";
|
||||
import { BAlert, BCard, BButton, BTab, BTabs } from "bootstrap-vue";
|
||||
import LoadingSpan from "components/LoadingSpan";
|
||||
import ExportRecordDetails from "components/Common/ExportRecordDetails.vue";
|
||||
import ExportRecordTable from "components/Common/ExportRecordTable.vue";
|
||||
import ExportOptions from "./ExportOptions.vue";
|
||||
import ExportToFileSourceForm from "components/Common/ExportForm.vue";
|
||||
import { getExportRecords, exportToFileSource, reimportHistoryFromRecord } from "./services";
|
||||
import { useTaskMonitor } from "composables/taskMonitor";
|
||||
import { useFileSources } from "composables/fileSources";
|
||||
import { useShortTermStorage, DEFAULT_EXPORT_PARAMS } from "composables/shortTermStorage";
|
||||
import { useConfirmDialog } from "composables/confirmDialog";
|
||||
|
||||
const { isRunning: isExportTaskRunning, waitForTask } = useTaskMonitor();
|
||||
const { hasWritable: hasWritableFileSources } = useFileSources();
|
||||
const { isPreparing: isPreparingDownload, downloadHistory, downloadObjectByRequestId } = useShortTermStorage();
|
||||
const { confirm } = useConfirmDialog();
|
||||
|
||||
const props = defineProps({
|
||||
historyId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const exportParams = reactive(DEFAULT_EXPORT_PARAMS);
|
||||
const isLoadingRecords = ref(true);
|
||||
const exportRecords = ref(null);
|
||||
const latestExportRecord = computed(() => (exportRecords.value?.length ? exportRecords.value.at(0) : null));
|
||||
const previousExportRecords = computed(() => (exportRecords.value ? exportRecords.value.slice(1) : null));
|
||||
const hasPreviousExports = computed(() => previousExportRecords.value?.length > 0);
|
||||
const availableRecordsMessage = computed(() =>
|
||||
isLoadingRecords.value
|
||||
? "Loading export records..."
|
||||
: "This history has no export records yet. You can choose one of the export options above."
|
||||
);
|
||||
const errorMessage = ref(null);
|
||||
const actionMessage = ref(null);
|
||||
const actionMessageVariant = ref(null);
|
||||
|
||||
onMounted(async () => {
|
||||
updateExports();
|
||||
});
|
||||
|
||||
watch(isExportTaskRunning, (newValue, oldValue) => {
|
||||
const hasFinished = oldValue && !newValue;
|
||||
if (hasFinished) {
|
||||
updateExports();
|
||||
}
|
||||
});
|
||||
|
||||
async function updateExports() {
|
||||
isLoadingRecords.value = true;
|
||||
try {
|
||||
errorMessage.value = null;
|
||||
exportRecords.value = await getExportRecords(props.historyId);
|
||||
const shouldWaitForTask = latestExportRecord.value?.isPreparing && !isExportTaskRunning.value;
|
||||
if (shouldWaitForTask) {
|
||||
waitForTask(latestExportRecord.value.taskUUID, 3000);
|
||||
}
|
||||
} catch (error) {
|
||||
errorMessage.value = error;
|
||||
}
|
||||
isLoadingRecords.value = false;
|
||||
}
|
||||
|
||||
async function doExportToFileSource(exportDirectory, fileName) {
|
||||
await exportToFileSource(props.historyId, exportDirectory, fileName, exportParams);
|
||||
updateExports();
|
||||
}
|
||||
|
||||
async function prepareDownload() {
|
||||
const upToDateDownloadRecord = findValidUpToDateDownloadRecord();
|
||||
if (upToDateDownloadRecord) {
|
||||
downloadObjectByRequestId(upToDateDownloadRecord.stsDownloadId);
|
||||
return;
|
||||
}
|
||||
await downloadHistory(props.historyId, { pollDelayInMs: 3000, exportParams: exportParams });
|
||||
updateExports();
|
||||
}
|
||||
|
||||
function downloadFromRecord(record) {
|
||||
if (record.canDownload) {
|
||||
downloadObjectByRequestId(record.stsDownloadId);
|
||||
}
|
||||
}
|
||||
|
||||
function findValidUpToDateDownloadRecord() {
|
||||
return exportRecords.value
|
||||
? exportRecords.value.find(
|
||||
(record) => record.canDownload && record.isUpToDate && record.exportParams?.equals(exportParams)
|
||||
)
|
||||
: null;
|
||||
}
|
||||
|
||||
async function reimportFromRecord(record) {
|
||||
const confirmed = await confirm(
|
||||
`Do you really want to import a new copy of this history exported ${record.elapsedTime}?`
|
||||
);
|
||||
if (confirmed) {
|
||||
reimportHistoryFromRecord(record)
|
||||
.then(() => {
|
||||
actionMessageVariant.value = "info";
|
||||
actionMessage.value =
|
||||
"The history is being imported in the background. Check your histories after a while to find it.";
|
||||
})
|
||||
.catch((reason) => {
|
||||
actionMessageVariant.value = "danger";
|
||||
actionMessage.value = reason;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function onActionMessageDismissedFromRecord() {
|
||||
actionMessage.value = null;
|
||||
actionMessageVariant.value = null;
|
||||
}
|
||||
|
||||
function updateExportParams(newParams) {
|
||||
exportParams.modelStoreFormat = newParams.modelStoreFormat;
|
||||
exportParams.includeFiles = newParams.includeFiles;
|
||||
exportParams.includeDeleted = newParams.includeDeleted;
|
||||
exportParams.includeHidden = newParams.includeHidden;
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<span class="history-export-component">
|
||||
<h1 class="h-lg">Export history {{ props.historyId }}</h1>
|
||||
|
||||
<export-options
|
||||
id="history-export-options"
|
||||
:export-params="exportParams"
|
||||
@onValueChanged="updateExportParams" />
|
||||
|
||||
<b-card no-body class="mt-3">
|
||||
<b-tabs pills card>
|
||||
<b-tab id="direct-download-tab" title="to direct download" title-link-class="tab-export-to-link" active>
|
||||
<p>
|
||||
Here you can generate a temporal download for your history. When your download link expires or
|
||||
your history changes you can re-generate it again.
|
||||
</p>
|
||||
<b-alert show variant="warning">
|
||||
History archive downloads can expire and are removed at regular intervals. For permanent
|
||||
storage, export to a <b>remote file</b> or download and then import the archive on another
|
||||
Galaxy server.
|
||||
</b-alert>
|
||||
<b-button
|
||||
class="direct-download-btn"
|
||||
:disabled="isPreparingDownload"
|
||||
variant="primary"
|
||||
@click="prepareDownload">
|
||||
Download
|
||||
</b-button>
|
||||
<span v-if="isPreparingDownload">
|
||||
<loading-span message="Galaxy is preparing your download, this will likely take a while" />
|
||||
</span>
|
||||
</b-tab>
|
||||
<b-tab
|
||||
v-if="hasWritableFileSources"
|
||||
id="file-source-tab"
|
||||
title="to remote file"
|
||||
title-link-class="tab-export-to-file">
|
||||
<p>
|
||||
If you need a "more permanent" way of storing your history archive you can export it directly to
|
||||
one of the available remote file sources here. You will be able to re-import it later as long as
|
||||
it remains available on the remote server.
|
||||
</p>
|
||||
<export-to-file-source-form
|
||||
what="history"
|
||||
:clear-input-after-export="true"
|
||||
@export="doExportToFileSource" />
|
||||
</b-tab>
|
||||
</b-tabs>
|
||||
</b-card>
|
||||
|
||||
<export-record-details
|
||||
v-if="latestExportRecord"
|
||||
:record="latestExportRecord"
|
||||
object-type="history"
|
||||
class="mt-3"
|
||||
:action-message="actionMessage"
|
||||
:action-message-variant="actionMessageVariant"
|
||||
@onDownload="downloadFromRecord"
|
||||
@onReimport="reimportFromRecord"
|
||||
@onActionMessageDismissed="onActionMessageDismissedFromRecord" />
|
||||
<b-alert v-else-if="errorMessage" id="last-export-record-error-alert" variant="danger" class="mt-3" show>
|
||||
{{ errorMessage }}
|
||||
</b-alert>
|
||||
<b-alert v-else id="no-export-records-alert" variant="info" class="mt-3" show>
|
||||
{{ availableRecordsMessage }}
|
||||
</b-alert>
|
||||
|
||||
<export-record-table
|
||||
v-if="hasPreviousExports"
|
||||
id="previous-export-records"
|
||||
:records="previousExportRecords"
|
||||
class="mt-3"
|
||||
@onDownload="downloadFromRecord"
|
||||
@onReimport="reimportFromRecord" />
|
||||
</span>
|
||||
</template>
|
||||
@@ -0,0 +1,77 @@
|
||||
import { ExportRecordModel } from "@/components/Common/models/exportRecordModel";
|
||||
import type { ObjectExportTaskResponse } from "@/components/Common/models/exportRecordModel";
|
||||
import { DEFAULT_EXPORT_PARAMS } from "@/composables/shortTermStorage";
|
||||
import type { components } from "@/schema";
|
||||
import { fetcher } from "@/schema";
|
||||
|
||||
type ModelStoreFormat = components["schemas"]["ModelStoreFormat"];
|
||||
|
||||
const _getExportRecords = fetcher.path("/api/histories/{history_id}/exports").method("get").create();
|
||||
const _exportToFileSource = fetcher.path("/api/histories/{history_id}/write_store").method("post").create();
|
||||
const _importFromStoreAsync = fetcher.path("/api/histories/from_store_async").method("post").create();
|
||||
|
||||
/**
|
||||
* A list of objects with the available export formats IDs and display names.
|
||||
*/
|
||||
export const AVAILABLE_EXPORT_FORMATS: { id: ModelStoreFormat; name: string }[] = [
|
||||
{ id: "rocrate.zip", name: "RO-Crate" },
|
||||
{ id: "tar.gz", name: "Compressed TGZ" },
|
||||
];
|
||||
|
||||
/**
|
||||
* Gets a list of export records for the given history.
|
||||
* @param historyId the encoded ID of the history
|
||||
* @param params query and pagination params
|
||||
* @returns a promise with a list of export records associated with the given history.
|
||||
*/
|
||||
export async function getExportRecords(historyId: string) {
|
||||
const response = await _getExportRecords(
|
||||
{
|
||||
history_id: historyId,
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
Accept: "application/vnd.galaxy.task.export+json",
|
||||
},
|
||||
}
|
||||
);
|
||||
return response.data.map((item: unknown) => new ExportRecordModel(item as ObjectExportTaskResponse));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param historyId the encoded ID of the history
|
||||
* @param exportDirectory the output directory in the file source
|
||||
* @param fileName the name of the output archive
|
||||
* @param exportParams additional parameters to configure the export
|
||||
* @returns A promise with the request response
|
||||
*/
|
||||
export async function exportToFileSource(
|
||||
historyId: string,
|
||||
exportDirectory: string,
|
||||
fileName: string,
|
||||
exportParams = DEFAULT_EXPORT_PARAMS
|
||||
) {
|
||||
const exportDirectoryUri = `${exportDirectory}/${fileName}.${exportParams.modelStoreFormat}`;
|
||||
|
||||
return _exportToFileSource({
|
||||
history_id: historyId,
|
||||
target_uri: exportDirectoryUri,
|
||||
model_store_format: exportParams.modelStoreFormat as ModelStoreFormat,
|
||||
include_files: exportParams.includeFiles,
|
||||
include_deleted: exportParams.includeDeleted,
|
||||
include_hidden: exportParams.includeHidden,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Imports a new history using the information stored in the given export record.
|
||||
* @param record The export record to be imported
|
||||
* @returns A promise with the request response
|
||||
*/
|
||||
export async function reimportHistoryFromRecord(record: ExportRecordModel) {
|
||||
return _importFromStoreAsync({
|
||||
store_content_uri: record.importUri,
|
||||
model_store_format: record.modelStoreFormat,
|
||||
});
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
*/
|
||||
import Backbone from "backbone";
|
||||
import store from "store";
|
||||
import { useHistoryItemsStore } from "stores/history/historyItemsStore";
|
||||
import { buildCollectionModal } from "./buildCollectionModal";
|
||||
import { createDatasetCollection } from "components/History/model/queries";
|
||||
import { watchHistory } from "store/historyStore/model/watchHistory";
|
||||
@@ -12,30 +13,14 @@ import { watchHistory } from "store/historyStore/model/watchHistory";
|
||||
export class HistoryPanelProxy {
|
||||
constructor() {
|
||||
const model = (this.model = new Backbone.Model({}));
|
||||
const historyItemsStore = useHistoryItemsStore();
|
||||
this.collection = {
|
||||
each(callback, filterText = "") {
|
||||
const historyItems = store.getters.getHistoryItems({ historyId: model.id, filterText: filterText });
|
||||
const historyItems = historyItemsStore.getHistoryItems(model.id, filterText);
|
||||
historyItems.forEach((model) => {
|
||||
callback(new Backbone.Model(model));
|
||||
});
|
||||
},
|
||||
on(name, callback) {
|
||||
this.off();
|
||||
this.unwatch = store.watch(
|
||||
(state, getters) => getters.getLatestCreateTime(),
|
||||
() => {
|
||||
callback();
|
||||
console.debug("History change watcher detected a change.", name);
|
||||
}
|
||||
);
|
||||
console.debug("History change watcher enabled.", name);
|
||||
},
|
||||
off(name) {
|
||||
if (this.unwatch) {
|
||||
this.unwatch();
|
||||
console.debug("History change watcher disabled.", name);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// watch the store, update history id
|
||||
|
||||
@@ -87,7 +87,6 @@ export async function bulkUpdate(history, operation, filters, items = [], params
|
||||
params,
|
||||
};
|
||||
const response = await axios.put(prependPath(url), payload);
|
||||
console.debug(`Submitted request to ${operation} selected content in bulk. Parameters: ${params}`, response);
|
||||
return doResponse(response);
|
||||
}
|
||||
|
||||
|
||||
@@ -76,12 +76,14 @@ export default {
|
||||
},
|
||||
onDownload(config) {
|
||||
if (!config.enable_celery_tasks) {
|
||||
console.log("celery tasks not enabled - setting href to fallback URL.");
|
||||
window.location.assign(safePath(this.fallbackUrl));
|
||||
return;
|
||||
} else {
|
||||
this.waiting = true;
|
||||
axios
|
||||
.post(this.downloadEndpoint, this.postParameters)
|
||||
.then(this.handleInitialize)
|
||||
.catch(this.handleError);
|
||||
}
|
||||
this.waiting = true;
|
||||
axios.post(this.downloadEndpoint, this.postParameters).then(this.handleInitialize).catch(this.handleError);
|
||||
},
|
||||
handleInitialize(response) {
|
||||
const storageRequestId = response.data.storage_request_id;
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import Multiselect from "vue-multiselect";
|
||||
import Tag from "./Tag.vue";
|
||||
import { ref, computed } from "vue";
|
||||
import Multiselect from "vue-multiselect";
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import { useUserTags } from "composables/user";
|
||||
import { useToast } from "composables/toast";
|
||||
import { useUid } from "composables/utils/uid";
|
||||
import { library } from "@fortawesome/fontawesome-svg-core";
|
||||
import { faTags, faCheck, faTimes, faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import Tag from "./Tag.vue";
|
||||
import { useUserTags } from "@/composables/user";
|
||||
import { useToast } from "@/composables/toast";
|
||||
import { useUid } from "@/composables/utils/uid";
|
||||
|
||||
import type { Ref } from "vue";
|
||||
|
||||
@@ -119,12 +119,12 @@ function onTagClicked(tag: string) {
|
||||
v-if="!disabled"
|
||||
ref="multiselectElement"
|
||||
placeholder="Add Tags"
|
||||
openDirection="bottom"
|
||||
open-direction="bottom"
|
||||
:value="tags"
|
||||
:options="userTags"
|
||||
:multiple="true"
|
||||
:taggable="true"
|
||||
:closeOnSelect="false"
|
||||
:close-on-select="false"
|
||||
@tag="onAddTag"
|
||||
@input="onInput"
|
||||
@open="onOpen"
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import { keyedColorScheme } from "utils/color";
|
||||
import { computed } from "vue";
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import { library } from "@fortawesome/fontawesome-svg-core";
|
||||
import { faTimes } from "@fortawesome/free-solid-svg-icons";
|
||||
import { keyedColorScheme } from "@/utils/color";
|
||||
|
||||
export interface TagProps {
|
||||
option: string;
|
||||
|
||||
@@ -8,10 +8,12 @@ import MockConfigProvider from "../providers/MockConfigProvider";
|
||||
import MockCurrentHistory from "components/providers/MockCurrentHistory";
|
||||
import Vue from "vue";
|
||||
import Vuex from "vuex";
|
||||
import { createPinia } from "pinia";
|
||||
import { userStore } from "store/userStore";
|
||||
import { configStore } from "store/configStore";
|
||||
|
||||
const localVue = getLocalVue();
|
||||
const pinia = createPinia();
|
||||
|
||||
describe("ToolForm", () => {
|
||||
let wrapper;
|
||||
@@ -52,6 +54,7 @@ describe("ToolForm", () => {
|
||||
FormDisplay: true,
|
||||
},
|
||||
provide: { store },
|
||||
pinia,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -107,6 +107,8 @@
|
||||
|
||||
<script>
|
||||
import { getGalaxyInstance } from "app";
|
||||
import { useHistoryItemsStore } from "stores/history/historyItemsStore";
|
||||
import { mapState } from "pinia";
|
||||
import { getToolFormData, updateToolFormData, submitJob } from "./services";
|
||||
import { allowCachedJobs } from "./utilities";
|
||||
import { refreshContentsWrapper } from "utils/data";
|
||||
@@ -189,6 +191,7 @@ export default {
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState(useHistoryItemsStore, ["getLatestCreateTime"]),
|
||||
toolName() {
|
||||
return this.formConfig.name;
|
||||
},
|
||||
@@ -219,21 +222,19 @@ export default {
|
||||
}
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.requestTool().then(() => {
|
||||
watch: {
|
||||
getLatestCreateTime() {
|
||||
const Galaxy = getGalaxyInstance();
|
||||
if (Galaxy && Galaxy.currHistoryPanel) {
|
||||
console.debug(`ToolForm::created - Started listening to history changes. [${this.id}]`);
|
||||
Galaxy.currHistoryPanel.collection.on("change", this.onHistoryChange, this);
|
||||
console.debug("History change watcher detected a change.");
|
||||
this.onHistoryChange();
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
beforeDestroy() {
|
||||
const Galaxy = getGalaxyInstance();
|
||||
if (Galaxy && Galaxy.currHistoryPanel) {
|
||||
Galaxy.currHistoryPanel.collection.off("change", this.onHistoryChange, this);
|
||||
console.debug(`ToolForm::beforeDestroy - Stopped listening to history changes. [${this.id}]`);
|
||||
}
|
||||
created() {
|
||||
this.requestTool().then(() => {
|
||||
console.debug(`ToolForm::created - Started listening to history changes. [${this.id}]`);
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
emailAllowed(config, user) {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script setup>
|
||||
import { computed } from "vue";
|
||||
import { getAppRoot } from "onload/loadConfig";
|
||||
import { useFormattedToolHelp } from "composables/formattedToolHelp";
|
||||
|
||||
const props = defineProps({
|
||||
content: {
|
||||
@@ -9,57 +8,7 @@ const props = defineProps({
|
||||
},
|
||||
});
|
||||
|
||||
const formattedContent = computed(() => {
|
||||
const node = document.createElement("div");
|
||||
node.innerHTML = props.content;
|
||||
|
||||
const links = node.getElementsByTagName("a");
|
||||
Array.from(links).forEach((link) => {
|
||||
link.target = "_blank";
|
||||
});
|
||||
|
||||
const images = node.getElementsByTagName("img");
|
||||
Array.from(images).forEach((image) => {
|
||||
if (image.src.includes("admin_toolshed")) {
|
||||
image.src = getAppRoot() + image.src;
|
||||
}
|
||||
});
|
||||
|
||||
// loop these levels backwards to avoid increasing heading twice
|
||||
[5, 4, 3, 2, 1].forEach((level) => {
|
||||
increaseHeadingLevel(node, level, 2);
|
||||
});
|
||||
|
||||
return node.innerHTML;
|
||||
});
|
||||
|
||||
/**
|
||||
* @param {HTMLElement} node
|
||||
* @param {number} level
|
||||
* @param {number} increaseBy
|
||||
*/
|
||||
function increaseHeadingLevel(node, level, increaseBy) {
|
||||
// cap target level at 6 (highest heading level)
|
||||
let targetLevel = level + increaseBy;
|
||||
if (targetLevel > 6) {
|
||||
targetLevel = 6;
|
||||
}
|
||||
|
||||
const headings = node.getElementsByTagName(`h${level}`);
|
||||
|
||||
// create new headings with target level and copy contents + attributes
|
||||
Array.from(headings).forEach((heading) => {
|
||||
const newTag = document.createElement(`h${targetLevel}`);
|
||||
newTag.innerHTML = heading.innerHTML;
|
||||
|
||||
Array.from(heading.attributes).forEach((attribute) => {
|
||||
newTag.setAttribute(attribute.name, attribute.value);
|
||||
});
|
||||
|
||||
heading.insertAdjacentElement("beforebegin", newTag);
|
||||
heading.remove();
|
||||
});
|
||||
}
|
||||
const { formattedContent } = useFormattedToolHelp(props.content);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,17 +1,24 @@
|
||||
<template>
|
||||
<b-button
|
||||
v-if="offset > 200"
|
||||
v-b-tooltip.noninteractive.hover
|
||||
class="ui-btn-back-to-top btn-circle"
|
||||
class="back-to-top"
|
||||
:class="{ show: offset > 100 }"
|
||||
title="Scroll To Top"
|
||||
variant="info"
|
||||
@click="$emit('click')">
|
||||
<i class="fa fa-arrow-up" />
|
||||
<FontAwesomeIcon icon="fa-chevron-up" />
|
||||
</b-button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import { library } from "@fortawesome/fontawesome-svg-core";
|
||||
import { faChevronUp } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
library.add(faChevronUp);
|
||||
|
||||
export default {
|
||||
components: { FontAwesomeIcon },
|
||||
props: {
|
||||
offset: {
|
||||
type: Number,
|
||||
@@ -20,3 +27,17 @@ export default {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.back-to-top {
|
||||
bottom: 0.5rem;
|
||||
left: 0.5rem;
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
transition: opacity 0.4s;
|
||||
|
||||
&.show {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -4,36 +4,34 @@
|
||||
:filter-settings="filterSettings"
|
||||
:toolbox="toolbox"
|
||||
:panel-view="panelView">
|
||||
<section class="overflow-auto h-100" @scroll="onScroll">
|
||||
<section class="tools-list">
|
||||
<div class="mb-2">
|
||||
<span class="row mb-2">
|
||||
<span class="col">
|
||||
<h1 v-if="hasFilters" class="d-inline-block h-lg">Advanced Tool Search Results</h1>
|
||||
<h1 v-else class="d-inline-block h-lg">
|
||||
Consolidated view of {{ itemsLoaded.length }} available tools.
|
||||
</h1>
|
||||
</span>
|
||||
</span>
|
||||
<h1 class="h-lg">Search Results</h1>
|
||||
<span v-if="itemsLoaded.length !== 0" class="row">
|
||||
<span v-if="hasFilters" class="col d-inline-block">
|
||||
<span v-if="filterCount" class="col d-inline-block d-flex align-items-baseline flex-gapx-1">
|
||||
Found {{ itemsLoaded.length }} tools for
|
||||
<a id="popover-filters" href="javascript:void(0)">filters</a>.
|
||||
<b-popover target="popover-filters" triggers="hover" placement="top">
|
||||
<b-button id="popover-filters" class="ui-link">
|
||||
{{ filterCount }}
|
||||
{{ filterCount === 1 ? "filter" : "filters" }}.
|
||||
</b-button>
|
||||
<b-popover target="popover-filters" triggers="hover focus" placement="bottom">
|
||||
<template v-slot:title>Filters</template>
|
||||
<div v-for="(value, filter) in filterSettings" :key="filter">
|
||||
<b>{{ filter }}</b
|
||||
>: {{ value }}
|
||||
</div>
|
||||
</b-popover>
|
||||
Click <a href="javascript:void(0)" @click.stop="showAllTools">here</a> for a consolidated view
|
||||
of all tools in this Galaxy instance.
|
||||
<b-button variant="link" size="sm" @click.stop="showAllTools">
|
||||
<FontAwesomeIcon icon="fa-times" />
|
||||
Clear filters
|
||||
</b-button>
|
||||
</span>
|
||||
<span v-else class="col d-inline-block">
|
||||
No filters applied. Please add filters to the menu in the Tool Panel.
|
||||
No filters applied. Please add filters to the Advanced Tool Search in the Tool Panel.
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<div ref="scrollContainer" class="overflow-auto">
|
||||
<b-alert v-if="loading" class="m-2" variant="info" show>
|
||||
<LoadingSpan message="Loading Advanced Search Results" />
|
||||
</b-alert>
|
||||
@@ -44,7 +42,7 @@
|
||||
<ToolsListTable :tools="itemsLoaded" />
|
||||
</div>
|
||||
</div>
|
||||
<ScrollToTopButton :offset="offset" @click="scrollToTop" />
|
||||
<ScrollToTopButton :offset="scrollTop" @click="scrollToTop" />
|
||||
</section>
|
||||
</ToolsProvider>
|
||||
</template>
|
||||
@@ -54,6 +52,13 @@ import LoadingSpan from "components/LoadingSpan";
|
||||
import { ToolsProvider } from "components/providers/storeProviders";
|
||||
import ToolsListTable from "./ToolsListTable";
|
||||
import ScrollToTopButton from "./ScrollToTopButton";
|
||||
import { useAnimationFrameScroll } from "composables/sensors/animationFrameScroll";
|
||||
import { ref } from "vue";
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import { library } from "@fortawesome/fontawesome-svg-core";
|
||||
import { faTimes } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
library.add(faTimes);
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -61,6 +66,7 @@ export default {
|
||||
ToolsListTable,
|
||||
ToolsProvider,
|
||||
ScrollToTopButton,
|
||||
FontAwesomeIcon,
|
||||
},
|
||||
props: {
|
||||
name: {
|
||||
@@ -80,9 +86,13 @@ export default {
|
||||
default: "",
|
||||
},
|
||||
},
|
||||
data() {
|
||||
setup() {
|
||||
const scrollContainer = ref(null);
|
||||
const { scrollTop } = useAnimationFrameScroll(scrollContainer);
|
||||
|
||||
return {
|
||||
offset: 0,
|
||||
scrollContainer,
|
||||
scrollTop,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -101,16 +111,13 @@ export default {
|
||||
});
|
||||
return newFilterSettings;
|
||||
},
|
||||
hasFilters() {
|
||||
filterCount() {
|
||||
return Object.keys(this.filterSettings).length;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onScroll(e) {
|
||||
this.offset = e.target.scrollTop;
|
||||
},
|
||||
scrollToTop() {
|
||||
this.$el.scrollTop = 0;
|
||||
this.$refs.scrollContainer.scrollTo({ top: 0, behavior: "smooth" });
|
||||
},
|
||||
showAllTools() {
|
||||
this.$router.push({ path: "/tools/list" });
|
||||
@@ -118,3 +125,12 @@ export default {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tools-list {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
<script setup>
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import { ref, computed } from "vue";
|
||||
import { useFormattedToolHelp } from "composables/formattedToolHelp";
|
||||
import ToolFavoriteButton from "components/Tool/Buttons/ToolFavoriteButton";
|
||||
const props = defineProps({
|
||||
id: { type: String, required: true },
|
||||
name: { type: String, required: true },
|
||||
section: { type: String, required: true },
|
||||
description: { type: String, default: null },
|
||||
summary: { type: String, default: null },
|
||||
help: { type: String, default: null },
|
||||
version: { type: String, default: null },
|
||||
link: { type: String, default: null },
|
||||
workflowCompatible: { type: Boolean, default: false },
|
||||
local: { type: Boolean, default: false },
|
||||
});
|
||||
|
||||
const emit = defineEmits(["open"]);
|
||||
|
||||
const showHelp = ref(false);
|
||||
|
||||
const formattedToolHelp = computed(() => {
|
||||
if (showHelp.value) {
|
||||
const { formattedContent } = useFormattedToolHelp(props.help);
|
||||
return formattedContent.value;
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<script>
|
||||
import { library } from "@fortawesome/fontawesome-svg-core";
|
||||
import {
|
||||
faWrench,
|
||||
faGlobe,
|
||||
faCheck,
|
||||
faTimes,
|
||||
faAngleDown,
|
||||
faAngleUp,
|
||||
faExclamationTriangle,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
library.add(faWrench, faGlobe, faCheck, faTimes, faAngleDown, faAngleUp, faExclamationTriangle);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="tool-list-item ui-portlet-section">
|
||||
<div class="top-bar bg-secondary px-2 py-1 rounded-right">
|
||||
<div class="py-1 d-flex flex-wrap flex-gapx-1">
|
||||
<span>
|
||||
<FontAwesomeIcon v-if="props.local" icon="fa-wrench" fixed-width />
|
||||
<FontAwesomeIcon v-else icon="fa-globe" fixed-width />
|
||||
|
||||
<b-button v-if="props.local" class="ui-link text-dark" @click="() => emit('open')">
|
||||
<b>{{ props.name }}</b>
|
||||
</b-button>
|
||||
<b-button v-else class="ui-link text-dark" :href="props.link">
|
||||
<b>{{ props.name }}</b>
|
||||
</b-button>
|
||||
</span>
|
||||
<span itemprop="description">{{ props.description }}</span>
|
||||
<span>(Galaxy Version {{ props.version }})</span>
|
||||
</div>
|
||||
<div>
|
||||
<ToolFavoriteButton :id="props.id" />
|
||||
|
||||
<b-button v-if="props.local" variant="primary" size="sm" @click="() => emit('open')">
|
||||
<FontAwesomeIcon icon="fa-wrench" fixed-width />
|
||||
Open
|
||||
</b-button>
|
||||
<b-button v-else variant="primary" size="sm" :href="props.link">
|
||||
<FontAwesomeIcon icon="fa-globe" fixed-width />
|
||||
Open
|
||||
</b-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="portlet-content">
|
||||
<div class="d-flex flex-gapx-1 py-2">
|
||||
<span v-if="props.section" class="info px-1 rounded">
|
||||
<b>Section:</b> <b-link :to="`/tools/list?section=${props.section}`">{{ section }}</b-link>
|
||||
</span>
|
||||
|
||||
<span v-if="!props.local" class="info px-1 rounded">
|
||||
<FontAwesomeIcon icon="fa-globe" fixed-width />
|
||||
External
|
||||
</span>
|
||||
|
||||
<span v-if="!props.workflowCompatible" class="warn px-1 rounded">
|
||||
<FontAwesomeIcon icon="fa-exclamation-triangle" />
|
||||
Not Workflow compatible
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div v-if="props.summary" v-html="props.summary"></div>
|
||||
|
||||
<div v-if="props.help" class="mt-2">
|
||||
<b-button v-if="!showHelp" class="ui-link" @click="() => (showHelp = true)">
|
||||
<FontAwesomeIcon icon="fa-angle-down" />
|
||||
Show tool help
|
||||
</b-button>
|
||||
<b-button v-else class="ui-link" @click="() => (showHelp = false)">
|
||||
<FontAwesomeIcon icon="fa-angle-up" />
|
||||
Hide tool help
|
||||
</b-button>
|
||||
|
||||
<div v-if="showHelp" class="mt-2" v-html="formattedToolHelp"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "theme/blue.scss";
|
||||
|
||||
.tool-list-item {
|
||||
.info {
|
||||
background-color: scale-color($brand-info, $lightness: +75%);
|
||||
}
|
||||
|
||||
.success {
|
||||
background-color: scale-color($brand-success, $lightness: +75%);
|
||||
}
|
||||
|
||||
.warn {
|
||||
background-color: scale-color($brand-warning, $lightness: +75%);
|
||||
}
|
||||
|
||||
.top-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,59 +1,25 @@
|
||||
<template>
|
||||
<div v-infinite-scroll="loadTools" infinite-scroll-disabled="busy">
|
||||
<b-table striped bordered :fields="fields" :items="buffer">
|
||||
<template v-slot:cell(name)="row">
|
||||
<span v-if="!row.item.help">
|
||||
<b>{{ row.item.name }}</b> {{ row.item.description }}
|
||||
</span>
|
||||
<span v-else>
|
||||
<b-link href="javascript:void(0)" role="button" @click.stop="row.toggleDetails()">
|
||||
<b>{{ row.item.name }}</b> {{ row.item.description }}
|
||||
</b-link>
|
||||
<p v-if="!row.item._showDetails && row.item.summary" v-html="row.item.summary" />
|
||||
</span>
|
||||
</template>
|
||||
<template v-slot:row-details="row">
|
||||
<b-card v-if="row.item.help">
|
||||
<p class="mb-1" v-html="row.item.help" />
|
||||
<a
|
||||
:href="row.item.target === 'galaxy_main' ? 'javascript:void(0)' : row.item.link"
|
||||
@click.stop="onOpen(row.item)">
|
||||
Click here to open the tool
|
||||
</a>
|
||||
</b-card>
|
||||
</template>
|
||||
<template v-slot:cell(section)="row">
|
||||
{{ row.item.panel_section_name }}
|
||||
</template>
|
||||
<template v-slot:cell(workflow)="row">
|
||||
<span
|
||||
v-if="row.item.is_workflow_compatible"
|
||||
v-b-tooltip.hover
|
||||
class="fa fa-check text-success"
|
||||
title="Is Workflow Compatible" />
|
||||
<span v-else v-b-tooltip.hover class="fa fa-times text-danger" title="Not Workflow Compatible" />
|
||||
</template>
|
||||
<template v-slot:cell(target)="row">
|
||||
<span
|
||||
v-if="row.item.target === 'galaxy_main'"
|
||||
v-b-tooltip.hover
|
||||
class="fa fa-check text-success"
|
||||
title="Is Local" />
|
||||
<span v-else v-b-tooltip.hover class="fa fa-times text-danger" title="Not Local" />
|
||||
</template>
|
||||
<template v-slot:cell(open)="row">
|
||||
<b-button
|
||||
v-b-tooltip.hover.top
|
||||
:title="'Open Tool' | localize"
|
||||
class="fa fa-play"
|
||||
size="sm"
|
||||
variant="primary"
|
||||
:href="row.item.target === 'galaxy_main' ? 'javascript:void(0)' : row.item.link"
|
||||
@click.stop="onOpen(row.item)" />
|
||||
</template>
|
||||
</b-table>
|
||||
<div
|
||||
v-infinite-scroll="loadTools"
|
||||
class="tools-list-table"
|
||||
infinite-scroll-distance="200"
|
||||
infinite-scroll-disabled="busy">
|
||||
<ToolsListItem
|
||||
v-for="item of buffer"
|
||||
:id="item.id"
|
||||
:key="item.id"
|
||||
:name="item.name"
|
||||
:section="item.panel_section_name"
|
||||
:description="item.description"
|
||||
:summary="item.summary"
|
||||
:help="item.help"
|
||||
:local="item.target === 'galaxy_main'"
|
||||
:link="item.link"
|
||||
:workflow-compatible="item.is_workflow_compatible"
|
||||
:version="item.version"
|
||||
@open="() => onOpen(item)" />
|
||||
<div>
|
||||
<i v-if="allLoaded">All {{ tools.length > 1 ? tools.length : "" }} results loaded</i>
|
||||
<div v-if="allLoaded" class="list-end my-2">- End of search results -</div>
|
||||
<b-overlay :show="busy" opacity="0.5" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -61,15 +27,16 @@
|
||||
|
||||
<script>
|
||||
import Vue from "vue";
|
||||
import _l from "utils/localization";
|
||||
import infiniteScroll from "vue-infinite-scroll";
|
||||
import { useGlobalUploadModal } from "composables/globalUploadModal";
|
||||
import { fetchData } from "./services";
|
||||
import ToolsListItem from "./ToolsListItem";
|
||||
|
||||
const defaultBufferLen = 4;
|
||||
const loadTimeout = 100;
|
||||
|
||||
export default {
|
||||
components: { ToolsListItem },
|
||||
directives: { infiniteScroll },
|
||||
props: {
|
||||
tools: {
|
||||
@@ -86,32 +53,6 @@ export default {
|
||||
allLoaded: false,
|
||||
bufferLen: 0,
|
||||
busy: false,
|
||||
fields: [
|
||||
{
|
||||
key: "name",
|
||||
label: _l("Name"),
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
key: "section",
|
||||
label: _l("Section"),
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
key: "workflow",
|
||||
label: _l("Workflow Compatible"),
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
key: "target",
|
||||
label: _l("Local Tool"),
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
key: "open",
|
||||
label: "",
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -185,3 +126,19 @@ export default {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "theme/blue.scss";
|
||||
|
||||
.tools-list-table {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
|
||||
.list-end {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
color: $text-light;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup>
|
||||
import UploadModalContent from "./UploadModalContent";
|
||||
|
||||
import { ref } from "vue";
|
||||
import { ref, watch } from "vue";
|
||||
import { getAppRoot } from "onload";
|
||||
|
||||
import { useCurrentUser } from "composables/user";
|
||||
@@ -74,6 +74,19 @@ async function open(overrideOptions) {
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => showModal.value,
|
||||
(modalShown) => setIframeEvents(modalShown)
|
||||
);
|
||||
|
||||
function setIframeEvents(disableEvents) {
|
||||
const element = document.getElementById("galaxy_main");
|
||||
|
||||
if (element) {
|
||||
element.style["pointer-events"] = disableEvents ? "none" : "auto";
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
open,
|
||||
});
|
||||
|
||||
@@ -138,7 +138,6 @@ export default {
|
||||
if (pjas[this.emailPayloadKey]) {
|
||||
pjas[this.emailActionKey] = true;
|
||||
}
|
||||
console.debug("FormSection - Setting new data.", this.postJobActions, pjas);
|
||||
this.formData = pjas;
|
||||
},
|
||||
setEmailAction(pjas) {
|
||||
|
||||
@@ -189,14 +189,12 @@ export default {
|
||||
},
|
||||
postChanges(newVersion) {
|
||||
const payload = Object.assign({}, this.mainValues);
|
||||
console.debug("FormTool - Posting changes.", payload);
|
||||
const options = this.configForm;
|
||||
let toolId = options.id;
|
||||
let toolVersion = options.version;
|
||||
if (newVersion) {
|
||||
toolId = toolId.replace(toolVersion, newVersion);
|
||||
toolVersion = newVersion;
|
||||
console.debug("FormTool - Tool version changed.", toolId, toolVersion);
|
||||
}
|
||||
this.$emit("onSetData", this.nodeId, {
|
||||
tool_id: toolId,
|
||||
|
||||
@@ -721,7 +721,6 @@ export default {
|
||||
this.lastQueue
|
||||
.enqueue(loadWorkflow, { id, version, workflow: this })
|
||||
.then((data) => {
|
||||
console.debug("Editor - Loading workflow:", id);
|
||||
this._loadEditorData(data);
|
||||
})
|
||||
.catch((response) => {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup>
|
||||
import TrsTool from "./TrsTool";
|
||||
import TrsUrlImport from "./TrsUrlImport";
|
||||
import { Toast } from "composables/toast";
|
||||
import { Services } from "../services";
|
||||
import { getGalaxyInstance } from "app";
|
||||
@@ -23,6 +24,10 @@ const props = defineProps({
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
queryTrsUrl: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
isRun: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
@@ -115,6 +120,22 @@ const importVersion = (trsId, toolIdToImport, version = null, isRunFormRedirect
|
||||
importing.value = false;
|
||||
});
|
||||
};
|
||||
|
||||
const importVersionFromUrl = (url, isRunFormRedirect = false) => {
|
||||
importing.value = true;
|
||||
errorMessage.value = null;
|
||||
services
|
||||
.importTrsToolFromUrl(url)
|
||||
.then((response) => {
|
||||
redirectOnImport(safePath("/"), response, isRunFormRedirect);
|
||||
})
|
||||
.catch((e) => {
|
||||
errorMessage.value = e || "Import failed for an unknown reason.";
|
||||
})
|
||||
.finally(() => {
|
||||
importing.value = false;
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -153,6 +174,10 @@ const importVersion = (trsId, toolIdToImport, version = null, isRunFormRedirect
|
||||
:trs-tool="trsTool"
|
||||
@onImport="importVersion(trsSelection.id, trsTool.id, $event)" />
|
||||
</div>
|
||||
<hr />
|
||||
<div>
|
||||
<TrsUrlImport :query-trs-url="props.queryTrsUrl" @onImport="importVersionFromUrl($event)" />
|
||||
</div>
|
||||
</b-card>
|
||||
<b-alert v-else class="text-center my-2" show variant="danger">
|
||||
Anonymous user cannot import workflows, please register or log in
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
<script setup>
|
||||
import { computed, ref } from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
queryTrsUrl: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
const trsUrl = ref(props.queryTrsUrl);
|
||||
|
||||
const isImportDisabled = computed(() => {
|
||||
return !trsUrl.value;
|
||||
});
|
||||
|
||||
const importTooltip = computed(() => {
|
||||
return isImportDisabled.value ? "You must provide a TRS URL." : "Import workflow from TRS URL";
|
||||
});
|
||||
|
||||
const emit = defineEmits(["onImport"]);
|
||||
|
||||
const submit = (ev) => {
|
||||
ev.preventDefault();
|
||||
emit("onImport", trsUrl.value);
|
||||
};
|
||||
|
||||
// Automatically trigger the import if the TRS URL was provided as a query param
|
||||
if (trsUrl.value) {
|
||||
emit("onImport", trsUrl.value);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<b-form class="mt-4" @submit="submit">
|
||||
<h2 class="h-sm">alternatively, provide a TRS URL directly</h2>
|
||||
<b-form-group label="TRS URL:" label-class="font-weight-bold">
|
||||
<b-form-input id="trs-import-url-input" v-model="trsUrl" aria-label="TRS URL" type="url" />
|
||||
If the workflow is accessible via a TRS URL, enter the URL above and click Import.
|
||||
</b-form-group>
|
||||
<b-button
|
||||
id="trs-url-import-button"
|
||||
type="submit"
|
||||
:disabled="isImportDisabled"
|
||||
:title="importTooltip"
|
||||
variant="primary">
|
||||
Import workflow
|
||||
</b-button>
|
||||
</b-form>
|
||||
</template>
|
||||
@@ -110,4 +110,18 @@ export class Services {
|
||||
rethrowSimple(e);
|
||||
}
|
||||
}
|
||||
|
||||
async importTrsToolFromUrl(trsUrl) {
|
||||
const data = {
|
||||
archive_source: "trs_tool",
|
||||
trs_url: trsUrl,
|
||||
};
|
||||
const url = safePath("/api/workflows");
|
||||
try {
|
||||
const response = await axios.post(url, data);
|
||||
return response.data;
|
||||
} catch (e) {
|
||||
rethrowSimple(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,45 +1,33 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, onMounted } from "vue";
|
||||
import { fetcher } from "@/schema";
|
||||
|
||||
const jobLock = ref(false);
|
||||
const jobLockUpdating = ref(true);
|
||||
|
||||
const jobLockStatus = fetcher.path("/api/job_lock").method("get").create();
|
||||
const jobLockUpdate = fetcher.path("/api/job_lock").method("put").create();
|
||||
|
||||
watch(jobLock, async (newVal) => {
|
||||
jobLockUpdating.value = true;
|
||||
const { data } = await jobLockUpdate({ active: jobLock.value });
|
||||
jobLock.value = data.active;
|
||||
jobLockUpdating.value = false;
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
// TODO: see if we can upstream an optional arg when no params are required?
|
||||
// i.e. jobLockStatus() instead of jobLockStatus({})
|
||||
const response = await jobLockStatus({});
|
||||
jobLock.value = response.data.active;
|
||||
jobLockUpdating.value = false;
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<b-form-group>
|
||||
<b-form-checkbox id="prevent-job-dispatching" v-model="jobLock" switch size="lg">
|
||||
<b-form-checkbox id="prevent-job-dispatching" v-model="jobLock" :disabled="jobLockUpdating" switch size="lg">
|
||||
Job dispatching is currently
|
||||
<strong>{{ jobLockDisplay ? "locked" : "unlocked" }}</strong>
|
||||
<strong>{{ jobLock ? "locked" : "unlocked" }}</strong>
|
||||
</b-form-checkbox>
|
||||
</b-form-group>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getAppRoot } from "onload/loadConfig";
|
||||
import axios from "axios";
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
jobLock: false,
|
||||
jobLockDisplay: false,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
jobLock(newVal) {
|
||||
this.handleJobLock(axios.put(`${getAppRoot()}api/job_lock`, { active: this.jobLock }));
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.initJobLock();
|
||||
},
|
||||
methods: {
|
||||
initJobLock() {
|
||||
this.handleJobLock(axios.get(`${getAppRoot()}api/job_lock`));
|
||||
},
|
||||
handleJobLock(axiosPromise) {
|
||||
axiosPromise
|
||||
.then((response) => {
|
||||
this.jobLock = response.data.active;
|
||||
this.jobLockDisplay = response.data.active;
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -192,5 +192,4 @@ export const StoreProvider = (storeAction, storeGetter, storeCountGetter = undef
|
||||
|
||||
export const DatasetProvider = StoreProvider("fetchDataset", "getDataset");
|
||||
export const CollectionElementsProvider = StoreProvider("fetchCollectionElements", "getCollectionElements");
|
||||
export const HistoryItemsProvider = StoreProvider("fetchHistoryItems", "getHistoryItems", "getTotalMatchesCount");
|
||||
export const ToolsProvider = StoreProvider("fetchAllTools", "getTools");
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
import { ref } from "vue";
|
||||
import axios from "axios";
|
||||
import { getAppRoot } from "onload/loadConfig";
|
||||
|
||||
/**
|
||||
* Fetches a detailed array of datatypes available on this galaxy instance.
|
||||
* Does not cache the result or use a store.
|
||||
*/
|
||||
export function useDetailedDatatypes() {
|
||||
const datatypesLoading = ref(true);
|
||||
const datatypes = ref([]);
|
||||
|
||||
async function getDatatypes() {
|
||||
try {
|
||||
const datatypesPromise = axios.get(`${getAppRoot()}api/datatypes?extension_only=false`);
|
||||
const datatypeEDAMFormatsPromise = axios.get(`${getAppRoot()}api/datatypes/edam_formats/detailed`);
|
||||
const datatypeEDAMDataPromise = axios.get(`${getAppRoot()}api/datatypes/edam_data/detailed`);
|
||||
|
||||
const [baseTypes, datatypeEDAMFormats, datatypeEDAMData] = await Promise.all([
|
||||
datatypesPromise,
|
||||
datatypeEDAMFormatsPromise,
|
||||
datatypeEDAMDataPromise,
|
||||
]);
|
||||
|
||||
datatypes.value = baseTypes.data.map((type, i) => {
|
||||
const typeEDAMFormat = datatypeEDAMFormats.data[type.extension] ?? null;
|
||||
const typeEDAMData = datatypeEDAMData.data[type.extension] ?? null;
|
||||
|
||||
return {
|
||||
extension: type.extension,
|
||||
description: type.description,
|
||||
descriptionUrl: type.description_url,
|
||||
edamFormat: typeEDAMFormat.prefix_IRI,
|
||||
edamFormatLabel: typeEDAMFormat.label,
|
||||
edamFormatDefinition: typeEDAMFormat.definition,
|
||||
edamData: typeEDAMData.prefix_IRI,
|
||||
edamDataLabel: typeEDAMData.label,
|
||||
edamDataDefinition: typeEDAMData.definition,
|
||||
};
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("unable to fetch available datatypes\n", e);
|
||||
} finally {
|
||||
datatypesLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
getDatatypes();
|
||||
|
||||
return { datatypes, datatypesLoading };
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import { ref } from "vue";
|
||||
import { fetcher } from "@/schema";
|
||||
|
||||
import type { Ref } from "vue";
|
||||
|
||||
const datatypesFetcher = fetcher.path("/api/datatypes").method("get").create();
|
||||
const edamFormatsFetcher = fetcher.path("/api/datatypes/edam_formats/detailed").method("get").create();
|
||||
const edamDataFetcher = fetcher.path("/api/datatypes/edam_data/detailed").method("get").create();
|
||||
|
||||
export interface DetailedDatatypes {
|
||||
extension: string;
|
||||
description?: string;
|
||||
descriptionUrl?: string;
|
||||
edamFormat?: string;
|
||||
edamFormatLabel?: string;
|
||||
edamFormatDefinition?: string;
|
||||
edamData?: string;
|
||||
edamDataLabel?: string;
|
||||
edamDataDefinition?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches a detailed array of datatypes available on this galaxy instance.
|
||||
* Does not cache the result or use a store.
|
||||
*/
|
||||
export function useDetailedDatatypes() {
|
||||
const datatypesLoading = ref(true);
|
||||
const datatypes: Ref<DetailedDatatypes[]> = ref([]);
|
||||
|
||||
async function getDatatypes() {
|
||||
try {
|
||||
const datatypesPromise = datatypesFetcher({ extension_only: true });
|
||||
const datatypeEDAMFormatsPromise = edamFormatsFetcher({});
|
||||
const datatypeEDAMDataPromise = edamDataFetcher({});
|
||||
|
||||
const [baseTypes, datatypeEDAMFormats, datatypeEDAMData] = await Promise.all([
|
||||
datatypesPromise,
|
||||
datatypeEDAMFormatsPromise,
|
||||
datatypeEDAMDataPromise,
|
||||
]);
|
||||
|
||||
type BaseTypes = Exclude<typeof baseTypes.data, string[]>;
|
||||
|
||||
datatypes.value = (baseTypes.data as BaseTypes).map((type) => {
|
||||
const typeEDAMFormat = datatypeEDAMFormats.data[type.extension] ?? null;
|
||||
const typeEDAMData = datatypeEDAMData.data[type.extension] ?? null;
|
||||
|
||||
return {
|
||||
extension: type.extension,
|
||||
description: type.description,
|
||||
descriptionUrl: type.description_url,
|
||||
edamFormat: typeEDAMFormat?.prefix_IRI,
|
||||
edamFormatLabel: typeEDAMFormat?.label,
|
||||
edamFormatDefinition: typeEDAMFormat?.definition,
|
||||
edamData: typeEDAMData?.prefix_IRI,
|
||||
edamDataLabel: typeEDAMData?.label,
|
||||
edamDataDefinition: typeEDAMData?.definition,
|
||||
};
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("unable to fetch available datatypes\n", e);
|
||||
} finally {
|
||||
datatypesLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
getDatatypes();
|
||||
|
||||
return { datatypes, datatypesLoading };
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { ref, readonly, onMounted } from "vue";
|
||||
import { Services } from "components/FilesDialog/services";
|
||||
|
||||
const fileSourcesService = new Services();
|
||||
|
||||
/**
|
||||
* Composable for accessing and working with file sources.
|
||||
*/
|
||||
export function useFileSources() {
|
||||
const isLoading = ref(true);
|
||||
const hasWritable = ref(false);
|
||||
const fileSources = ref([]);
|
||||
|
||||
onMounted(async () => {
|
||||
fileSources.value = await fileSourcesService.getFileSources();
|
||||
hasWritable.value = fileSources.value.some((fs) => fs.writable);
|
||||
isLoading.value = false;
|
||||
});
|
||||
|
||||
return {
|
||||
/**
|
||||
* The list of available file sources from the server.
|
||||
*/
|
||||
fileSources: readonly(fileSources),
|
||||
/**
|
||||
* Whether the file sources are being loaded from the server.
|
||||
*/
|
||||
isLoading: readonly(isLoading),
|
||||
/**
|
||||
* Whether the user can write files to any of the available file sources.
|
||||
*/
|
||||
hasWritable: readonly(hasWritable),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import { computed, unref } from "vue";
|
||||
import { getAppRoot } from "onload/loadConfig";
|
||||
|
||||
/**
|
||||
* Increase the heading levels of all child nodes of a node
|
||||
* @param {HTMLElement} node
|
||||
* @param {number} increaseBy
|
||||
*/
|
||||
function increaseHeadingLevels(node, increaseBy) {
|
||||
[5, 4, 3, 2, 1].forEach((level) => {
|
||||
increaseHeadingLevel(node, level, increaseBy);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Increase a single heading level
|
||||
*/
|
||||
function increaseHeadingLevel(node, level, increaseBy) {
|
||||
// cap target level at 6 (highest heading level)
|
||||
let targetLevel = level + increaseBy;
|
||||
if (targetLevel > 6) {
|
||||
targetLevel = 6;
|
||||
}
|
||||
|
||||
const headings = node.getElementsByTagName(`h${level}`);
|
||||
|
||||
// create new headings with target level and copy contents + attributes
|
||||
Array.from(headings).forEach((heading) => {
|
||||
const newTag = document.createElement(`h${targetLevel}`);
|
||||
newTag.innerHTML = heading.innerHTML;
|
||||
|
||||
Array.from(heading.attributes).forEach((attribute) => {
|
||||
newTag.setAttribute(attribute.name, attribute.value);
|
||||
});
|
||||
|
||||
heading.insertAdjacentElement("beforebegin", newTag);
|
||||
heading.remove();
|
||||
});
|
||||
}
|
||||
|
||||
export function useFormattedToolHelp(helpContent, headingLevelIncrease = 2) {
|
||||
const formattedContent = computed(() => {
|
||||
const node = document.createElement("div");
|
||||
node.innerHTML = unref(helpContent);
|
||||
|
||||
const links = node.getElementsByTagName("a");
|
||||
Array.from(links).forEach((link) => {
|
||||
link.target = "_blank";
|
||||
});
|
||||
|
||||
const images = node.getElementsByTagName("img");
|
||||
Array.from(images).forEach((image) => {
|
||||
if (image.src.includes("admin_toolshed")) {
|
||||
image.src = getAppRoot() + image.src;
|
||||
}
|
||||
});
|
||||
|
||||
increaseHeadingLevels(node, unref(headingLevelIncrease));
|
||||
|
||||
return node.innerHTML;
|
||||
});
|
||||
|
||||
return { formattedContent };
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
import { ref, readonly } from "vue";
|
||||
import axios from "axios";
|
||||
import { safePath } from "utils/redirect";
|
||||
import { rethrowSimple } from "utils/simple-error";
|
||||
|
||||
export const DEFAULT_EXPORT_PARAMS = {
|
||||
modelStoreFormat: "rocrate.zip",
|
||||
includeFiles: true,
|
||||
includeDeleted: false,
|
||||
includeHidden: false,
|
||||
};
|
||||
|
||||
const DEFAULT_POLL_DELAY = 1000;
|
||||
const DEFAULT_OPTIONS = { exportParams: DEFAULT_EXPORT_PARAMS, pollDelayInMs: DEFAULT_POLL_DELAY };
|
||||
|
||||
/**
|
||||
* Composable to simplify and reuse the logic for downloading objects using Galaxy's Short Term Storage system.
|
||||
*/
|
||||
export function useShortTermStorage() {
|
||||
let timeout = null;
|
||||
let pollDelay = DEFAULT_POLL_DELAY;
|
||||
|
||||
const isPreparing = ref(false);
|
||||
|
||||
async function downloadHistory(historyId, options = DEFAULT_OPTIONS) {
|
||||
return prepareObjectDownload(historyId, "histories", options);
|
||||
}
|
||||
|
||||
async function downloadWorkflowInvocation(invocationId, options = DEFAULT_OPTIONS) {
|
||||
return prepareObjectDownload(invocationId, "invocations", options);
|
||||
}
|
||||
|
||||
function downloadObjectByRequestId(storageRequestId) {
|
||||
const url = safePath(`/api/short_term_storage/${storageRequestId}`);
|
||||
window.location.assign(url);
|
||||
}
|
||||
|
||||
async function prepareObjectDownload(object_id, object_api, options = DEFAULT_OPTIONS) {
|
||||
const finalOptions = Object.assign(DEFAULT_OPTIONS, options);
|
||||
resetTimeout();
|
||||
isPreparing.value = true;
|
||||
pollDelay = finalOptions.pollDelayInMs;
|
||||
const url = safePath(`/api/${object_api}/${object_id}/prepare_store_download`);
|
||||
const exportParams = {
|
||||
model_store_format: finalOptions.exportParams.modelStoreFormat,
|
||||
include_files: finalOptions.exportParams.includeFiles,
|
||||
include_deleted: finalOptions.exportParams.includeDeleted,
|
||||
include_hidden: finalOptions.exportParams.includeHidden,
|
||||
};
|
||||
|
||||
const response = await axios.post(url, exportParams).catch(handleError);
|
||||
handleInitialize(response);
|
||||
}
|
||||
|
||||
function handleInitialize(response) {
|
||||
const storageRequestId = response.data.storage_request_id;
|
||||
pollStorageRequestId(storageRequestId);
|
||||
}
|
||||
|
||||
function pollStorageRequestId(storageRequestId) {
|
||||
const url = safePath(`/api/short_term_storage/${storageRequestId}/ready`);
|
||||
axios
|
||||
.get(url)
|
||||
.then((r) => {
|
||||
handlePollResponse(r, storageRequestId);
|
||||
})
|
||||
.catch(handleError);
|
||||
}
|
||||
|
||||
function handlePollResponse(response, storageRequestId) {
|
||||
const ready = response.data;
|
||||
if (ready) {
|
||||
isPreparing.value = false;
|
||||
downloadObjectByRequestId(storageRequestId);
|
||||
} else {
|
||||
pollAfterDelay(storageRequestId);
|
||||
}
|
||||
}
|
||||
|
||||
function handleError(err) {
|
||||
rethrowSimple(err);
|
||||
isPreparing.value = false;
|
||||
}
|
||||
|
||||
function pollAfterDelay(storageRequestId) {
|
||||
resetTimeout();
|
||||
timeout = setTimeout(() => {
|
||||
pollStorageRequestId(storageRequestId);
|
||||
}, pollDelay);
|
||||
}
|
||||
|
||||
function resetTimeout() {
|
||||
if (timeout) {
|
||||
clearTimeout(timeout);
|
||||
timeout = null;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
/**
|
||||
* Starts preparing a history download file. When `isPreparing` is false the download will start automatically.
|
||||
* @param {String} historyId The ID of the history to be downloaded
|
||||
* @param {Object} options Options for the download preparation
|
||||
*/
|
||||
downloadHistory,
|
||||
/**
|
||||
* Starts preparing a workflow invocation download file. When `isPreparing` is false the download will start automatically.
|
||||
* @param {String} invocationId The ID of the workflow invocation to be downloaded
|
||||
* @param {Object} options Options for the download preparation
|
||||
*/
|
||||
downloadWorkflowInvocation,
|
||||
/**
|
||||
* Starts a direct download of the object associated with the given `storageRequestId`.
|
||||
* For the download to succeed, the associated `storageRequestId` must be `ready` and not in failure state.
|
||||
* @param {String} storageRequestId The storage request ID associated to the object to be downloaded
|
||||
*/
|
||||
downloadObjectByRequestId,
|
||||
/**
|
||||
* Whether the download is still being prepared.
|
||||
*/
|
||||
isPreparing: readonly(isPreparing),
|
||||
};
|
||||
}
|
||||
@@ -4,13 +4,14 @@ import axios from "axios";
|
||||
const SUCCESS_STATE = "SUCCESS";
|
||||
const FAILURE_STATE = "FAILURE";
|
||||
const TASK_READY_STATES = [SUCCESS_STATE, FAILURE_STATE];
|
||||
const POLL_DELAY = 1000;
|
||||
const DEFAULT_POLL_DELAY = 1000;
|
||||
|
||||
/**
|
||||
* Composable for waiting on Galaxy background tasks.
|
||||
*/
|
||||
export function useTaskMonitor() {
|
||||
let timeout = null;
|
||||
let pollDelay = DEFAULT_POLL_DELAY;
|
||||
|
||||
const isRunning = ref(false);
|
||||
const status = ref(null);
|
||||
@@ -21,7 +22,8 @@ export function useTaskMonitor() {
|
||||
const hasFailed = computed(() => status.value === FAILURE_STATE);
|
||||
const queryStateUrl = computed(() => `/api/tasks/${currentTaskId.value}/state`);
|
||||
|
||||
function waitForTask(taskId) {
|
||||
function waitForTask(taskId, pollDelayInMs = DEFAULT_POLL_DELAY) {
|
||||
pollDelay = pollDelayInMs;
|
||||
resetState();
|
||||
currentTaskId.value = taskId;
|
||||
isRunning.value = true;
|
||||
@@ -46,7 +48,7 @@ export function useTaskMonitor() {
|
||||
resetTimeout();
|
||||
timeout = setTimeout(() => {
|
||||
fetchTaskStatus(taskId);
|
||||
}, POLL_DELAY);
|
||||
}, pollDelay);
|
||||
}
|
||||
|
||||
function handleError(err) {
|
||||
@@ -69,8 +71,9 @@ export function useTaskMonitor() {
|
||||
return {
|
||||
/**
|
||||
* Waits for a particular task ID to be completed.
|
||||
* While the task is pending, the state will be updated every second by polling the server.
|
||||
* While the task is pending, the state will be updated every `pollDelayInMs` by polling the server.
|
||||
* @param {string} taskId The task ID
|
||||
* @param {Number} pollDelayInMs The time (milliseconds) between poll requests to update the task state.
|
||||
*/
|
||||
waitForTask,
|
||||
/**
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
import { ref } from "vue";
|
||||
|
||||
let toastRef = ref(null);
|
||||
|
||||
export const setToastComponentRef = (newRef) => {
|
||||
// eslint-disable-next-line vue/no-ref-as-operand
|
||||
toastRef = newRef;
|
||||
};
|
||||
|
||||
/**
|
||||
* Direct export to simplify usage in Options Api component.
|
||||
* Use 'useToast' for the Composition Api instead.
|
||||
*/
|
||||
export const Toast = {
|
||||
success(message, title = "Success") {
|
||||
toastRef.value.showToast(message, title, "success");
|
||||
},
|
||||
|
||||
info(message, title = "Info") {
|
||||
toastRef.value.showToast(message, title, "info");
|
||||
},
|
||||
|
||||
warning(message, title = "Warning") {
|
||||
toastRef.value.showToast(message, title, "warning");
|
||||
},
|
||||
|
||||
error(message, title = "Error") {
|
||||
toastRef.value.showToast(message, title, "error");
|
||||
},
|
||||
};
|
||||
|
||||
export function useToast() {
|
||||
return { ...Toast };
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { ref, type Ref } from "vue";
|
||||
|
||||
import type Vue from "vue";
|
||||
import type ToastComponentFile from "@/components/Toast";
|
||||
|
||||
export type ToastComponent = Vue & typeof ToastComponentFile.methods;
|
||||
|
||||
let toastRef: Ref<ToastComponent | null> = ref(null);
|
||||
|
||||
export const setToastComponentRef = (newRef: typeof toastRef) => {
|
||||
// eslint-disable-next-line vue/no-ref-as-operand
|
||||
toastRef = newRef;
|
||||
};
|
||||
|
||||
/**
|
||||
* Direct export to simplify usage in Options Api component.
|
||||
* Use 'useToast' for the Composition Api instead.
|
||||
*/
|
||||
export const Toast = {
|
||||
success(message: string, title = "Success") {
|
||||
toastRef.value?.showToast(message, title, "success");
|
||||
},
|
||||
|
||||
info(message: string, title = "Info") {
|
||||
toastRef.value?.showToast(message, title, "info");
|
||||
},
|
||||
|
||||
warning(message: string, title = "Warning") {
|
||||
toastRef.value?.showToast(message, title, "warning");
|
||||
},
|
||||
|
||||
error(message: string, title = "Error") {
|
||||
toastRef.value?.showToast(message, title, "error");
|
||||
},
|
||||
};
|
||||
|
||||
export function useToast() {
|
||||
return { ...Toast };
|
||||
}
|
||||
@@ -1,11 +1,10 @@
|
||||
import { standardInit, addInitialization } from "onload";
|
||||
|
||||
import Vue, { provide } from "vue";
|
||||
import App from "./App.vue";
|
||||
import store from "store";
|
||||
import { getRouter } from "./router";
|
||||
import { createPinia, PiniaVuePlugin } from "pinia";
|
||||
import { PiniaVuePlugin, createPinia } from "pinia";
|
||||
import piniaPluginPersistedstate from "pinia-plugin-persistedstate";
|
||||
import App from "./App.vue";
|
||||
import { getRouter } from "./router";
|
||||
import { addInitialization, standardInit } from "@/onload";
|
||||
import store from "@/store";
|
||||
|
||||
Vue.use(PiniaVuePlugin);
|
||||
const pinia = createPinia();
|
||||
|
||||
@@ -17,8 +17,6 @@ export function patchRouterPush(VueRouter) {
|
||||
if (force) {
|
||||
location = addSearchParams(location, { __vkey__: Date.now() });
|
||||
}
|
||||
// log upcoming location
|
||||
console.debug("VueRouter - push: ", location);
|
||||
// verify if confirmation is required
|
||||
if (this.confirmation) {
|
||||
if (confirm("There are unsaved changes which will be lost.")) {
|
||||
|
||||
@@ -62,6 +62,7 @@ import { APIKey } from "components/User/APIKey";
|
||||
import { CloudAuth } from "components/User/CloudAuth";
|
||||
import { ExternalIdentities } from "components/User/ExternalIdentities";
|
||||
import { HistoryExport } from "components/HistoryExport/index";
|
||||
import HistoryExportTasks from "components/History/Export/HistoryExport";
|
||||
import { StorageDashboardRouter } from "components/User/DiskUsage";
|
||||
|
||||
Vue.use(VueRouter);
|
||||
@@ -235,7 +236,9 @@ export function getRouter(Galaxy) {
|
||||
},
|
||||
{
|
||||
path: "histories/:historyId/export",
|
||||
component: HistoryExport,
|
||||
get component() {
|
||||
return Galaxy.config.enable_celery_tasks ? HistoryExportTasks : HistoryExport;
|
||||
},
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
@@ -469,6 +472,7 @@ export function getRouter(Galaxy) {
|
||||
queryTrsServer: route.query.trs_server,
|
||||
queryTrsId: route.query.trs_id,
|
||||
queryTrsVersionId: route.query.trs_version,
|
||||
queryTrsUrl: route.query.trs_url,
|
||||
isRun: route.query.run_form == "true",
|
||||
}),
|
||||
},
|
||||
|
||||
@@ -1,141 +0,0 @@
|
||||
import Backbone from "backbone";
|
||||
import { getAppRoot } from "onload/loadConfig";
|
||||
import axios from "axios";
|
||||
import _l from "utils/localization";
|
||||
import Ui from "mvc/ui/ui-misc";
|
||||
import Vue from "vue";
|
||||
import RuleBasedCollectionCreatorModal from "components/Collections/RuleBasedCollectionCreatorModal";
|
||||
import { RulesDisplay } from "components/RulesDisplay";
|
||||
|
||||
/**
|
||||
* Bridge rule based builder and the tool form.
|
||||
*/
|
||||
var View = Backbone.View.extend({
|
||||
initialize: function (options) {
|
||||
this.model = new Backbone.Model();
|
||||
this.target = options.target;
|
||||
const view = this;
|
||||
|
||||
// create insert edit button
|
||||
this.browse_button = new Ui.Button({
|
||||
title: _l("Edit"),
|
||||
icon: "fa fa-edit",
|
||||
tooltip: _l("Edit Rules"),
|
||||
onclick: () => {
|
||||
if (view.target) {
|
||||
view._fetchCollectionAndEdit();
|
||||
} else {
|
||||
view._showRuleEditor(null);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// add change event. fires on trigger
|
||||
this.on("change", () => {
|
||||
if (options.onchange) {
|
||||
options.onchange(this.value());
|
||||
}
|
||||
});
|
||||
|
||||
// create elements
|
||||
this.setElement(this._template(options));
|
||||
this.$(".ui-rules-edit-button").append(this.browse_button.$el);
|
||||
var rulesDisplayInstance = Vue.extend(RulesDisplay);
|
||||
var vm = document.createElement("div");
|
||||
this.$(".ui-rules-preview").append(vm);
|
||||
this.instance = new rulesDisplayInstance({
|
||||
propsData: {
|
||||
initialRules: {
|
||||
rules: [],
|
||||
mapping: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
this.instance.$mount(vm);
|
||||
},
|
||||
|
||||
_fetchCollectionAndEdit: function () {
|
||||
const view = this;
|
||||
const url = `${getAppRoot()}api/dataset_collections/${view.target.id}?instance_type=history`;
|
||||
axios
|
||||
.get(url)
|
||||
.then((response) => this._showCollection(response))
|
||||
.catch(view._renderFetchError);
|
||||
},
|
||||
|
||||
_showCollection: function (response) {
|
||||
const elements = response.data;
|
||||
this._showRuleEditor(elements);
|
||||
},
|
||||
|
||||
_showRuleEditor: function (elements) {
|
||||
const elementsType = "collection_contents";
|
||||
const importType = "collections";
|
||||
const value = this._value;
|
||||
const options = {
|
||||
saveRulesFn: (rules) => this._handleRulesSave(rules),
|
||||
initialRules: value,
|
||||
};
|
||||
RuleBasedCollectionCreatorModal.ruleBasedCollectionCreatorModal(
|
||||
elements,
|
||||
elementsType,
|
||||
importType,
|
||||
options
|
||||
).catch(() => {});
|
||||
},
|
||||
|
||||
_handleRulesSave: function (rules) {
|
||||
this._setValue(rules);
|
||||
},
|
||||
|
||||
_renderFetchError: function (e) {
|
||||
console.log(e);
|
||||
console.log("problem fetching collection");
|
||||
},
|
||||
|
||||
/** Main Template */
|
||||
_template: function (options) {
|
||||
return `
|
||||
<div class="ui-rules-edit clearfix">
|
||||
<span class="ui-rules-preview" />
|
||||
<span class="ui-rules-edit-button float-left" />
|
||||
</div>
|
||||
`;
|
||||
},
|
||||
|
||||
/** Return/Set current value */
|
||||
value: function (new_value) {
|
||||
if (new_value !== undefined) {
|
||||
this._setValue(new_value);
|
||||
} else {
|
||||
return this._getValue();
|
||||
}
|
||||
},
|
||||
|
||||
/** Update input element options */
|
||||
update: function (input_def) {
|
||||
this.target = input_def.target;
|
||||
},
|
||||
|
||||
/** Returns current value */
|
||||
_getValue: function () {
|
||||
return this._value;
|
||||
},
|
||||
|
||||
/** Sets current value */
|
||||
_setValue: function (new_value) {
|
||||
if (new_value) {
|
||||
if (typeof new_value == "string") {
|
||||
new_value = JSON.parse(new_value);
|
||||
}
|
||||
this._value = new_value;
|
||||
this.model.trigger("error", null);
|
||||
this.trigger("change");
|
||||
this.instance.inputRules = new_value;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export default {
|
||||
View: View,
|
||||
};
|
||||
@@ -31,8 +31,12 @@ export default Backbone.View.extend({
|
||||
this.$buttons = this.$(".charts-buttons");
|
||||
this.chart = new Chart({}, options);
|
||||
this.chart.plugin = options.visualization_plugin;
|
||||
this.chart.requiresConfirmation = asBoolean(this.chart.plugin.specs.confirm);
|
||||
this.chart.plugin.specs = this.chart.plugin.specs || {};
|
||||
if (Object.keys(this.chart.plugin.specs).length === 0) {
|
||||
this.chart.requiresConfirmation = false;
|
||||
} else {
|
||||
this.chart.requiresConfirmation = asBoolean(this.chart.plugin.specs.confirm);
|
||||
}
|
||||
this.chart_load = options.chart_load;
|
||||
this.message = new Ui.Message();
|
||||
this.deferred = new Deferred();
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
// Finds <link rel="index"> in head element and pulls root url fragment from
|
||||
// there That should probably be a <base> tag instead since that's how
|
||||
// they're using <link rel="index" />
|
||||
|
||||
import { serverPath } from "utils/serverPath";
|
||||
import { serverPath } from "@/utils/serverPath";
|
||||
|
||||
/**
|
||||
* Finds <link rel="index"> in head element and pulls root url fragment from
|
||||
* there That should probably be a <base> tag instead since that's how
|
||||
* they're using <link rel="index" />
|
||||
*
|
||||
* @param {string} [defaultRoot="/"]
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getRootFromIndexLink(defaultRoot = "/"): string {
|
||||
const links = document.getElementsByTagName("link");
|
||||
const indexLink = Array.from(links).find((link) => link.rel == "index");
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Fetcher } from "openapi-typescript-fetch";
|
||||
import { getAppRoot } from "onload/loadConfig";
|
||||
import { rethrowSimple } from "utils/simple-error";
|
||||
import { getAppRoot } from "@/onload/loadConfig";
|
||||
import { rethrowSimple } from "@/utils/simple-error";
|
||||
import type { paths } from "./schema";
|
||||
import type { Middleware } from "openapi-typescript-fetch";
|
||||
|
||||
|
||||
+1485
-909
File diff suppressed because it is too large
Load Diff
@@ -1,93 +0,0 @@
|
||||
/**
|
||||
* Requests history items by reacting to changes of filter props passed
|
||||
* to the history items provider used in the history panel.
|
||||
*/
|
||||
|
||||
import { reverse } from "lodash";
|
||||
import { LastQueue } from "utils/promise-queue";
|
||||
import { urlData } from "utils/url";
|
||||
import { mergeArray } from "./model/utilities";
|
||||
import { getFilters, getQueryString, testFilters } from "utils/filterConversion";
|
||||
|
||||
const limit = 100;
|
||||
const queue = new LastQueue();
|
||||
|
||||
const state = {
|
||||
items: {},
|
||||
itemKey: "hid",
|
||||
latestCreateTime: new Date(),
|
||||
totalMatchesCount: undefined,
|
||||
lastCheckedTime: new Date(),
|
||||
isWatching: false,
|
||||
};
|
||||
|
||||
const getters = {
|
||||
getHistoryItems:
|
||||
(state) =>
|
||||
({ historyId, filterText }) => {
|
||||
const itemArray = state.items[historyId] || [];
|
||||
const filters = getFilters(filterText);
|
||||
const filtered = itemArray.filter((item) => {
|
||||
if (!item) {
|
||||
return false;
|
||||
}
|
||||
if (!testFilters(filters, item)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
return reverse(filtered);
|
||||
},
|
||||
getLatestCreateTime: (state) => () => state.latestCreateTime,
|
||||
getTotalMatchesCount: (state) => () => state.totalMatchesCount,
|
||||
getLastCheckedTime: (state) => () => state.lastCheckedTime,
|
||||
getWatchingVisibility: (state) => () => state.isWatching,
|
||||
};
|
||||
|
||||
const actions = {
|
||||
fetchHistoryItems: async ({ commit }, { historyId, filterText, offset }) => {
|
||||
const queryString = getQueryString(filterText);
|
||||
const params = `v=dev&order=hid&offset=${offset}&limit=${limit}`;
|
||||
const url = `/api/histories/${historyId}/contents?${params}&${queryString}`;
|
||||
const headers = { accept: "application/vnd.galaxy.history.contents.stats+json" };
|
||||
await queue.enqueue(urlData, { url, headers }, historyId).then((data) => {
|
||||
const stats = data.stats;
|
||||
commit("saveQueryStats", { stats });
|
||||
const payload = data.contents;
|
||||
commit("saveHistoryItems", { historyId, payload });
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
const mutations = {
|
||||
saveHistoryItems: (state, { historyId, payload }) => {
|
||||
// merges incoming payload into existing state
|
||||
mergeArray(historyId, payload, state.items, state.itemKey);
|
||||
|
||||
// keep track of latest create time for items
|
||||
payload.forEach((item) => {
|
||||
if (item.state == "ok") {
|
||||
const itemCreateTime = new Date(item.create_time);
|
||||
if (itemCreateTime > state.latestCreateTime) {
|
||||
state.latestCreateTime = itemCreateTime;
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
setLastCheckedTime: (state, { checkForUpdate }) => {
|
||||
state.lastCheckedTime = checkForUpdate;
|
||||
},
|
||||
setWatchingVisibility: (state, { watchingVisibility }) => {
|
||||
state.isWatching = watchingVisibility;
|
||||
},
|
||||
saveQueryStats: (state, { stats }) => {
|
||||
state.totalMatchesCount = stats.total_matches;
|
||||
},
|
||||
};
|
||||
|
||||
export const historyItemsStore = {
|
||||
state,
|
||||
getters,
|
||||
actions,
|
||||
mutations,
|
||||
};
|
||||
@@ -1,4 +1,3 @@
|
||||
export { collectionElementsStore } from "./collectionElementsStore";
|
||||
export { datasetStore } from "./datasetStore";
|
||||
export { historyItemsStore } from "./historyItemsStore";
|
||||
export { historyStore } from "./historyStore";
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
*/
|
||||
|
||||
import defaultStore from "store/index";
|
||||
import { useHistoryItemsStore } from "stores/history/historyItemsStore";
|
||||
import { urlData } from "utils/url";
|
||||
import { loadSet } from "utils/setCache";
|
||||
import { getCurrentHistoryFromServer } from "./queries";
|
||||
@@ -37,12 +38,13 @@ function setVisibilityThrottle() {
|
||||
}
|
||||
|
||||
export async function watchHistoryOnce(store) {
|
||||
const historyItemsStore = useHistoryItemsStore();
|
||||
// "Reset" watchTimeout so we don't queue up watchHistory calls in rewatchHistory.
|
||||
watchTimeout = null;
|
||||
// get current history
|
||||
const checkForUpdate = new Date();
|
||||
const history = await getCurrentHistoryFromServer(lastUpdateTime);
|
||||
store.commit("setLastCheckedTime", { checkForUpdate });
|
||||
historyItemsStore.setLastCheckedTime(checkForUpdate);
|
||||
if (!history || !history.id) {
|
||||
return;
|
||||
}
|
||||
@@ -73,7 +75,7 @@ export async function watchHistoryOnce(store) {
|
||||
// pass changed items to attached stores
|
||||
store.commit("history/setHistory", history);
|
||||
store.commit("saveDatasets", { payload });
|
||||
store.commit("saveHistoryItems", { historyId, payload });
|
||||
historyItemsStore.saveHistoryItems(historyId, payload);
|
||||
store.commit("saveCollectionObjects", { payload });
|
||||
// trigger changes in legacy handler
|
||||
const Galaxy = getGalaxyInstance();
|
||||
@@ -86,10 +88,11 @@ export async function watchHistoryOnce(store) {
|
||||
}
|
||||
|
||||
export async function watchHistory(store = defaultStore) {
|
||||
const historyItemsStore = useHistoryItemsStore();
|
||||
// Only set up visibility listeners once, whenever a watch is first started
|
||||
if (watchingVisibility === false) {
|
||||
watchingVisibility = true;
|
||||
store.commit("setWatchingVisibility", { watchingVisibility });
|
||||
historyItemsStore.setWatchingVisibility(watchingVisibility);
|
||||
document.addEventListener("visibilitychange", setVisibilityThrottle);
|
||||
}
|
||||
try {
|
||||
@@ -98,7 +101,7 @@ export async function watchHistory(store = defaultStore) {
|
||||
// error alerting the user that watch history failed
|
||||
console.warn(error);
|
||||
watchingVisibility = false;
|
||||
store.commit("setWatchingVisibility", { watchingVisibility });
|
||||
historyItemsStore.setWatchingVisibility(watchingVisibility);
|
||||
} finally {
|
||||
watchTimeout = setTimeout(() => {
|
||||
watchHistory(store);
|
||||
|
||||
@@ -4,22 +4,21 @@ import { watchHistoryOnce } from "./watchHistory";
|
||||
import { collectionElementsStore } from "store/historyStore/collectionElementsStore";
|
||||
import { datasetStore } from "store/historyStore/datasetStore";
|
||||
import { historyStore } from "store/historyStore/historyStore";
|
||||
import { historyItemsStore } from "store/historyStore/historyItemsStore";
|
||||
import { useHistoryItemsStore } from "stores/history/historyItemsStore";
|
||||
import { createPinia, mapState } from "pinia";
|
||||
import { mount, createLocalVue } from "@vue/test-utils";
|
||||
import Vuex from "vuex";
|
||||
|
||||
const pinia = createPinia();
|
||||
|
||||
const testApp = {
|
||||
template: `<div/>`,
|
||||
computed: {
|
||||
...mapState(useHistoryItemsStore, ["getHistoryItems"]),
|
||||
currentHistoryId() {
|
||||
return this.$store.getters["history/currentHistoryId"];
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
getHistoryItems(filterObj) {
|
||||
return this.$store.getters["getHistoryItems"](filterObj);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
describe("watchHistory", () => {
|
||||
@@ -54,6 +53,7 @@ describe("watchHistory", () => {
|
||||
axiosMock = new MockAdapter(axios);
|
||||
const localVue = createLocalVue();
|
||||
localVue.use(Vuex);
|
||||
useHistoryItemsStore(pinia);
|
||||
|
||||
wrapper = mount(testApp, {
|
||||
store: new Vuex.Store({
|
||||
@@ -61,10 +61,10 @@ describe("watchHistory", () => {
|
||||
collectionElements: collectionElementsStore,
|
||||
dataset: datasetStore,
|
||||
history: historyStore,
|
||||
historyItems: historyItemsStore,
|
||||
},
|
||||
}),
|
||||
localVue,
|
||||
pinia,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -79,9 +79,9 @@ describe("watchHistory", () => {
|
||||
.onGet(/api\/histories\/history-id\/contents?.*/)
|
||||
.replyOnce(200, historyItems);
|
||||
await watchHistoryOnce(wrapper.vm.$store);
|
||||
expect(wrapper.vm.getHistoryItems({ historyId: "history-id", filterText: "" }).length).toBe(2);
|
||||
expect(wrapper.vm.getHistoryItems({ historyId: "history-id", filterText: "second" })[0].hid).toBe(2);
|
||||
expect(wrapper.vm.getHistoryItems({ historyId: "history-id", filterText: "state:ok" })[0].hid).toBe(1);
|
||||
expect(wrapper.vm.getHistoryItems("history-id", "").length).toBe(2);
|
||||
expect(wrapper.vm.getHistoryItems("history-id", "second")[0].hid).toBe(2);
|
||||
expect(wrapper.vm.getHistoryItems("history-id", "state:ok")[0].hid).toBe(1);
|
||||
});
|
||||
|
||||
it("survives a failing request", async () => {
|
||||
@@ -96,7 +96,7 @@ describe("watchHistory", () => {
|
||||
|
||||
await watchHistoryOnce(wrapper.vm.$store);
|
||||
expect(wrapper.vm.currentHistoryId).toBe("history-id");
|
||||
expect(wrapper.vm.getHistoryItems({ historyId: "history-id", filterText: "" }).length).toBe(2);
|
||||
expect(wrapper.vm.getHistoryItems("history-id", "").length).toBe(2);
|
||||
try {
|
||||
await watchHistoryOnce(wrapper.vm.$store);
|
||||
} catch (error) {
|
||||
@@ -123,6 +123,6 @@ describe("watchHistory", () => {
|
||||
]);
|
||||
await watchHistoryOnce(wrapper.vm.$store);
|
||||
// We should have received the update and have 3 items in the history
|
||||
expect(wrapper.vm.getHistoryItems({ historyId: "history-id", filterText: "" }).length).toBe(3);
|
||||
expect(wrapper.vm.getHistoryItems("history-id", "").length).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -15,7 +15,7 @@ import { tagStore } from "./tagStore";
|
||||
import { jobMetricsStore } from "./jobMetricsStore";
|
||||
import { jobDestinationParametersStore } from "./jobDestinationParametersStore";
|
||||
import { invocationStore } from "./invocationStore";
|
||||
import { collectionElementsStore, datasetStore, historyItemsStore, historyStore } from "./historyStore";
|
||||
import { collectionElementsStore, datasetStore, historyStore } from "./historyStore";
|
||||
import { userStore, userFlagsStore } from "./userStore";
|
||||
import { configStore } from "./configStore";
|
||||
import { toolStore } from "./toolStore";
|
||||
@@ -66,7 +66,6 @@ export function createStore() {
|
||||
jobMetrics: jobMetricsStore,
|
||||
gridSearch: gridSearchStore,
|
||||
history: historyStore,
|
||||
historyItems: historyItemsStore,
|
||||
panels: panelStore,
|
||||
tags: tagStore,
|
||||
tools: toolStore,
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
/**
|
||||
* Requests history items by reacting to changes of filter props passed
|
||||
* to the history items provider used in the history panel.
|
||||
*/
|
||||
|
||||
import { defineStore } from "pinia";
|
||||
|
||||
import { reverse } from "lodash";
|
||||
import { LastQueue } from "utils/promise-queue";
|
||||
import { urlData } from "utils/url";
|
||||
import { mergeArray } from "store/historyStore/model/utilities";
|
||||
import { getFilters, getQueryString, testFilters } from "utils/filterConversion";
|
||||
|
||||
const limit = 100;
|
||||
const queue = new LastQueue();
|
||||
|
||||
export const useHistoryItemsStore = defineStore("historyItemsStore", {
|
||||
state: () => ({
|
||||
items: {},
|
||||
itemKey: "hid",
|
||||
latestCreateTime: new Date(),
|
||||
totalMatchesCount: undefined,
|
||||
lastCheckedTime: new Date(),
|
||||
isWatching: false,
|
||||
}),
|
||||
getters: {
|
||||
getHistoryItems: (state) => {
|
||||
return (historyId, filterText) => {
|
||||
const itemArray = state.items[historyId] || [];
|
||||
const filters = getFilters(filterText);
|
||||
const filtered = itemArray.filter((item) => {
|
||||
if (!item) {
|
||||
return false;
|
||||
}
|
||||
if (!testFilters(filters, item)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
return reverse(filtered);
|
||||
};
|
||||
},
|
||||
getLatestCreateTime: (state) => {
|
||||
return state.latestCreateTime;
|
||||
},
|
||||
getTotalMatchesCount: (state) => {
|
||||
return state.totalMatchesCount;
|
||||
},
|
||||
getLastCheckedTime: (state) => {
|
||||
return state.lastCheckedTime;
|
||||
},
|
||||
getWatchingVisibility: (state) => {
|
||||
return state.isWatching;
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
async fetchHistoryItems(historyId, filterText, offset) {
|
||||
const queryString = getQueryString(filterText);
|
||||
const params = `v=dev&order=hid&offset=${offset}&limit=${limit}`;
|
||||
const url = `/api/histories/${historyId}/contents?${params}&${queryString}`;
|
||||
const headers = { accept: "application/vnd.galaxy.history.contents.stats+json" };
|
||||
await queue.enqueue(urlData, { url, headers }, historyId).then((data) => {
|
||||
const stats = data.stats;
|
||||
this.totalMatchesCount = stats.total_matches;
|
||||
const payload = data.contents;
|
||||
this.saveHistoryItems(historyId, payload);
|
||||
});
|
||||
},
|
||||
// Setters
|
||||
saveHistoryItems(historyId, payload) {
|
||||
this.$patch((state) => {
|
||||
// merges incoming payload into existing state
|
||||
mergeArray(historyId, payload, state.items, state.itemKey);
|
||||
// keep track of latest create time for items
|
||||
payload.forEach((item) => {
|
||||
if (item.state == "ok") {
|
||||
const itemCreateTime = new Date(item.create_time);
|
||||
if (itemCreateTime > state.latestCreateTime) {
|
||||
state.latestCreateTime = itemCreateTime;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
setLastCheckedTime(checkForUpdate) {
|
||||
this.lastCheckedTime = checkForUpdate;
|
||||
},
|
||||
setWatchingVisibility(watchingVisibility) {
|
||||
this.isWatching = watchingVisibility;
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -102,15 +102,6 @@ $ui-margin-horizontal-large: $margin-v * 2;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.ui-btn-back-to-top {
|
||||
bottom: 2em;
|
||||
position: fixed;
|
||||
opacity: 0.5;
|
||||
}
|
||||
.ui-btn-back-to-top:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
// thumbnails
|
||||
.ui-thumbnails {
|
||||
.ui-thumbnails-item {
|
||||
@@ -442,3 +433,18 @@ $ui-margin-horizontal-large: $margin-v * 2;
|
||||
.h-text {
|
||||
font-size: $font-size-base;
|
||||
}
|
||||
|
||||
// makes a button element look like a link
|
||||
.ui-link {
|
||||
border: none !important;
|
||||
background: none !important;
|
||||
padding: 0 !important;
|
||||
color: $brand-primary;
|
||||
display: inline;
|
||||
line-height: unset;
|
||||
vertical-align: unset;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,54 +1,54 @@
|
||||
// eslint-disable-next-line no-undef
|
||||
define(["i18n!nls/locale"], function (localeStrings) {
|
||||
/**
|
||||
* @param {String} strToLocalize
|
||||
* @returns
|
||||
*/
|
||||
var localize = function (strToLocalize) {
|
||||
return localeStrings[strToLocalize] || strToLocalize;
|
||||
};
|
||||
import localeStrings from "i18n!nls/locale";
|
||||
|
||||
localize.cacheNonLocalized = false;
|
||||
/**
|
||||
* @param {String} strToLocalize
|
||||
* @returns
|
||||
*/
|
||||
|
||||
localize._setUserLocale = function _setUserLocale(user, config) {
|
||||
const global_locale =
|
||||
config.default_locale && config.default_locale != "auto" ? config.default_locale.toLowerCase() : false;
|
||||
let localeStringsSubset = [];
|
||||
|
||||
let extra_user_preferences = {};
|
||||
if (user && user.attributes.preferences && "extra_user_preferences" in user.attributes.preferences) {
|
||||
extra_user_preferences = JSON.parse(user.attributes.preferences.extra_user_preferences);
|
||||
export function localize(strToLocalize) {
|
||||
return localeStringsSubset[strToLocalize] || strToLocalize;
|
||||
}
|
||||
|
||||
export function _setUserLocale(user, config) {
|
||||
const global_locale =
|
||||
config.default_locale && config.default_locale != "auto" ? config.default_locale.toLowerCase() : false;
|
||||
|
||||
let extra_user_preferences = {};
|
||||
if (user && user.attributes.preferences && "extra_user_preferences" in user.attributes.preferences) {
|
||||
extra_user_preferences = JSON.parse(user.attributes.preferences.extra_user_preferences);
|
||||
}
|
||||
|
||||
let user_locale =
|
||||
"localization|locale" in extra_user_preferences
|
||||
? extra_user_preferences["localization|locale"].toLowerCase()
|
||||
: false;
|
||||
|
||||
if (user_locale == "auto") {
|
||||
user_locale = false;
|
||||
}
|
||||
|
||||
const nav_locale =
|
||||
typeof navigator === "undefined"
|
||||
? "__root"
|
||||
: (navigator.language || navigator.userLanguage || "__root").toLowerCase();
|
||||
|
||||
const locale = user_locale || nav_locale || global_locale;
|
||||
|
||||
sessionStorage.setItem("currentLocale", locale);
|
||||
}
|
||||
|
||||
export function _getUserLocale(user, config) {
|
||||
let locale = null;
|
||||
if (Object.prototype.hasOwnProperty.call(localeStrings, "__root")) {
|
||||
locale = sessionStorage.getItem("currentLocale");
|
||||
if (locale) {
|
||||
localeStringsSubset =
|
||||
localeStrings["__" + locale] || localeStrings["__" + locale.split("-")[0]] || localeStrings.__root;
|
||||
}
|
||||
}
|
||||
return locale;
|
||||
}
|
||||
|
||||
let user_locale =
|
||||
"localization|locale" in extra_user_preferences
|
||||
? extra_user_preferences["localization|locale"].toLowerCase()
|
||||
: false;
|
||||
|
||||
if (user_locale == "auto") {
|
||||
user_locale = false;
|
||||
}
|
||||
|
||||
const nav_locale =
|
||||
typeof navigator === "undefined"
|
||||
? "__root"
|
||||
: (navigator.language || navigator.userLanguage || "__root").toLowerCase();
|
||||
|
||||
const locale = user_locale || nav_locale || global_locale;
|
||||
|
||||
sessionStorage.setItem("currentLocale", locale);
|
||||
};
|
||||
|
||||
localize._getUserLocale = function _getUserLocale(user, config) {
|
||||
let locale = null;
|
||||
if (Object.prototype.hasOwnProperty.call(localeStrings, "__root")) {
|
||||
locale = sessionStorage.getItem("currentLocale");
|
||||
if (locale) {
|
||||
localeStrings =
|
||||
localeStrings["__" + locale] || localeStrings["__" + locale.split("-")[0]] || localeStrings.__root;
|
||||
}
|
||||
}
|
||||
return locale;
|
||||
};
|
||||
|
||||
return localize;
|
||||
});
|
||||
export default localize;
|
||||
|
||||
@@ -420,6 +420,28 @@ history_export:
|
||||
export_button: '.export-button'
|
||||
success_message: '.history-export-component .alert-success'
|
||||
|
||||
history_export_tasks:
|
||||
selectors:
|
||||
direct_download: '.direct-download-btn'
|
||||
file_source_tab: '.tab-export-to-file'
|
||||
remote_file_name_input: '#file-source-tab #name'
|
||||
toggle_options_link: '#toggle-options-link'
|
||||
export_format_selector: '#format-selector'
|
||||
select_format:
|
||||
type: xpath
|
||||
selector: '//select[@id="format-selector"]/option[@value="${format}"]'
|
||||
|
||||
last_export_record:
|
||||
selectors:
|
||||
details: '.export-record-details'
|
||||
preparing_export: '.export-record-details .loading-icon'
|
||||
export_format: '.record-archive-format'
|
||||
up_to_date_icon: '.record-up-to-date-icon'
|
||||
outdated_icon: '.record-outdated-icon'
|
||||
expiration_warning_icon: '.record-expiration-warning-icon'
|
||||
download_btn: '.record-download-btn'
|
||||
reimport_btn: '.record-reimport-btn'
|
||||
|
||||
history_import:
|
||||
selectors:
|
||||
radio_button_remote_files: '.history-import-component .fa-folder-open'
|
||||
@@ -525,6 +547,8 @@ trs_import:
|
||||
select_server:
|
||||
type: xpath
|
||||
selector: "//a[contains(@class, 'dropdown-item') and text() = '${server}']"
|
||||
url_input: "#trs-import-url-input"
|
||||
url_import_button: "#trs-url-import-button"
|
||||
|
||||
workflow_run:
|
||||
selectors:
|
||||
@@ -918,4 +942,3 @@ job_details:
|
||||
type: xpath
|
||||
selector: '//td[@id="galaxy-tool-id"][normalize-space(text()) = "${tool_id}"]'
|
||||
tool_exit_code: '#exit-code'
|
||||
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
/**
|
||||
* Returns the path of the current or given URL.
|
||||
*
|
||||
* @param {string} [rawUrl=window.location.href]
|
||||
* @returns {string}
|
||||
*/
|
||||
|
||||
export function serverPath(rawUrl: string = window.location.href): string {
|
||||
const url = new URL(rawUrl);
|
||||
return url.pathname;
|
||||
|
||||
@@ -4,7 +4,6 @@ import { rethrowSimple } from "utils/simple-error";
|
||||
|
||||
export async function urlData({ url, headers, params }) {
|
||||
try {
|
||||
console.debug("Requesting data from: ", url);
|
||||
headers = headers || {};
|
||||
params = params || {};
|
||||
const { data } = await axios.get(safePath(url), { headers, params });
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
const path = require("path");
|
||||
|
||||
const { defaults: tsjPreset } = require("ts-jest/presets");
|
||||
|
||||
// For a detailed explanation regarding each configuration property, visit:
|
||||
@@ -16,191 +15,35 @@ const modulesToTransform = [
|
||||
].join("|");
|
||||
|
||||
module.exports = {
|
||||
// All imported modules in your tests should be mocked automatically
|
||||
// automock: false,
|
||||
|
||||
// Stop running tests after `n` failures
|
||||
// bail: 0,
|
||||
|
||||
// The directory where Jest should store its cached dependency information
|
||||
// cacheDirectory: "/private/var/folders/gm/53ksmsz51njf80yl3snw6g500000gn/T/jest_dx",
|
||||
|
||||
// Automatically clear mock calls and instances between every test
|
||||
preset: "ts-jest",
|
||||
clearMocks: true,
|
||||
|
||||
// Indicates whether the coverage information should be collected while executing the test
|
||||
// collectCoverage: false,
|
||||
|
||||
// An array of glob patterns indicating a set of files for which coverage information should be collected
|
||||
// collectCoverageFrom: undefined,
|
||||
|
||||
// The directory where Jest should output its coverage files
|
||||
coverageDirectory: "coverage",
|
||||
|
||||
// An array of regexp pattern strings used to skip coverage collection
|
||||
// coveragePathIgnorePatterns: [
|
||||
// "/node_modules/"
|
||||
// ],
|
||||
|
||||
// A list of reporter names that Jest uses when writing coverage reports
|
||||
// coverageReporters: [
|
||||
// "json",
|
||||
// "text",
|
||||
// "lcov",
|
||||
// "clover"
|
||||
// ],
|
||||
|
||||
// An object that configures minimum threshold enforcement for coverage results
|
||||
// coverageThreshold: undefined,
|
||||
|
||||
// A path to a custom dependency extractor
|
||||
// dependencyExtractor: undefined,
|
||||
|
||||
// Make calling deprecated APIs throw helpful error messages
|
||||
// errorOnDeprecated: false,
|
||||
|
||||
// Force coverage collection from ignored files using an array of glob patterns
|
||||
// forceCoverageMatch: [],
|
||||
|
||||
// A path to a module which exports an async function that is triggered once before all test suites
|
||||
// globalSetup: undefined,
|
||||
|
||||
// A path to a module which exports an async function that is triggered once after all test suites
|
||||
// globalTeardown: undefined,
|
||||
|
||||
// A set of global variables that need to be available in all test environments
|
||||
globals: { __webpack_public_path__: "" },
|
||||
|
||||
// The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers.
|
||||
// maxWorkers: "50%",
|
||||
|
||||
// An array of directory names to be searched recursively up from the requiring module's location
|
||||
// moduleDirectories: [
|
||||
// "node_modules"
|
||||
// ],
|
||||
|
||||
// An array of file extensions your modules use
|
||||
moduleFileExtensions: ["js", "json", "vue", "yml", "txt", "ts"],
|
||||
|
||||
modulePaths: ["<rootDir>/src/", "<rootDir>/tests/","<rootDir>/node_modules/", "./"],
|
||||
|
||||
// A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module
|
||||
// Some of these should turn into true mocks, instead of this module name mapping hack.
|
||||
moduleFileExtensions: ["js", "ts", "json", "vue", "yml", "txt"],
|
||||
modulePaths: ["<rootDir>/src/", "<rootDir>/tests/", "<rootDir>/node_modules/", "./"],
|
||||
moduleNameMapper: {
|
||||
"\\.(css|scss)$": "<rootDir>/tests/jest/__mocks__/style.js",
|
||||
"^@fontsource/.*": "<rootDir>/tests/jest/__mocks__/font.js",
|
||||
"^config$": "<rootDir>/tests/jest/__mocks__/config.js",
|
||||
handsontable: "node_modules/handsontable/dist/handsontable.js",
|
||||
"utils/localization$": "<rootDir>/tests/jest/__mocks__/localization.js",
|
||||
"viz/trackster$": "<rootDir>/tests/jest/__mocks__/trackster.js",
|
||||
"rxjs/internal/scheduler/AsyncScheduler":
|
||||
"<rootDir>/node_modules/rxjs/dist/esm/internal/scheduler/AsyncScheduler.js",
|
||||
"^@/(.*)$": "<rootDir>/src/$1",
|
||||
"^@tests/(.*)$": "<rootDir>/tests/$1",
|
||||
},
|
||||
|
||||
// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
|
||||
modulePathIgnorePatterns: ["<rootDir>/src/.*/__mocks__"],
|
||||
|
||||
// Activates notifications for test results
|
||||
// notify: false,
|
||||
|
||||
// An enum that specifies notification mode. Requires { notify: true }
|
||||
// notifyMode: "failure-change",
|
||||
|
||||
// A preset that is used as a base for Jest's configuration
|
||||
preset: "ts-jest",
|
||||
|
||||
// Run tests from one or more projects
|
||||
// projects: undefined,
|
||||
|
||||
// Use this configuration option to add custom reporters to Jest
|
||||
// reporters: undefined,
|
||||
|
||||
// Automatically reset mock state between every test
|
||||
// resetMocks: false,
|
||||
|
||||
// Reset the module registry before running each individual test
|
||||
// resetModules: false,
|
||||
|
||||
// A path to a custom resolver
|
||||
// resolver: undefined,
|
||||
|
||||
// Automatically restore mock state between every test
|
||||
// restoreMocks: false,
|
||||
|
||||
// The root directory that Jest should scan for tests and modules within
|
||||
// This should be the galaxy `client` directory.
|
||||
rootDir: path.join(__dirname, "../../"),
|
||||
|
||||
// A list of paths to directories that Jest should use to search for files in
|
||||
roots: ["<rootDir>", "<rootDir>/src/", "<rootDir>/tests/jest/standalone/", "<rootDir>/docs/"],
|
||||
|
||||
// Allows you to use a custom runner instead of Jest's default test runner
|
||||
// runner: "jest-runner",
|
||||
|
||||
// The paths to modules that run some code to configure or set up the testing environment before each test
|
||||
// setupFiles: [],
|
||||
|
||||
// A list of paths to modules that run some code to configure or set up the testing framework before each test
|
||||
roots: ["<rootDir>/src/", "<rootDir>/tests/jest/standalone/"],
|
||||
setupFilesAfterEnv: ["<rootDir>/tests/jest/jest.setup.js"],
|
||||
|
||||
// A list of paths to snapshot serializer modules Jest should use for snapshot testing
|
||||
// snapshotSerializers: [],
|
||||
|
||||
// The test environment that will be used for testing
|
||||
testEnvironment: "jsdom",
|
||||
|
||||
// Options that will be passed to the testEnvironment
|
||||
// testEnvironmentOptions: {},
|
||||
|
||||
// Adds a location field to test results
|
||||
// testLocationInResults: false,
|
||||
|
||||
// The glob patterns Jest uses to detect test files
|
||||
// testMatch: [
|
||||
// "**/__tests__/**/*.[jt]s?(x)",
|
||||
// "**/?(*.)+(spec|test).[tj]s?(x)"
|
||||
// ],
|
||||
|
||||
// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
|
||||
// testPathIgnorePatterns: [
|
||||
// "/node_modules/"
|
||||
// ],
|
||||
|
||||
// The regexp pattern or array of patterns that Jest uses to detect test files
|
||||
// testRegex: [],
|
||||
|
||||
// This option allows the use of a custom results processor
|
||||
// testResultsProcessor: undefined,
|
||||
|
||||
// This option allows use of a custom test runner
|
||||
// testRunner: "jasmine2",
|
||||
|
||||
// This option sets the URL for the jsdom environment. It is reflected in properties such as location.href
|
||||
// testURL: "http://localhost",
|
||||
|
||||
// Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout"
|
||||
// timers: "real",
|
||||
|
||||
// A map from regular expressions to paths to transformers
|
||||
transform: {
|
||||
...tsjPreset.transform,
|
||||
"^.+\\.js$": "babel-jest",
|
||||
"^.*\\.(vue)$": "@vue/vue2-jest",
|
||||
"^.+\\.ya?ml$": "<rootDir>/tests/jest/yaml-jest.js",
|
||||
"^.+\\.txt$": "<rootDir>/tests/jest/jest-raw-loader.js",
|
||||
...tsjPreset.transform,
|
||||
},
|
||||
|
||||
// An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
|
||||
transformIgnorePatterns: [`/node_modules/(?!${modulesToTransform})`],
|
||||
|
||||
// An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
|
||||
// unmockedModulePathPatterns: undefined,
|
||||
|
||||
// Indicates whether each individual test should be reported during the run
|
||||
// verbose: undefined,
|
||||
|
||||
// An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
|
||||
// watchPathIgnorePatterns: [],
|
||||
|
||||
// Whether to use watchman for file crawling
|
||||
// watchman: true,
|
||||
};
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import "@testing-library/jest-dom";
|
||||
import Vue from 'vue';
|
||||
|
||||
// Set Vue to suppress production / devtools / etc. warnings
|
||||
Vue.config.productionTip = false;
|
||||
Vue.config.devtools = false;
|
||||
|
||||
/* still don't understand what was invoking the following, but nothing should,
|
||||
and this makes the tag tests work correctly */
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import RuleDefs from "components/RuleBuilder/rule-definitions";
|
||||
import RuleDefs from "@/components/RuleBuilder/rule-definitions";
|
||||
import SPEC_TEST_CASES from "./rules_dsl_spec.yml";
|
||||
|
||||
function applyRules(rules: Array<any>, data: Array<Array<string>>, sources: Array<number>) {
|
||||
|
||||
@@ -3,16 +3,20 @@
|
||||
"compilerOptions": {
|
||||
"target": "es2016",
|
||||
"module": "es6",
|
||||
"baseUrl": "./src",
|
||||
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["src/*"],
|
||||
"@tests/*": ["tests/*"]
|
||||
},
|
||||
"outDir": "./dist/build",
|
||||
|
||||
"allowJs": true,
|
||||
"checkJs": false,
|
||||
|
||||
"outDir": "./dist/build",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"vueCompilerOptions": {
|
||||
"target": 2.7
|
||||
},
|
||||
"include": ["./types/*.d.ts", "./src/**/*.ts", "./src/**/*.tsx", "./src/**/*.vue", "./tests/**/*.ts"],
|
||||
"include": ["./types/*.d.ts", "./src/**/*.ts", "./src/**/*.tsx", "./src/**/*.vue", "./tests/**/*.ts"]
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ const MiniCssExtractPlugin = require("mini-css-extract-plugin");
|
||||
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
|
||||
const DuplicatePackageCheckerPlugin = require("@cerner/duplicate-package-checker-webpack-plugin");
|
||||
const { DumpMetaPlugin } = require("dumpmeta-webpack-plugin");
|
||||
const TsconfigPathsPlugin = require("tsconfig-paths-webpack-plugin");
|
||||
|
||||
const scriptsBase = path.join(__dirname, "src");
|
||||
const testsBase = path.join(__dirname, "tests");
|
||||
@@ -43,6 +44,7 @@ module.exports = (env = {}, argv = {}) => {
|
||||
filename: "[name].bundled.js",
|
||||
},
|
||||
resolve: {
|
||||
plugins: [new TsconfigPathsPlugin({ extensions: [".ts", ".js", ".json", ".vue", ".scss"] })],
|
||||
extensions: [".ts", ".js", ".json", ".vue", ".scss"],
|
||||
modules: [scriptsBase, "node_modules", styleBase, testsBase],
|
||||
fallback: {
|
||||
|
||||
+47
-14
@@ -1405,7 +1405,7 @@
|
||||
resolved "https://registry.yarnpkg.com/@fortawesome/vue-fontawesome/-/vue-fontawesome-2.0.9.tgz#7df35818135be54fd87568f16ec7b998a3d0cce8"
|
||||
integrity sha512-tUmO92PFHbLOplitjHNBVGMJm6S57vp16tBXJVPKSI/6CfjrgLycqKxEpC6f7qsOqUdoXs5nIv4HLUfrOMHzuw==
|
||||
|
||||
"@handsontable/vue@^2.0.0-beta1":
|
||||
"@handsontable/vue@^2.0.0":
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@handsontable/vue/-/vue-2.0.0.tgz#28ae7d247a89738088abc32584562925dde18db0"
|
||||
integrity sha512-QVqJywrQWwJzoDCXibZkXrg+T91hNCTCN9qznmum4FSewL0MUtaHotv7Zc/D4scvEwpwWnKx5vpw4CY14V4OYw==
|
||||
@@ -3067,6 +3067,11 @@ big.js@^5.2.2:
|
||||
resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328"
|
||||
integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==
|
||||
|
||||
bignumber.js@^8.1.1:
|
||||
version "8.1.1"
|
||||
resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-8.1.1.tgz#4b072ae5aea9c20f6730e4e5d529df1271c4d885"
|
||||
integrity sha512-QD46ppGintwPGuL1KqmwhR0O+N2cZUg8JG/VzwI2e28sM9TqHjQB10lI4QAaMHVbLzwVLLAwEglpKPViWX+5NQ==
|
||||
|
||||
binary-extensions@^2.0.0:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d"
|
||||
@@ -4427,6 +4432,14 @@ enhanced-resolve@^5.10.0:
|
||||
graceful-fs "^4.2.4"
|
||||
tapable "^2.2.0"
|
||||
|
||||
enhanced-resolve@^5.7.0:
|
||||
version "5.12.0"
|
||||
resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.12.0.tgz#300e1c90228f5b570c4d35babf263f6da7155634"
|
||||
integrity sha512-QHTXI/sZQmko1cbDoNAa3mJ5qhWUUNAq3vR0/YiD379fWQrcfuoX1+HW2S0MTt7XmoPLapdaDKUtelUSPic7hQ==
|
||||
dependencies:
|
||||
graceful-fs "^4.2.4"
|
||||
tapable "^2.2.0"
|
||||
|
||||
ent@~2.2.0:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/ent/-/ent-2.2.0.tgz#e964219325a21d05f44466a2f686ed6ce5f5dd1d"
|
||||
@@ -5523,13 +5536,13 @@ handle-thing@^2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-2.0.1.tgz#857f79ce359580c340d43081cc648970d0bb234e"
|
||||
integrity sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==
|
||||
|
||||
handsontable@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/handsontable/-/handsontable-2.0.0.tgz#b22fbd61d3193eb63b9c798e73843f55856ef022"
|
||||
integrity sha512-o8yCOgyV0LkBrEyMrGtfex4UDJ+Pw3+Eeoxk9rRg2LmlziJsgwGlz5e4W1YP3/Ew7bPW/iyGlL4q1bp+6pcD/g==
|
||||
handsontable@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/handsontable/-/handsontable-4.0.0.tgz#1a9591a3f6c2fe13564955d945ea91d20b6c4cf1"
|
||||
integrity sha512-+i9jkz5D3+Da7bIL4+YjF4hnY/nWjR8ZfcCo9BHuHaYY/ri2CYH75OobVxpMyPTspQlXjyQoWE6v0qhHuesoUw==
|
||||
dependencies:
|
||||
moment "2.20.1"
|
||||
numbro "1.11.0"
|
||||
numbro "^2.0.6"
|
||||
pikaday "1.5.1"
|
||||
|
||||
has-bigints@^1.0.1, has-bigints@^1.0.2:
|
||||
@@ -7742,10 +7755,12 @@ number-is-nan@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d"
|
||||
integrity sha512-4jbtZXNAsfZbAHiiqjLPBiCl16dES1zI4Hpzzxw61Tk+loF+sBDBKx1ICKKKwIqQ7M0mFn1TmkN7euSncWgHiQ==
|
||||
|
||||
numbro@1.11.0:
|
||||
version "1.11.0"
|
||||
resolved "https://registry.yarnpkg.com/numbro/-/numbro-1.11.0.tgz#39aa17b358b4682aec8ca0d5755f35c5d9ce8f9e"
|
||||
integrity sha512-HLgprbMBJf/h3zQ91KpvjzTWyxd9pS2xOFoEBTdkUusBi9eSqYq+RlhFn8i+WSHy/ghGRtbN5brnABOR21z4kA==
|
||||
numbro@^2.0.6:
|
||||
version "2.3.6"
|
||||
resolved "https://registry.yarnpkg.com/numbro/-/numbro-2.3.6.tgz#4bd622ebe59ccbc49dad365c5b9eed200781fa21"
|
||||
integrity sha512-pxpoTT3hVxQGaOA2RTzXR/muonQNd1K1HPJbWo7QOmxPwiPmoFCFfsG9XXgW3uqjyzezJ0P9IvCPDXUtJexjwg==
|
||||
dependencies:
|
||||
bignumber.js "^8.1.1"
|
||||
|
||||
nwsapi@^2.2.2:
|
||||
version "2.2.2"
|
||||
@@ -10061,6 +10076,24 @@ ts-loader@^9.4.1:
|
||||
micromatch "^4.0.0"
|
||||
semver "^7.3.4"
|
||||
|
||||
tsconfig-paths-webpack-plugin@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/tsconfig-paths-webpack-plugin/-/tsconfig-paths-webpack-plugin-4.0.0.tgz#84008fc3e3e0658fdb0262758b07b4da6265ff1a"
|
||||
integrity sha512-fw/7265mIWukrSHd0i+wSwx64kYUSAKPfxRDksjKIYTxSAp9W9/xcZVBF4Kl0eqQd5eBpAQ/oQrc5RyM/0c1GQ==
|
||||
dependencies:
|
||||
chalk "^4.1.0"
|
||||
enhanced-resolve "^5.7.0"
|
||||
tsconfig-paths "^4.0.0"
|
||||
|
||||
tsconfig-paths@^4.0.0:
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-4.1.1.tgz#7f23094ce897fcf4a93f67c4776e813003e48b75"
|
||||
integrity sha512-VgPrtLKpRgEAJsMj5Q/I/mXouC6A/7eJ/X4Nuk6o0cRPwBtznYxTCU4FodbexbzH9somBPEXYi0ZkUViUpJ21Q==
|
||||
dependencies:
|
||||
json5 "^2.2.1"
|
||||
minimist "^1.2.6"
|
||||
strip-bom "^3.0.0"
|
||||
|
||||
tsconfig@^7.0.0:
|
||||
version "7.0.0"
|
||||
resolved "https://registry.yarnpkg.com/tsconfig/-/tsconfig-7.0.0.tgz#84538875a4dc216e5c4a5432b3a4dec3d54e91b7"
|
||||
@@ -10544,10 +10577,10 @@ vue-template-es2015-compiler@^1.9.0:
|
||||
resolved "https://registry.yarnpkg.com/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.9.1.tgz#1ee3bc9a16ecbf5118be334bb15f9c46f82f5825"
|
||||
integrity sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==
|
||||
|
||||
vue-virtual-scroll-list@^2.3.3:
|
||||
version "2.3.3"
|
||||
resolved "https://registry.yarnpkg.com/vue-virtual-scroll-list/-/vue-virtual-scroll-list-2.3.3.tgz#290f0d04be814b93585f2ef232097b671141e5b1"
|
||||
integrity sha512-heuwlZ+lEdcVSp66CpVXnyNdsiHl/XU1cvqJb0yUHGlLv6DGK8OX16FH8I3w/wKNXtHSPF9itdBBGJFrXkyHeg==
|
||||
vue-virtual-scroll-list@^2.3.4:
|
||||
version "2.3.4"
|
||||
resolved "https://registry.yarnpkg.com/vue-virtual-scroll-list/-/vue-virtual-scroll-list-2.3.4.tgz#0d08c2d26299d8609018b8c04f5e3ca608fbe2d6"
|
||||
integrity sha512-a9bmmIhAdnKcp8NG0C9ZQ+pW9nAOrDv5SHqJfbbEpXxVVERCtbIgGeBnkwrGgmoILBoFz/WANIZgA22G44F0Yw==
|
||||
|
||||
vue@2.7.8, vue@^2.6.10, vue@^2.7.8:
|
||||
version "2.7.8"
|
||||
|
||||
@@ -437,11 +437,12 @@ window.bundleEntries.load = function (opt) {
|
||||
});
|
||||
};
|
||||
|
||||
const safe_download_url = `${options.root}${dataset.download_url}`;
|
||||
$.ajax({
|
||||
url: dataset.download_url,
|
||||
url: safe_download_url,
|
||||
success: function(content) {
|
||||
const $chartViewer = $("#" + opt.target);
|
||||
$chartViewer.html("<img id='image-annotate' src='" + dataset.download_url + "' />");
|
||||
$chartViewer.html("<img id='image-annotate' src='" + safe_download_url + "' />");
|
||||
$chartViewer.css("overflow", "auto");
|
||||
$chartViewer.css("position", "relative");
|
||||
const $image = $chartViewer.find("img");
|
||||
|
||||
@@ -87,7 +87,7 @@ function run_search_algorithm( cytoscape, root_id, type, self ) {
|
||||
// Add css class for the selected edge(s)
|
||||
algorithm.path[i].addClass( 'searchpath' );
|
||||
i++;
|
||||
// Animate the edges and nodes coloring
|
||||
// Animate the edges and nodes coloring
|
||||
// of the path with a delay of 500ms
|
||||
setTimeout( selectNextElement, 500 );
|
||||
}
|
||||
@@ -166,8 +166,9 @@ window.bundleEntries.load = function (options) {
|
||||
cytoscape = null,
|
||||
sif_file_ext = "sif",
|
||||
highlighted_color = settings.get( 'color_picker_highlighted' );
|
||||
const safe_download_url = `${options.root}${dataset.download_url}`;
|
||||
$.ajax({
|
||||
url : dataset.download_url,
|
||||
url : safe_download_url,
|
||||
success : function( content ) {
|
||||
// Select data for the graph
|
||||
if( dataset.file_ext === sif_file_ext ) {
|
||||
@@ -246,7 +247,7 @@ window.bundleEntries.load = function (options) {
|
||||
// On tapping any node, BFS or DFS start from that node
|
||||
cytoscape.$( 'node' ).on('tap', function( e ) {
|
||||
var ele = e.cyTarget,
|
||||
search_algorithm = settings.get( 'search_algorithm' ),
|
||||
search_algorithm = settings.get( 'search_algorithm' ),
|
||||
traversal_type = settings.get( 'graph_traversal' );
|
||||
// If search algorithm and traversal both are chosen,
|
||||
// search algorithm will take preference
|
||||
|
||||
@@ -3,8 +3,9 @@ window.bundleEntries = window.bundleEntries || {};
|
||||
window.bundleEntries.load = function (options) {
|
||||
var chart = options.chart;
|
||||
var dataset = options.dataset;
|
||||
const safe_download_url = `${options.root}${dataset.download_url}`;
|
||||
$.ajax({
|
||||
url: dataset.download_url,
|
||||
url: safe_download_url,
|
||||
success: function(response) {
|
||||
var input = response.split('\n');
|
||||
var app = new DrawRNA({
|
||||
|
||||
@@ -14,7 +14,8 @@ Object.assign(window.bundleEntries || {}, {
|
||||
menu: "small",
|
||||
bootstrapMenu: "true" == settings.get("menu"),
|
||||
});
|
||||
msaViz.u.file.importURL(dataset.download_url, () => {
|
||||
const safe_download_url = `${options.root}${dataset.download_url}`;
|
||||
msaViz.u.file.importURL(safe_download_url, () => {
|
||||
msaViz.render();
|
||||
chart.state("ok", "Chart drawn.");
|
||||
options.process.resolve();
|
||||
|
||||
@@ -19,8 +19,9 @@ window.bundleEntries.load = function (options) {
|
||||
opacity: settings.get("opacity"),
|
||||
};
|
||||
stage_parameters = { ext: dataset.extension, defaultRepresentation: true };
|
||||
const safe_download_url = `${options.root}${dataset.download_url}`;
|
||||
try {
|
||||
stage.loadFile(dataset.download_url, stage_parameters).then(function (component) {
|
||||
stage.loadFile(safe_download_url, stage_parameters).then(function (component) {
|
||||
component.addRepresentation(settings.get("mode"), representation_parameters);
|
||||
options.chart.state("ok", "Chart drawn.");
|
||||
options.process.resolve();
|
||||
|
||||
@@ -240,10 +240,11 @@ window.bundleEntries = window.bundleEntries || {};
|
||||
window.bundleEntries.load = function (options) {
|
||||
const chart = options.chart;
|
||||
const dataset = options.dataset;
|
||||
const safe_download_url = `${options.root}${dataset.download_url}`;
|
||||
$.ajax({
|
||||
url: dataset.download_url,
|
||||
url: safe_download_url,
|
||||
success: () => {
|
||||
MapViewer.loadFile(dataset.download_url, dataset.extension, options, chart);
|
||||
MapViewer.loadFile(safe_download_url, dataset.extension, options, chart);
|
||||
},
|
||||
error: () => {
|
||||
chart.state("failed", "Failed to access dataset.");
|
||||
|
||||
@@ -4,8 +4,9 @@ _.extend(window.bundleEntries || {}, {
|
||||
var chart = options.chart;
|
||||
var dataset = options.dataset;
|
||||
var settings = options.chart.settings;
|
||||
const safe_download_url = `${options.root}${dataset.download_url}`;
|
||||
$.ajax( {
|
||||
url : dataset.download_url,
|
||||
url : safe_download_url,
|
||||
success : function( content ) {
|
||||
try {
|
||||
var tree = Phylocanvas.default.createTree( options.target ),
|
||||
|
||||
@@ -10,8 +10,9 @@ window.bundleEntries.load = function (options) {
|
||||
antialias: true,
|
||||
outline: true,
|
||||
});
|
||||
const safe_download_url = `${options.root}${dataset.download_url}`;
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open("GET", options.dataset.download_url);
|
||||
xhr.open("GET", safe_download_url);
|
||||
xhr.onload = function () {
|
||||
if (xhr.status === 200) {
|
||||
var structure = pv.io.pdb(xhr.response);
|
||||
|
||||
@@ -3832,6 +3832,73 @@
|
||||
:Type: str
|
||||
|
||||
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
``ga4gh_service_id``
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
:Description:
|
||||
Service ID for GA4GH services (exposed via the service-info
|
||||
endpoint for the Galaxy DRS API). If unset, one will be generated
|
||||
using the URL the target API requests are made against.
|
||||
For more information on GA4GH service definitions - check out
|
||||
https://github.com/ga4gh-discovery/ga4gh-service-registry and
|
||||
https://editor.swagger.io/?url=https://raw.githubusercontent.com/ga4gh-discovery/ga4gh-service-registry/develop/service-registry.yaml
|
||||
This value should likely reflect your service's URL. For instance
|
||||
for usegalaxy.org this value should be org.usegalaxy. Particular
|
||||
Galaxy implementations will treat this value as a prefix and
|
||||
append the service type to this ID. For instance for the DRS
|
||||
service "id" (available via the DRS API) for the above
|
||||
configuration value would be org.usegalaxy.drs.
|
||||
:Default: ``None``
|
||||
:Type: str
|
||||
|
||||
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
``ga4gh_service_organization_name``
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
:Description:
|
||||
Service name for host organization (exposed via the service-info
|
||||
endpoint for the Galaxy DRS API). If unset, one will be generated
|
||||
using ga4gh_service_id.
|
||||
For more information on GA4GH service definitions - check out
|
||||
https://github.com/ga4gh-discovery/ga4gh-service-registry and
|
||||
https://editor.swagger.io/?url=https://raw.githubusercontent.com/ga4gh-discovery/ga4gh-service-registry/develop/service-registry.yaml
|
||||
:Default: ``None``
|
||||
:Type: str
|
||||
|
||||
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
``ga4gh_service_organization_url``
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
:Description:
|
||||
Organization URL for host organization (exposed via the
|
||||
service-info endpoint for the Galaxy DRS API). If unset, one will
|
||||
be generated using the URL the target API requests are made
|
||||
against.
|
||||
For more information on GA4GH service definitions - check out
|
||||
https://github.com/ga4gh-discovery/ga4gh-service-registry and
|
||||
https://editor.swagger.io/?url=https://raw.githubusercontent.com/ga4gh-discovery/ga4gh-service-registry/develop/service-registry.yaml
|
||||
:Default: ``None``
|
||||
:Type: str
|
||||
|
||||
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
``ga4gh_service_environment``
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
:Description:
|
||||
Service environment (exposed via the service-info endpoint for the
|
||||
Galaxy DRS API) for implemented GA4GH services.
|
||||
Suggested values are prod, test, dev, staging.
|
||||
For more information on GA4GH service definitions - check out
|
||||
https://github.com/ga4gh-discovery/ga4gh-service-registry and
|
||||
https://editor.swagger.io/?url=https://raw.githubusercontent.com/ga4gh-discovery/ga4gh-service-registry/develop/service-registry.yaml
|
||||
:Default: ``None``
|
||||
:Type: str
|
||||
|
||||
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
``enable_tool_tags``
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user