Skip to content

Commit

Permalink
Merge pull request #19423 from ahmedhamidawan/catch_errors_in_toolStore
Browse files Browse the repository at this point in the history
[24.1] Catch errors in `toolStore` and `ToolPanel`
  • Loading branch information
mvdbeek authored Jan 21, 2025
2 parents d18ab54 + 5ebece7 commit b0fe8c9
Show file tree
Hide file tree
Showing 3 changed files with 149 additions and 42 deletions.
99 changes: 93 additions & 6 deletions client/src/components/Panels/ToolPanel.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { createPinia } from "pinia";
import { getLocalVue } from "tests/jest/helpers";

import { useConfig } from "@/composables/config";
import { useToolStore } from "@/stores/toolStore";

import viewsList from "./testData/viewsList";
import ToolPanel from "./ToolPanel";
Expand All @@ -18,23 +19,66 @@ import { types_to_icons } from "./utilities";
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.mockReturnValue({
config: {},
isConfigLoaded: true,
});

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

describe("ToolPanel", () => {
it("test navigation of tool panel views menu", async () => {
/** 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);
const view = viewsList[viewKey];

// mock pre set current panel view to be the non-default view
const { ref } = require("vue");
const { useUserLocalStorage } = require("@/composables/userLocalStorage");
useUserLocalStorage.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, {
Expand All @@ -54,12 +98,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 @@ -74,7 +123,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 @@ -97,4 +146,42 @@ describe("ToolPanel", () => {
}
}
});

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 @@ -3,10 +3,11 @@ import { library } from "@fortawesome/fontawesome-svg-core";
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 @@ -40,17 +41,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 @@ -106,6 +110,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 @@ -187,6 +193,11 @@ function onInsertWorkflowSteps(workflowId: string, workflowStepCount: number | u
@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

0 comments on commit b0fe8c9

Please sign in to comment.