Better backup list view and filtering

This commit is contained in:
sepehr 2024-07-31 17:48:24 +03:30
parent 7e977823c5
commit 6cc1060e36
8 changed files with 297 additions and 49 deletions

View file

@ -396,23 +396,22 @@ export class dataProvider {
return this.MikroWizardRPC.sendJsonRequest("/api/firmware/get_firms", data); return this.MikroWizardRPC.sendJsonRequest("/api/firmware/get_firms", data);
} }
get_backups(devid:Number=0,page:Number,size:Number,search:any) { get_backups(data:any) {
var data = {
'devid':devid,
'page':page,
'size':size,
'search':search
}
return this.MikroWizardRPC.sendJsonRequest("/api/backup/list", data); return this.MikroWizardRPC.sendJsonRequest("/api/backup/list", data);
} }
get_backup(id:number){ get_backup(id:number){
var data = { var data = {
'id':id 'id':id
} }
return this.MikroWizardRPC.sendJsonRequest("/api/backup/get", data); return this.MikroWizardRPC.sendJsonRequest("/api/backup/get", data);
} }
restore_backup(id:number){
var data = {
'backupid':id
}
return this.MikroWizardRPC.sendJsonRequest("/api/backup/restore", data);
}
get_downloadable_firms() { get_downloadable_firms() {

View file

@ -189,8 +189,6 @@ export class MikroWizardProvider {
return Promise.resolve(); return Promise.resolve();
} }
public getUserContext(context: any) { public getUserContext(context: any) {
localStorage.setItem("user_context", JSON.stringify(context)); localStorage.setItem("user_context", JSON.stringify(context));

View file

@ -12,7 +12,6 @@ import { AuthRoutingModule } from './auth-routing.module';
import { AuthComponent } from './auth.component'; import { AuthComponent } from './auth.component';
import { GuiGridModule } from '@generic-ui/ngx-grid'; import { GuiGridModule } from '@generic-ui/ngx-grid';
import { faCoffee } from '@fortawesome/free-solid-svg-icons';
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
import {MatDatepickerModule} from '@angular/material/datepicker'; import {MatDatepickerModule} from '@angular/material/datepicker';

View file

@ -1,13 +1,62 @@
<c-row> <c-row>
<c-col xs> <c-col xs>
<c-card class="mb-4"> <c-card class="mb-4">
<c-card-header>Backups</c-card-header> <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-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>
</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>
</c-col>
</c-row>
</c-card-header>
<c-card-body> <c-card-body>
<c-badge color="warning" *ngIf="devid!=0">Filtered Result For Device ID {{devid}}</c-badge> <c-row>
<div [visible]="filters_visible" cCollapse>
<gui-grid [source]="source" [searching]="searching" [paging]="paging" [columnMenu]="columnMenu" <c-col xs [lg]="12" class="example-form">
[sorting]="sorting" [infoPanel]="infoPanel" [columnMenu]="columnMenu" [sorting]="sorting" <mat-form-field>
[infoPanel]="infoPanel" [autoResizeWidth]=true> <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>
<gui-grid [source]="source" [paging]="paging" [columnMenu]="columnMenu" [sorting]="sorting"
[infoPanel]="infoPanel" [columnMenu]="columnMenu" [sorting]="sorting" [infoPanel]="infoPanel"
[autoResizeWidth]=true>
<gui-grid-column header="#No" type="NUMBER" field="index" width=25 align="CENTER"> <gui-grid-column header="#No" type="NUMBER" field="index" width=25 align="CENTER">
<ng-template let-value="item.index" let-item="item" let-index="index"> <ng-template let-value="item.index" let-item="item" let-index="index">
{{value}} {{value}}
@ -40,8 +89,10 @@
</gui-grid-column> </gui-grid-column>
<gui-grid-column header="Action" field="id"> <gui-grid-column header="Action" field="id">
<ng-template let-value="item.id" let-item="item" let-index="index"> <ng-template let-value="item.id" let-item="item" let-index="index">
<button cButton color="info" size="sm" (click)="ShowBackup(item.id)" class="mx-1"><i <button cButton color="info" size="sm" (click)="ShowBackup(item)" class="mx-1"><i
style="margin: 1px 5px;color:#ffffff;" class="fa-solid fa-eye"></i>Show backup</button> style="margin: 1px 5px;color:#ffffff;" class="fa-solid fa-eye"></i>Show backup</button>
<button *ngIf="ispro" 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>
</ng-template> </ng-template>
</gui-grid-column> </gui-grid-column>
</gui-grid> </gui-grid>
@ -50,19 +101,94 @@
</c-col> </c-col>
</c-row> </c-row>
<c-modal #BakcupModal backdrop="static" [(visible)]="BakcupModalVisible" id="BakcupModal" size="xl"> <c-modal #BakcupModal backdrop="static" [(visible)]="BakcupModalVisible" id="BakcupModal" [fullscreen]="true">
<c-modal-header> <c-modal-header>
<h6 cModalTitle>Please Confirm Action </h6> <h6 cModalTitle>Please Confirm Action </h6>
<button [cModalToggle]="BakcupModal.id" cButtonClose></button> <button [cModalToggle]="BakcupModal.id" cButtonClose></button>
</c-modal-header> </c-modal-header>
<c-modal-body> <c-modal-body>
<pre> <pre style="height: 100%;">
<code *ngIf="!loading" language="properties" style="height:70vh" [highlight]="codeForHighlightAuto" <code *ngIf="!loading" language="properties" style="height:70vh" [highlight]="codeForHighlightAuto"
lineNumbers></code></pre> lineNumbers></code></pre>
</c-modal-body> </c-modal-body>
<c-modal-footer style="justify-content: space-between;">
<button [cdkCopyToClipboard]="codeForHighlightAuto" [style.background-color]="copy_msg ? 'green' : null" (click)="copy_this()" cButton color="secondary">
<i class="fa-regular fa-copy"></i> To clipboard
</button>
<div>
<button (click)="restore_backup(false)" class=" mx-3" cButton color="danger">
Restore this
</button>
<button [cModalToggle]="BakcupModal.id" cButton color="info">
Close
</button>
</div>
</c-modal-footer>
</c-modal>
<c-modal #CompareModal backdrop="static" [(visible)]="CompareModalVisible" [fullscreen]="true" id="CompareModal">
<c-modal-header>
<h6 cModalTitle>Comparing Configs </h6>
<c-form-check (click)="switch_compare_type()" sizing="xl" class="mx-5" switch>
<h6>
<input cFormCheckInput [checked]="compare_type=='unified'"
style="width: 2.5rem;margin-left: -2.8em;cursor: pointer;" type="checkbox" />
<label cFormCheckLabel style="padding-top: calc((1.8em - 1rem) / 2);">
<span *ngIf="compare_type=='sided'">Sided compare</span>
<span *ngIf="compare_type=='unified'">Unified compare</span>
</label>
</h6>
</c-form-check>
<button [cModalToggle]="CompareModal.id" cButtonClose></button>
</c-modal-header>
<c-modal-body *ngIf="comparecontents.length>1">
<h5>
Comparing <c-badge color="dark" style="font-size: 0.8rem;">{{compareitems[0].id}}:{{compareitems[0].devname}}
{{compareitems[0].createdC}} </c-badge> With
<c-badge color="dark" style="font-size: 0.8rem;">{{compareitems[1].id}}:{{compareitems[1].devname}}
{{compareitems[1].createdC}} </c-badge>
</h5>
<ngx-unified-diff *ngIf="compare_type=='unified'" class="ngx-diff-light-theme" [before]="comparecontents[0]"
[after]="comparecontents[1]" />
<ngx-side-by-side-diff *ngIf="compare_type=='sided'" class="ngx-diff-light-theme" [before]="comparecontents[0]"
[after]="comparecontents[1]" />
</c-modal-body>
<c-modal-footer> <c-modal-footer>
<button [cModalToggle]="BakcupModal.id" cButton color="info"> <button [cModalToggle]="CompareModal.id" cButton color="info">
Close Close
</button> </button>
</c-modal-footer> </c-modal-footer>
</c-modal> </c-modal>
<c-modal #ConfirmModal backdrop="static" [(visible)]="ConfirmModalVisible" id="runConfirmModal">
<c-modal-header>
<h6 cModalTitle>Please Confirm Action </h6>
<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>
<c-modal-footer>
<button *ngIf="ispro" (click)="restore_backup(true)" cButton color="info">
Restore this
</button>
<button cButton [cModalToggle]="ConfirmModal.id" color="info">
Cancel
</button>
</c-modal-footer>
</c-modal>
<c-toaster position="fixed" placement="top-end"></c-toaster>

View file

@ -0,0 +1,4 @@
@import 'ngx-diff/styles/default-theme';
::ng-deep .modal-xl {
--cui-modal-width: 90vw!important;
}

View file

@ -1,4 +1,4 @@
import { Component, OnInit } from "@angular/core"; import { Component, OnInit, QueryList, ViewChildren } from "@angular/core";
import { dataProvider } from "../../providers/mikrowizard/data"; import { dataProvider } from "../../providers/mikrowizard/data";
import { Router, ActivatedRoute } from "@angular/router"; import { Router, ActivatedRoute } from "@angular/router";
import { loginChecker } from "../../providers/login_checker"; import { loginChecker } from "../../providers/login_checker";
@ -14,23 +14,12 @@ import {
GuiRowSelectionType, GuiRowSelectionType,
} from "@generic-ui/ngx-grid"; } from "@generic-ui/ngx-grid";
import { formatInTimeZone } from "date-fns-tz"; import { formatInTimeZone } from "date-fns-tz";
import { ToasterComponent } from "@coreui/angular";
interface IUser { import { AppToastComponent } from "../toast-simple/toast.component";
name: string;
state: string;
registered: string;
country: string;
usage: number;
period: string;
payment: string;
activity: string;
avatar: string;
status: string;
color: string;
}
@Component({ @Component({
templateUrl: "backups.component.html", templateUrl: "backups.component.html",
styleUrls: ["backups.component.scss"],
}) })
export class BackupsComponent implements OnInit { export class BackupsComponent implements OnInit {
public uid: number; public uid: number;
@ -39,7 +28,13 @@ export class BackupsComponent implements OnInit {
public filterText: string; public filterText: string;
public filters: any = {}; public filters: any = {};
public codeForHighlightAuto: string = ""; public codeForHighlightAuto: string = "";
public ispro: boolean = false;
public ConfirmModalVisible: boolean = false;
public CompareModalVisible: boolean = false;
public compareitems:any=[];
public comparecontents:any=[];
public compare_type="unified";
public copy_msg:boolean=false;
constructor( constructor(
private data_provider: dataProvider, private data_provider: dataProvider,
private router: Router, private router: Router,
@ -56,6 +51,8 @@ export class BackupsComponent implements OnInit {
_self.uid = res.uid; _self.uid = res.uid;
_self.uname = res.name; _self.uname = res.name;
_self.tz = res.tz; _self.tz = res.tz;
_self.ispro = res['ISPRO']
const userId = _self.uid; const userId = _self.uid;
if (res.role != "admin") { if (res.role != "admin") {
@ -69,6 +66,7 @@ export class BackupsComponent implements OnInit {
return value !== undefined && value !== null && value !== ""; return value !== undefined && value !== null && value !== "";
} }
} }
@ViewChildren(ToasterComponent) viewChildren!: QueryList<ToasterComponent>;
public source: Array<any> = []; public source: Array<any> = [];
public columns: Array<GuiColumn> = []; public columns: Array<GuiColumn> = [];
@ -77,7 +75,8 @@ export class BackupsComponent implements OnInit {
public Selectedrows: any; public Selectedrows: any;
public BakcupModalVisible: boolean = false; public BakcupModalVisible: boolean = false;
public devid: number = 0; public devid: number = 0;
public filters_visible: boolean = false;
public currentBackup:any=false;
public sorting = { public sorting = {
enabled: true, enabled: true,
multiSorting: true, multiSorting: true,
@ -94,6 +93,14 @@ export class BackupsComponent implements OnInit {
display: GuiPagingDisplay.ADVANCED, display: GuiPagingDisplay.ADVANCED,
}; };
toasterForm = {
autohide: true,
delay: 3000,
position: "fixed",
fade: true,
closeButton: true,
};
public columnMenu: GuiColumnMenu = { public columnMenu: GuiColumnMenu = {
enabled: true, enabled: true,
sort: true, sort: true,
@ -115,25 +122,124 @@ export class BackupsComponent implements OnInit {
ngOnInit(): void { ngOnInit(): void {
this.devid = Number(this.route.snapshot.paramMap.get("devid")); this.devid = Number(this.route.snapshot.paramMap.get("devid"));
if (this.devid > 0) {
this.filters["devid"] = this.devid;
}
this.initGridTable(); this.initGridTable();
} }
logger(item: any) { logger(item: any) {
console.dir(item); console.dir(item);
} }
switch_compare_type(){
if(this.compare_type=='unified')
this.compare_type='sided'
else
this.compare_type='unified'
}
copy_this() {
//show text copy to clipboard for 3 seconds
this.copy_msg = true;
setTimeout(() => {
this.copy_msg = false;
}, 1000);
}
show_toast(title: string, body: string, color: string) {
const { ...props } = { ...this.toasterForm, color, title, body };
const componentRef = this.viewChildren.first.addToast(
AppToastComponent,
props,
{}
);
componentRef.instance["closeButton"] = props.closeButton;
}
ShowBackup(id: number) { ShowBackup(backup: any) {
this.BakcupModalVisible = true; var _self=this;
this.loading = true; this.loading = true;
this.data_provider.get_backup(id).then((res) => { this.currentBackup = backup;
this.codeForHighlightAuto = res.content; this.data_provider.get_backup(backup.id).then((res) => {
this.loading = false; if('content' in res){
_self.codeForHighlightAuto = res.content;
_self.loading = false;
_self.BakcupModalVisible = true;
}
else{
this.show_toast('Error', 'Error loading backup file', 'danger')
}
}); });
} }
toggleCollapse(): void {
this.filters_visible = !this.filters_visible;
}
restore_backup(apply:boolean=false){
var _slef=this;
if (!apply){
this.ConfirmModalVisible = true;
return;
}
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')
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')
}
else
this.show_toast('Error', 'Error restoring backup', 'danger')
}
});
}
}
start_compare(){
var _self=this;
this.comparecontents=[]
this.compareitems.forEach((element:any) => {
_self.data_provider.get_backup(element.id).then((res) => {
if('content' in res){
_self.comparecontents.push(res.content);
}
if(_self.comparecontents.length==_self.compareitems.length)
_self.CompareModalVisible=true;
});
});
}
add_for_compare(item:any){
//Only two items for compare
if(this.compareitems.length<2)
this.compareitems.filter((i:any)=>{
return i.id!=item.id;
}).length==this.compareitems.length && this.compareitems.push(item);
else{
//remove first element and add new item
this.compareitems.shift();
this.compareitems.push(item);
}
}
delete_compare(i:number){
//delete item index i from compareitems
this.compareitems.splice(i,1);
}
reinitgrid(field: string, $event: any) {
if (field == "start") this.filters["start_time"] = $event.target.value;
else if (field == "end") this.filters["end_time"] = $event.target.value;
else if (field == "search") this.filters["search"] = $event;
this.initGridTable();
}
initGridTable(): void { initGridTable(): void {
var _self=this; var _self=this;
this.data_provider.get_backups(this.devid, 0, 0, false).then((res) => { this.data_provider.get_backups(this.filters).then((res) => {
let index = 1; let index = 1;
this.source = res.map((d: any) => { this.source = res.map((d: any) => {
d.index = index; d.index = index;
@ -142,11 +248,9 @@ export class BackupsComponent implements OnInit {
_self.tz, _self.tz,
"yyyy-MM-dd HH:mm:ss XXX" "yyyy-MM-dd HH:mm:ss XXX"
); );
// d.created = [d.created.split("T")[0],d.created.split("T")[1].split(".")[0]].join(" ")
index += 1; index += 1;
return d; return d;
}); });
console.dir(this.source);
this.loading = false; this.loading = false;
}); });
} }

View file

@ -10,10 +10,19 @@ import {
CollapseModule, CollapseModule,
BadgeModule, BadgeModule,
ModalModule, ModalModule,
FormModule,
ToastModule,
} from "@coreui/angular"; } from "@coreui/angular";
import { BackupsRoutingModule } from "./backups-routing.module"; import { BackupsRoutingModule } from "./backups-routing.module";
import { BackupsComponent } from "./backups.component"; import { BackupsComponent } from "./backups.component";
import { GuiGridModule } from "@generic-ui/ngx-grid"; import { GuiGridModule } from "@generic-ui/ngx-grid";
import { MatDatepickerModule } from "@angular/material/datepicker";
import { MatInputModule } from "@angular/material/input";
import { MatFormFieldModule } from "@angular/material/form-field";
import { MatSelectModule } from "@angular/material/select";
import { FormsModule } from "@angular/forms";
import { UnifiedDiffComponent,SideBySideDiffComponent } from 'ngx-diff';
import { ClipboardModule } from "@angular/cdk/clipboard";
@NgModule({ @NgModule({
imports: [ imports: [
@ -21,6 +30,8 @@ import { GuiGridModule } from "@generic-ui/ngx-grid";
CardModule, CardModule,
CommonModule, CommonModule,
GridModule, GridModule,
FormModule,
FormsModule,
ButtonModule, ButtonModule,
ButtonModule, ButtonModule,
GuiGridModule, GuiGridModule,
@ -30,6 +41,14 @@ import { GuiGridModule } from "@generic-ui/ngx-grid";
HighlightAuto, HighlightAuto,
HighlightLineNumbers, HighlightLineNumbers,
ModalModule, ModalModule,
MatFormFieldModule,
MatInputModule,
MatDatepickerModule,
MatSelectModule,
UnifiedDiffComponent,
SideBySideDiffComponent,
ToastModule,
ClipboardModule
], ],
declarations: [BackupsComponent], declarations: [BackupsComponent],
}) })

View file

@ -57,7 +57,6 @@
</c-row> </c-row>
<c-modal-header> <c-modal-header>
<c-modal #EditTaskModal backdrop="static" size="xl" [(visible)]="EditTaskModalVisible" id="EditTaskModal"> <c-modal #EditTaskModal backdrop="static" size="xl" [(visible)]="EditTaskModalVisible" id="EditTaskModal">
<c-modal-header> <c-modal-header>
<h5 *ngIf="SelectedTask['action']=='edit'" cModalTitle>Editing device {{SelectedTask['name']}}</h5> <h5 *ngIf="SelectedTask['action']=='edit'" cModalTitle>Editing device {{SelectedTask['name']}}</h5>
@ -88,7 +87,7 @@
</select> </select>
</c-input-group> </c-input-group>
<h6 *ngIf="SelectedTask['task_type']=='firmware'" >Update Version Strategy</h6> <h6 *ngIf="SelectedTask['task_type']=='firmware'" >Update Version Strategy</h6>
<c-card *ngIf="SelectedTask['task_type']=='firmware'"> <c-card *ngIf="SelectedTask['task_type']=='firmware'">
<c-card-header style="padding: 0;"> <c-card-header style="padding: 0;">
<c-input-group> <c-input-group>
<c-button-group aria-label="Basic radio toggle button group" <c-button-group aria-label="Basic radio toggle button group"