fixes
This commit is contained in:
@@ -28,8 +28,6 @@ def serialize_router(router: Router, global_settings) -> RouterResponse:
|
|||||||
effective_username = router_user
|
effective_username = router_user
|
||||||
uses_global_switchos_credentials = False
|
uses_global_switchos_credentials = False
|
||||||
has_effective_password = bool(router_password)
|
has_effective_password = bool(router_password)
|
||||||
uses_global_ssh_key = False
|
|
||||||
has_effective_ssh_key = False
|
|
||||||
|
|
||||||
if router.device_type == 'switchos':
|
if router.device_type == 'switchos':
|
||||||
effective_username = router_user or default_swos_user
|
effective_username = router_user or default_swos_user
|
||||||
@@ -37,15 +35,11 @@ def serialize_router(router: Router, global_settings) -> RouterResponse:
|
|||||||
(not router_user and default_swos_user) or (not router_password and default_swos_password)
|
(not router_user and default_swos_user) or (not router_password and default_swos_password)
|
||||||
)
|
)
|
||||||
has_effective_password = bool(router_password or default_swos_password)
|
has_effective_password = bool(router_password or default_swos_password)
|
||||||
else:
|
|
||||||
uses_password_auth = bool(router_password)
|
|
||||||
uses_global_ssh_key = bool(has_global_key and not has_router_key and not uses_password_auth)
|
|
||||||
has_effective_ssh_key = bool(has_router_key or uses_global_ssh_key)
|
|
||||||
|
|
||||||
payload = RouterResponse.model_validate(router, from_attributes=True).model_dump()
|
payload = RouterResponse.model_validate(router, from_attributes=True).model_dump()
|
||||||
payload['effective_username'] = effective_username
|
payload['effective_username'] = effective_username
|
||||||
payload['uses_global_ssh_key'] = uses_global_ssh_key
|
payload['uses_global_ssh_key'] = router.device_type == 'routeros' and has_global_key and not has_router_key
|
||||||
payload['has_effective_ssh_key'] = has_effective_ssh_key
|
payload['has_effective_ssh_key'] = router.device_type == 'routeros' and (has_router_key or has_global_key)
|
||||||
payload['uses_global_switchos_credentials'] = uses_global_switchos_credentials
|
payload['uses_global_switchos_credentials'] = uses_global_switchos_credentials
|
||||||
payload['has_effective_password'] = has_effective_password
|
payload['has_effective_password'] = has_effective_password
|
||||||
payload['supports_export'] = router.device_type == 'routeros'
|
payload['supports_export'] = router.device_type == 'routeros'
|
||||||
|
|||||||
@@ -15,12 +15,6 @@ from app.services.swos_beta_service import swos_beta_service
|
|||||||
|
|
||||||
|
|
||||||
class RouterService:
|
class RouterService:
|
||||||
connect_timeout_seconds = 10
|
|
||||||
auth_timeout_seconds = 10
|
|
||||||
banner_timeout_seconds = 10
|
|
||||||
command_timeout_seconds = 20
|
|
||||||
sftp_timeout_seconds = 20
|
|
||||||
|
|
||||||
def ping(self, router: Router):
|
def ping(self, router: Router):
|
||||||
if getattr(router, 'disable_ping', False):
|
if getattr(router, 'disable_ping', False):
|
||||||
return {'router_id': router.id, 'reachable': False, 'latency_ms': None, 'disabled': True}
|
return {'router_id': router.id, 'reachable': False, 'latency_ms': None, 'disabled': True}
|
||||||
@@ -65,41 +59,28 @@ class RouterService:
|
|||||||
def _connect(self, router: Router, global_ssh_key: str | None = None):
|
def _connect(self, router: Router, global_ssh_key: str | None = None):
|
||||||
client = paramiko.SSHClient()
|
client = paramiko.SSHClient()
|
||||||
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
|
key_source = router.ssh_key.strip() if router.ssh_key and router.ssh_key.strip() else (global_ssh_key or "")
|
||||||
router_key = (router.ssh_key or '').strip()
|
|
||||||
router_password = (router.ssh_password or '').strip()
|
|
||||||
global_key = (global_ssh_key or '').strip()
|
|
||||||
|
|
||||||
use_password_auth = bool(router_password and not router_key)
|
|
||||||
key_source = router_key or ('' if use_password_auth else global_key)
|
|
||||||
|
|
||||||
connect_kwargs = {
|
|
||||||
'hostname': router.host,
|
|
||||||
'port': router.port,
|
|
||||||
'username': router.ssh_user,
|
|
||||||
'timeout': self.connect_timeout_seconds,
|
|
||||||
'auth_timeout': self.auth_timeout_seconds,
|
|
||||||
'banner_timeout': self.banner_timeout_seconds,
|
|
||||||
'allow_agent': False,
|
|
||||||
'look_for_keys': False,
|
|
||||||
}
|
|
||||||
|
|
||||||
if key_source:
|
if key_source:
|
||||||
pkey = self._load_pkey(key_source)
|
pkey = self._load_pkey(key_source)
|
||||||
client.connect(pkey=pkey, **connect_kwargs)
|
client.connect(router.host, port=router.port, username=router.ssh_user, pkey=pkey, timeout=10)
|
||||||
else:
|
else:
|
||||||
client.connect(password=router_password or None, **connect_kwargs)
|
client.connect(
|
||||||
|
router.host,
|
||||||
transport = client.get_transport()
|
port=router.port,
|
||||||
if transport is not None:
|
username=router.ssh_user,
|
||||||
transport.set_keepalive(15)
|
password=router.ssh_password,
|
||||||
|
timeout=10,
|
||||||
|
allow_agent=False,
|
||||||
|
look_for_keys=False,
|
||||||
|
banner_timeout=10,
|
||||||
|
)
|
||||||
return client
|
return client
|
||||||
|
|
||||||
def export(self, router: Router, global_ssh_key: str | None = None) -> str:
|
def export(self, router: Router, global_ssh_key: str | None = None) -> str:
|
||||||
if router.device_type != 'routeros':
|
if router.device_type != 'routeros':
|
||||||
raise ValueError('Export tekstowy jest dostępny tylko dla RouterOS.')
|
raise ValueError('Export tekstowy jest dostępny tylko dla RouterOS.')
|
||||||
client = self._connect(router, global_ssh_key)
|
client = self._connect(router, global_ssh_key)
|
||||||
_, stdout, _ = client.exec_command('/export', timeout=self.command_timeout_seconds)
|
_, stdout, _ = client.exec_command('/export')
|
||||||
output = stdout.read().decode('utf-8', errors='ignore')
|
output = stdout.read().decode('utf-8', errors='ignore')
|
||||||
client.close()
|
client.close()
|
||||||
return output
|
return output
|
||||||
@@ -111,10 +92,9 @@ class RouterService:
|
|||||||
return local_path
|
return local_path
|
||||||
|
|
||||||
client = self._connect(router, global_ssh_key)
|
client = self._connect(router, global_ssh_key)
|
||||||
_, stdout, _ = client.exec_command(f'/system backup save name={backup_name}', timeout=self.command_timeout_seconds)
|
_, stdout, _ = client.exec_command(f'/system backup save name={backup_name}')
|
||||||
stdout.channel.recv_exit_status()
|
stdout.channel.recv_exit_status()
|
||||||
sftp = client.open_sftp()
|
sftp = client.open_sftp()
|
||||||
sftp.get_channel().settimeout(self.sftp_timeout_seconds)
|
|
||||||
remote_file = f'{backup_name}.backup'
|
remote_file = f'{backup_name}.backup'
|
||||||
sftp.get(remote_file, local_path)
|
sftp.get(remote_file, local_path)
|
||||||
try:
|
try:
|
||||||
@@ -130,7 +110,6 @@ class RouterService:
|
|||||||
raise ValueError('Przywracanie plików jest dostępne tylko dla RouterOS.')
|
raise ValueError('Przywracanie plików jest dostępne tylko dla RouterOS.')
|
||||||
client = self._connect(router, global_ssh_key)
|
client = self._connect(router, global_ssh_key)
|
||||||
sftp = client.open_sftp()
|
sftp = client.open_sftp()
|
||||||
sftp.get_channel().settimeout(self.sftp_timeout_seconds)
|
|
||||||
target_name = Path(local_backup_path).name
|
target_name = Path(local_backup_path).name
|
||||||
sftp.put(local_backup_path, target_name)
|
sftp.put(local_backup_path, target_name)
|
||||||
sftp.close()
|
sftp.close()
|
||||||
@@ -140,9 +119,9 @@ class RouterService:
|
|||||||
tested_at = datetime.utcnow()
|
tested_at = datetime.utcnow()
|
||||||
try:
|
try:
|
||||||
client = self._connect(router, global_ssh_key)
|
client = self._connect(router, global_ssh_key)
|
||||||
_, stdout, _ = client.exec_command('/system resource print without-paging', timeout=self.command_timeout_seconds)
|
_, stdout, _ = client.exec_command('/system resource print without-paging')
|
||||||
resource_output = stdout.read().decode('utf-8', errors='ignore')
|
resource_output = stdout.read().decode('utf-8', errors='ignore')
|
||||||
_, stdout, _ = client.exec_command('/system identity print', timeout=self.command_timeout_seconds)
|
_, stdout, _ = client.exec_command('/system identity print')
|
||||||
identity_output = stdout.read().decode('utf-8', errors='ignore')
|
identity_output = stdout.read().decode('utf-8', errors='ignore')
|
||||||
client.close()
|
client.close()
|
||||||
model = 'Unknown'
|
model = 'Unknown'
|
||||||
|
|||||||
@@ -60,60 +60,3 @@ def test_router_list_marks_global_ssh_key_usage(monkeypatch, tmp_path):
|
|||||||
payload = list_response.json()
|
payload = list_response.json()
|
||||||
assert payload[0]["uses_global_ssh_key"] is True
|
assert payload[0]["uses_global_ssh_key"] is True
|
||||||
assert payload[0]["has_effective_ssh_key"] is True
|
assert payload[0]["has_effective_ssh_key"] is True
|
||||||
|
|
||||||
|
|
||||||
def test_router_password_auth_overrides_global_ssh_key(monkeypatch, tmp_path):
|
|
||||||
monkeypatch.setenv("DATABASE_URL", f"sqlite:///{tmp_path / 'routers-password.db'}")
|
|
||||||
monkeypatch.setenv("DATA_DIR", str(tmp_path / 'data-password'))
|
|
||||||
monkeypatch.setenv("SECRET_KEY", "test-secret")
|
|
||||||
monkeypatch.setenv("DEFAULT_ADMIN_USERNAME", "admin")
|
|
||||||
monkeypatch.setenv("DEFAULT_ADMIN_PASSWORD", "admin")
|
|
||||||
|
|
||||||
with TestClient(app) as client:
|
|
||||||
login_response = client.post("/api/auth/login", data={"username": "admin", "password": "admin"})
|
|
||||||
token = login_response.json()["access_token"]
|
|
||||||
headers = {"Authorization": f"Bearer {token}"}
|
|
||||||
|
|
||||||
settings_response = client.put(
|
|
||||||
"/api/settings",
|
|
||||||
json={
|
|
||||||
"backup_retention_days": 7,
|
|
||||||
"log_retention_days": 7,
|
|
||||||
"export_cron": "",
|
|
||||||
"binary_cron": "",
|
|
||||||
"retention_cron": "",
|
|
||||||
"enable_auto_export": False,
|
|
||||||
"connection_test_interval_minutes": 0,
|
|
||||||
"global_ssh_key": "-----BEGIN OPENSSH PRIVATE KEY-----\nabc\n-----END OPENSSH PRIVATE KEY-----",
|
|
||||||
"pushover_token": None,
|
|
||||||
"pushover_userkey": None,
|
|
||||||
"notify_failures_only": True,
|
|
||||||
"smtp_host": None,
|
|
||||||
"smtp_port": 587,
|
|
||||||
"smtp_login": None,
|
|
||||||
"smtp_password": None,
|
|
||||||
"smtp_notifications_enabled": False,
|
|
||||||
"recipient_email": None,
|
|
||||||
"clear_global_ssh_key": False
|
|
||||||
},
|
|
||||||
headers=headers,
|
|
||||||
)
|
|
||||||
assert settings_response.status_code == 200
|
|
||||||
|
|
||||||
create_response = client.post(
|
|
||||||
"/api/routers",
|
|
||||||
json={
|
|
||||||
"name": "edge02",
|
|
||||||
"host": "10.0.0.2",
|
|
||||||
"port": 22,
|
|
||||||
"ssh_user": "admin",
|
|
||||||
"ssh_password": "secret-pass",
|
|
||||||
"ssh_key": None
|
|
||||||
},
|
|
||||||
headers=headers,
|
|
||||||
)
|
|
||||||
assert create_response.status_code == 200
|
|
||||||
payload = create_response.json()
|
|
||||||
assert payload["uses_global_ssh_key"] is False
|
|
||||||
assert payload["has_effective_ssh_key"] is False
|
|
||||||
assert payload["has_effective_password"] is True
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { HttpErrorResponse } from '@angular/common/http';
|
|
||||||
import { Injectable, inject } from '@angular/core';
|
import { Injectable, inject } from '@angular/core';
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
import { ConfirmationService, MessageService } from 'primeng/api';
|
import { ConfirmationService, MessageService } from 'primeng/api';
|
||||||
@@ -46,17 +45,6 @@ export class UiService {
|
|||||||
this.messageService.clear();
|
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> {
|
confirm(options: ConfirmOptions): Promise<boolean> {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
let resolved = false;
|
let resolved = false;
|
||||||
@@ -87,42 +75,7 @@ export class UiService {
|
|||||||
return this.t(key, params);
|
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 {
|
private t(key: string, params?: Record<string, unknown>): string {
|
||||||
return this.translate.instant(key, params);
|
return this.translate.instant(key, params);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -42,7 +42,6 @@
|
|||||||
<small class="form-field--full table-secondary">{{ passwordsMatch ? ('auth.passwordsMatchHint' | translate) : ('auth.passwordsMismatch' | translate) }}</small>
|
<small class="form-field--full table-secondary">{{ passwordsMatch ? ('auth.passwordsMatchHint' | translate) : ('auth.passwordsMismatch' | translate) }}</small>
|
||||||
<small *ngIf="error" class="form-error form-field--full">{{ error }}</small>
|
<small *ngIf="error" class="form-error form-field--full">{{ error }}</small>
|
||||||
<div class="dialog-actions form-field--full auth-card__actions auth-card__actions--split">
|
<div class="dialog-actions form-field--full auth-card__actions auth-card__actions--split">
|
||||||
<a class="auth-link" routerLink="/">{{ 'auth.backToApp' | translate }}</a>
|
|
||||||
<button pButton type="submit" styleClass="auth-primary-btn" [disabled]="form.invalid || submitting" [loading]="submitting" [label]="'auth.changePassword' | translate"></button>
|
<button pButton type="submit" styleClass="auth-primary-btn" [disabled]="form.invalid || submitting" [loading]="submitting" [label]="'auth.changePassword' | translate"></button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -62,7 +62,7 @@
|
|||||||
</app-section-card>
|
</app-section-card>
|
||||||
|
|
||||||
<app-section-card class="diff-configs-table-section" [title]="'diffConfigs.tableTitle' | translate" [subtitle]="'diffConfigs.tableSubtitle' | translate">
|
<app-section-card class="diff-configs-table-section" [title]="'diffConfigs.tableTitle' | translate" [subtitle]="'diffConfigs.tableSubtitle' | translate">
|
||||||
<p-table [value]="exportFiles" [loading]="loading" [rows]="8" [paginator]="exportFiles.length > 8" responsiveLayout="stack" [breakpoint]="'960px'" styleClass="app-table repository-table">
|
<p-table [value]="exportFiles" [loading]="loading" [rows]="8" [paginator]="exportFiles.length > 8" responsiveLayout="stack" [breakpoint]="'960px'" styleClass="app-table repository-table repository-table--diff">
|
||||||
<ng-template pTemplate="header">
|
<ng-template pTemplate="header">
|
||||||
<tr>
|
<tr>
|
||||||
<th>{{ 'files.fileColumn' | translate }}</th>
|
<th>{{ 'files.fileColumn' | translate }}</th>
|
||||||
@@ -75,7 +75,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<span class="p-column-title">{{ 'files.fileColumn' | translate }}</span>
|
<span class="p-column-title">{{ 'files.fileColumn' | translate }}</span>
|
||||||
<div class="table-primary">{{ item.file_name }}</div>
|
<div class="table-primary table-primary--ellipsis" [attr.title]="item.file_name">{{ item.file_name }}</div>
|
||||||
<small class="table-secondary">{{ 'files.checksum' | translate }}: {{ checksumShort(item.checksum) }}</small>
|
<small class="table-secondary">{{ 'files.checksum' | translate }}: {{ checksumShort(item.checksum) }}</small>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
@@ -90,11 +90,19 @@
|
|||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span class="p-column-title">{{ 'files.compareColumn' | translate }}</span>
|
<span class="p-column-title">{{ 'files.compareColumn' | translate }}</span>
|
||||||
<div class="table-actions table-actions--labels table-actions--stack">
|
<div class="table-actions table-actions--labels table-actions--stack table-actions--desktop-row">
|
||||||
<button pButton type="button" size="small" styleClass="table-action-btn table-action-btn--wide" severity="secondary" icon="pi pi-arrow-left" [label]="'files.setOlder' | translate" (click)="assignCompare('left', item)"></button>
|
<button pButton type="button" size="small" styleClass="table-action-btn table-action-btn--wide" severity="secondary" icon="pi pi-arrow-left" [label]="'files.setOlder' | translate" (click)="assignCompare('left', item)"></button>
|
||||||
<button pButton type="button" size="small" styleClass="table-action-btn table-action-btn--wide" severity="secondary" icon="pi pi-arrow-right" [label]="'files.setNewer' | translate" (click)="assignCompare('right', item)"></button>
|
<button pButton type="button" size="small" styleClass="table-action-btn table-action-btn--wide" severity="secondary" icon="pi pi-arrow-right" [label]="'files.setNewer' | translate" (click)="assignCompare('right', item)"></button>
|
||||||
<button pButton type="button" size="small" styleClass="table-action-btn table-action-btn--wide" severity="info" icon="pi pi-eye" [label]="'common.preview' | translate" (click)="viewExport(item)"></button>
|
<button pButton type="button" size="small" styleClass="table-action-btn table-action-btn--wide" severity="info" icon="pi pi-eye" [label]="'common.preview' | translate" (click)="viewExport(item)"></button>
|
||||||
</div>
|
</div>
|
||||||
|
<details class="table-row-menu table-row-menu--compare">
|
||||||
|
<summary><i class="pi pi-ellipsis-h"></i><span>{{ 'files.compareColumn' | translate }}</span></summary>
|
||||||
|
<div class="table-row-menu__list">
|
||||||
|
<button pButton type="button" size="small" styleClass="table-action-btn table-action-btn--wide" severity="secondary" icon="pi pi-arrow-left" [label]="'files.setOlder' | translate" (click)="assignCompare('left', item)"></button>
|
||||||
|
<button pButton type="button" size="small" styleClass="table-action-btn table-action-btn--wide" severity="secondary" icon="pi pi-arrow-right" [label]="'files.setNewer' | translate" (click)="assignCompare('right', item)"></button>
|
||||||
|
<button pButton type="button" size="small" styleClass="table-action-btn table-action-btn--wide" severity="info" icon="pi pi-eye" [label]="'common.preview' | translate" (click)="viewExport(item)"></button>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|||||||
@@ -87,7 +87,7 @@
|
|||||||
</app-section-card>
|
</app-section-card>
|
||||||
|
|
||||||
<app-section-card class="repository-table-section" [title]="'files.tableTitle' | translate" [subtitle]="'files.tableSubtitle' | translate">
|
<app-section-card class="repository-table-section" [title]="'files.tableTitle' | translate" [subtitle]="'files.tableSubtitle' | translate">
|
||||||
<p-table [value]="files" [(selection)]="selected" dataKey="id" [rows]="10" [loading]="loading" [paginator]="files.length > 10" responsiveLayout="stack" [breakpoint]="'960px'" styleClass="app-table repository-table">
|
<p-table [value]="files" [(selection)]="selected" dataKey="id" [rows]="10" [loading]="loading" [paginator]="files.length > 10" responsiveLayout="stack" [breakpoint]="'960px'" styleClass="app-table repository-table repository-table--files">
|
||||||
<ng-template pTemplate="header">
|
<ng-template pTemplate="header">
|
||||||
<tr>
|
<tr>
|
||||||
<th style="width:3rem"></th>
|
<th style="width:3rem"></th>
|
||||||
@@ -108,7 +108,7 @@
|
|||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span class="p-column-title">{{ 'files.fileColumn' | translate }}</span>
|
<span class="p-column-title">{{ 'files.fileColumn' | translate }}</span>
|
||||||
<div class="table-primary">{{ item.file_name }}</div>
|
<div class="table-primary table-primary--ellipsis" [attr.title]="item.file_name">{{ item.file_name }}</div>
|
||||||
<small class="table-secondary">{{ 'files.checksum' | translate }}: {{ checksumShort(item.checksum) }}</small>
|
<small class="table-secondary">{{ 'files.checksum' | translate }}: {{ checksumShort(item.checksum) }}</small>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
@@ -132,24 +132,42 @@
|
|||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span class="p-column-title">{{ 'files.compareColumn' | translate }}</span>
|
<span class="p-column-title">{{ 'files.compareColumn' | translate }}</span>
|
||||||
<div class="table-actions table-actions--stack" *ngIf="item.backup_type === 'export'; else noCompare">
|
<div class="table-actions table-actions--stack table-actions--desktop-row" *ngIf="item.backup_type === 'export'; else noCompare">
|
||||||
<button pButton type="button" size="small" styleClass="table-action-btn table-action-btn--wide" severity="secondary" icon="pi pi-arrow-left" [label]="'files.setOlder' | translate" (click)="assignCompare('left', item)"></button>
|
<button pButton type="button" size="small" styleClass="table-action-btn table-action-btn--wide" severity="secondary" icon="pi pi-arrow-left" [label]="'files.setOlder' | translate" (click)="assignCompare('left', item)"></button>
|
||||||
<button pButton type="button" size="small" styleClass="table-action-btn table-action-btn--wide" severity="secondary" icon="pi pi-arrow-right" [label]="'files.setNewer' | translate" (click)="assignCompare('right', item)"></button>
|
<button pButton type="button" size="small" styleClass="table-action-btn table-action-btn--wide" severity="secondary" icon="pi pi-arrow-right" [label]="'files.setNewer' | translate" (click)="assignCompare('right', item)"></button>
|
||||||
<button pButton type="button" size="small" styleClass="table-action-btn table-action-btn--wide" severity="help" icon="pi pi-code" [label]="'files.latestForRouter' | translate" (click)="compareClosestForRouter(item)"></button>
|
<button pButton type="button" size="small" styleClass="table-action-btn table-action-btn--wide" severity="help" icon="pi pi-code" [label]="'files.latestForRouter' | translate" (click)="compareClosestForRouter(item)"></button>
|
||||||
</div>
|
</div>
|
||||||
|
<details class="table-row-menu table-row-menu--compare" *ngIf="item.backup_type === 'export'">
|
||||||
|
<summary><i class="pi pi-ellipsis-h"></i><span>{{ 'files.compareColumn' | translate }}</span></summary>
|
||||||
|
<div class="table-row-menu__list">
|
||||||
|
<button pButton type="button" size="small" styleClass="table-action-btn table-action-btn--wide" severity="secondary" icon="pi pi-arrow-left" [label]="'files.setOlder' | translate" (click)="assignCompare('left', item)"></button>
|
||||||
|
<button pButton type="button" size="small" styleClass="table-action-btn table-action-btn--wide" severity="secondary" icon="pi pi-arrow-right" [label]="'files.setNewer' | translate" (click)="assignCompare('right', item)"></button>
|
||||||
|
<button pButton type="button" size="small" styleClass="table-action-btn table-action-btn--wide" severity="help" icon="pi pi-code" [label]="'files.latestForRouter' | translate" (click)="compareClosestForRouter(item)"></button>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
<ng-template #noCompare>
|
<ng-template #noCompare>
|
||||||
<small class="table-secondary">{{ 'files.binaryNoCompare' | translate }}</small>
|
<small class="table-secondary">{{ 'files.binaryNoCompare' | translate }}</small>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span class="p-column-title">{{ 'files.actionsColumn' | translate }}</span>
|
<span class="p-column-title">{{ 'files.actionsColumn' | translate }}</span>
|
||||||
<div class="table-actions table-actions--labels table-actions--stack">
|
<div class="table-actions table-actions--labels table-actions--stack table-actions--desktop-row">
|
||||||
<button pButton type="button" size="small" styleClass="table-action-btn table-action-btn--wide" icon="pi pi-download" [label]="'common.download' | translate" (click)="download(item.id)"></button>
|
<button pButton type="button" size="small" styleClass="table-action-btn table-action-btn--wide" icon="pi pi-download" [label]="'common.download' | translate" (click)="download(item.id)"></button>
|
||||||
<button pButton type="button" size="small" styleClass="table-action-btn table-action-btn--wide" severity="secondary" icon="pi pi-envelope" [label]="'common.email' | translate" (click)="sendEmail(item.id)"></button>
|
<button pButton type="button" size="small" styleClass="table-action-btn table-action-btn--wide" severity="secondary" icon="pi pi-envelope" [label]="'common.email' | translate" (click)="sendEmail(item.id)"></button>
|
||||||
<button *ngIf="item.backup_type==='export'" pButton type="button" size="small" styleClass="table-action-btn table-action-btn--wide" severity="info" icon="pi pi-eye" [label]="'common.preview' | translate" (click)="viewExport(item)"></button>
|
<button *ngIf="item.backup_type==='export'" pButton type="button" size="small" styleClass="table-action-btn table-action-btn--wide" severity="info" icon="pi pi-eye" [label]="'common.preview' | translate" (click)="viewExport(item)"></button>
|
||||||
<button *ngIf="item.backup_type==='binary' && item.device_type==='routeros'" pButton type="button" size="small" styleClass="table-action-btn table-action-btn--wide" severity="help" icon="pi pi-upload" [label]="'common.restore' | translate" (click)="upload(item)"></button>
|
<button *ngIf="item.backup_type==='binary' && item.device_type==='routeros'" pButton type="button" size="small" styleClass="table-action-btn table-action-btn--wide" severity="help" icon="pi pi-upload" [label]="'common.restore' | translate" (click)="upload(item)"></button>
|
||||||
<button pButton type="button" size="small" styleClass="table-action-btn table-action-btn--wide" severity="danger" icon="pi pi-trash" [label]="'common.delete' | translate" (click)="deleteOne(item.id)"></button>
|
<button pButton type="button" size="small" styleClass="table-action-btn table-action-btn--wide" severity="danger" icon="pi pi-trash" [label]="'common.delete' | translate" (click)="deleteOne(item.id)"></button>
|
||||||
</div>
|
</div>
|
||||||
|
<details class="table-row-menu">
|
||||||
|
<summary><i class="pi pi-ellipsis-h"></i><span>{{ 'common.actions' | translate }}</span></summary>
|
||||||
|
<div class="table-row-menu__list">
|
||||||
|
<button pButton type="button" size="small" styleClass="table-action-btn table-action-btn--wide" icon="pi pi-download" [label]="'common.download' | translate" (click)="download(item.id)"></button>
|
||||||
|
<button pButton type="button" size="small" styleClass="table-action-btn table-action-btn--wide" severity="secondary" icon="pi pi-envelope" [label]="'common.email' | translate" (click)="sendEmail(item.id)"></button>
|
||||||
|
<button *ngIf="item.backup_type==='export'" pButton type="button" size="small" styleClass="table-action-btn table-action-btn--wide" severity="info" icon="pi pi-eye" [label]="'common.preview' | translate" (click)="viewExport(item)"></button>
|
||||||
|
<button *ngIf="item.backup_type==='binary' && item.device_type==='routeros'" pButton type="button" size="small" styleClass="table-action-btn table-action-btn--wide" severity="help" icon="pi pi-upload" [label]="'common.restore' | translate" (click)="upload(item)"></button>
|
||||||
|
<button pButton type="button" size="small" styleClass="table-action-btn table-action-btn--wide" severity="danger" icon="pi pi-trash" [label]="'common.delete' | translate" (click)="deleteOne(item.id)"></button>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|||||||
@@ -150,7 +150,7 @@
|
|||||||
|
|
||||||
<div class="dashboard-grid router-detail-grid router-detail-grid--stack" *ngIf="!isSwitchos">
|
<div class="dashboard-grid router-detail-grid router-detail-grid--stack" *ngIf="!isSwitchos">
|
||||||
<app-section-card [title]="'routers.exportsTableTitle' | translate" [subtitle]="'routers.exportsTableSubtitle' | translate">
|
<app-section-card [title]="'routers.exportsTableTitle' | translate" [subtitle]="'routers.exportsTableSubtitle' | translate">
|
||||||
<p-table [value]="exportBackups" responsiveLayout="stack" [breakpoint]="'960px'" styleClass="app-table repository-table">
|
<p-table [value]="exportBackups" responsiveLayout="stack" [breakpoint]="'960px'" styleClass="app-table repository-table repository-table--router-detail">
|
||||||
<ng-template pTemplate="header">
|
<ng-template pTemplate="header">
|
||||||
<tr><th>{{ 'files.fileColumn' | translate }}</th><th>{{ 'files.createdColumn' | translate }}</th><th>{{ 'common.actions' | translate }}</th></tr>
|
<tr><th>{{ 'files.fileColumn' | translate }}</th><th>{{ 'files.createdColumn' | translate }}</th><th>{{ 'common.actions' | translate }}</th></tr>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
@@ -158,7 +158,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<span class="p-column-title">{{ 'files.fileColumn' | translate }}</span>
|
<span class="p-column-title">{{ 'files.fileColumn' | translate }}</span>
|
||||||
<div class="table-primary">{{ item.file_name }}</div>
|
<div class="table-primary table-primary--ellipsis" [attr.title]="item.file_name">{{ item.file_name }}</div>
|
||||||
<small class="table-secondary">{{ 'files.exportType' | translate }}</small>
|
<small class="table-secondary">{{ 'files.exportType' | translate }}</small>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
@@ -167,13 +167,23 @@
|
|||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span class="p-column-title">{{ 'common.actions' | translate }}</span>
|
<span class="p-column-title">{{ 'common.actions' | translate }}</span>
|
||||||
<div class="table-actions table-actions--labels table-actions--tight">
|
<div class="table-actions table-actions--labels table-actions--tight table-actions--desktop-row">
|
||||||
<button pButton type="button" size="small" styleClass="table-action-btn table-action-btn--wide table-action-btn--compact" icon="pi pi-download" [label]="'common.download' | translate" (click)="download(item.id)"></button>
|
<button pButton type="button" size="small" styleClass="table-action-btn table-action-btn--wide table-action-btn--compact" icon="pi pi-download" [label]="'common.download' | translate" (click)="download(item.id)"></button>
|
||||||
<button pButton type="button" size="small" styleClass="table-action-btn table-action-btn--wide table-action-btn--compact" severity="info" icon="pi pi-eye" [label]="'common.preview' | translate" (click)="viewExport(item.id)"></button>
|
<button pButton type="button" size="small" styleClass="table-action-btn table-action-btn--wide table-action-btn--compact" severity="info" icon="pi pi-eye" [label]="'common.preview' | translate" (click)="viewExport(item.id)"></button>
|
||||||
<button pButton type="button" size="small" styleClass="table-action-btn table-action-btn--wide table-action-btn--compact" severity="secondary" icon="pi pi-envelope" [label]="'common.email' | translate" (click)="sendEmail(item.id)"></button>
|
<button pButton type="button" size="small" styleClass="table-action-btn table-action-btn--wide table-action-btn--compact" severity="secondary" icon="pi pi-envelope" [label]="'common.email' | translate" (click)="sendEmail(item.id)"></button>
|
||||||
<button pButton *ngIf="i > 0" type="button" size="small" styleClass="table-action-btn table-action-btn--wide table-action-btn--compact" severity="help" icon="pi pi-code" [label]="'common.diff' | translate" (click)="compareToLatest(item.id)"></button>
|
<button pButton *ngIf="i > 0" type="button" size="small" styleClass="table-action-btn table-action-btn--wide table-action-btn--compact" severity="help" icon="pi pi-code" [label]="'common.diff' | translate" (click)="compareToLatest(item.id)"></button>
|
||||||
<button pButton type="button" size="small" styleClass="table-action-btn table-action-btn--wide table-action-btn--compact" severity="danger" icon="pi pi-trash" [label]="'common.delete' | translate" (click)="remove(item.id)"></button>
|
<button pButton type="button" size="small" styleClass="table-action-btn table-action-btn--wide table-action-btn--compact" severity="danger" icon="pi pi-trash" [label]="'common.delete' | translate" (click)="remove(item.id)"></button>
|
||||||
</div>
|
</div>
|
||||||
|
<details class="table-row-menu">
|
||||||
|
<summary><i class="pi pi-ellipsis-h"></i><span>{{ 'common.actions' | translate }}</span></summary>
|
||||||
|
<div class="table-row-menu__list">
|
||||||
|
<button pButton type="button" size="small" styleClass="table-action-btn table-action-btn--wide table-action-btn--compact" icon="pi pi-download" [label]="'common.download' | translate" (click)="download(item.id)"></button>
|
||||||
|
<button pButton type="button" size="small" styleClass="table-action-btn table-action-btn--wide table-action-btn--compact" severity="info" icon="pi pi-eye" [label]="'common.preview' | translate" (click)="viewExport(item.id)"></button>
|
||||||
|
<button pButton type="button" size="small" styleClass="table-action-btn table-action-btn--wide table-action-btn--compact" severity="secondary" icon="pi pi-envelope" [label]="'common.email' | translate" (click)="sendEmail(item.id)"></button>
|
||||||
|
<button pButton *ngIf="i > 0" type="button" size="small" styleClass="table-action-btn table-action-btn--wide table-action-btn--compact" severity="help" icon="pi pi-code" [label]="'common.diff' | translate" (click)="compareToLatest(item.id)"></button>
|
||||||
|
<button pButton type="button" size="small" styleClass="table-action-btn table-action-btn--wide table-action-btn--compact" severity="danger" icon="pi pi-trash" [label]="'common.delete' | translate" (click)="remove(item.id)"></button>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
@@ -183,7 +193,7 @@
|
|||||||
|
|
||||||
<div class="dashboard-grid router-detail-grid router-detail-grid--stack">
|
<div class="dashboard-grid router-detail-grid router-detail-grid--stack">
|
||||||
<app-section-card [title]="'routers.binaryTableTitle' | translate" [subtitle]="'routers.binaryTableSubtitle' | translate">
|
<app-section-card [title]="'routers.binaryTableTitle' | translate" [subtitle]="'routers.binaryTableSubtitle' | translate">
|
||||||
<p-table [value]="binaryBackups" responsiveLayout="stack" [breakpoint]="'960px'" styleClass="app-table repository-table">
|
<p-table [value]="binaryBackups" responsiveLayout="stack" [breakpoint]="'960px'" styleClass="app-table repository-table repository-table--router-detail">
|
||||||
<ng-template pTemplate="header">
|
<ng-template pTemplate="header">
|
||||||
<tr><th>{{ 'files.fileColumn' | translate }}</th><th>{{ 'files.createdColumn' | translate }}</th><th>{{ 'common.actions' | translate }}</th></tr>
|
<tr><th>{{ 'files.fileColumn' | translate }}</th><th>{{ 'files.createdColumn' | translate }}</th><th>{{ 'common.actions' | translate }}</th></tr>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
@@ -191,7 +201,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<span class="p-column-title">{{ 'files.fileColumn' | translate }}</span>
|
<span class="p-column-title">{{ 'files.fileColumn' | translate }}</span>
|
||||||
<div class="table-primary">{{ item.file_name }}</div>
|
<div class="table-primary table-primary--ellipsis" [attr.title]="item.file_name">{{ item.file_name }}</div>
|
||||||
<small class="table-secondary">{{ 'files.binaryType' | translate }}</small>
|
<small class="table-secondary">{{ 'files.binaryType' | translate }}</small>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
@@ -200,12 +210,21 @@
|
|||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span class="p-column-title">{{ 'common.actions' | translate }}</span>
|
<span class="p-column-title">{{ 'common.actions' | translate }}</span>
|
||||||
<div class="table-actions table-actions--labels table-actions--tight">
|
<div class="table-actions table-actions--labels table-actions--tight table-actions--desktop-row">
|
||||||
<button pButton type="button" size="small" styleClass="table-action-btn table-action-btn--wide table-action-btn--compact" icon="pi pi-download" [label]="'common.download' | translate" (click)="download(item.id)"></button>
|
<button pButton type="button" size="small" styleClass="table-action-btn table-action-btn--wide table-action-btn--compact" icon="pi pi-download" [label]="'common.download' | translate" (click)="download(item.id)"></button>
|
||||||
<button *ngIf="!isSwitchos" pButton type="button" size="small" styleClass="table-action-btn table-action-btn--wide table-action-btn--compact" severity="help" icon="pi pi-upload" [label]="'common.restore' | translate" (click)="upload(item.id)"></button>
|
<button *ngIf="!isSwitchos" pButton type="button" size="small" styleClass="table-action-btn table-action-btn--wide table-action-btn--compact" severity="help" icon="pi pi-upload" [label]="'common.restore' | translate" (click)="upload(item.id)"></button>
|
||||||
<button pButton type="button" size="small" styleClass="table-action-btn table-action-btn--wide table-action-btn--compact" severity="secondary" icon="pi pi-envelope" [label]="'common.email' | translate" (click)="sendEmail(item.id)"></button>
|
<button pButton type="button" size="small" styleClass="table-action-btn table-action-btn--wide table-action-btn--compact" severity="secondary" icon="pi pi-envelope" [label]="'common.email' | translate" (click)="sendEmail(item.id)"></button>
|
||||||
<button pButton type="button" size="small" styleClass="table-action-btn table-action-btn--wide table-action-btn--compact" severity="danger" icon="pi pi-trash" [label]="'common.delete' | translate" (click)="remove(item.id)"></button>
|
<button pButton type="button" size="small" styleClass="table-action-btn table-action-btn--wide table-action-btn--compact" severity="danger" icon="pi pi-trash" [label]="'common.delete' | translate" (click)="remove(item.id)"></button>
|
||||||
</div>
|
</div>
|
||||||
|
<details class="table-row-menu">
|
||||||
|
<summary><i class="pi pi-ellipsis-h"></i><span>{{ 'common.actions' | translate }}</span></summary>
|
||||||
|
<div class="table-row-menu__list">
|
||||||
|
<button pButton type="button" size="small" styleClass="table-action-btn table-action-btn--wide table-action-btn--compact" icon="pi pi-download" [label]="'common.download' | translate" (click)="download(item.id)"></button>
|
||||||
|
<button *ngIf="!isSwitchos" pButton type="button" size="small" styleClass="table-action-btn table-action-btn--wide table-action-btn--compact" severity="help" icon="pi pi-upload" [label]="'common.restore' | translate" (click)="upload(item.id)"></button>
|
||||||
|
<button pButton type="button" size="small" styleClass="table-action-btn table-action-btn--wide table-action-btn--compact" severity="secondary" icon="pi pi-envelope" [label]="'common.email' | translate" (click)="sendEmail(item.id)"></button>
|
||||||
|
<button pButton type="button" size="small" styleClass="table-action-btn table-action-btn--wide table-action-btn--compact" severity="danger" icon="pi pi-trash" [label]="'common.delete' | translate" (click)="remove(item.id)"></button>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { finalize, timeout } from 'rxjs';
|
|
||||||
import { HttpResponse } from '@angular/common/http';
|
import { HttpResponse } from '@angular/common/http';
|
||||||
import { Component, OnInit, inject } from '@angular/core';
|
import { Component, OnInit, inject } from '@angular/core';
|
||||||
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
|
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||||
@@ -243,25 +242,17 @@ export class RouterDetailPageComponent implements OnInit {
|
|||||||
if (payload.device_type === 'switchos') {
|
if (payload.device_type === 'switchos') {
|
||||||
payload.ssh_key = '';
|
payload.ssh_key = '';
|
||||||
}
|
}
|
||||||
this.api.http
|
this.api.http.put<DeviceItem>(`${this.api.baseUrl}/routers/${this.routerId}`, payload).subscribe({
|
||||||
.put<DeviceItem>(`${this.api.baseUrl}/routers/${this.routerId}`, payload)
|
next: (routerItem) => {
|
||||||
.pipe(
|
this.routerItem = routerItem;
|
||||||
timeout(15000),
|
this.connection = this.mapStoredConnection(routerItem);
|
||||||
finalize(() => {
|
this.editVisible = false;
|
||||||
this.saving = false;
|
this.ui.success('toast.routerUpdated');
|
||||||
})
|
},
|
||||||
)
|
complete: () => {
|
||||||
.subscribe({
|
this.saving = false;
|
||||||
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() {
|
saveSettings() {
|
||||||
@@ -277,25 +268,17 @@ export class RouterDetailPageComponent implements OnInit {
|
|||||||
payload.disable_export_backups = true;
|
payload.disable_export_backups = true;
|
||||||
payload.disable_binary_backups = true;
|
payload.disable_binary_backups = true;
|
||||||
}
|
}
|
||||||
this.api.http
|
this.api.http.put<DeviceItem>(`${this.api.baseUrl}/routers/${this.routerId}`, payload).subscribe({
|
||||||
.put<DeviceItem>(`${this.api.baseUrl}/routers/${this.routerId}`, payload)
|
next: (routerItem) => {
|
||||||
.pipe(
|
this.routerItem = routerItem;
|
||||||
timeout(15000),
|
this.connection = this.mapStoredConnection(routerItem);
|
||||||
finalize(() => {
|
this.patchSettingsForm(routerItem);
|
||||||
this.savingSettings = false;
|
this.ui.success('toast.routerUpdated');
|
||||||
})
|
},
|
||||||
)
|
complete: () => {
|
||||||
.subscribe({
|
this.savingSettings = false;
|
||||||
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) {
|
private patchSettingsForm(item: DeviceItem) {
|
||||||
@@ -344,28 +327,20 @@ export class RouterDetailPageComponent implements OnInit {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.testing = true;
|
this.testing = true;
|
||||||
this.api.http
|
this.api.http.get<ConnectionSnapshot>(`${this.api.baseUrl}/routers/${this.routerId}/test-connection`).subscribe({
|
||||||
.get<ConnectionSnapshot>(`${this.api.baseUrl}/routers/${this.routerId}/test-connection`)
|
next: (result) => {
|
||||||
.pipe(
|
this.connection = result;
|
||||||
timeout(15000),
|
this.syncStoredConnection(result);
|
||||||
finalize(() => {
|
if (result.success) {
|
||||||
this.testing = false;
|
this.ui.success('toast.connectionSuccessful');
|
||||||
})
|
} else {
|
||||||
)
|
this.ui.error('toast.connectionFailed');
|
||||||
.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) {
|
compareToLatest(id: number) {
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { finalize, timeout } from 'rxjs';
|
|
||||||
import { Component, OnInit, inject } from '@angular/core';
|
import { Component, OnInit, inject } from '@angular/core';
|
||||||
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
|
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
@@ -168,23 +167,16 @@ export class RoutersPageComponent implements OnInit {
|
|||||||
? this.api.http.put(`${this.api.baseUrl}/routers/${this.editingId}`, payload)
|
? this.api.http.put(`${this.api.baseUrl}/routers/${this.editingId}`, payload)
|
||||||
: this.api.http.post(`${this.api.baseUrl}/routers`, payload);
|
: this.api.http.post(`${this.api.baseUrl}/routers`, payload);
|
||||||
|
|
||||||
request$
|
request$.subscribe({
|
||||||
.pipe(
|
next: () => {
|
||||||
timeout(15000),
|
this.ui.success(this.editingId ? 'toast.routerUpdated' : 'toast.routerCreated');
|
||||||
finalize(() => {
|
this.visible = false;
|
||||||
this.saving = false;
|
this.load();
|
||||||
})
|
},
|
||||||
)
|
complete: () => {
|
||||||
.subscribe({
|
this.saving = false;
|
||||||
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) {
|
async remove(id: number) {
|
||||||
|
|||||||
@@ -310,7 +310,7 @@
|
|||||||
</span>
|
</span>
|
||||||
<span class="form-field">
|
<span class="form-field">
|
||||||
<label>{{ 'settings.defaultSwitchosPassword' | translate }}</label>
|
<label>{{ 'settings.defaultSwitchosPassword' | translate }}</label>
|
||||||
<input pInputText formControlName="default_switchos_password" placeholder="••••••••" />
|
<input pInputText type="password" formControlName="default_switchos_password" placeholder="••••••••" />
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -56,7 +56,6 @@
|
|||||||
"currentPassword": "Current password",
|
"currentPassword": "Current password",
|
||||||
"newPassword": "New password",
|
"newPassword": "New password",
|
||||||
"backToLogin": "Back to login",
|
"backToLogin": "Back to login",
|
||||||
"backToApp": "Back to app",
|
|
||||||
"loginSubtitle": "Sign in to continue.",
|
"loginSubtitle": "Sign in to continue.",
|
||||||
"loginFailed": "Login failed",
|
"loginFailed": "Login failed",
|
||||||
"accountCreated": "Account created",
|
"accountCreated": "Account created",
|
||||||
@@ -458,8 +457,6 @@
|
|||||||
"archivePrepared": "Archive prepared.",
|
"archivePrepared": "Archive prepared.",
|
||||||
"exportedRouters": "Export completed for {{count}} devices.",
|
"exportedRouters": "Export completed for {{count}} devices.",
|
||||||
"binaryCompletedRouters": "Binary backup 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.",
|
"routerCreated": "Router created.",
|
||||||
"routerUpdated": "Router updated.",
|
"routerUpdated": "Router updated.",
|
||||||
"routerDeleted": "Router deleted.",
|
"routerDeleted": "Router deleted.",
|
||||||
|
|||||||
@@ -56,7 +56,6 @@
|
|||||||
"currentPassword": "Contraseña actual",
|
"currentPassword": "Contraseña actual",
|
||||||
"newPassword": "Nueva contraseña",
|
"newPassword": "Nueva contraseña",
|
||||||
"backToLogin": "Volver al inicio de sesión",
|
"backToLogin": "Volver al inicio de sesión",
|
||||||
"backToApp": "Volver a la app",
|
|
||||||
"loginSubtitle": "Inicia sesión para continuar.",
|
"loginSubtitle": "Inicia sesión para continuar.",
|
||||||
"loginFailed": "Error de inicio de sesión",
|
"loginFailed": "Error de inicio de sesión",
|
||||||
"accountCreated": "Cuenta creada",
|
"accountCreated": "Cuenta creada",
|
||||||
@@ -440,8 +439,6 @@
|
|||||||
"archivePrepared": "Archivo preparado.",
|
"archivePrepared": "Archivo preparado.",
|
||||||
"exportedRouters": "Exportación completada para {{count}} routers.",
|
"exportedRouters": "Exportación completada para {{count}} routers.",
|
||||||
"binaryCompletedRouters": "Copia binaria 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.",
|
"routerCreated": "Router creado.",
|
||||||
"routerUpdated": "Router actualizado.",
|
"routerUpdated": "Router actualizado.",
|
||||||
"routerDeleted": "Router eliminado.",
|
"routerDeleted": "Router eliminado.",
|
||||||
|
|||||||
@@ -56,7 +56,6 @@
|
|||||||
"currentPassword": "Nåværende passord",
|
"currentPassword": "Nåværende passord",
|
||||||
"newPassword": "Nytt passord",
|
"newPassword": "Nytt passord",
|
||||||
"backToLogin": "Tilbake til innlogging",
|
"backToLogin": "Tilbake til innlogging",
|
||||||
"backToApp": "Tilbake til appen",
|
|
||||||
"loginSubtitle": "Logg inn for å fortsette.",
|
"loginSubtitle": "Logg inn for å fortsette.",
|
||||||
"loginFailed": "Innlogging mislyktes",
|
"loginFailed": "Innlogging mislyktes",
|
||||||
"accountCreated": "Konto opprettet",
|
"accountCreated": "Konto opprettet",
|
||||||
@@ -440,8 +439,6 @@
|
|||||||
"archivePrepared": "Arkiv klargjort.",
|
"archivePrepared": "Arkiv klargjort.",
|
||||||
"exportedRouters": "Export fullført for {{count}} rutere.",
|
"exportedRouters": "Export fullført for {{count}} rutere.",
|
||||||
"binaryCompletedRouters": "Binær backup 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.",
|
"routerCreated": "Ruter opprettet.",
|
||||||
"routerUpdated": "Ruter oppdatert.",
|
"routerUpdated": "Ruter oppdatert.",
|
||||||
"routerDeleted": "Ruter slettet.",
|
"routerDeleted": "Ruter slettet.",
|
||||||
|
|||||||
@@ -56,7 +56,6 @@
|
|||||||
"currentPassword": "Obecne hasło",
|
"currentPassword": "Obecne hasło",
|
||||||
"newPassword": "Nowe hasło",
|
"newPassword": "Nowe hasło",
|
||||||
"backToLogin": "Powrót do logowania",
|
"backToLogin": "Powrót do logowania",
|
||||||
"backToApp": "Powrót do aplikacji",
|
|
||||||
"loginSubtitle": "Zaloguj się, aby kontynuować.",
|
"loginSubtitle": "Zaloguj się, aby kontynuować.",
|
||||||
"loginFailed": "Logowanie nie powiodło się",
|
"loginFailed": "Logowanie nie powiodło się",
|
||||||
"accountCreated": "Konto zostało utworzone",
|
"accountCreated": "Konto zostało utworzone",
|
||||||
@@ -458,8 +457,6 @@
|
|||||||
"archivePrepared": "Archiwum zostało przygotowane.",
|
"archivePrepared": "Archiwum zostało przygotowane.",
|
||||||
"exportedRouters": "Wykonano export dla {{count}} urządzeń.",
|
"exportedRouters": "Wykonano export dla {{count}} urządzeń.",
|
||||||
"binaryCompletedRouters": "Wykonano backup binarny 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.",
|
"routerCreated": "Urządzenie zostało dodane.",
|
||||||
"routerUpdated": "Urządzenie zostało zaktualizowane.",
|
"routerUpdated": "Urządzenie zostało zaktualizowane.",
|
||||||
"routerDeleted": "Urządzenie zostało usunięte.",
|
"routerDeleted": "Urządzenie zostało usunięte.",
|
||||||
|
|||||||
@@ -2598,9 +2598,13 @@ app-page-header{
|
|||||||
}
|
}
|
||||||
|
|
||||||
.repository-table .p-button .p-button-label{
|
.repository-table .p-button .p-button-label{
|
||||||
white-space: normal;
|
white-space: nowrap;
|
||||||
overflow-wrap: anywhere;
|
overflow-wrap: normal;
|
||||||
word-break: break-word;
|
word-break: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repository-table .p-column-title{
|
||||||
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.repository-table .p-button .p-button-icon{
|
.repository-table .p-button .p-button-icon{
|
||||||
@@ -3528,10 +3532,229 @@ body.dark-theme .device-toggle.is-active{background:linear-gradient(135deg,color
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.repository-table .p-column-title{
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
.repository-table .p-button.table-action-btn,
|
.repository-table .p-button.table-action-btn,
|
||||||
.repository-table .p-button.table-action-btn--wide,
|
.repository-table .p-button.table-action-btn--wide,
|
||||||
.repository-table .p-button.table-action-btn--compact{
|
.repository-table .p-button.table-action-btn--compact{
|
||||||
width: 100%;
|
width: 100%;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.repository-table .p-button .p-button-label{
|
||||||
|
white-space: normal;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.repository-table .table-actions--labels,
|
||||||
|
.repository-table .table-actions--tight{
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repository-table .table-actions--tight{
|
||||||
|
flex-wrap: wrap;
|
||||||
|
overflow-x: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repository-table .p-button.table-action-btn,
|
||||||
|
.repository-table .p-button.table-action-btn--wide,
|
||||||
|
.repository-table .p-button.table-action-btn--compact{
|
||||||
|
width: auto;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repository-table.app-table .p-datatable-thead > tr > th:last-child,
|
||||||
|
.repository-table.app-table .p-datatable-tbody > tr > td:last-child{
|
||||||
|
width: 1%;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* --- table usability fix 2026-04-15 --- */
|
||||||
|
.repository-table .p-column-title{display:none !important;}
|
||||||
|
@media (max-width: 960px){
|
||||||
|
.repository-table .p-column-title{display:inline-flex !important;align-items:center;}
|
||||||
|
}
|
||||||
|
|
||||||
|
.repository-table--files .p-datatable-thead > tr > th:first-child,
|
||||||
|
.repository-table--files .p-datatable-tbody > tr > td:first-child{
|
||||||
|
width: 3rem;
|
||||||
|
padding-inline: 0.55rem;
|
||||||
|
}
|
||||||
|
.repository-table--router-detail .p-datatable-thead > tr > th:first-child,
|
||||||
|
.repository-table--router-detail .p-datatable-tbody > tr > td:first-child,
|
||||||
|
.repository-table--diff .p-datatable-thead > tr > th:first-child,
|
||||||
|
.repository-table--diff .p-datatable-tbody > tr > td:first-child{
|
||||||
|
width: auto;
|
||||||
|
padding-inline: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repository-table .table-primary--ellipsis{
|
||||||
|
display:block;
|
||||||
|
max-width:100%;
|
||||||
|
white-space:nowrap;
|
||||||
|
overflow:hidden;
|
||||||
|
text-overflow:ellipsis;
|
||||||
|
}
|
||||||
|
.repository-table .table-secondary{
|
||||||
|
overflow:hidden;
|
||||||
|
text-overflow:ellipsis;
|
||||||
|
}
|
||||||
|
.repository-table.app-table .p-datatable-thead > tr > th,
|
||||||
|
.repository-table.app-table .p-datatable-tbody > tr > td{
|
||||||
|
min-width:0;
|
||||||
|
}
|
||||||
|
.repository-table.app-table .p-datatable-thead > tr > th:last-child,
|
||||||
|
.repository-table.app-table .p-datatable-tbody > tr > td:last-child{
|
||||||
|
width: 280px;
|
||||||
|
min-width: 280px;
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
|
.repository-table--diff.app-table .p-datatable-thead > tr > th:last-child,
|
||||||
|
.repository-table--diff.app-table .p-datatable-tbody > tr > td:last-child{
|
||||||
|
width: 220px;
|
||||||
|
min-width: 220px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repository-table .table-actions--desktop-row{
|
||||||
|
display:flex;
|
||||||
|
flex-direction:row;
|
||||||
|
flex-wrap:nowrap;
|
||||||
|
align-items:center;
|
||||||
|
gap:0.45rem;
|
||||||
|
overflow:hidden;
|
||||||
|
}
|
||||||
|
.repository-table .table-actions--desktop-row .p-button{
|
||||||
|
flex:0 0 auto;
|
||||||
|
}
|
||||||
|
.repository-table .table-row-menu{
|
||||||
|
display:none;
|
||||||
|
position:relative;
|
||||||
|
}
|
||||||
|
.repository-table .table-row-menu summary{
|
||||||
|
display:inline-flex;
|
||||||
|
align-items:center;
|
||||||
|
gap:8px;
|
||||||
|
list-style:none;
|
||||||
|
cursor:pointer;
|
||||||
|
padding:8px 12px;
|
||||||
|
border-radius:12px;
|
||||||
|
border:1px solid color-mix(in srgb,var(--border-color) 82%, transparent);
|
||||||
|
background:color-mix(in srgb,var(--surface-2) 92%, transparent);
|
||||||
|
color:var(--text-main);
|
||||||
|
font-weight:700;
|
||||||
|
white-space:nowrap;
|
||||||
|
}
|
||||||
|
.repository-table .table-row-menu summary::-webkit-details-marker{display:none;}
|
||||||
|
.repository-table .table-row-menu__list{
|
||||||
|
position:absolute;
|
||||||
|
right:0;
|
||||||
|
top:calc(100% + 8px);
|
||||||
|
z-index:25;
|
||||||
|
display:grid;
|
||||||
|
gap:8px;
|
||||||
|
min-width:220px;
|
||||||
|
padding:10px;
|
||||||
|
border-radius:16px;
|
||||||
|
border:1px solid color-mix(in srgb,var(--border-color) 82%, transparent);
|
||||||
|
background:color-mix(in srgb,var(--surface-1) 96%, transparent);
|
||||||
|
box-shadow:var(--shadow-lg);
|
||||||
|
}
|
||||||
|
.repository-table .table-row-menu__list .p-button{width:100%;justify-content:center;}
|
||||||
|
|
||||||
|
@media (max-width: 1400px){
|
||||||
|
.repository-table--router-detail .table-actions--desktop-row,
|
||||||
|
.repository-table--diff .table-actions--desktop-row{
|
||||||
|
display:none;
|
||||||
|
}
|
||||||
|
.repository-table--router-detail .table-row-menu,
|
||||||
|
.repository-table--diff .table-row-menu{
|
||||||
|
display:inline-block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (max-width: 1520px){
|
||||||
|
.repository-table--files .table-actions--desktop-row{
|
||||||
|
display:none;
|
||||||
|
}
|
||||||
|
.repository-table--files .table-row-menu{
|
||||||
|
display:inline-block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (max-width: 960px){
|
||||||
|
.repository-table.app-table .p-datatable-thead > tr > th:last-child,
|
||||||
|
.repository-table.app-table .p-datatable-tbody > tr > td:last-child,
|
||||||
|
.repository-table--diff.app-table .p-datatable-thead > tr > th:last-child,
|
||||||
|
.repository-table--diff.app-table .p-datatable-tbody > tr > td:last-child{
|
||||||
|
width:auto;
|
||||||
|
min-width:0;
|
||||||
|
}
|
||||||
|
.repository-table .table-actions--desktop-row,
|
||||||
|
.repository-table .table-row-menu{
|
||||||
|
display:none !important;
|
||||||
|
}
|
||||||
|
.repository-table .table-primary--ellipsis,
|
||||||
|
.repository-table .table-secondary{
|
||||||
|
white-space:normal;
|
||||||
|
overflow:visible;
|
||||||
|
text-overflow:unset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* repository table desktop layout fix 2026-04-15 v4 */
|
||||||
|
.repository-table--files.app-table .p-datatable-thead > tr > th:nth-child(2),
|
||||||
|
.repository-table--files.app-table .p-datatable-tbody > tr > td:nth-child(2){
|
||||||
|
width: 34%;
|
||||||
|
max-width: 0;
|
||||||
|
}
|
||||||
|
.repository-table--files.app-table .p-datatable-thead > tr > th:nth-child(7),
|
||||||
|
.repository-table--files.app-table .p-datatable-tbody > tr > td:nth-child(7){
|
||||||
|
width: 230px;
|
||||||
|
min-width: 230px;
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
|
.repository-table--files.app-table .p-datatable-thead > tr > th:nth-child(8),
|
||||||
|
.repository-table--files.app-table .p-datatable-tbody > tr > td:nth-child(8){
|
||||||
|
width: 200px;
|
||||||
|
min-width: 200px;
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
|
.repository-table--files .table-actions--desktop-row{
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 0.45rem;
|
||||||
|
}
|
||||||
|
.repository-table--files .table-actions--desktop-row .p-button{
|
||||||
|
width: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.repository-table--files .table-primary--ellipsis,
|
||||||
|
.repository-table--files .table-secondary{
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
@media (max-width: 1520px){
|
||||||
|
.repository-table--files .table-actions--desktop-row{
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.repository-table--files .table-row-menu{
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (max-width: 960px){
|
||||||
|
.repository-table--files.app-table .p-datatable-thead > tr > th:nth-child(2),
|
||||||
|
.repository-table--files.app-table .p-datatable-tbody > tr > td:nth-child(2),
|
||||||
|
.repository-table--files.app-table .p-datatable-thead > tr > th:nth-child(7),
|
||||||
|
.repository-table--files.app-table .p-datatable-tbody > tr > td:nth-child(7),
|
||||||
|
.repository-table--files.app-table .p-datatable-thead > tr > th:nth-child(8),
|
||||||
|
.repository-table--files.app-table .p-datatable-tbody > tr > td:nth-child(8){
|
||||||
|
width: auto;
|
||||||
|
min-width: 0;
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user