+
+
+ Serial: {{ stats['serial'] }}
+ Copy
+
+
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}}
+
+
+
+
+
+
License User name is not set in settings read more!
+
Serial number not submitedread more!
-
-
-
+
+
+
+
+
+
+
@@ -165,4 +261,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..f1ff857 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";
@@ -31,12 +32,12 @@ import { ClipboardModule } from "@angular/cdk/clipboard";
ReactiveFormsModule,
ButtonModule,
TemplateIdDirective,
- ButtonModule,
ButtonGroupModule,
ChartjsModule,
CarouselModule,
BadgeModule,
ClipboardModule,
+ ModalModule,
],
declarations: [DashboardComponent],
})
diff --git a/src/app/views/device_detail/active-users/active-users-routing.module.ts b/src/app/views/device_detail/active-users/active-users-routing.module.ts
new file mode 100644
index 0000000..ab5a4a4
--- /dev/null
+++ b/src/app/views/device_detail/active-users/active-users-routing.module.ts
@@ -0,0 +1,21 @@
+import { NgModule } from '@angular/core';
+import { RouterModule, Routes } from '@angular/router';
+import { ActiveUsersComponent } from './active-users.component';
+
+const routes: Routes = [
+ {
+ path: '',
+ component: ActiveUsersComponent,
+ data: {
+ title: 'Widgets'
+ }
+ }
+];
+
+
+@NgModule({
+ imports: [RouterModule.forChild(routes)],
+ exports: [RouterModule]
+})
+export class ActiveUsersRoutingModule {
+}
diff --git a/src/app/views/device_detail/active-users/active-users.component.html b/src/app/views/device_detail/active-users/active-users.component.html
new file mode 100644
index 0000000..5dca6d7
--- /dev/null
+++ b/src/app/views/device_detail/active-users/active-users.component.html
@@ -0,0 +1,38 @@
+
+
+
+
+ Active Users
+
+
+ Current active users :
+ {{active_users.length}}
+
+
+
+ | # |
+ Address |
+ Att |
+ Group |
+ Name |
+ Via |
+ kill |
+
+
+
+
+ | {{i+1}} |
+ {{item['address']}} |
+ {{item['when']}} |
+ {{item['group']}} |
+ {{item['name']}} |
+ {{item['via']}} |
+ |
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/app/views/device_detail/active-users/active-users.component.scss b/src/app/views/device_detail/active-users/active-users.component.scss
new file mode 100644
index 0000000..e69de29
diff --git a/src/app/views/device_detail/active-users/active-users.component.ts b/src/app/views/device_detail/active-users/active-users.component.ts
new file mode 100644
index 0000000..215c1d7
--- /dev/null
+++ b/src/app/views/device_detail/active-users/active-users.component.ts
@@ -0,0 +1,27 @@
+import { AfterContentInit, ChangeDetectionStrategy, ChangeDetectorRef, Component,Input } from '@angular/core';
+import { dataProvider } from "../../../providers/mikrowizard/data";
+@Component({
+ selector: 'app-active-users',
+ templateUrl: './active-users.component.html',
+ styleUrls: ['./active-users.component.scss'],
+ changeDetection: ChangeDetectionStrategy.Default
+})
+export class ActiveUsersComponent implements AfterContentInit {
+ @Input() active_users: any;
+ @Input() devid: number;
+
+ constructor(
+ private changeDetectorRef: ChangeDetectorRef,
+ private data_provider: dataProvider,
+ ) {}
+ killsession(item:any){
+ console.log(item);
+ this.data_provider.killSession(this.devid,item).then((res) => {
+ this.active_users = res;
+ });
+ }
+ ngAfterContentInit(): void {
+
+ this.changeDetectorRef.detectChanges();
+ }
+}
diff --git a/src/app/views/device_detail/active-users/active-users.module.ts b/src/app/views/device_detail/active-users/active-users.module.ts
new file mode 100644
index 0000000..65adf0b
--- /dev/null
+++ b/src/app/views/device_detail/active-users/active-users.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 { ActiveUsersComponent } from './active-users.component';
+
+@NgModule({
+ declarations: [
+ ActiveUsersComponent,
+ ],
+ imports: [
+ CardModule,
+ CommonModule,
+ GridModule,
+ ProgressModule,
+ FormModule,
+ ButtonModule,
+ ButtonGroupModule,
+ NavbarModule,
+ ModalModule,
+ TableModule,
+ UtilitiesModule,
+ BadgeModule,
+ TooltipModule,
+ ],
+ exports: [
+ ActiveUsersComponent,
+ ]
+})
+export class ActiveUsersModule {
+}
diff --git a/src/app/views/device_detail/device-info/device-info-routing.module.ts b/src/app/views/device_detail/device-info/device-info-routing.module.ts
new file mode 100644
index 0000000..4d141ab
--- /dev/null
+++ b/src/app/views/device_detail/device-info/device-info-routing.module.ts
@@ -0,0 +1,21 @@
+import { NgModule } from '@angular/core';
+import { RouterModule, Routes } from '@angular/router';
+import { DeviceInfoComponent } from './device-info.component';
+
+const routes: Routes = [
+ {
+ path: '',
+ component: DeviceInfoComponent,
+ data: {
+ title: 'Widgets'
+ }
+ }
+];
+
+
+@NgModule({
+ imports: [RouterModule.forChild(routes)],
+ exports: [RouterModule]
+})
+export class DeviceInfoRoutingModule {
+}
diff --git a/src/app/views/device_detail/device-info/device-info.component.html b/src/app/views/device_detail/device-info/device-info.component.html
new file mode 100644
index 0000000..a20b979
--- /dev/null
+++ b/src/app/views/device_detail/device-info/device-info.component.html
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+ Device offline or not accessible for MikroWizard
+
+
+ Firmware Upgrade Detected
+
+
+ Firmware Upgrade Detected
+
+
+
+ {{item.key}}
+ {{ item.value }}
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/app/views/device_detail/device-info/device-info.component.scss b/src/app/views/device_detail/device-info/device-info.component.scss
new file mode 100644
index 0000000..e69de29
diff --git a/src/app/views/device_detail/device-info/device-info.component.ts b/src/app/views/device_detail/device-info/device-info.component.ts
new file mode 100644
index 0000000..a2d8521
--- /dev/null
+++ b/src/app/views/device_detail/device-info/device-info.component.ts
@@ -0,0 +1,25 @@
+import { AfterContentInit, ChangeDetectionStrategy, ChangeDetectorRef, Component,Input } from '@angular/core';
+
+@Component({
+ selector: 'app-device-info',
+ templateUrl: './device-info.component.html',
+ styleUrls: ['./device-info.component.scss'],
+ changeDetection: ChangeDetectionStrategy.Default
+})
+export class DeviceInfoComponent implements AfterContentInit {
+ @Input() devdata: any;
+ constructor(
+ private changeDetectorRef: ChangeDetectorRef
+ ) {}
+
+ checkitem(item: any) {
+ if (item.value && !item.key.match("sensors|id|_availble|interfaces|active_users|ping|status|created|online|syslog_configured|port")) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+ ngAfterContentInit(): void {
+ this.changeDetectorRef.detectChanges();
+ }
+}
diff --git a/src/app/views/device_detail/device-info/device-info.module.ts b/src/app/views/device_detail/device-info/device-info.module.ts
new file mode 100644
index 0000000..3b05bf7
--- /dev/null
+++ b/src/app/views/device_detail/device-info/device-info.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 { DeviceInfoComponent } from './device-info.component';
+
+@NgModule({
+ declarations: [
+ DeviceInfoComponent,
+ ],
+ imports: [
+ CardModule,
+ AlertModule,
+ CommonModule,
+ GridModule,
+ ProgressModule,
+ FormModule,
+ ButtonModule,
+ ButtonGroupModule,
+ ChartjsModule,
+ WidgetsModule,
+ NavbarModule,
+ ModalModule,
+ TableModule,
+ UtilitiesModule,
+ BadgeModule,
+ NavModule,
+ TabsModule,
+ ],
+ exports: [
+ DeviceInfoComponent,
+ ]
+})
+export class DeviceInfoModule {
+}
diff --git a/src/app/views/device_detail/device.component.html b/src/app/views/device_detail/device.component.html
index 2da3363..140e6d0 100644
--- a/src/app/views/device_detail/device.component.html
+++ b/src/app/views/device_detail/device.component.html
@@ -1,234 +1,321 @@
-
Firmware
- Update availble For This Device!
-
Device is updated but needs to upgrade firmware!
-
-
-
-
-
-
-
- {{devdata['name'] }} ( {{devdata['ip'] }} )
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{item.key}}
- {{ item.value }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+ {{ 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"] }}
+
+
-
-
-
- =objectlen(raddata.value['data'])/4 && i<(objectlen(raddata.value['data'])/4)*2">
- | {{d.key}} |
- {{d.value}} |
-
-
-
-
-
-
-
-
-
- =(objectlen(raddata.value['data'])/4)*2 && i<(objectlen(raddata.value['data'])/4)*3">
- | {{d.key}} |
- {{d.value}} |
-
-
-
-
-
-
-
-
-
- =(objectlen(raddata.value['data'])/4)*3">
- | {{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 }} |
+
+
+
+
+
+
+
+
+
+ = objectlen(raddata.value['data']) / 4 &&
+ i < (objectlen(raddata.value['data']) / 4) * 2
+ ">
+ |
+ {{ d.key }}
+ |
+ {{ d.value }} |
+
+
+
+
+
+
+
+
+
+ = (objectlen(raddata.value['data']) / 4) * 2 &&
+ i < (objectlen(raddata.value['data']) / 4) * 3
+ ">
+ |
+ {{ d.key }}
+ |
+ {{ d.value }} |
+
+
+
+
+
+
+
+
+
+ = (objectlen(raddata.value['data']) / 4) * 3
+ ">
+ | {{ d.key }} |
+ {{ d.value }} |
+
+
+
+
+
+
+
+
+
+
+
+ |
+ Strength at rates
+ |
+
+ {{ st }}
+ |
+
+
+
+
+
+
+
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
- {{interface_rate['name']}}
+ {{ interface_rate["name"] }}
-
-
+
@@ -265,42 +278,68 @@
- Editing Group
+ Task History
-
-
- Group Members :
-
-
-
- {{value}}
-
-
-
- {{value}}
-
-
-
- {{value}}
-
-
-
- {{value}}
-
-
-
-
- download
-
-
-
-
-
-
+
+
+
+ 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}}
+ 0" style="color: red; margin-left: 5px;">✗ {{item.failed_count}}
+
+
+
+
+
+
+ Details
+
+
+ CSV
+
+
+
+
@@ -319,6 +358,10 @@
update?
Are you sure that You want to update firmware of selected
devices?
+ Are you sure that You want to upgrade firmware of selected
+ devices?
+ Are you sure that You want to reboot the selected
+ devices?
Are you sure that You want toDelete Device {{selected_device.name}} ?
@@ -337,6 +380,12 @@
Yes
+
+ Yes
+
+
+ Yes
+
Yes,Delete Device
@@ -378,7 +427,8 @@
peer ip
-
@@ -394,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
+
+
0">
+
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
+
+
+ Download Results
+
+
+
+
+
+ Add Devices
+
+
+ Close
+
+
+ Cancel
+
+
+
+
+
+
+
+ Web Access Options
+
+
+
+ Choose how to access the device:
+
+
+ Proxy Access, through Mikrowizard server
+
+
+ Direct Access
+
+
+
+
+ Cancel
+
+
+
+
+
+
+ {{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 Address |
+ Status |
+ Error Details |
+ Error Details |
+
+
+
+
+ | {{result.ip}} |
+
+
+ {{result.added ? 'Success' : 'Failed'}}
+
+ |
+
+ {{result.failures || 'N/A'}}
+ |
+
+ {{result.faileres || 'N/A'}}
+ |
+
+
+
+
+
+ Success: {{selectedTaskDetails.success_count}}
+ Failed: {{selectedTaskDetails.failed_count}}
+
+
+
+
+ Download CSV
+
+
+ Close
+
+
+
+
\ 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 8ce0f7f..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,14 +188,19 @@ 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 }]);
break;
+ case "devlogs":
+ this.router.navigate(["/devlogs", { devid: dev.id }]);
+ break;
case "logacc":
this.router.navigate(["/accountlog", { devid: dev.id }]);
break;
@@ -180,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";
@@ -194,7 +223,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 +259,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 +352,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;
});
}
@@ -354,55 +400,106 @@ export class DevicesComponent implements OnInit, OnDestroy {
}
check_firmware() {
var _self = this;
+ this.ConfirmModalVisible = false;
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);
+ }
});
}
update_firmware() {
var _self = this;
+ this.ConfirmModalVisible = false;
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 if ("error" in res) {
+ _self.show_toast("Error", res.error, "danger");
+ }
+ else{
_self.show_toast("info", "Updating Firmwares Sent", "light");
- _self.initGridTable();
+ _self.initGridTable();}
});
}
upgrade_firmware() {
var _self = this;
+ this.ConfirmModalVisible = false;
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();
+ }
});
}
reboot_devices() {
var _self = this;
+ this.ConfirmModalVisible = false;
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,13 +514,23 @@ export class DevicesComponent implements OnInit, OnDestroy {
};
_self.data_provider.get_dev_list(data).then((res) => {
- _self.source = res.map((x: any) => {
+ if ("error" in res && res.error.indexOf("Unauthorized")) {
+ _self.show_toast(
+ "Error",
+ "You are not authorized to perform this action",
+ "danger"
+ );
+ }
+ else{
+ _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;
+ }
});
}
@@ -516,8 +623,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 +646,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;
@@ -545,25 +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],
})
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}}
+
+
+
+
+
+
+ View devices
+
+
+
+
+
+
+
Actions Menu
+
+ Edit Group
+
+ Manage Users
+
+ Update Firmware
+
+ Upgrade Firmware
+
+ Delete Group
+
+
@@ -59,92 +97,146 @@
-
-
- Group Edit
+
+
+ Edit Device Group
-
-
-
-
-
+
+
+
+
+
+
-
-
- Group Members :
-
-
-
- {{value}}
-
-
-
- {{value}}
-
-
-
-
-
-
-
-
-
- Delete {{MemberRows.length}} Selected
-
-
-
+ Add new Members
+
+ {{groupMembers.length}} Devices
+
+
+
+
+
+
+ Current Group Members
+
+ 0" cButton color="danger" size="sm" variant="outline" class="me-2">
+ Remove {{MemberRows.length}} Selected
+
+
+ Add Devices
+
+
+
+
+
+
+
No devices in this group
+
Click "Add Devices" to start adding devices to this group
+
+ 0">
+
+
+
+
+
+ {{value}}
+
+
+
+
+
+ {{value}}
+
+
+
+
+
+
+
+
+
+
+
+
+
-
- save
+
+
+ Save Changes
+
- Close
+ Cancel
-
-
- Members not in Group
+
+
+ Add Devices to Group
-
-
- Members Availble to add:
-
-
-
- {{value}}
-
-
-
- {{value}}
-
-
-
-
- {{value}}
-
-
-
-
-
-
+
+
+
+ 0" color="info" class="d-flex align-items-center mb-0">
+
+ {{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
+
+ 0">
+
+
+
+
+
+ {{value}}
+
+
+
+
+
+ {{value}}
+
+
+
+
+ {{value}}
+
+
+
+
+
+
-
- Add {{
- NewMemberRows.length }}
-
- Close
-
+
+
+ Select devices from the list above to add them to the group
+
+
+ 0" (click)="add_new_members()" cButton color="success">
+ Add {{NewMemberRows.length}} Device(s)
+
+
+ Cancel
+
+
@@ -175,4 +267,200 @@
Close
+
+
+
+ User Permissions - {{selectedGroup?.name}}
+
+
+
+
+
+ Add User Permission
+
+
+
+
+
+
+
+
0" class="search-dropdown">
+
+ {{user.username}} ({{user.first_name}} {{user.last_name}})
+
+
+
+ No users found
+
+
+
+
+
+
+
+
0" class="search-dropdown">
+
+ {{perm.name}}
+
+
+
+ No permissions found
+
+
+
+
+
+ Add Permission
+
+
+
+
+
+
+
+ Current Permissions ({{selectedGroup?.assigned_users?.length || 0}} users)
+
+
+
+
+
No users have permissions for this group
+
+ 0" class="table-responsive">
+
+
+
+ | # |
+ User |
+ Name |
+ Permission |
+ Actions |
+
+
+
+
+ | {{i + 1}} |
+
+
+
+ {{user.username}}
+
+ |
+ {{user.first_name}} {{user.last_name}} |
+
+ {{user.perm_name}}
+ |
+
+
+
+
+
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+ Close
+
+
+
+
+
+
+ Change Permission for {{editingUser?.username}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Update
+
+
+ Cancel
+
+
+
+
+
+
+ Remove Permission
+
+
+
+
+
+
Are you sure you want to remove {{removingUser?.username}}'s permission from group {{selectedGroup?.name}}?
+
This action cannot be undone.
+
+
+
+
+ Remove
+
+
+ Cancel
+
+
+
+
+
+ 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.
+
+
+
+
+ {{firmwareAction | titlecase}} Firmware
+
+
+ Cancel
+
+
\ 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],
})
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 @@
+
+
+
+
+ Refresh
+
+
+
+
+
+
+
+
+
+
+
+ Web Access Options
+
+
+
+ Choose how to access the device:
+
+
+ Proxy Access , through Mikrowizard server
+
+
+ Direct Access
+
+
+
+
+ Cancel
+
+
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 {}
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..8b66fb9 100644
--- a/src/app/views/settings/settings.component.html
+++ b/src/app/views/settings/settings.component.html
@@ -1,216 +1,510 @@
-
-
-
- Firmware Manager
-
-
- Firmware in repository:
-
-
-
- {{value}}
-
-
-
- {{value}}
-
-
-
-
- {{value}}
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+ Firmware Manager
+
+
+ System Configuration
+
+
-
-
- |
- Add new Permission
- |
-
-
-
- Fetching Information from mikrotik website...
-
- |
-
-
-
-
-
-
-
- {{firm}}
-
-
-
- |
-
- Download to
- repository
-
- |
-
+
+
+
+
+
+
+ Firmware Repository
+
+
+
+ {{source.length}} Versions Available
+
+
+
+
+ Tip: Keep multiple firmware versions for compatibility. Always test updates on non-critical devices first.
+
+
+
+
+
+ {{value}}
+
+
+
+
+ {{value}}
+
+
+
+
+ {{value.substring(0, 16)}}...
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
- * Choose how Mikrowizard should update old v6 firmwares
-
+
+
+
+
+
+
+ Internet Required: Downloads from mikrotik.com. Ensure stable connection and access to mikrotik.com domain.
+
+
+
+
+
+
+
0" class="search-dropdown">
+
+ {{firm}}
+
+
+
+ No versions found
+
+
+
+
+
+ Download
+
+
+
+
+
+ Fetching available versions from MikroTik...
+
+
+
-
-
-
-
-
-
-
-
- * The version of firmware to install routers
-
-
-
-
-
-
-
-
-
- * The version of firmware to install on V6 routers
-
- Save
-
-
-
-
- System Settings
-
-
-
- Rad Secret
-
-
- * Radius Secret of Mikrowizard Radius Server
-
-
-
-
- System URL
-
-
- * Default system access URl
-
-
-
- Default IP
-
-
- * Default Mikrowizard Access IP
-
-
- System Time Zone
-
- Select event type
-
-
-
-
-
- {{tz.text}}
-
-
-
-
- * Default TimeZone for the system
-
-
-
-
- Default User
-
- Default password
-
-
- * Default username and Password for searching new devices
-
-
- License Username
-
-
- * The username that you registred in Mikrowizard.com,Required for License Activation
-
+
+
+
+ Update Configuration
+
+
+
+
+ Best Practice: Use "Keep v6" for legacy devices. Test new firmware versions in lab environment first.
+
+
+
+
+
+
+
+
+
+
+
+ 📋 Keep v6: v6 devices use V6 Firmware Version, v7 devices use Default Firmware Version
+
+
-
-
-
-
- * Force User Groups under user>groups configuration of each router to match Mikrowizard Permissions and
- monitor for any change to prevent/fix the configuration.
-
+
+
+
+
+
+
+
+ 💡 Tip: Choose stable releases for production. LTS versions are only available for v6 firmware
+
+
+
+
+
+
+
+
+
+
+ Firmware version for RouterOS v6 devices
+
+
+
+
+
+
+ Save Firmware Settings
+
+
+
+
-
-
-
-
- * 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.
-
+
+
+
+
+
+ Network Configuration
+
+
+
+
+ Network Setup: Configure these settings to match your network infrastructure for proper device communication.
+
+
+
+
+
+
+
+ 🌐 Examples: https://mikrowizard.company.com or 192.168.1.100
+
+
+
+
+
+
+
+ 🔧 Tip: Use static IP outside DHCP range (e.g., 192.168.1.100-200)
+
+
+
+
+
+
+
+ 🔐 Internal RADIUS: Secret for MikroWizard's built-in RADIUS server (for MikroTik admin authentication)
+
+
+
+
+
+
+
+
+
0" class="search-dropdown">
+
+ {{tz.text}}
+
+
+
+ No timezones found
+
+
+
Default system timezone
+
+
+
+
-
-
-
-
- * 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.
-
+
+
+
+ Device Discovery Credentials
+
+
+
+
+ Security Warning: Change default passwords immediately after device setup. Use strong, unique credentials.
+
+
+
+
+
+
+
+ 🔍 Device Discovery: Username for finding MikroTik devices with default credentials in your network
+
+
+
+
+
+
+
+ 🔍 Device Discovery: Password for finding MikroTik devices with default credentials (often empty on new devices)
+
+
+
+
-
-
-
- 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
-
+
+
+
+ License Configuration
+
+
+
+
+
+
+
+
+ 🔑 Same username you use to login to mikrowizard.com website
+
+
+
+
-
-
-
- PRO
- * Force login to devices using otp for all users.(you can make exceptions for each user)
-
- Save
+
+
+
+ System Behavior
+
+
+
+
+
+
+
+
+
+
+
+
+ 📊 Manual Mode: Updates require confirmation and actions in dashboard view
+
+
+
+
-
-
-
-
+
+
+
+ Security & Enforcement
+
+
+
+
+ Important: These settings will override device configurations. Test in lab environment before enabling in production.
+
+
+
+
+
+
+
Force Permissions
+
+
+
🔄 Sync MikroTik user groups with MikroWizard permissions
+
💡 Ensures consistent user access control across all devices
+
+
+
+
+
+
+
+
+
+
+
+
+
Force RADIUS
+
+
+
🔐 Configure RADIUS on MikroTik devices for admin user authentication
+
👥 For MikroTik system users authentication (not end users)
+
+
+
+
+
+
+
+
+
+
+
+
+
Force Syslog
+
+
+
📊 Automatically configure syslog settings on managed devices
+
⚠️ Will OVERWRITE existing syslog configurations on devices
+
+
+
+
+
+
+
+
+
+
+
+
+
Safe Updates PRO
+
+
+
🛡️ Install required intermediate firmware versions before target version
+
✅ Highly recommended for production environments
+
+
+
+
+
+
+
+
+
+
+
+
+
Force Device OTP PRO
+
+
+
🔐 Require OTP authentication for all device access
+
⚠️ Ensure users have OTP configured before enabling
+
+
+
+
+
+
+
+
+
+
+
+
+
WebFig Auto Login PRO
+
+
+
🌐 Create temporary OTP for automatic WebFig login
+
🔑 Users don't need to input passwords to access WebFig admin interface
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Save System Settings
+
+
+
+
+
+
+
+ Loading system configuration...
+
+
+
diff --git a/src/app/views/settings/settings.component.scss b/src/app/views/settings/settings.component.scss
index 0185fbf..364f8c3 100644
--- a/src/app/views/settings/settings.component.scss
+++ b/src/app/views/settings/settings.component.scss
@@ -5,6 +5,629 @@
}
}
}
+
.mdc-line-ripple.mdc-line-ripple--deactivating.ng-star-inserted {
display: none!important;
+}
+
+/* Settings Container */
+.settings-container {
+ max-width: 1200px;
+ margin: 0 auto;
+}
+
+.settings-card {
+ border: none;
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
+ border-radius: 12px;
+ overflow: hidden;
+}
+
+.settings-header {
+ background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
+ border-bottom: 1px solid #dee2e6;
+ padding: 1.25rem 1.5rem;
+}
+
+/* Tabs */
+.settings-tabs {
+ background: #f8f9fa;
+ border-bottom: 1px solid #dee2e6;
+ padding: 0 1.5rem;
+ display: flex;
+}
+
+.tab-link {
+ border: none;
+ background: transparent;
+ border-radius: 0;
+ padding: 1rem 1.5rem;
+ font-weight: 500;
+ color: #6c757d;
+ transition: all 0.2s ease;
+ border-bottom: 3px solid transparent;
+ cursor: pointer;
+}
+
+.tab-link:hover {
+ color: #495057;
+ background: rgba(13, 110, 253, 0.05);
+}
+
+.tab-link.active {
+ color: #0d6efd;
+ background: white;
+ border-bottom-color: #0d6efd;
+}
+
+/* Tab Content */
+.tab-content {
+ padding: 2rem;
+}
+
+/* Section Titles */
+.section-title {
+ color: #495057;
+ font-weight: 600;
+ font-size: 1.1rem;
+ margin-bottom: 1rem;
+ display: flex;
+ align-items: center;
+}
+
+/* Status Section */
+.status-section {
+ background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);
+ border: 1px solid #e9ecef;
+ border-radius: 8px;
+ padding: 1.5rem;
+}
+
+/* Download Section */
+.download-section {
+ .section-card {
+ background: linear-gradient(135deg, #e7f3ff 0%, #f8f9fa 100%);
+ border: 2px dashed #0d6efd;
+ border-radius: 8px;
+ padding: 1.5rem;
+ transition: all 0.2s ease;
+ }
+
+ .section-card:hover {
+ border-color: #0a58ca;
+ background: linear-gradient(135deg, #cfe2ff 0%, #e7f3ff 100%);
+ }
+
+ .section-header {
+ display: flex;
+ align-items: center;
+ font-size: 1rem;
+ font-weight: 600;
+ color: #495057;
+ }
+}
+
+/* Firmware Settings */
+.firmware-settings {
+ background: #f8f9fa;
+ border-radius: 8px;
+ padding: 1.5rem;
+ border: 1px solid #e9ecef;
+}
+
+/* Config Sections */
+.config-section {
+ background: #f8f9fa;
+ border-radius: 8px;
+ padding: 1.5rem;
+ border: 1px solid #e9ecef;
+}
+
+/* Setting Groups */
+.setting-group {
+ margin-bottom: 1rem;
+}
+
+.setting-label {
+ display: block;
+ font-weight: 600;
+ color: #495057;
+ margin-bottom: 0.5rem;
+ font-size: 0.9rem;
+}
+
+.setting-help {
+ color: #6c757d;
+ font-size: 0.8rem;
+ margin-top: 0.25rem;
+ display: block;
+}
+
+/* Security Switches */
+.security-switches {
+ display: grid;
+ gap: 1rem;
+}
+
+.switch-item {
+ background: white;
+ border: 1px solid #e9ecef;
+ border-radius: 8px;
+ padding: 1.25rem;
+ transition: all 0.2s ease;
+}
+
+.switch-item:hover {
+ border-color: #0d6efd;
+ box-shadow: 0 2px 4px rgba(13, 110, 253, 0.1);
+}
+
+.switch-content {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.switch-info {
+ flex: 1;
+ margin-right: 1rem;
+}
+
+.switch-title {
+ font-size: 1rem;
+ font-weight: 600;
+ color: #495057;
+ margin-bottom: 0.25rem;
+ display: flex;
+ align-items: center;
+}
+
+.switch-description {
+ color: #6c757d;
+ font-size: 0.85rem;
+ margin: 0;
+ line-height: 1.4;
+}
+
+/* Compact Grid */
+.compact-grid {
+ border-radius: 6px;
+ overflow: hidden;
+ border: 1px solid #e9ecef;
+}
+
+/* Version Badge */
+.version-badge {
+ font-family: 'Courier New', monospace;
+ font-weight: 600;
+}
+
+
+
+/* Form Controls */
+.form-control, .form-select {
+ border-radius: 6px;
+ border: 1px solid #ced4da;
+ padding: 0.5rem 0.75rem;
+ font-size: 0.9rem;
+ transition: all 0.2s ease;
+}
+
+.form-control:focus, .form-select:focus {
+ border-color: #0d6efd;
+ box-shadow: 0 0 0 0.2rem rgba(13, 110, 253, 0.25);
+}
+
+/* Save Section */
+.save-section {
+ text-align: center;
+ padding-top: 1rem;
+ border-top: 1px solid #e9ecef;
+}
+
+/* Form Check Labels */
+.form-check-label {
+ font-weight: 500;
+}
+
+/* Button Enhancements */
+.btn {
+ border-radius: 6px;
+ font-weight: 500;
+ transition: all 0.2s ease;
+}
+
+.btn:hover {
+ transform: translateY(-1px);
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+}
+
+/* Badge Enhancements */
+.badge {
+ font-weight: 500;
+ padding: 0.4em 0.6em;
+}
+
+/* Mobile Responsive for Informative Elements */
+@media (max-width: 768px) {
+ .info-banner,
+ .warning-banner,
+ .success-banner {
+ padding: 0.5rem 0.75rem;
+ font-size: 0.8rem;
+ }
+
+ .help-icon {
+ font-size: 0.75rem;
+ }
+
+ .setting-help {
+ font-size: 0.75rem;
+ }
+
+ .switch-description {
+ font-size: 0.8rem;
+ }
+
+ .switch-description small {
+ font-size: 0.7rem;
+ }
+
+ ::ng-deep .tooltip-inner {
+ max-width: 250px;
+ font-size: 0.75rem;
+ }
+}
+
+@media (max-width: 576px) {
+ .info-banner,
+ .warning-banner,
+ .success-banner {
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 0.25rem;
+ }
+
+ .setting-label {
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 0.25rem;
+ }
+}
+
+/* Original Responsive Design */
+@media (max-width: 768px) {
+ .settings-header {
+ padding: 1rem;
+ }
+
+ .settings-tabs {
+ padding: 0 1rem;
+ }
+
+ .tab-content {
+ padding: 1rem;
+ }
+
+ .tab-link {
+ padding: 0.75rem 1rem;
+ font-size: 0.9rem;
+ }
+
+ .status-section,
+ .config-section,
+ .firmware-settings {
+ padding: 1rem;
+ }
+
+ .download-section .section-card {
+ padding: 1rem;
+ }
+
+ .switch-content {
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 1rem;
+ }
+
+ .switch-info {
+ margin-right: 0;
+ }
+
+ .section-title {
+ font-size: 1rem;
+ }
+}
+
+@media (max-width: 576px) {
+ .settings-container {
+ margin: 0 0.5rem;
+ }
+
+ .settings-card {
+ border-radius: 8px;
+ }
+
+ .tab-content {
+ padding: 0.75rem;
+ }
+
+ .status-section,
+ .config-section,
+ .firmware-settings {
+ padding: 0.75rem;
+ }
+}
+
+/* Search Select Components */
+.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: #0d6efd;
+ box-shadow: 0 0 0 0.2rem rgba(13, 110, 253, 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;
+ font-size: 0.85rem;
+}
+
+.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;
+ font-size: 0.8rem;
+}
+
+/* Animation for dropdown */
+@keyframes fadeIn {
+ from { opacity: 0; transform: translateY(-10px); }
+ to { opacity: 1; transform: translateY(0); }
+}
+
+.search-dropdown {
+ animation: fadeIn 0.2s ease-out;
+}
+
+/* Informative Design Elements */
+.help-icon {
+ font-size: 0.8rem;
+ cursor: help;
+ opacity: 0.7;
+ transition: opacity 0.2s ease;
+}
+
+.help-icon:hover {
+ opacity: 1;
+}
+
+.help-icon.text-danger {
+ color: #dc3545 !important;
+ opacity: 1;
+}
+
+.help-icon.text-warning {
+ color: #fd7e14 !important;
+ opacity: 0.9;
+}
+
+.info-banner {
+ background: linear-gradient(135deg, #e3f2fd 0%, #bbdefb 100%);
+ border: 1px solid #2196f3;
+ border-radius: 6px;
+ padding: 0.75rem 1rem;
+ color: #0d47a1;
+ font-size: 0.85rem;
+ display: flex;
+ align-items: center;
+}
+
+.warning-banner {
+ background: linear-gradient(135deg, #fff3e0 0%, #ffcc80 100%);
+ border: 1px solid #ff9800;
+ border-radius: 6px;
+ padding: 0.75rem 1rem;
+ color: #e65100;
+ font-size: 0.85rem;
+ display: flex;
+ align-items: center;
+}
+
+.success-banner {
+ background: linear-gradient(135deg, #e8f5e8 0%, #c8e6c9 100%);
+ border: 1px solid #4caf50;
+ border-radius: 6px;
+ padding: 0.75rem 1rem;
+ color: #2e7d32;
+ font-size: 0.85rem;
+ display: flex;
+ align-items: center;
+}
+
+/* Enhanced Setting Help */
+.setting-help {
+ color: #6c757d;
+ font-size: 0.8rem;
+ margin-top: 0.25rem;
+ display: block;
+ line-height: 1.4;
+}
+
+.setting-help strong {
+ color: #495057;
+}
+
+/* Enhanced Form Labels */
+.setting-label {
+ display: flex;
+ align-items: center;
+ font-weight: 600;
+ color: #495057;
+ margin-bottom: 0.5rem;
+ font-size: 0.9rem;
+}
+
+/* Enhanced Switch Descriptions */
+.switch-description {
+ color: #6c757d;
+ font-size: 0.85rem;
+ margin: 0;
+ line-height: 1.4;
+}
+
+.switch-description small {
+ display: block;
+ margin-top: 0.25rem;
+ font-size: 0.75rem;
+}
+
+.switch-description .text-warning {
+ color: #856404 !important;
+}
+
+.switch-description .text-info {
+ color: #0c5460 !important;
+}
+
+.switch-description .text-success {
+ color: #155724 !important;
+}
+
+.switch-description .text-danger {
+ color: #721c24 !important;
+}
+
+.setting-help.text-danger {
+ color: #721c24 !important;
+ font-weight: 600;
+}
+
+.setting-help.text-warning {
+ color: #856404 !important;
+ font-weight: 600;
+}
+
+/* Enhanced Section Titles */
+.section-title {
+ color: #495057;
+ font-weight: 600;
+ font-size: 1.1rem;
+ margin-bottom: 1rem;
+ display: flex;
+ align-items: center;
+}
+
+/* Tooltip Enhancements */
+::ng-deep .tooltip {
+ font-size: 0.8rem;
+}
+
+::ng-deep .tooltip-inner {
+ max-width: 300px;
+ text-align: left;
+ background-color: #2c3e50;
+ border-radius: 6px;
+ padding: 0.5rem 0.75rem;
+}
+
+/* Enhanced Form Controls with Status */
+.form-control.has-warning {
+ border-color: #ffc107;
+ box-shadow: 0 0 0 0.2rem rgba(255, 193, 7, 0.25);
+}
+
+.form-control.has-success {
+ border-color: #28a745;
+ box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25);
+}
+
+.form-control.has-error {
+ border-color: #dc3545;
+ box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25);
+}
+
+/* Enhanced Select Options */
+.form-select option {
+ padding: 0.5rem;
+}
+
+/* Status Indicators */
+.status-indicator {
+ display: inline-flex;
+ align-items: center;
+ padding: 0.25rem 0.5rem;
+ border-radius: 4px;
+ font-size: 0.75rem;
+ font-weight: 500;
+ margin-left: 0.5rem;
+}
+
+.status-indicator.recommended {
+ background: #d4edda;
+ color: #155724;
+ border: 1px solid #c3e6cb;
+}
+
+.status-indicator.warning {
+ background: #fff3cd;
+ color: #856404;
+ border: 1px solid #ffeaa7;
+}
+
+.status-indicator.critical {
+ background: #f8d7da;
+ color: #721c24;
+ border: 1px solid #f5c6cb;
+}
+
+/* Grid Enhancements */
+::ng-deep .gui-grid {
+ .gui-grid-header {
+ background: #f8f9fa;
+ font-weight: 600;
+ }
+
+ .gui-grid-cell {
+ padding: 0.75rem 0.5rem;
+ }
}
\ 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..53a387a 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",
@@ -38,6 +37,17 @@ export class SettingsComponent implements OnInit {
public filters: any = {};
public firms: any = {};
public firmtodownload: any = {};
+ public activeTab: string = 'firmware';
+
+ // Search functionality properties
+ public firmwareSearch: string = '';
+ public showFirmwareDropdown: boolean = false;
+ public filteredFirmwares: any[] = [];
+
+ public timezoneSearch: string = '';
+ public showTimezoneDropdown: boolean = false;
+ public filteredTimezones: any[] = [];
+
constructor(
private data_provider: dataProvider,
private router: Router,
@@ -137,6 +147,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 +166,7 @@ export class SettingsComponent implements OnInit {
"danger"
);
}
+ }
});
}
else
@@ -160,6 +179,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 +204,7 @@ export class SettingsComponent implements OnInit {
}
_self.ConfirmModalVisible = !_self.ConfirmModalVisible;
_self.loading = false;
+ }
});
}
@@ -204,14 +232,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,10 +292,24 @@ 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"] = "";
_self.timezones = _self.TimeZones.timezones;
+ _self.filteredTimezones = _self.TimeZones.timezones;
+ // Set initial timezone search display
+ const currentTz = _self.timezones.find((tz: any) => tz.utc[0] === _self.sysconfigs['timezone']['value']);
+ if (currentTz) {
+ _self.timezoneSearch = currentTz.text;
+ }
_self.sysconfigs["force_syslog"]["value"] = /true/i.test(
_self.sysconfigs["force_syslog"]["value"]
);
@@ -265,7 +327,33 @@ export class SettingsComponent implements OnInit {
_self.sysconfigs["otp_force"]["value"]
);
}
+ if(_self.ispro && "proxy_auto_login" in _self.sysconfigs){
+ _self.sysconfigs["proxy_auto_login"]["value"] = /true/i.test(
+ _self.sysconfigs["proxy_auto_login"]["value"]
+ );
+ }
+ else if(_self.ispro){
+ _self.sysconfigs["proxy_auto_login"] = {
+ "value": true
+ }
+ }
+ //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;
+ }
});
}
@@ -274,7 +362,50 @@ export class SettingsComponent implements OnInit {
this.data_provider.get_downloadable_firms().then((res) => {
let index = 1;
_self.firms = res.versions;
+ _self.filteredFirmwares = res.versions;
_self.loading = false;
});
}
+
+ // Firmware search methods
+ filterFirmwares(event: any): void {
+ const searchTerm = event.target.value.toLowerCase();
+ this.firmwareSearch = searchTerm;
+ this.filteredFirmwares = this.firms.filter((firm: string) =>
+ firm.toLowerCase().includes(searchTerm)
+ );
+ }
+
+ selectFirmware(firmware: string): void {
+ this.firmtodownload = firmware;
+ this.firmwareSearch = firmware;
+ this.showFirmwareDropdown = false;
+ }
+
+ hideFirmwareDropdown(): void {
+ setTimeout(() => {
+ this.showFirmwareDropdown = false;
+ }, 200);
+ }
+
+ // Timezone search methods
+ filterTimezones(event: any): void {
+ const searchTerm = event.target.value.toLowerCase();
+ this.timezoneSearch = searchTerm;
+ this.filteredTimezones = this.timezones.filter((tz: any) =>
+ tz.text.toLowerCase().includes(searchTerm)
+ );
+ }
+
+ selectTimezone(timezone: any): void {
+ this.sysconfigs['timezone']['value'] = timezone.utc[0];
+ this.timezoneSearch = timezone.text;
+ this.showTimezoneDropdown = false;
+ }
+
+ hideTimezoneDropdown(): void {
+ setTimeout(() => {
+ this.showTimezoneDropdown = false;
+ }, 200);
+ }
}
diff --git a/src/app/views/settings/settings.module.ts b/src/app/views/settings/settings.module.ts
index 116d06d..b482be7 100644
--- a/src/app/views/settings/settings.module.ts
+++ b/src/app/views/settings/settings.module.ts
@@ -10,14 +10,14 @@ import {
SpinnerModule,
ToastModule,
ModalModule,
+ BadgeModule,
+ TooltipModule,
} from "@coreui/angular";
import { SettingsRoutingModule } from "./settings-routing.module";
import { SettingsComponent } from "./settings.component";
import { GuiGridModule } from "@generic-ui/ngx-grid";
import { FormsModule } from "@angular/forms";
-import { MatSelectModule } from "@angular/material/select";
-import { NgxMatSelectSearchModule } from "ngx-mat-select-search";
@NgModule({
imports: [
@@ -30,11 +30,11 @@ import { NgxMatSelectSearchModule } from "ngx-mat-select-search";
ButtonModule,
ButtonGroupModule,
GuiGridModule,
- MatSelectModule,
- NgxMatSelectSearchModule,
SpinnerModule,
ToastModule,
ModalModule,
+ BadgeModule,
+ TooltipModule,
],
declarations: [SettingsComponent],
})
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 :
{
+ _self.data_provider.get_snippets("", "", "", 0, 1000,false).then((res) => {
_self.source = res.map((x: any) => {
x.created = [
x.created.split("T")[0],
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.
+
- Editing User {{SelectedUser['name']}}
- Adding new User
+ Edit: {{SelectedUser['username']}}
+ Add User
-
-
-
-
-
-
-
- First Name
-
-
- Last Name
-
-
-
-
-
-
-
-
-
-
-
-
-
- MikroWizard permisssions :
-
-
-
-
-
-
-
- Read
- Write
- Full
- None
-
-
-
+
+
+