Skip to content

Commit

Permalink
Merge remote-tracking branch 'upstream/release_24.1' into HEAD
Browse files Browse the repository at this point in the history
  • Loading branch information
ahmedhamidawan committed Jan 21, 2025
2 parents f3e9ff4 + b0fe8c9 commit 3baafc2
Show file tree
Hide file tree
Showing 4 changed files with 165 additions and 45 deletions.
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
75 changes: 42 additions & 33 deletions client/src/stores/toolStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,28 +158,33 @@ export const useToolStore = defineStore("toolStore", () => {
}

async function fetchTools(filterSettings?: FilterSettings) {
// This is if we are performing a backend search
if (filterSettings && Object.keys(filterSettings).length !== 0) {
// Parsing filterSettings to Whoosh query
const q = createWhooshQuery(filterSettings);
// already have results for this query
if (toolResults.value[q]) {
return;
}
const { data } = await axios.get(`${getAppRoot()}api/tools`, { params: { q } });
saveToolResults(q, data);
try {
const { data } = await axios.get(`${getAppRoot()}api/tools`, { params: { q } });
saveToolResults(q, data);
} catch (e) {
rethrowSimple(e);
}
}

// This is if we are fetching all tools by ids
if (!loading.value && !allToolsByIdFetched.value) {
loading.value = true;
await axios
.get(`${getAppRoot()}api/tools?in_panel=False`)
.then(({ data }) => {
saveAllTools(data as Tool[]);
loading.value = false;
})
.catch((error) => {
console.error(error);
loading.value = false;
});
try {
const { data } = await axios.get(`${getAppRoot()}api/tools?in_panel=False`);
saveAllTools(data as Tool[]);
} catch (e) {
rethrowSimple(e);
} finally {
loading.value = false;
}
}
}

Expand All @@ -204,23 +209,24 @@ export const useToolStore = defineStore("toolStore", () => {
async function initCurrentPanelView(siteDefaultPanelView: string) {
if (!loading.value && !isPanelPopulated.value) {
loading.value = true;
const panelView = currentPanelView.value || siteDefaultPanelView;
if (currentPanelView.value == "") {
currentPanelView.value = panelView;
currentPanelView.value = currentPanelView.value || siteDefaultPanelView;
try {
if (!currentPanelView.value) {
throw new Error("No valid panel view found.");
}
const { data } = await axios.get(`${getAppRoot()}api/tool_panels/${currentPanelView.value}`);
savePanelView(currentPanelView.value, data);
loading.value = false;
} catch (e) {
loading.value = false;

if (currentPanelView.value !== siteDefaultPanelView) {
// If the stored panelView failed to load, try the default panel for this site.
await setCurrentPanelView(siteDefaultPanelView);
} else {
rethrowSimple(e);
}
}
await axios
.get(`${getAppRoot()}api/tool_panels/${panelView}`)
.then(({ data }) => {
loading.value = false;
savePanelView(panelView, data);
})
.catch(async (error) => {
loading.value = false;
if (error.response && error.response.status == 400) {
// Assume the stored panelView disappeared, revert to the panel default for this site.
await setCurrentPanelView(siteDefaultPanelView);
}
});
}
}

Expand All @@ -235,18 +241,21 @@ export const useToolStore = defineStore("toolStore", () => {
const { data } = await axios.get(`${getAppRoot()}api/tool_panels/${panelView}`);
currentPanelView.value = panelView;
savePanelView(panelView, data);
loading.value = false;
} catch (e) {
const error = e as { response: { data: { err_msg: string } } };
console.error("Could not change panel view", error.response.data.err_msg ?? error.response);
rethrowSimple(e);
} finally {
loading.value = false;
}
}
}

async function fetchPanel(panelView: string) {
const { data } = await axios.get(`${getAppRoot()}api/tool_panels/${panelView}`);
savePanelView(panelView, data);
try {
const { data } = await axios.get(`${getAppRoot()}api/tool_panels/${panelView}`);
savePanelView(panelView, data);
} catch (e) {
rethrowSimple(e);
}
}

function saveToolForId(toolId: string, toolData: Tool) {
Expand Down
2 changes: 1 addition & 1 deletion lib/galaxy/util/config_templates.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@


class StrictModel(BaseModel):
model_config = ConfigDict(extra="forbid")
model_config = ConfigDict(extra="forbid", coerce_numbers_to_str=True)


class BaseTemplateVariable(StrictModel):
Expand Down

0 comments on commit 3baafc2

Please sign in to comment.