diff --git a/src/frontend/src/modules/Settings/Configuration/ImportSettings.vue b/src/frontend/src/modules/Settings/Configuration/ImportSettings.vue index 06b61117e..d69d9c4b0 100644 --- a/src/frontend/src/modules/Settings/Configuration/ImportSettings.vue +++ b/src/frontend/src/modules/Settings/Configuration/ImportSettings.vue @@ -118,24 +118,77 @@
- - {{ r.success ? "check_circle" : "error" }} - -
- {{ entityLabel(r.type) }} - - - - +
+ + {{ r.success ? "check_circle" : "error" }} + +
+ {{ entityLabel(r.type) }} + + + + +
+ + + {{ + expandedErrors[idx] + ? "keyboard_arrow_up" + : "keyboard_arrow_down" + }} + + +
+
+
+
+ {{ item.name }} + + {{ msg }} + +
+
+
+
+ {{ field }} + {{ msg }} +
+
@@ -251,6 +304,7 @@ export default { pollingTimeout: null, pendingResults: [], pollingEntityType: null, + expandedErrors: {}, } }, computed: { @@ -296,6 +350,16 @@ export default { const entity = IMPORT_ENTITIES.find((e) => e.key === type) return entity ? entity.label : type }, + hasErrorDetails(result) { + return ( + (result.failedItems && result.failedItems.length > 0) || + (result.validationErrors && + Object.keys(result.validationErrors).length > 0) + ) + }, + toggleErrorDetails(idx) { + this.$set(this.expandedErrors, idx, !this.expandedErrors[idx]) + }, async importAll() { if (!this.hasSelectedFiles) { this.alertNotify("warn", "Please select at least one file to import") @@ -344,25 +408,36 @@ export default { importResult.imported_count === 0 && importResult.failed_count > 0 ) - results.push({ + const resultEntry = { type: entity.key, success: importSuccess, data: importResult, error: importSuccess ? null : importResult.message || `Failed to import ${entity.label}`, - }) + } + if (importResult.failed && importResult.failed.length > 0) { + resultEntry.failedItems = importResult.failed + } + if (importResult.errors) { + resultEntry.validationErrors = importResult.errors + } + results.push(resultEntry) this.clearFile(entity.key) } catch (error) { const errorMessage = error.exception?.message || error.message || `Failed to import ${entity.label}` - results.push({ + const resultEntry = { type: entity.key, success: false, error: errorMessage, - }) + } + if (error.errors) { + resultEntry.validationErrors = error.errors + } + results.push(resultEntry) } finally { this.$set(this.loadingStates, entity.key, false) } @@ -396,11 +471,17 @@ export default { if (status.status === "completed") { this.stopPolling() const importResult = status.result || {} - this.onAsyncImportDone(importResult.success !== false, null, { - added_count: importResult.added_count, - modified_count: importResult.modified_count, - failed_count: importResult.failed_count, - }) + this.onAsyncImportDone( + importResult.success !== false, + null, + { + added_count: importResult.added_count, + modified_count: importResult.modified_count, + failed_count: importResult.failed_count, + }, + importResult.failed, + importResult.errors, + ) } else if (status.status === "failed") { this.stopPolling() const failedResult = status.result || {} @@ -415,6 +496,8 @@ export default { modified_count: failedResult.modified_count ?? 0, failed_count: failedResult.failed_count ?? 0, }, + failedResult.failed, + failedResult.errors, ) } } catch (e) { @@ -435,22 +518,25 @@ export default { this.pollingJobId = null this.importing = false }, - onAsyncImportDone(success, error, data) { + onAsyncImportDone(success, error, data, failedItems, validationErrors) { const results = [...this.pendingResults] this.pendingResults = [] const entityType = this.pollingEntityType this.pollingEntityType = null - if (success) { - results.push({ type: entityType, success: true, data }) - } else { - results.push({ - type: entityType, - success: false, - error: error || "Import failed", - data, - }) + const resultEntry = { + type: entityType, + success, + data, + error: success ? null : error || "Import failed", + } + if (failedItems && failedItems.length > 0) { + resultEntry.failedItems = failedItems + } + if (validationErrors) { + resultEntry.validationErrors = validationErrors } + results.push(resultEntry) this.showResults(results) }, @@ -482,6 +568,7 @@ export default { this.alertNotify("error", `${totalFailed} record(s) failed to import`) } + this.expandedErrors = {} this.importResult = { results, added_count: totalAdded, @@ -641,7 +728,6 @@ export default { .result-details { display: flex; flex-direction: column; - gap: 0.5rem; } .result-row { @@ -684,4 +770,55 @@ export default { font-size: 0.85rem; color: #666; } + +.result-row-wrapper { + margin-bottom: 0.5rem; +} + +.error-toggle-btn { + margin-left: auto; + flex-shrink: 0; +} + +.error-details-panel { + margin-top: 0.25rem; + margin-left: 2.5rem; + max-height: 200px; + overflow-y: auto; + border: 1px solid #e0e0e0; + border-radius: 4px; + background-color: #fafafa; +} + +.error-list { + padding: 0.5rem; +} + +.error-item { + display: flex; + flex-direction: column; + padding: 0.375rem 0.5rem; + border-bottom: 1px solid #eee; +} + +.error-item:last-child { + border-bottom: none; +} + +.error-item-name { + font-weight: 500; + font-size: 0.8rem; + color: #333; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.error-item-msg { + font-size: 0.78rem; + color: #c62828; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} diff --git a/src/frontend/src/services/ApplianceImportService.js b/src/frontend/src/services/ApplianceImportService.js index caaafdf9e..1aaa578e5 100644 --- a/src/frontend/src/services/ApplianceImportService.js +++ b/src/frontend/src/services/ApplianceImportService.js @@ -21,11 +21,19 @@ export class ApplianceImportService { } return responseData.data } catch (e) { - if (e.exception) { + if (e.message && e.type) { throw e } const errorMessage = e.response?.data?.message || e.message - return new ErrorHandler(errorMessage, "http", e.response?.status) + const error = { + message: errorMessage, + type: "http", + status_code: e.response?.status, + } + if (e.response?.data?.errors) { + error.errors = e.response.data.errors + } + throw error } } } diff --git a/src/frontend/src/services/ClusterImportService.js b/src/frontend/src/services/ClusterImportService.js index dee8315d1..a6023d010 100644 --- a/src/frontend/src/services/ClusterImportService.js +++ b/src/frontend/src/services/ClusterImportService.js @@ -21,11 +21,19 @@ export class ClusterImportService { } return responseData.data } catch (e) { - if (e.exception) { + if (e.message && e.type) { throw e } const errorMessage = e.response?.data?.message || e.message - return new ErrorHandler(errorMessage, "http", e.response?.status) + const error = { + message: errorMessage, + type: "http", + status_code: e.response?.status, + } + if (e.response?.data?.errors) { + error.errors = e.response.data.errors + } + throw error } } } diff --git a/src/frontend/src/services/CustomerImportService.js b/src/frontend/src/services/CustomerImportService.js index cf554ee46..0bf030742 100644 --- a/src/frontend/src/services/CustomerImportService.js +++ b/src/frontend/src/services/CustomerImportService.js @@ -21,11 +21,19 @@ export class CustomerImportService { } return responseData.data } catch (e) { - if (e.exception) { + if (e.message && e.type) { throw e } const errorMessage = e.response?.data?.message || e.message - return new ErrorHandler(errorMessage, "http", e.response?.status) + const error = { + message: errorMessage, + type: "http", + status_code: e.response?.status, + } + if (e.response?.data?.errors) { + error.errors = e.response.data.errors + } + throw error } } } diff --git a/src/frontend/src/services/DeviceImportService.js b/src/frontend/src/services/DeviceImportService.js index 8cc2efa9b..9a6c9cdff 100644 --- a/src/frontend/src/services/DeviceImportService.js +++ b/src/frontend/src/services/DeviceImportService.js @@ -21,11 +21,19 @@ export class DeviceImportService { } return responseData.data } catch (e) { - if (e.exception) { + if (e.message && e.type) { throw e } const errorMessage = e.response?.data?.message || e.message - return new ErrorHandler(errorMessage, "http", e.response?.status) + const error = { + message: errorMessage, + type: "http", + status_code: e.response?.status, + } + if (e.response?.data?.errors) { + error.errors = e.response.data.errors + } + throw error } } } diff --git a/src/frontend/src/services/SettingsImportService.js b/src/frontend/src/services/SettingsImportService.js index ac98be9cd..b9998e38c 100644 --- a/src/frontend/src/services/SettingsImportService.js +++ b/src/frontend/src/services/SettingsImportService.js @@ -18,11 +18,19 @@ export class SettingsImportService { } return responseData.data } catch (e) { - if (e.exception) { + if (e.message && e.type) { throw e } const errorMessage = e.response?.data?.message || e.message - return new ErrorHandler(errorMessage, "http", e.response?.status) + const error = { + message: errorMessage, + type: "http", + status_code: e.response?.status, + } + if (e.response?.data?.errors) { + error.errors = e.response.data.errors + } + throw error } } } diff --git a/src/frontend/src/services/TransactionImportService.js b/src/frontend/src/services/TransactionImportService.js index d228623f7..0981e9c79 100644 --- a/src/frontend/src/services/TransactionImportService.js +++ b/src/frontend/src/services/TransactionImportService.js @@ -21,11 +21,19 @@ export class TransactionImportService { } return responseData.data } catch (e) { - if (e.exception) { + if (e.message && e.type) { throw e } const errorMessage = e.response?.data?.message || e.message - return new ErrorHandler(errorMessage, "http", e.response?.status) + const error = { + message: errorMessage, + type: "http", + status_code: e.response?.status, + } + if (e.response?.data?.errors) { + error.errors = e.response.data.errors + } + throw error } } } diff --git a/src/frontend/src/services/UserPermissionImportService.js b/src/frontend/src/services/UserPermissionImportService.js index b48d63d0b..6f9b812ef 100644 --- a/src/frontend/src/services/UserPermissionImportService.js +++ b/src/frontend/src/services/UserPermissionImportService.js @@ -21,11 +21,19 @@ export class UserPermissionImportService { } return responseData.data } catch (e) { - if (e.exception) { + if (e.message && e.type) { throw e } const errorMessage = e.response?.data?.message || e.message - return new ErrorHandler(errorMessage, "http", e.response?.status) + const error = { + message: errorMessage, + type: "http", + status_code: e.response?.status, + } + if (e.response?.data?.errors) { + error.errors = e.response.data.errors + } + throw error } } }