Update Markdown UI to use STS for PDF export if tasks are enabled.

This commit is contained in:
John Chilton
2022-04-13 09:54:01 -04:00
parent fa4ee7b9b9
commit 3968090eaf
7 changed files with 250 additions and 8 deletions
+1
View File
@@ -153,6 +153,7 @@
"ignore-loader": "^0.1.2",
"imports-loader": "^3.1.1",
"jest": "^27.4.7",
"jest-location-mock": "^1.0.9",
"jest-raw-loader": "^1.0.1",
"jest-transform-yaml": "1.0.0",
"json-loader": "^0.5.7",
+10 -8
View File
@@ -3,16 +3,12 @@
<LoadingSpan v-if="loading" />
<div v-else>
<div>
<b-button
<sts-download-button
v-if="effectiveExportLink"
class="float-right markdown-pdf-export"
title="Download PDF"
variant="link"
role="button"
v-b-tooltip.hover.bottom
@click="onDownload">
<font-awesome-icon icon="download" />
</b-button>
:fallback-url="exportLink"
:download-endpoint="downloadEndpoint"
title="Download PDF"></sts-download-button>
<b-button
v-if="!readOnly"
class="float-right markdown-edit"
@@ -114,6 +110,7 @@ import JobParameters from "./Elements/JobParameters";
import ToolStd from "./Elements/ToolStd";
import WorkflowDisplay from "./Elements/Workflow/WorkflowDisplay";
import Visualization from "./Elements/Visualization";
import StsDownloadButton from "./StsDownloadButton";
const FUNCTION_VALUE_REGEX = `\\s*(?:[\\w_\\-]+|\\"[^\\"]+\\"|\\'[^\\']+\\')\\s*`;
const FUNCTION_CALL = `\\s*[\\w\\|]+\\s*=` + FUNCTION_VALUE_REGEX;
@@ -149,6 +146,7 @@ export default {
Visualization,
InvocationTime,
FontAwesomeIcon,
StsDownloadButton,
},
props: {
markdownConfig: {
@@ -159,6 +157,10 @@ export default {
type: Boolean,
default: false,
},
downloadEndpoint: {
type: String,
default: null,
},
readOnly: {
type: Boolean,
default: false,
@@ -0,0 +1,76 @@
import MockConfigProvider from "components/providers/MockConfigProvider";
import { mount } from "@vue/test-utils";
import flushPromises from "flush-promises";
import { getLocalVue } from "jest/helpers";
import StsDownloadButton from "./StsDownloadButton";
import axios from "axios";
import MockAdapter from "axios-mock-adapter";
import "jest-location-mock";
const localVue = getLocalVue();
const NO_TASKS_CONFIG = {
enable_celery_tasks: false,
};
const TASKS_CONFIG = {
enable_celery_tasks: true,
};
const FALLBACK_URL = "http://cow.com/direct_download";
const DOWNLOAD_ENDPOINT = "http://cow.com/prepare_download";
const STORAGE_REQUEST_ID = "moocow1235";
async function mountStsDownloadButtonWrapper(config) {
const wrapper = mount(StsDownloadButton, {
propsData: {
title: "my title",
fallbackUrl: FALLBACK_URL,
downloadEndpoint: DOWNLOAD_ENDPOINT,
},
stubs: {
ConfigProvider: MockConfigProvider(config),
},
localVue,
});
await flushPromises();
return wrapper;
}
describe("StsDownloadButton", () => {
let axiosMock;
beforeEach(async () => {
axiosMock = new MockAdapter(axios);
});
afterEach(async () => {
axiosMock.reset();
});
it("should fallback to a URL if tasks not enabled", async () => {
const wrapper = await mountStsDownloadButtonWrapper(NO_TASKS_CONFIG);
wrapper.vm.onDownload(NO_TASKS_CONFIG);
await flushPromises();
expect(window.location).toBeAt(FALLBACK_URL);
});
it("should poll until ready", async () => {
axiosMock.onPost(DOWNLOAD_ENDPOINT).reply(200, { storage_request_id: STORAGE_REQUEST_ID });
const wrapper = await mountStsDownloadButtonWrapper(TASKS_CONFIG);
axiosMock.onGet(`api/short_term_storage/${STORAGE_REQUEST_ID}/ready`).reply(200, true);
wrapper.vm.onDownload(TASKS_CONFIG);
await flushPromises();
expect(window.location).toBeAt(`api/short_term_storage/${STORAGE_REQUEST_ID}`);
});
it("should be in a waiting state while polling", async () => {
axiosMock.onPost(DOWNLOAD_ENDPOINT).reply(200, { storage_request_id: STORAGE_REQUEST_ID });
const wrapper = await mountStsDownloadButtonWrapper(TASKS_CONFIG);
axiosMock.onGet(`api/short_term_storage/${STORAGE_REQUEST_ID}/ready`).reply(200, false);
expect(wrapper.vm.waiting).toBeFalsy();
wrapper.vm.onDownload(TASKS_CONFIG);
await flushPromises();
expect(wrapper.vm.waiting).toBeTruthy();
});
});
@@ -0,0 +1,116 @@
<template>
<config-provider v-slot="{ config, loading }">
<b-button
v-if="!loading && canDownload(config)"
class="float-right markdown-pdf-export"
:title="title"
variant="link"
role="button"
v-b-tooltip.hover.bottom
@click="onDownload(config)">
<font-awesome-icon icon="spinner" spin v-if="waiting" />
<font-awesome-icon icon="download" v-else />
</b-button>
</config-provider>
</template>
<script>
/*
A Bootstrap Button with logic for interfacing with Galaxy's short term storage
component (STS).
*/
import { getAppRoot } from "onload/loadConfig";
import { BButton } from "bootstrap-vue";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { library } from "@fortawesome/fontawesome-svg-core";
import { faDownload, faSpinner } from "@fortawesome/free-solid-svg-icons";
library.add(faDownload, faSpinner);
import ConfigProvider from "components/providers/ConfigProvider";
import { Toast } from "ui/toast";
import axios from "axios";
export default {
components: {
ConfigProvider,
FontAwesomeIcon,
BButton,
},
props: {
title: {
type: String,
required: true,
},
downloadEndpoint: {
type: String,
required: true,
},
fallbackUrl: {
type: String,
default: null,
},
},
data() {
return {
waiting: false,
delay: 200,
};
},
destroyed() {
this.clearTimeout();
},
methods: {
canDownload(config) {
if (!config.enable_celery_tasks) {
return this.fallbackUrl != null;
}
return true;
},
onDownload(config) {
if (!config.enable_celery_tasks) {
console.log("celery tasks not enabled - setting href to fallback URL.");
window.location.assign(this.fallbackUrl);
return;
}
this.waiting = true;
axios.post(this.downloadEndpoint).then(this.handleInitialize).catch(this.handleError);
},
handleInitialize(response) {
const storageRequestId = response.data.storage_request_id;
this.pollStorageRequestId(storageRequestId);
},
pollStorageRequestId(storageRequestId) {
const url = `${getAppRoot()}api/short_term_storage/${storageRequestId}/ready`;
axios
.get(url)
.then((r) => {
this.handlePollResponse(r, storageRequestId);
})
.catch(this.handleError);
},
handlePollResponse(response, storageRequestId) {
const ready = response.data;
if (ready) {
const url = `${getAppRoot()}api/short_term_storage/${storageRequestId}`;
window.location.assign(url);
this.waiting = false;
} else {
this.pollAfterDelay(storageRequestId);
}
},
handleError(err) {
Toast.error(`Failed to generate download: ${err}`);
this.waiting = false;
},
clearTimeout() {
if (this.timeout) {
clearTimeout(this.timeout);
}
},
pollAfterDelay(storageRequestId) {
this.clearTimeout();
this.timeout = setTimeout(() => {
this.pollStorageRequestId(storageRequestId);
}, this.delay);
},
},
};
</script>
@@ -4,6 +4,7 @@
v-if="!loading"
:markdown-config="markdownConfig"
:enable_beta_markdown_export="config.enable_beta_markdown_export"
:download-endpoint="stsUrl(config)"
:export-link="exportUrl"
@onEdit="onEdit" />
</config-provider>
@@ -51,6 +52,9 @@ export default {
onEdit() {
window.location = this.editUrl;
},
stsUrl(config) {
return `${this.dataUrl}/prepare_download`;
},
async getContent() {
try {
const response = await axios.get(this.dataUrl);
+42
View File
@@ -1216,6 +1216,11 @@
resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.3.tgz#e45e384e4b8ec16bce2fd903af78450f6bf7ec98"
integrity sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==
"@jedmao/location@^3.0.0":
version "3.0.0"
resolved "https://registry.yarnpkg.com/@jedmao/location/-/location-3.0.0.tgz#f2b24e937386f95252f3a1fefbf7ca2e0a4b87e9"
integrity sha512-p7mzNlgJbCioUYLUEKds3cQG4CHONVFJNYqMe6ocEtENCL/jYmMo1Q3ApwsMmU+L0ZkaDJEyv4HokaByLoPwlQ==
"@jest/console@^27.4.6":
version "27.4.6"
resolved "https://registry.yarnpkg.com/@jest/console/-/console-27.4.6.tgz#0742e6787f682b22bdad56f9db2a8a77f6a86107"
@@ -3758,6 +3763,11 @@ diff-sequences@^27.4.0:
resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-27.4.0.tgz#d783920ad8d06ec718a060d00196dfef25b132a5"
integrity sha512-YqiQzkrsmHMH5uuh8OdQFU9/ZpADnwzml8z0O5HvRNda+5UZsaX/xN+AAxfR2hWq1Y7HZnAzO9J5lJXOuDz2Ww==
diff-sequences@^27.5.1:
version "27.5.1"
resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-27.5.1.tgz#eaecc0d327fd68c8d9672a1e64ab8dccb2ef5327"
integrity sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ==
diff@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/diff/-/diff-5.0.0.tgz#7ed6ad76d859d030787ec35855f5b1daf31d852b"
@@ -5913,6 +5923,16 @@ jest-diff@^27.0.0, jest-diff@^27.4.6:
jest-get-type "^27.4.0"
pretty-format "^27.4.6"
jest-diff@^27.0.1:
version "27.5.1"
resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-27.5.1.tgz#a07f5011ac9e6643cf8a95a462b7b1ecf6680def"
integrity sha512-m0NvkX55LDt9T4mctTEgnZk3fmEg3NRYutvMPWM/0iPnkFj2wIeF45O1718cMSOFO1vINkqmxqD8vE37uTEbqw==
dependencies:
chalk "^4.0.0"
diff-sequences "^27.5.1"
jest-get-type "^27.5.1"
pretty-format "^27.5.1"
jest-docblock@^27.4.0:
version "27.4.0"
resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-27.4.0.tgz#06c78035ca93cbbb84faf8fce64deae79a59f69f"
@@ -5961,6 +5981,11 @@ jest-get-type@^27.4.0:
resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-27.4.0.tgz#7503d2663fffa431638337b3998d39c5e928e9b5"
integrity sha512-tk9o+ld5TWq41DkK14L4wox4s2D9MtTpKaAVzXfr5CUKm5ZK2ExcaFE0qls2W71zE/6R2TxxrK9w2r6svAFDBQ==
jest-get-type@^27.5.1:
version "27.5.1"
resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-27.5.1.tgz#3cd613c507b0f7ace013df407a1c1cd578bcb4f1"
integrity sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw==
jest-haste-map@^27.4.6:
version "27.4.6"
resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-27.4.6.tgz#c60b5233a34ca0520f325b7e2cc0a0140ad0862a"
@@ -6012,6 +6037,14 @@ jest-leak-detector@^27.4.6:
jest-get-type "^27.4.0"
pretty-format "^27.4.6"
jest-location-mock@^1.0.9:
version "1.0.9"
resolved "https://registry.yarnpkg.com/jest-location-mock/-/jest-location-mock-1.0.9.tgz#f4466362423b273e12ca3716467a3d478ce78fa8"
integrity sha512-DN/v7Zsa3N4uGgWTCrMrPPxhZORr/4N5gi+u7Tk6sLdORYplrC0//wfFN5FOtx4ZdQzDVfY6rLa4d+wfTKzQHw==
dependencies:
"@jedmao/location" "^3.0.0"
jest-diff "^27.0.1"
jest-matcher-utils@^27.4.6:
version "27.4.6"
resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-27.4.6.tgz#53ca7f7b58170638590e946f5363b988775509b8"
@@ -7944,6 +7977,15 @@ pretty-format@^27.0.0, pretty-format@^27.4.6:
ansi-styles "^5.0.0"
react-is "^17.0.1"
pretty-format@^27.5.1:
version "27.5.1"
resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-27.5.1.tgz#2181879fdea51a7a5851fb39d920faa63f01d88e"
integrity sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==
dependencies:
ansi-regex "^5.0.1"
ansi-styles "^5.0.0"
react-is "^17.0.1"
pretty-hrtime@^1.0.0:
version "1.0.3"
resolved "https://registry.yarnpkg.com/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz#b7e3ea42435a4c9b2759d99e0f201eb195802ee1"
+1
View File
@@ -205,6 +205,7 @@ class ConfigSerializer(base.ModelSerializer):
"release_doc_base_url": _use_config,
"expose_user_email": _use_config,
"enable_tool_source_display": _use_config,
"enable_celery_tasks": _use_config,
"user_library_import_dir_available": lambda item, key, **context: bool(item.get("user_library_import_dir")),
"welcome_directory": _use_config,
}