This commit is contained in:
Mateusz Gruszczyński
2026-04-15 09:22:15 +02:00
parent b02a2f3a8e
commit 14f83cd549
10 changed files with 235 additions and 63 deletions

View File

@@ -1,3 +1,4 @@
import { HttpErrorResponse } from '@angular/common/http';
import { Injectable, inject } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { ConfirmationService, MessageService } from 'primeng/api';
@@ -45,6 +46,17 @@ export class UiService {
this.messageService.clear();
}
apiError(error: unknown, fallbackDetailKey: string) {
const detail = this.extractErrorMessage(error) || this.t(fallbackDetailKey);
this.messageService.add({
severity: 'error',
summary: this.t('toast.error'),
detail
});
}
confirm(options: ConfirmOptions): Promise<boolean> {
return new Promise((resolve) => {
let resolved = false;
@@ -75,7 +87,42 @@ export class UiService {
return this.t(key, params);
}
private extractErrorMessage(error: unknown): string | null {
if (!error) {
return null;
}
if (error instanceof HttpErrorResponse) {
if ((error as { name?: string }).name === 'TimeoutError') {
return this.t('toast.requestTimeout');
}
const payload = error.error;
if (typeof payload === 'string' && payload.trim()) {
return payload.trim();
}
if (payload && typeof payload === 'object') {
const detail = (payload as { detail?: unknown }).detail;
if (typeof detail === 'string' && detail.trim()) {
return detail.trim();
}
}
if (typeof error.message === 'string' && error.message.trim()) {
return error.message.trim();
}
return null;
}
const maybeTimeout = error as { name?: string; message?: string };
if (maybeTimeout.name === 'TimeoutError') {
return this.t('toast.requestTimeout');
}
if (typeof maybeTimeout.message === 'string' && maybeTimeout.message.trim()) {
return maybeTimeout.message.trim();
}
return null;
}
private t(key: string, params?: Record<string, unknown>): string {
return this.translate.instant(key, params);
}
}

View File

@@ -1,4 +1,5 @@
import { CommonModule } from '@angular/common';
import { finalize, timeout } from 'rxjs';
import { HttpResponse } from '@angular/common/http';
import { Component, OnInit, inject } from '@angular/core';
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
@@ -242,17 +243,25 @@ export class RouterDetailPageComponent implements OnInit {
if (payload.device_type === 'switchos') {
payload.ssh_key = '';
}
this.api.http.put<DeviceItem>(`${this.api.baseUrl}/routers/${this.routerId}`, payload).subscribe({
next: (routerItem) => {
this.routerItem = routerItem;
this.connection = this.mapStoredConnection(routerItem);
this.editVisible = false;
this.ui.success('toast.routerUpdated');
},
complete: () => {
this.saving = false;
}
});
this.api.http
.put<DeviceItem>(`${this.api.baseUrl}/routers/${this.routerId}`, payload)
.pipe(
timeout(15000),
finalize(() => {
this.saving = false;
})
)
.subscribe({
next: (routerItem) => {
this.routerItem = routerItem;
this.connection = this.mapStoredConnection(routerItem);
this.editVisible = false;
this.ui.success('toast.routerUpdated');
},
error: (error) => {
this.ui.apiError(error, 'toast.routerSaveFailed');
}
});
}
saveSettings() {
@@ -268,17 +277,25 @@ export class RouterDetailPageComponent implements OnInit {
payload.disable_export_backups = true;
payload.disable_binary_backups = true;
}
this.api.http.put<DeviceItem>(`${this.api.baseUrl}/routers/${this.routerId}`, payload).subscribe({
next: (routerItem) => {
this.routerItem = routerItem;
this.connection = this.mapStoredConnection(routerItem);
this.patchSettingsForm(routerItem);
this.ui.success('toast.routerUpdated');
},
complete: () => {
this.savingSettings = false;
}
});
this.api.http
.put<DeviceItem>(`${this.api.baseUrl}/routers/${this.routerId}`, payload)
.pipe(
timeout(15000),
finalize(() => {
this.savingSettings = false;
})
)
.subscribe({
next: (routerItem) => {
this.routerItem = routerItem;
this.connection = this.mapStoredConnection(routerItem);
this.patchSettingsForm(routerItem);
this.ui.success('toast.routerUpdated');
},
error: (error) => {
this.ui.apiError(error, 'toast.routerSaveFailed');
}
});
}
private patchSettingsForm(item: DeviceItem) {
@@ -327,20 +344,28 @@ export class RouterDetailPageComponent implements OnInit {
return;
}
this.testing = true;
this.api.http.get<ConnectionSnapshot>(`${this.api.baseUrl}/routers/${this.routerId}/test-connection`).subscribe({
next: (result) => {
this.connection = result;
this.syncStoredConnection(result);
if (result.success) {
this.ui.success('toast.connectionSuccessful');
} else {
this.ui.error('toast.connectionFailed');
this.api.http
.get<ConnectionSnapshot>(`${this.api.baseUrl}/routers/${this.routerId}/test-connection`)
.pipe(
timeout(15000),
finalize(() => {
this.testing = false;
})
)
.subscribe({
next: (result) => {
this.connection = result;
this.syncStoredConnection(result);
if (result.success) {
this.ui.success('toast.connectionSuccessful');
} else {
this.ui.apiError({ message: result.error }, 'toast.connectionFailed');
}
},
error: (error) => {
this.ui.apiError(error, 'toast.connectionFailed');
}
},
complete: () => {
this.testing = false;
}
});
});
}
compareToLatest(id: number) {

View File

@@ -1,4 +1,5 @@
import { CommonModule } from '@angular/common';
import { finalize, timeout } from 'rxjs';
import { Component, OnInit, inject } from '@angular/core';
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
import { Router } from '@angular/router';
@@ -167,16 +168,23 @@ export class RoutersPageComponent implements OnInit {
? this.api.http.put(`${this.api.baseUrl}/routers/${this.editingId}`, payload)
: this.api.http.post(`${this.api.baseUrl}/routers`, payload);
request$.subscribe({
next: () => {
this.ui.success(this.editingId ? 'toast.routerUpdated' : 'toast.routerCreated');
this.visible = false;
this.load();
},
complete: () => {
this.saving = false;
}
});
request$
.pipe(
timeout(15000),
finalize(() => {
this.saving = false;
})
)
.subscribe({
next: () => {
this.ui.success(this.editingId ? 'toast.routerUpdated' : 'toast.routerCreated');
this.visible = false;
this.load();
},
error: (error) => {
this.ui.apiError(error, 'toast.routerSaveFailed');
}
});
}
async remove(id: number) {

View File

@@ -458,6 +458,8 @@
"archivePrepared": "Archive prepared.",
"exportedRouters": "Export completed for {{count}} devices.",
"binaryCompletedRouters": "Binary backup completed for {{count}} devices.",
"routerSaveFailed": "Could not save device.",
"requestTimeout": "Request timed out. Check connection and try again.",
"routerCreated": "Router created.",
"routerUpdated": "Router updated.",
"routerDeleted": "Router deleted.",

View File

@@ -440,6 +440,8 @@
"archivePrepared": "Archivo preparado.",
"exportedRouters": "Exportación completada para {{count}} routers.",
"binaryCompletedRouters": "Copia binaria completada para {{count}} routers.",
"routerSaveFailed": "No se pudo guardar el dispositivo.",
"requestTimeout": "Se agotó el tiempo de espera. Comprueba la conexión e inténtalo de nuevo.",
"routerCreated": "Router creado.",
"routerUpdated": "Router actualizado.",
"routerDeleted": "Router eliminado.",

View File

@@ -440,6 +440,8 @@
"archivePrepared": "Arkiv klargjort.",
"exportedRouters": "Export fullført for {{count}} rutere.",
"binaryCompletedRouters": "Binær backup fullført for {{count}} rutere.",
"routerSaveFailed": "Kunne ikke lagre enheten.",
"requestTimeout": "Tidsavbrudd for forespørselen. Sjekk tilkoblingen og prøv igjen.",
"routerCreated": "Ruter opprettet.",
"routerUpdated": "Ruter oppdatert.",
"routerDeleted": "Ruter slettet.",

View File

@@ -458,6 +458,8 @@
"archivePrepared": "Archiwum zostało przygotowane.",
"exportedRouters": "Wykonano export dla {{count}} urządzeń.",
"binaryCompletedRouters": "Wykonano backup binarny dla {{count}} urządzeń.",
"routerSaveFailed": "Nie udało się zapisać urządzenia.",
"requestTimeout": "Przekroczono czas oczekiwania. Sprawdź połączenie i spróbuj ponownie.",
"routerCreated": "Urządzenie zostało dodane.",
"routerUpdated": "Urządzenie zostało zaktualizowane.",
"routerDeleted": "Urządzenie zostało usunięte.",