Skip to content

Commit

Permalink
Merge branch 'release_24.2' into dev
Browse files Browse the repository at this point in the history
  • Loading branch information
nsoranzo committed Jan 22, 2025
2 parents 5f484fc + d1ea6dd commit 4a2bdac
Show file tree
Hide file tree
Showing 10 changed files with 220 additions and 58 deletions.
2 changes: 2 additions & 0 deletions client/src/components/Help/terms.yml
Original file line number Diff line number Diff line change
Expand Up @@ -145,3 +145,5 @@ galaxy:
A utility for uploading files to a Galaxy server from the command line. Use ``galaxy-upload`` to upload
file(s) to a Galaxy server, and ``galaxy-history-search``, a helper utility to find Galaxy histories
to pass to the ``galaxy-upload`` command.
ruleBased: |
Galaxy can bulk import lists & tables of URLs into datasets or collections using reproducible rules.
2 changes: 1 addition & 1 deletion client/src/components/Masthead/Masthead.vue
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ onMounted(() => {
position: absolute;
left: 1.6rem;
top: 1.6rem;
font-size: 0.4rem;
font-size: 0.6rem;
font-weight: bold;
}
}
Expand Down
116 changes: 108 additions & 8 deletions client/src/components/Panels/ToolPanel.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,32 @@ import MockAdapter from "axios-mock-adapter";
import flushPromises from "flush-promises";
import { createPinia } from "pinia";
import { getLocalVue } from "tests/jest/helpers";
import { ref } from "vue";

import toolsList from "@/components/ToolsView/testData/toolsList.json";
import toolsListInPanel from "@/components/ToolsView/testData/toolsListInPanel.json";
import { useUserLocalStorage } from "@/composables/userLocalStorage";
import { useToolStore } from "@/stores/toolStore";

import viewsList from "./testData/viewsList.json";
import viewsListJson from "./testData/viewsList.json";
import { types_to_icons } from "./utilities";

import ToolPanel from "./ToolPanel.vue";

interface ToolPanelView {
id: string;
model_class: string;
name: string;
description: string | null;
view_type: string;
searchable: boolean;
}

const localVue = getLocalVue();

const TEST_PANELS_URI = "/api/tool_panels";
const DEFAULT_VIEW_ID = "default";
const PANEL_VIEW_ERR_MSG = "Error loading panel view";

jest.mock("@/composables/config", () => ({
useConfig: jest.fn(() => ({
Expand All @@ -26,16 +40,59 @@ jest.mock("@/composables/config", () => ({
})),
}));

jest.mock("@/composables/userLocalStorage", () => ({
useUserLocalStorage: jest.fn(() => ref(DEFAULT_VIEW_ID)),
}));

describe("ToolPanel", () => {
it("test navigation of tool panel views menu", async () => {
const viewsList = viewsListJson as Record<string, ToolPanelView>;

/** Mocks and stores a non-default panel view as the current panel view */
function storeNonDefaultView() {
// find a view in object viewsList that is not DEFAULT_VIEW_ID
const viewKey = Object.keys(viewsList).find((id) => id !== DEFAULT_VIEW_ID);
if (!viewKey) {
throw new Error("No non-default view found in viewsList");
}
const view = viewsList[viewKey];
if (!view) {
throw new Error(`View with key ${viewKey} not found in viewsList`);
}
// ref and useUserLocalStorage are already imported at the top
(useUserLocalStorage as jest.Mock).mockImplementation(() => ref(viewKey));
return { viewKey, view };
}

/**
* Sets up wrapper for ToolPanel component
* @param {String} errorView If provided, we mock an error for this view
* @param {Boolean} failDefault If true and error view is provided, we
* mock an error for the default view as well
* @returns wrapper
*/
async function createWrapper(errorView = "", failDefault = false) {
const axiosMock = new MockAdapter(axios);
axiosMock
.onGet(/\/api\/tool_panels\/.*/)
.reply(200, toolsListInPanel)
.onGet(`/api/tools?in_panel=False`)
.replyOnce(200, toolsList)
.onGet(TEST_PANELS_URI)
.reply(200, { default_panel_view: "default", views: viewsList });
.reply(200, { default_panel_view: DEFAULT_VIEW_ID, views: viewsList });

if (errorView) {
axiosMock.onGet(`/api/tool_panels/${errorView}`).reply(400, { err_msg: PANEL_VIEW_ERR_MSG });
if (errorView !== DEFAULT_VIEW_ID && !failDefault) {
axiosMock.onGet(`/api/tool_panels/${DEFAULT_VIEW_ID}`).reply(200, toolsListInPanel);
} else if (failDefault) {
axiosMock.onGet(`/api/tool_panels/${DEFAULT_VIEW_ID}`).reply(400, { err_msg: PANEL_VIEW_ERR_MSG });
}
} else {
// mock response for all panel views
axiosMock.onGet(/\/api\/tool_panels\/.*/).reply(200, toolsListInPanel);
}

// setting this because for the default view, we just show "Tools" as the name
// even though the backend returns "Full Tool Panel"
viewsList[DEFAULT_VIEW_ID]!.name = "Tools";

const pinia = createPinia();
const wrapper = mount(ToolPanel as object, {
Expand All @@ -56,12 +113,17 @@ describe("ToolPanel", () => {

await flushPromises();

return { wrapper };
}

it("test navigation of tool panel views menu", async () => {
const { wrapper } = await createWrapper();
// there is a panel view selector initially collapsed
expect(wrapper.find(".panel-view-selector").exists()).toBe(true);
expect(wrapper.find(".dropdown-menu.show").exists()).toBe(false);

// Test: starts up with a default panel view, click to open menu
expect(wrapper.find("#toolbox-heading").text()).toBe("Tools");
expect(wrapper.find("#toolbox-heading").text()).toBe(viewsList[DEFAULT_VIEW_ID]!.name);
await wrapper.find("#toolbox-heading").trigger("click");
await flushPromises();

Expand All @@ -76,7 +138,7 @@ describe("ToolPanel", () => {
for (const [key, value] of Object.entries(viewsList)) {
// find dropdown item
const currItem = dropdownMenu.find(`[data-panel-id='${key}']`);
if (key !== "default") {
if (key !== DEFAULT_VIEW_ID) {
// Test: check if the panel view has appropriate description
const description = currItem.attributes().title || null;
expect(description).toBe(value.description);
Expand All @@ -93,12 +155,50 @@ describe("ToolPanel", () => {
expect(panelViewIcon.classes()).toContain(
`fa-${types_to_icons[value.view_type as keyof typeof types_to_icons]}`
);
expect(wrapper.find("#toolbox-heading").text()).toBe(value.name);
expect(wrapper.find("#toolbox-heading").text()).toBe(value!.name);
} else {
// Test: check if the default panel view is already selected, and no icon
expect(currItem.find(".fa-check").exists()).toBe(true);
expect(wrapper.find("[data-description='panel view header icon']").exists()).toBe(false);
}
}
});

it("initializes non default current panel view correctly", async () => {
const { viewKey, view } = storeNonDefaultView();

const { wrapper } = await createWrapper();

// starts up with a non default panel view
expect(wrapper.find("#toolbox-heading").text()).toBe(view!.name);
const toolStore = useToolStore();
expect(toolStore.currentPanelView).toBe(viewKey);
});

it("changes panel to default if current panel view throws error", async () => {
const { viewKey, view } = storeNonDefaultView();

const { wrapper } = await createWrapper(viewKey);

// does not initialize non default panel view, and changes to default
expect(wrapper.find("#toolbox-heading").text()).not.toBe(view!.name);
expect(wrapper.find("#toolbox-heading").text()).toBe(viewsList[DEFAULT_VIEW_ID]!.name);
const toolStore = useToolStore();
expect(toolStore.currentPanelView).toBe(DEFAULT_VIEW_ID);

// toolbox loaded
expect(wrapper.find('[data-description="panel toolbox"]').exists()).toBe(true);
});

it("simply shows error if even default panel view throws error", async () => {
const { viewKey } = storeNonDefaultView();

const { wrapper } = await createWrapper(viewKey, true);

// toolbox not loaded
expect(wrapper.find('[data-description="panel toolbox"]').exists()).toBe(false);

// error message shown
expect(wrapper.find('[data-description="tool panel error message"]').text()).toBe(PANEL_VIEW_ERR_MSG);
});
});
17 changes: 14 additions & 3 deletions client/src/components/Panels/ToolPanel.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@
import { faCaretDown } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { storeToRefs } from "pinia";
import { computed, onMounted, ref, watch } from "vue";
import { computed, ref, watch } from "vue";
import { useToolStore } from "@/stores/toolStore";
import localize from "@/utils/localization";
import { errorMessageAsString, rethrowSimple } from "@/utils/simple-error";
import { types_to_icons } from "./utilities";
Expand Down Expand Up @@ -37,17 +38,20 @@ const { currentPanelView, defaultPanelView, isPanelPopulated, loading, panel, pa
const loadingView = ref<string | undefined>(undefined);
const query = ref("");
const showAdvanced = ref(false);
const errorMessage = ref<string | undefined>(undefined);
onMounted(async () => {
initializeToolPanel();
async function initializeToolPanel() {
try {
await toolStore.fetchPanelViews();
await initializeTools();
} catch (error) {
console.error(error);
errorMessage.value = errorMessageAsString(error);
} finally {
arePanelsFetched.value = true;
}
});
}
watch(
() => currentPanelView.value,
Expand Down Expand Up @@ -118,6 +122,8 @@ async function initializeTools() {
await toolStore.initCurrentPanelView(defaultPanelView.value);
} catch (error: any) {
console.error("ToolPanel - Intialize error:", error);
errorMessage.value = errorMessageAsString(error);
rethrowSimple(error);
}
}
Expand Down Expand Up @@ -206,6 +212,11 @@ watch(
@onInsertModule="onInsertModule"
@onInsertWorkflow="onInsertWorkflow"
@onInsertWorkflowSteps="onInsertWorkflowSteps" />
<div v-else-if="errorMessage" data-description="tool panel error message">
<b-alert class="m-2" variant="danger" show>
{{ errorMessage }}
</b-alert>
</div>
<div v-else>
<b-badge class="alert-info w-100">
<LoadingSpan message="Loading Toolbox" />
Expand Down
46 changes: 37 additions & 9 deletions client/src/components/Upload/UploadModal.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script setup>
import { BModal } from "bootstrap-vue";
import { BCarousel, BCarouselSlide, BModal } from "bootstrap-vue";
import { storeToRefs } from "pinia";
import { ref, watch } from "vue";
Expand All @@ -13,7 +13,7 @@ import ExternalLink from "../ExternalLink.vue";
import HelpText from "../Help/HelpText.vue";
import UploadContainer from "./UploadContainer.vue";
const { currentUser } = storeToRefs(useUserStore());
const { currentUser, hasSeenUploadHelp } = storeToRefs(useUserStore());
const { currentHistoryId, currentHistory } = useUserHistories(currentUser);
const { config, isConfigLoaded } = useConfig();
Expand Down Expand Up @@ -65,7 +65,14 @@ async function open(overrideOptions) {
watch(
() => showModal.value,
(modalShown) => setIframeEvents(["galaxy_main"], modalShown)
(modalShown) => {
setIframeEvents(["galaxy_main"], modalShown);
// once the modal closes the first time a user sees help, we never show it again
if (!modalShown && !hasSeenUploadHelp.value) {
hasSeenUploadHelp.value = true;
}
}
);
defineExpose({
Expand All @@ -91,12 +98,33 @@ defineExpose({
to <b>{{ currentHistory.name }}</b>
</span>
</h2>
<span>
<ExternalLink href="https://galaxy-upload.readthedocs.io/en/latest/"> Click here </ExternalLink>
to check out the
<HelpText uri="galaxy.upload.galaxyUploadUtil" text="galaxy-upload" />
util!
</span>
<BCarousel v-if="!hasSeenUploadHelp" :interval="4000" no-touch>
<BCarouselSlide>
<template v-slot:img>
<span class="text-nowrap float-right">
<ExternalLink href="https://galaxy-upload.readthedocs.io/en/latest/">
Click here
</ExternalLink>
to check out the
<HelpText uri="galaxy.upload.galaxyUploadUtil" text="galaxy-upload" />
util!
</span>
</template>
</BCarouselSlide>
<BCarouselSlide>
<template v-slot:img>
<span class="text-nowrap float-right">
More info on <HelpText uri="galaxy.upload.ruleBased" text="Rule-based" /> uploads
<ExternalLink
href="https://training.galaxyproject.org/training-material/topics/galaxy-interface/tutorials/upload-rules/tutorial.html">
here
</ExternalLink>
.
</span>
</template>
</BCarouselSlide>
</BCarousel>
</div>
</template>
<UploadContainer
Expand Down
2 changes: 1 addition & 1 deletion client/src/composables/persistentRef.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export function syncRefToLocalStorage<T>(key: string, refToSync: Ref<T>) {
window.localStorage.setItem(key, stringified);
};

if (stored) {
if (stored !== null) {
try {
refToSync.value = parse(stored, typeof refToSync.value as "string" | "number" | "boolean" | "object");
} catch (e) {
Expand Down
Loading

0 comments on commit 4a2bdac

Please sign in to comment.