From 95d84faf176abef885c40acb631e78bb0a7f4c9a Mon Sep 17 00:00:00 2001 From: sepehr Date: Thu, 2 Jan 2025 20:20:59 +0300 Subject: [PATCH 01/15] Bugs: Fixed Firmware download from the Mikrotik website when there are multiple npk available Fixed Mikrowizard system permission error when it is set to None Fixed user device group permissions Some minor UI improvements Fix IP scan for one IP scan / Fix not scanning the last IP in the range Fix manual snippet execution not working when device groups are selected Some minor bug fixes and improvements New: Show background tasks and be able to stop them while running in the background (like an IP scanner) Add support for manual MikroWizard update dashboard/settings page update to version 1.0.5 Enhancement: Show permission error in some pages when the user doesn't have permission for that page/action show better charts/graphs in the dashboard and device interface details show more info on the dashboard about update and version information and license --- src/app/app.module.ts | 4 +- .../default-header.component.html | 43 +++++++ .../default-header.component.ts | 30 ++++- .../default-layout.component.html | 32 +++++- .../default-layout.component.ts | 68 ++++++++++-- src/app/providers/mikrowizard/data.ts | 21 +++- .../views/dashboard/dashboard.component.html | 105 +++++++++++++++--- .../views/dashboard/dashboard.component.ts | 48 ++++++-- src/app/views/dashboard/dashboard.module.ts | 2 + .../views/device_detail/device.component.ts | 27 ++++- src/app/views/devices/devices.component.html | 7 +- src/app/views/devices/devices.component.ts | 93 +++++++++++++++- .../permissions/permissions.component.ts | 19 +++- .../views/settings/settings.component.html | 42 ++++--- .../views/settings/settings.component.scss | 3 + src/app/views/settings/settings.component.ts | 65 ++++++++++- .../views/snippets/snippets.component.html | 3 +- .../user_manager/user_manager.component.ts | 84 +++++++++++++- src/app/views/vault/vault.component.ts | 27 +++++ 19 files changed, 647 insertions(+), 76 deletions(-) diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 3160f40..bb74798 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -40,6 +40,7 @@ import { SidebarModule, TabsModule, UtilitiesModule, + TableModule, ModalModule } from '@coreui/angular'; @@ -85,7 +86,8 @@ export function loginStatusProviderFactory(provider: loginChecker) { CardModule, NgScrollbarModule, ModalModule, - FontAwesomeModule + FontAwesomeModule, + TableModule ], providers: [ { diff --git a/src/app/containers/default-layout/default-header/default-header.component.html b/src/app/containers/default-layout/default-header/default-header.component.html index adefb87..b83982a 100644 --- a/src/app/containers/default-layout/default-header/default-header.component.html +++ b/src/app/containers/default-layout/default-header/default-header.component.html @@ -49,6 +49,7 @@ --> + | @@ -97,3 +98,45 @@ + + + + +
    +
  • +
    Runnig Background Tasks
    +
  • +
  • + + + + + + + + + + + + + + +
    Task name
    {{i+1}}{{task.name}} + + +
    +
  • +
+
+
+ + diff --git a/src/app/containers/default-layout/default-header/default-header.component.ts b/src/app/containers/default-layout/default-header/default-header.component.ts index e957c15..cd6c204 100644 --- a/src/app/containers/default-layout/default-header/default-header.component.ts +++ b/src/app/containers/default-layout/default-header/default-header.component.ts @@ -15,6 +15,7 @@ export class DefaultHeaderComponent extends HeaderComponent { @Input() sidebarId: string = "sidebar"; @Output() UserModalEvent = new EventEmitter(); + @Output() ConfirmModalEvent = new EventEmitter(); public newMessages = new Array(4) public newTasks = new Array(5) @@ -25,7 +26,9 @@ export class DefaultHeaderComponent extends HeaderComponent { public uname: string; public fname: string; public lname: string; - public UserProfileModalVisible : boolean = false; + public ConfirmModalVisible : boolean = false; + public tasks : any = []; + public timer : any; constructor( private classToggler: ClassToggleService, @@ -53,7 +56,9 @@ export class DefaultHeaderComponent extends HeaderComponent { callParent(action:string): void { this.UserModalEvent.next(action); } - + callParentConfirm(action:string,data:any): void { + this.ConfirmModalEvent.next({action:action,data:data}); + } logout() { this.data_provider.logout().then(res => { this.router.navigate(['login']); @@ -62,6 +67,27 @@ export class DefaultHeaderComponent extends HeaderComponent { ngOnInit(): void { var _self = this; + console.log('DefaultHeaderComponent'); this.get_user_info(); + this.data_provider.get_running_tasks().then(res => { + _self.tasks = res['tasks'].filter((x:any) => x.status); + }) + // get running tasks every 5 seconds + this.timer=setInterval(function(){ + _self.get_running_tasks(); + }, 5000); } + + get_running_tasks(){ + var _self = this; + this.data_provider.get_running_tasks().then(res => { + _self.tasks = res['tasks'].filter((x:any) => x.status); + }) + } + ngOnDestroy(): void { + clearInterval(this.timer); + } + + + } diff --git a/src/app/containers/default-layout/default-layout.component.html b/src/app/containers/default-layout/default-layout.component.html index ea53bfd..83af2f0 100644 --- a/src/app/containers/default-layout/default-layout.component.html +++ b/src/app/containers/default-layout/default-layout.component.html @@ -21,7 +21,7 @@
-
@@ -105,4 +105,34 @@ Close + + + + + +
Please Confirm Stopping Background Task
+
Please Confirm Stopping Background Task
+ +
+ +
+

Are you sure you want to stop the task {{ data['name'] }}?

+
+
+ Stopping this task will cause reload of other background tasks +
+
+ Clear browser cache and Reload the page or hit Ctrl+F5 to load latest Mikrofront version +
+
+ + + + + + +
\ No newline at end of file diff --git a/src/app/containers/default-layout/default-layout.component.ts b/src/app/containers/default-layout/default-layout.component.ts index 67c1d09..8830ab9 100644 --- a/src/app/containers/default-layout/default-layout.component.ts +++ b/src/app/containers/default-layout/default-layout.component.ts @@ -6,6 +6,7 @@ import { navItems } from './_nav'; import { dataProvider } from '../../providers/mikrowizard/data'; import { arch } from 'os'; import { DomSanitizer } from '@angular/platform-browser'; +import { disconnect } from 'process'; @Component({ selector: 'app-dashboard', @@ -23,11 +24,14 @@ export class DefaultLayoutComponent implements OnInit { public ispro: boolean=false; public action: string="password"; public UserProfileModalVisible:boolean; + public ConfirmModalVisible:boolean; public error:any=false; public currentStep:number=1; public qrCode:any=false; public totpCode:string=''; public errorMessage:any=false; + public data:any={}; + public timer:any; public password:any={ 'cupass':'', 'pass1':'', @@ -130,7 +134,41 @@ export class DefaultLayoutComponent implements OnInit { else this.UserProfileModalVisible = true; } - + show_confirm_modal(data:any){ + this.data={}; + if (data.action=='CancelTask'){ + this.action=data.action; + this.data=data.data; + console.dir(this.data); + console.dir(this.action); + //disable submit button + this.data['SubmitDisable']=false; + this.ConfirmModalVisible = true; + } + if (data.action=='update'){ + this.action='update'; + this.data={}; + this.ConfirmModalVisible = true; + } + } + ConfirmAction(){ + var _self=this; + if(this.action=='CancelTask'){ + this.data_provider.stop_task(this.data['signal']).then(res => { + //disable submit button + if(res['status']=='success'){ + setTimeout(function () { + _self.ConfirmModalVisible = false; + }, 5000); + } + this.data['SubmitDisable']=true; + //wait 5 seconds before hiding the modal + }) + } + if(this.action=='update'){ + window.location.href = window.location.href.replace(/#.*$/, '') + } + } submit(){ var _self=this; if(!_self.passvalid['pass2']){ @@ -190,12 +228,28 @@ export class DefaultLayoutComponent implements OnInit { } }); }); - _self.data_provider.get_front_version().then((res:any) => { - if(res['version']!=this.version){ - console.dir("New version is available. Please refresh the page."); - window.location.href = window.location.href.replace(/#.*$/, ''); - } - }); + // check first time after 10 seconds + setTimeout(function(){ + _self.data_provider.get_front_version().then((res:any) => { + if(res['version']!=_self.version){ + console.log("New version is available. Please refresh the page."); + _self.show_confirm_modal({action:'update'}); + // window.location.href = window.location.href.replace(/#.*$/, ''); + } + }); + }, 10000); + // check for new version every 5 seconds + this.timer=setInterval(function(){ + _self.data_provider.get_front_version().then((res:any) => { + if(res['version']!=_self.version){ + console.log("New version is available. Please refresh the page."); + _self.show_confirm_modal({action:'update'}); + // window.location.href = window.location.href.replace(/#.*$/, ''); + } + }); + }, 60000); + } + clearTimer() { clearInterval(this.timer); } } diff --git a/src/app/providers/mikrowizard/data.ts b/src/app/providers/mikrowizard/data.ts index 8553378..b80fbb0 100644 --- a/src/app/providers/mikrowizard/data.ts +++ b/src/app/providers/mikrowizard/data.ts @@ -70,9 +70,10 @@ export class dataProvider { } return this.MikroWizardRPC.sendJsonRequest("/api/user/change_password", data); } - dashboard_stats(versioncheck:boolean){ + dashboard_stats(versioncheck:boolean,front_version:string){ var data={ - 'versioncheck':versioncheck + 'versioncheck':versioncheck, + 'front_version':front_version } return this.MikroWizardRPC.sendJsonRequest("/api/dashboard/stats", data); } @@ -537,7 +538,21 @@ export class dataProvider { return this.MikroWizardRPC.sendJsonRequest("/api/sysconfig/save_all", data); } - + get_running_tasks(){ + return this.MikroWizardRPC.sendJsonRequest("/api/tasks/list", {}); + } + stop_task(signal:number){ + var data={ + 'signal':signal + } + return this.MikroWizardRPC.sendJsonRequest("/api/tasks/stop", data); + } + apply_update(action:string){ + var data={ + 'action':action + } + return this.MikroWizardRPC.sendJsonRequest("/api/sysconfig/apply_update", data); + } //// //// End api funcs //// diff --git a/src/app/views/dashboard/dashboard.component.html b/src/app/views/dashboard/dashboard.component.html index 2dcc8a7..fb8a721 100644 --- a/src/app/views/dashboard/dashboard.component.html +++ b/src/app/views/dashboard/dashboard.component.html @@ -125,29 +125,72 @@ - - + +
- Serial: {{ stats['serial'] }} - Copy +

Version and Serial information

+
- Not Registred OR Not internet access +
+ Serial: {{ stats['serial'] }} + Copy +
+ Not Registred Learn how to register and get automatic updates!
-
+
+ Unable connect to server/Check server internet connection +
+
+
+ Serial: {{ stats['serial'] }} + Copy +
Registred License Type : {{stats['license']}} + Manual update + Auto update
- +
+ Your Mikroman version : {{stats['version']}} + + + + + Your Mikrofront version : {{front_version}} + + + + +
+ + - - - + + @@ -165,4 +208,38 @@ - \ No newline at end of file + + + + +
Please Confirm Mikroman Update
+
Please Confirm MikroFront Update
+ +
+ +
+

Are you sure you want to apply latest Mikroman Update ver {{ stats['latest_version'] }}?

+

By updating Mikroman the MikroFront update is also get checked and applyed

+

If you made any special changes to configuration files or python files it will be removed automaticlaly

+
+
+ Applying the update will cause reload of the server couple of times +
+
+

Are you sure you want to apply latest MikroFront Update ver {{ stats['front_latest_version'] }}?

+
+
+ Applying the update will cause reload of the page,
+ Also please make sure you have the latest Mikroman before updating MikroFront. + Updating to latest MikroFront without getting latest Mikroman can cause problems
+
+
+ + + + +
\ No newline at end of file diff --git a/src/app/views/dashboard/dashboard.component.ts b/src/app/views/dashboard/dashboard.component.ts index 3d2099c..f5cc1a7 100644 --- a/src/app/views/dashboard/dashboard.component.ts +++ b/src/app/views/dashboard/dashboard.component.ts @@ -14,6 +14,9 @@ export class DashboardComponent implements OnInit { public uname: string; public tz: string; public copy_msg: any = false; + public ConfirmModalVisible: boolean = false; + public action: string = ""; + front_version=require('../../../../package.json').version; constructor( private data_provider: dataProvider, private router: Router, @@ -41,7 +44,9 @@ export class DashboardComponent implements OnInit { trafficRadio: new UntypedFormControl("5m"), }); public chart_data: any = {}; - Chartoptions = { + + public Chartoptions = { + responsive: true, plugins: { tooltip: { callbacks: { @@ -87,17 +92,17 @@ export class DashboardComponent implements OnInit { stacked: true, position: "left", type: "linear", - color: "#17522f", + color: "#4caf50", grid: { - color: "rgba(23, 82, 47, 0.3)", + color: "rgba(76, 175, 79, 0.3)", backgroundColor: "transparent", - borderColor: "#f86c6b", - pointHoverBackgroundColor: "#f86c6b", + borderColor: "#4caf50", + pointHoverBackgroundColor: "#4caf50", borderWidth: 1, borderDash: [8, 5], }, ticks: { - color: "#17522f", + color: "#000000", callback: function (value: any, index: any, ticks: any) { const units = ["bit", "Kib", "Mib", "Gib", "Tib"]; var res = value; @@ -119,10 +124,10 @@ export class DashboardComponent implements OnInit { position: "right", type: "linear", grid: { - color: "rgba(23, 82, 47, 0.3)", + color: "rgba(255, 152, 0, 0.4)", backgroundColor: "transparent", - borderColor: "#f86c6b", - pointHoverBackgroundColor: "#f86c6b", + borderColor: "#ff9800", + pointHoverBackgroundColor: "#ff9800", borderWidth: 1, borderDash: [8, 5], }, @@ -130,7 +135,7 @@ export class DashboardComponent implements OnInit { width: 2, }, ticks: { - color: "#171951", + color: "#000000", callback: function (value: any, index: any, ticks: any) { const units = ["bit", "Kib", "Mib", "Gib", "Tib"]; var res = value; @@ -147,7 +152,7 @@ export class DashboardComponent implements OnInit { elements: { line: { borderWidth: 1, - tension: 0.4, + tension: 0.1, }, point: { radius: 2, @@ -181,7 +186,7 @@ export class DashboardComponent implements OnInit { } initStats() { var _self = this; - this.data_provider.dashboard_stats(true).then((res) => { + this.data_provider.dashboard_stats(true,this.front_version).then((res) => { _self.stats = res; }); } @@ -200,4 +205,23 @@ export class DashboardComponent implements OnInit { this.delta = value; this.initTrafficChart(); } + showConfirmModal(action: string) { + this.action = action; + this.ConfirmModalVisible = true + } + ConfirmAction() { + var _self = this; + this.data_provider.apply_update(this.action).then((res) => { + if (res["status"]=='success') { + if (_self.action=='update_mikroman') { + _self.stats['update_inprogress']=true; + } + if (_self.action=='update_mikrofront') { + _self.stats['front_update_inprogress']=true; + } + _self.action=""; + _self.ConfirmModalVisible = false; + } + }); + } } diff --git a/src/app/views/dashboard/dashboard.module.ts b/src/app/views/dashboard/dashboard.module.ts index c605424..20a6600 100644 --- a/src/app/views/dashboard/dashboard.module.ts +++ b/src/app/views/dashboard/dashboard.module.ts @@ -11,6 +11,7 @@ import { ProgressModule, TemplateIdDirective, BadgeModule, + ModalModule, CarouselModule, } from "@coreui/angular"; @@ -37,6 +38,7 @@ import { ClipboardModule } from "@angular/cdk/clipboard"; CarouselModule, BadgeModule, ClipboardModule, + ModalModule, ], declarations: [DashboardComponent], }) diff --git a/src/app/views/device_detail/device.component.ts b/src/app/views/device_detail/device.component.ts index d77c1e4..64ab631 100644 --- a/src/app/views/device_detail/device.component.ts +++ b/src/app/views/device_detail/device.component.ts @@ -123,8 +123,9 @@ export class DeviceComponent implements OnInit, OnDestroy { type: GuiRowSelectionType.CHECKBOX, mode: GuiRowSelectionMode.MULTIPLE, }; - Chartoptions = { + responsive: true, + _self :this, plugins: { tooltip: { callbacks: { @@ -164,7 +165,19 @@ export class DeviceComponent implements OnInit, OnDestroy { }, maintainAspectRatio: true, scales: { - x: { display: false }, + x: { + + title: { + display: true, + text: 'Time', + color: '#333', + }, + ticks: { + autoSkip: true, + maxTicksLimit: 30, + color: '#333', + } + }, yA: { display: true, stacked: true, @@ -177,7 +190,9 @@ export class DeviceComponent implements OnInit, OnDestroy { }, ticks: { color: "#17522f", - callback: function (value: any, index: any, ticks: any) { + callback: (value: any) => { + if(this.total_type=="pps") + return value + " pps"; const units = ["bit", "Kib", "Mib", "Gib", "Tib"]; var res = value; let unitIndex = 0; @@ -206,7 +221,9 @@ export class DeviceComponent implements OnInit, OnDestroy { }, ticks: { color: "#171951", - callback: function (value: any, index: any, ticks: any) { + callback: (value: any) =>{ + if(this.total_type=="pps") + return value + " pps"; const units = ["bit", "Kib", "Mib", "Gib", "Tib"]; var res = value; let unitIndex = 0; @@ -222,7 +239,7 @@ export class DeviceComponent implements OnInit, OnDestroy { elements: { line: { borderWidth: 1, - tension: 0.4, + tension: 0.1, }, point: { radius: 4, diff --git a/src/app/views/devices/devices.component.html b/src/app/views/devices/devices.component.html index 5a8cf61..cfc5f75 100644 --- a/src/app/views/devices/devices.component.html +++ b/src/app/views/devices/devices.component.html @@ -265,12 +265,11 @@ -
Editing Group
+
Scan History :
-
Group Members :
  {{value}} - - + +   {{value}} diff --git a/src/app/views/devices/devices.component.ts b/src/app/views/devices/devices.component.ts index 8ce0f7f..edb012b 100644 --- a/src/app/views/devices/devices.component.ts +++ b/src/app/views/devices/devices.component.ts @@ -194,7 +194,7 @@ export class DevicesComponent implements OnInit, OnDestroy { this.selected_device = dev; this.data_provider.get_editform(dev.id).then((res) => { if ("error" in res) { - if (res.error.indexOf("Unauthorized")) { + if ("error" in res && res.error.indexOf("Unauthorized")) { _self.show_toast( "Error", "You are not authorized to perform this action", @@ -230,8 +230,17 @@ export class DevicesComponent implements OnInit, OnDestroy { var _self = this; this.ConfirmModalVisible = false; this.data_provider.delete_devices(this.Selectedrows).then((res) => { + if ("error" in res && res.error.indexOf("Unauthorized")) { + _self.show_toast( + "Error", + "You are not authorized to perform this action", + "danger" + ); + } + else{ _self.show_toast("Success", "Device Deleted", "success"); this.initGridTable(); + } }); } @@ -314,6 +323,14 @@ export class DevicesComponent implements OnInit, OnDestroy { }); } } + if ("error" in res && res.error.indexOf("Unauthorized")) { + _self.show_toast( + "Error", + "You are not authorized to perform this action", + "danger" + ); + } + else _self.scanwizard_step = step; }); } @@ -357,11 +374,20 @@ export class DevicesComponent implements OnInit, OnDestroy { this.data_provider .check_firmware(this.Selectedrows.toString()) .then((res) => { + if ("error" in res && res.error.indexOf("Unauthorized")) { + _self.show_toast( + "Error", + "You are not authorized to perform this action", + "danger" + ); + } + else{ _self.show_toast("info", "Checking Firmwares", "light"); _self.ConfirmModalVisible = false; setTimeout(function () { if (_self.Selectedrows.length < 1) _self.initGridTable(); }, 1); + } }); } @@ -370,8 +396,16 @@ export class DevicesComponent implements OnInit, OnDestroy { this.data_provider .update_firmware(this.Selectedrows.toString()) .then((res) => { + if ("error" in res && res.error.indexOf("Unauthorized")) { + _self.show_toast( + "Error", + "You are not authorized to perform this action", + "danger" + ); + } + else{ _self.show_toast("info", "Updating Firmwares Sent", "light"); - _self.initGridTable(); + _self.initGridTable();} }); } @@ -380,8 +414,17 @@ export class DevicesComponent implements OnInit, OnDestroy { this.data_provider .upgrade_firmware(this.Selectedrows.toString()) .then((res) => { + if ("error" in res && res.error.indexOf("Unauthorized")) { + _self.show_toast( + "Error", + "You are not authorized to perform this action", + "danger" + ); + } + else{ _self.show_toast("info", "Upgrading Firmwares", "light"); _self.initGridTable(); + } }); } @@ -390,19 +433,37 @@ export class DevicesComponent implements OnInit, OnDestroy { this.data_provider .reboot_devices(this.Selectedrows.toString()) .then((res) => { + if ("error" in res && res.error.indexOf("Unauthorized")) { + _self.show_toast( + "Error", + "You are not authorized to perform this action", + "danger" + ); + } + else{ _self.show_toast("info", "Reboot sent", "light"); _self.ConfirmModalVisible = !_self.ConfirmModalVisible; _self.initGridTable(); + } }); } get_groups() { var _self = this; this.data_provider.get_devgroup_list().then((res) => { + if ("error" in res && res.error.indexOf("Unauthorized")) { + _self.show_toast( + "Error", + "You are not authorized to perform this action", + "danger" + ); + } + else{ if( "status" in res && res.status == 'failed' ) _self.groups = false else _self.groups = res; + } }); } @@ -417,6 +478,14 @@ export class DevicesComponent implements OnInit, OnDestroy { }; _self.data_provider.get_dev_list(data).then((res) => { + if ("error" in res && res.error.indexOf("Unauthorized")) { + _self.show_toast( + "Error", + "You are not authorized to perform this action", + "danger" + ); + } + else{ _self.source = res.map((x: any) => { if (x.upgrade_availble) _self.upgrades.push(x); if (x.update_availble) _self.updates.push(x); @@ -424,6 +493,7 @@ export class DevicesComponent implements OnInit, OnDestroy { }); _self.device_interval(); _self.loading = false; + } }); } @@ -516,8 +586,17 @@ export class DevicesComponent implements OnInit, OnDestroy { _self.selected_device['editform']['password']="Loading ..."; if (_self.ispro && !this.show_pass){ _self.data_provider.get_device_pass(this.selected_device['id']).then((res) => { + if ("error" in res && "error" in res && res.error.indexOf("Unauthorized")) { + _self.show_toast( + "Error", + "You are not authorized to perform this action", + "danger" + ); + } + else{ _self.selected_device['editform']['password']=res['password']; this.show_pass=!this.show_pass; + } }); } else{ @@ -530,7 +609,14 @@ export class DevicesComponent implements OnInit, OnDestroy { this.data_provider .scan_results() .then((res) => { - console.dir(res); + if ("error" in res && res.error.indexOf("Unauthorized")) { + _self.show_toast( + "Error", + "You are not authorized to perform this action", + "danger" + ); + } + else{ let index = 1; _self.ExecutedData= res.data.map((d: any) => { d.index = index; @@ -551,6 +637,7 @@ export class DevicesComponent implements OnInit, OnDestroy { index += 1; return d; }); + } }); } diff --git a/src/app/views/permissions/permissions.component.ts b/src/app/views/permissions/permissions.component.ts index d8fac73..1ce6ac0 100644 --- a/src/app/views/permissions/permissions.component.ts +++ b/src/app/views/permissions/permissions.component.ts @@ -225,17 +225,26 @@ export class PermissionsComponent implements OnInit { } else { var _self = this; this.data_provider.delete_perm(_self.SelectedPerm["id"]).then((res) => { - if (res["status"] == "failed") { + if ("error" in res && res.error.indexOf("Unauthorized")) { _self.show_toast( "Error", - res.err, + "You are not authorized to perform this action", "danger" ); - return; } else{ - _self.initGridTable(); - _self.DeleteConfirmModalVisible = false; + if (res["status"] == "failed") { + _self.show_toast( + "Error", + res.err, + "danger" + ); + return; + } + else{ + _self.initGridTable(); + _self.DeleteConfirmModalVisible = false; + } } }); } diff --git a/src/app/views/settings/settings.component.html b/src/app/views/settings/settings.component.html index b0cc3a5..d3fe44e 100644 --- a/src/app/views/settings/settings.component.html +++ b/src/app/views/settings/settings.component.html @@ -72,7 +72,7 @@ - + * Choose how Mikrowizard should update old v6 firmwares
@@ -85,7 +85,7 @@ - + * The version of firmware to install routers @@ -97,7 +97,7 @@ - + * The version of firmware to install on V6 routers @@ -112,7 +112,7 @@ Rad Secret - + * Radius Secret of Mikrowizard Radius Server @@ -121,7 +121,7 @@ System URL - + * Default system access URl @@ -129,7 +129,7 @@ Default IP - + * Default Mikrowizard Access IP @@ -146,7 +146,7 @@ - + * Default TimeZone for the system @@ -157,20 +157,34 @@ Default password - + * Default username and Password for searching new devices + + + + + + * Choose if Mikrowizard should download updates automaticaly when availble or wait for user to download/apply updates + + License Username - + * The username that you registred in Mikrowizard.com,Required for License Activation - + * Force User Groups under user>groups configuration of each router to match Mikrowizard Permissions and monitor for any change to prevent/fix the configuration. @@ -178,7 +192,7 @@ - + * Force Radius config under radius>client and user>aaa setting of each router that added to Mikrowizard and monitor for any change to prevent/fix the configuration. @@ -186,7 +200,7 @@ - + * Force Syslog config under system>logs setting of each router that added to Mikrowizard and monitor syslog setting for any change to prevent/fix the configuration. @@ -194,7 +208,7 @@ - PRO + PRO * Download and install reqired firmware before installing the target firmware . for example it will install latest 7.12 then upgrade to newer version >7.13 or install Required packages before update @@ -202,7 +216,7 @@ - PRO + PRO * Force login to devices using otp for all users.(you can make exceptions for each user) diff --git a/src/app/views/settings/settings.component.scss b/src/app/views/settings/settings.component.scss index 0185fbf..897c549 100644 --- a/src/app/views/settings/settings.component.scss +++ b/src/app/views/settings/settings.component.scss @@ -7,4 +7,7 @@ } .mdc-line-ripple.mdc-line-ripple--deactivating.ng-star-inserted { display: none!important; +} +.form-check-label{ + font-weight: bold; } \ No newline at end of file diff --git a/src/app/views/settings/settings.component.ts b/src/app/views/settings/settings.component.ts index 394a771..5501013 100644 --- a/src/app/views/settings/settings.component.ts +++ b/src/app/views/settings/settings.component.ts @@ -22,7 +22,6 @@ import { import { ToasterComponent } from "@coreui/angular"; import { AppToastComponent } from "../toast-simple/toast.component"; import { TimeZones } from "./timezones-data"; -import { error } from "console"; @Component({ templateUrl: "settings.component.html", @@ -137,6 +136,14 @@ export class SettingsComponent implements OnInit { _self.currentFirm=firm; if(del){ this.data_provider.delete_firm(this.currentFirm.id).then((res) => { + if ("error" in res && res.error.indexOf("Unauthorized")) { + _self.show_toast( + "Error", + "You are not authorized to perform this action", + "danger" + ); + } + else{ if (res.status == true){ _self.DeleteConfirmModalVisible=false; _self.initFirmsTable(); @@ -148,6 +155,7 @@ export class SettingsComponent implements OnInit { "danger" ); } + } }); } else @@ -160,6 +168,14 @@ export class SettingsComponent implements OnInit { this.data_provider .download_firmware_to_repository(this.firmtodownload) .then((res) => { + if ("error" in res && res.error.indexOf("Unauthorized")) { + _self.show_toast( + "Error", + "You are not authorized to perform this action", + "danger" + ); + } + else{ if (res.status == true) { // show toast that we are already downloading _self.show_toast( @@ -177,6 +193,7 @@ export class SettingsComponent implements OnInit { } _self.ConfirmModalVisible = !_self.ConfirmModalVisible; _self.loading = false; + } }); } @@ -204,14 +221,34 @@ export class SettingsComponent implements OnInit { this.firmwaretoinstallv6 ) .then((res) => { + if ("error" in res && res.error.indexOf("Unauthorized")) { + _self.show_toast( + "Error", + "You are not authorized to perform this action", + "danger" + ); + } + else{ _self.initFirmsTable(); + } }); } saveSysSetting() { var _self = this; this.data_provider.save_sys_setting(this.sysconfigs).then((res) => { - _self.initsettings(); + if ("error" in res && res.error.indexOf("Unauthorized")) { + _self.show_toast( + "Error", + "You are not authorized to perform this action", + "danger" + ); + } + else{ + _self.show_toast("Settings", "Settings saved", "success"); + _self.initsettings(); + + } }); } @@ -244,6 +281,14 @@ export class SettingsComponent implements OnInit { initsettings(): void { var _self = this; this.data_provider.get_settings().then((res) => { + if ("error" in res && res.error.indexOf("Unauthorized")) { + _self.show_toast( + "Error", + "You are not authorized to perform this action", + "danger" + ); + } + else{ _self.sysconfigs = res.sysconfigs; _self.sysconfigs["default_user"]["value"] = ""; _self.sysconfigs["default_password"]["value"] = ""; @@ -265,7 +310,23 @@ export class SettingsComponent implements OnInit { _self.sysconfigs["otp_force"]["value"] ); } + //check if update_mode is in the sysconfigs + if ("update_mode" in _self.sysconfigs){ + //convert string to json + _self.sysconfigs["update_mode"]["value"] = JSON.parse(_self.sysconfigs["update_mode"]["value"]); + } + else{ + //create default update_mode and set mode to auto + _self.sysconfigs["update_mode"] = { + "value": { + "mode": "auto", + "update_back" : false, + "update_front" : false + } + } + } _self.SysConfigloading = false; + } }); } diff --git a/src/app/views/snippets/snippets.component.html b/src/app/views/snippets/snippets.component.html index 2b68c32..4b3d199 100644 --- a/src/app/views/snippets/snippets.component.html +++ b/src/app/views/snippets/snippets.component.html @@ -159,12 +159,11 @@ -
Editing Group
+
Exec history
-
Group Members :
{ + if ("error" in res && res.error.indexOf("Unauthorized")) { + _self.show_toast( + "Error", + "You are not authorized to perform this action", + "danger" + ); + } + else{ if ("id" in res && !("status" in res)) { _self.initGridTable(); this.EditTaskModalVisible = false; @@ -178,6 +186,7 @@ export class UserManagerComponent implements OnInit { //show error _self.show_toast("Error", res.err, "danger"); } + } }); } else { if (_self.userperms.length > 0) { @@ -187,8 +196,17 @@ export class UserManagerComponent implements OnInit { } _self.SelectedUser["adminperms"] = _self.adminperms; this.data_provider.edit_user(_self.SelectedUser).then((res) => { + if ("error" in res && res.error.indexOf("Unauthorized")) { + _self.show_toast( + "Error", + "You are not authorized to perform this action", + "danger" + ); + } + else{ _self.initGridTable(); _self.EditTaskModalVisible = false; + } }); } // @@ -197,14 +215,32 @@ export class UserManagerComponent implements OnInit { editAddUser(item: any, action: string) { var _self = this; this.data_provider.get_perms(1, 1000, "").then((res) => { + if ("error" in res && res.error.indexOf("Unauthorized")) { + _self.show_toast( + "Error", + "You are not authorized to perform this action", + "danger" + ); + } + else{ _self.allPerms = res.map((x: any) => { return { id: x["id"], name: x.name }; }); _self.data_provider.get_devgroup_list().then((res) => { + if ("error" in res && res.error.indexOf("Unauthorized")) { + _self.show_toast( + "Error", + "You are not authorized to perform this action", + "danger" + ); + } + else{ _self.allDevGroups = res.map((x: any) => { return { id: x["id"], name: x.name }; }); + } }); + } }); if (action == "showadd") { this.userperms = []; @@ -241,7 +277,6 @@ export class UserManagerComponent implements OnInit { this.data_provider.get_user_restrictions(this.SelectedUser["id"]).then((res) => { _self.userresttrictions = res; - console.log(_self.userresttrictions); _self.RestrictionsTaskModalVisible = true; }); } @@ -270,13 +305,23 @@ export class UserManagerComponent implements OnInit { } save_sec(){ + var _self=this; this.data_provider.save_user_restrictions(this.SelectedUser.id,this.userresttrictions).then((res) => { + if ("error" in res && res.error.indexOf("Unauthorized")) { + _self.show_toast( + "Error", + "You are not authorized to perform this action", + "danger" + ); + } + else{ if('status' in res && res['status']=='success') this.RestrictionsTaskModalVisible = false; else if('status' in res && res['status']=='failed') this.show_toast("Error", res.err, "danger"); else this.show_toast("Error", "Somthing went wrong", "danger"); + } }); } @@ -289,9 +334,18 @@ export class UserManagerComponent implements OnInit { this.devgroup["id"] ) .then((res) => { + if ("error" in res && res.error.indexOf("Unauthorized")) { + _self.show_toast( + "Error", + "You are not authorized to perform this action", + "danger" + ); + } + else{ _self.get_user_perms(_self.SelectedUser["id"]); _self.permission = 0; _self.devgroup = 0; + } }); } @@ -314,8 +368,17 @@ export class UserManagerComponent implements OnInit { } else { var _self = this; this.data_provider.delete_user(_self.SelectedUser["id"]).then((res) => { + if ("error" in res && res.error.indexOf("Unauthorized")) { + _self.show_toast( + "Error", + "You are not authorized to perform this action", + "danger" + ); + } + else{ _self.initGridTable(); _self.DeleteConfirmModalVisible = false; + } }); } } @@ -329,8 +392,18 @@ export class UserManagerComponent implements OnInit { } confirm_delete_perm(item: any) { + var _self = this; this.data_provider.Delete_user_perm(item.id).then((res) => { + if ("error" in res && res.error.indexOf("Unauthorized")) { + _self.show_toast( + "Error", + "You are not authorized to perform this action", + "danger" + ); + } + else{ this.get_user_perms(this.SelectedUser["id"]); + } }); } @@ -344,11 +417,20 @@ export class UserManagerComponent implements OnInit { var pageSize = 10; var searchstr = ""; this.data_provider.get_users(page, pageSize, searchstr).then((res) => { + if ("error" in res && res.error.indexOf("Unauthorized")) { + _self.show_toast( + "Error", + "You are not authorized to perform this action", + "danger" + ); + } + else{ _self.source = res.map((x: any) => { return x; }); _self.SelectedUser = {}; _self.loading = false; + } }); } } diff --git a/src/app/views/vault/vault.component.ts b/src/app/views/vault/vault.component.ts index acf81ee..861e550 100644 --- a/src/app/views/vault/vault.component.ts +++ b/src/app/views/vault/vault.component.ts @@ -201,6 +201,14 @@ export class VaultComponent implements OnInit { get_passwords(){ var _self=this; this.data_provider.get_passwords(this.filters).then((res) => { + if ("error" in res && res.error.indexOf("Unauthorized")) { + _self.show_toast( + "Error", + "You are not authorized to perform this action", + "danger" + ); + } + else{ _self.passwords=res.data.map((d: any) => { d.changed = formatInTimeZone( d.changed.split(".")[0] + ".000Z", @@ -209,6 +217,7 @@ export class VaultComponent implements OnInit { ); return d; }); + } }); } @@ -216,8 +225,17 @@ export class VaultComponent implements OnInit { var _self=this; _self.password=""; this.data_provider.reveal_password(devid,username).then((res) => { + if ("error" in res && res.error.indexOf("Unauthorized")) { + _self.show_toast( + "Error", + "You are not authorized to perform this action", + "danger" + ); + } + else{ _self.password=res.password; _self.PasswordModalVisible=true; + } }); } @@ -255,6 +273,14 @@ export class VaultComponent implements OnInit { get_vault_history(){ var _self=this; this.data_provider.vault_history().then((res) => { + if ("error" in res && res.error.indexOf("Unauthorized")) { + _self.show_toast( + "Error", + "You are not authorized to perform this action", + "danger" + ); + } + else{ let index = 1; _self.vault_history=res.data.map((d: any) => { d.index = index; @@ -275,6 +301,7 @@ export class VaultComponent implements OnInit { index += 1; return d; }); + } }); } From 982406aa216c5f0ea61b96e7cca411cf13db5c2f Mon Sep 17 00:00:00 2001 From: sepehr Date: Thu, 2 Jan 2025 23:58:20 +0300 Subject: [PATCH 02/15] close modal with update cancel --- .../containers/default-layout/default-layout.component.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/app/containers/default-layout/default-layout.component.ts b/src/app/containers/default-layout/default-layout.component.ts index 8830ab9..e59f4bd 100644 --- a/src/app/containers/default-layout/default-layout.component.ts +++ b/src/app/containers/default-layout/default-layout.component.ts @@ -250,6 +250,9 @@ export class DefaultLayoutComponent implements OnInit { }, 60000); } - clearTimer() { clearInterval(this.timer); } + clearTimer() { + clearInterval(this.timer); + this.ConfirmModalVisible = false; + } } From d8aa93f7ec326bd033ec94eb49b3a0561677a89e Mon Sep 17 00:00:00 2001 From: sepehr Date: Sat, 4 Jan 2025 21:13:48 +0300 Subject: [PATCH 03/15] show serial number if internet connection not availble --- src/app/views/dashboard/dashboard.component.html | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/app/views/dashboard/dashboard.component.html b/src/app/views/dashboard/dashboard.component.html index fb8a721..e454cf9 100644 --- a/src/app/views/dashboard/dashboard.component.html +++ b/src/app/views/dashboard/dashboard.component.html @@ -144,6 +144,13 @@ updates!
+
+ Serial: {{ stats['serial'] }} + Copy +
Unable connect to server/Check server internet connection
From 10d4cff4a46eec2a8cfff1406c9870bf431ef723 Mon Sep 17 00:00:00 2001 From: sepehr Date: Mon, 3 Feb 2025 12:35:12 +0300 Subject: [PATCH 04/15] New menu/page for configuration sync and cloning (pro feature). Now displays DHCP server details along with historical lease information(pro feature)). Added a ping status component for monitoring device connectivity. Device pages now include logs, accounting, and authorization data for improved tracking. New component in the device page to display active sessions with the ability to terminate them. Redesigned Device Detail Page Reload Buttons for device details page Tabs Device detail pages now refresh every 30 seconds. Restored the missing device logs action menu on the devices list page. Improved handling of duplicate serial numbers when the license serial is not set, with better registration status details on the dashboard. Various minor stability and performance improvements. --- src/app/app-routing.module.ts | 5 + src/app/containers/default-layout/_nav.ts | 9 +- src/app/providers/mikrowizard/data.ts | 48 ++ src/app/views/acc_log/acc.component.html | 18 +- src/app/views/acc_log/acc.component.scss | 4 + src/app/views/acc_log/acc.component.ts | 14 +- src/app/views/acc_log/acc.module.ts | 5 + src/app/views/auth_log/auth.component.html | 24 +- src/app/views/auth_log/auth.component.scss | 4 + src/app/views/auth_log/auth.component.ts | 14 +- src/app/views/auth_log/auth.module.ts | 7 +- src/app/views/cloner/cloner-routing.module.ts | 21 + src/app/views/cloner/cloner.component.html | 239 ++++++++ src/app/views/cloner/cloner.component.ts | 458 ++++++++++++++++ src/app/views/cloner/cloner.module.ts | 44 ++ src/app/views/cloner/cloner.scss | 133 +++++ .../views/dashboard/dashboard.component.html | 10 +- .../active-users-routing.module.ts | 21 + .../active-users/active-users.component.html | 38 ++ .../active-users/active-users.component.scss | 0 .../active-users/active-users.component.ts | 27 + .../active-users/active-users.module.ts | 46 ++ .../device-info/device-info-routing.module.ts | 21 + .../device-info/device-info.component.html | 40 ++ .../device-info/device-info.component.scss | 0 .../device-info/device-info.component.ts | 25 + .../device-info/device-info.module.ts | 56 ++ .../views/device_detail/device.component.html | 517 ++++++++++-------- .../views/device_detail/device.component.scss | 32 ++ .../views/device_detail/device.component.ts | 83 ++- src/app/views/device_detail/device.module.ts | 25 +- .../dhcp-info/dhcp-info.component.html | 168 ++++++ .../dhcp-info/dhcp-info.component.scss | 0 .../dhcp-info/dhcp-info.component.ts | 47 ++ .../dhcp-info/dhcp-info.module.ts | 57 ++ .../dhcp-info/dhcp-inforouting.module.ts | 21 + .../ping-status/ping-status-routing.module.ts | 21 + .../ping-status/ping-status.component.html | 42 ++ .../ping-status/ping-status.component.scss | 0 .../ping-status/ping-status.component.ts | 19 + .../ping-status/ping-status.module.ts | 46 ++ .../radio-data/interface-bw.component.html | 77 +++ .../radio-data/radio-data-routing.module.ts | 21 + .../radio-data/radio-data.component.scss | 0 .../radio-data/radio-data.component.ts | 24 + .../radio-data/radio-data.module.ts | 56 ++ .../views/device_logs/devlogs.component.html | 30 +- .../views/device_logs/devlogs.component.scss | 4 + .../views/device_logs/devlogs.component.ts | 15 +- src/app/views/device_logs/devlogs.module.ts | 3 + src/app/views/devices/devices.component.html | 25 +- src/app/views/devices/devices.component.ts | 6 + src/app/views/syslog/syslog.component.html | 7 +- src/app/views/syslog/syslog.component.scss | 4 + src/app/views/syslog/syslog.module.ts | 2 + 55 files changed, 2412 insertions(+), 271 deletions(-) create mode 100644 src/app/views/cloner/cloner-routing.module.ts create mode 100644 src/app/views/cloner/cloner.component.html create mode 100644 src/app/views/cloner/cloner.component.ts create mode 100644 src/app/views/cloner/cloner.module.ts create mode 100644 src/app/views/cloner/cloner.scss create mode 100644 src/app/views/device_detail/active-users/active-users-routing.module.ts create mode 100644 src/app/views/device_detail/active-users/active-users.component.html create mode 100644 src/app/views/device_detail/active-users/active-users.component.scss create mode 100644 src/app/views/device_detail/active-users/active-users.component.ts create mode 100644 src/app/views/device_detail/active-users/active-users.module.ts create mode 100644 src/app/views/device_detail/device-info/device-info-routing.module.ts create mode 100644 src/app/views/device_detail/device-info/device-info.component.html create mode 100644 src/app/views/device_detail/device-info/device-info.component.scss create mode 100644 src/app/views/device_detail/device-info/device-info.component.ts create mode 100644 src/app/views/device_detail/device-info/device-info.module.ts create mode 100644 src/app/views/device_detail/dhcp-info/dhcp-info.component.html create mode 100644 src/app/views/device_detail/dhcp-info/dhcp-info.component.scss create mode 100644 src/app/views/device_detail/dhcp-info/dhcp-info.component.ts create mode 100644 src/app/views/device_detail/dhcp-info/dhcp-info.module.ts create mode 100644 src/app/views/device_detail/dhcp-info/dhcp-inforouting.module.ts create mode 100644 src/app/views/device_detail/ping-status/ping-status-routing.module.ts create mode 100644 src/app/views/device_detail/ping-status/ping-status.component.html create mode 100644 src/app/views/device_detail/ping-status/ping-status.component.scss create mode 100644 src/app/views/device_detail/ping-status/ping-status.component.ts create mode 100644 src/app/views/device_detail/ping-status/ping-status.module.ts create mode 100644 src/app/views/device_detail/radio-data/interface-bw.component.html create mode 100644 src/app/views/device_detail/radio-data/radio-data-routing.module.ts create mode 100644 src/app/views/device_detail/radio-data/radio-data.component.scss create mode 100644 src/app/views/device_detail/radio-data/radio-data.component.ts create mode 100644 src/app/views/device_detail/radio-data/radio-data.module.ts diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index 806ed24..3c4186a 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -84,6 +84,11 @@ const routes: Routes = [ loadChildren: () => import('./views/user_tasks/user_tasks.module').then((m) => m.UserTasksModule) }, + { + path: 'cloner', + loadChildren: () => + import('./views/cloner/cloner.module').then((m) => m.ClonerModule) + }, { path: 'snippets', loadChildren: () => diff --git a/src/app/containers/default-layout/_nav.ts b/src/app/containers/default-layout/_nav.ts index 6339791..d313617 100644 --- a/src/app/containers/default-layout/_nav.ts +++ b/src/app/containers/default-layout/_nav.ts @@ -60,10 +60,17 @@ export const navItems: INavData[] = [ icon: 'fa-solid fa-database' }, { - name: 'snippets', + name: 'Snippets', url: '/snippets', icon: 'fa-solid fa-code' }, + { + name: 'Sync and Cloner', + url: '/cloner', + icon: 'fa-solid fa-rotate', + attributes: { 'pro':true } + + }, { name: 'Password Vault', url: '/vault', diff --git a/src/app/providers/mikrowizard/data.ts b/src/app/providers/mikrowizard/data.ts index b80fbb0..ba6c8a4 100644 --- a/src/app/providers/mikrowizard/data.ts +++ b/src/app/providers/mikrowizard/data.ts @@ -172,6 +172,12 @@ export class dataProvider { } return this.MikroWizardRPC.sendJsonRequest("/api/dev/radio/sensors", data); } + get_dev_dhcp_info(id: number){ + var data={ + 'devid':id, + } + return this.MikroWizardRPC.sendJsonRequest("/api/dev/dhcp-server/get", data); + } get_dev_ifstat(id: number,delta:string="5m",iface:string="ether1",type:string="bps") { var data={ 'devid':id, @@ -553,6 +559,48 @@ export class dataProvider { } return this.MikroWizardRPC.sendJsonRequest("/api/sysconfig/apply_update", data); } + + + get_cloner_list() { + return this.MikroWizardRPC.sendJsonRequest("/api/cloner/list", {}); + } + + Add_cloner(data:any,members:any) { + data['members']=members; + return this.MikroWizardRPC.sendJsonRequest("/api/cloner/create", data); + } + + Delete_cloner(clonerid:number) { + var data={ + 'clonerid':clonerid, + } + return this.MikroWizardRPC.sendJsonRequest("/api/cloner/delete", data); + } + + Edit_cloner(data:any,members:any) { + data['members']=members; + return this.MikroWizardRPC.sendJsonRequest("/api/cloner/edit", data); + } + + get_cloner_members(clonerid:number) { + var data={ + 'clonerid':clonerid, + } + return this.MikroWizardRPC.sendJsonRequest("/api/cloner/memberdetails", data); + } + killSession(devid:number,item:any){ + var data={ + 'devid':devid, + 'item':item + } + return this.MikroWizardRPC.sendJsonRequest("/api/dev/kill_session", data); + } + getDhcpHistory(item:any){ + var data={ + 'item':item + } + return this.MikroWizardRPC.sendJsonRequest("/api/dhcp-history/get", data); + } //// //// End api funcs //// diff --git a/src/app/views/acc_log/acc.component.html b/src/app/views/acc_log/acc.component.html index 159b290..3b1b9b8 100644 --- a/src/app/views/acc_log/acc.component.html +++ b/src/app/views/acc_log/acc.component.html @@ -1,10 +1,22 @@ - + - - Accunting Logs + +
Accunting Logs + + + +
+ Filtered Result For Device ID + {{devid}} + + Showing last 24 hours logs by default. Use filters to modify the date and time. +
+ +
+
+ + + + + {{value}} + + + + + {{value}} + + + + + {{value}} + + + + + + + + + + + +
+
+
+ + + +
Editing Cloner {{SelectedCloner['name']}}
+
Adding new task
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
{{ section.title }}
+
+
+ + +
{{ command }}
+
+ + +
+
+
+
+
+
+
+
+
+ +
Peers :
+ + + + +   {{value}} + + + + {{value}} + + + + + + + + + +
+ +
+ + + + + +
+ + + + +
Editing Group
+ +
+ + +
Group Members :
+ + + +   {{value}} + + + + {{value}} + + + + + {{value}} + + + +
+
+
+
+ + + + + +
+ + + + +
Confirm delete {{ SelectedCloner['name'] }}
+ +
+ + Are you sure that You want to delete following task ? +
+
+ + + + + + + + + + + + + +
Taks name : {{ SelectedCloner['name'] }}
Description : {{ SelectedCloner['description'] }}
Cron exec : {{ SelectedCloner['desc_cron'] }}
+
+ + + + +
+ + \ No newline at end of file diff --git a/src/app/views/cloner/cloner.component.ts b/src/app/views/cloner/cloner.component.ts new file mode 100644 index 0000000..70a8a90 --- /dev/null +++ b/src/app/views/cloner/cloner.component.ts @@ -0,0 +1,458 @@ +import { Component, OnInit, OnDestroy, ViewChildren ,QueryList } from "@angular/core"; +import { dataProvider } from "../../providers/mikrowizard/data"; +import { Router } from "@angular/router"; +import { loginChecker } from "../../providers/login_checker"; +import { + GuiSelectedRow, + GuiSearching, + GuiInfoPanel, + GuiColumn, + GuiColumnMenu, + GuiPaging, + GuiPagingDisplay, + GuiRowSelectionMode, + GuiRowSelection, + GuiRowSelectionType, +} from "@generic-ui/ngx-grid"; +import { NgxSuperSelectOptions } from "ngx-super-select"; +import { _getFocusedElementPierceShadowDom } from "@angular/cdk/platform"; + + +import { ToasterComponent } from "@coreui/angular"; +import { AppToastComponent } from "../toast-simple/toast.component"; + +@Component({ + templateUrl: "cloner.component.html", + styleUrls: ["cloner.scss"], + +}) +export class ClonerComponent implements OnInit { + public uid: number; + public uname: string; + public ispro: boolean = false; + + constructor( + private data_provider: dataProvider, + private router: Router, + private login_checker: loginChecker + ) { + var _self = this; + if (!this.login_checker.isLoggedIn()) { + setTimeout(function () { + _self.router.navigate(["login"]); + }, 100); + } + this.data_provider.getSessionInfo().then((res) => { + _self.uid = res.uid; + _self.uname = res.name; + _self.ispro = res['ISPRO'] + const userId = _self.uid; + + if (res.role != "admin") { + setTimeout(function () { + _self.router.navigate(["/user/dashboard"]); + }, 100); + } + }); + //get datagrid data + function isNotEmpty(value: any): boolean { + return value !== undefined && value !== null && value !== ""; + } + } + @ViewChildren(ToasterComponent) viewChildren!: QueryList; + + public source: Array = []; + public columns: Array = []; + public loading: boolean = true; + public rows: any = []; + public SelectedCloner: any = {}; + public SelectedClonerItems: any = ""; + public EditClonerModalVisible: boolean = false; + public DeleteConfirmModalVisible: boolean = false; + public Members: any = ""; + public SelectedMembers: any = []; + public NewMemberModalVisible: boolean = false; + public availbleMembers: any = []; + public NewMemberRows: any = []; + public SelectedNewMemberRows: any; + public master: number = 0; + public active_commands:any=[]; + public tabs:any=[ + { + "name": "Network", + "sections": [ + { + "title": "IP Management", + "commands": [ + "/ip address", + "/ip cloud", + "/ip dhcp-server", + "/ip dns", + "/ip pool", + "/ip route", + "/ip vrf" + ] + }, + { + "title": "IP Services", + "commands": [ + "/ip service" + ] + } + ] + }, + { + "name": "Security", + "sections": [ + { + "title": "Firewall", + "commands": [ + "/ip firewall address-list", + "/ip firewall connection", + "/ip firewall layer7-protocol", + "/ip firewall nat", + "/ip firewall service-port", + "/ip firewall calea", + "/ip firewall filter", + "/ip firewall mangle", + "/ip firewall raw" + ] + }, + { + "title": "IPSec", + "commands": [ + "/ip ipsec identity", + "/ip ipsec key", + "/ip ipsec peer", + "/ip ipsec profile", + "/ip ipsec settings", + "/ip ipsec statistics", + "/ip ipsec proposal", + "/ip ipsec policy", + "/ip ipsec mode-config", + "/ip ipsec active-peers", + "/ip ipsec installed-sa" + ] + } + ] + }, + { + "name": "System", + "sections": [ + { + "title": "Scripts & Scheduling", + "commands": [ + "/system script", + "/system scheduler" + ] + }, + { + "title": "Time Management", + "commands": [ + "/system ntp client servers", + "/system ntp client", + "/system ntp server", + "/system clock" + ] + }, + { + "title": "RADIUS", + "commands": [ + "/radius" + ] + } + ] + }, + { + "name": "MPLS", + "sections": [ + { + "title": "MPLS Configuration", + "commands": [ + "/mpls forwarding-table", + "/mpls interface", + "/mpls ldp", + "/mpls settings", + "/mpls traffic-eng" + ] + }, + { + "title": "VPLS", + "commands": [ + "/interface vpls" + ] + } + ] + } + ]; + + public sorting = { + enabled: true, + multiSorting: true, + }; + searching: GuiSearching = { + enabled: true, + placeholder: "Search Devices", + }; + + options: Partial = { + selectionMode: "single", + actionsEnabled: false, + displayExpr: "name", + valueExpr: "id", + placeholder: "Snippet", + searchEnabled: true, + enableDarkMode: false, + }; + + public paging: GuiPaging = { + enabled: true, + page: 1, + pageSize: 10, + pageSizes: [5, 10, 25, 50], + display: GuiPagingDisplay.ADVANCED, + }; + + public columnMenu: GuiColumnMenu = { + enabled: true, + sort: true, + columnsManager: true, + }; + + toasterForm = { + autohide: true, + delay: 3000, + position: "fixed", + fade: true, + closeButton: true, + }; + + public infoPanel: GuiInfoPanel = { + enabled: true, + infoDialog: false, + columnsManager: true, + schemaManager: true, + }; + + public rowSelection: boolean | GuiRowSelection = { + enabled: true, + type: GuiRowSelectionType.CHECKBOX, + mode: GuiRowSelectionMode.MULTIPLE, + }; + activate_command(command:string){ + // add to active_commands if it not added before + if(!this.active_commands.includes(command)){ + this.active_commands.push(command); + } + else{ + this.active_commands=this.active_commands.filter((x:any)=>x!=command); + } + } + 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; + } + show_new_member_form() { + this.NewMemberModalVisible = true; + var _self = this; + _self.availbleMembers = []; + this.SelectedNewMemberRows = []; + this.NewMemberRows = []; + + var data = { + group_id: false, + search: false, + page: false, + size: 10000, + }; + + if (this.SelectedCloner["pair_type"] == "devices") + _self.data_provider.get_dev_list(data).then((res) => { + _self.availbleMembers = res.filter( + (x: any) => !_self.SelectedClonerItems.includes(x.id) + ); + _self.NewMemberModalVisible = true; + }); + else + _self.data_provider.get_devgroup_list().then((res) => { + _self.availbleMembers = res.filter( + (x: any) => !_self.SelectedClonerItems.includes(x.id) + ); + _self.NewMemberModalVisible = true; + }); + } + + ngOnInit(): void { + this.initGridTable(); + } + + submit(action: string) { + var _self = this; + if(_self.master==0 && _self.SelectedCloner['direction']=='oneway'){ + _self.show_toast( + "Error", + "Master device is not selected", + "danger" + ); + return; + } + if(_self.SelectedCloner['direction']=='twoway' && _self.SelectedCloner['pair_type']=='groups'){ + _self.show_toast( + "Error", + "Using Groups is only allowed with Master Mode", + "danger" + ); + return; + } + if (_self.master!=0 && _self.SelectedCloner['direction']=='oneway'){ + _self.SelectedCloner["masterid"]=_self.master; + } + _self.SelectedCloner["active_commands"]=_self.active_commands; + if (action == "add") { + this.data_provider + .Add_cloner(_self.SelectedCloner, _self.SelectedClonerItems) + .then((res) => { + _self.initGridTable(); + }); + } else { + this.data_provider + .Edit_cloner(_self.SelectedCloner, _self.SelectedClonerItems) + .then((res) => { + _self.initGridTable(); + }); + } + this.EditClonerModalVisible = false; + } + + onSelectedRowsNewMembers(rows: Array): void { + this.NewMemberRows = rows; + this.SelectedNewMemberRows = rows.map((m: GuiSelectedRow) => m.source); + } + + add_new_members() { + var _self = this; + _self.SelectedMembers = [ + ...new Set(_self.SelectedMembers.concat(_self.SelectedNewMemberRows)), + ]; + + _self.SelectedClonerItems = _self.SelectedMembers.map((x: any) => { + return x.id; + }); + + this.NewMemberModalVisible = false; + } + + set_master(id:number){ + var _self=this; + this.master=id; + // sort SelectedMembers and put master on top + this.SelectedMembers= + [ + ...new Set( + this.SelectedMembers.sort((a:any,b:any)=>{ + if(a.id==_self.master) return -1; + if(b.id==_self.master) return 1; + return 0; + }) + + ), + ]; + } + + + editAddCloner(item: any, action: string) { + if (action == "showadd") { + this.SelectedCloner = { + id: 0, + name: "", + description: "", + pair_type: "devices", + live_mode: false, + schedule: false, + cron: "", + desc_cron: "", + direction: "oneway", + members: "", + action: "add", + }; + this.SelectedMembers = []; + this.SelectedClonerItems = []; + this.EditClonerModalVisible = true; + return; + } + + var _self = this; + this.SelectedCloner = { ...item }; + this.active_commands=item['active_commands']?JSON.parse(item['active_commands']):[]; + if (action != "select_change" ) { + this.SelectedCloner["action"] = "edit"; + this.data_provider.get_cloner_members(_self.SelectedCloner.id).then((res) => { + _self.SelectedMembers = res; + if(_self.SelectedCloner["master"] && _self.SelectedCloner['direction']=='oneway'){ + _self.set_master(_self.SelectedCloner["master"]); + } + _self.EditClonerModalVisible = true; + _self.SelectedClonerItems = res.map((x: any) => { + return x.id; + }); + }); + } else { + _self.SelectedMembers = []; + this.SelectedClonerItems = []; + } + } + + + in_active_commands(command:string){ + return this.active_commands.includes(command); + } + remove_member(item: any) { + var _self = this; + _self.SelectedMembers = _self.SelectedMembers.filter( + (x: any) => x.id != item.id + ); + _self.SelectedClonerItems = _self.SelectedMembers.map((x: any) => { + return x.id; + }); + } + + get_member_by_id(id: string) { + return this.Members.find((x: any) => x.id == id); + } + + confirm_delete(item: any = "", del: boolean = false) { + if (!del) { + this.SelectedCloner = { ...item }; + this.DeleteConfirmModalVisible = true; + } else { + var _self = this; + this.data_provider.Delete_cloner(_self.SelectedCloner["id"]).then((res) => { + _self.initGridTable(); + _self.DeleteConfirmModalVisible = false; + }); + } + } + + form_changed() { + this.editAddCloner(this.SelectedCloner, "select_change"); + } + + logger(item: any) { + console.dir(item); + } + + initGridTable(): void { + var _self = this; + this.data_provider.get_cloner_list().then((res) => { + _self.source = res.map((x: any) => { + return x; + }); + _self.loading = false; + }); + } +} diff --git a/src/app/views/cloner/cloner.module.ts b/src/app/views/cloner/cloner.module.ts new file mode 100644 index 0000000..d42f20e --- /dev/null +++ b/src/app/views/cloner/cloner.module.ts @@ -0,0 +1,44 @@ +import { NgModule } from "@angular/core"; +import { CommonModule } from "@angular/common"; +import { FormsModule,ReactiveFormsModule } from "@angular/forms"; + +import { + ButtonModule, + CardModule, + FormModule, + GridModule, + ModalModule, + ButtonGroupModule, + ToastModule, + TooltipModule, + NavModule, + TabsModule, +} from "@coreui/angular"; +import { ClonerRoutingModule } from "./cloner-routing.module"; +import { ClonerComponent } from "./cloner.component"; +import { GuiGridModule } from "@generic-ui/ngx-grid"; + +import { NgxSuperSelectModule} from "ngx-super-select"; + +@NgModule({ + imports: [ + ClonerRoutingModule, + CardModule, + CommonModule, + GridModule, + FormModule, + ButtonModule, + ButtonGroupModule, + GuiGridModule, + ModalModule, + ReactiveFormsModule, + FormsModule, + NgxSuperSelectModule, + ToastModule, + TooltipModule, + NavModule, + TabsModule, + ], + declarations: [ClonerComponent], +}) +export class ClonerModule {} diff --git a/src/app/views/cloner/cloner.scss b/src/app/views/cloner/cloner.scss new file mode 100644 index 0000000..a502ea6 --- /dev/null +++ b/src/app/views/cloner/cloner.scss @@ -0,0 +1,133 @@ + +.nav-underline { + border-bottom: 2px solid var(--cui-nav-underline-border-color, #c4c9d0) +} + +.nav-underline .nav-item { + margin-bottom: -2px; + cursor: pointer; +} + +.nav-underline .nav-link { + color: var(--cui-nav-underline-link-color, #768192); + border-style: none none solid!important; + border-width:2px; + position:relative; + bottom:-1px; + cursor: pointer; + +} + +.nav-underline .nav-link:hover,.nav-underline .nav-link:focus { + border-color: var(--cui-nav-underline-link-active-border-color, #321fdb) +} + +.nav-underline .nav-link.active,.nav-underline .show>.nav-link { + color: var(--cui-nav-underline-link-active-color, #321fdb); + background: transparent; + border-color: var(--cui-nav-underline-link-active-border-color, #321fdb) +} + + + + + + +.custom-control-label::before, .custom-control-label::after { + top: 0.1rem; + width: 2rem; + height: 1rem; + } + + .custom-control-input:checked ~ .custom-control-label::before { + color: #fff; + border-color: #3498db; + background-color: #3498db; + } + + .custom-control-input:focus ~ .custom-control-label::before { + box-shadow: 0 0 0 0.2rem rgba(52, 152, 219, 0.25); + } + + h5.cloner-sections { + color: #3498db; + margin-bottom: 5px; + font-size: 15px; + font-weight: 600; } + + .nav-link { + color: #333; + } + + .nav-link.active { + color: #3498db; + border-bottom: 2px solid #3498db; + } + + .command-sections c-card-body{ + display: flex!important; + justify-content: space-between!important; + padding:5px!important; + align-items: center!important; + justify-content: space-between!important; + border-radius: 4px; +} + +.command-sections c-card-body h6{ + margin: 0!important; + font-size: 12px!important; + color: var(--primary-color); + white-space: nowrap!important; + overflow: hidden!important; + text-overflow: ellipsis!important; + width: 80%!important; + font-weight: bold; + +} +.command-sections c-card{ + border: 1px solid #e0e0e0!important;; + border-radius: 4px!important;; +} + + /* Checkbox Styling */ + .custom-switch { + position: relative; + width: 40px; + height: 20px; + } + + .custom-switch input { + display: none; + } + + .custom-control-label { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + border-radius: 20px; + background: #ccc; + cursor: pointer; + transition: all 0.3s ease; + } + + .custom-control-label::after { + content: ''; + position: absolute; + left: 2px; + top: 2px; + width: 16px; + height: 16px; + background: #fff; + border-radius: 50%; + transition: all 0.3s ease; + } + + .custom-switch input:checked + .custom-control-label { + background: #3498db; + } + + .custom-switch input:checked + .custom-control-label::after { + left: calc(100% - 18px); + } \ No newline at end of file diff --git a/src/app/views/dashboard/dashboard.component.html b/src/app/views/dashboard/dashboard.component.html index e454cf9..af6b2e9 100644 --- a/src/app/views/dashboard/dashboard.component.html +++ b/src/app/views/dashboard/dashboard.component.html @@ -139,9 +139,8 @@ Copy
- Not Registred - Learn how to register and get automatic - updates! + Not Registred + License Validation failed
@@ -153,7 +152,7 @@
Unable connect to server/Check server internet connection
-
+
Serial: Update availble
+

License User name is not set in settings read more!

+

Serial number not submitedread more!

+ - - - - - - - - + + + + +

+ {{ devdata["name"] }} ( {{ devdata["ip"] }} ) +

+
+ +
+
+
- {{value}} - {{item['default-name']}} -
-
+ + - - - - {{value}} - - - - - -
{{convert_bw_human(value,'rx')}}
-
-
- - - - {{convert_bw_human(value,'tx')}} - - - - - - curr:{{value}}
- max : {{item['max-l2mtu']}} -
-
- - - - {{convert_bw_human(value,'rx')}} - - - - - - - {{convert_bw_human(value,'tx')}} - - - - - - {{value}} - - - - - - {{value}} - - - - - {{value}} - - - - - - - -
-
-
-
-
-
- - - - -
Radio data
- -
{{raddata.key}}
- + +
Loading...
+ + + + + + + + + + + + + + + + + + + + - - - - - - - - - - -
{{d.key}}{{d.value}}
+ + + + + + - + + + + + + + +
+ + + + + + + + + + {{ value }} - {{ item["default-name"] }} + + - - - - - - - - - -
{{d.key}}{{d.value}}
-
- - - - - - - - - - -
{{d.key}}{{d.value}}
-
- - - - - - - - - - -
{{d.key}}{{d.value}}
-
+ + + {{ value }} + + + + +
{{ convert_bw_human(value, "rx") }}
+
+
+ + + {{ convert_bw_human(value, "tx") }} + + + + + curr:{{ value }}
+ max : {{ item["max-l2mtu"] }} +
+
+ + + {{ convert_bw_human(value, "rx") }} + + + + + {{ convert_bw_human(value, "tx") }} + + + + + {{ value }} + + + + + {{ value }} + + + + + {{ value }} + + + + + + + + +
+
+
+
- - - - - - - - - -
Strength at rates - {{st}} -
-
+ + + + + + +
Radio data
+
+ +
{{ raddata.key }}
+ + + + + + + + + + + + +
+ {{ d.key }} + {{ d.value }}
+
+ + + + + + + + + + +
+ {{ d.key }} + {{ d.value }}
+
+ + + + + + + + + + +
+ {{ d.key }} + {{ d.value }}
+
+ + + + + + + + + + +
{{ d.key }}{{ d.value }}
+
+
+ + + + + + + + + +
+ Strength at rates + + {{ st }} +
+
+
+
+
+
- - +
+ + + + + + + + + + + + + + +
- -
{{interface_rate['name']}}
+
{{ interface_rate["name"] }}
- - + + + + + + + + + + + + + +
History for {{current_dhcp['mac-address']}}
+ +
+ + + + +
{{value}}
+
+
+ + + {{value}} + + + + + {{value}} + + + + + {{item.name}} ({{value}}) + + +
+
+ + + +
\ No newline at end of file diff --git a/src/app/views/device_detail/dhcp-info/dhcp-info.component.scss b/src/app/views/device_detail/dhcp-info/dhcp-info.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/app/views/device_detail/dhcp-info/dhcp-info.component.ts b/src/app/views/device_detail/dhcp-info/dhcp-info.component.ts new file mode 100644 index 0000000..31d21c9 --- /dev/null +++ b/src/app/views/device_detail/dhcp-info/dhcp-info.component.ts @@ -0,0 +1,47 @@ +import { AfterContentInit, ChangeDetectionStrategy, ChangeDetectorRef, Component,Input } from '@angular/core'; +import { dataProvider } from "../../../providers/mikrowizard/data"; +import { formatInTimeZone } from "date-fns-tz"; + +@Component({ + selector: 'app-dhcp-info', + templateUrl: './dhcp-info.component.html', + styleUrls: ['./dhcp-info.component.scss'], + changeDetection: ChangeDetectionStrategy.Default +}) +export class DhcpInfoComponent implements AfterContentInit { + @Input() dhcp_server_data: any; + @Input() small_screen: boolean=false; + @Input() columnMenu: any; + @Input() sorting: any; + @Input() searching: any; + @Input() infoPanel: any; + @Input() paging: any; + @Input() rowSelectionMode: any; + @Input() tz: any; + dhcp_history:any; + dhcp_history_modal: boolean = false; + current_dhcp:any; + constructor( + private changeDetectorRef: ChangeDetectorRef, + private data_provider: dataProvider, + ) {} + + show_history(item:any) { + var _self = this; + this.data_provider.getDhcpHistory(item).then((res) => { + _self.current_dhcp = item; + this.dhcp_history = res.map((d: any) => { + d.eventtime = formatInTimeZone( + d.eventtime.split(".")[0] + ".000Z", + _self.tz, + "yyyy-MM-dd HH:mm:ss XXX" + ); + return d; + }); + this.dhcp_history_modal=!this.dhcp_history_modal; + }); + } + ngAfterContentInit(): void { + this.changeDetectorRef.detectChanges(); + } +} diff --git a/src/app/views/device_detail/dhcp-info/dhcp-info.module.ts b/src/app/views/device_detail/dhcp-info/dhcp-info.module.ts new file mode 100644 index 0000000..d493f2a --- /dev/null +++ b/src/app/views/device_detail/dhcp-info/dhcp-info.module.ts @@ -0,0 +1,57 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +import { + ButtonGroupModule, + ButtonModule, + CardModule, + FormModule, + GridModule, + ProgressModule, + NavbarModule, + AlertModule, + ModalModule, + TableModule, + UtilitiesModule, + BadgeModule, + NavModule, + TabsModule, +} from '@coreui/angular'; +import { ChartjsModule } from '@coreui/angular-chartjs'; + +import { WidgetsModule } from "../../widgets/widgets.module"; + +// import { WidgetsRoutingModule } from './widgets-routing.module'; +import { DhcpInfoComponent } from './dhcp-info.component'; +import { GuiGridModule } from "@generic-ui/ngx-grid"; + +@NgModule({ + declarations: [ + DhcpInfoComponent, + ], + imports: [ + CardModule, + AlertModule, + CommonModule, + GridModule, + ProgressModule, + FormModule, + ButtonModule, + ButtonGroupModule, + ChartjsModule, + WidgetsModule, + NavbarModule, + ModalModule, + GuiGridModule, + TableModule, + UtilitiesModule, + BadgeModule, + NavModule, + TabsModule, + ], + exports: [ + DhcpInfoComponent, + ] +}) +export class DhcpInfoModule { +} diff --git a/src/app/views/device_detail/dhcp-info/dhcp-inforouting.module.ts b/src/app/views/device_detail/dhcp-info/dhcp-inforouting.module.ts new file mode 100644 index 0000000..7216364 --- /dev/null +++ b/src/app/views/device_detail/dhcp-info/dhcp-inforouting.module.ts @@ -0,0 +1,21 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { DhcpInfoComponent } from './dhcp-info.component'; + +const routes: Routes = [ + { + path: '', + component: DhcpInfoComponent, + data: { + title: 'Widgets' + } + } +]; + + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule] +}) +export class DhcpInfoRoutingModule { +} diff --git a/src/app/views/device_detail/ping-status/ping-status-routing.module.ts b/src/app/views/device_detail/ping-status/ping-status-routing.module.ts new file mode 100644 index 0000000..2aa3361 --- /dev/null +++ b/src/app/views/device_detail/ping-status/ping-status-routing.module.ts @@ -0,0 +1,21 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { PingStatsComponent } from './ping-status.component'; + +const routes: Routes = [ + { + path: '', + component: PingStatsComponent, + data: { + title: 'Widgets' + } + } +]; + + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule] +}) +export class PingStatsRoutingModule { +} diff --git a/src/app/views/device_detail/ping-status/ping-status.component.html b/src/app/views/device_detail/ping-status/ping-status.component.html new file mode 100644 index 0000000..10a201c --- /dev/null +++ b/src/app/views/device_detail/ping-status/ping-status.component.html @@ -0,0 +1,42 @@ + + + + +
Ping status
+
+ +
Successful pings : + {{ping['successful_pings']}}
+
| Failed pings : {{ping['failed_pings']}}
+
| Avrage : + {{ping['average_ping_time']}} ms
+ + + + + + + + + + + + + + + + + + + + + +
#HostTimeStatusQualitydetails
{{i+1}}{{item['host']}}{{item['time']}} ms{{item['status']}}{{item['ping_quality']}}
+
+
+
+
\ No newline at end of file diff --git a/src/app/views/device_detail/ping-status/ping-status.component.scss b/src/app/views/device_detail/ping-status/ping-status.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/app/views/device_detail/ping-status/ping-status.component.ts b/src/app/views/device_detail/ping-status/ping-status.component.ts new file mode 100644 index 0000000..ec2a2b8 --- /dev/null +++ b/src/app/views/device_detail/ping-status/ping-status.component.ts @@ -0,0 +1,19 @@ +import { AfterContentInit, ChangeDetectionStrategy, ChangeDetectorRef, Component,Input } from '@angular/core'; + +@Component({ + selector: 'app-ping-stats', + templateUrl: './ping-status.component.html', + styleUrls: ['./ping-status.component.scss'], + changeDetection: ChangeDetectionStrategy.Default +}) +export class PingStatsComponent implements AfterContentInit { + @Input() ping: any; + constructor( + private changeDetectorRef: ChangeDetectorRef + ) {} + + ngAfterContentInit(): void { + + this.changeDetectorRef.detectChanges(); + } +} diff --git a/src/app/views/device_detail/ping-status/ping-status.module.ts b/src/app/views/device_detail/ping-status/ping-status.module.ts new file mode 100644 index 0000000..cc3b3a0 --- /dev/null +++ b/src/app/views/device_detail/ping-status/ping-status.module.ts @@ -0,0 +1,46 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +import { + ButtonGroupModule, + ButtonModule, + CardModule, + FormModule, + GridModule, + ProgressModule, + NavbarModule, + ModalModule, + TableModule, + UtilitiesModule, + BadgeModule, + TooltipModule, +} from '@coreui/angular'; + +// import { WidgetsRoutingModule } from './widgets-routing.module'; +import { PingStatsComponent } from './ping-status.component'; + +@NgModule({ + declarations: [ + PingStatsComponent, + ], + imports: [ + CardModule, + CommonModule, + GridModule, + ProgressModule, + FormModule, + ButtonModule, + ButtonGroupModule, + NavbarModule, + ModalModule, + TableModule, + UtilitiesModule, + BadgeModule, + TooltipModule, + ], + exports: [ + PingStatsComponent, + ] +}) +export class PingStatsModule { +} diff --git a/src/app/views/device_detail/radio-data/interface-bw.component.html b/src/app/views/device_detail/radio-data/interface-bw.component.html new file mode 100644 index 0000000..250b2e4 --- /dev/null +++ b/src/app/views/device_detail/radio-data/interface-bw.component.html @@ -0,0 +1,77 @@ + + +
Radio data
+
+ +
{{raddata.key}}
+ + + + + + + + + + + + +
{{d.key}}{{d.value}}
+
+ + + + + + + + + + + +
{{d.key}}{{d.value}}
+
+ + + + + + + + + + +
{{d.key}}{{d.value}}
+
+ + + + + + + + + + +
{{d.key}}{{d.value}}
+
+
+ + + + + + + + + +
+ Strength at rates + {{st}} +
+
+
+
+
\ No newline at end of file diff --git a/src/app/views/device_detail/radio-data/radio-data-routing.module.ts b/src/app/views/device_detail/radio-data/radio-data-routing.module.ts new file mode 100644 index 0000000..53b7f99 --- /dev/null +++ b/src/app/views/device_detail/radio-data/radio-data-routing.module.ts @@ -0,0 +1,21 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { RadioDataComponent } from './radio-data.component'; + +const routes: Routes = [ + { + path: '', + component: RadioDataComponent, + data: { + title: 'Widgets' + } + } +]; + + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule] +}) +export class RadioDataRoutingModule { +} diff --git a/src/app/views/device_detail/radio-data/radio-data.component.scss b/src/app/views/device_detail/radio-data/radio-data.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/app/views/device_detail/radio-data/radio-data.component.ts b/src/app/views/device_detail/radio-data/radio-data.component.ts new file mode 100644 index 0000000..ace8e53 --- /dev/null +++ b/src/app/views/device_detail/radio-data/radio-data.component.ts @@ -0,0 +1,24 @@ +import { AfterContentInit, ChangeDetectionStrategy, ChangeDetectorRef, Component,Input } from '@angular/core'; + +@Component({ + selector: 'app-iradio-data', + templateUrl: './radio-data.component.html', + styleUrls: ['./radio-data.component.scss'], + changeDetection: ChangeDetectionStrategy.Default +}) +export class RadioDataComponent implements AfterContentInit { + @Input() raddata: any; + constructor( + private changeDetectorRef: ChangeDetectorRef + ) {} + + objectlen(object:any){ + return Object.keys(object).length; + } + strangth_at_rate_extract(data:string){ + return data.split(','); + } + ngAfterContentInit(): void { + this.changeDetectorRef.detectChanges(); + } +} diff --git a/src/app/views/device_detail/radio-data/radio-data.module.ts b/src/app/views/device_detail/radio-data/radio-data.module.ts new file mode 100644 index 0000000..0b286bb --- /dev/null +++ b/src/app/views/device_detail/radio-data/radio-data.module.ts @@ -0,0 +1,56 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +import { + ButtonGroupModule, + ButtonModule, + CardModule, + FormModule, + GridModule, + ProgressModule, + NavbarModule, + AlertModule, + ModalModule, + TableModule, + UtilitiesModule, + BadgeModule, + NavModule, + TabsModule, +} from '@coreui/angular'; +import { IconModule } from '@coreui/icons-angular'; +import { ChartjsModule } from '@coreui/angular-chartjs'; + +import { WidgetsModule } from "../../widgets/widgets.module"; + +// import { WidgetsRoutingModule } from './widgets-routing.module'; +import { RadioDataComponent } from './radio-data.component'; + +@NgModule({ + declarations: [ + RadioDataComponent, + ], + imports: [ + CardModule, + AlertModule, + CommonModule, + GridModule, + ProgressModule, + FormModule, + ButtonModule, + ButtonGroupModule, + ChartjsModule, + WidgetsModule, + NavbarModule, + ModalModule, + TableModule, + UtilitiesModule, + BadgeModule, + NavModule, + TabsModule, + ], + exports: [ + RadioDataComponent, + ] +}) +export class RadioDataModule { +} diff --git a/src/app/views/device_logs/devlogs.component.html b/src/app/views/device_logs/devlogs.component.html index 89ffbf0..5d06c01 100644 --- a/src/app/views/device_logs/devlogs.component.html +++ b/src/app/views/device_logs/devlogs.component.html @@ -1,10 +1,22 @@ - + - - Device LOGS + +
Device LOGS + + + +
+ Filtered Result For Device ID + {{devid}} + + Showing last 24 hours logs by default. Use filters to modify the date and time. +
@@ -176,8 +179,8 @@
Please select searching method
- +
@@ -256,8 +259,9 @@
-
- Empty username and password means system default +
+ Empty username and password means system default configuration
@@ -271,9 +275,7 @@ + [source]="ExecutedData" [columnMenu]="columnMenu" [sorting]="sorting" [infoPanel]="infoPanel" [paging]="paging">   {{value}} @@ -286,18 +288,18 @@   {{value}} - + {{value}} - + -
+

@@ -377,7 +379,8 @@ peer ip - diff --git a/src/app/views/devices/devices.component.ts b/src/app/views/devices/devices.component.ts index edb012b..ef56bf8 100644 --- a/src/app/views/devices/devices.component.ts +++ b/src/app/views/devices/devices.component.ts @@ -173,6 +173,9 @@ export class DevicesComponent implements OnInit, OnDestroy { case "logauth": this.router.navigate(["/authlog", { devid: dev.id }]); break; + case "devlogs": + this.router.navigate(["/devlogs", { devid: dev.id }]); + break; case "logacc": this.router.navigate(["/accountlog", { devid: dev.id }]); break; @@ -403,6 +406,9 @@ export class DevicesComponent implements OnInit, OnDestroy { "danger" ); } + else if ("error" in res) { + _self.show_toast("Error", res.error, "danger"); + } else{ _self.show_toast("info", "Updating Firmwares Sent", "light"); _self.initGridTable();} diff --git a/src/app/views/syslog/syslog.component.html b/src/app/views/syslog/syslog.component.html index 6be0bf3..0f804b1 100644 --- a/src/app/views/syslog/syslog.component.html +++ b/src/app/views/syslog/syslog.component.html @@ -3,8 +3,11 @@ - - Devices + +
Devices
+ + Showing last 24 hours logs by default. Use filters to modify the date and time. +
- - | + + size="sm" style="color: #fff;"> Scan +
- + + + + + + Group @@ -37,6 +43,10 @@ (selectedRows)="onSelectedRows($event)" [autoResizeWidth]=true> + @@ -55,7 +65,7 @@
{{value}}
-
@@ -115,9 +125,9 @@ - + Upgrade Firmware
@@ -154,8 +164,9 @@ Firmware
  • - +
  • + @@ -259,8 +270,6 @@
    -
    Empty username and password means system default configuration
    @@ -269,39 +278,68 @@ -
    Scan History :
    +
    Task History
    - - - - - -   {{value}} - - - -   {{value}} - - - -   {{value}} - - - - {{value}} - - - - - - - - -
    -
    -
    + +
    + + Filter by Type + + +
    + + + + + + {{getTaskTypeLabel(value)}} + + + + +
    +
    Start: {{item.started}}
    +
    End: {{item.ended}}
    +
    +
    +
    + + +
    +
    {{item.start_ip}} - {{item.end_ip}}
    +
    User: {{item.username}}
    +
    +
    +
    {{item.device_count}} devices
    +
    {{item.task_id}}
    +
    +
    +
    + + +
    + ✓ {{item.success_count}} + ✗ {{item.failed_count}} +
    +
    +
    + + + + + + +
    + + @@ -396,4 +444,213 @@
    + + + +
    Add Devices from CSV
    + +
    + +
    +
    Upload CSV File
    +

    Please upload a CSV file containing device information with columns: IP Address, Username, Password, API Port

    + +
    +
    Preview (First 3 rows):
    + + + + + + + + + + + +
    {{header}}
    {{cell}}
    +
    Column Mapping:
    +
    +
    + + +
    +
    + + +
    +
    +
    +
    + + +
    +
    + + +
    +
    +
    +
    +
    +
    + Loading... +
    +
    {{uploadStatus}}
    +
    +
    +
    Upload Complete
    +
    + Success: {{uploadResult.success}} devices added successfully
    + Failed: {{uploadResult.failed}} devices failed to add +
    + +
    +
    + + + + + +
    + + + + +
    Web Access Options
    + +
    + +

    Choose how to access the device:

    +
    + + +
    +
    + + + +
    + + + + +
    {{getTaskTypeLabel(selectedTaskDetails?.task_type)}} Results
    + +
    + +
    + + + Task Type: {{getTaskTypeLabel(selectedTaskDetails.task_type)}}
    + Started: {{selectedTaskDetails.started}}
    + Completed: {{selectedTaskDetails.ended}} +
    + +
    + IP Range: {{selectedTaskDetails.start_ip}} - {{selectedTaskDetails.end_ip}}
    + Username: {{selectedTaskDetails.username}} +
    +
    + Task ID: {{selectedTaskDetails.task_id}}
    + Total Devices: {{selectedTaskDetails.device_count}} +
    +
    +
    +
    +
    +
    +
    Detailed Results:
    +
    + +
    +
    + + + + + + + + + + + + + + + + + +
    IP AddressStatusError DetailsError Details
    {{result.ip}} + + {{result.added ? 'Success' : 'Failed'}} + + + {{result.failures || 'N/A'}} + + {{result.faileres || 'N/A'}} +
    + +
    + Success: {{selectedTaskDetails.success_count}} + Failed: {{selectedTaskDetails.failed_count}} +
    +
    + + + + +
    + \ No newline at end of file diff --git a/src/app/views/devices/devices.component.ts b/src/app/views/devices/devices.component.ts index ef56bf8..832760d 100644 --- a/src/app/views/devices/devices.component.ts +++ b/src/app/views/devices/devices.component.ts @@ -39,7 +39,7 @@ export class DevicesComponent implements OnInit, OnDestroy { public tz: string; public ispro:boolean=false; - constructor( + constructor( private data_provider: dataProvider, private route: ActivatedRoute, private router: Router, @@ -72,6 +72,7 @@ export class DevicesComponent implements OnInit, OnDestroy { @ViewChild("grid", { static: true }) gridComponent: GuiGridComponent; @ViewChildren(ToasterComponent) viewChildren!: QueryList; public source: Array = []; + public originalSource: Array = []; public columns: Array = []; public loading: boolean = true; public rows: any = []; @@ -94,6 +95,28 @@ export class DevicesComponent implements OnInit, OnDestroy { public show_pass: boolean = false; public ExecutedDataModalVisible: boolean = false; public ExecutedData: any = []; + public filteredExecutedData: any = []; + public selectedTaskType: string = 'all'; + public detailsModalVisible: boolean = false; + public selectedTaskDetails: any = null; + public detailsCurrentPage: number = 1; + public detailsPageSize: number = 10; + public detailsPaginatedResults: any[] = []; + public detailsSearchTerm: string = ''; + public filteredDetailsResults: any[] = []; + public showWebAccessModal: boolean = false; + public currentDeviceInfo: any = null; + public addDeviceModalVisible: boolean = false; + public addDeviceStep: number = 1; + public csvFile: File | null = null; + public csvData: any[] = []; + public csvHeaders: string[] = []; + public csvPreview: any[] = []; + public columnMapping = { ip: '', username: '', password: '', port: '' }; + public uploadStatus: string = 'Processing devices...'; + public uploadResult = { success: 0, failed: 0, resultFile: null }; + public currentTaskId: string = ''; + public statusCheckTimer: any; toasterForm = { autohide: true, @@ -165,10 +188,12 @@ export class DevicesComponent implements OnInit, OnDestroy { this.check_firmware(); break; case "update": - this.update_firmware(); + this.ConfirmAction = "update"; + this.ConfirmModalVisible = true; break; case "upgrade": - this.upgrade_firmware(); + this.ConfirmAction = "upgrade"; + this.ConfirmModalVisible = true; break; case "logauth": this.router.navigate(["/authlog", { devid: dev.id }]); @@ -183,7 +208,8 @@ export class DevicesComponent implements OnInit, OnDestroy { this.router.navigate(["/backups", { devid: dev.id }]); break; case "reboot": - this.reboot_devices(); + this.ConfirmAction = "reboot"; + this.ConfirmModalVisible = true; break; case "delete": this.ConfirmAction = "delete"; @@ -374,6 +400,7 @@ export class DevicesComponent implements OnInit, OnDestroy { } check_firmware() { var _self = this; + this.ConfirmModalVisible = false; this.data_provider .check_firmware(this.Selectedrows.toString()) .then((res) => { @@ -396,6 +423,7 @@ export class DevicesComponent implements OnInit, OnDestroy { update_firmware() { var _self = this; + this.ConfirmModalVisible = false; this.data_provider .update_firmware(this.Selectedrows.toString()) .then((res) => { @@ -417,6 +445,7 @@ export class DevicesComponent implements OnInit, OnDestroy { upgrade_firmware() { var _self = this; + this.ConfirmModalVisible = false; this.data_provider .upgrade_firmware(this.Selectedrows.toString()) .then((res) => { @@ -436,6 +465,7 @@ export class DevicesComponent implements OnInit, OnDestroy { reboot_devices() { var _self = this; + this.ConfirmModalVisible = false; this.data_provider .reboot_devices(this.Selectedrows.toString()) .then((res) => { @@ -492,11 +522,12 @@ export class DevicesComponent implements OnInit, OnDestroy { ); } else{ - _self.source = res.map((x: any) => { + _self.originalSource = res.map((x: any) => { if (x.upgrade_availble) _self.upgrades.push(x); if (x.update_availble) _self.updates.push(x); return x; }); + _self.source = [..._self.originalSource]; _self.device_interval(); _self.loading = false; } @@ -637,26 +668,273 @@ export class DevicesComponent implements OnInit, OnDestroy { _self.tz, "yyyy-MM-dd HH:mm:ss XXX" ); - d.start_ip=d.info.start_ip; - d.end_ip=d.info.end_ip; + d.start_ip=d.info.start_ip || 'N/A'; + d.end_ip=d.info.end_ip || 'N/A'; + d.task_id=d.info.task_id || 'N/A'; + d.device_count=d.info.device_count || 0; + d.username=d.info.username || 'N/A'; d.result=JSON.parse(d.result); + d.success_count = d.result.filter((r: any) => r.added === true).length; + d.failed_count = d.result.filter((r: any) => r.added === false).length; index += 1; return d; }); + _self.filteredExecutedData = [..._self.ExecutedData]; } }); } + filterByTaskType() { + if (this.selectedTaskType === 'all') { + this.filteredExecutedData = [...this.ExecutedData]; + } else { + this.filteredExecutedData = this.ExecutedData.filter((d: any) => d.task_type === this.selectedTaskType); + } + } + showTaskDetails(task: any) { + this.selectedTaskDetails = task; + this.detailsCurrentPage = 1; + this.updateDetailsPagination(); + this.detailsModalVisible = true; + } + updateDetailsPagination() { + if (!this.selectedTaskDetails?.result) return; + + // Filter results based on search term + this.filteredDetailsResults = this.selectedTaskDetails.result.filter((result: any) => + result.ip.toLowerCase().includes(this.detailsSearchTerm.toLowerCase()) || + (result.failures && result.failures.toLowerCase().includes(this.detailsSearchTerm.toLowerCase())) || + (result.faileres && result.faileres.toLowerCase().includes(this.detailsSearchTerm.toLowerCase())) + ); + + const startIndex = (this.detailsCurrentPage - 1) * this.detailsPageSize; + const endIndex = startIndex + this.detailsPageSize; + this.detailsPaginatedResults = this.filteredDetailsResults.slice(startIndex, endIndex); + } + onDetailsPageChange(page: number) { + this.detailsCurrentPage = page; + this.updateDetailsPagination(); + } + getTotalDetailsPages(): number { + if (!this.filteredDetailsResults) return 0; + return Math.ceil(this.filteredDetailsResults.length / this.detailsPageSize); + } + onDetailsSearch() { + this.detailsCurrentPage = 1; + this.updateDetailsPagination(); + } + closeDetailsModal() { + this.detailsModalVisible = false; + this.selectedTaskDetails = null; + this.detailsPaginatedResults = []; + this.filteredDetailsResults = []; + this.detailsCurrentPage = 1; + this.detailsSearchTerm = ''; + } + filterUpdatable() { + this.source = this.originalSource.filter(device => device.update_availble); + } + filterUpgradable() { + this.source = this.originalSource.filter(device => device.upgrade_availble); + } + + clearFilter() { + this.source = [...this.originalSource]; + } + + webAccess(device: any) { + this.currentDeviceInfo = device; + if (this.ispro) { + this.showWebAccessModal = true; + } else { + this.openDirectAccess(); + } + } + + openProxyAccess() { + if (this.currentDeviceInfo?.id) { + window.open(`/api/proxy/init?devid=${this.currentDeviceInfo.id}`, '_blank'); + } else { + const ip = this.currentDeviceInfo?.ip; + if (ip) { + window.open(`/api/proxy/init?dev_ip=${ip}`, '_blank'); + } + } + this.showWebAccessModal = false; + } + + openDirectAccess() { + const ip = this.currentDeviceInfo?.ip; + if (ip) { + window.open(`http://${ip}`, '_blank'); + } + this.showWebAccessModal = false; + } + + closeWebAccessModal() { + this.showWebAccessModal = false; + } + + openAddDeviceModal() { + this.addDeviceModalVisible = true; + this.resetAddDeviceForm(); + } + + closeAddDeviceModal() { + this.addDeviceModalVisible = false; + this.resetAddDeviceForm(); + } + + resetAddDeviceForm() { + this.addDeviceStep = 1; + this.csvFile = null; + this.csvData = []; + this.csvHeaders = []; + this.csvPreview = []; + this.columnMapping = { ip: '', username: '', password: '', port: '' }; + this.uploadStatus = 'Processing devices...'; + this.uploadResult = { success: 0, failed: 0, resultFile: null }; + this.currentTaskId = ''; + clearTimeout(this.statusCheckTimer); + } + + onFileSelected(event: any) { + const file = event.target.files[0]; + if (file && file.type === 'text/csv') { + this.csvFile = file; + this.parseCSV(file); + } + } + + parseCSV(file: File) { + const reader = new FileReader(); + reader.onload = (e: any) => { + const csv = e.target.result; + const lines = csv.split('\n').filter((line: string) => line.trim()); + + if (lines.length > 0) { + this.csvHeaders = lines[0].split(',').map((header: string) => header.trim()); + this.csvData = lines.slice(1).map((line: string) => + line.split(',').map((cell: string) => cell.trim()) + ); + this.csvPreview = this.csvData.slice(0, 3); + } + }; + reader.readAsText(file); + } + + isValidMapping(): boolean { + return this.columnMapping.ip !== '' && + this.columnMapping.username !== '' && + this.columnMapping.password !== '' && + this.columnMapping.port !== '' && + this.csvData.length > 0; + } + + uploadDevices() { + if (!this.isValidMapping()) return; + + this.addDeviceStep = 2; + + const devices = this.csvData.map(row => ({ + ip: row[parseInt(this.columnMapping.ip)], + username: row[parseInt(this.columnMapping.username)], + password: row[parseInt(this.columnMapping.password)], + port: row[parseInt(this.columnMapping.port)] + })); + + this.data_provider.bulk_add_devices(devices).then((res) => { + if ('error' in res) { + this.addDeviceStep = 3; + this.show_toast('Error', 'Failed to start device upload', 'danger'); + this.uploadResult = { success: 0, failed: devices.length, resultFile: null }; + } else if ('taskId' in res) { + this.currentTaskId = res.taskId; + this.uploadStatus = 'Processing devices...'; + this.checkUploadStatus(); + } else { + this.addDeviceStep = 3; + this.show_toast('Error', 'Invalid response from server', 'danger'); + this.uploadResult = { success: 0, failed: devices.length, resultFile: null }; + } + }).catch(() => { + this.addDeviceStep = 3; + this.uploadResult = { success: 0, failed: devices.length, resultFile: null }; + this.show_toast('Error', 'Failed to upload devices', 'danger'); + }); + } + + checkUploadStatus() { + clearTimeout(this.statusCheckTimer); + + this.data_provider.bulk_add_status(this.currentTaskId).then((res) => { + if ('error' in res) { + this.addDeviceStep = 3; + this.show_toast('Error', 'Failed to check upload status', 'danger'); + this.uploadResult = { success: 0, failed: 0, resultFile: null }; + return; + } + + if (res.status === 'completed') { + this.addDeviceStep = 3; + this.uploadResult = { + success: res.success || 0, + failed: res.failed || 0, + resultFile: res.resultFile || null + }; + this.show_toast('Success', `${res.success} devices added successfully`, 'success'); + this.initGridTable(); + } else if (res.status === 'failed') { + this.addDeviceStep = 3; + this.show_toast('Error', res.message || 'Upload failed', 'danger'); + this.uploadResult = { success: 0, failed: 0, resultFile: null }; + } else { + // Still processing + this.uploadStatus = res.message || 'Processing devices...'; + this.statusCheckTimer = setTimeout(() => { + this.checkUploadStatus(); + }, 3000); + } + }).catch(() => { + this.addDeviceStep = 3; + this.show_toast('Error', 'Failed to check upload status', 'danger'); + this.uploadResult = { success: 0, failed: 0, resultFile: null }; + }); + } + + downloadResults() { + if (this.uploadResult.resultFile) { + const link = document.createElement('a'); + link.href = this.uploadResult.resultFile; + link.download = 'device_upload_results.csv'; + link.click(); + } + } + + getTaskTypeLabel(taskType: string): string { + switch(taskType) { + case 'ip-scan': return 'IP Scan'; + case 'bulk-add': return 'Bulk Add'; + default: return taskType; + } + } + + getStatusColor(success: number, failed: number): string { + if (failed === 0) return 'success'; + if (success === 0) return 'danger'; + return 'warning'; + } ngOnDestroy(): void { clearTimeout(this.scan_timer); + clearTimeout(this.statusCheckTimer); } } diff --git a/src/app/views/devices/devices.module.ts b/src/app/views/devices/devices.module.ts index f16b801..53cd568 100644 --- a/src/app/views/devices/devices.module.ts +++ b/src/app/views/devices/devices.module.ts @@ -17,6 +17,7 @@ import { ModalModule, ListGroupModule, TooltipModule, + TableModule, } from "@coreui/angular"; import { MatMenuModule } from "@angular/material/menu"; import { DevicesRoutingModule } from "./devices-routing.module"; @@ -44,6 +45,7 @@ import { GuiGridModule } from "@generic-ui/ngx-grid"; ListGroupModule, MatMenuModule, TooltipModule, + TableModule, ], declarations: [DevicesComponent], }) From 996c189076d58b892e42e7027f222f6987f25737 Mon Sep 17 00:00:00 2001 From: sepehr Date: Thu, 16 Oct 2025 17:33:19 +0300 Subject: [PATCH 06/15] feat: redesign device group management interface - Complete UI/UX redesign of device groups table - Enhanced member addition interface - Add user management directly from device groups - Add firmware group actions (update and upgrade) - Improved bulk operations for device groups --- .../devices_group/devgroup.component.html | 460 ++++++++++++++---- .../devices_group/devgroup.component.scss | 108 ++++ .../views/devices_group/devgroup.component.ts | 252 ++++++++++ .../views/devices_group/devgroup.module.ts | 8 + 4 files changed, 742 insertions(+), 86 deletions(-) create mode 100644 src/app/views/devices_group/devgroup.component.scss diff --git a/src/app/views/devices_group/devgroup.component.html b/src/app/views/devices_group/devgroup.component.html index fb48f5f..57f80a0 100644 --- a/src/app/views/devices_group/devgroup.component.html +++ b/src/app/views/devices_group/devgroup.component.html @@ -25,29 +25,67 @@ - All Devices + All Devices - 0 Members - {{value.length}} Members + + 0 + + + {{value.length}} + - - {{value}} + {{formatCreateTime(value)}} - - - - - + + + + {{value.length}} + + + + + + + + + +
    +
  • Actions Menu
  • + + + + + +
    +
    @@ -59,92 +97,146 @@ - - -
    Group Edit
    + + +
    Edit Device Group
    - - -
    - - + + +
    +
    + +
    - - -
    Group Members :
    - - - -   {{value}} - - - - {{value}} - - - - - - - - -
    - -
    -
    - + + {{groupMembers.length}} Devices + +
    + + + + +
    Current Group Members
    +
    + + +
    +
    + +
    + +
    No devices in this group
    +

    Click "Add Devices" to start adding devices to this group

    +
    +
    + + + +
    + + {{value}} +
    +
    +
    + + + {{value}} + + + + + + + +
    +
    +
    +
    - - + + - - -
    Members not in Group
    + + +
    Add Devices to Group
    - - -
    Members Availble to add:
    - - - -   {{value}} - - - - {{value}} - - - - - {{value}} - - - -
    -
    -
    + + +
    + + + {{NewMemberRows.length}} device(s) selected for addition + +
    + + + + +
    Available Devices ({{availbleMembers.length}} total)
    +
    + +
    + +
    All devices are already in groups
    +

    No available devices to add to this group

    +
    +
    + + + +
    + + {{value}} +
    +
    +
    + + + {{value}} + + + + + {{value}} + + +
    +
    +
    +
    - - - + +
    + Select devices from the list above to add them to the group +
    +
    + + +
    @@ -175,4 +267,200 @@ Close
    + + + +
    User Permissions - {{selectedGroup?.name}}
    + +
    + + + +
    Add User Permission
    +
    + + + +
    + + +
    +
    + {{user.username}} ({{user.first_name}} {{user.last_name}}) +
    +
    +
    + No users found +
    +
    +
    + +
    + + +
    +
    + {{perm.name}} +
    +
    +
    + No permissions found +
    +
    +
    + + + +
    +
    +
    + + +
    Current Permissions ({{selectedGroup?.assigned_users?.length || 0}} users)
    +
    + +
    + +

    No users have permissions for this group

    +
    +
    + + + + + + + + + + + + + + + + + + + +
    #UserNamePermissionActions
    {{i + 1}} +
    + + {{user.username}} +
    +
    {{user.first_name}} {{user.last_name}} + {{user.perm_name}} + +
    + + +
    +
    +
    +
    +
    +
    + + + +
    + + + +
    Change Permission for {{editingUser?.username}}
    + +
    + +
    + +
    +
    + + +
    +
    + + + + +
    + + + +
    Remove Permission
    + +
    + +
    + +

    Are you sure you want to remove {{removingUser?.username}}'s permission from group {{selectedGroup?.name}}?

    +

    This action cannot be undone.

    +
    +
    + + + + +
    + + +
    Confirm Firmware {{firmwareAction | titlecase}}
    + +
    + +
    + +

    Are you sure you want to {{firmwareAction}} firmware for all devices in group {{selectedGroupForFirmware?.name}}?

    +

    This action will affect all devices in this group and may take some time to complete.

    +
    +
    + + + +
    \ No newline at end of file diff --git a/src/app/views/devices_group/devgroup.component.scss b/src/app/views/devices_group/devgroup.component.scss new file mode 100644 index 0000000..c845baa --- /dev/null +++ b/src/app/views/devices_group/devgroup.component.scss @@ -0,0 +1,108 @@ +.users-summary { + min-height: 60px; + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.user-badges { + display: flex; + flex-wrap: wrap; + gap: 0.25rem; +} + +.user-badges c-badge { + font-size: 0.7rem; + max-width: 80px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +@media (max-width: 768px) { + .users-summary { + min-height: auto; + } + + .user-badges c-badge { + max-width: 60px; + font-size: 0.65rem; + } +} + +/* Search Select Components */ +.search-select-wrapper { + position: relative; +} + +.search-label { + display: block; + font-size: 0.8rem; + color: #6c757d; + margin-bottom: 0.25rem; + font-weight: 500; +} + +.search-input { + width: 100%; +} + +.search-dropdown { + position: absolute; + top: 100%; + left: 0; + right: 0; + background: white; + border: 1px solid #ced4da; + border-top: none; + border-radius: 0 0 0.375rem 0.375rem; + max-height: 200px; + overflow-y: auto; + z-index: 1000; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); +} + +.search-option { + padding: 0.5rem 0.75rem; + cursor: pointer; + border-bottom: 1px solid #f1f3f4; + font-size: 0.85rem; + transition: background-color 0.2s ease; +} + +.search-option:hover { + background-color: #f8f9fa; +} + +.search-option:last-child { + border-bottom: none; +} + +.search-no-results { + padding: 0.75rem; + text-align: center; + color: #6c757d; + font-size: 0.8rem; + font-style: italic; + background: white; + border: 1px solid #ced4da; + border-top: none; + border-radius: 0 0 0.375rem 0.375rem; + position: absolute; + top: 100%; + left: 0; + right: 0; + z-index: 1000; +} + +.compact-select { + font-size: 0.85rem; + height: calc(1.8em + 0.75rem + 2px); + padding: 0.375rem 0.75rem; + border-radius: 0.375rem; +} + +.compact-select:focus { + border-color: #0d6efd; + box-shadow: 0 0 0 0.2rem rgba(13, 110, 253, 0.25); +} \ No newline at end of file diff --git a/src/app/views/devices_group/devgroup.component.ts b/src/app/views/devices_group/devgroup.component.ts index 5ee1248..5614bfa 100644 --- a/src/app/views/devices_group/devgroup.component.ts +++ b/src/app/views/devices_group/devgroup.component.ts @@ -2,6 +2,7 @@ import { Component, OnInit } from "@angular/core"; import { dataProvider } from "../../providers/mikrowizard/data"; import { Router } from "@angular/router"; import { loginChecker } from "../../providers/login_checker"; +import { formatInTimeZone } from "date-fns-tz"; import { GuiSearching, GuiSelectedRow, @@ -31,10 +32,12 @@ interface IUser { @Component({ templateUrl: "devgroup.component.html", + styleUrls: ["devgroup.component.scss"] }) export class DevicesGroupComponent implements OnInit { public uid: number; public uname: string; + public tz: string; constructor( private data_provider: dataProvider, @@ -50,6 +53,7 @@ export class DevicesGroupComponent implements OnInit { this.data_provider.getSessionInfo().then((res) => { _self.uid = res.uid; _self.uname = res.name; + _self.tz = res.tz; const userId = _self.uid; if (res.role != "admin") { @@ -83,6 +87,30 @@ export class DevicesGroupComponent implements OnInit { id: 0, name: "", }; + public selectedGroup: any = null; + public UserManagementModalVisible: boolean = false; + public EditPermissionModalVisible: boolean = false; + public RemovePermissionModalVisible: boolean = false; + public availableUsers: any[] = []; + public availablePermissions: any[] = []; + public selectedUserId: string = ""; + public selectedPermId: string = ""; + public selectedUser: any = null; + public selectedPermission: any = null; + public userSearch: string = ''; + public permissionSearch: string = ''; + public filteredUsers: any[] = []; + public filteredPermissions: any[] = []; + public showUserDropdown: boolean = false; + public showPermissionDropdown: boolean = false; + public editingUser: any = null; + public removingUser: any = null; + public newPermissionId: string = ""; + private deviceCache: { [key: number]: any[] } = {}; + private loadingDevices: { [key: number]: boolean } = {}; + public FirmwareConfirmModalVisible: boolean = false; + public firmwareAction: string = ""; + public selectedGroupForFirmware: any = null; public DefaultCurrentGroup: any = { array_agg: [], created: "", @@ -182,6 +210,9 @@ export class DevicesGroupComponent implements OnInit { save_group() { var _self = this; this.data_provider.update_save_group(this.currentGroup).then((res) => { + // Clear device cache for this group + delete this.deviceCache[this.currentGroup.id]; + delete this.loadingDevices[this.currentGroup.id]; _self.initGridTable(); _self.EditGroupModalVisible = false; }); @@ -237,4 +268,225 @@ export class DevicesGroupComponent implements OnInit { this.loading = false; }); } + + manageUsers(group: any): void { + this.selectedGroup = { ...group }; + this.loadAvailableUsers(); + this.loadAvailablePermissions(); + this.UserManagementModalVisible = true; + } + + loadAvailableUsers(): void { + this.data_provider.get_users(1, 1000, "").then((res) => { + this.availableUsers = res.filter((user: any) => + !this.selectedGroup.assigned_users.some((assignedUser: any) => assignedUser.user_id === user.id) + ); + this.filteredUsers = [...this.availableUsers]; + }); + } + + loadAvailablePermissions(): void { + this.data_provider.get_perms(1, 1000, "").then((res) => { + this.availablePermissions = res; + this.filteredPermissions = [...this.availablePermissions]; + }); + } + + filterUsers(event: any): void { + const query = event.target.value.toLowerCase(); + this.filteredUsers = this.availableUsers.filter((user: any) => + user.username.toLowerCase().includes(query) || + (user.first_name + ' ' + user.last_name).toLowerCase().includes(query) + ); + } + + filterPermissions(event: any): void { + const query = event.target.value.toLowerCase(); + this.filteredPermissions = this.availablePermissions.filter((perm: any) => + perm.name.toLowerCase().includes(query) + ); + } + + selectUser(user: any): void { + this.selectedUser = user; + this.selectedUserId = user.id; + this.userSearch = user.username + ' (' + user.first_name + ' ' + user.last_name + ')'; + this.showUserDropdown = false; + } + + selectPermission(perm: any): void { + this.selectedPermission = perm; + this.selectedPermId = perm.id; + this.permissionSearch = perm.name; + this.showPermissionDropdown = false; + } + + hideUserDropdown(): void { + setTimeout(() => this.showUserDropdown = false, 200); + } + + hidePermissionDropdown(): void { + setTimeout(() => this.showPermissionDropdown = false, 200); + } + + addUserPermission(): void { + if (!this.selectedUser || !this.selectedPermission) return; + + console.log('Adding user permission:', { + userId: this.selectedUser.id, + permissionId: this.selectedPermission.id, + groupId: this.selectedGroup.id, + selectedUser: this.selectedUser, + selectedPermission: this.selectedPermission + }); + + this.data_provider.Add_user_perm(this.selectedUser.id, +this.selectedPermission.id, this.selectedGroup.id) + .then((res) => { + console.log('Add user permission response:', res); + this.initGridTable(); + this.selectedUserId = ""; + this.selectedPermId = ""; + this.selectedUser = null; + this.selectedPermission = null; + this.userSearch = ""; + this.permissionSearch = ""; + // Refresh the selected group data + this.data_provider.get_devgroup_list().then((groups) => { + this.selectedGroup = groups.find((g: any) => g.id === this.selectedGroup.id); + this.loadAvailableUsers(); + }); + }); + } + + editUserPermission(user: any): void { + this.editingUser = { ...user }; + this.newPermissionId = user.perm_id.toString(); + this.EditPermissionModalVisible = true; + } + + updateUserPermission(): void { + if (!this.newPermissionId) return; + + // Remove old permission and add new one + this.data_provider.Delete_user_perm(this.editingUser.id).then(() => { + this.data_provider.Add_user_perm(this.editingUser.user_id, +this.newPermissionId, this.selectedGroup.id) + .then(() => { + this.EditPermissionModalVisible = false; + this.initGridTable(); + // Refresh the selected group data + this.data_provider.get_devgroup_list().then((groups) => { + this.selectedGroup = groups.find((g: any) => g.id === this.selectedGroup.id); + }); + }); + }); + } + + removeUserPermission(user: any): void { + this.removingUser = { ...user }; + this.RemovePermissionModalVisible = true; + } + + confirmRemovePermission(): void { + this.data_provider.Delete_user_perm(this.removingUser.id).then(() => { + this.RemovePermissionModalVisible = false; + this.initGridTable(); + // Refresh the selected group data + this.data_provider.get_devgroup_list().then((groups) => { + this.selectedGroup = groups.find((g: any) => g.id === this.selectedGroup.id); + this.loadAvailableUsers(); + }); + }); + } + + getPermissionColor(permName: string): string { + const colorMap: { [key: string]: string } = { + 'full': 'success', + 'read': 'info', + 'write': 'warning', + 'admin': 'danger', + 'test': 'secondary' + }; + return colorMap[permName] || 'primary'; + } + + getUsersTooltip(users: any[]): string { + if (users.length === 0) return 'No users assigned'; + + const maxShow = 10; + const userList = users.slice(0, maxShow).map((user, index) => + `• ${user.username} (${user.perm_name})` + ).join('\n'); + + return users.length > maxShow + ? `${userList}\n━━━━━━━━━━━━━━━━\n+${users.length - maxShow} more users` + : userList; + } + + getDevicesTooltip(group: any): string { + if (group.id === 1) return 'All devices in the system'; + if (!group.array_agg || group.array_agg[0] === null) return 'No devices assigned'; + + // Check if data is cached + if (this.deviceCache[group.id]) { + const devices = this.deviceCache[group.id]; + const maxShow = 10; + const deviceList = devices.slice(0, maxShow).map(device => + `• ${device.name} (${device.ip})` + ).join('\n'); + + return devices.length > maxShow + ? `${deviceList}\n━━━━━━━━━━━━━━━━\n+${devices.length - maxShow} more devices` + : deviceList; + } + + // Check if already loading + if (this.loadingDevices[group.id]) { + return 'Loading devices...'; + } + + // Start loading + this.loadingDevices[group.id] = true; + this.data_provider.get_devgroup_members(group.id).then((devices) => { + this.deviceCache[group.id] = devices; + this.loadingDevices[group.id] = false; + }).catch(() => { + this.loadingDevices[group.id] = false; + }); + + return 'Loading devices...'; + } + + formatCreateTime(dateString: string): string { + if (!dateString || !this.tz) return dateString; + return formatInTimeZone( + dateString.split(".")[0] + ".000Z", + this.tz, + "yyyy-MM-dd HH:mm:ss XXX" + ); + } + + groupFirmwareAction(group: any, action: string): void { + this.selectedGroupForFirmware = group; + this.firmwareAction = action; + this.FirmwareConfirmModalVisible = true; + } + + confirmGroupFirmwareAction(): void { + if (!this.selectedGroupForFirmware) return; + + this.data_provider.group_firmware_action(this.selectedGroupForFirmware.id, this.firmwareAction) + .then((res) => { + if ("error" in res) { + console.error('Firmware action failed:', res.error); + } else { + const actionText = this.firmwareAction === 'update' ? 'Update' : 'Upgrade'; + console.log(`${actionText} firmware initiated for group: ${this.selectedGroupForFirmware.name}`); + } + this.FirmwareConfirmModalVisible = false; + }) + .catch((error) => { + console.error('Firmware action error:', error); + this.FirmwareConfirmModalVisible = false; + }); + } } diff --git a/src/app/views/devices_group/devgroup.module.ts b/src/app/views/devices_group/devgroup.module.ts index 0fe8c12..1dc3e06 100644 --- a/src/app/views/devices_group/devgroup.module.ts +++ b/src/app/views/devices_group/devgroup.module.ts @@ -2,6 +2,7 @@ import { NgModule } from "@angular/core"; import { CommonModule } from "@angular/common"; import { + AlertModule, ButtonGroupModule, ButtonModule, CardModule, @@ -9,15 +10,19 @@ import { GridModule, CollapseModule, ModalModule, + TooltipModule, + ListGroupModule, } from "@coreui/angular"; import { DevicesGroupRoutingModule } from "./devgroup-routing.module"; import { DevicesGroupComponent } from "./devgroup.component"; import { GuiGridModule } from "@generic-ui/ngx-grid"; import { BadgeModule } from "@coreui/angular"; import { FormsModule } from "@angular/forms"; +import { MatMenuModule } from "@angular/material/menu"; @NgModule({ imports: [ DevicesGroupRoutingModule, + AlertModule, CardModule, CommonModule, GridModule, @@ -29,6 +34,9 @@ import { FormsModule } from "@angular/forms"; CollapseModule, ModalModule, BadgeModule, + TooltipModule, + MatMenuModule, + ListGroupModule, ], declarations: [DevicesGroupComponent], }) From b20a3d78261f8605c0a06dde12e0dbc8d5fa8583 Mon Sep 17 00:00:00 2001 From: sepehr Date: Thu, 16 Oct 2025 17:33:27 +0300 Subject: [PATCH 07/15] feat: add network topology maps for pro users - Add comprehensive network visualization module - Pro feature with advanced topology mapping - Interactive network device mapping - Integration with navigation and routing --- src/app/app-routing.module.ts | 5 + src/app/containers/default-layout/_nav.ts | 6 + src/app/views/maps/code-typescript.txt | 377 +++++++++++++++++ src/app/views/maps/maps-routing.module.ts | 21 + src/app/views/maps/maps.component.html | 82 ++++ src/app/views/maps/maps.component.scss | 256 ++++++++++++ src/app/views/maps/maps.component.ts | 479 ++++++++++++++++++++++ src/app/views/maps/maps.module.ts | 60 +++ 8 files changed, 1286 insertions(+) create mode 100644 src/app/views/maps/code-typescript.txt create mode 100644 src/app/views/maps/maps-routing.module.ts create mode 100644 src/app/views/maps/maps.component.html create mode 100644 src/app/views/maps/maps.component.scss create mode 100644 src/app/views/maps/maps.component.ts create mode 100644 src/app/views/maps/maps.module.ts diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index 3c4186a..2b30133 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -49,6 +49,11 @@ const routes: Routes = [ loadChildren: () => import('./views/devices_group/devgroup.module').then((m) => m.DevicesGroupModule) }, + { + path: 'maps', + loadChildren: () => + import('./views/maps/maps.module').then((m) => m.MapsModule) + }, { path: 'authlog', loadChildren: () => diff --git a/src/app/containers/default-layout/_nav.ts b/src/app/containers/default-layout/_nav.ts index d313617..00c76ce 100644 --- a/src/app/containers/default-layout/_nav.ts +++ b/src/app/containers/default-layout/_nav.ts @@ -28,6 +28,12 @@ export const navItems: INavData[] = [ // linkProps: { fragment: 'someAnchor' }, icon: 'fa-solid fa-layer-group' }, + { + name: 'Network Maps', + url: '/maps', + icon:'fa-solid fa-map', + attributes: { 'pro':true } + }, // { // name: 'Tools', // url: '/login', diff --git a/src/app/views/maps/code-typescript.txt b/src/app/views/maps/code-typescript.txt new file mode 100644 index 0000000..64f7093 --- /dev/null +++ b/src/app/views/maps/code-typescript.txt @@ -0,0 +1,377 @@ +import { Component, OnInit, OnDestroy, ViewChild, ElementRef } from "@angular/core"; +import { dataProvider } from "../../providers/mikrowizard/data"; +import { loginChecker } from "../../providers/login_checker"; +import { Router } from "@angular/router"; +import { formatInTimeZone } from "date-fns-tz"; +import { Network } from 'vis-network/peer'; +import { DataSet } from 'vis-data'; + +@Component({ + templateUrl: "maps.component.html", + styleUrls: ["maps.component.scss"], +}) +export class MapsComponent implements OnInit { + public uid: number; + public uname: string; + public ispro: boolean = false; + public tz: string; + public savedPositions: any = {}; + public savedPositionsKey = "network-layout"; + public selectedDevice: any = null; + constructor( + private data_provider: dataProvider, + private router: Router, + private login_checker: loginChecker + ) { + var _self = this; + if (!this.login_checker.isLoggedIn()) { + setTimeout(function () { + _self.router.navigate(["login"]); + }, 100); + } + this.data_provider.getSessionInfo().then((res) => { + _self.uid = res.uid; + _self.uname = res.name; + _self.ispro = res.ISPRO; + if (!_self.ispro) + setTimeout(function () { + _self.router.navigate(["dashboard"]); + }, 100); + _self.tz = res.tz; + }); + } + + @ViewChild('network', { static: true }) networkContainer: ElementRef | undefined; + + mikrotikData: any[] = []; + + + + + ngOnInit(): void { + this.loadFontAwesome(); + this.savedPositions = JSON.parse(localStorage.getItem(this.savedPositionsKey) || "{}"); + this.loadNetworkData(); + } + + loadNetworkData(): void { + this.data_provider.getNetworkMap().then((res) => { + this.mikrotikData = res; + console.dir(res); + setTimeout(() => { + this.createNetworkMap(); + }, 100); + }); + } + + loadFontAwesome() { + if (!document.querySelector('link[href*="font-awesome"]')) { + const link = document.createElement('link'); + link.rel = 'stylesheet'; + link.href = 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css'; + document.head.appendChild(link); + } + } + +createNetworkMap() { + const container = this.networkContainer?.nativeElement; + if (!container) return; + + let nodes = new DataSet([]); + let edges = new DataSet([]); + let deviceMap: { [key: string]: string } = {}; // uniqueId to nodeId mapping + let allDevices: { [key: string]: any } = {}; // uniqueId to device info mapping + let macToDevice: { [key: string]: string } = {}; // MAC -> uniqueId mapping + const hasSavedPositions = Object.keys(this.savedPositions).length > 0; + let nodeIdCounter = 1; + const getUniqueId = (obj: any): string => { + if (obj.device_id) return `dev_${obj.device_id}`; + if (obj.mac) return `mac_${obj.mac}`; + if (obj.software_id) return `sw_${obj.software_id}`; + if (obj.hostname) return `host_${obj.hostname}`; + return `unknown_${obj.address || Math.random().toString(36).slice(2)}`; + }; + // Collect all devices + this.mikrotikData.forEach((device) => { + const deviceId = device.device_id || `${device.name}_${Date.now()}`; + + if (!allDevices[deviceId]) { + allDevices[deviceId] = { + name: device.name, + type: 'Router', + brand: 'MikroTik' + }; + } + + Object.entries(device.interfaces).forEach(([_, iface]: [string, any]) => { + if (iface.mac) { + macToDevice[iface.mac] = deviceId; + } + + iface.neighbors.forEach((neighbor: any) => { + const neighborId = + neighbor.device_id || + neighbor.software_id || + `${neighbor.hostname}_${neighbor.mac}_${neighbor.address || 'unknown'}`; + + if (!allDevices[neighborId]) { + allDevices[neighborId] = { + name: neighbor.hostname || neighbor.mac || 'Unknown', + type: neighbor.type || 'Router', + brand: neighbor.brand || 'MikroTik' + }; + } + + if (neighbor.mac) { + macToDevice[neighbor.mac] = neighborId; + } + }); + }); + }); + + // Create nodes + Object.entries(allDevices).forEach(([uniqueId, device]: [string, any]) => { + const nodeId = `node_${nodeIdCounter++}`; + deviceMap[uniqueId] = nodeId; + + nodes.add({ + id: nodeId, + label: device.name, + shape: 'image', + image: this.getDeviceIcon(device.type || 'Unknown', device.brand || 'Unknown'), + size: 15, + font: { size: 11, color: '#333', face: 'Arial, sans-serif' }, + ...(hasSavedPositions && this.savedPositions[nodeId] + ? { x: this.savedPositions[nodeId].x, y: this.savedPositions[nodeId].y } + : {}) + } as any); + }); + + // Create edges + this.mikrotikData.forEach((device) => { + Object.entries(device.interfaces).forEach(([ifaceName, iface]: [string, any]) => { + const sourceDeviceId = macToDevice[iface.mac]; + iface.neighbors.forEach((neighbor: any) => { + const targetDeviceId = macToDevice[neighbor.mac]; + + if (deviceMap[sourceDeviceId] && deviceMap[targetDeviceId]) { + const edgeId = `${sourceDeviceId}_${targetDeviceId}`; + const reverseId = `${targetDeviceId}_${sourceDeviceId}`; + + if (!edges.get().find(e => e.id === edgeId || e.id === reverseId)) { + edges.add({ + id: edgeId, + from: deviceMap[sourceDeviceId], + to: deviceMap[targetDeviceId], + label: ifaceName, + color: { color: '#34495e', highlight: '#3498db' }, + width: 3, + smooth: { type: 'continuous', roundness: 0.1 }, + font: { + size: 12, + color: '#2c3e50', + face: 'Arial, sans-serif', + strokeWidth: 1, + strokeColor: '#ffffff', + align: 'horizontal' + } + } as any); + } + } + }); + }); + }); + + const data = { nodes, edges }; + const options = { physics: { enabled: true, stabilization: { iterations: 100 }, barnesHut: { gravitationalConstant: -8000, centralGravity: 0.3, springLength: 200, springConstant: 0.04, damping: 0.09 } }, interaction: { hover: true, dragNodes: true, dragView: true, zoomView: true, hoverConnectedEdges: false, selectConnectedEdges: false, navigationButtons: false, keyboard: false }, nodes: { borderWidth: 3, shadow: true }, edges: { shadow: true, smooth: true, length: 150 }, manipulation: { enabled: false } }; + const network = new Network(container, data, options); + + // Keep your existing events (dragEnd, click, stabilization, etc.) + // No changes needed below + network.on('dragEnd', () => { + const positions = network.getPositions(); + this.savedPositions = positions; + localStorage.setItem(this.savedPositionsKey, JSON.stringify(positions)); + }); + + network.on('click', (event: any) => { + if (event.nodes[0]) { + const clickedNode = nodes.get(event.nodes[0]); + const canvasPosition = network.canvasToDOM(event.pointer.canvas); + const containerRect = container.getBoundingClientRect(); + const mainContainer = document.querySelector('.main-container') as HTMLElement; + const mainRect = mainContainer?.getBoundingClientRect() || containerRect; + + let adjustedX = canvasPosition.x + containerRect.left - mainRect.left + 20; + let adjustedY = canvasPosition.y + containerRect.top - mainRect.top - 50; + + const popupWidth = 280; + const popupHeight = 200; + const viewportWidth = window.innerWidth; + const viewportHeight = window.innerHeight; + + if (adjustedX + popupWidth > viewportWidth) adjustedX -= popupWidth + 40; + if (adjustedY + popupHeight > viewportHeight) adjustedY -= popupHeight + 40; + if (adjustedX < 20) adjustedX = 20; + if (adjustedY < 20) adjustedY = 20; + + this.handleNodeClick(clickedNode, { x: adjustedX, y: adjustedY }); + } + }); + + network.on('stabilizationIterationsDone', () => { + network.fit(); + if (!hasSavedPositions) { + const positions = network.getPositions(); + this.savedPositions = positions; + localStorage.setItem(this.savedPositionsKey, JSON.stringify(positions)); + } + }); + + if (hasSavedPositions) { + setTimeout(() => network.fit(), 500); + } +} + + handleNodeClick(node: any, position?: { x: number, y: number }) { + this.selectedDevice = node; + if (position) { + this.selectedDevice.popupPosition = position; + } + } + + closeDeviceDetails() { + this.selectedDevice = null; + } + + getDeviceInterfaces() { + if (!this.selectedDevice) return []; + + const device = this.mikrotikData.find(d => d.name === this.selectedDevice.label); + if (!device) return []; + + return Object.entries(device.interfaces).map(([name, data]: [string, any]) => ({ + name, + address: data.address, + mac: data.mac + })); + } + + getNodeColor(deviceType: string): string { + const colors = { + 'gateway': '#dc3545', + 'router': '#fd7e14', + 'switch': '#6f42c1', + 'ap': '#20c997', + 'cpe': '#0dcaf0' + }; + return (colors as any)[deviceType] || '#6c757d'; + } + + getNodeBorderColor(deviceType: string): string { + const borderColors = { + 'gateway': '#b02a37', + 'router': '#e8681a', + 'switch': '#59359a', + 'ap': '#1aa179', + 'cpe': '#0baccc' + }; + return (borderColors as any)[deviceType] || '#495057'; + } + + getDeviceIcon(deviceType: string, brand: string): string { + const basePath = './assets/Network-Icons-SVG/'; + const type = deviceType.toLowerCase(); + const brandName = brand.toLowerCase(); + + // MikroTik devices + if (brandName === 'mikrotik') { + if (type === 'switch') { + return `${basePath}cumulus-switch-v2.svg`; + } + return `${basePath}cumulus-router-v2.svg`; + } + + // Cisco devices + if (brandName === 'cisco') { + if (type === 'switch') { + return `${basePath}cisco-switch-l2.svg`; + } + return `${basePath}cisco-router.svg`; + } + + // Juniper devices + if (brandName === 'juniper') { + if (type === 'switch') { + return `${basePath}juniper-switch-l2.svg`; + } + return `${basePath}juniper-router.svg`; + } + + // HPE/Aruba devices + if (brandName === 'hpe/aruba' || brandName === 'aruba' || brandName === 'hpe') { + if (type === 'server') { + return `${basePath}generic-server-1.svg`; + } + return `${basePath}arista-switch.svg`; + } + + // Ubiquiti devices + if (brandName === 'ubiquiti' || brandName === 'ubnt') { + if (type === 'switch') { + return `${basePath}generic-switch-l2-v1-colour.svg`; + } + return `${basePath}generic-router-colour.svg`; + } + + // Default icons by type + const defaultIcons = { + 'switch': `${basePath}generic-switch-l2-v1-colour.svg`, + 'router': `${basePath}generic-router-colour.svg`, + 'router/switch': `${basePath}generic-router-colour.svg`, + 'server': `${basePath}generic-server-1.svg`, + 'unknown': `${basePath}generic-router-colour.svg` + }; + + return (defaultIcons as any)[type] || `${basePath}generic-router-colour.svg`; + } + + getDefaultPosition(deviceName: string, index: number): { x: number, y: number } { + const positions = { + 'Core Router': { x: 0, y: 0 }, + 'Edge Router': { x: -200, y: -100 }, + 'Distribution Switch': { x: 200, y: -100 }, + 'Access Point 1': { x: 100, y: 100 }, + 'Access Point 2': { x: 300, y: 100 }, + 'Customer Router 1': { x: 0, y: 200 }, + 'Customer Router 2': { x: 200, y: 200 } + }; + return (positions as any)[deviceName] || { x: index * 100, y: index * 50 }; + } + + webAccess() { + if (!this.selectedDevice) return; + const device = this.mikrotikData.find(d => d.name === this.selectedDevice.label); + if (device) { + const firstInterface = Object.values(device.interfaces)[0] as any; + const ip = firstInterface.address.split('/')[0]; + window.open(`http://${ip}`, '_blank'); + } + } + + showMoreInfo() { + console.log('More info for:', this.selectedDevice); + // Implement modal or detailed view + } + + pingDevice() { + console.log('Ping device:', this.selectedDevice); + // Implement ping functionality + } + + configureDevice() { + console.log('Configure device:', this.selectedDevice); + // Implement configuration interface + } + +} diff --git a/src/app/views/maps/maps-routing.module.ts b/src/app/views/maps/maps-routing.module.ts new file mode 100644 index 0000000..db9975a --- /dev/null +++ b/src/app/views/maps/maps-routing.module.ts @@ -0,0 +1,21 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; + +import { MapsComponent } from './maps.component'; + +const routes: Routes = [ + { + path: '', + component: MapsComponent, + data: { + title: $localize`Maps Wall` + } + } +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule] +}) +export class MapsRoutingModule { +} diff --git a/src/app/views/maps/maps.component.html b/src/app/views/maps/maps.component.html new file mode 100644 index 0000000..3319749 --- /dev/null +++ b/src/app/views/maps/maps.component.html @@ -0,0 +1,82 @@ + + + + +
    +
    +
    +
    + +
    +
    +
    + {{ selectedDevice.label }} + +
    + +
    +
    +
    Type: {{getDeviceInfo()?.type}} ({{getDeviceInfo()?.brand}})
    +
    Board: {{getDeviceInfo()?.board}}
    +
    Version: {{getDeviceInfo()?.version}}
    +
    System: {{getDeviceInfo()?.systemDescription}}
    +
    IP: {{getPrimaryIP()}}
    +
    Neighbors: {{getNeighborCount()}}
    +
    + +
    +
    +
    + {{ interface.name }} + ({{ interface.neighbors }} neighbors) +
    + {{ interface.address }} +
    +
    + +
    + + + +
    +
    +
    +
    + + + + +
    Web Access Options
    + +
    + +

    Choose how to access the device:

    +
    + + +
    +
    + + + +
    diff --git a/src/app/views/maps/maps.component.scss b/src/app/views/maps/maps.component.scss new file mode 100644 index 0000000..26682f1 --- /dev/null +++ b/src/app/views/maps/maps.component.scss @@ -0,0 +1,256 @@ +:host { + .network-container { + height: calc(100vh - 165px); + margin: 0; + position: relative; + } + + .network-col { + padding: 0; + } + + .network-card { + height: 100%; + border: none; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08); + border-radius: 12px; + overflow: hidden; + position: relative; + } + + .refresh-btn { + position: absolute; + top: 10px; + right: 10px; + z-index: 1000; + padding: 6px 12px; + font-size: 12px; + } + + .network-canvas { + width: 100%; + height: calc(100vh - 200px); + background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%); + border-radius: 8px; + border: 1px solid #dee2e6; + box-shadow: 0 2px 8px rgba(0,0,0,0.1); + + ::ng-deep .vis-network { + cursor: default; + + .vis-item { + cursor: pointer; + + &:hover { + cursor: pointer; + } + + &:active { + cursor: grabbing; + } + } + } + } + + .floating-sidebar { + position: fixed; + z-index: 10000; + animation: slideIn 0.3s ease; + } + + .device-panel { + background: rgba(44, 62, 80, 0.95); + backdrop-filter: blur(10px); + border-radius: 8px; + border: 1px solid rgba(52, 73, 94, 0.8); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); + width: 280px; + max-height: calc(100vh - 40px); + color: white; + display: flex; + flex-direction: column; + } + + .panel-header { + padding: 12px 16px; + border-bottom: 1px solid rgba(52, 73, 94, 0.5); + display: flex; + justify-content: space-between; + align-items: center; + } + + .device-name { + font-weight: 600; + font-size: 14px; + color: #ecf0f1; + } + + .close-btn { + color: #bdc3c7; + border: none; + background: rgba(255, 255, 255, 0.1); + border-radius: 4px; + width: 24px; + height: 24px; + display: flex; + align-items: center; + justify-content: center; + + &:hover { + background: rgba(255, 255, 255, 0.2); + color: white; + } + } + + .panel-content { + padding: 16px; + overflow-y: auto; + flex: 1; + + /* Override global scrollbar styles */ + scrollbar-width: thin !important; + scrollbar-color: #bdc3c7 rgba(52, 73, 94, 0.3) !important; + + /* Custom scrollbar styling for webkit */ + &::-webkit-scrollbar { + width: 8px !important; + } + + &::-webkit-scrollbar-track { + background: rgba(52, 73, 94, 0.3) !important; + border-radius: 4px; + } + + &::-webkit-scrollbar-thumb { + background: #bdc3c7 !important; + border-radius: 4px; + + &:hover { + background: #ecf0f1 !important; + } + } + } + + .interfaces-section { + margin-bottom: 16px; + margin-top: 3px; + + .interface-row { + max-height: none; + } + } + + .section-title { + font-size: 12px; + font-weight: 600; + color: #95a5a6; + margin-bottom: 8px; + text-transform: uppercase; + letter-spacing: 0.5px; + } + + .interface-row { + display: flex; + flex-direction: column; + padding: 1px 0; + border-bottom: 1px solid rgba(52, 73, 94, 0.3); + font-size: 12px; + gap: 2px; + + &:last-child { + border-bottom: none; + } + } + + .if-header { + display: flex; + justify-content: space-between; + align-items: center; + } + + .if-name { + color: #3498db; + font-weight: 500; + } + + .if-ip { + color: #ecf0f1; + font-family: monospace; + word-break: break-all; + } + + .if-neighbors { + color: #95a5a6; + font-size: 11px; + } + + .actions-section { + display: flex; + gap: 8px; + + &:has(button:only-child) { + button { + flex: 1; + } + } + + &:not(:has(button:only-child)) { + display: grid; + grid-template-columns: 1fr 1fr; + } + } + + .compact-btn { + padding: 6px 8px; + font-size: 11px; + border-radius: 4px; + display: flex; + align-items: center; + gap: 4px; + justify-content: center; + transition: all 0.2s ease; + + &:hover { + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); + } + } + + @keyframes slideIn { + from { + opacity: 0; + transform: translateX(20px); + } + to { + opacity: 1; + transform: translateX(0); + } + } +} + +.mdc-line-ripple.mdc-line-ripple--deactivating.ng-star-inserted { + display: none!important; +} + +::ng-deep .main-container{ + padding:0!important; + margin-top:-10px; +} + +::ng-deep .header{ + margin-bottom: 0.9rem!important; +} + +@media only screen and (max-width: 768px) { + :host .floating-sidebar { + position: fixed; + top: 10px; + right: 10px; + left: 10px; + width: auto; + } + + :host .device-panel { + width: 100%; + } +} \ No newline at end of file diff --git a/src/app/views/maps/maps.component.ts b/src/app/views/maps/maps.component.ts new file mode 100644 index 0000000..d545e8a --- /dev/null +++ b/src/app/views/maps/maps.component.ts @@ -0,0 +1,479 @@ +import { Component, OnInit, OnDestroy, ViewChild, ElementRef } from "@angular/core"; +import { dataProvider } from "../../providers/mikrowizard/data"; +import { loginChecker } from "../../providers/login_checker"; +import { Router } from "@angular/router"; +import { formatInTimeZone } from "date-fns-tz"; +import { Network } from 'vis-network/peer'; +import { DataSet } from 'vis-data'; + +@Component({ + templateUrl: "maps.component.html", + styleUrls: ["maps.component.scss"], +}) +export class MapsComponent implements OnInit { + public uid: number; + public uname: string; + public ispro: boolean = false; + public tz: string; + public savedPositions: any = {}; + public savedPositionsKey = "network-layout"; + public selectedDevice: any = null; + public showWebAccessModal: boolean = false; + public showMoreInfoModal: boolean = false; + public currentDeviceInfo: any = null; + constructor( + private data_provider: dataProvider, + private router: Router, + private login_checker: loginChecker + ) { + var _self = this; + if (!this.login_checker.isLoggedIn()) { + setTimeout(function () { + _self.router.navigate(["login"]); + }, 100); + } + this.data_provider.getSessionInfo().then((res) => { + _self.uid = res.uid; + _self.uname = res.name; + _self.ispro = res.ISPRO; + if (!_self.ispro) + setTimeout(function () { + _self.router.navigate(["dashboard"]); + }, 100); + _self.tz = res.tz; + }); + } + + @ViewChild('network', { static: true }) networkContainer: ElementRef | undefined; + + mikrotikData: any[] = []; + + ngOnInit(): void { + this.loadFontAwesome(); + this.savedPositions = JSON.parse(localStorage.getItem(this.savedPositionsKey) || "{}"); + this.loadNetworkData(); + } + + loadNetworkData(): void { + this.data_provider.getNetworkMap().then((res) => { + this.mikrotikData = res; + console.dir(res); + setTimeout(() => { + this.createNetworkMap(); + }, 100); + }); + } + + refreshData(): void { + this.selectedDevice = null; + this.loadNetworkData(); + } + + + + loadFontAwesome() { + if (!document.querySelector('link[href*="font-awesome"]')) { + const link = document.createElement('link'); + link.rel = 'stylesheet'; + link.href = 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css'; + document.head.appendChild(link); + } + } + + createNetworkMap() { + const container = this.networkContainer?.nativeElement; + if (!container) return; + + let nodes = new DataSet([]); + let edges = new DataSet([]); + let deviceMap: { [key: string]: string } = {}; // uniqueId (hostname) to nodeId mapping + let allDevices: { [key: string]: any } = {}; // uniqueId to device info mapping + let macToDevice: { [key: string]: string } = {}; // MAC -> uniqueId (hostname) mapping + const hasSavedPositions = Object.keys(this.savedPositions).length > 0; + let nodeIdCounter = 1; + + // Collect all devices using hostname as consistent unique ID + this.mikrotikData.forEach((device) => { + const deviceId = device.name; // Use name (hostname) as unique ID + + if (!allDevices[deviceId]) { + allDevices[deviceId] = { + name: device.name, + type: 'Router', + brand: 'MikroTik' + }; + } + + Object.entries(device.interfaces).forEach(([_, iface]: [string, any]) => { + if (iface.mac) { + macToDevice[iface.mac] = deviceId; // Map to hostname + } + + if (iface.neighbors && Array.isArray(iface.neighbors)) { + iface.neighbors.forEach((neighbor: any) => { + const neighborId = neighbor.hostname || 'Unknown'; // Use hostname + + if (!allDevices[neighborId]) { + allDevices[neighborId] = { + name: neighbor.hostname || neighbor.mac || 'Unknown', + type: neighbor.type || 'Router', + brand: neighbor.brand || 'MikroTik' + }; + } + + if (neighbor.mac) { + macToDevice[neighbor.mac] = neighborId; // Map to hostname + } + }); + } + }); + }); + + // Create nodes + Object.entries(allDevices).forEach(([uniqueId, device]: [string, any]) => { + const nodeId = `node_${nodeIdCounter++}`; + deviceMap[uniqueId] = nodeId; + + nodes.add({ + id: nodeId, + label: device.name, + shape: 'image', + image: this.getDeviceIcon(device.type || 'Unknown', device.brand || 'Unknown'), + size: 15, + font: { size: 11, color: '#333', face: 'Arial, sans-serif' }, + ...(hasSavedPositions && this.savedPositions[nodeId] + ? { x: this.savedPositions[nodeId].x, y: this.savedPositions[nodeId].y } + : {}) + } as any); + }); + +// Create edges - collect all connections first +let connectionMap: { [key: string]: string[] } = {}; + +this.mikrotikData.forEach((device) => { + const deviceName = device.name; + Object.entries(device.interfaces).forEach(([ifaceName, iface]: [string, any]) => { + const sourceDeviceId = iface.mac ? macToDevice[iface.mac] : deviceName; + + if (iface.neighbors && Array.isArray(iface.neighbors)) { + iface.neighbors.forEach((neighbor: any) => { + const targetDeviceId = neighbor.mac ? macToDevice[neighbor.mac] : null; + + if (deviceMap[sourceDeviceId] && targetDeviceId && deviceMap[targetDeviceId]) { + const connectionKey = [sourceDeviceId, targetDeviceId].sort().join('_'); + const interfacePair = neighbor.interface ? `${ifaceName}↔${neighbor.interface}` : ifaceName; + + if (!connectionMap[connectionKey]) { + connectionMap[connectionKey] = []; + } + + if (!connectionMap[connectionKey].includes(interfacePair)) { + connectionMap[connectionKey].push(interfacePair); + } + } + }); + } + }); +}); + +// Create edges with combined labels +Object.entries(connectionMap).forEach(([connectionKey, interfacePairs]) => { + const [sourceDeviceId, targetDeviceId] = connectionKey.split('_'); + let edgeLabel = interfacePairs.join('\n'); + + // Limit to max 2 interface pairs to avoid overcrowding + if (interfacePairs.length > 2) { + edgeLabel = interfacePairs.slice(0, 2).join('\n') + '\n+' + (interfacePairs.length - 2); + } + + edges.add({ + id: connectionKey, + from: deviceMap[sourceDeviceId], + to: deviceMap[targetDeviceId], + label: edgeLabel, + color: { color: '#34495e', highlight: '#3498db' }, + width: 3, + smooth: { type: 'continuous', roundness: 0.1 }, + font: { + size: 9, + color: '#2c3e50', + face: 'Arial, sans-serif', + strokeWidth: 2, + strokeColor: '#ffffff', + align: 'horizontal' + } + } as any); +}); + const data = { nodes, edges }; + const options = { physics: { enabled: true, stabilization: { iterations: 100 }, barnesHut: { gravitationalConstant: -8000, centralGravity: 0.3, springLength: 200, springConstant: 0.04, damping: 0.09 } }, interaction: { hover: true, dragNodes: true, dragView: true, zoomView: true, hoverConnectedEdges: false, selectConnectedEdges: false, navigationButtons: false, keyboard: false }, nodes: { borderWidth: 3, shadow: true }, edges: { shadow: true, smooth: true, length: 150 }, manipulation: { enabled: false } }; + const network = new Network(container, data, options); + + // Keep your existing events (dragEnd, click, stabilization, etc.) + // No changes needed below + network.on('dragEnd', () => { + const positions = network.getPositions(); + this.savedPositions = positions; + localStorage.setItem(this.savedPositionsKey, JSON.stringify(positions)); + }); + + network.on('click', (event: any) => { + if (event.nodes[0]) { + const clickedNode = nodes.get(event.nodes[0]); + const canvasPosition = network.canvasToDOM(event.pointer.canvas); + const containerRect = container.getBoundingClientRect(); + const mainContainer = document.querySelector('.main-container') as HTMLElement; + const mainRect = mainContainer?.getBoundingClientRect() || containerRect; + + let adjustedX = canvasPosition.x + containerRect.left - mainRect.left + 20; + let adjustedY = canvasPosition.y + containerRect.top - mainRect.top - 50; + + const popupWidth = 280; + const maxPopupHeight = window.innerHeight - 40; + const viewportWidth = window.innerWidth; + const viewportHeight = window.innerHeight; + + if (adjustedX + popupWidth > viewportWidth) adjustedX -= popupWidth + 40; + if (adjustedY + maxPopupHeight > viewportHeight) adjustedY = viewportHeight - maxPopupHeight - 20; + if (adjustedX < 20) adjustedX = 20; + if (adjustedY < 20) adjustedY = 20; + + this.handleNodeClick(clickedNode, { x: adjustedX, y: adjustedY }); + } + }); + + network.on('stabilizationIterationsDone', () => { + network.fit(); + if (!hasSavedPositions) { + const positions = network.getPositions(); + this.savedPositions = positions; + localStorage.setItem(this.savedPositionsKey, JSON.stringify(positions)); + } + }); + + if (hasSavedPositions) { + setTimeout(() => network.fit(), 500); + } + } + + handleNodeClick(node: any, position?: { x: number, y: number }) { + this.selectedDevice = node; + if (position) { + this.selectedDevice.popupPosition = position; + } + } + + closeDeviceDetails() { + this.selectedDevice = null; + } + + getDeviceInfo() { + if (!this.selectedDevice) return null; + + const device = this.mikrotikData.find(d => d.name === this.selectedDevice.label); + + if (device) { + // Main device found + const interfaces = Object.entries(device.interfaces).map(([name, data]: [string, any]) => ({ + name, + address: data.address || 'N/A', + mac: data.mac || 'N/A', + neighbors: data.neighbors?.length || 0 + })); + + const interfaceWithNeighbors = Object.values(device.interfaces) + .find((iface: any) => iface.neighbors?.length > 0) as any; + const firstNeighbor = interfaceWithNeighbors?.neighbors?.[0]; + + return { + name: device.name, + deviceId: device.device_id, + type: firstNeighbor?.type || 'Router', + brand: firstNeighbor?.brand || 'MikroTik', + board: firstNeighbor?.board || 'Unknown', + version: firstNeighbor?.version || 'Unknown', + systemDescription: firstNeighbor?.['system-description'] || null, + softwareId: firstNeighbor?.['software-id'] || 'N/A', + interfaces + }; + } else { + // Search in neighbor data + for (const mainDevice of this.mikrotikData) { + for (const iface of Object.values(mainDevice.interfaces)) { + const neighbor = (iface as any).neighbors?.find((n: any) => n.hostname === this.selectedDevice.label); + if (neighbor) { + return { + name: neighbor.hostname, + deviceId: null, + type: neighbor.type || 'Router', + brand: neighbor.brand || 'MikroTik', + board: neighbor.board || 'Unknown', + version: neighbor.version || 'Unknown', + systemDescription: neighbor['system-description'] || null, + softwareId: neighbor['software-id'] || 'N/A', + interfaces: [{ name: neighbor.interface || 'Unknown', address: neighbor.address || 'N/A', mac: neighbor.mac || 'N/A', neighbors: 0 }] + }; + } + } + } + } + + return null; + } + + getNodeColor(deviceType: string): string { + const colors = { + 'gateway': '#dc3545', + 'router': '#fd7e14', + 'switch': '#6f42c1', + 'ap': '#20c997', + 'cpe': '#0dcaf0' + }; + return (colors as any)[deviceType] || '#6c757d'; + } + + getNodeBorderColor(deviceType: string): string { + const borderColors = { + 'gateway': '#b02a37', + 'router': '#e8681a', + 'switch': '#59359a', + 'ap': '#1aa179', + 'cpe': '#0baccc' + }; + return (borderColors as any)[deviceType] || '#495057'; + } + + getDeviceIcon(deviceType: string, brand: string): string { + const basePath = './assets/Network-Icons-SVG/'; + const type = deviceType.toLowerCase(); + const brandName = brand.toLowerCase(); + + // MikroTik devices + if (brandName === 'mikrotik') { + if (type === 'switch') { + return `${basePath}cumulus-switch-v2.svg`; + } + return `${basePath}cumulus-router-v2.svg`; + } + + // Cisco devices + if (brandName === 'cisco') { + if (type === 'switch') { + return `${basePath}cisco-switch-l2.svg`; + } + return `${basePath}cisco-router.svg`; + } + + // Juniper devices + if (brandName === 'juniper') { + if (type === 'switch') { + return `${basePath}juniper-switch-l2.svg`; + } + return `${basePath}juniper-router.svg`; + } + + // HPE/Aruba devices + if (brandName === 'hpe/aruba' || brandName === 'aruba' || brandName === 'hpe') { + if (type === 'server') { + return `${basePath}generic-server-1.svg`; + } + return `${basePath}arista-switch.svg`; + } + + // Ubiquiti devices + if (brandName === 'ubiquiti' || brandName === 'ubnt') { + if (type === 'switch') { + return `${basePath}generic-switch-l2-v1-colour.svg`; + } + return `${basePath}generic-router-colour.svg`; + } + + // Default icons by type + const defaultIcons = { + 'switch': `${basePath}generic-switch-l2-v1-colour.svg`, + 'router': `${basePath}generic-router-colour.svg`, + 'router/switch': `${basePath}generic-router-colour.svg`, + 'server': `${basePath}generic-server-1.svg`, + 'unknown': `${basePath}generic-router-colour.svg` + }; + + return (defaultIcons as any)[type] || `${basePath}generic-router-colour.svg`; + } + + getDefaultPosition(deviceName: string, index: number): { x: number, y: number } { + const positions = { + 'Core Router': { x: 0, y: 0 }, + 'Edge Router': { x: -200, y: -100 }, + 'Distribution Switch': { x: 200, y: -100 }, + 'Access Point 1': { x: 100, y: 100 }, + 'Access Point 2': { x: 300, y: 100 }, + 'Customer Router 1': { x: 0, y: 200 }, + 'Customer Router 2': { x: 200, y: 200 } + }; + return (positions as any)[deviceName] || { x: index * 100, y: index * 50 }; + } + + webAccess() { + if (!this.selectedDevice) return; + this.currentDeviceInfo = this.getDeviceInfo(); + this.showWebAccessModal = true; + this.closeDeviceDetails(); + } + + openProxyAccess() { + if (this.currentDeviceInfo?.deviceId) { + window.open(`/api/proxy/init?devid=${this.currentDeviceInfo.deviceId}`, '_blank'); + } else { + const ip = this.currentDeviceInfo?.interfaces.find((iface: any) => iface.address !== 'N/A')?.address?.split('/')[0]; + if (ip) { + window.open(`/api/proxy/init?dev_ip=${ip}`, '_blank'); + } + } + this.showWebAccessModal = false; + } + + openDirectAccess() { + const ip = this.currentDeviceInfo?.interfaces.find((iface: any) => iface.address !== 'N/A')?.address?.split('/')[0]; + if (ip) { + window.open(`http://${ip}`, '_blank'); + } + this.showWebAccessModal = false; + } + + closeWebAccessModal() { + this.showWebAccessModal = false; + } + + getNeighborCount() { + const deviceInfo = this.getDeviceInfo(); + return deviceInfo?.interfaces.reduce((total, iface) => total + iface.neighbors, 0) || 0; + } + + getPrimaryIP() { + const deviceInfo = this.getDeviceInfo(); + const primaryInterface = deviceInfo?.interfaces.find(iface => iface.address !== 'N/A'); + return primaryInterface?.address?.split('/')[0] || 'N/A'; + } + + getDeviceInterfaces() { + const deviceInfo = this.getDeviceInfo(); + return deviceInfo?.interfaces || []; + } + + showMoreInfo() { + const deviceInfo = this.getDeviceInfo(); + if (deviceInfo?.deviceId) { + window.open(`/#/device-stats;id=${deviceInfo.deviceId}`, '_blank'); + } + } + + pingDevice(devid: number) { + console.log('Ping device:', this.selectedDevice); + // Implement ping functionality + } + + configureDevice(devid: number) { + console.log('Configure device:', this.selectedDevice); + // Implement configuration interface + } + +} \ No newline at end of file diff --git a/src/app/views/maps/maps.module.ts b/src/app/views/maps/maps.module.ts new file mode 100644 index 0000000..beb35e3 --- /dev/null +++ b/src/app/views/maps/maps.module.ts @@ -0,0 +1,60 @@ +import { NgModule } from "@angular/core"; +import { CommonModule } from "@angular/common"; +import { ReactiveFormsModule } from "@angular/forms"; +import { FormsModule } from "@angular/forms"; + +import { + ButtonGroupModule, + ButtonModule, + CardModule, + GridModule, + WidgetModule, + ProgressModule, + TemplateIdDirective, + TooltipModule, + BadgeModule, + CarouselModule, + ListGroupModule, + ModalModule, + TableModule, + UtilitiesModule +} from "@coreui/angular"; +import { IconModule } from "@coreui/icons-angular"; + +import { ChartjsModule } from "@coreui/angular-chartjs"; +import { NgScrollbarModule } from 'ngx-scrollbar'; +import { MapsRoutingModule } from "./maps-routing.module"; +import { MapsComponent } from "./maps.component"; +import { ClipboardModule } from "@angular/cdk/clipboard"; +import { InfiniteScrollModule } from 'ngx-infinite-scroll'; + +@NgModule({ + imports: [ + MapsRoutingModule, + CardModule, + WidgetModule, + CommonModule, + GridModule, + ProgressModule, + ReactiveFormsModule, + ButtonModule, + ModalModule, + FormsModule, + TemplateIdDirective, + ButtonModule, + ButtonGroupModule, + ChartjsModule, + CarouselModule, + BadgeModule, + ClipboardModule, + ListGroupModule, + NgScrollbarModule, + TableModule, + TooltipModule, + UtilitiesModule, + InfiniteScrollModule, + IconModule + ], + declarations: [MapsComponent], +}) +export class MapsModule {} From 2f68c499365d8fdd7e5d36181a998d6bbc3738e9 Mon Sep 17 00:00:00 2001 From: sepehr Date: Thu, 16 Oct 2025 17:33:47 +0300 Subject: [PATCH 08/15] feat: redesign user tasks with advanced scheduling - Complete UI/UX redesign of user tasks interface - Add cron selector with examples - Improved task scheduling interface - Enhanced task management and monitoring --- .../user_tasks/user_tasks.component.html | 503 ++++++++++++------ .../user_tasks/user_tasks.component.scss | 371 +++++++++++++ .../views/user_tasks/user_tasks.component.ts | 188 ++++++- src/app/views/user_tasks/user_tasks.module.ts | 6 + 4 files changed, 884 insertions(+), 184 deletions(-) create mode 100644 src/app/views/user_tasks/user_tasks.component.scss diff --git a/src/app/views/user_tasks/user_tasks.component.html b/src/app/views/user_tasks/user_tasks.component.html index f7da0bc..f37fd7c 100644 --- a/src/app/views/user_tasks/user_tasks.component.html +++ b/src/app/views/user_tasks/user_tasks.component.html @@ -57,146 +57,255 @@ - -
    Editing device {{SelectedTask['name']}}
    -
    Adding new task
    + +
    + Edit Task: {{SelectedTask['name']}} +
    +
    + Create New Task +
    - -
    - - + + +
    +
    +
    Basic Information
    + Define the task name, description and type +
    + + + + + + + + + + +
    - -
    - - + +
    +
    +
    Task Configuration
    + Configure task-specific settings and parameters +
    + + +
    + + +
    Backup Configuration
    +
    + +
    + + This task will create configuration backups of selected devices. Backups are stored securely and can be restored later. +
    +
    +
    +
    + + + + +
    Firmware Update Strategy
    +
    + +
    + + + + + +
    + +
    + + Uses global MikroWizard update strategy settings. Check Settings page for configuration. +
    + +
    + + Downloads latest firmware from mikrotik.com. Server needs internet access. +
    + +
    + + + + + + + + + + +
    +
    +
    + + +
    + + +
    Script/Snippet Configuration
    +
    + + + + + + The selected script will be executed on all target devices when this task runs. + + +
    +
    - - - - - -
    Update Version Strategy
    - - - - - - - - - - - - - - - - - - The version of firmware will be selected based on global settings of Mikrowizard Update strategy. -
    - Please check settings page for more info and configuration -
    -
    - - - - The version of firmware will be selected based on latest availble version from Mikrotik website!. -
    - V6 Firmware update Behavior and safe install is based on global Mikrowizard setting.(check settings page) -
    - **with this option MikroWizard will download latest availble firmware from mikrotik.com. Please keep in mind that server needs internet access to mikrotik.com
    -
    - - - - - - * The version of firmware to install routers - - - - - - - * The version of firmware to install on V6 routers - - -
    -
    - - - - -
    - - + + +
    +
    +
    Schedule Configuration
    + Set when this task should run automatically +
    +
    + +
    +
    + + +
    +
    +
    +
    {{cron.label}}
    +
    {{cron.value}}
    +
    {{cron.description}}
    +
    +
    +
    + No matching cron presets found +
    +
    + + {{getCronDescription()}} + +
    + + Quick Examples: + * * * * * = every minute | + 0 2 * * * = daily at 2 AM | + 0 */6 * * * = every 6 hours + +
    +
    + + +
    +
    +
    Target Selection
    + Choose which devices or groups this task will affect +
    +
    + + + + +
    - - - - - -
    Members :
    - - - -   {{value}} - - - - {{value}} - - - - - - - - -
    - + + + +
    + Selected {{SelectedTask['selection_type'] === 'devices' ? 'Devices' : 'Groups'}} +
    +
    + {{SelectedMembers.length}} selected + +
    +
    + +
    + +
    No {{SelectedTask['selection_type']}} selected
    +

    Click "Add {{SelectedTask['selection_type'] === 'devices' ? 'Devices' : 'Groups'}}" to select targets for this task

    +
    +
    + + + +
    + + {{value}} +
    +
    +
    + + + {{value}} + + + + + + + +
    +
    +
    +
    +
    - - - - + +
    + All fields marked with * are required +
    +
    + + + +
    - - -
    Editing Group
    + + +
    + Add {{SelectedTask['selection_type'] === 'devices' ? 'Devices' : 'Groups'}} to Task +
    - - -
    Group Members :
    - - - -   {{value}} - - - - {{value}} - - - - - {{value}} - - - -
    -
    -
    + + +
    + + + {{NewMemberRows.length}} {{SelectedTask['selection_type']}}(s) selected for addition to this task + +
    + + + + +
    + + Available {{SelectedTask['selection_type'] === 'devices' ? 'Devices' : 'Groups'}} ({{availbleMembers.length}} total) +
    +
    + +
    + +
    All {{SelectedTask['selection_type']}} are already assigned
    +

    No available {{SelectedTask['selection_type']}} to add to this task

    +
    +
    + + + +
    + + {{value}} +
    +
    +
    + + + {{value}} + + + + + {{value}} + + +
    +
    +
    +
    - - - + +
    + Select {{SelectedTask['selection_type']}} from the list above to add them to this task +
    +
    + + +
    @@ -340,4 +491,6 @@ Close
    - \ No newline at end of file + + + \ No newline at end of file diff --git a/src/app/views/user_tasks/user_tasks.component.scss b/src/app/views/user_tasks/user_tasks.component.scss new file mode 100644 index 0000000..ccea1e6 --- /dev/null +++ b/src/app/views/user_tasks/user_tasks.component.scss @@ -0,0 +1,371 @@ +/* Task Form Sections */ +.task-form-section, .task-config-section, .schedule-section, .target-section { + border: 1px solid #e9ecef; + border-radius: 6px; + padding: 0.75rem; + background: #f8f9fa; +} + +.section-header { + border-bottom: 1px solid #dee2e6; + padding-bottom: 0.375rem; + margin-bottom: 0.75rem; +} + +.section-title { + color: #495057; + font-weight: 600; + font-size: 0.875rem; +} + +/* Form Inputs */ +.form-input { + border: 1px solid #ced4da; + border-radius: 4px; + padding: 0.375rem 0.75rem; + font-size: 0.875rem; + transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; +} + +.form-input:focus { + border-color: #80bdff; + box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25); +} + +/* Strategy Buttons */ +.strategy-buttons .btn-group { + box-shadow: 0 1px 3px rgba(0,0,0,0.1); +} + +.strategy-buttons button { + font-size: 0.8rem; + padding: 0.375rem 0.75rem; +} + +/* Cron Dropdown Styles */ +.cron-input-wrapper { + position: relative; +} + +.input-group { + position: relative; + display: flex; + flex-wrap: nowrap; + align-items: stretch; + width: 100%; +} + +.input-group .search-input { + border-top-right-radius: 0; + border-bottom-right-radius: 0; + border-right: none; + flex: 1; +} + +.input-group .btn { + border-top-left-radius: 0; + border-bottom-left-radius: 0; + border-left: 1px solid #ced4da; + flex-shrink: 0; +} + +.search-select-wrapper { + position: relative; + width: 100%; +} + +.search-input { + width: 100%; + padding: 0.5rem 0.75rem; + border: 1px solid #ced4da; + border-radius: 6px; + font-size: 0.9rem; + transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; +} + +.search-input:focus { + border-color: #80bdff; + box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25); + outline: 0; +} + +.search-dropdown { + position: absolute; + top: 100%; + left: 0; + right: 0; + background: white; + border: 1px solid #ced4da; + border-top: none; + border-radius: 0 0 6px 6px; + max-height: 300px; + overflow-y: auto; + z-index: 1000; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); +} + +.search-option { + padding: 0.5rem 0.75rem; + cursor: pointer; + border-bottom: 1px solid #f8f9fa; + transition: background-color 0.15s ease-in-out; +} + +.search-option:hover { + background-color: #f8f9fa; +} + +.search-option:last-child { + border-bottom: none; +} + +.search-no-results { + padding: 0.75rem; + text-align: center; + color: #6c757d; + font-style: italic; +} + +/* Cron-specific dropdown styles */ +.cron-dropdown { + max-height: 400px; +} + +.cron-option { + padding: 0.75rem; + border-bottom: 1px solid #e9ecef; +} + +.cron-option:hover { + background-color: #e3f2fd; +} + +.cron-option.selected { + background-color: #bbdefb; + border-left: 4px solid #2196f3; +} + +.cron-option.selected:hover { + background-color: #90caf9; +} + +.cron-label { + font-weight: 600; + color: #495057; + font-size: 0.9rem; +} + +.cron-value { + font-family: 'Courier New', monospace; + color: #007bff; + font-size: 0.85rem; + margin: 0.25rem 0; + background: #f8f9fa; + padding: 0.25rem 0.5rem; + border-radius: 4px; + display: inline-block; +} + +.cron-description { + color: #6c757d; + font-size: 0.8rem; + font-style: italic; +} + +/* Target Type Selector */ +.target-type-selector .btn-group { + box-shadow: 0 1px 3px rgba(0,0,0,0.1); +} + +.target-type-selector button { + font-size: 0.8rem; + padding: 0.375rem 0.75rem; +} + +/* Card Enhancements */ +.c-card { + box-shadow: 0 2px 4px rgba(0,0,0,0.1); + border: 1px solid #e9ecef; +} + +.c-card-header { + background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%); + border-bottom: 1px solid #dee2e6; + font-weight: 600; +} + +/* Alert Enhancements */ +.alert { + border-radius: 8px; + border: none; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); +} + +.alert-info { + background: linear-gradient(135deg, #d1ecf1 0%, #bee5eb 100%); + color: #0c5460; +} + +.alert-success { + background: linear-gradient(135deg, #d4edda 0%, #c3e6cb 100%); + color: #155724; +} + +.alert-warning { + background: linear-gradient(135deg, #fff3cd 0%, #ffeaa7 100%); + color: #856404; +} + +/* Badge Enhancements */ +.badge { + font-size: 0.7rem; + padding: 0.25em 0.5em; + border-radius: 4px; +} + +/* Button Enhancements */ +.btn { + border-radius: 4px; + font-weight: 500; + transition: all 0.15s ease-in-out; +} + +.btn-sm { + padding: 0.25rem 0.5rem; + font-size: 0.8rem; +} + +.btn:hover { + transform: translateY(-1px); + box-shadow: 0 4px 8px rgba(0,0,0,0.15); +} + +/* Modal Enhancements */ +.modal-header { + border-bottom: 1px solid #dee2e6; + background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%); +} + +.modal-footer { + border-top: 1px solid #dee2e6; + background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%); +} + +/* Grid Enhancements */ +.gui-grid { + border-radius: 8px; + overflow: hidden; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); +} + +/* Responsive Design */ +@media (max-width: 768px) { + .task-form-section, .task-config-section, .schedule-section, .target-section { + padding: 0.5rem; + } + + .section-title { + font-size: 0.8rem; + } + + .strategy-buttons button, + .target-type-selector button { + font-size: 0.75rem; + padding: 0.25rem 0.5rem; + } + + .form-input { + padding: 0.25rem 0.5rem; + font-size: 0.8rem; + } + + .cron-label { + font-size: 0.8rem; + } + + .cron-value { + font-size: 0.75rem; + } + + .cron-description { + font-size: 0.7rem; + } + + .c-modal-body { + padding: 0.75rem !important; + } +} + +@media (max-width: 576px) { + .task-form-section, .task-config-section, .schedule-section, .target-section { + padding: 0.375rem; + margin-bottom: 0.75rem; + } + + .section-header { + margin-bottom: 0.5rem; + } + + .strategy-buttons button, + .target-type-selector button { + font-size: 0.7rem; + padding: 0.25rem 0.375rem; + } + + .input-group .btn { + padding: 0.25rem 0.5rem; + font-size: 0.8rem; + } +} + +/* Animation for smooth transitions */ +@keyframes fadeIn { + from { opacity: 0; transform: translateY(-10px); } + to { opacity: 1; transform: translateY(0); } +} + +.search-dropdown { + animation: fadeIn 0.2s ease-out; +} + +/* Focus states for accessibility */ +.search-option:focus, +.cron-option:focus { + outline: 2px solid #007bff; + outline-offset: -2px; + background-color: #e3f2fd; +} + +/* Loading states */ +.loading-spinner { + display: inline-block; + width: 1rem; + height: 1rem; + border: 2px solid #f3f3f3; + border-top: 2px solid #007bff; + border-radius: 50%; + animation: spin 1s linear infinite; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +/* Code examples */ +code { + background-color: #f8f9fa; + color: #e83e8c; + padding: 0.2em 0.4em; + border-radius: 3px; + font-size: 0.875em; + font-family: 'Courier New', monospace; +} + +/* Improved spacing */ +.mt-1 { margin-top: 0.25rem !important; } +.mt-2 { margin-top: 0.5rem !important; } +.me-1 { margin-right: 0.25rem !important; } +.me-2 { margin-right: 0.5rem !important; } +.ms-2 { margin-left: 0.5rem !important; } +.d-block { display: block !important; } \ No newline at end of file diff --git a/src/app/views/user_tasks/user_tasks.component.ts b/src/app/views/user_tasks/user_tasks.component.ts index 63d6163..b19e2e6 100644 --- a/src/app/views/user_tasks/user_tasks.component.ts +++ b/src/app/views/user_tasks/user_tasks.component.ts @@ -1,4 +1,4 @@ -import { Component, OnInit, OnDestroy } from "@angular/core"; +import { Component, OnInit, OnDestroy, QueryList, ViewChildren } from "@angular/core"; import { dataProvider } from "../../providers/mikrowizard/data"; import { Router } from "@angular/router"; import { loginChecker } from "../../providers/login_checker"; @@ -16,16 +16,28 @@ import { } from "@generic-ui/ngx-grid"; import { NgxSuperSelectOptions } from "ngx-super-select"; import { _getFocusedElementPierceShadowDom } from "@angular/cdk/platform"; +import { AppToastComponent } from "../toast-simple/toast.component"; +import { ToasterComponent } from "@coreui/angular"; @Component({ templateUrl: "user_tasks.component.html", + styleUrls: ["user_tasks.component.scss"] }) export class UserTasksComponent implements OnInit { public uid: number; public uname: string; public ispro: boolean = false; + @ViewChildren(ToasterComponent) viewChildren!: QueryList; + toasterForm = { + autohide: true, + delay: 10000, + position: "fixed", + fade: true, + closeButton: true, + }; + constructor( private data_provider: dataProvider, private router: Router, @@ -76,6 +88,55 @@ export class UserTasksComponent implements OnInit { public firmwaretoinstallv6: string = "none"; public updateBehavior: string = "keep"; public firms_loaded: boolean = false; + public predefinedCrons: any[] = [ + // High Frequency Monitoring + { label: 'Every minute', value: '* * * * *', description: 'Critical monitoring - runs every minute' }, + { label: 'Every 2 minutes', value: '*/2 * * * *', description: 'High frequency monitoring' }, + { label: 'Every 5 minutes', value: '*/5 * * * *', description: 'Standard monitoring interval' }, + { label: 'Every 10 minutes', value: '*/10 * * * *', description: 'Regular monitoring checks' }, + { label: 'Every 15 minutes', value: '*/15 * * * *', description: 'Moderate monitoring frequency' }, + { label: 'Every 30 minutes', value: '*/30 * * * *', description: 'Low frequency monitoring' }, + + // Hourly Operations + { label: 'Every hour', value: '0 * * * *', description: 'Hourly network checks' }, + { label: 'Every 2 hours', value: '0 */2 * * *', description: 'Bi-hourly operations' }, + { label: 'Every 4 hours', value: '0 */4 * * *', description: 'Quarterly daily checks' }, + { label: 'Every 6 hours', value: '0 */6 * * *', description: 'Four times daily' }, + { label: 'Every 8 hours', value: '0 */8 * * *', description: 'Three times daily' }, + { label: 'Every 12 hours', value: '0 */12 * * *', description: 'Twice daily operations' }, + + // Daily Maintenance + { label: 'Daily at midnight', value: '0 0 * * *', description: 'Daily maintenance at 00:00' }, + { label: 'Daily at 1 AM', value: '0 1 * * *', description: 'Daily backup at 01:00' }, + { label: 'Daily at 2 AM', value: '0 2 * * *', description: 'Daily maintenance at 02:00' }, + { label: 'Daily at 3 AM', value: '0 3 * * *', description: 'Low traffic maintenance at 03:00' }, + { label: 'Daily at 6 AM', value: '0 6 * * *', description: 'Pre-business hours check' }, + { label: 'Daily at 6 PM', value: '0 18 * * *', description: 'End of business day backup' }, + { label: 'Daily at 10 PM', value: '0 22 * * *', description: 'Evening maintenance at 22:00' }, + + // Business Hours + { label: 'Workdays at 8 AM', value: '0 8 * * 1-5', description: 'Start of business day - Mon to Fri' }, + { label: 'Workdays at 9 AM', value: '0 9 * * 1-5', description: 'Business hours start check' }, + { label: 'Workdays at 12 PM', value: '0 12 * * 1-5', description: 'Midday check - Mon to Fri' }, + { label: 'Workdays at 5 PM', value: '0 17 * * 1-5', description: 'End of business day - Mon to Fri' }, + { label: 'Workdays at 6 PM', value: '0 18 * * 1-5', description: 'After hours backup - Mon to Fri' }, + + // Weekly Operations + { label: 'Weekly (Sunday midnight)', value: '0 0 * * 0', description: 'Weekly maintenance - Sunday 00:00' }, + { label: 'Weekly (Monday midnight)', value: '0 0 * * 1', description: 'Weekly start - Monday 00:00' }, + { label: 'Weekly (Friday 6 PM)', value: '0 18 * * 5', description: 'End of week backup - Friday 18:00' }, + { label: 'Weekly (Saturday 2 AM)', value: '0 2 * * 6', description: 'Weekend maintenance - Saturday 02:00' }, + + // Monthly Operations + { label: 'Monthly (1st at midnight)', value: '0 0 1 * *', description: 'Monthly maintenance - 1st of month' }, + { label: 'Monthly (1st at 2 AM)', value: '0 2 1 * *', description: 'Monthly backup - 1st at 02:00' }, + { label: 'Monthly (15th at midnight)', value: '0 0 15 * *', description: 'Mid-month maintenance - 15th' }, + { label: 'Monthly (last day)', value: '0 0 28-31 * *', description: 'End of month operations' } + ]; + public showCronDropdown: boolean = false; + public cronSearch: string = ''; + public selectedCronPreset: any = null; + public filteredCrons: any[] = []; public sorting = { enabled: true, multiSorting: true, @@ -156,22 +217,43 @@ export class UserTasksComponent implements OnInit { this.initGridTable(); } + 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; + } + submit(action: string) { var _self = this; if (action == "add") { this.data_provider .Add_task(_self.SelectedTask, _self.SelectedTaskItems) .then((res) => { - _self.initGridTable(); + if (res && res.status === 'failed') { + _self.show_toast("Error", res.err, "danger"); + } else { + _self.show_toast("Success", "Task created successfully", "success"); + _self.initGridTable(); + _self.EditTaskModalVisible = false; + } }); } else { this.data_provider .Edit_task(_self.SelectedTask, _self.SelectedTaskItems) .then((res) => { - _self.initGridTable(); + if (res && res.status === 'failed') { + _self.show_toast("Error", res.err, "danger"); + } else { + _self.show_toast("Success", "Task updated successfully", "success"); + _self.initGridTable(); + _self.EditTaskModalVisible = false; + } }); } - this.EditTaskModalVisible = false; } onSelectedRowsNewMembers(rows: Array): void { @@ -197,7 +279,7 @@ export class UserTasksComponent implements OnInit { this.SelectedTask = { id: 0, action: "add", - taskcron: "* * * * *", + cron: "0 2 * * *", desc_cron: "", description: "", members: "", @@ -206,7 +288,9 @@ export class UserTasksComponent implements OnInit { snippetid: "", task_type: "backup", }; - this.SelectedTask['data'] = { 'strategy': 'system', 'version_to_install': '', 'version_to_install_6': '' } + this.SelectedTask['data'] = { 'strategy': 'system', 'version_to_install': '', 'version_to_install_6': '' }; + this.cronSearch = ''; + this.selectedCronPreset = null; this.SelectedMembers = []; this.SelectedTaskItems = []; this.EditTaskModalVisible = true; @@ -215,6 +299,12 @@ export class UserTasksComponent implements OnInit { var _self = this; this.SelectedTask = { ...item }; + + // Initialize cron search and preset tracking + this.cronSearch = ''; + const currentCron = this.SelectedTask['cron']; + this.selectedCronPreset = this.predefinedCrons.find(cron => cron.value === currentCron) || null; + if (this.SelectedTask['task_type'] == 'firmware' && 'data' in this.SelectedTask && this.SelectedTask['data']) { this.SelectedTask['data'] = JSON.parse(this.SelectedTask['data']); if (this.SelectedTask['data']['strategy'] == 'defined') { @@ -243,7 +333,7 @@ export class UserTasksComponent implements OnInit { } } - _self.data_provider.get_snippets("", "", "", 0, 1000).then((res) => { + _self.data_provider.get_snippets("", "", "", 0, 1000,false).then((res) => { _self.Snippets = res.map((x: any) => { return { id: x.id, name: x.name }; }); @@ -315,7 +405,7 @@ export class UserTasksComponent implements OnInit { onSnippetsValueChanged(v: any) { var _self = this; if (v == "" || v.length < 3) return; - _self.data_provider.get_snippets(v, "", "", 0, 1000).then((res) => { + _self.data_provider.get_snippets(v, "", "", 0, 1000,false).then((res) => { _self.Snippets = res.map((x: any) => { return { id: String(x.id), name: x.name }; }); @@ -333,7 +423,12 @@ export class UserTasksComponent implements OnInit { } else { var _self = this; this.data_provider.Delete_task(_self.SelectedTask["id"]).then((res) => { - _self.initGridTable(); + if (res && res.status === 'failed') { + _self.show_toast("Error", res.err, "danger"); + } else { + _self.show_toast("Success", "Task deleted successfully", "success"); + _self.initGridTable(); + } _self.DeleteConfirmModalVisible = false; }); } @@ -363,4 +458,79 @@ export class UserTasksComponent implements OnInit { _self.loading = false; }); } + + selectCron(cron: any): void { + this.SelectedTask['cron'] = cron.value; + this.selectedCronPreset = cron; + this.cronSearch = ''; + this.showCronDropdown = false; + } + + filterCrons(event: any): void { + const searchTerm = event.target.value.toLowerCase(); + this.cronSearch = searchTerm; + this.selectedCronPreset = null; + + if (searchTerm.length > 0) { + this.filteredCrons = this.predefinedCrons.filter(cron => + cron.label.toLowerCase().includes(searchTerm) || + cron.description.toLowerCase().includes(searchTerm) || + cron.value.includes(searchTerm) + ); + } else { + this.filteredCrons = this.predefinedCrons; + } + } + + hideCronDropdown(): void { + setTimeout(() => { + this.showCronDropdown = false; + }, 200); + } + + onCronInputFocus(): void { + this.filteredCrons = this.predefinedCrons; + this.showCronDropdown = true; + + // If current cron matches a preset, highlight it + const currentCron = this.SelectedTask['cron']; + this.selectedCronPreset = this.predefinedCrons.find(cron => cron.value === currentCron) || null; + } + + onCronInputChange(event: any): void { + this.SelectedTask['cron'] = event.target.value; + this.selectedCronPreset = null; + + // Check if the entered value matches any preset + const enteredValue = event.target.value; + const matchingPreset = this.predefinedCrons.find(cron => cron.value === enteredValue); + if (matchingPreset) { + this.selectedCronPreset = matchingPreset; + } + } + + getCronDescription(): string { + if (this.selectedCronPreset) { + return this.selectedCronPreset.description; + } + + const currentCron = this.SelectedTask['cron']; + const matchingPreset = this.predefinedCrons.find(cron => cron.value === currentCron); + return matchingPreset ? matchingPreset.description : 'Custom cron expression'; + } + + onTaskTypeChange(): void { + if (this.SelectedTask['task_type'] === 'snippet') { + this.loadSnippets(); + } + } + + loadSnippets(): void { + var _self = this; + _self.data_provider.get_snippets("", "", "", 0, 10).then((res) => { + _self.Snippets = res.map((x: any) => { + return { id: x.id, name: x.name }; + }); + }); + } } diff --git a/src/app/views/user_tasks/user_tasks.module.ts b/src/app/views/user_tasks/user_tasks.module.ts index fdc3f47..e59ef63 100644 --- a/src/app/views/user_tasks/user_tasks.module.ts +++ b/src/app/views/user_tasks/user_tasks.module.ts @@ -9,6 +9,9 @@ import { GridModule, ModalModule, ButtonGroupModule, + BadgeModule, + AlertModule, + ToastModule, } from "@coreui/angular"; import { UserTasksRoutingModule } from "./user_tasks-routing.module"; import { UserTasksComponent } from "./user_tasks.component"; @@ -25,6 +28,9 @@ import { NgxSuperSelectModule} from "ngx-super-select"; FormModule, ButtonModule, ButtonGroupModule, + BadgeModule, + AlertModule, + ToastModule, GuiGridModule, ModalModule, ReactiveFormsModule, From cdc2e0cabf22c59ebd97138ab464032445e38eaf Mon Sep 17 00:00:00 2001 From: sepehr Date: Thu, 16 Oct 2025 17:33:57 +0300 Subject: [PATCH 09/15] feat: redesign sync and cloner interface for pro users - Complete UI/UX overhaul of cloner module - Enhanced configuration synchronization - Pro feature improvements - Better cloning workflow and interface --- src/app/views/cloner/cloner-styles.scss | 494 +++++++++++++++++++++ src/app/views/cloner/cloner.component.html | 386 ++++++++++------ src/app/views/cloner/cloner.component.ts | 31 +- src/app/views/cloner/cloner.module.ts | 7 +- src/app/views/cloner/cloner.scss | 1 + 5 files changed, 783 insertions(+), 136 deletions(-) create mode 100644 src/app/views/cloner/cloner-styles.scss diff --git a/src/app/views/cloner/cloner-styles.scss b/src/app/views/cloner/cloner-styles.scss new file mode 100644 index 0000000..dd8953e --- /dev/null +++ b/src/app/views/cloner/cloner-styles.scss @@ -0,0 +1,494 @@ +/* Modern Cloner Component Styles */ + +/* Form Sections */ +.cloner-form-section { + border-radius: 6px; + background: #f8f9fa; + padding: 0.75rem; + border: 1px solid #e9ecef; +} + +.section-header { + border-bottom: 1px solid #e9ecef; + padding-bottom: 0.5rem; +} + +.section-title { + color: #495057; + font-weight: 600; + font-size: 0.95rem; +} + +.form-input { + border-radius: 6px; + border: 1px solid #ced4da; + padding: 0.5rem 0.75rem; + font-size: 0.9rem; + transition: all 0.2s ease; +} + +.form-input-sm { + border-radius: 4px; + border: 1px solid #ced4da; + padding: 0.375rem 0.5rem; + font-size: 0.85rem; + height: calc(1.5em + 0.75rem + 2px); +} + +.form-input:focus { + border-color: #0d6efd; + box-shadow: 0 0 0 0.2rem rgba(13, 110, 253, 0.25); +} + +.form-label-sm { + display: block; + font-size: 0.8rem; + color: #6c757d; + margin-bottom: 0.25rem; + font-weight: 500; +} + +.form-label-xs { + display: block; + font-size: 0.75rem; + color: #6c757d; + margin-bottom: 0.125rem; + font-weight: 500; +} + +.form-select-sm { + font-size: 0.85rem; + height: calc(1.8em + 0.75rem + 2px); + padding: 0.375rem 0.75rem; + border-radius: 0.375rem; +} + +.form-select-xs { + font-size: 0.8rem; + height: calc(1.5em + 0.75rem + 2px); + padding: 0.25rem 0.5rem; + border-radius: 0.25rem; +} + +/* Commands Configuration */ +.commands-container { + background: white; + border-radius: 8px; + border: 1px solid #dee2e6; + overflow: hidden; +} + +.commands-container-compact { + background: white; + border-radius: 6px; + border: 1px solid #dee2e6; + overflow: hidden; +} + +.commands-nav { + background: #f8f9fa; + border-bottom: 2px solid #e9ecef; + padding: 0.5rem 1rem; +} + +.commands-nav-compact { + background: #f8f9fa; + border-bottom: 1px solid #e9ecef; + padding: 0.25rem 0.5rem; +} + +.commands-nav .nav-item { + margin-bottom: -2px; + cursor: pointer; +} + +.commands-nav .nav-link { + color: #6c757d; + border-style: none none solid; + border-width: 2px; + position: relative; + bottom: -1px; + cursor: pointer; + padding: 0.5rem 1rem; + font-size: 0.9rem; + font-weight: 500; +} + +.commands-nav .nav-link:hover, +.commands-nav .nav-link:focus { + border-color: #0d6efd; + color: #0d6efd; +} + +.commands-nav .nav-link.active { + color: #0d6efd; + background: transparent; + border-color: #0d6efd; +} + +.command-sections { + padding: 1rem; + min-height: 200px; +} + +.command-sections-compact { + padding: 0.5rem; + min-height: 120px; +} + +.command-category { + margin-bottom: 1.5rem; +} + +.command-category-compact { + margin-bottom: 0.75rem; +} + +.category-title { + color: #0d6efd; + margin-bottom: 0.75rem; + font-size: 0.9rem; + font-weight: 600; + border-bottom: 1px solid #e9ecef; + padding-bottom: 0.25rem; +} + +.category-title-compact { + color: #0d6efd; + margin-bottom: 0.375rem; + font-size: 0.8rem; + font-weight: 600; + border-bottom: 1px solid #e9ecef; + padding-bottom: 0.125rem; +} + +.commands-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + gap: 0.5rem; +} + +.commands-grid-compact { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + gap: 0.25rem; +} + +.command-item { + background: white; + border: 1px solid #e0e0e0; + border-radius: 6px; + transition: all 0.2s ease; +} + +.command-item-compact { + background: white; + border: 1px solid #e0e0e0; + border-radius: 4px; + transition: all 0.2s ease; +} + +.command-item:hover { + border-color: #0d6efd; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); +} + +.command-content { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.5rem 0.75rem; +} + +.command-content-compact { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.25rem 0.5rem; +} + +.command-name { + font-size: 0.8rem; + font-weight: 600; + color: #495057; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + flex: 1; + margin-right: 0.5rem; +} + +.command-name-compact { + font-size: 0.7rem; + font-weight: 600; + color: #495057; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + flex: 1; + margin-right: 0.25rem; +} + +/* Custom Switch Styling */ +.custom-switch { + position: relative; + width: 40px; + height: 20px; + flex-shrink: 0; +} + +.custom-switch-compact { + position: relative; + width: 32px; + height: 16px; + flex-shrink: 0; +} + +.custom-switch-compact input { + display: none; +} + +.custom-switch-compact .custom-control-label { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + border-radius: 16px; + background: #ccc; + cursor: pointer; + transition: all 0.3s ease; +} + +.custom-switch-compact .custom-control-label::after { + content: ''; + position: absolute; + left: 2px; + top: 2px; + width: 12px; + height: 12px; + background: #fff; + border-radius: 50%; + transition: all 0.3s ease; +} + +.custom-switch-compact input:checked + .custom-control-label { + background: #0d6efd; +} + +.custom-switch-compact input:checked + .custom-control-label::after { + left: calc(100% - 14px); +} + +.custom-switch input { + display: none; +} + +.custom-control-label { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + border-radius: 20px; + background: #ccc; + cursor: pointer; + transition: all 0.3s ease; +} + +.custom-control-label::after { + content: ''; + position: absolute; + left: 2px; + top: 2px; + width: 16px; + height: 16px; + background: #fff; + border-radius: 50%; + transition: all 0.3s ease; +} + +.custom-switch input:checked + .custom-control-label { + background: #0d6efd; +} + +.custom-switch input:checked + .custom-control-label::after { + left: calc(100% - 18px); +} + +/* Master Device Selection */ +.master-selection { + background: white; + border-radius: 6px; + border: 1px solid #dee2e6; + padding: 0.75rem; +} + +.master-selection-compact { + background: white; + border-radius: 4px; + border: 1px solid #dee2e6; + padding: 0.5rem; +} + +.master-device { + display: flex; + align-items: center; + padding: 0.5rem; + background: #fff3cd; + border: 1px solid #ffeaa7; + border-radius: 6px; +} + +.master-device-compact { + display: flex; + align-items: center; + padding: 0.375rem; + background: #fff3cd; + border: 1px solid #ffeaa7; + border-radius: 4px; +} + +.master-info { + display: flex; + align-items: center; +} + +.no-master { + display: flex; + align-items: center; + padding: 0.5rem; + background: #f8d7da; + border: 1px solid #f5c6cb; + border-radius: 6px; +} + +.no-master-compact { + display: flex; + align-items: center; + padding: 0.375rem; + background: #f8d7da; + border: 1px solid #f5c6cb; + border-radius: 4px; +} + +/* Peers Container */ +.peers-container { + background: white; + border-radius: 8px; + border: 1px solid #dee2e6; + overflow: hidden; +} + +.empty-peers { + display: flex; + flex-direction: column; + align-items: center; + padding: 2rem; + text-align: center; +} + +.empty-icon { + margin-bottom: 1rem; +} + +.empty-text strong { + display: block; + margin-bottom: 0.5rem; + color: #495057; +} + +.add-members-section { + padding: 1rem; + background: #f8f9fa; + border-top: 1px solid #dee2e6; +} + +/* Modal Enhancements */ +.c-modal-header.bg-light { + background: #f8f9fa !important; + border-bottom: 1px solid #dee2e6; +} + +.c-modal-header.bg-success { + background: #198754 !important; + border-bottom: 1px solid #146c43; +} + +.c-modal-footer.bg-light { + background: #f8f9fa !important; + border-top: 1px solid #dee2e6; +} + +.c-modal-body { + max-height: 80vh; + overflow-y: auto; +} + +/* Button Groups */ +.btn-group .btn { + font-size: 0.75rem; + padding: 0.25rem 0.5rem; +} + +.btn-group .btn:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +/* Responsive Design */ +@media (max-width: 768px) { + .cloner-form-section { + padding: 0.75rem; + margin-bottom: 0.75rem; + } + + .commands-grid { + grid-template-columns: 1fr; + } + + .command-content { + padding: 0.5rem; + } + + .command-name { + font-size: 0.75rem; + } + + .c-modal-dialog { + margin: 0.25rem; + } + + .section-title { + font-size: 0.9rem; + } + + .commands-nav .nav-link { + padding: 0.375rem 0.75rem; + font-size: 0.8rem; + } + + .master-device, + .no-master { + flex-direction: column; + align-items: flex-start; + gap: 0.25rem; + } +} + +@media (max-width: 576px) { + .c-modal-footer { + flex-direction: column; + align-items: stretch; + } + + .c-modal-footer > div { + width: 100%; + text-align: center; + margin-bottom: 0.5rem; + } + + .c-modal-footer > div:last-child { + margin-bottom: 0; + } +} \ No newline at end of file diff --git a/src/app/views/cloner/cloner.component.html b/src/app/views/cloner/cloner.component.html index b622ec7..67fd369 100644 --- a/src/app/views/cloner/cloner.component.html +++ b/src/app/views/cloner/cloner.component.html @@ -15,17 +15,27 @@ + + + {{value}} + + {{value}} - - - {{value}} + + + {{value == 'twoway' ? 'Two Way' : 'Master Mode'}} - + + + {{value ? 'Active' : 'Inactive'}} + + + {{value}} @@ -46,109 +56,190 @@ - - -
    Editing Cloner {{SelectedCloner['name']}}
    -
    Adding new task
    + + +
    Edit Cloner: {{SelectedCloner['name']}}
    +
    Add New Cloner
    - - - - - - - - - - - - - - - - - - - - - - - - - - -