From 05ab4e3effa8bb7a891f61bb6d570f5078ebb04b Mon Sep 17 00:00:00 2001 From: brancan Date: Mon, 11 Aug 2025 12:33:04 -0300 Subject: [PATCH] feat(webui): improve API error handling with enhanced toast notifications - Add ShowAPIError function for better error display - Enhance ShowToast with adaptive duration and HTML support - Add protection against undefined array forEach errors - Improve CSS styling for error messages - Support Pydantic validation errors with structured display This improves user experience by showing clear, structured error messages instead of basic console errors, making debugging easier for users. --- webui/assets/css/customize.css | 34 +++++++++ webui/assets/js/site.js | 135 ++++++++++++++++++++++++++------- 2 files changed, 140 insertions(+), 29 deletions(-) diff --git a/webui/assets/css/customize.css b/webui/assets/css/customize.css index 21765c17..a45e6b68 100644 --- a/webui/assets/css/customize.css +++ b/webui/assets/css/customize.css @@ -18,6 +18,40 @@ top: 80px; right: 10px; z-index: 999; + min-width: 400px; + max-width: 600px; +} + +/* Estilos para mensajes de error en el toast */ +.toast .toast-body { + font-size: 0.9rem; + line-height: 1.4; +} + +.toast .toast-body strong { + color: inherit; +} + +.toast .toast-body code { + background-color: rgba(255, 255, 255, 0.2); + padding: 2px 4px; + border-radius: 3px; + font-family: 'Courier New', monospace; +} + +.toast .toast-body small { + opacity: 0.8; + font-size: 0.85rem; +} + +/* Toast de advertencia con mejor legibilidad */ +.toast.bg-warning .toast-body { + color: #000 !important; +} + +.toast.bg-warning .toast-body code { + background-color: rgba(0, 0, 0, 0.1); + color: #000; } .offcanvas.offcanvas-start { diff --git a/webui/assets/js/site.js b/webui/assets/js/site.js index acfadbb8..7fe2fe88 100644 --- a/webui/assets/js/site.js +++ b/webui/assets/js/site.js @@ -324,9 +324,11 @@ function GetPresentNode(){ success: function (data) { ShowProgress(); let CandidateHtml = EMPTYSTR; - data.candidates.forEach((element) => { - CandidateHtml = `${CandidateHtml}${element}`; - }); + if (data.candidates && Array.isArray(data.candidates)) { + data.candidates.forEach((element) => { + CandidateHtml = `${CandidateHtml}${element}`; + }); + } document.getElementById('node-info').innerHTML = `
  • Software Version ${data.swversion} @@ -339,9 +341,8 @@ function GetPresentNode(){
    ${CandidateHtml}
  • `; }, error: function (jqXHR, textStatus, errorThrown) { - console.log(jqXHR); document.getElementById('node-info').innerHTML = EMPTYSTR; - ShowToast(jqXHR.responseJSON.error); + ShowAPIError(jqXHR, textStatus, errorThrown); } }); @@ -351,9 +352,11 @@ function GetPresentNode(){ success: function (data) { ShowProgress(); let MembersHtml = EMPTYSTR; - data.members.forEach((element) => { - MembersHtml = `${MembersHtml}${element}`; - }); + if (data.members && Array.isArray(data.members)) { + data.members.forEach((element) => { + MembersHtml = `${MembersHtml}${element}`; + }); + } document.getElementById('cluster-info').innerHTML = `
  • Cluster Name ${data.name} @@ -378,9 +381,8 @@ function GetPresentNode(){ `; }, error: function (jqXHR, textStatus, errorThrown) { - console.log(jqXHR); document.getElementById('cluster-info').innerHTML = EMPTYSTR; - ShowToast(jqXHR.responseJSON.error); + ShowAPIError(jqXHR, textStatus, errorThrown); } }); } @@ -404,9 +406,8 @@ function GeneralGetPresent(SettingName){ } }, error: function (jqXHR, textStatus, errorThrown) { - console.log(jqXHR); document.getElementById(presentation).innerHTML = EMPTYSTR; - ShowToast(jqXHR.responseJSON.error); + ShowAPIError(jqXHR, textStatus, errorThrown); } }); } @@ -459,7 +460,14 @@ function GeneralRemove(name, SettingName){ }, error: function (jqXHR, textStatus, errorThrown) { console.log(jqXHR); - ShowToast(jqXHR.responseJSON.error); + + // Mostrar el modal de errores si hay respuesta JSON + if (jqXHR.responseJSON) { + ShowErrorModal(jqXHR.responseJSON); + } else { + // Fallback al toast si no hay respuesta JSON + ShowToast('Error de conexión: ' + textStatus, 'danger'); + } } }); } @@ -485,8 +493,7 @@ function GeneralModify(name, SettingName){ } }, error: function (jqXHR, textStatus, errorThrown) { - console.log(jqXHR); - ShowToast(jqXHR.responseJSON.error); + ShowAPIError(jqXHR, textStatus, errorThrown); } }); } @@ -530,8 +537,7 @@ function GeneralSubmit(name, SettingName, method="POST"){ offcanvaspanel.hide(); }, error: function (jqXHR, textStatus, errorThrown) { - console.log(jqXHR); - ShowToast(jqXHR.responseJSON.error); + ShowAPIError(jqXHR, textStatus, errorThrown); } }); } @@ -618,9 +624,8 @@ function AccessDomainPolicyDetail(Adomain){ `; }, error: function (jqXHR, textStatus, errorThrown) { - console.log(jqXHR); document.getElementById(`DetailAD${Adomain}`).innerHTML = EMPTYSTR; - ShowToast(jqXHR.responseJSON.error); + ShowAPIError(jqXHR, textStatus, errorThrown); } }); } @@ -669,9 +674,8 @@ function AccessUserDirectoryDetail(Adomain){ } }, error: function (jqXHR, textStatus, errorThrown) { - console.log(jqXHR); document.getElementById(`TableAD${Adomain}`).innerHTML = EMPTYSTR; - ShowToast(jqXHR.responseJSON.error); + ShowAPIError(jqXHR, textStatus, errorThrown); } }); } @@ -689,8 +693,7 @@ function RemoveAccessUser(domain, user){ AccessUserDirectoryDetail(domain); }, error: function (jqXHR, textStatus, errorThrown) { - console.log(jqXHR); - ShowToast(jqXHR.responseJSON.error); + ShowAPIError(jqXHR, textStatus, errorThrown); } }); } @@ -714,8 +717,7 @@ function UpdateAccessUser(domain, user){ PresentCanvas(userdata, domain, SettingName, 'PATCH'); }, error: function (jqXHR, textStatus, errorThrown) { - console.log(jqXHR); - ShowToast(jqXHR.responseJSON.error); + ShowAPIError(jqXHR, textStatus, errorThrown); } }); } @@ -829,9 +831,8 @@ function RoutingTableDetail(Rtablename){ }; }, error: function (jqXHR, textStatus, errorThrown) { - console.log(jqXHR); document.getElementById("DetailRT"+Rtablename).innerHTML = EMPTYSTR; - ShowToast(jqXHR.responseJSON.error); + ShowAPIError(jqXHR, textStatus, errorThrown); } }); } @@ -848,8 +849,7 @@ function RemoveRoutingRecord(tablename, match, value){ RoutingTableDetail(tablename); }, error: function (jqXHR, textStatus, errorThrown) { - console.log(jqXHR); - ShowToast(jqXHR.responseJSON.error); + ShowAPIError(jqXHR, textStatus, errorThrown); } }); } @@ -940,6 +940,83 @@ function ShowToast(message, msgtype='danger'){ ToastMsgEMLS.classList.remove('bg-warning'); ToastMsgEMLS.classList.remove('bg-success'); } + + // Configurar duración del toast según el tipo de mensaje + let delay = 3500; // 3.5 segundos por defecto + if (msgtype === 'warning') { + delay = 8000; // 8 segundos para errores de validación + } else if (msgtype === 'danger') { + delay = 6000; // 6 segundos para errores críticos + } + + // Configurar el delay del toast + ToastMsgEMLS.setAttribute('data-bs-delay', delay); + document.getElementById('event-message').innerHTML = message; $('.toast').toast('show'); } + +// Función mejorada para mostrar errores de la API +function ShowAPIError(jqXHR, textStatus, errorThrown) { + console.log(jqXHR); + + let errorMessage = ''; + let msgType = 'danger'; + + if (jqXHR.responseJSON) { + const error = jqXHR.responseJSON; + + if (error.detail && Array.isArray(error.detail)) { + // Error de validación de Pydantic + msgType = 'warning'; + errorMessage = '🚨 Errores de Validación:
    '; + + error.detail.forEach((err, index) => { + let fieldName = err.loc && err.loc.length > 1 ? err.loc[1] : 'Campo'; + let errorMsg = err.msg || 'Error de validación'; + let inputValue = err.input ? ` (Valor: ${err.input})` : ''; + let expectedValues = err.ctx && err.ctx.expected ? `
    💡 Valores permitidos: ${err.ctx.expected}` : ''; + + errorMessage += `• ${fieldName}: ${errorMsg}${inputValue}${expectedValues}
    `; + }); + + // Agregar sugerencia de ayuda + errorMessage += '
    💡 Revisa los valores ingresados y asegúrate de que cumplan con el formato requerido.'; + } else if (error.error) { + errorMessage = `❌ Error: ${error.error}`; + } else if (error.message) { + errorMessage = `❌ Error: ${error.message}`; + } else { + errorMessage = '❌ Ha ocurrido un error inesperado'; + } + + // Agregar código de estado si está disponible + if (jqXHR.status) { + let statusText = ''; + switch(jqXHR.status) { + case 400: statusText = 'Solicitud Incorrecta'; break; + case 401: statusText = 'No Autorizado'; break; + case 403: statusText = 'Prohibido'; break; + case 404: statusText = 'No Encontrado'; break; + case 422: statusText = 'Entidad No Procesable'; break; + case 500: statusText = 'Error del Servidor'; break; + default: statusText = 'Error'; + } + errorMessage += `
    📊 Código: ${jqXHR.status} (${statusText})`; + } + } else { + // Error de conexión + msgType = 'danger'; + errorMessage = `🌐 Error de conexión: ${textStatus}`; + + if (textStatus === 'timeout') { + errorMessage = '⏰ Tiempo de espera agotado. Verifica tu conexión a internet.'; + } else if (textStatus === 'error') { + errorMessage = '🔌 Error de conexión. Verifica que el servidor esté disponible.'; + } + } + + ShowToast(errorMessage, msgType); +} + +