new features
This commit is contained in:
@@ -4,10 +4,11 @@
|
||||
[subtitle]="subtitle"
|
||||
>
|
||||
<div header-actions class="header-actions-row">
|
||||
<button *ngIf="!isSwitchos" pButton type="button" icon="pi pi-upload" [label]="'routers.exportOne' | translate" [loading]="exporting" (click)="runExport()"></button>
|
||||
<button pButton type="button" severity="secondary" icon="pi pi-database" [label]="(isSwitchos ? 'routers.downloadSwitchBackup' : 'routers.binaryOne') | translate" [loading]="runningBinary" (click)="runBinary()"></button>
|
||||
<button pButton type="button" severity="secondary" icon="pi pi-pencil" [label]="'common.edit' | translate" (click)="openEdit()"></button>
|
||||
<button *ngIf="!isSwitchos" pButton type="button" icon="pi pi-upload" [label]="'routers.exportOne' | translate" [loading]="exporting" [disabled]="routerItem?.disable_all_backups || routerItem?.disable_export_backups" (click)="runExport()"></button>
|
||||
<button pButton type="button" severity="secondary" icon="pi pi-database" [label]="(isSwitchos ? 'routers.downloadSwitchBackup' : 'routers.binaryOne') | translate" [loading]="runningBinary" [disabled]="routerItem?.disable_all_backups || routerItem?.disable_binary_backups" (click)="runBinary()"></button>
|
||||
<button pButton type="button" severity="info" icon="pi pi-wifi" [label]="'routers.testConnection' | translate" [loading]="testing" (click)="testConnection()"></button>
|
||||
<button pButton type="button" severity="danger" icon="pi pi-trash" [label]="'routers.deleteRouter' | translate" [loading]="deletingRouter" (click)="deleteRouter()"></button>
|
||||
<button pButton type="button" severity="danger" icon="pi pi-trash" [label]="'routers.deleteDevice' | translate" [loading]="deletingRouter" (click)="deleteRouter()"></button>
|
||||
</div>
|
||||
</app-page-header>
|
||||
|
||||
@@ -46,43 +47,93 @@
|
||||
</ng-template>
|
||||
</app-section-card>
|
||||
|
||||
<div class="router-detail-inspection-stack" *ngIf="!isSwitchos">
|
||||
<app-section-card [title]="'routers.previewTitle' | translate" [subtitle]="'routers.previewSubtitle' | translate">
|
||||
<div class="router-modal-summary" *ngIf="hasPreview; else noPreview">
|
||||
<div>
|
||||
<strong>{{ previewTitle }}</strong>
|
||||
<small>{{ 'routers.previewModalHint' | translate }}</small>
|
||||
</div>
|
||||
<div class="dialog-actions">
|
||||
<button pButton type="button" severity="info" icon="pi pi-eye" [label]="'routers.openPreviewModal' | translate" (click)="openPreviewModal()"></button>
|
||||
</div>
|
||||
<app-section-card [title]="'routers.backupSettingsTitle' | translate" [subtitle]="'routers.backupSettingsHint' | translate">
|
||||
<form [formGroup]="settingsForm" class="device-settings-form" (ngSubmit)="saveSettings()">
|
||||
<div class="device-settings-stack">
|
||||
<label class="device-toggle device-toggle--primary" [class.is-active]="settingsForm.controls.disable_all_backups.value">
|
||||
<input type="checkbox" formControlName="disable_all_backups" />
|
||||
<span class="device-toggle__switch" aria-hidden="true"></span>
|
||||
<span class="device-toggle__icon"><i class="pi pi-ban"></i></span>
|
||||
<span class="device-toggle__content">
|
||||
<strong>{{ 'routers.disableAllBackups' | translate }}</strong>
|
||||
<small>{{ 'routers.disableAllBackupsHint' | translate }}</small>
|
||||
</span>
|
||||
<span class="device-toggle__state">{{ (settingsForm.controls.disable_all_backups.value ? 'common.enabled' : 'common.disabled') | translate }}</span>
|
||||
</label>
|
||||
<label class="device-toggle" *ngIf="!isSwitchos" [class.is-active]="settingsForm.controls.disable_export_backups.value">
|
||||
<input type="checkbox" formControlName="disable_export_backups" />
|
||||
<span class="device-toggle__switch" aria-hidden="true"></span>
|
||||
<span class="device-toggle__icon"><i class="pi pi-file-export"></i></span>
|
||||
<span class="device-toggle__content">
|
||||
<strong>{{ 'routers.disableExports' | translate }}</strong>
|
||||
<small>{{ 'routers.disableExportsHint' | translate }}</small>
|
||||
</span>
|
||||
<span class="device-toggle__state">{{ (settingsForm.controls.disable_export_backups.value ? 'common.enabled' : 'common.disabled') | translate }}</span>
|
||||
</label>
|
||||
<label class="device-toggle" [class.is-active]="settingsForm.controls.disable_binary_backups.value">
|
||||
<input type="checkbox" formControlName="disable_binary_backups" />
|
||||
<span class="device-toggle__switch" aria-hidden="true"></span>
|
||||
<span class="device-toggle__icon"><i class="pi pi-database"></i></span>
|
||||
<span class="device-toggle__content">
|
||||
<strong>{{ 'routers.disableBinaryBackups' | translate }}</strong>
|
||||
<small>{{ 'routers.disableBinaryBackupsHint' | translate }}</small>
|
||||
</span>
|
||||
<span class="device-toggle__state">{{ (settingsForm.controls.disable_binary_backups.value ? 'common.enabled' : 'common.disabled') | translate }}</span>
|
||||
</label>
|
||||
<label class="device-toggle" [class.is-active]="settingsForm.controls.disable_ping.value">
|
||||
<input type="checkbox" formControlName="disable_ping" />
|
||||
<span class="device-toggle__switch" aria-hidden="true"></span>
|
||||
<span class="device-toggle__icon"><i class="pi pi-wifi"></i></span>
|
||||
<span class="device-toggle__content">
|
||||
<strong>{{ 'routers.disablePing' | translate }}</strong>
|
||||
<small>{{ 'routers.disablePingHint' | translate }}</small>
|
||||
</span>
|
||||
<span class="device-toggle__state">{{ (settingsForm.controls.disable_ping.value ? 'common.enabled' : 'common.disabled') | translate }}</span>
|
||||
</label>
|
||||
</div>
|
||||
<ng-template #noPreview>
|
||||
<div class="empty-state compact-empty">
|
||||
<i class="pi pi-eye"></i>
|
||||
<p>{{ 'routers.noPreview' | translate }}</p>
|
||||
</div>
|
||||
</ng-template>
|
||||
</app-section-card>
|
||||
<div class="dialog-actions device-settings-actions">
|
||||
<button pButton type="submit" [loading]="savingSettings" [disabled]="savingSettings" [label]="'common.save' | translate"></button>
|
||||
</div>
|
||||
</form>
|
||||
</app-section-card>
|
||||
</div>
|
||||
|
||||
<app-section-card [title]="'routers.diffTitle' | translate" [subtitle]="'routers.diffSubtitle' | translate">
|
||||
<div class="router-modal-summary" *ngIf="hasDiff && diffData; else noDiff">
|
||||
<div>
|
||||
<strong>{{ diffData.left_file_name }} → {{ diffData.right_file_name }}</strong>
|
||||
<small>{{ 'routers.diffModalHint' | translate }}</small>
|
||||
</div>
|
||||
<div class="dialog-actions">
|
||||
<button pButton type="button" severity="help" icon="pi pi-code" [label]="'routers.openDiffModal' | translate" (click)="openDiffModal()"></button>
|
||||
</div>
|
||||
<div class="router-detail-split-grid" *ngIf="!isSwitchos">
|
||||
<app-section-card [title]="'routers.previewTitle' | translate" [subtitle]="'routers.previewSubtitle' | translate">
|
||||
<div class="router-modal-summary" *ngIf="hasPreview; else noPreview">
|
||||
<div>
|
||||
<strong>{{ previewTitle }}</strong>
|
||||
<small>{{ 'routers.previewModalHint' | translate }}</small>
|
||||
</div>
|
||||
<ng-template #noDiff>
|
||||
<div class="empty-state compact-empty">
|
||||
<i class="pi pi-code"></i>
|
||||
<p>{{ 'routers.noDiff' | translate }}</p>
|
||||
</div>
|
||||
</ng-template>
|
||||
</app-section-card>
|
||||
</div>
|
||||
<div class="dialog-actions">
|
||||
<button pButton type="button" severity="info" icon="pi pi-eye" [label]="'routers.openPreviewModal' | translate" (click)="openPreviewModal()"></button>
|
||||
</div>
|
||||
</div>
|
||||
<ng-template #noPreview>
|
||||
<div class="empty-state compact-empty">
|
||||
<i class="pi pi-eye"></i>
|
||||
<p>{{ 'routers.noPreview' | translate }}</p>
|
||||
</div>
|
||||
</ng-template>
|
||||
</app-section-card>
|
||||
|
||||
<app-section-card [title]="'routers.diffTitle' | translate" [subtitle]="'routers.diffSubtitle' | translate">
|
||||
<div class="router-modal-summary" *ngIf="hasDiff && diffData; else noDiff">
|
||||
<div>
|
||||
<strong>{{ diffData.left_file_name }} → {{ diffData.right_file_name }}</strong>
|
||||
<small>{{ 'routers.diffModalHint' | translate }}</small>
|
||||
</div>
|
||||
<div class="dialog-actions">
|
||||
<button pButton type="button" severity="help" icon="pi pi-code" [label]="'routers.openDiffModal' | translate" (click)="openDiffModal()"></button>
|
||||
</div>
|
||||
</div>
|
||||
<ng-template #noDiff>
|
||||
<div class="empty-state compact-empty">
|
||||
<i class="pi pi-code"></i>
|
||||
<p>{{ 'routers.noDiff' | translate }}</p>
|
||||
</div>
|
||||
</ng-template>
|
||||
</app-section-card>
|
||||
</div>
|
||||
|
||||
<div class="dashboard-grid router-detail-grid router-detail-grid--stack" *ngIf="!isSwitchos">
|
||||
@@ -168,3 +219,84 @@
|
||||
<pre class="code-preview preview-dialog__content">{{ diffText }}</pre>
|
||||
</ng-template>
|
||||
</p-dialog>
|
||||
|
||||
|
||||
<p-dialog [(visible)]="editVisible" [modal]="true" [draggable]="false" [resizable]="false" [style]="{ width: 'min(760px, 96vw)' }" styleClass="router-dialog">
|
||||
<ng-template pTemplate="header">
|
||||
<div class="router-dialog-header">
|
||||
<div class="router-dialog-header__icon">
|
||||
<i class="pi" [ngClass]="selectedDeviceType === 'switchos' ? 'pi-sitemap' : 'pi-server'"></i>
|
||||
</div>
|
||||
<div class="router-dialog-header__text">
|
||||
<div class="router-dialog-header__eyebrow">
|
||||
{{ 'routers.deviceType' | translate }} · {{ selectedDeviceType === 'switchos' ? ('routers.switchos' | translate) : ('routers.routeros' | translate) }}
|
||||
</div>
|
||||
<div class="router-dialog-header__title">{{ 'routers.editDialogTitle' | translate }}</div>
|
||||
<small>
|
||||
{{
|
||||
selectedDeviceType === 'switchos'
|
||||
? ('routers.switchDialogSubtitle' | translate)
|
||||
: ('routers.routerDialogSubtitle' | translate)
|
||||
}}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<form [formGroup]="form" (ngSubmit)="saveEdit()" class="router-dialog-form">
|
||||
<section class="router-dialog-panel">
|
||||
<div class="router-dialog-panel__header">
|
||||
<div>
|
||||
<strong>{{ 'routers.connectionSectionTitle' | translate }}</strong>
|
||||
<p>{{ 'routers.connectionSectionHint' | translate }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-grid-2 router-dialog-grid">
|
||||
<span class="form-field">
|
||||
<label>{{ 'routers.name' | translate }}</label>
|
||||
<input pInputText formControlName="name" />
|
||||
</span>
|
||||
<span class="form-field">
|
||||
<label>{{ 'routers.deviceType' | translate }}</label>
|
||||
<p-select [appendTo]="'body'" [options]="deviceTypeOptions" formControlName="device_type" optionLabel="label" optionValue="value"></p-select>
|
||||
</span>
|
||||
<span class="form-field">
|
||||
<label>{{ 'routers.host' | translate }}</label>
|
||||
<input pInputText formControlName="host" />
|
||||
</span>
|
||||
<span class="form-field">
|
||||
<label>{{ 'routers.port' | translate }}</label>
|
||||
<input pInputText type="number" formControlName="port" />
|
||||
</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="router-dialog-panel">
|
||||
<div class="router-dialog-panel__header">
|
||||
<div>
|
||||
<strong>{{ 'routers.credentialsSectionTitle' | translate }}</strong>
|
||||
<p>{{ selectedDeviceType === 'switchos' ? ('routers.switchDialogSubtitle' | translate) : ('routers.routerDialogSubtitle' | translate) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-grid-2 router-dialog-grid">
|
||||
<span class="form-field">
|
||||
<label>{{ 'routers.sshUser' | translate }}</label>
|
||||
<input pInputText formControlName="ssh_user" [placeholder]="selectedDeviceType === 'switchos' ? ('routers.switchUserPlaceholder' | translate) : 'admin'" />
|
||||
</span>
|
||||
<span class="form-field">
|
||||
<label>{{ 'routers.sshPassword' | translate }}</label>
|
||||
<input pInputText type="password" formControlName="ssh_password" [placeholder]="selectedDeviceType === 'switchos' ? ('routers.switchPasswordPlaceholder' | translate) : ('routers.optionalPassword' | translate)" />
|
||||
</span>
|
||||
<span class="form-field form-field--full" *ngIf="selectedDeviceType === 'routeros'">
|
||||
<label>{{ 'routers.sshPrivateKey' | translate }}</label>
|
||||
<textarea pTextarea formControlName="ssh_key" rows="8" [placeholder]="'routers.optionalPrivateKey' | translate"></textarea>
|
||||
</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="dialog-actions router-dialog-actions">
|
||||
<button pButton type="button" severity="secondary" [label]="'common.cancel' | translate" (click)="editVisible=false"></button>
|
||||
<button pButton type="submit" [disabled]="form.invalid || saving" [loading]="saving" [label]="'routers.saveRouter' | translate"></button>
|
||||
</div>
|
||||
</form>
|
||||
</p-dialog>
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { HttpResponse } from '@angular/common/http';
|
||||
import { Component, OnInit, inject } from '@angular/core';
|
||||
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { ButtonModule } from 'primeng/button';
|
||||
import { DialogModule } from 'primeng/dialog';
|
||||
import { InputTextModule } from 'primeng/inputtext';
|
||||
import { SelectModule } from 'primeng/select';
|
||||
import { TableModule } from 'primeng/table';
|
||||
import { TagModule } from 'primeng/tag';
|
||||
import { TextareaModule } from 'primeng/textarea';
|
||||
|
||||
import { ApiService } from '../../core/services/api.service';
|
||||
import { UiService } from '../../core/services/ui.service';
|
||||
@@ -22,9 +26,16 @@ interface DeviceItem {
|
||||
host: string;
|
||||
port: number;
|
||||
device_type: DeviceType;
|
||||
ssh_user?: string | null;
|
||||
ssh_password?: string | null;
|
||||
ssh_key?: string | null;
|
||||
effective_username?: string | null;
|
||||
supports_export: boolean;
|
||||
supports_restore_upload: boolean;
|
||||
disable_all_backups?: boolean;
|
||||
disable_export_backups?: boolean;
|
||||
disable_binary_backups?: boolean;
|
||||
disable_ping?: boolean;
|
||||
last_connection_status?: boolean | null;
|
||||
last_connection_tested_at?: string | null;
|
||||
last_connection_error?: string | null;
|
||||
@@ -80,7 +91,21 @@ interface BackupDiffResponse {
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
imports: [CommonModule, TranslateModule, ButtonModule, DialogModule, TableModule, TagModule, PageHeaderComponent, SectionCardComponent, StatCardComponent],
|
||||
imports: [
|
||||
CommonModule,
|
||||
TranslateModule,
|
||||
ReactiveFormsModule,
|
||||
ButtonModule,
|
||||
DialogModule,
|
||||
TableModule,
|
||||
TagModule,
|
||||
InputTextModule,
|
||||
SelectModule,
|
||||
TextareaModule,
|
||||
PageHeaderComponent,
|
||||
SectionCardComponent,
|
||||
StatCardComponent
|
||||
],
|
||||
templateUrl: './router-detail-page.component.html'
|
||||
})
|
||||
export class RouterDetailPageComponent implements OnInit {
|
||||
@@ -88,6 +113,7 @@ export class RouterDetailPageComponent implements OnInit {
|
||||
private readonly api = inject(ApiService);
|
||||
private readonly router = inject(Router);
|
||||
private readonly ui = inject(UiService);
|
||||
private readonly fb = inject(FormBuilder);
|
||||
|
||||
routerId!: number;
|
||||
routerItem: DeviceItem | null = null;
|
||||
@@ -98,16 +124,42 @@ export class RouterDetailPageComponent implements OnInit {
|
||||
previewTitle = '';
|
||||
previewVisible = false;
|
||||
diffVisible = false;
|
||||
editVisible = false;
|
||||
diffData: BackupDiffResponse | null = null;
|
||||
exporting = false;
|
||||
runningBinary = false;
|
||||
testing = false;
|
||||
deletingRouter = false;
|
||||
saving = false;
|
||||
savingSettings = false;
|
||||
readonly deviceTypeOptions = [
|
||||
{ label: 'RouterOS', value: 'routeros' },
|
||||
{ label: 'SwitchOS', value: 'switchos' }
|
||||
];
|
||||
readonly form = this.fb.nonNullable.group({
|
||||
name: ['', Validators.required],
|
||||
device_type: ['routeros' as DeviceType, Validators.required],
|
||||
host: ['', Validators.required],
|
||||
port: [22, Validators.required],
|
||||
ssh_user: ['admin'],
|
||||
ssh_password: '',
|
||||
ssh_key: ''
|
||||
});
|
||||
readonly settingsForm = this.fb.nonNullable.group({
|
||||
disable_all_backups: false,
|
||||
disable_export_backups: false,
|
||||
disable_binary_backups: false,
|
||||
disable_ping: false
|
||||
});
|
||||
|
||||
get isSwitchos(): boolean {
|
||||
return this.routerItem?.device_type === 'switchos';
|
||||
}
|
||||
|
||||
get selectedDeviceType(): DeviceType {
|
||||
return this.form.controls.device_type.value;
|
||||
}
|
||||
|
||||
get exportBackups(): BackupItem[] {
|
||||
return this.backups.filter((item) => item.backup_type === 'export');
|
||||
}
|
||||
@@ -145,6 +197,14 @@ export class RouterDetailPageComponent implements OnInit {
|
||||
|
||||
ngOnInit() {
|
||||
this.routerId = Number(this.route.snapshot.paramMap.get('id'));
|
||||
this.form.controls.device_type.valueChanges.subscribe((deviceType) => {
|
||||
this.applyDeviceDefaults((deviceType || 'routeros') as DeviceType);
|
||||
});
|
||||
this.settingsForm.controls.disable_all_backups.valueChanges.subscribe((disabled) => {
|
||||
if (disabled) {
|
||||
this.settingsForm.patchValue({ disable_export_backups: true, disable_binary_backups: true }, { emitEvent: false });
|
||||
}
|
||||
});
|
||||
this.load();
|
||||
}
|
||||
|
||||
@@ -152,12 +212,86 @@ export class RouterDetailPageComponent implements OnInit {
|
||||
this.api.http.get<DeviceItem>(`${this.api.baseUrl}/routers/${this.routerId}`).subscribe((routerItem) => {
|
||||
this.routerItem = routerItem;
|
||||
this.connection = this.mapStoredConnection(routerItem);
|
||||
this.patchSettingsForm(routerItem);
|
||||
});
|
||||
this.api.http.get<BackupItem[]>(`${this.api.baseUrl}/backups/router/${this.routerId}`).subscribe((r) => (this.backups = r));
|
||||
}
|
||||
|
||||
openEdit() {
|
||||
if (!this.routerItem) {
|
||||
return;
|
||||
}
|
||||
this.form.reset({
|
||||
name: this.routerItem.name,
|
||||
device_type: this.routerItem.device_type,
|
||||
host: this.routerItem.host,
|
||||
port: this.routerItem.port,
|
||||
ssh_user: this.routerItem.ssh_user ?? '',
|
||||
ssh_password: this.routerItem.ssh_password ?? '',
|
||||
ssh_key: this.routerItem.ssh_key ?? ''
|
||||
});
|
||||
this.editVisible = true;
|
||||
}
|
||||
|
||||
saveEdit() {
|
||||
if (this.form.invalid || this.saving) {
|
||||
return;
|
||||
}
|
||||
this.saving = true;
|
||||
const payload = this.form.getRawValue();
|
||||
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;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
saveSettings() {
|
||||
if (!this.routerItem || this.savingSettings) {
|
||||
return;
|
||||
}
|
||||
this.savingSettings = true;
|
||||
const payload = this.settingsForm.getRawValue();
|
||||
if (this.routerItem.device_type === 'switchos') {
|
||||
payload.disable_export_backups = true;
|
||||
}
|
||||
if (payload.disable_all_backups) {
|
||||
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;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private patchSettingsForm(item: DeviceItem) {
|
||||
this.settingsForm.reset({
|
||||
disable_all_backups: !!item.disable_all_backups,
|
||||
disable_export_backups: !!item.disable_export_backups,
|
||||
disable_binary_backups: !!item.disable_binary_backups,
|
||||
disable_ping: !!item.disable_ping
|
||||
}, { emitEvent: false });
|
||||
}
|
||||
|
||||
runExport() {
|
||||
if (this.exporting || this.isSwitchos) {
|
||||
if (this.exporting || this.isSwitchos || this.routerItem?.disable_all_backups || this.routerItem?.disable_export_backups) {
|
||||
return;
|
||||
}
|
||||
this.exporting = true;
|
||||
@@ -173,7 +307,7 @@ export class RouterDetailPageComponent implements OnInit {
|
||||
}
|
||||
|
||||
runBinary() {
|
||||
if (this.runningBinary) {
|
||||
if (this.runningBinary || this.routerItem?.disable_all_backups || this.routerItem?.disable_binary_backups) {
|
||||
return;
|
||||
}
|
||||
this.runningBinary = true;
|
||||
@@ -332,6 +466,14 @@ export class RouterDetailPageComponent implements OnInit {
|
||||
};
|
||||
}
|
||||
|
||||
private applyDeviceDefaults(deviceType: DeviceType) {
|
||||
if (deviceType === 'switchos') {
|
||||
this.form.patchValue({ port: 80, ssh_key: '', ssh_user: this.form.controls.ssh_user.value || '' }, { emitEvent: false });
|
||||
return;
|
||||
}
|
||||
this.form.patchValue({ port: 22, ssh_user: this.form.controls.ssh_user.value || 'admin' }, { emitEvent: false });
|
||||
}
|
||||
|
||||
private openBlob(response: HttpResponse<Blob>, fallbackName: string) {
|
||||
const disposition = response.headers.get('content-disposition') || '';
|
||||
const match = disposition.match(/filename="?([^";]+)"?/i);
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
<app-section-card [title]="'routers.listTitle' | translate" [subtitle]="'routers.listSubtitle' | translate">
|
||||
<p-table [value]="routers" responsiveLayout="scroll" styleClass="app-table">
|
||||
<ng-template pTemplate="header">
|
||||
<tr><th>{{ 'routers.name' | translate }}</th><th>{{ 'routers.endpoint' | translate }}</th><th>{{ 'routers.access' | translate }}</th><th>{{ 'common.actions' | translate }}</th></tr>
|
||||
<tr><th>{{ 'routers.name' | translate }}</th><th>{{ 'routers.endpoint' | translate }}</th><th>{{ 'routers.access' | translate }}</th><th>{{ 'routers.backupPolicy' | translate }}</th><th>{{ 'routers.ping' | translate }}</th><th>{{ 'common.actions' | translate }}</th></tr>
|
||||
</ng-template>
|
||||
<ng-template pTemplate="body" let-routerItem>
|
||||
<tr>
|
||||
@@ -42,6 +42,13 @@
|
||||
<p-tag [value]="secondaryAccessTag(routerItem).value" [severity]="secondaryAccessTag(routerItem).severity"></p-tag>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="table-primary">{{ backupPolicyLabel(routerItem) }}</div>
|
||||
<small class="table-secondary" *ngIf="routerItem.disable_all_backups">{{ 'common.disabled' | translate }}</small>
|
||||
</td>
|
||||
<td>
|
||||
<small class="table-secondary">{{ pingLabel(routerItem) }}</small>
|
||||
</td>
|
||||
<td>
|
||||
<div class="table-actions table-actions--labels">
|
||||
<button pButton type="button" size="small" styleClass="table-action-btn table-action-btn--wide" icon="pi pi-arrow-right" [label]="'common.open' | translate" (click)="open(routerItem.id)"></button>
|
||||
|
||||
@@ -32,6 +32,17 @@ interface RouterItem {
|
||||
has_effective_ssh_key?: boolean;
|
||||
uses_global_switchos_credentials?: boolean;
|
||||
has_effective_password?: boolean;
|
||||
disable_all_backups?: boolean;
|
||||
disable_export_backups?: boolean;
|
||||
disable_binary_backups?: boolean;
|
||||
disable_ping?: boolean;
|
||||
}
|
||||
|
||||
interface RouterPingStatus {
|
||||
router_id: number;
|
||||
reachable: boolean;
|
||||
latency_ms?: number | null;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
@Component({
|
||||
@@ -62,6 +73,7 @@ export class RoutersPageComponent implements OnInit {
|
||||
editingId: number | null = null;
|
||||
saving = false;
|
||||
routers: RouterItem[] = [];
|
||||
pingStatuses: Record<number, RouterPingStatus> = {};
|
||||
readonly deviceTypeOptions = [
|
||||
{ label: 'RouterOS', value: 'routeros' },
|
||||
{ label: 'SwitchOS', value: 'switchos' }
|
||||
@@ -100,12 +112,31 @@ export class RoutersPageComponent implements OnInit {
|
||||
}
|
||||
|
||||
load() {
|
||||
this.api.http.get<RouterItem[]>(`${this.api.baseUrl}/routers`).subscribe((r) => (this.routers = r));
|
||||
this.api.http.get<RouterItem[]>(`${this.api.baseUrl}/routers`).subscribe((r) => {
|
||||
this.routers = r;
|
||||
this.loadPingStatuses();
|
||||
});
|
||||
}
|
||||
|
||||
loadPingStatuses() {
|
||||
this.api.http.get<{ items: RouterPingStatus[] }>(`${this.api.baseUrl}/routers/ping-statuses`).subscribe({
|
||||
next: (response) => {
|
||||
this.pingStatuses = response.items.reduce<Record<number, RouterPingStatus>>((acc, item) => {
|
||||
acc[item.router_id] = item;
|
||||
return acc;
|
||||
}, {});
|
||||
},
|
||||
error: () => {
|
||||
this.pingStatuses = {};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
openCreate() {
|
||||
this.editingId = null;
|
||||
this.form.reset({ name: '', device_type: 'routeros', host: '', port: 22, ssh_user: 'admin', ssh_password: '', ssh_key: '' });
|
||||
this.form.reset({
|
||||
name: '', device_type: 'routeros', host: '', port: 22, ssh_user: 'admin', ssh_password: '', ssh_key: ''
|
||||
});
|
||||
this.visible = true;
|
||||
}
|
||||
|
||||
@@ -174,6 +205,35 @@ export class RoutersPageComponent implements OnInit {
|
||||
return item.effective_username || item.ssh_user || '—';
|
||||
}
|
||||
|
||||
pingLabel(item: RouterItem): string {
|
||||
if (item.disable_ping) {
|
||||
return this.ui.instant('routers.pingDisabled');
|
||||
}
|
||||
const ping = this.pingStatuses[item.id];
|
||||
if (!ping) {
|
||||
return this.ui.instant('routers.pingChecking');
|
||||
}
|
||||
if (!ping.reachable) {
|
||||
return this.ui.instant('routers.noPing');
|
||||
}
|
||||
const value = typeof ping.latency_ms === 'number' ? Math.round(ping.latency_ms) : null;
|
||||
return value === null ? this.ui.instant('routers.pingAvailable') : `${this.ui.instant('routers.ping')}: ${value} ms`;
|
||||
}
|
||||
|
||||
backupPolicyLabel(item: RouterItem): string {
|
||||
if (item.disable_all_backups) {
|
||||
return this.ui.instant('routers.backupsDisabledAll');
|
||||
}
|
||||
const parts: string[] = [];
|
||||
if (!item.disable_export_backups && item.device_type === 'routeros') {
|
||||
parts.push(this.ui.instant('routers.exportOne'));
|
||||
}
|
||||
if (!item.disable_binary_backups) {
|
||||
parts.push(this.ui.instant('routers.binaryOne'));
|
||||
}
|
||||
return parts.length ? parts.join(' / ') : this.ui.instant('routers.backupsDisabledAll');
|
||||
}
|
||||
|
||||
primaryAccessTag(item: RouterItem): { value: string; severity: 'success' | 'warn' | 'secondary' | 'info' } {
|
||||
if (item.device_type === 'switchos') {
|
||||
if (item.uses_global_switchos_credentials) {
|
||||
|
||||
Reference in New Issue
Block a user