Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merge Release 24.2 into dev #19443

Open
wants to merge 13 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -55,12 +112,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 @@ -75,7 +137,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 @@ -92,12 +154,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 @@ -36,17 +37,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 @@ -117,6 +121,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 @@ -204,6 +210,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
Loading