From cc8119d2c9bea9444be60bbcab391e4a0d1de14f Mon Sep 17 00:00:00 2001 From: Mikhail-iontsev Date: Sun, 14 Apr 2024 16:07:59 +0700 Subject: [PATCH 1/2] feat: added config for custom error messages feat: added accordion for technical details of an error --- .../plugins/tailwind/components/accordion.ts | 86 ++++ src/app/plugins/tailwind/tailwindStyles.ts | 2 + src/main.ts | 4 +- .../exploreReports/model/store/data.module.ts | 380 +++++++++--------- .../error/model/config/errorMessages.ts | 40 ++ src/widgets/error/ui/Error.vue | 101 +++-- .../explorer/model/store/explorer.module.ts | 13 +- 7 files changed, 374 insertions(+), 252 deletions(-) create mode 100644 src/app/plugins/tailwind/components/accordion.ts create mode 100644 src/widgets/error/model/config/errorMessages.ts diff --git a/src/app/plugins/tailwind/components/accordion.ts b/src/app/plugins/tailwind/components/accordion.ts new file mode 100644 index 00000000..a353e755 --- /dev/null +++ b/src/app/plugins/tailwind/components/accordion.ts @@ -0,0 +1,86 @@ +export default { + accordiontab: { + root: { + class: "mb-1", + }, + header: ({ props }) => ({ + class: [ + // State + { + "select-none pointer-events-none cursor-default opacity-60": + props?.disabled, + }, + ], + }), + headerAction: ({ context }) => ({ + class: [ + //Font + "font-bold", + "leading-none", + + // Alignments + "flex items-center", + "relative", + + // Sizing + "p-5", + + // Shape + "rounded-t-md", + { + "rounded-br-md rounded-bl-md": !context.active, + "rounded-br-0 rounded-bl-0": context.active, + }, + + // Color + "border border-surface-200 dark:border-surface-700", + "bg-surface-50 dark:bg-surface-800", + "text-surface-600 dark:text-surface-0/80", + { "text-surface-900": context.active }, + + // Transition + "transition duration-200 ease-in-out", + "transition-shadow duration-200", + + // States + "hover:bg-surface-100 dark:hover:bg-surface-700", + "hover:text-surface-900", + "focus:outline-none focus:outline-offset-0 focus-visible:ring focus-visible:ring-primary-400/50 ring-inset dark:focus-visible:ring-primary-300/50", // Focus + + // Misc + "cursor-pointer no-underline select-none", + ], + }), + headerIcon: { + class: "inline-block mr-2", + }, + headerTitle: { + class: "leading-none", + }, + content: { + class: [ + // Spacing + "p-5", + + //Shape + "rounded-tl-none rounded-tr-none rounded-br-lg rounded-bl-lg", + "border-t-0", + + // Color + "bg-surface-0 dark:bg-surface-800", + "border border-surface-200 dark:border-surface-700", + "text-surface-700 dark:text-surface-0/80", + ], + }, + transition: { + enterFromClass: "max-h-0", + enterActiveClass: + "overflow-hidden transition-[max-height] duration-1000 ease-[cubic-bezier(0.42,0,0.58,1)]", + enterToClass: "max-h-[1000px]", + leaveFromClass: "max-h-[1000px]", + leaveActiveClass: + "overflow-hidden transition-[max-height] duration-[450ms] ease-[cubic-bezier(0,1,0,1)]", + leaveToClass: "max-h-0", + }, + }, +}; diff --git a/src/app/plugins/tailwind/tailwindStyles.ts b/src/app/plugins/tailwind/tailwindStyles.ts index 09085622..799a31f6 100644 --- a/src/app/plugins/tailwind/tailwindStyles.ts +++ b/src/app/plugins/tailwind/tailwindStyles.ts @@ -25,6 +25,7 @@ import slider from "@/app/plugins/tailwind/components/slider"; import cascadeSelect from "@/app/plugins/tailwind/components/cascadeSelect"; import floatLabel from "@/app/plugins/tailwind/components/floatLabel"; import checkBox from "@/app/plugins/tailwind/components/checkBox"; +import accordion from "@/app/plugins/tailwind/components/accordion"; export const tailwindTheme = { tabview, @@ -54,4 +55,5 @@ export const tailwindTheme = { cascadeSelect, floatLabel, checkBox, + accordion, }; diff --git a/src/main.ts b/src/main.ts index 40979a59..a08b9ba7 100644 --- a/src/main.ts +++ b/src/main.ts @@ -20,6 +20,7 @@ import { tailwindTheme } from "@/app/plugins/tailwind/tailwindStyles"; import ConfirmationService from "primevue/confirmationservice"; import resize from "@/shared/lib/directives/resize"; import { errorActions } from "@/widgets/error"; +import errorMessages from "@/widgets/error/model/config/errorMessages"; // adds reactive router module to global state sync(store, router); @@ -34,12 +35,11 @@ environment.load().then(() => { app.config.errorHandler = (err) => { // Handle the error globally store.dispatch(errorActions.NEW_ERROR, { - message: `An unexpected error has occurred (${err.name})`, + userMessage: errorMessages.technicalError.codeError, name: err.name, details: err.message, stack: err.stack, type: "unexpected", - page: store.state.route.name, }); }; app diff --git a/src/processes/exploreReports/model/store/data.module.ts b/src/processes/exploreReports/model/store/data.module.ts index b7af3d3d..3f4d30a6 100644 --- a/src/processes/exploreReports/model/store/data.module.ts +++ b/src/processes/exploreReports/model/store/data.module.ts @@ -13,10 +13,10 @@ import { } from "./actions.type"; import { errorActions } from "@/widgets/error"; -import { SourceRelease } from "@/processes/exploreReports/model/interfaces/files/SourceIndex"; import db from "@/shared/api/duckdb/instance"; import getDuckDBFilePath from "@/shared/api/duckdb/files"; import environment from "@/shared/api/environment"; +import errorMessages from "@/widgets/error/model/config/errorMessages"; const state = { data: {}, @@ -73,6 +73,65 @@ async function fetchDuckDBData(file, payload, path) { })); } +function commitData(data, { dispatch, commit }, reportName) { + try { + postprocessing[reportName] + ? commit(SET_DATA, postprocessing[reportName](data)) + : commit(SET_DATA, data); + } catch (e) { + dispatch(errorActions.NEW_ERROR, { + userMessage: errorMessages.technicalError.codeError, + name: e.name, + details: e.message, + stack: e.stack, + type: "unexpected", + page: reportName, + }); + } +} + +function processData(data, isDuckDb, fileName) { + if (isDuckDb) { + return convertTableToArray(data); + } + if (!isDuckDb && preprocessing[fileName]) { + return preprocessing[fileName](data); + } else { + return data; + } +} + +function handleNetworkError(responses, { dispatch }, reportName, isDuckDb) { + const errorCode = responses[0].reason?.response?.status; + let errorMessage; + const errorDetails = responses.map((val) => ({ + url: val.reason?.config?.url, + errorCode, + })); + if (isDuckDb) { + errorMessage = + "The file is unavailable or the server isn't responding. Please check your internet connection and your data folder then try again"; + } + if (errorCode === 404 && !isDuckDb) { + errorMessage = errorMessages.reportsMissingFiles[reportName]; + } + if ( + ((errorCode && errorCode >= 500) || typeof errorCode !== "number") && + !isDuckDb + ) { + errorMessage = errorMessages.technicalError.networkError; + } + + if (errorMessage) { + const message = responses[0].reason.stack; + dispatch(errorActions.NEW_ERROR, { + userMessage: errorMessage, + technicalMessage: { errorCode, errorDetails, message }, + details: errorDetails, + }); + } +} + function convertTableToArray(table) { const dataTable = []; for (const row of table) { @@ -94,6 +153,9 @@ const actions = { }, async [FETCH_FILES]({ commit, dispatch, rootState }, payload) { + const isDuckDb = + payload.duckdb_supported && environment.DUCKDB_ENABLED === "true"; + const reportName = rootState.route.name; const path = { cdm: { cdm_source_key: rootState.route.params.cdm }, release: rootState.route.params.release, @@ -101,11 +163,7 @@ const actions = { concept: rootState.route.params.concept, }; const promises = payload.files.map((file) => { - if ( - payload.duckdb_supported && - environment.DUCKDB_ENABLED === "true" && - file.source !== "axios" - ) { + if (isDuckDb && file.source !== "axios") { return fetchDuckDBData(file, payload, path); } else { return fetchAxiosData(file, path); @@ -113,47 +171,37 @@ const actions = { }); let data = {}; - await Promise.allSettled(promises).then((responses) => { responses.forEach((response, index) => { const status = response.status; const fileData = response.value?.data; const fileName = payload.files[index].name; + const isRequired = payload.files[index].required; if (status === "fulfilled") { - data[fileName] = - response.value.duckdb_supported && - environment.DUCKDB_ENABLED === "true" && - payload.files[index].source !== "axios" - ? convertTableToArray(fileData) - : preprocessing[fileName] - ? preprocessing[fileName](fileData) - : fileData; - } else if (status === "rejected" && payload.files[index].required) { - const message = response.reason.message; - const url = response.reason?.config?.url; - dispatch(errorActions.NEW_ERROR, { message, details: url }); - data = null; - return; + //data processing + data[fileName] = processData( + fileData, + isDuckDb && payload.files[index].source !== "axios", + fileName + ); } else { + if (isRequired) { + handleNetworkError( + [response], + { dispatch }, + reportName, + isDuckDb && payload.files[index].source !== "axios" + ); + data = null; + return; + } data[fileName] = []; } }); - try { - if (data) { - postprocessing[rootState.route.name] - ? commit(SET_DATA, postprocessing[rootState.route.name](data)) - : commit(SET_DATA, data); - } - } catch (e) { - dispatch(errorActions.NEW_ERROR, { - message: `An unexpected error has occurred (${e.name})`, - name: e.name, - details: e.message, - stack: e.stack, - type: "unexpected", - page: rootState.route.name, - }); + + if (data) { + commitData(data, { dispatch, commit }, reportName); } }); }, @@ -162,94 +210,61 @@ const actions = { { commit, dispatch, rootState, rootGetters }, payload ) { - const promises = payload.files.reduce( - (obj, file) => ({ - ...obj, - [file.name]: rootGetters.getSources.reduce( - (filesArray, currentSource) => { - const loadedFiles = file.instanceParams.reduce( - (array, currentInstance) => { - return [ - ...array, - payload.duckdb_supported && - environment.DUCKDB_ENABLED === "true" && - file.source !== "axios" - ? fetchDuckDBData(file, payload, { - cdm: currentSource, - release: currentSource.releases[0].release_id, - domain: currentInstance.domain - ? currentInstance.domain - : rootState.route.params.domain, - concept: currentInstance.concept - ? currentInstance.concept - : rootState.route.params.concept, - }) - : fetchAxiosData(file, { - cdm: currentSource, - release: currentSource.releases[0].release_id, - domain: currentInstance.domain - ? currentInstance.domain - : rootState.route.params.domain, - concept: currentInstance.concept - ? currentInstance.concept - : rootState.route.params.concept, - }), - ]; - }, - [] - ); - return [...filesArray, ...loadedFiles]; - }, - [] - ), - }), - {} - ); + const isDuckDb = + environment.DUCKDB_ENABLED === "true" && payload.duckdb_supported; - let data = {}; - for (const file in promises) { - await Promise.allSettled(promises[file]).then((responses) => { - data[file] = responses - .filter((response) => response.status === "fulfilled") - .map( - ( - filtered: PromiseFulfilledResult<{ - data: never[]; - payload: { cdm: string }; - }> - ) => ({ - data: - payload.duckdb_supported && - environment.DUCKDB_ENABLED === "true" - ? convertTableToArray(filtered.value.data) - : preprocessing[file] - ? preprocessing[file](filtered.value.data) - : filtered.value?.data, - source: filtered.value?.payload.cdm, - }) + const reportName = rootState.route.name; + const promises = payload.files.reduce((obj, file) => { + obj[file.name] = rootGetters.getSources.reduce( + (filesArray, currentSource) => { + const loadedFiles = file.instanceParams.reduce( + (array, currentInstance) => { + const path = { + cdm: currentSource, + release: currentSource.releases[0].release_id, + domain: currentInstance.domain || rootState.route.params.domain, + concept: + currentInstance.concept || rootState.route.params.concept, + }; + const fetchData = + isDuckDb && file.source !== "axios" + ? fetchDuckDBData(file, payload, path) + : fetchAxiosData(file, path); + return [...array, fetchData]; + }, + [] ); - if (data[file].length === 0 && payload.criticalError) { - data = null; - dispatch(errorActions.NEW_ERROR, { - message: "No files found across data sources", - details: "No additional data", - }); - } - }); - } - try { - if (data) { - postprocessing[rootState.route.name] - ? commit(SET_DATA, postprocessing[rootState.route.name](data)) - : commit(SET_DATA, data); + + return [...filesArray, ...loadedFiles]; + }, + [] + ); + + return obj; + }, {}); + + const data = {}; + for (const file in promises) { + const responses = await Promise.allSettled(promises[file]); + + data[file] = responses + .filter((response) => response.status === "fulfilled") + .map((filtered) => ({ + data: isDuckDb + ? convertTableToArray(filtered.value.data) + : preprocessing[file] + ? preprocessing[file](filtered.value.data) + : filtered.value?.data, + source: filtered.value?.payload.cdm, + })); + + //handle network error + if (data[file].length === 0) { + handleNetworkError(responses, { dispatch }, reportName, isDuckDb); } - } catch (e) { - dispatch(errorActions.NEW_ERROR, { - message: `An unexpected error has occurred (${e.name})`, - details: e.message, - stack: e.stack, - type: "unexpected", - }); + } + if (data) { + commitData(data, { dispatch, commit }, reportName); } }, @@ -257,82 +272,61 @@ const actions = { { commit, dispatch, rootState, rootGetters }, payload ) { - const promises = payload.files.reduce( - (obj, file) => ({ - ...obj, - [file.name]: rootGetters.getSelectedSource.releases.map((release) => { - if ( - payload.duckdb_supported && - environment.DUCKDB_ENABLED === "true" && - file.source !== "axios" - ) { - return fetchDuckDBData(file, payload, { - cdm: rootGetters.getSelectedSource, - release: release.release_id, - domain: rootState.route.params.domain, - concept: rootState.route.params.concept, - }); - } else { - return apiService( - { - url: getFilePath({ - cdm: rootGetters.getSelectedSource.cdm_source_key, - release: release.release_id, - domain: rootState.route.params.domain, - concept: rootState.route.params.concept, - })[file.name], - method: "get", - }, - release.release_name - ); - } - }), - }), - {} - ); - let data = {}; - for (const file in promises) { - await Promise.allSettled(promises[file]).then((responses) => { - data[file] = responses - .filter((response) => response.status === "fulfilled") - .map( - ( - filtered: PromiseFulfilledResult<{ - data: never; - payload: SourceRelease; - }> - ) => ({ - data: - payload.duckdb_supported && - environment.DUCKDB_ENABLED === "true" - ? convertTableToArray(filtered.value?.data) - : filtered.value.data, - release: filtered.value?.payload, - }) - ); - if (data[file].length === 0) { - data = null; - dispatch(errorActions.NEW_ERROR, { - message: "No files found across current data source releases", - details: rootGetters.getSelectedSource.cdm_source_abbreviation, + const isDuckDb = + payload.duckdb_supported && environment.DUCKDB_ENABLED === "true"; + const reportName = rootState.route.name; + const promises = payload.files.reduce((obj, file) => { + const selectedSource = rootGetters.getSelectedSource; + const params = { + cdm: selectedSource.cdm_source_key, + domain: rootState.route.params.domain, + concept: rootState.route.params.concept, + }; + + obj[file.name] = selectedSource.releases.map((release) => { + if (isDuckDb && file.source !== "axios") { + return fetchDuckDBData(file, payload, { + ...params, + release: release.release_id, }); + } else { + const url = getFilePath({ + ...params, + release: release.release_id, + })[file.name]; + + return apiService( + { + url, + method: "get", + }, + release.release_name + ); } }); - } - try { - if (data) { - postprocessing[rootState.route.name] - ? commit(SET_DATA, postprocessing[rootState.route.name](data)) - : commit(SET_DATA, data); + + return obj; + }, {}); + + let data = {}; + for (const file in promises) { + const responses = await Promise.allSettled(promises[file]); + data[file] = responses + .filter((response) => response.status === "fulfilled") + .map((filtered) => { + const { data, payload } = filtered.value; + return { + data: isDuckDb ? convertTableToArray(data) : data, + release: payload, + }; + }); + if (data[file].length === 0) { + handleNetworkError(responses[0], { dispatch }, reportName, isDuckDb); + data = null; } - } catch (e) { - dispatch(errorActions.NEW_ERROR, { - message: `An unexpected error has occurred (${e.name})`, - details: e.message, - stack: e.stack, - type: "unexpected", - page: rootState.route.name, - }); + } + if (data) { + commitData(data, { dispatch, commit }, reportName); } }, }; diff --git a/src/widgets/error/model/config/errorMessages.ts b/src/widgets/error/model/config/errorMessages.ts new file mode 100644 index 00000000..39e7c169 --- /dev/null +++ b/src/widgets/error/model/config/errorMessages.ts @@ -0,0 +1,40 @@ +import * as files from "@/shared/config/files"; + +export default { + reportsMissingFiles: { + temporalCharacterization: + "Temporal characterization file is not found. Please make sure it exists within the current release folder then try again", + feasibility: "", + networkPerformance: + "Network performance file is not found. Please make sure it exists within the data directory then try again", + networkDiversityReport: "1", + networkUnmappedSourceCodes: "", + networkConcept: "", + networkDataQuality: "", + cohorts: "", + cohortReport: "", + dataStrandReport: "", + population: "No files found across all data sources", + concept: "Seems like the data file for that particular concept is missing.", + dataQuality: "", + death: "", + dataDensity: "", + domainTable: "", + metadata: "", + observationPeriod: "", + performance: "", + person: "Person files not found", + unmappedSourceCodes: "", + dataQualityHistory: "", + domainContinuity: "", + sourceConceptOverlay: "", + index: "Index file is missing. Please run AresIndexer then try again", + }, + technicalError: { + codeError: + "Something went wrong. Please submit an issue on Github and describe the steps to reproduce it", + networkError: + "It seems that the server is not available at this time. Please make sure you have internet connection" + + " and the server is running then reload the page", + }, +}; diff --git a/src/widgets/error/ui/Error.vue b/src/widgets/error/ui/Error.vue index 5661b369..1a29a0a8 100644 --- a/src/widgets/error/ui/Error.vue +++ b/src/widgets/error/ui/Error.vue @@ -12,26 +12,20 @@

Something went wrong

- {{ - store.getters.getErrors[0].message - }} - -- - { {{ store.getters.getErrors[0].details }} } -
- -
-

Additional information:

-

{{ row }}

+ {{ errors[0].userMessage }}
-
+
+ + +

{{ row }}

+
+
+
+ +