347 lines
10 KiB
TypeScript
347 lines
10 KiB
TypeScript
import { CommonModule } from '@angular/common';
|
|
import { HttpResponse } from '@angular/common/http';
|
|
import { Component, OnInit, inject } from '@angular/core';
|
|
import { ActivatedRoute, Router } from '@angular/router';
|
|
import { TranslateModule } from '@ngx-translate/core';
|
|
import { ButtonModule } from 'primeng/button';
|
|
import { DialogModule } from 'primeng/dialog';
|
|
import { TableModule } from 'primeng/table';
|
|
import { TagModule } from 'primeng/tag';
|
|
|
|
import { ApiService } from '../../core/services/api.service';
|
|
import { UiService } from '../../core/services/ui.service';
|
|
import { PageHeaderComponent } from '../../shared/ui/page-header.component';
|
|
import { SectionCardComponent } from '../../shared/ui/section-card.component';
|
|
import { StatCardComponent } from '../../shared/ui/stat-card.component';
|
|
|
|
type DeviceType = 'routeros' | 'switchos';
|
|
|
|
interface DeviceItem {
|
|
id: number;
|
|
name: string;
|
|
host: string;
|
|
port: number;
|
|
device_type: DeviceType;
|
|
effective_username?: string | null;
|
|
supports_export: boolean;
|
|
supports_restore_upload: boolean;
|
|
last_connection_status?: boolean | null;
|
|
last_connection_tested_at?: string | null;
|
|
last_connection_error?: string | null;
|
|
last_connection_hostname?: string | null;
|
|
last_connection_model?: string | null;
|
|
last_connection_version?: string | null;
|
|
last_connection_uptime?: string | null;
|
|
last_connection_transport?: string | null;
|
|
last_connection_server?: string | null;
|
|
last_connection_auth_mode?: string | null;
|
|
last_connection_http_status?: string | null;
|
|
last_connection_backup_available?: boolean | null;
|
|
}
|
|
|
|
interface BackupItem {
|
|
id: number;
|
|
file_name: string;
|
|
backup_type: 'export' | 'binary';
|
|
created_at: string;
|
|
device_type: DeviceType;
|
|
}
|
|
|
|
interface ConnectionSnapshot {
|
|
success: boolean;
|
|
tested_at: string;
|
|
hostname: string;
|
|
model: string;
|
|
version?: string | null;
|
|
uptime: string;
|
|
error?: string | null;
|
|
transport?: string | null;
|
|
server?: string | null;
|
|
auth_mode?: string | null;
|
|
http_status?: string | null;
|
|
backup_available?: boolean | null;
|
|
}
|
|
|
|
interface BackupDiffStats {
|
|
added: number;
|
|
removed: number;
|
|
modified: number;
|
|
context: number;
|
|
}
|
|
|
|
interface BackupDiffResponse {
|
|
left_backup_id: number;
|
|
right_backup_id: number;
|
|
left_file_name?: string | null;
|
|
right_file_name?: string | null;
|
|
diff_text: string;
|
|
stats?: BackupDiffStats | null;
|
|
}
|
|
|
|
@Component({
|
|
standalone: true,
|
|
imports: [CommonModule, TranslateModule, ButtonModule, DialogModule, TableModule, TagModule, PageHeaderComponent, SectionCardComponent, StatCardComponent],
|
|
templateUrl: './router-detail-page.component.html'
|
|
})
|
|
export class RouterDetailPageComponent implements OnInit {
|
|
private readonly route = inject(ActivatedRoute);
|
|
private readonly api = inject(ApiService);
|
|
private readonly router = inject(Router);
|
|
private readonly ui = inject(UiService);
|
|
|
|
routerId!: number;
|
|
routerItem: DeviceItem | null = null;
|
|
backups: BackupItem[] = [];
|
|
connection: ConnectionSnapshot | null = null;
|
|
exportContent = '';
|
|
diffText = '';
|
|
previewTitle = '';
|
|
previewVisible = false;
|
|
diffVisible = false;
|
|
diffData: BackupDiffResponse | null = null;
|
|
exporting = false;
|
|
runningBinary = false;
|
|
testing = false;
|
|
deletingRouter = false;
|
|
|
|
get isSwitchos(): boolean {
|
|
return this.routerItem?.device_type === 'switchos';
|
|
}
|
|
|
|
get exportBackups(): BackupItem[] {
|
|
return this.backups.filter((item) => item.backup_type === 'export');
|
|
}
|
|
|
|
get binaryBackups(): BackupItem[] {
|
|
return this.backups.filter((item) => item.backup_type === 'binary');
|
|
}
|
|
|
|
get connectionStateLabel(): string {
|
|
if (!this.connection) {
|
|
return this.ui.instant('common.idle');
|
|
}
|
|
return this.connection.success ? this.ui.instant('common.ok') : this.ui.instant('common.failed');
|
|
}
|
|
|
|
get hasPreview(): boolean {
|
|
return !!this.exportContent;
|
|
}
|
|
|
|
get hasDiff(): boolean {
|
|
return !!this.diffText;
|
|
}
|
|
|
|
get subtitle(): string {
|
|
if (!this.routerItem) {
|
|
return this.ui.instant('routers.detailSubtitle');
|
|
}
|
|
const suffix = this.routerItem.effective_username ? ` · ${this.routerItem.effective_username}` : '';
|
|
return `${this.routerItem.host}:${this.routerItem.port}${suffix}`;
|
|
}
|
|
|
|
get deviceTypeLabel(): string {
|
|
return this.ui.instant(this.isSwitchos ? 'routers.switchos' : 'routers.routeros');
|
|
}
|
|
|
|
ngOnInit() {
|
|
this.routerId = Number(this.route.snapshot.paramMap.get('id'));
|
|
this.load();
|
|
}
|
|
|
|
load() {
|
|
this.api.http.get<DeviceItem>(`${this.api.baseUrl}/routers/${this.routerId}`).subscribe((routerItem) => {
|
|
this.routerItem = routerItem;
|
|
this.connection = this.mapStoredConnection(routerItem);
|
|
});
|
|
this.api.http.get<BackupItem[]>(`${this.api.baseUrl}/backups/router/${this.routerId}`).subscribe((r) => (this.backups = r));
|
|
}
|
|
|
|
runExport() {
|
|
if (this.exporting || this.isSwitchos) {
|
|
return;
|
|
}
|
|
this.exporting = true;
|
|
this.api.http.post(`${this.api.baseUrl}/backups/router/${this.routerId}/export`, {}).subscribe({
|
|
next: () => {
|
|
this.ui.success('toast.exportCreated');
|
|
this.load();
|
|
},
|
|
complete: () => {
|
|
this.exporting = false;
|
|
}
|
|
});
|
|
}
|
|
|
|
runBinary() {
|
|
if (this.runningBinary) {
|
|
return;
|
|
}
|
|
this.runningBinary = true;
|
|
this.api.http.post(`${this.api.baseUrl}/backups/router/${this.routerId}/binary`, {}).subscribe({
|
|
next: () => {
|
|
this.ui.success('toast.binaryCreated');
|
|
this.load();
|
|
},
|
|
complete: () => {
|
|
this.runningBinary = false;
|
|
}
|
|
});
|
|
}
|
|
|
|
testConnection() {
|
|
if (this.testing) {
|
|
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');
|
|
}
|
|
},
|
|
complete: () => {
|
|
this.testing = false;
|
|
}
|
|
});
|
|
}
|
|
|
|
compareToLatest(id: number) {
|
|
const latest = this.exportBackups[0];
|
|
if (!latest || latest.id === id) {
|
|
return;
|
|
}
|
|
this.api.http.get<BackupDiffResponse>(`${this.api.baseUrl}/backups/${id}/diff/${latest.id}`).subscribe((response) => {
|
|
this.diffData = response;
|
|
this.diffText = response.diff_text;
|
|
this.ui.clear();
|
|
this.diffVisible = true;
|
|
});
|
|
}
|
|
|
|
async remove(id: number) {
|
|
const accepted = await this.ui.confirm({ messageKey: 'confirm.deleteBackup', acceptKey: 'common.delete' });
|
|
if (!accepted) {
|
|
return;
|
|
}
|
|
this.api.http.delete(`${this.api.baseUrl}/backups/${id}`).subscribe(() => {
|
|
this.ui.success('toast.backupDeleted');
|
|
this.load();
|
|
});
|
|
}
|
|
|
|
upload(id: number) {
|
|
if (this.isSwitchos) {
|
|
return;
|
|
}
|
|
this.api.http.post(`${this.api.baseUrl}/backups/router/${this.routerId}/upload/${id}`, {}).subscribe(() => {
|
|
this.ui.success('toast.binaryUploaded');
|
|
});
|
|
}
|
|
|
|
async deleteRouter() {
|
|
if (this.deletingRouter) {
|
|
return;
|
|
}
|
|
const accepted = await this.ui.confirm({ messageKey: 'confirm.deleteRouterWithFiles', acceptKey: 'common.delete' });
|
|
if (!accepted) {
|
|
return;
|
|
}
|
|
this.deletingRouter = true;
|
|
this.api.http.delete(`${this.api.baseUrl}/routers/${this.routerId}`).subscribe({
|
|
next: () => this.router.navigate(['/routers']),
|
|
complete: () => {
|
|
this.deletingRouter = false;
|
|
}
|
|
});
|
|
}
|
|
|
|
download(id: number) {
|
|
this.api.http
|
|
.get(`${this.api.baseUrl}/backups/${id}/download`, { observe: 'response', responseType: 'blob' })
|
|
.subscribe((response) => this.openBlob(response, `backup-${id}`));
|
|
}
|
|
|
|
viewExport(id: number) {
|
|
const backup = this.exportBackups.find((item) => item.id === id);
|
|
this.api.http.get<{ content: string }>(`${this.api.baseUrl}/backups/${id}/view`).subscribe((r) => {
|
|
this.exportContent = r.content;
|
|
this.previewTitle = backup?.file_name || this.ui.instant('routers.previewTitle');
|
|
this.ui.clear();
|
|
this.previewVisible = true;
|
|
});
|
|
}
|
|
|
|
sendEmail(id: number) {
|
|
this.api.http.post(`${this.api.baseUrl}/backups/${id}/email`, {}).subscribe(() => {
|
|
this.ui.success('toast.backupSentEmail');
|
|
});
|
|
}
|
|
|
|
openPreviewModal() {
|
|
this.ui.clear();
|
|
this.previewVisible = true;
|
|
}
|
|
|
|
openDiffModal() {
|
|
this.ui.clear();
|
|
this.diffVisible = true;
|
|
}
|
|
|
|
private mapStoredConnection(routerItem: DeviceItem): ConnectionSnapshot | null {
|
|
if (!routerItem?.last_connection_tested_at) {
|
|
return null;
|
|
}
|
|
return {
|
|
success: Boolean(routerItem.last_connection_status),
|
|
tested_at: routerItem.last_connection_tested_at,
|
|
hostname: routerItem.last_connection_hostname || routerItem.name,
|
|
model: routerItem.last_connection_model || 'Unknown',
|
|
version: routerItem.last_connection_version,
|
|
uptime: routerItem.last_connection_uptime || 'Unknown',
|
|
error: routerItem.last_connection_error || null,
|
|
transport: routerItem.last_connection_transport || null,
|
|
server: routerItem.last_connection_server || null,
|
|
auth_mode: routerItem.last_connection_auth_mode || null,
|
|
http_status: routerItem.last_connection_http_status || null,
|
|
backup_available: routerItem.last_connection_backup_available ?? null
|
|
};
|
|
}
|
|
|
|
private syncStoredConnection(result: ConnectionSnapshot) {
|
|
if (!this.routerItem) {
|
|
return;
|
|
}
|
|
this.routerItem = {
|
|
...this.routerItem,
|
|
last_connection_status: result.success,
|
|
last_connection_tested_at: result.tested_at,
|
|
last_connection_hostname: result.hostname,
|
|
last_connection_model: result.model,
|
|
last_connection_version: result.version,
|
|
last_connection_uptime: result.uptime,
|
|
last_connection_error: result.error,
|
|
last_connection_transport: result.transport,
|
|
last_connection_server: result.server,
|
|
last_connection_auth_mode: result.auth_mode,
|
|
last_connection_http_status: result.http_status,
|
|
last_connection_backup_available: result.backup_available
|
|
};
|
|
}
|
|
|
|
private openBlob(response: HttpResponse<Blob>, fallbackName: string) {
|
|
const disposition = response.headers.get('content-disposition') || '';
|
|
const match = disposition.match(/filename="?([^";]+)"?/i);
|
|
const filename = match?.[1] || fallbackName;
|
|
const url = URL.createObjectURL(response.body || new Blob());
|
|
const link = document.createElement('a');
|
|
link.href = url;
|
|
link.download = filename;
|
|
link.click();
|
|
setTimeout(() => URL.revokeObjectURL(url), 60_000);
|
|
}
|
|
}
|