Merge pull request #15783 from jmchilton/object_store_ui_followup_2

Convert more of the object store selection client to TypeScript.
This commit is contained in:
Dannon
2023-03-16 10:21:11 -04:00
committed by GitHub
23 changed files with 497 additions and 377 deletions
+1
View File
@@ -142,6 +142,7 @@
"@babel/preset-typescript": "^7.18.6",
"@cerner/duplicate-package-checker-webpack-plugin": "^2.3.0",
"@testing-library/jest-dom": "^5.16.4",
"@types/markdown-it": "^12.2.3",
"@typescript-eslint/eslint-plugin": "^5.51.0",
"@typescript-eslint/parser": "^5.51.0",
"@vue/test-utils": "^1.3.4",
@@ -60,4 +60,27 @@ describe("SelectPreferredStore.vue", () => {
const emitted = wrapper.emitted();
expect(emitted["updated"][0][0]).toEqual(null);
});
it("updates object store to on non-null selection", async () => {
const wrapper = mountComponent();
await flushPromises();
const els = wrapper.findAll(PREFERENCES.object_store_selection.option_buttons.selector);
expect(els.length).toBe(3);
const galaxyDefaultOption = wrapper.find(
PREFERENCES.object_store_selection.option_button({ object_store_id: "object_store_2" }).selector
);
expect(galaxyDefaultOption.exists()).toBeTruthy();
axiosMock
.onPut(
`/api/histories/${TEST_HISTORY_ID}`,
expect.objectContaining({ preferred_object_store_id: "object_store_2" })
)
.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("object_store_2");
});
});
@@ -1,76 +1,69 @@
<script lang="ts" setup>
import { computed, ref } from "vue";
import axios from "axios";
import SelectObjectStore from "@/components/ObjectStore/SelectObjectStore.vue";
import { prependPath } from "@/utils/redirect";
import { errorMessageAsString } from "@/utils/simple-error";
const props = defineProps({
userPreferredObjectStoreId: {
type: String,
default: null,
},
history: {
type: Object,
required: true,
},
});
const error = ref<string | null>(null);
const selectedObjectStoreId = ref(props.history.preferred_object_store_id);
const newDatasetsDescription = "New dataset outputs from tools and workflows executed in this history";
const galaxySelectionDefaultTitle = "Use Galaxy Defaults";
const galaxySelectionDefaultDescription =
"Selecting this will reset Galaxy to default behaviors configured by your Galaxy administrator.";
const userSelectionDefaultTitle = "Use Your User Preference Defaults";
const userSelectionDefaultDescription =
"Selecting this will cause the history to not set a default and to fallback to your user preference defined default.";
const defaultOptionTitle = computed(() => {
if (props.userPreferredObjectStoreId) {
return userSelectionDefaultTitle;
} else {
return galaxySelectionDefaultTitle;
}
});
const defaultOptionDescription = computed(() => {
if (props.userPreferredObjectStoreId) {
return userSelectionDefaultDescription;
} else {
return galaxySelectionDefaultDescription;
}
});
const emit = defineEmits<{
(e: "updated", id: string | null): void;
}>();
async function handleSubmit(preferredObjectStoreId: string | null) {
const payload = { preferred_object_store_id: preferredObjectStoreId };
const url = prependPath(`api/histories/${props.history.id}`);
try {
await axios.put(url, payload);
} catch (e) {
error.value = errorMessageAsString(e);
}
selectedObjectStoreId.value = preferredObjectStoreId;
emit("updated", preferredObjectStoreId);
}
</script>
<template>
<SelectObjectStore
:parent-error="error"
:parent-error="error || undefined"
:for-what="newDatasetsDescription"
:selected-object-store-id="selectedObjectStoreId"
:default-option-title="defaultOptionTitle"
:default-option-description="defaultOptionDescription"
@onSubmit="handleSubmit" />
</template>
<script>
import axios from "axios";
import SelectObjectStore from "components/ObjectStore/SelectObjectStore";
import { prependPath } from "utils/redirect";
import { errorMessageAsString } from "utils/simple-error";
export default {
components: {
SelectObjectStore,
},
props: {
userPreferredObjectStoreId: {
type: String,
default: null,
},
history: {
type: Object,
required: true,
},
},
data() {
const selectedObjectStoreId = this.history.preferred_object_store_id;
return {
error: null,
selectedObjectStoreId: selectedObjectStoreId,
newDatasetsDescription: "New dataset outputs from tools and workflows executed in this history",
popoverPlacement: "left",
galaxySelectionDefaultTitle: "Use Galaxy Defaults",
galaxySelectionDefaultDescription:
"Selecting this will reset Galaxy to default behaviors configured by your Galaxy administrator.",
userSelectionDefaultTitle: "Use Your User Preference Defaults",
userSelectionDefaultDescription:
"Selecting this will cause the history to not set a default and to fallback to your user preference defined default.",
};
},
computed: {
defaultOptionTitle() {
if (this.userPreferredObjectStoreId) {
return this.userSelectionDefaultTitle;
} else {
return this.galaxySelectionDefaultTitle;
}
},
defaultOptionDescription() {
if (this.userPreferredObjectStoreId) {
return this.userSelectionDefaultDescription;
} else {
return this.galaxySelectionDefaultDescription;
}
},
},
methods: {
async handleSubmit(preferredObjectStoreId) {
const payload = { preferred_object_store_id: preferredObjectStoreId };
const url = prependPath(`api/histories/${this.history.id}`);
try {
await axios.put(url, payload);
} catch (e) {
this.error = errorMessageAsString(e);
}
this.selectedObjectStoreId = preferredObjectStoreId;
this.$emit("updated", preferredObjectStoreId);
},
},
};
</script>
@@ -32,7 +32,7 @@ import ObjectStoreRestrictionSpan from "./ObjectStoreRestrictionSpan";
import QuotaUsageBar from "components/User/DiskUsage/Quota/QuotaUsageBar";
import { QuotaSourceUsageProvider } from "components/User/DiskUsage/Quota/QuotaUsageProvider";
import ObjectStoreBadges from "./ObjectStoreBadges";
import adminConfigMixin from "./adminConfigMixin";
import { adminMarkup } from "./adminConfig";
export default {
components: {
@@ -41,7 +41,6 @@ export default {
QuotaSourceUsageProvider,
QuotaUsageBar,
},
mixins: [adminConfigMixin],
props: {
storageInfo: {
type: Object,
@@ -57,7 +56,7 @@ export default {
return this.storageInfo.quota?.source;
},
descriptionRendered() {
return this.adminMarkup(this.storageInfo.description);
return adminMarkup(this.storageInfo.description);
},
isPrivate() {
return this.storageInfo.private;
@@ -23,8 +23,10 @@ describe("ObjectStoreBadge", () => {
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");
const popoverStub = wrapper.find("b-popover-stub");
const popoverText = popoverStub.text();
expect(popoverText).toContain(TEST_MESSAGE);
expect(popoverText).toContain("more secure by the Galaxy adminstrator");
});
it("should render a valid badge for less_secure type", async () => {
@@ -32,7 +34,9 @@ describe("ObjectStoreBadge", () => {
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");
const popoverStub = wrapper.find("b-popover-stub");
const popoverText = popoverStub.text();
expect(popoverText).toContain(TEST_MESSAGE);
expect(popoverText).toContain("less secure by the Galaxy adminstrator");
});
});
@@ -1,9 +1,79 @@
<script lang="ts" setup>
import { computed } from "vue";
import { adminMarkup } from "./adminConfig";
import { FontAwesomeIcon, FontAwesomeLayers } from "@fortawesome/vue-fontawesome";
import type { components } from "@/schema";
import "./badgeIcons";
type BadgeType = components["schemas"]["BadgeDict"];
const MESSAGES = {
restricted:
"This dataset is stored on storage restricted to a single user. It can not be shared, pubished, or added to Galaxy data libraries.",
user_defined: "This storage was user defined and is not managed by the Galaxy adminstrator.",
quota: "A Galaxy quota is enabled for this object store.",
no_quota: "No Galaxy quota is enabled for this object store.",
faster: "This storage has been marked as a faster option by the Galaxy adminstrator.",
slower: "This storage has been marked as a slower option by the Galaxy adminstrator.",
short_term: "This storage has been marked routinely purged by the Galaxy adminstrator.",
backed_up: "This storage has been marked as backed up by the Galaxy adminstrator.",
not_backed_up: "This storage has been marked as not backed up by the Galaxy adminstrator.",
more_secure:
"This storage has been marked as more secure by the Galaxy adminstrator. The Galaxy web application doesn't make any additional promises regarding security for this storage.",
less_secure:
"This storage has been marked as less secure by the Galaxy adminstrator. The Galaxy web application doesn't make any additional promises regarding security for this storage.",
more_stable:
"This storage has been marked as more stable by the Galaxy adminstrator - expect jobs to fail less because of storage issues for this storage.",
less_stable:
"This storage has been marked as less stable by the Galaxy adminstrator - expect jobs to fail more because of storage issues for this storage.",
cloud: "This is cloud storage.",
};
interface ObjectStoreBadgeProps {
badge: BadgeType;
size?: string;
moreOnHover?: boolean;
}
const props = withDefaults(defineProps<ObjectStoreBadgeProps>(), {
size: "3x",
moreOnHover: true,
});
const advantage = "storage-advantage";
const disadvantage = "storage-disadvantage";
const neutral = "storage-neutral";
const transparent = "reduced-opacity";
const stockMessage = computed(() => {
return MESSAGES[props.badge.type];
});
const layerClasses = computed(() => {
return [`fa-${props.size}`, "fa-fw"];
});
const badgeType = computed(() => {
return props.badge.type;
});
const shrink = computed(() => {
return { transform: "shrink-6" };
});
const message = computed(() => {
return adminMarkup(props.badge.message);
});
</script>
<template>
<span>
<span ref="iconTarget" class="object-store-badge-wrapper">
<FontAwesomeLayers :class="layerClasses" :data-badge-type="badgeType">
<FontAwesomeIcon v-if="badgeType == 'restricted'" icon="user-lock" :class="disadvantage" />
<!--
<FontAwesomeIcon v-if="badgeType == 'user_defined'" icon="plug" :class="neutral" />
-->
<FontAwesomeIcon v-if="badgeType == 'quota'" icon="chart-line" :class="disadvantage" />
<FontAwesomeIcon v-if="badgeType == 'no_quota'" icon="chart-line" :class="neutral" v-bind="shrink" />
<FontAwesomeIcon v-if="badgeType == 'no_quota'" icon="ban" :class="[transparent, advantage]" />
@@ -70,108 +140,6 @@
</span>
</template>
<script>
import adminConfigMixin from "./adminConfigMixin";
import { FontAwesomeIcon, FontAwesomeLayers } from "@fortawesome/vue-fontawesome";
import { library } from "@fortawesome/fontawesome-svg-core";
import {
faUserLock,
faChartLine,
faBan,
faCircleNotch,
faPlug,
faTachometerAlt,
faArchive,
faRecycle,
faKey,
faShieldAlt,
faCloud,
} from "@fortawesome/free-solid-svg-icons";
library.add(
faUserLock,
faChartLine,
faBan,
faCircleNotch,
faPlug,
faTachometerAlt,
faArchive,
faRecycle,
faKey,
faShieldAlt,
faCloud
);
const MESSAGES = {
restricted:
"This dataset is stored on storage restricted to a single user. It can not be shared, pubished, or added to Galaxy data libraries.",
user_defined: "This storage was user defined and is not managed by the Galaxy adminstrator.",
quota: "A Galaxy quota is enabled for this object store.",
no_quota: "No Galaxy quota is enabled for this object store.",
faster: "This storage has been marked as a faster option by the Galaxy adminstrator.",
slower: "This storage has been marked as a slower option by the Galaxy adminstrator.",
short_term: "This storage has been marked routinely purged by the Galaxy adminstrator.",
backed_up: "This storage has been marked as backed up by the Galaxy adminstrator.",
not_backed_up: "This storage has been marked as not backed up by the Galaxy adminstrator.",
more_secure:
"This storage has been marked as more secure by the Galaxy adminstrator. The Galaxy web application doesn't make any additional promises regarding security for this storage.",
less_secure:
"This storage has been marked as less secure by the Galaxy adminstrator. The Galaxy web application doesn't make any additional promises regarding security for this storage.",
more_stable:
"This storage has been marked as more stable by the Galaxy adminstrator - expect jobs to fail less because of storage issues for this storage.",
less_stable:
"This storage has been marked as less stable by the Galaxy adminstrator - expect jobs to fail more because of storage issues for this storage.",
cloud: "This is cloud storage.",
};
export default {
components: {
FontAwesomeLayers,
FontAwesomeIcon,
},
mixins: [adminConfigMixin],
props: {
badge: {
type: Object,
required: true,
},
size: {
type: String,
default: "3x",
},
moreOnHover: {
type: Boolean,
default: true,
},
},
data() {
return {
advantage: "storage-advantage",
disadvantage: "storage-disadvantage",
neutral: "storage-neutral",
transparent: "reduced-opacity",
};
},
computed: {
stockMessage() {
return MESSAGES[this.badge.type];
},
layerClasses() {
return [`fa-${this.size}`, "fa-fw"];
},
badgeType() {
return this.badge.type;
},
shrink() {
return { transform: "shrink-6" };
},
message() {
return this.adminMarkup(this.badge.message);
},
},
};
</script>
<style scoped>
.reduced-opacity {
opacity: 0.65;
@@ -0,0 +1,40 @@
import { shallowMount } from "@vue/test-utils";
import { getLocalVue } from "tests/jest/helpers";
import ObjectStoreBadges from "./ObjectStoreBadges";
import ObjectStoreBadge from "./ObjectStoreBadge";
const localVue = getLocalVue(true);
const TEST_MESSAGE = "a test message provided by backend";
const BADGES = [
{ type: "more_secure", message: TEST_MESSAGE },
{ type: "slower", message: TEST_MESSAGE },
];
describe("ObjectStoreBadges", () => {
let wrapper;
it("should render all badges in array", async () => {
wrapper = shallowMount(ObjectStoreBadges, {
propsData: { badges: BADGES },
localVue,
});
const badgeListEl = wrapper.find(".object-store-badges");
expect(badgeListEl.exists()).toBeTruthy();
const badges = wrapper.findAllComponents(ObjectStoreBadge);
expect(badges.length).toBe(2);
expect(badges.at(0).attributes("size")).toBe("3x");
});
it("should pass along size attributes", async () => {
wrapper = shallowMount(ObjectStoreBadges, {
propsData: { badges: BADGES, size: "2x" },
localVue,
});
const badgeListEl = wrapper.find(".object-store-badges");
expect(badgeListEl.exists()).toBeTruthy();
const badges = wrapper.findAllComponents(ObjectStoreBadge);
expect(badges.length).toBe(2);
expect(badges.at(0).attributes("size")).toBe("2x");
});
});
@@ -1,3 +1,20 @@
<script lang="ts" setup>
import ObjectStoreBadge from "./ObjectStoreBadge.vue";
import type { components } from "@/schema";
type BadgeType = components["schemas"]["BadgeDict"];
interface ObjectStoreBadgesProps {
badges: Array<BadgeType>;
size?: string;
moreOnHover?: boolean;
}
withDefaults(defineProps<ObjectStoreBadgesProps>(), {
size: "3x",
moreOnHover: true,
});
</script>
<template>
<div class="object-store-badges">
<ObjectStoreBadge
@@ -5,31 +22,7 @@
:key="idx"
:badge="badge"
:size="size"
:moreOnHover="moreOnHover">
:more-on-hover="moreOnHover">
</ObjectStoreBadge>
</div>
</template>
<script>
import ObjectStoreBadge from "./ObjectStoreBadge";
export default {
components: {
ObjectStoreBadge,
},
props: {
badges: {
type: Array,
required: true,
},
size: {
type: String,
default: "3x",
},
moreOnHover: {
type: Boolean,
default: true,
},
},
};
</script>
@@ -1,34 +1,28 @@
<script lang="ts" setup>
import Vue from "vue";
import BootstrapVue from "bootstrap-vue";
import { computed } from "vue";
Vue.use(BootstrapVue);
const props = defineProps({
isPrivate: Boolean,
});
const text = computed(() => (props.isPrivate ? "private" : "unrestricted"));
const title = computed(() => {
if (props.isPrivate) {
return "This dataset is stored on storage restricted to a single user. It can not be shared, published, or added to Galaxy data libraries.";
} else {
return "This dataset is stored on unrestricted storage. With sufficient Galaxy permissions, this dataset can be published, shared, or added to Galaxy data libraries.";
}
});
</script>
<template>
<span v-b-tooltip.hover class="stored-how" :title="title">{{ text }}</span>
</template>
<script>
import Vue from "vue";
import BootstrapVue from "bootstrap-vue";
Vue.use(BootstrapVue);
export default {
props: {
isPrivate: {
// private is reserved word
type: Boolean,
},
},
computed: {
text() {
return this.isPrivate ? "private" : "unrestricted";
},
title() {
if (this.isPrivate) {
return "This dataset is stored on storage restricted to a single user. It can not be shared, pubished, or added to Galaxy data libraries.";
} else {
return "This dataset is stored on unrestricted storage. With sufficient Galaxy permissions, this dataset can be published, shared, or added to Galaxy data libraries.";
}
},
},
};
</script>
<style scoped>
/* Give visual indication of mouseover info */
.stored-how {
@@ -7,29 +7,17 @@ import ObjectStoreBadges from "@/components/ObjectStore/ObjectStoreBadges.vue";
import ProvidedQuotaSourceUsageBar from "@/components/User/DiskUsage/Quota/ProvidedQuotaSourceUsageBar.vue";
import { getSelectableObjectStores } from "./services";
const props = defineProps({
selectedObjectStoreId: {
type: String,
default: null,
},
defaultOptionTitle: {
// "Use Your User Preference Defaults"
type: String,
required: true,
},
defaultOptionDescription: {
// "Selecting this will cause the history to not set a default and to fallback to your user preference defined default."
type: String,
required: true,
},
forWhat: {
type: String,
required: true,
},
parentError: {
type: String,
default: null,
},
interface SelectObjectStoreProps {
selectedObjectStoreId?: String | null;
defaultOptionTitle: String;
defaultOptionDescription: String;
forWhat: String;
parentError?: String | null;
}
const props = withDefaults(defineProps<SelectObjectStoreProps>(), {
selectedObjectStoreId: null,
parentError: null,
});
const loading = ref(true);
@@ -77,7 +65,9 @@ function variant(objectStoreId: string) {
}
}
const emit = defineEmits(["onSubmit"]);
const emit = defineEmits<{
(e: "onSubmit", id: string | null): void;
}>();
async function handleSubmit(preferredObjectStoreId: string) {
emit("onSubmit", preferredObjectStoreId);
@@ -1,6 +1,8 @@
import { mount } from "@vue/test-utils";
import { getLocalVue } from "tests/jest/helpers";
import ShowSelectedObjectStore from "./ShowSelectedObjectStore";
import LoadingSpan from "@/components/LoadingSpan.vue";
import DescribeObjectStore from "@/components/ObjectStore/DescribeObjectStore.vue";
import axios from "axios";
import MockAdapter from "axios-mock-adapter";
import flushPromises from "flush-promises";
@@ -34,12 +36,12 @@ describe("ShowSelectedObjectStore", () => {
DescribeObjectStore: true,
},
});
let loadingEl = wrapper.find("loadingspan-stub");
let loadingEl = wrapper.findComponent(LoadingSpan);
expect(loadingEl.exists()).toBeTruthy();
expect(loadingEl.attributes("message")).toBeLocalizationOf("Loading object store details");
expect(loadingEl.find(".loading-message").text()).toContainLocalizationOf("Loading object store details");
await flushPromises();
loadingEl = wrapper.find("loadingspan-stub");
loadingEl = wrapper.findComponent(LoadingSpan);
expect(loadingEl.exists()).toBeFalsy();
expect(wrapper.find("describeobjectstore-stub").exists()).toBeTruthy();
expect(wrapper.findComponent(DescribeObjectStore).exists()).toBeTruthy();
});
});
@@ -1,3 +1,20 @@
<script setup lang="ts">
import LoadingSpan from "@/components/LoadingSpan.vue";
import { ObjectStoreDetailsProvider } from "@/components/providers/ObjectStoreProvider";
import DescribeObjectStore from "@/components/ObjectStore/DescribeObjectStore.vue";
interface ShowSelectObjectStoreProps {
forWhat: String;
preferredObjectStoreId?: String | null;
}
withDefaults(defineProps<ShowSelectObjectStoreProps>(), {
preferredObjectStoreId: null,
});
const loadingMessage = "Loading object store details";
</script>
<template>
<ObjectStoreDetailsProvider
:id="preferredObjectStoreId"
@@ -6,32 +23,3 @@
<DescribeObjectStore v-else :what="forWhat" :storage-info="storageInfo"> </DescribeObjectStore>
</ObjectStoreDetailsProvider>
</template>
<script>
import LoadingSpan from "components/LoadingSpan";
import { ObjectStoreDetailsProvider } from "components/providers/ObjectStoreProvider";
import DescribeObjectStore from "components/ObjectStore/DescribeObjectStore";
export default {
components: {
DescribeObjectStore,
LoadingSpan,
ObjectStoreDetailsProvider,
},
props: {
forWhat: {
type: String,
required: true,
},
preferredObjectStoreId: {
type: String,
default: null,
},
},
data() {
return {
loadingMessage: "Loading object store details",
};
},
};
</script>
@@ -0,0 +1,11 @@
import MarkdownIt from "markdown-it";
export function adminMarkup(markup: string): string | null {
let markupHtml;
if (markup) {
markupHtml = MarkdownIt({ html: true }).render(markup);
} else {
markupHtml = null;
}
return markupHtml;
}
@@ -1,15 +0,0 @@
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;
},
},
};
@@ -0,0 +1,33 @@
/* I can't get this to type properly in type script, so
handling it here in JavaScript. I get a variant of this
error (https://github.com/FortAwesome/Font-Awesome/issues/12575).
*/
import { library } from "@fortawesome/fontawesome-svg-core";
import {
faUserLock,
faChartLine,
faBan,
faCircleNotch,
faPlug,
faTachometerAlt,
faArchive,
faRecycle,
faKey,
faShieldAlt,
faCloud,
} from "@fortawesome/free-solid-svg-icons";
library.add(
faUserLock,
faChartLine,
faBan,
faCircleNotch,
faPlug,
faTachometerAlt,
faArchive,
faRecycle,
faKey,
faShieldAlt,
faCloud
);
@@ -0,0 +1,41 @@
import { mount } from "@vue/test-utils";
import { getLocalVue } from "tests/jest/helpers";
import { setupSelectableMock } from "@/components/ObjectStore/mockServices";
setupSelectableMock();
import flushPromises from "flush-promises";
import ToolSelectPreferredObjectStore from "./ToolSelectPreferredObjectStore.vue";
const localVue = getLocalVue(true);
function mountComponent() {
const wrapper = mount(ToolSelectPreferredObjectStore, {
propsData: { toolPreferredObjectStoreId: null },
localVue,
});
return wrapper;
}
import { ROOT_COMPONENT } from "@/utils/navigation";
const PREFERENCES = ROOT_COMPONENT.preferences;
describe("ToolSelectPreferredObjectStore.vue", () => {
it("updates object store to default on selection null", async () => {
const wrapper = mountComponent();
await flushPromises();
const els = wrapper.findAll(PREFERENCES.object_store_selection.option_buttons.selector);
expect(els.length).toBe(3);
const galaxyDefaultOption = wrapper.find(
PREFERENCES.object_store_selection.option_button({ object_store_id: "__null__" }).selector
);
expect(galaxyDefaultOption.exists()).toBeTruthy();
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);
});
});
@@ -1,48 +1,36 @@
<script lang="ts" setup>
import SelectObjectStore from "@/components/ObjectStore/SelectObjectStore.vue";
import { ref } from "vue";
interface ToolSelectProps {
toolPreferredObjectStoreId?: String | null;
}
const props = withDefaults(defineProps<ToolSelectProps>(), {
toolPreferredObjectStoreId: null,
});
const selectedObjectStoreId = ref<String | null>(props.toolPreferredObjectStoreId);
const newDatasetsDescription = "The default object store for the outputs of this tool";
const defaultOptionTitle = "Use Defaults";
const defaultOptionDescription =
"If the history has a default set, that will be used. If instead, you've set an option in your user preferences - that will be assumed to be your default selection. Finally, the Galaxy configuration will be used.";
const emit = defineEmits<{
(e: "updated", id: string | null): void;
}>();
async function handleSubmit(preferredObjectStoreId: string | null) {
selectedObjectStoreId.value = preferredObjectStoreId;
emit("updated", preferredObjectStoreId);
}
</script>
<template>
<SelectObjectStore
:root="root"
:for-what="newDatasetsDescription"
:selected-object-store-id="selectedObjectStoreId"
:default-option-title="defaultOptionTitle"
:default-option-description="defaultOptionDescription"
@onSubmit="handleSubmit" />
</template>
<script>
import SelectObjectStore from "components/ObjectStore/SelectObjectStore";
export default {
components: {
SelectObjectStore,
},
props: {
root: {
type: String,
required: true,
},
toolPreferredObjectStoreId: {
type: String,
default: null,
},
},
data() {
return {
selectedObjectStoreId: this.toolPreferredObjectStoreId,
newDatasetsDescription: "The default object store for the outputs of this tool",
};
},
computed: {
defaultOptionTitle() {
return "Use Defaults";
},
defaultOptionDescription() {
return "If the history has a default set, that will be used. If instead, you've set an option in your user preferences - that will be assumed to be your default selection. Finally, the Galaxy configuration will be used.";
},
},
methods: {
async handleSubmit(preferredObjectStoreId) {
this.selectedObjectStoreId = preferredObjectStoreId;
this.$emit("updated", preferredObjectStoreId);
},
},
};
</script>
@@ -8,7 +8,7 @@
<ShowSelectedObjectStore
v-if="toolPreferredObjectStoreId"
:preferred-object-store-id="toolPreferredObjectStoreId"
forWhat="Galaxy will default to storing this tool run's output in">
for-what="Galaxy will default to storing this tool run's output in">
</ShowSelectedObjectStore>
<div v-else>
No selection has been made for this tool execution. Defaults from history, user, or Galaxy will be used.
@@ -0,0 +1,41 @@
import { mount } from "@vue/test-utils";
import { getLocalVue } from "tests/jest/helpers";
import { setupSelectableMock } from "@/components/ObjectStore/mockServices";
setupSelectableMock();
import flushPromises from "flush-promises";
import WorkflowSelectPreferredObjectStore from "./WorkflowSelectPreferredObjectStore.vue";
const localVue = getLocalVue(true);
function mountComponent() {
const wrapper = mount(WorkflowSelectPreferredObjectStore, {
propsData: { invocationPreferredObjectStoreId: null },
localVue,
});
return wrapper;
}
import { ROOT_COMPONENT } from "@/utils/navigation";
const PREFERENCES = ROOT_COMPONENT.preferences;
describe("WorkflowSelectPreferredObjectStore.vue", () => {
it("updates object store to default on selection null", async () => {
const wrapper = mountComponent();
await flushPromises();
const els = wrapper.findAll(PREFERENCES.object_store_selection.option_buttons.selector);
expect(els.length).toBe(3);
const galaxyDefaultOption = wrapper.find(
PREFERENCES.object_store_selection.option_button({ object_store_id: "__null__" }).selector
);
expect(galaxyDefaultOption.exists()).toBeTruthy();
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);
});
});
@@ -1,3 +1,31 @@
<script lang="ts" setup>
import SelectObjectStore from "@/components/ObjectStore/SelectObjectStore.vue";
import { ref } from "vue";
interface Props {
invocationPreferredObjectStoreId?: String | null;
}
const props = withDefaults(defineProps<Props>(), {
invocationPreferredObjectStoreId: null,
});
const emit = defineEmits<{
(e: "updated", id: string | null): void;
}>();
const selectedObjectStoreId = ref<String | null>(props.invocationPreferredObjectStoreId);
const newDatasetsDescription = "The default object store for the outputs of this workflow invocation";
const defaultOptionTitle = "Use Defaults";
const defaultOptionDescription =
"If the history has a default set, that will be used. If instead, you've set an option in your user preferences - that will be assumed to be your default selection. Finally, the Galaxy configuration will be used.";
async function handleSubmit(preferredObjectStoreId: string | null) {
selectedObjectStoreId.value = preferredObjectStoreId;
emit("updated", preferredObjectStoreId);
}
</script>
<template>
<SelectObjectStore
:for-what="newDatasetsDescription"
@@ -6,38 +34,3 @@
:default-option-description="defaultOptionDescription"
@onSubmit="handleSubmit" />
</template>
<script>
import SelectObjectStore from "components/ObjectStore/SelectObjectStore";
export default {
components: {
SelectObjectStore,
},
props: {
invocationPreferredObjectStoreId: {
type: String,
default: null,
},
},
data() {
return {
selectedObjectStoreId: this.invocationPreferredObjectStoreId,
newDatasetsDescription: "The default object store for the outputs of this workflow invocation",
};
},
computed: {
defaultOptionTitle() {
return "Use Defaults";
},
defaultOptionDescription() {
return "If the history has a default set, that will be used. If instead, you've set an option in your user preferences - that will be assumed to be your default selection. Finally, the Galaxy configuration will be used.";
},
},
methods: {
async handleSubmit(preferredObjectStoreId) {
this.selectedObjectStoreId = preferredObjectStoreId;
this.$emit("updated", preferredObjectStoreId);
},
},
};
</script>
@@ -9,6 +9,7 @@
</b-button>
<WorkflowTargetPreferredObjectStorePopover
target="workflow-storage-indicator-primary"
:title-suffix="suffixPrimary"
:invocation-preferred-object-store-id="selectedObjectStoreId">
</WorkflowTargetPreferredObjectStorePopover>
<b-modal
+14
View File
@@ -72,6 +72,20 @@ expect.extend({
};
}
},
toContainLocalizationOf(received, str) {
const pass = received.indexOf(testLocalize(str)) >= 0;
if (pass) {
return {
message: () => `expected ${received} to contain localization of ${str}`,
pass: true,
};
} else {
return {
message: () => `expected ${received} to contain localization of ${str}`,
pass: false,
};
}
},
});
// Creates a watcher on the indicated vm/prop for use in testing
+18
View File
@@ -2348,6 +2348,24 @@
resolved "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz"
integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==
"@types/linkify-it@*":
version "3.0.2"
resolved "https://registry.yarnpkg.com/@types/linkify-it/-/linkify-it-3.0.2.tgz#fd2cd2edbaa7eaac7e7f3c1748b52a19143846c9"
integrity sha512-HZQYqbiFVWufzCwexrvh694SOim8z2d+xJl5UNamcvQFejLY/2YUtzXHYi3cHdI7PMlS8ejH2slRAOJQ32aNbA==
"@types/markdown-it@^12.2.3":
version "12.2.3"
resolved "https://registry.yarnpkg.com/@types/markdown-it/-/markdown-it-12.2.3.tgz#0d6f6e5e413f8daaa26522904597be3d6cd93b51"
integrity sha512-GKMHFfv3458yYy+v/N8gjufHO6MSZKCOXpZc5GXIWWy8uldwfmPn98vp81gZ5f9SVw8YYBctgfJ22a2d7AOMeQ==
dependencies:
"@types/linkify-it" "*"
"@types/mdurl" "*"
"@types/mdurl@*":
version "1.0.2"
resolved "https://registry.yarnpkg.com/@types/mdurl/-/mdurl-1.0.2.tgz#e2ce9d83a613bacf284c7be7d491945e39e1f8e9"
integrity sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA==
"@types/mime@*":
version "3.0.1"
resolved "https://registry.npmjs.org/@types/mime/-/mime-3.0.1.tgz"