Skip to content

Commit 3baafc2

Browse files
Merge remote-tracking branch 'upstream/release_24.1' into HEAD
2 parents f3e9ff4 + b0fe8c9 commit 3baafc2

File tree

4 files changed

+165
-45
lines changed

4 files changed

+165
-45
lines changed

client/src/components/Panels/ToolPanel.test.ts

+108-8
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,32 @@ import MockAdapter from "axios-mock-adapter";
66
import flushPromises from "flush-promises";
77
import { createPinia } from "pinia";
88
import { getLocalVue } from "tests/jest/helpers";
9+
import { ref } from "vue";
910

1011
import toolsList from "@/components/ToolsView/testData/toolsList.json";
1112
import toolsListInPanel from "@/components/ToolsView/testData/toolsListInPanel.json";
13+
import { useUserLocalStorage } from "@/composables/userLocalStorage";
14+
import { useToolStore } from "@/stores/toolStore";
1215

13-
import viewsList from "./testData/viewsList.json";
16+
import viewsListJson from "./testData/viewsList.json";
1417
import { types_to_icons } from "./utilities";
1518

1619
import ToolPanel from "./ToolPanel.vue";
1720

21+
interface ToolPanelView {
22+
id: string;
23+
model_class: string;
24+
name: string;
25+
description: string | null;
26+
view_type: string;
27+
searchable: boolean;
28+
}
29+
1830
const localVue = getLocalVue();
1931

2032
const TEST_PANELS_URI = "/api/tool_panels";
33+
const DEFAULT_VIEW_ID = "default";
34+
const PANEL_VIEW_ERR_MSG = "Error loading panel view";
2135

2236
jest.mock("@/composables/config", () => ({
2337
useConfig: jest.fn(() => ({
@@ -26,16 +40,59 @@ jest.mock("@/composables/config", () => ({
2640
})),
2741
}));
2842

43+
jest.mock("@/composables/userLocalStorage", () => ({
44+
useUserLocalStorage: jest.fn(() => ref(DEFAULT_VIEW_ID)),
45+
}));
46+
2947
describe("ToolPanel", () => {
30-
it("test navigation of tool panel views menu", async () => {
48+
const viewsList = viewsListJson as Record<string, ToolPanelView>;
49+
50+
/** Mocks and stores a non-default panel view as the current panel view */
51+
function storeNonDefaultView() {
52+
// find a view in object viewsList that is not DEFAULT_VIEW_ID
53+
const viewKey = Object.keys(viewsList).find((id) => id !== DEFAULT_VIEW_ID);
54+
if (!viewKey) {
55+
throw new Error("No non-default view found in viewsList");
56+
}
57+
const view = viewsList[viewKey];
58+
if (!view) {
59+
throw new Error(`View with key ${viewKey} not found in viewsList`);
60+
}
61+
// ref and useUserLocalStorage are already imported at the top
62+
(useUserLocalStorage as jest.Mock).mockImplementation(() => ref(viewKey));
63+
return { viewKey, view };
64+
}
65+
66+
/**
67+
* Sets up wrapper for ToolPanel component
68+
* @param {String} errorView If provided, we mock an error for this view
69+
* @param {Boolean} failDefault If true and error view is provided, we
70+
* mock an error for the default view as well
71+
* @returns wrapper
72+
*/
73+
async function createWrapper(errorView = "", failDefault = false) {
3174
const axiosMock = new MockAdapter(axios);
3275
axiosMock
33-
.onGet(/\/api\/tool_panels\/.*/)
34-
.reply(200, toolsListInPanel)
3576
.onGet(`/api/tools?in_panel=False`)
3677
.replyOnce(200, toolsList)
3778
.onGet(TEST_PANELS_URI)
38-
.reply(200, { default_panel_view: "default", views: viewsList });
79+
.reply(200, { default_panel_view: DEFAULT_VIEW_ID, views: viewsList });
80+
81+
if (errorView) {
82+
axiosMock.onGet(`/api/tool_panels/${errorView}`).reply(400, { err_msg: PANEL_VIEW_ERR_MSG });
83+
if (errorView !== DEFAULT_VIEW_ID && !failDefault) {
84+
axiosMock.onGet(`/api/tool_panels/${DEFAULT_VIEW_ID}`).reply(200, toolsListInPanel);
85+
} else if (failDefault) {
86+
axiosMock.onGet(`/api/tool_panels/${DEFAULT_VIEW_ID}`).reply(400, { err_msg: PANEL_VIEW_ERR_MSG });
87+
}
88+
} else {
89+
// mock response for all panel views
90+
axiosMock.onGet(/\/api\/tool_panels\/.*/).reply(200, toolsListInPanel);
91+
}
92+
93+
// setting this because for the default view, we just show "Tools" as the name
94+
// even though the backend returns "Full Tool Panel"
95+
viewsList[DEFAULT_VIEW_ID]!.name = "Tools";
3996

4097
const pinia = createPinia();
4198
const wrapper = mount(ToolPanel as object, {
@@ -55,12 +112,17 @@ describe("ToolPanel", () => {
55112

56113
await flushPromises();
57114

115+
return { wrapper };
116+
}
117+
118+
it("test navigation of tool panel views menu", async () => {
119+
const { wrapper } = await createWrapper();
58120
// there is a panel view selector initially collapsed
59121
expect(wrapper.find(".panel-view-selector").exists()).toBe(true);
60122
expect(wrapper.find(".dropdown-menu.show").exists()).toBe(false);
61123

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

@@ -75,7 +137,7 @@ describe("ToolPanel", () => {
75137
for (const [key, value] of Object.entries(viewsList)) {
76138
// find dropdown item
77139
const currItem = dropdownMenu.find(`[data-panel-id='${key}']`);
78-
if (key !== "default") {
140+
if (key !== DEFAULT_VIEW_ID) {
79141
// Test: check if the panel view has appropriate description
80142
const description = currItem.attributes().title || null;
81143
expect(description).toBe(value.description);
@@ -92,12 +154,50 @@ describe("ToolPanel", () => {
92154
expect(panelViewIcon.classes()).toContain(
93155
`fa-${types_to_icons[value.view_type as keyof typeof types_to_icons]}`
94156
);
95-
expect(wrapper.find("#toolbox-heading").text()).toBe(value.name);
157+
expect(wrapper.find("#toolbox-heading").text()).toBe(value!.name);
96158
} else {
97159
// Test: check if the default panel view is already selected, and no icon
98160
expect(currItem.find(".fa-check").exists()).toBe(true);
99161
expect(wrapper.find("[data-description='panel view header icon']").exists()).toBe(false);
100162
}
101163
}
102164
});
165+
166+
it("initializes non default current panel view correctly", async () => {
167+
const { viewKey, view } = storeNonDefaultView();
168+
169+
const { wrapper } = await createWrapper();
170+
171+
// starts up with a non default panel view
172+
expect(wrapper.find("#toolbox-heading").text()).toBe(view!.name);
173+
const toolStore = useToolStore();
174+
expect(toolStore.currentPanelView).toBe(viewKey);
175+
});
176+
177+
it("changes panel to default if current panel view throws error", async () => {
178+
const { viewKey, view } = storeNonDefaultView();
179+
180+
const { wrapper } = await createWrapper(viewKey);
181+
182+
// does not initialize non default panel view, and changes to default
183+
expect(wrapper.find("#toolbox-heading").text()).not.toBe(view!.name);
184+
expect(wrapper.find("#toolbox-heading").text()).toBe(viewsList[DEFAULT_VIEW_ID]!.name);
185+
const toolStore = useToolStore();
186+
expect(toolStore.currentPanelView).toBe(DEFAULT_VIEW_ID);
187+
188+
// toolbox loaded
189+
expect(wrapper.find('[data-description="panel toolbox"]').exists()).toBe(true);
190+
});
191+
192+
it("simply shows error if even default panel view throws error", async () => {
193+
const { viewKey } = storeNonDefaultView();
194+
195+
const { wrapper } = await createWrapper(viewKey, true);
196+
197+
// toolbox not loaded
198+
expect(wrapper.find('[data-description="panel toolbox"]').exists()).toBe(false);
199+
200+
// error message shown
201+
expect(wrapper.find('[data-description="tool panel error message"]').text()).toBe(PANEL_VIEW_ERR_MSG);
202+
});
103203
});

client/src/components/Panels/ToolPanel.vue

+14-3
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@
22
import { faCaretDown } from "@fortawesome/free-solid-svg-icons";
33
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
44
import { storeToRefs } from "pinia";
5-
import { computed, onMounted, ref, watch } from "vue";
5+
import { computed, ref, watch } from "vue";
66
77
import { useToolStore } from "@/stores/toolStore";
88
import localize from "@/utils/localization";
9+
import { errorMessageAsString, rethrowSimple } from "@/utils/simple-error";
910
1011
import { types_to_icons } from "./utilities";
1112
@@ -36,17 +37,20 @@ const { currentPanelView, defaultPanelView, isPanelPopulated, loading, panel, pa
3637
const loadingView = ref<string | undefined>(undefined);
3738
const query = ref("");
3839
const showAdvanced = ref(false);
40+
const errorMessage = ref<string | undefined>(undefined);
3941
40-
onMounted(async () => {
42+
initializeToolPanel();
43+
async function initializeToolPanel() {
4144
try {
4245
await toolStore.fetchPanelViews();
4346
await initializeTools();
4447
} catch (error) {
4548
console.error(error);
49+
errorMessage.value = errorMessageAsString(error);
4650
} finally {
4751
arePanelsFetched.value = true;
4852
}
49-
});
53+
}
5054
5155
watch(
5256
() => currentPanelView.value,
@@ -117,6 +121,8 @@ async function initializeTools() {
117121
await toolStore.initCurrentPanelView(defaultPanelView.value);
118122
} catch (error: any) {
119123
console.error("ToolPanel - Intialize error:", error);
124+
errorMessage.value = errorMessageAsString(error);
125+
rethrowSimple(error);
120126
}
121127
}
122128
@@ -204,6 +210,11 @@ watch(
204210
@onInsertModule="onInsertModule"
205211
@onInsertWorkflow="onInsertWorkflow"
206212
@onInsertWorkflowSteps="onInsertWorkflowSteps" />
213+
<div v-else-if="errorMessage" data-description="tool panel error message">
214+
<b-alert class="m-2" variant="danger" show>
215+
{{ errorMessage }}
216+
</b-alert>
217+
</div>
207218
<div v-else>
208219
<b-badge class="alert-info w-100">
209220
<LoadingSpan message="Loading Toolbox" />

client/src/stores/toolStore.ts

+42-33
Original file line numberDiff line numberDiff line change
@@ -158,28 +158,33 @@ export const useToolStore = defineStore("toolStore", () => {
158158
}
159159

160160
async function fetchTools(filterSettings?: FilterSettings) {
161+
// This is if we are performing a backend search
161162
if (filterSettings && Object.keys(filterSettings).length !== 0) {
162163
// Parsing filterSettings to Whoosh query
163164
const q = createWhooshQuery(filterSettings);
164165
// already have results for this query
165166
if (toolResults.value[q]) {
166167
return;
167168
}
168-
const { data } = await axios.get(`${getAppRoot()}api/tools`, { params: { q } });
169-
saveToolResults(q, data);
169+
try {
170+
const { data } = await axios.get(`${getAppRoot()}api/tools`, { params: { q } });
171+
saveToolResults(q, data);
172+
} catch (e) {
173+
rethrowSimple(e);
174+
}
170175
}
176+
177+
// This is if we are fetching all tools by ids
171178
if (!loading.value && !allToolsByIdFetched.value) {
172179
loading.value = true;
173-
await axios
174-
.get(`${getAppRoot()}api/tools?in_panel=False`)
175-
.then(({ data }) => {
176-
saveAllTools(data as Tool[]);
177-
loading.value = false;
178-
})
179-
.catch((error) => {
180-
console.error(error);
181-
loading.value = false;
182-
});
180+
try {
181+
const { data } = await axios.get(`${getAppRoot()}api/tools?in_panel=False`);
182+
saveAllTools(data as Tool[]);
183+
} catch (e) {
184+
rethrowSimple(e);
185+
} finally {
186+
loading.value = false;
187+
}
183188
}
184189
}
185190

@@ -204,23 +209,24 @@ export const useToolStore = defineStore("toolStore", () => {
204209
async function initCurrentPanelView(siteDefaultPanelView: string) {
205210
if (!loading.value && !isPanelPopulated.value) {
206211
loading.value = true;
207-
const panelView = currentPanelView.value || siteDefaultPanelView;
208-
if (currentPanelView.value == "") {
209-
currentPanelView.value = panelView;
212+
currentPanelView.value = currentPanelView.value || siteDefaultPanelView;
213+
try {
214+
if (!currentPanelView.value) {
215+
throw new Error("No valid panel view found.");
216+
}
217+
const { data } = await axios.get(`${getAppRoot()}api/tool_panels/${currentPanelView.value}`);
218+
savePanelView(currentPanelView.value, data);
219+
loading.value = false;
220+
} catch (e) {
221+
loading.value = false;
222+
223+
if (currentPanelView.value !== siteDefaultPanelView) {
224+
// If the stored panelView failed to load, try the default panel for this site.
225+
await setCurrentPanelView(siteDefaultPanelView);
226+
} else {
227+
rethrowSimple(e);
228+
}
210229
}
211-
await axios
212-
.get(`${getAppRoot()}api/tool_panels/${panelView}`)
213-
.then(({ data }) => {
214-
loading.value = false;
215-
savePanelView(panelView, data);
216-
})
217-
.catch(async (error) => {
218-
loading.value = false;
219-
if (error.response && error.response.status == 400) {
220-
// Assume the stored panelView disappeared, revert to the panel default for this site.
221-
await setCurrentPanelView(siteDefaultPanelView);
222-
}
223-
});
224230
}
225231
}
226232

@@ -235,18 +241,21 @@ export const useToolStore = defineStore("toolStore", () => {
235241
const { data } = await axios.get(`${getAppRoot()}api/tool_panels/${panelView}`);
236242
currentPanelView.value = panelView;
237243
savePanelView(panelView, data);
238-
loading.value = false;
239244
} catch (e) {
240-
const error = e as { response: { data: { err_msg: string } } };
241-
console.error("Could not change panel view", error.response.data.err_msg ?? error.response);
245+
rethrowSimple(e);
246+
} finally {
242247
loading.value = false;
243248
}
244249
}
245250
}
246251

247252
async function fetchPanel(panelView: string) {
248-
const { data } = await axios.get(`${getAppRoot()}api/tool_panels/${panelView}`);
249-
savePanelView(panelView, data);
253+
try {
254+
const { data } = await axios.get(`${getAppRoot()}api/tool_panels/${panelView}`);
255+
savePanelView(panelView, data);
256+
} catch (e) {
257+
rethrowSimple(e);
258+
}
250259
}
251260

252261
function saveToolForId(toolId: string, toolData: Tool) {

lib/galaxy/util/config_templates.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@
6666

6767

6868
class StrictModel(BaseModel):
69-
model_config = ConfigDict(extra="forbid")
69+
model_config = ConfigDict(extra="forbid", coerce_numbers_to_str=True)
7070

7171

7272
class BaseTemplateVariable(StrictModel):

0 commit comments

Comments
 (0)