feat: redesign backups management interface

- Complete UI/UX redesign of backups module
- Improved backup viewing and management
- Enhanced backup operations interface
- Better backup history and restoration
This commit is contained in:
sepehr 2025-10-16 17:34:19 +03:00
parent 069e53cec6
commit 07808822f7
4 changed files with 298 additions and 92 deletions

View file

@ -2,58 +2,97 @@
<c-col xs>
<c-card class="mb-4">
<c-card-header>
<c-row>
<c-col xs [lg]="8">
Backups <c-badge color="warning" *ngIf="devid!=0">Filtered Result For Device ID {{devid}}</c-badge>
<c-row class="align-items-center">
<c-col xs [lg]="6">
<div class="d-flex align-items-center">
<h5 class="mb-0 me-3"><i class="fa-solid fa-database me-2"></i>Backups</h5>
<c-badge color="warning" *ngIf="devid!=0" class="fs-6">
<i class="fa-solid fa-filter me-1"></i>Device ID {{devid}}
</c-badge>
</div>
</c-col>
<c-col xs [lg]="3">
<c-row>
<c-col>
<ng-container *ngIf="compareitems.length>0">
<div>
<c-badge color="dark" style="font-size: 0.7rem;"
*ngFor="let item of compareitems;index as i">{{item.id}}:{{item.devname}} {{item.createdC}} <span
style="cursor: pointer;" (click)="delete_compare(i)">X</span></c-badge>
<c-col xs [lg]="6">
<div class="d-flex justify-content-end align-items-center gap-2">
<!-- Compare Selection Panel -->
<div *ngIf="compareitems.length > 0" class="compare-panel me-2">
<div class="d-flex align-items-center gap-2">
<c-badge color="info" class="fs-6">
<i class="fa-solid fa-code-compare me-1"></i>{{compareitems.length}} Selected
</c-badge>
<div class="selected-items d-flex gap-1">
<c-badge color="secondary" *ngFor="let item of compareitems; index as i"
class="selected-item d-flex align-items-center">
<span class="me-1">{{item.devname}}</span>
<i class="fa-solid fa-times cursor-pointer" (click)="delete_compare(i)"
title="Remove from comparison"></i>
</c-badge>
</div>
</ng-container>
</c-col>
<c-col style="padding: 0;">
<button *ngIf="compareitems.length>1" (click)="start_compare()" cButton class="me-1"
color="primary">Compare</button>
</c-col>
</c-row>
</c-col>
<c-col styyle="border-left: 1px solid #ccc;" xs [lg]="1">
<button (click)="toggleCollapse()" cButton class="me-1" color="primary"><i
class="fa-solid fa-filter mr-1"></i>Filter</button>
<button *ngIf="compareitems.length > 1" (click)="start_compare()"
cButton color="success" size="sm">
<i class="fa-solid fa-code-compare me-1"></i>Compare
</button>
<button (click)="clearAllCompare()" cButton color="secondary" size="sm" variant="outline">
<i class="fa-solid fa-trash me-1"></i>Clear
</button>
</div>
</div>
<!-- Filter Toggle -->
<button (click)="toggleCollapse()" cButton color="primary" variant="outline">
<i class="fa-solid fa-filter me-1"></i>Filters
<i class="fa-solid" [class]="filters_visible ? 'fa-chevron-up' : 'fa-chevron-down'"
style="margin-left: 0.5rem; font-size: 0.8rem;"></i>
</button>
</div>
</c-col>
</c-row>
</c-card-header>
<c-card-body>
<c-row>
<div [visible]="filters_visible" cCollapse>
<c-col xs [lg]="12" class="example-form">
<mat-form-field>
<mat-label>Start date</mat-label>
<input matInput [matDatepicker]="picker1" (dateChange)="reinitgrid('start',$event)"
[(ngModel)]="filters['start_time']" />
<mat-datepicker-toggle matIconSuffix [for]="picker1"></mat-datepicker-toggle>
<mat-datepicker #picker1></mat-datepicker>
</mat-form-field>
<mat-form-field>
<mat-label>End date</mat-label>
<input matInput [matDatepicker]="picker2" (dateChange)="reinitgrid('end',$event)"
[(ngModel)]="filters['end_time']" />
<mat-datepicker-toggle matIconSuffix [for]="picker2"></mat-datepicker-toggle>
<mat-datepicker #picker2></mat-datepicker>
</mat-form-field>
<mat-form-field *ngIf="ispro">
<mat-label>Config search</mat-label>
<input (ngModelChange)="reinitgrid('search',$event)" [(ngModel)]="filters['search']" matInput>
</mat-form-field>
</c-col>
</div>
</c-row>
<!-- Enhanced Filter Panel -->
<div [visible]="filters_visible" cCollapse class="mb-3">
<c-card class="border-0 bg-light">
<c-card-body class="py-3">
<c-row class="g-3 align-items-end">
<c-col xs="12" md="4">
<div class="filter-group">
<label class="form-label fw-semibold mb-2">
<i class="fa-solid fa-calendar-days me-1 text-primary"></i>Start Date
</label>
<mat-form-field appearance="outline" class="w-100">
<input matInput [matDatepicker]="picker1" (dateChange)="reinitgrid('start',$event)"
[(ngModel)]="filters['start_time']" placeholder="Select start date" />
<mat-datepicker-toggle matIconSuffix [for]="picker1"></mat-datepicker-toggle>
<mat-datepicker #picker1></mat-datepicker>
</mat-form-field>
</div>
</c-col>
<c-col xs="12" md="4">
<div class="filter-group">
<label class="form-label fw-semibold mb-2">
<i class="fa-solid fa-calendar-days me-1 text-primary"></i>End Date
</label>
<mat-form-field appearance="outline" class="w-100">
<input matInput [matDatepicker]="picker2" (dateChange)="reinitgrid('end',$event)"
[(ngModel)]="filters['end_time']" placeholder="Select end date" />
<mat-datepicker-toggle matIconSuffix [for]="picker2"></mat-datepicker-toggle>
<mat-datepicker #picker2></mat-datepicker>
</mat-form-field>
</div>
</c-col>
<c-col xs="12" md="4" *ngIf="ispro">
<div class="filter-group">
<label class="form-label fw-semibold mb-2">
<i class="fa-solid fa-magnifying-glass me-1 text-primary"></i>Config Search
</label>
<mat-form-field appearance="outline" class="w-100">
<input (ngModelChange)="reinitgrid('search',$event)" [(ngModel)]="filters['search']"
matInput placeholder="Search in configurations...">
</mat-form-field>
</div>
</c-col>
</c-row>
</c-card-body>
</c-card>
</div>
<gui-grid [source]="source" [paging]="paging" [columnMenu]="columnMenu" [sorting]="sorting"
[infoPanel]="infoPanel" [columnMenu]="columnMenu" [sorting]="sorting" [infoPanel]="infoPanel"
[autoResizeWidth]=true>
@ -87,13 +126,29 @@
{{value}}
</ng-template>
</gui-grid-column>
<gui-grid-column header="Action" field="id">
<gui-grid-column header="Actions" field="id" width="280">
<ng-template let-value="item.id" let-item="item" let-index="index">
<button cButton [disabled]="backuploading" color="info" size="sm" (click)="ShowBackup(item)" class="mx-1">
<i *ngIf="backuploading" style="margin: 1px 5px;color:#ffffff;" class="fa-solid fa-spinner fa-spin"></i>
<i *ngIf="!backuploading" style="margin: 1px 5px;color:#ffffff;" class="fa-solid fa-eye"></i>Show backup</button>
<button *ngIf="ispro" cButton color="info" size="sm" (click)="add_for_compare(item)" class="mx-1"><i
style="margin: 1px 5px;color:#ffffff;" class="fa-solid fa-eye"></i>Compare</button>
<div class="d-flex gap-1">
<button cButton [disabled]="backuploading" color="primary" size="sm"
(click)="ShowBackup(item)" variant="outline" title="View backup content">
<i *ngIf="backuploading && currentBackup?.id === item.id"
class="fa-solid fa-spinner fa-spin me-1"></i>
<i *ngIf="!backuploading || currentBackup?.id !== item.id"
class="fa-solid fa-eye me-1"></i>
View
</button>
<button *ngIf="ispro" cButton color="info" size="sm" variant="outline"
(click)="add_for_compare(item)" title="Add to comparison"
[disabled]="isInCompareList(item)">
<i class="fa-solid fa-code-compare me-1"></i>
{{isInCompareList(item) ? 'Added' : 'Compare'}}
</button>
<button *ngIf="ispro" cButton color="warning" size="sm" variant="outline"
(click)="restore_backup(false, false, item)" title="Restore this backup">
<i class="fa-solid fa-rotate-left me-1"></i>
Restore
</button>
</div>
</ng-template>
</gui-grid-column>
</gui-grid>
@ -164,31 +219,80 @@
</c-modal-footer>
</c-modal>
<!-- Initial Restore Confirmation -->
<c-modal #ConfirmModal backdrop="static" [(visible)]="ConfirmModalVisible" id="runConfirmModal">
<c-modal-header>
<h6 cModalTitle>Please Confirm Action </h6>
<c-modal-header class="bg-warning text-dark">
<h5 cModalTitle><i class="fa-solid fa-exclamation-triangle me-2"></i>Confirm Backup Restore</h5>
<button [cModalToggle]="ConfirmModal.id" cButtonClose></button>
</c-modal-header>
<c-modal-body>
<span>restore backup ?</span>
<ng-container>
Are you sure that You want to <code style="padding: 0!important;">Restore this configuration</code> on
device?<br />
<hr>
<p class="text-danger">
All Current device configuration will be reset:<br /><br />
* All state data/history on router will be reset<br />
* All other local users on router will be deleted<br />
* After restore the password of the local user will be same as configured in MikroWizard<br />
</p>
</ng-container>
<c-modal-body class="p-4">
<div class="text-center mb-3">
<i class="fa-solid fa-rotate-left fa-3x text-warning mb-3"></i>
<h6>Restore Configuration Backup</h6>
</div>
<div class="backup-info bg-light p-3 rounded mb-3">
<h6 class="mb-2"><i class="fa-solid fa-info-circle me-2 text-primary"></i>Backup Details</h6>
<div class="row">
<div class="col-6"><strong>Device:</strong> {{currentBackup?.devname}}</div>
<div class="col-6"><strong>IP:</strong> {{currentBackup?.devip}}</div>
<div class="col-6"><strong>Date:</strong> {{currentBackup?.createdC}}</div>
<div class="col-6"><strong>Size:</strong> {{currentBackup?.filesize}}</div>
</div>
</div>
<c-alert color="danger" class="mb-0">
<h6 class="alert-heading"><i class="fa-solid fa-warning me-2"></i>Critical Warning</h6>
<p class="mb-2">This action will completely reset the device configuration:</p>
<ul class="mb-0">
<li>All current device configuration will be overwritten</li>
<li>All state data and history on router will be reset</li>
<li>All other local users on router will be deleted</li>
<li>Device will reboot and apply the restored configuration</li>
</ul>
</c-alert>
</c-modal-body>
<c-modal-footer>
<button *ngIf="ispro" (click)="restore_backup(true)" cButton color="info">
Restore this
<c-modal-footer class="d-flex justify-content-between">
<button cButton [cModalToggle]="ConfirmModal.id" color="secondary">
<i class="fa-solid fa-times me-1"></i>Cancel
</button>
<button cButton [cModalToggle]="ConfirmModal.id" color="info">
Cancel
<button *ngIf="ispro" (click)="restore_backup(true, false)" cButton color="warning">
<i class="fa-solid fa-arrow-right me-1"></i>Continue to Final Confirmation
</button>
</c-modal-footer>
</c-modal>
<!-- Critical Double Confirmation -->
<c-modal #CriticalConfirmModal backdrop="static" [(visible)]="CriticalConfirmModalVisible" id="CriticalConfirmModal">
<c-modal-header class="bg-danger text-white">
<h5 cModalTitle><i class="fa-solid fa-skull-crossbones me-2"></i>CRITICAL: Final Confirmation Required</h5>
</c-modal-header>
<c-modal-body class="p-4">
<div class="text-center mb-4">
<i class="fa-solid fa-exclamation-triangle fa-4x text-danger mb-3"></i>
<h5 class="text-danger">DESTRUCTIVE ACTION</h5>
<p class="mb-0">You are about to permanently overwrite device configuration</p>
</div>
<div class="confirmation-box bg-light border border-danger p-3 rounded">
<p class="mb-3 fw-bold">To proceed with this critical action, type <code>CONFIRM</code> in the box below:</p>
<input cFormControl [(ngModel)]="confirmationText" placeholder="Type CONFIRM to proceed"
class="form-control text-center fw-bold" style="letter-spacing: 2px;" />
</div>
<c-alert color="info" class="mt-3 mb-0">
<small><i class="fa-solid fa-info-circle me-1"></i>
This confirmation ensures you understand the critical nature of this operation.
</small>
</c-alert>
</c-modal-body>
<c-modal-footer class="d-flex justify-content-between">
<button (click)="cancelCriticalRestore()" cButton color="secondary">
<i class="fa-solid fa-times me-1"></i>Cancel
</button>
<button (click)="restore_backup(true, true)" cButton color="danger"
[disabled]="confirmationText !== 'CONFIRM'">
<i class="fa-solid fa-rotate-left me-1"></i>RESTORE BACKUP
</button>
</c-modal-footer>
</c-modal>

View file

@ -1,7 +1,72 @@
@import 'ngx-diff/styles/default-theme';
::ng-deep .modal-xl {
--cui-modal-width: 90vw!important;
}
::ng-deep pre {
display: block!important;
}
// Enhanced UI Styles
.compare-panel {
background: rgba(13, 110, 253, 0.1);
border: 1px solid rgba(13, 110, 253, 0.2);
border-radius: 0.375rem;
padding: 0.5rem;
}
.selected-item {
font-size: 0.75rem;
.fa-times {
font-size: 0.7rem;
opacity: 0.7;
transition: opacity 0.2s;
&:hover {
opacity: 1;
}
}
}
.cursor-pointer {
cursor: pointer;
}
.filter-group {
.form-label {
font-size: 0.875rem;
color: #495057;
}
::ng-deep .mat-mdc-form-field {
.mat-mdc-text-field-wrapper {
background-color: white;
}
}
}
.confirmation-box {
input {
font-size: 1.1rem;
&:focus {
border-color: #dc3545;
box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25);
}
}
}
// Action buttons spacing
.d-flex.gap-1 {
gap: 0.25rem !important;
}
// Backup info styling
.backup-info {
.row > div {
margin-bottom: 0.5rem;
font-size: 0.9rem;
}
}

View file

@ -30,11 +30,13 @@ export class BackupsComponent implements OnInit {
public codeForHighlightAuto: string = "";
public ispro: boolean = false;
public ConfirmModalVisible: boolean = false;
public CriticalConfirmModalVisible: boolean = false;
public CompareModalVisible: boolean = false;
public compareitems:any=[];
public comparecontents:any=[];
public compare_type="unified";
public copy_msg:boolean=false;
public confirmationText: string = '';
constructor(
private data_provider: dataProvider,
@ -186,31 +188,55 @@ export class BackupsComponent implements OnInit {
this.filters_visible = !this.filters_visible;
}
restore_backup(apply:boolean=false){
var _slef=this;
if (!apply){
restore_backup(apply: boolean = false, doubleConfirmed: boolean = false, backup?: any) {
var _self = this;
// Set current backup if provided
if (backup) {
this.currentBackup = backup;
}
if (!apply) {
// Step 1: Show initial confirmation
this.ConfirmModalVisible = true;
return;
}
if (!this.currentBackup)
if (!this.currentBackup) {
return;
if(apply){
_slef.ConfirmModalVisible = false;
_slef.BakcupModalVisible = true;
this.show_toast('Success', 'Backup restored successfully', 'success')
this.show_toast('Info', 'Wait for the router to reboot and apply config', 'info')
}
if (apply && !doubleConfirmed) {
// Step 2: Show critical confirmation
this.ConfirmModalVisible = false;
this.CriticalConfirmModalVisible = true;
this.confirmationText = '';
return;
}
if (apply && doubleConfirmed) {
// Step 3: Execute restore
_self.CriticalConfirmModalVisible = false;
_self.BakcupModalVisible = false;
this.data_provider.restore_backup(this.currentBackup.id).then((res) => {
if ('status' in res){
if(res['status']=='success'){
this.show_toast('Success', 'Backup restored successfully', 'success')
this.show_toast('Info', 'Wait for the router to reboot and apply config', 'info')
if ('status' in res) {
if (res['status'] == 'success') {
this.show_toast('Success', 'Backup restored successfully', 'success');
this.show_toast('Info', 'Wait for the router to reboot and apply config', 'info');
} else {
this.show_toast('Error', 'Error restoring backup', 'danger');
}
else
this.show_toast('Error', 'Error restoring backup', 'danger')
}
});
}
}
cancelCriticalRestore() {
this.CriticalConfirmModalVisible = false;
this.confirmationText = '';
this.currentBackup = null;
}
start_compare(){
var _self=this;
@ -243,10 +269,20 @@ export class BackupsComponent implements OnInit {
this.compareitems.push(item);
}
}
delete_compare(i:number){
//delete item index i from compareitems
this.compareitems.splice(i,1);
delete_compare(i: number) {
// Delete item index i from compareitems
this.compareitems.splice(i, 1);
}
clearAllCompare() {
// Clear all compare items
this.compareitems = [];
this.comparecontents = [];
}
isInCompareList(item: any): boolean {
// Check if item is already in compare list
return this.compareitems.some((compareItem: any) => compareItem.id === item.id);
}
reinitgrid(field: string, $event: any) {
if (field == "start") this.filters["start_time"] = $event.target.value;

View file

@ -12,6 +12,7 @@ import {
ModalModule,
FormModule,
ToastModule,
AlertModule,
} from "@coreui/angular";
import { BackupsRoutingModule } from "./backups-routing.module";
@ -34,10 +35,10 @@ import { ClipboardModule } from "@angular/cdk/clipboard";
FormModule,
FormsModule,
ButtonModule,
ButtonModule,
GuiGridModule,
CollapseModule,
BadgeModule,
AlertModule,
Highlight,
HighlightAuto,
HighlightLineNumbers,