diff --git a/client/src/components/Dataset/DatasetStorage/DatasetStorage.test.js b/client/src/components/Dataset/DatasetStorage/DatasetStorage.test.js index 821126543925..bfdedc98bda0 100644 --- a/client/src/components/Dataset/DatasetStorage/DatasetStorage.test.js +++ b/client/src/components/Dataset/DatasetStorage/DatasetStorage.test.js @@ -4,37 +4,18 @@ import { getLocalVue } from "tests/jest/helpers"; import flushPromises from "flush-promises"; import MockAdapter from "axios-mock-adapter"; import axios from "axios"; -import MarkdownIt from "markdown-it"; const localVue = getLocalVue(); const TEST_STORAGE_API_RESPONSE_WITHOUT_ID = { object_store_id: null, -}; -const TEST_STORAGE_API_RESPONSE_WITH_ID = { - object_store_id: "foobar", -}; -const TEST_STORAGE_API_RESPONSE_WITH_NAME = { - object_store_id: "foobar", - name: "my cool storage", - description: "My cool **markdown**", + private: false, }; const TEST_DATASET_ID = "1"; const TEST_STORAGE_URL = `/api/datasets/${TEST_DATASET_ID}/storage`; -const TEST_RENDERED_MARKDOWN_AS_HTML = "

My cool markdown\n"; const TEST_ERROR_MESSAGE = "Opps all errors."; -// works fine without mocking but I guess it is more JS unit-y with the mock? -jest.mock("markdown-it"); -MarkdownIt.mockImplementation(() => { - return { - render(markdown) { - return TEST_RENDERED_MARKDOWN_AS_HTML; - }, - }; -}); - -describe("Dataset Storage", () => { +describe("DatasetStorage.vue", () => { let axiosMock; let wrapper; @@ -46,9 +27,6 @@ describe("Dataset Storage", () => { wrapper = shallowMount(DatasetStorage, { propsData: { datasetId: TEST_DATASET_ID }, localVue, - stubs: { - "loading-span": true, - }, }); } @@ -62,6 +40,7 @@ describe("Dataset Storage", () => { mount(); await wrapper.vm.$nextTick(); expect(wrapper.findAll("loading-span-stub").length).toBe(1); + expect(wrapper.findAll("describe-object-store-stub").length).toBe(0); }); it("test error rendering...", async () => { @@ -78,44 +57,8 @@ describe("Dataset Storage", () => { it("test dataset storage with object store without id", async () => { await mountWithResponse(TEST_STORAGE_API_RESPONSE_WITHOUT_ID); expect(wrapper.findAll("loading-span-stub").length).toBe(0); - expect(wrapper.vm.descriptionRendered).toBeNull(); - const header = wrapper.findAll("h2"); - expect(header.length).toBe(1); - expect(header.at(0).text()).toBe("Dataset Storage"); - const byIdSpan = wrapper.findAll(".display-os-by-id"); - expect(byIdSpan.length).toBe(0); - const byNameSpan = wrapper.findAll(".display-os-by-name"); - expect(byNameSpan.length).toBe(0); - const byDefaultSpan = wrapper.findAll(".display-os-default"); - expect(byDefaultSpan.length).toBe(1); - }); - - it("test dataset storage with object store id", async () => { - await mountWithResponse(TEST_STORAGE_API_RESPONSE_WITH_ID); - expect(wrapper.findAll("loading-span-stub").length).toBe(0); - expect(wrapper.vm.storageInfo.object_store_id).toBe("foobar"); - expect(wrapper.vm.descriptionRendered).toBeNull(); - const header = wrapper.findAll("h2"); - expect(header.length).toBe(1); - expect(header.at(0).text()).toBe("Dataset Storage"); - const byIdSpan = wrapper.findAll(".display-os-by-id"); - expect(byIdSpan.length).toBe(1); - const byNameSpan = wrapper.findAll(".display-os-by-name"); - expect(byNameSpan.length).toBe(0); - }); - - it("test dataset storage with object store name", async () => { - await mountWithResponse(TEST_STORAGE_API_RESPONSE_WITH_NAME); - expect(wrapper.findAll("loading-span-stub").length).toBe(0); - expect(wrapper.vm.storageInfo.object_store_id).toBe("foobar"); - expect(wrapper.vm.descriptionRendered).toBe(TEST_RENDERED_MARKDOWN_AS_HTML); - const header = wrapper.findAll("h2"); - expect(header.length).toBe(1); - expect(header.at(0).text()).toBe("Dataset Storage"); - const byIdSpan = wrapper.findAll(".display-os-by-id"); - expect(byIdSpan.length).toBe(0); - const byNameSpan = wrapper.findAll(".display-os-by-name"); - expect(byNameSpan.length).toBe(1); + expect(wrapper.findAll("describe-object-store-stub").length).toBe(1); + expect(wrapper.vm.storageInfo.private).toEqual(false); }); afterEach(() => { diff --git a/client/src/components/Dataset/DatasetStorage/DatasetStorage.vue b/client/src/components/Dataset/DatasetStorage/DatasetStorage.vue index 8edb5fc3967c..6c35e101caa0 100644 --- a/client/src/components/Dataset/DatasetStorage/DatasetStorage.vue +++ b/client/src/components/Dataset/DatasetStorage/DatasetStorage.vue @@ -16,17 +16,7 @@

-

- This dataset is stored in - - a Galaxy object store named {{ storageInfo.name }} - - - a Galaxy object store with id {{ storageInfo.object_store_id }} - - the default configured Galaxy object store . -

-
+
@@ -34,12 +24,13 @@ diff --git a/client/src/components/History/CurrentHistory/PreferredStorePopover.vue b/client/src/components/History/CurrentHistory/PreferredStorePopover.vue new file mode 100644 index 000000000000..45374721179c --- /dev/null +++ b/client/src/components/History/CurrentHistory/PreferredStorePopover.vue @@ -0,0 +1,49 @@ + + + diff --git a/client/src/components/History/CurrentHistory/SelectPreferredStore.test.js b/client/src/components/History/CurrentHistory/SelectPreferredStore.test.js new file mode 100644 index 000000000000..14f50f81ba67 --- /dev/null +++ b/client/src/components/History/CurrentHistory/SelectPreferredStore.test.js @@ -0,0 +1,63 @@ +import { mount } from "@vue/test-utils"; +import { getLocalVue } from "tests/jest/helpers"; +import SelectPreferredStore from "./SelectPreferredStore"; +import axios from "axios"; +import MockAdapter from "axios-mock-adapter"; +import flushPromises from "flush-promises"; + +const localVue = getLocalVue(true); + +const TEST_HISTORY_ID = "myTestHistoryId"; + +const TEST_HISTORY = { + id: TEST_HISTORY_ID, + preferred_object_store_id: null, +}; + +function mountComponent() { + const wrapper = mount(SelectPreferredStore, { + propsData: { userPreferredObjectStoreId: null, history: TEST_HISTORY }, + 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("SelectPreferredStore.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/SelectPreferredStore.vue b/client/src/components/History/CurrentHistory/SelectPreferredStore.vue new file mode 100644 index 000000000000..6cf49f4f0801 --- /dev/null +++ b/client/src/components/History/CurrentHistory/SelectPreferredStore.vue @@ -0,0 +1,76 @@ + + + diff --git a/client/src/components/Libraries/LibraryFolder/TopToolbar/library-model.js b/client/src/components/Libraries/LibraryFolder/TopToolbar/library-model.js index 3db3e4a2e6ef..c16ff708ab3f 100644 --- a/client/src/components/Libraries/LibraryFolder/TopToolbar/library-model.js +++ b/client/src/components/Libraries/LibraryFolder/TopToolbar/library-model.js @@ -172,7 +172,7 @@ var HistoryContents = Backbone.Collection.extend({ this.id = options.id; }, url: function () { - return `${this.urlRoot + this.id}/contents`; + return `${this.urlRoot + this.id}/contents?shareable=true`; }, model: HistoryItem, }); diff --git a/client/src/components/ObjectStore/DescribeObjectStore.test.js b/client/src/components/ObjectStore/DescribeObjectStore.test.js new file mode 100644 index 000000000000..1c1acff73753 --- /dev/null +++ b/client/src/components/ObjectStore/DescribeObjectStore.test.js @@ -0,0 +1,83 @@ +import { shallowMount } from "@vue/test-utils"; +import DescribeObjectStore from "./DescribeObjectStore"; +import { getLocalVue } from "tests/jest/helpers"; +import MarkdownIt from "markdown-it"; + +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? +jest.mock("markdown-it"); +MarkdownIt.mockImplementation(() => { + return { + render(markdown) { + return TEST_RENDERED_MARKDOWN_AS_HTML; + }, + }; +}); + +describe("DescribeObjectStore.vue", () => { + let wrapper; + + async function mountWithResponse(response) { + wrapper = shallowMount(DescribeObjectStore, { + propsData: { storageInfo: response, what: "where i am throwing my test dataset" }, + localVue, + }); + } + + it("test dataset storage with object store without id", async () => { + await mountWithResponse(TEST_STORAGE_API_RESPONSE_WITHOUT_ID); + expect(wrapper.findAll("loading-span-stub").length).toBe(0); + expect(wrapper.vm.descriptionRendered).toBeNull(); + const byIdSpan = wrapper.findAll(".display-os-by-id"); + expect(byIdSpan.length).toBe(0); + const byNameSpan = wrapper.findAll(".display-os-by-name"); + expect(byNameSpan.length).toBe(0); + const byDefaultSpan = wrapper.findAll(".display-os-default"); + expect(byDefaultSpan.length).toBe(1); + }); + + it("test dataset storage with object store id", async () => { + await mountWithResponse(TEST_STORAGE_API_RESPONSE_WITH_ID); + expect(wrapper.findAll("loading-span-stub").length).toBe(0); + expect(wrapper.vm.storageInfo.object_store_id).toBe("foobar"); + expect(wrapper.vm.descriptionRendered).toBeNull(); + const byIdSpan = wrapper.findAll(".display-os-by-id"); + expect(byIdSpan.length).toBe(1); + const byNameSpan = wrapper.findAll(".display-os-by-name"); + expect(byNameSpan.length).toBe(0); + expect(wrapper.find("object-store-restriction-span-stub").props("isPrivate")).toBeFalsy(); + }); + + it("test dataset storage with object store name", async () => { + await mountWithResponse(TEST_STORAGE_API_RESPONSE_WITH_NAME); + expect(wrapper.findAll("loading-span-stub").length).toBe(0); + expect(wrapper.vm.storageInfo.object_store_id).toBe("foobar"); + expect(wrapper.vm.descriptionRendered).toBe(TEST_RENDERED_MARKDOWN_AS_HTML); + const byIdSpan = wrapper.findAll(".display-os-by-id"); + expect(byIdSpan.length).toBe(0); + const byNameSpan = wrapper.findAll(".display-os-by-name"); + expect(byNameSpan.length).toBe(1); + expect(wrapper.find("object-store-restriction-span-stub").props("isPrivate")).toBeTruthy(); + }); +}); diff --git a/client/src/components/ObjectStore/DescribeObjectStore.vue b/client/src/components/ObjectStore/DescribeObjectStore.vue new file mode 100644 index 000000000000..475f583957e6 --- /dev/null +++ b/client/src/components/ObjectStore/DescribeObjectStore.vue @@ -0,0 +1,70 @@ + + + diff --git a/client/src/components/ObjectStore/ObjectStoreBadge.test.js b/client/src/components/ObjectStore/ObjectStoreBadge.test.js new file mode 100644 index 000000000000..366127991bad --- /dev/null +++ b/client/src/components/ObjectStore/ObjectStoreBadge.test.js @@ -0,0 +1,38 @@ +import { mount } from "@vue/test-utils"; +import { getLocalVue } from "tests/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..d6f7c086cd1b --- /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/ObjectStoreRestrictionSpan.test.js b/client/src/components/ObjectStore/ObjectStoreRestrictionSpan.test.js new file mode 100644 index 000000000000..a022b92aa5c4 --- /dev/null +++ b/client/src/components/ObjectStore/ObjectStoreRestrictionSpan.test.js @@ -0,0 +1,27 @@ +import { shallowMount } from "@vue/test-utils"; +import { getLocalVue } from "tests/jest/helpers"; +import ObjectStoreRestrictionSpan from "./ObjectStoreRestrictionSpan"; + +const localVue = getLocalVue(); + +describe("ObjectStoreRestrictionSpan", () => { + let wrapper; + + it("should render info about private storage if isPrivate", () => { + wrapper = shallowMount(ObjectStoreRestrictionSpan, { + propsData: { isPrivate: true }, + localVue, + }); + expect(wrapper.find(".stored-how").text()).toBe("private"); + expect(wrapper.find(".stored-how").attributes("title")).toBeTruthy(); + }); + + it("should render info about unrestricted storage if not isPrivate", () => { + wrapper = shallowMount(ObjectStoreRestrictionSpan, { + propsData: { isPrivate: false }, + localVue, + }); + expect(wrapper.find(".stored-how").text()).toBe("unrestricted"); + expect(wrapper.find(".stored-how").attributes("title")).toBeTruthy(); + }); +}); diff --git a/client/src/components/ObjectStore/ObjectStoreRestrictionSpan.vue b/client/src/components/ObjectStore/ObjectStoreRestrictionSpan.vue new file mode 100644 index 000000000000..5f1bb689db60 --- /dev/null +++ b/client/src/components/ObjectStore/ObjectStoreRestrictionSpan.vue @@ -0,0 +1,38 @@ + + + + + diff --git a/client/src/components/ObjectStore/SelectObjectStore.vue b/client/src/components/ObjectStore/SelectObjectStore.vue new file mode 100644 index 000000000000..2d3096f4b04c --- /dev/null +++ b/client/src/components/ObjectStore/SelectObjectStore.vue @@ -0,0 +1,151 @@ + + + diff --git a/client/src/components/ObjectStore/ShowSelectedObjectStore.test.js b/client/src/components/ObjectStore/ShowSelectedObjectStore.test.js new file mode 100644 index 000000000000..142dc91f0b76 --- /dev/null +++ b/client/src/components/ObjectStore/ShowSelectedObjectStore.test.js @@ -0,0 +1,45 @@ +import { mount } from "@vue/test-utils"; +import { getLocalVue } from "tests/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 dde944c12025..f877b14af41c 100644 --- a/client/src/components/Tool/ToolCard.vue +++ b/client/src/components/Tool/ToolCard.vue @@ -6,6 +6,9 @@ import ToolOptionsButton from "components/Tool/Buttons/ToolOptionsButton.vue"; import ToolFooter from "components/Tool/ToolFooter"; import ToolHelp from "components/Tool/ToolHelp"; import Heading from "components/Common/Heading"; +import ToolSelectPreferredObjectStore from "./ToolSelectPreferredObjectStore"; +import ToolTargetPreferredObjectStorePopover from "./ToolTargetPreferredObjectStorePopover"; +import { getAppRoot } from "onload/loadConfig"; import { computed, ref, watch } from "vue"; import { useCurrentUser } from "composables/user"; @@ -45,9 +48,17 @@ const props = defineProps({ type: Boolean, default: false, }, + allowObjectStoreSelection: { + type: Boolean, + default: false, + }, + preferredObjectStoreId: { + type: String, + default: null, + }, }); -const emit = defineEmits(["onChangeVersion"]); +const emit = defineEmits(["onChangeVersion", "updatePreferredObjectStoreId"]); function onChangeVersion(v) { emit("onChangeVersion", v); @@ -68,9 +79,22 @@ function onSetError(e) { const { currentUser: user } = useCurrentUser(false, true); const hasUser = computed(() => !user.value.isAnonymous); - const versions = computed(() => props.options.versions); const showVersions = computed(() => props.options.versions?.length > 1); + +const root = computed(() => getAppRoot()); +const showPreferredObjectStoreModal = ref(false); +const toolPreferredObjectStoreId = ref(props.preferredObjectStoreId); + +function onShowObjectStoreSelect() { + showPreferredObjectStoreModal.value = true; +} + +function onUpdatePreferredObjectStoreId(selectedToolPreferredObjectStoreId) { + showPreferredObjectStoreModal.value = false; + toolPreferredObjectStoreId.value = selectedToolPreferredObjectStoreId; + emit("updatePreferredObjectStoreId", selectedToolPreferredObjectStoreId); +}