diff --git a/client/src/components/History/CurrentHistory/HistoryCounter.vue b/client/src/components/History/CurrentHistory/HistoryCounter.vue index a07353680023..5d5e72479deb 100644 --- a/client/src/components/History/CurrentHistory/HistoryCounter.vue +++ b/client/src/components/History/CurrentHistory/HistoryCounter.vue @@ -1,67 +1,97 @@ diff --git a/client/src/components/History/CurrentHistory/HistorySelectPreferredObjectStore.test.js b/client/src/components/History/CurrentHistory/HistorySelectPreferredObjectStore.test.js new file mode 100644 index 000000000000..6dc25f447c17 --- /dev/null +++ b/client/src/components/History/CurrentHistory/HistorySelectPreferredObjectStore.test.js @@ -0,0 +1,64 @@ +import { mount } from "@vue/test-utils"; +import { getLocalVue } from "jest/helpers"; +import HistorySelectPreferredObjectStore from "./HistorySelectPreferredObjectStore"; +import axios from "axios"; +import MockAdapter from "axios-mock-adapter"; +import flushPromises from "flush-promises"; + +const localVue = getLocalVue(true); + +const TEST_ROOT = "/"; +const TEST_HISTORY_ID = "myTestHistoryId"; + +const TEST_HISTORY = { + id: TEST_HISTORY_ID, + preferred_object_store_id: null, +}; + +function mountComponent() { + const wrapper = mount(HistorySelectPreferredObjectStore, { + propsData: { userPreferredObjectStoreId: null, history: TEST_HISTORY, root: TEST_ROOT }, + localVue, + }); + return wrapper; +} + +import { ROOT_COMPONENT } from "utils/navigation"; + +const OBJECT_STORES = [ + { object_store_id: "object_store_1", badges: [], quota: { enabled: false } }, + { object_store_id: "object_store_2", badges: [], quota: { enabled: false } }, +]; + +describe("HistorySelectPreferredObjectStore.vue", () => { + let axiosMock; + + beforeEach(async () => { + axiosMock = new MockAdapter(axios); + axiosMock.onGet("/api/object_store?selectable=true").reply(200, OBJECT_STORES); + }); + + afterEach(async () => { + axiosMock.restore(); + }); + + it("updates object store to default on selection null", async () => { + const wrapper = mountComponent(); + await flushPromises(); + const els = wrapper.findAll(ROOT_COMPONENT.preferences.object_store_selection.option_buttons.selector); + expect(els.length).toBe(3); + const galaxyDefaultOption = wrapper.find( + ROOT_COMPONENT.preferences.object_store_selection.option_button({ object_store_id: "__null__" }).selector + ); + expect(galaxyDefaultOption.exists()).toBeTruthy(); + axiosMock + .onPut(`/api/histories/${TEST_HISTORY_ID}`, expect.objectContaining({ preferred_object_store_id: null })) + .reply(202); + await galaxyDefaultOption.trigger("click"); + await flushPromises(); + const errorEl = wrapper.find(".object-store-selection-error"); + expect(errorEl.exists()).toBeFalsy(); + const emitted = wrapper.emitted(); + expect(emitted["updated"][0][0]).toEqual(null); + }); +}); diff --git a/client/src/components/History/CurrentHistory/HistorySelectPreferredObjectStore.vue b/client/src/components/History/CurrentHistory/HistorySelectPreferredObjectStore.vue new file mode 100644 index 000000000000..622329363faf --- /dev/null +++ b/client/src/components/History/CurrentHistory/HistorySelectPreferredObjectStore.vue @@ -0,0 +1,79 @@ + + + diff --git a/client/src/components/History/CurrentHistory/HistoryTargetPreferredObjectStorePopover.vue b/client/src/components/History/CurrentHistory/HistoryTargetPreferredObjectStorePopover.vue new file mode 100644 index 000000000000..1bdaddc0f71d --- /dev/null +++ b/client/src/components/History/CurrentHistory/HistoryTargetPreferredObjectStorePopover.vue @@ -0,0 +1,48 @@ + + + diff --git a/client/src/components/ObjectStore/DescribeObjectStore.test.js b/client/src/components/ObjectStore/DescribeObjectStore.test.js index 0bda7a01e887..63b6378f9b2e 100644 --- a/client/src/components/ObjectStore/DescribeObjectStore.test.js +++ b/client/src/components/ObjectStore/DescribeObjectStore.test.js @@ -11,18 +11,21 @@ const localVue = getLocalVue(); const TEST_STORAGE_API_RESPONSE_WITHOUT_ID = { object_store_id: null, private: false, + badges: [], }; const TEST_RENDERED_MARKDOWN_AS_HTML = "

My cool markdown\n"; const TEST_STORAGE_API_RESPONSE_WITH_ID = { object_store_id: "foobar", private: false, + badges: [], }; const TEST_STORAGE_API_RESPONSE_WITH_NAME = { object_store_id: "foobar", name: "my cool storage", description: "My cool **markdown**", private: true, + badges: [], }; // works fine without mocking but I guess it is more JS unit-y with the mock? diff --git a/client/src/components/ObjectStore/DescribeObjectStore.vue b/client/src/components/ObjectStore/DescribeObjectStore.vue index a352bf0a291f..af924dbf67ec 100644 --- a/client/src/components/ObjectStore/DescribeObjectStore.vue +++ b/client/src/components/ObjectStore/DescribeObjectStore.vue @@ -14,6 +14,7 @@ the default configured Galaxy object store . + diff --git a/client/src/components/ObjectStore/ObjectStoreBadge.test.js b/client/src/components/ObjectStore/ObjectStoreBadge.test.js new file mode 100644 index 000000000000..a02d930d575d --- /dev/null +++ b/client/src/components/ObjectStore/ObjectStoreBadge.test.js @@ -0,0 +1,38 @@ +import { mount } from "@vue/test-utils"; +import { getLocalVue } from "jest/helpers"; +import ObjectStoreBadge from "./ObjectStoreBadge"; +import { ROOT_COMPONENT } from "utils/navigation"; + +const localVue = getLocalVue(true); + +const TEST_MESSAGE = "a test message provided by backend"; + +describe("ObjectStoreBadge", () => { + let wrapper; + + function mountBadge(badge) { + wrapper = mount(ObjectStoreBadge, { + propsData: { badge }, + localVue, + stubs: { "b-popover": true }, + }); + } + + it("should render a valid badge for more_secure type", async () => { + mountBadge({ type: "more_secure", message: TEST_MESSAGE }); + const selector = ROOT_COMPONENT.object_store_details.badge_of_type({ type: "more_secure" }).selector; + const iconEl = wrapper.find(selector); + expect(iconEl.exists()).toBeTruthy(); + expect(wrapper.vm.message).toContain(TEST_MESSAGE); + expect(wrapper.vm.stockMessage).toContain("more secure by the Galaxy adminstrator"); + }); + + it("should render a valid badge for less_secure type", async () => { + mountBadge({ type: "less_secure", message: TEST_MESSAGE }); + const selector = ROOT_COMPONENT.object_store_details.badge_of_type({ type: "less_secure" }).selector; + const iconEl = wrapper.find(selector); + expect(iconEl.exists()).toBeTruthy(); + expect(wrapper.vm.message).toContain(TEST_MESSAGE); + expect(wrapper.vm.stockMessage).toContain("less secure by the Galaxy adminstrator"); + }); +}); diff --git a/client/src/components/ObjectStore/ObjectStoreBadge.vue b/client/src/components/ObjectStore/ObjectStoreBadge.vue new file mode 100644 index 000000000000..3a32ca8b529c --- /dev/null +++ b/client/src/components/ObjectStore/ObjectStoreBadge.vue @@ -0,0 +1,191 @@ + + + + + diff --git a/client/src/components/ObjectStore/ObjectStoreBadges.vue b/client/src/components/ObjectStore/ObjectStoreBadges.vue new file mode 100644 index 000000000000..1d6d8e21a149 --- /dev/null +++ b/client/src/components/ObjectStore/ObjectStoreBadges.vue @@ -0,0 +1,35 @@ + + + diff --git a/client/src/components/ObjectStore/SelectObjectStore.vue b/client/src/components/ObjectStore/SelectObjectStore.vue new file mode 100644 index 000000000000..d384c0507d7e --- /dev/null +++ b/client/src/components/ObjectStore/SelectObjectStore.vue @@ -0,0 +1,153 @@ + + + diff --git a/client/src/components/ObjectStore/ShowSelectedObjectStore.test.js b/client/src/components/ObjectStore/ShowSelectedObjectStore.test.js new file mode 100644 index 000000000000..8450ad447976 --- /dev/null +++ b/client/src/components/ObjectStore/ShowSelectedObjectStore.test.js @@ -0,0 +1,45 @@ +import { mount } from "@vue/test-utils"; +import { getLocalVue } from "jest/helpers"; +import ShowSelectedObjectStore from "./ShowSelectedObjectStore"; +import axios from "axios"; +import MockAdapter from "axios-mock-adapter"; +import flushPromises from "flush-promises"; + +const localVue = getLocalVue(true); +const TEST_OBJECT_ID = "os123"; +const OBJECT_STORE_DATA = { + object_store_id: TEST_OBJECT_ID, + badges: [], +}; + +describe("ShowSelectedObjectStore", () => { + let wrapper; + let axiosMock; + + beforeEach(async () => { + axiosMock = new MockAdapter(axios); + }); + + afterEach(async () => { + axiosMock.restore(); + }); + + it("should show a loading message and then a DescribeObjectStore component", async () => { + axiosMock.onGet(`/api/object_store/${TEST_OBJECT_ID}`).reply(200, OBJECT_STORE_DATA); + wrapper = mount(ShowSelectedObjectStore, { + propsData: { preferredObjectStoreId: TEST_OBJECT_ID, forWhat: "Data goes into..." }, + localVue, + stubs: { + LoadingSpan: true, + DescribeObjectStore: true, + }, + }); + let loadingEl = wrapper.find("loadingspan-stub"); + expect(loadingEl.exists()).toBeTruthy(); + expect(loadingEl.attributes("message")).toBeLocalizationOf("Loading object store details"); + await flushPromises(); + loadingEl = wrapper.find("loadingspan-stub"); + expect(loadingEl.exists()).toBeFalsy(); + expect(wrapper.find("describeobjectstore-stub").exists()).toBeTruthy(); + }); +}); diff --git a/client/src/components/ObjectStore/ShowSelectedObjectStore.vue b/client/src/components/ObjectStore/ShowSelectedObjectStore.vue new file mode 100644 index 000000000000..a0a2e9cf938f --- /dev/null +++ b/client/src/components/ObjectStore/ShowSelectedObjectStore.vue @@ -0,0 +1,37 @@ + + + diff --git a/client/src/components/ObjectStore/adminConfigMixin.js b/client/src/components/ObjectStore/adminConfigMixin.js new file mode 100644 index 000000000000..70f265a941ab --- /dev/null +++ b/client/src/components/ObjectStore/adminConfigMixin.js @@ -0,0 +1,15 @@ +import MarkdownIt from "markdown-it"; + +export default { + methods: { + adminMarkup(markup) { + let markupHtml; + if (markup) { + markupHtml = MarkdownIt({ html: true }).render(markup); + } else { + markupHtml = null; + } + return markupHtml; + }, + }, +}; diff --git a/client/src/components/ObjectStore/showTargetPopoverMixin.js b/client/src/components/ObjectStore/showTargetPopoverMixin.js new file mode 100644 index 000000000000..4fc88ce24863 --- /dev/null +++ b/client/src/components/ObjectStore/showTargetPopoverMixin.js @@ -0,0 +1,18 @@ +import ShowSelectedObjectStore from "./ShowSelectedObjectStore"; + +export default { + components: { + ShowSelectedObjectStore, + }, + props: { + titleSuffix: { + type: String, + default: null, + }, + }, + computed: { + title() { + return this.l(`Preferred Target Object Store ${this.titleSuffix || ""}`); + }, + }, +}; diff --git a/client/src/components/Tool/ToolCard.vue b/client/src/components/Tool/ToolCard.vue index b8c6230b428c..94e23c455448 100644 --- a/client/src/components/Tool/ToolCard.vue +++ b/client/src/components/Tool/ToolCard.vue @@ -75,6 +75,33 @@ @click="onRemoveFavorite"> + + + + + + + +

@@ -117,6 +144,9 @@ import { addFavorite, removeFavorite } from "components/Tool/services"; import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome"; import { library } from "@fortawesome/fontawesome-svg-core"; import { faWrench } from "@fortawesome/free-solid-svg-icons"; +import ToolSelectPreferredObjectStore from "./ToolSelectPreferredObjectStore"; +import ToolTargetPreferredObjectStorePopover from "./ToolTargetPreferredObjectStorePopover"; +import { getAppRoot } from "onload/loadConfig"; library.add(faWrench); @@ -127,6 +157,8 @@ export default { ToolFooter, ToolHelp, ToolSourceMenuItem, + ToolSelectPreferredObjectStore, + ToolTargetPreferredObjectStorePopover, }, props: { id: { @@ -165,11 +197,22 @@ export default { type: Boolean, default: false, }, + allowObjectStoreSelection: { + type: Boolean, + default: false, + }, + preferredObjectStoreId: { + type: String, + default: null, + }, }, data() { return { + root: getAppRoot(), webhookDetails: [], errorText: null, + showPreferredObjectStoreModal: false, + toolPreferredObjectStoreId: this.preferredObjectStoreId, }; }, computed: { @@ -246,6 +289,9 @@ export default { } ); }, + onShow() { + this.showPreferredObjectStoreModal = true; + }, onRemoveFavorite() { removeFavorite(this.user.id, this.id).then( (data) => { @@ -286,6 +332,11 @@ export default { favorites[objectType] = newFavorites[objectType]; this.$emit("onUpdateFavorites", this.user, JSON.stringify(favorites)); }, + onUpdatePreferredObjectStoreId(toolPreferredObjectStoreId) { + this.showPreferredObjectStoreModal = false; + this.toolPreferredObjectStoreId = toolPreferredObjectStoreId; + this.$emit("updatePreferredObjectStoreId", toolPreferredObjectStoreId); + }, }, }; diff --git a/client/src/components/Tool/ToolForm.vue b/client/src/components/Tool/ToolForm.vue index 2072ed30be14..391150e1aca1 100644 --- a/client/src/components/Tool/ToolForm.vue +++ b/client/src/components/Tool/ToolForm.vue @@ -39,8 +39,11 @@ :message-text="messageText" :message-variant="messageVariant" :disabled="disabled || showExecuting" + :allow-object-store-selection="config.object_store_allows_id_selection" + :preferred-object-store-id="preferredObjectStoreId" itemscope="itemscope" itemtype="https://schema.org/CreativeWork" + @updatePreferredObjectStoreId="onUpdatePreferredObjectStoreId" @onChangeVersion="onChangeVersion" @onUpdateFavorites="onUpdateFavorites">